Skip to content

[Work 28] 신규 약속 생성 시 지도 화면을 구성했습니다. - #10

Open
snughnu wants to merge 18 commits into
developfrom
WORK-28
Open

[Work 28] 신규 약속 생성 시 지도 화면을 구성했습니다.#10
snughnu wants to merge 18 commits into
developfrom
WORK-28

Conversation

@snughnu

@snughnu snughnu commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

JIRA

📝 작업 내용

📌 요약

  • 약속 생성 화면에서 지도 버튼을 눌렀을 때 뜨는 "장소 선택" 화면을 구성했습니다.
  • Naver Maps SDK를 도입해 실제 지도를 화면에 표시하고, 지도를 움직여 중앙 핀 위치의 장소(또는 주소)를 확인 후 약속 장소로 선택할 수 있습니다.

🔍 상세

1. 진입 흐름 (AppointmentCreationViewController)

  • 약속 생성 화면의 지도 버튼(onMapButtonTap)에서 PlaceSelectionViewControllerUINavigationController로 감싸 풀사이즈 시트로 present합니다.
  • isModalInPresentation = true로 스와이프 닫기를 막고, 화면 안의 X 버튼으로만 닫히도록 했습니다.
  • 확인 버튼을 누르면 선택된 Place가 콜백으로 전달되어
    AppointmentCreationViewModel.setPlace(_:)로 이어집니다.

2. 장소 선택 화면 (PlaceSelectionViewController / PlaceSelectionViewModel)

  • 상단 헤더(제목/부제/닫기 버튼) + 지도 + 하단 카드(장소명/주소/거리 + "다른 장소 검색"/"확인" 버튼) 구조입니다.
  • 지도와 하단 카드는 프레임상 맞닿아 있지만(naverMapView.bottomAnchor == bottomCard.topAnchor),
    뷰 계층상 카드가 나중에 추가되어 카드의 그림자가 지도 위로 자연스럽게 번지도록 했습니다.
  • 지도 중앙에는 화면 좌표 기준으로 고정된 커스텀 핀(centerPinImageView)을 얹었고,
    카메라 이동이 멈추면(NMFMapViewCameraDelegate.mapViewCameraIdle)
    핀의 실제 화면 위치를 projection.latlng(from:)으로 역산해 좌표를 계산합니다.
  • PlaceSelectionViewModel은 중앙 좌표 변경 스트림에 디바운스(300ms)와
    최소 이동 거리 필터(20m 미만은 재조회 안 함)를 걸어 불필요한 조회를 줄였습니다.
    (수치는 임의로 설정했습니다.)
  • 좌표 -> 근처 장소 조회는 "POI 우선, 없으면 역지오코딩 주소로 fallback" 구조입니다.
  • 로딩 중에는 "장소를 찾는 중..." 문구로 전환되고,
    근처 장소가 없을 때는 역지오코딩 결과(도로명 요약 + 상세주소)로 대체됩니다.
    조회 자체가 실패하면 "장소를 불러오지 못했습니다"로 구분해서 표시합니다.
  • 확인 버튼은 로딩 중이거나 표시할 장소가 없을 때 비활성화됩니다.

3. Naver Maps SDK 연동

  • SPM으로 NMapsMap 패키지를 추가하고, AppDelegate에서
    NMFAuthManager.shared().ncpKeyId로 인증을 초기화합니다.
  • Client ID는 Info.plist$(NMF_CLIENT_ID) 변수로 참조하고,
    실제 값은 .gitignore 처리된 Config.xcconfig(로컬 전용)에 둡니다.
  • Client ID는 공개되어도 괜찮다고 하지만,
    공개 저장소이므로 혹시 모를 과금을 방지하고자 값 자체를 커밋하지 않는 방식을 선택했습니다.

4. 실제 위치 연동

  • CoreLocationRepository를 새로 만들어 MockLocationRepository 대신 사용했습니다.
    권한 상태에 따라 요청/즉시 실패를 분기하고, requestLocation()으로 1회성 좌표를 받아옵니다.
  • 실기기에서 위치 권한 요청부터 실제 좌표 반영까지 확인했습니다.

5. 신규 UseCase / Repository / Core

  • GetNearbyPlaceUseCase + NearbyPlaceRepository(POI 조회, 반경 정책 포함)
  • ReverseGeocodeUseCase + ReverseGeocodingRepository(주소 fallback)
  • Coordinate.distance(to:)(Haversine 공식으로 두 좌표 간 거리 계산, km 단위)
  • 위 모두 Mock 구현체(MockNearbyPlaceRepository, MockReverseGeocodingRepository)로 동작을 검증했습니다.

💬 리뷰 노트

  • 지도 SDK로 Naver Maps를 선택한 이유
    • 국내 지도 표시(도로/건물 등 지형지물) 정확도가 중요한데,
      MapKit보다 국내 서비스(Naver/Kakao)가 우수하다고 생각했습니다.
    • Naver Maps SDK는 iOS 네이티브 SDK가 활발히 관리되고 있습니다.
    • 팀에서 자동차 길찾기로 Naver Directions를 검토 중이라,
      지도와 길찾기 제공사를 맞춰두면 향후 좌표계 정합성 관리가 수월해지는 기타 이점도 있습니다.
  • POI 조회는 전부 Mock입니다
    • Naver 생태계(Cloud Platform + Developers)를 확인한 결과,
      좌표 기반으로 근처 POI(상호명)를 조회할 수 있는 API가 없는걸로 확인됩니다.
      Reverse Geocoding은 도로명주소까지만 제공합니다.
    • 실제 POI 조회가 가능한 API로 카카오 로컬 API(좌표+반경 기반 검색 지원),
      TMap 장소 통합 검색 API 등이 있는 것으로 보이나,
      정확한 파라미터/응답 품질/무료 이용량은 확인하지 못해 더 자세한 조사가 필요합니다.
  • "다른 장소 검색" 버튼(searchOtherPlaceButton)은 액션이 연결되어 있지 않습니다
  • 탭바(홈/약속/마이페이지)를 임시로 연동해 화면 전환 흐름을 눈으로 확인할 수 있게 했습니다
  • 로컬 빌드를 위해 Config.xcconfig 파일 추가가 필요합니다
    • WhereAreYou/WhereAreYou/Config.xcconfig 경로에 위치해야 하는 파일로,
      Naver Maps SDK Client ID가 담겨 있습니다.
  • 반경 값(현재 50m)과 재조회 임계값(20m)은 줌 레벨과 무관한 임시 고정값입니다
    • 실제로는 줌 레벨에 따라 화면에 보이는 실제 거리가 달라지므로, 이 값들이 사용자 경험에 안 맞을 수 있습니다.
      실제 API 연동 및 줌 레벨 연동은 이후 작업할 때 같이 고려되어야 할 것 같습니다.
  • 위치기반서비스사업자 신고 관련
    • 위치정보법상 신고 대상일 가능성이 있어 확인이 필요해보입니다.
      별도 조사 후 문서로 작성하겠습니다. 이번 작업 범위에서는 제외하였습니다.
  • UI/흐름 위주로 봐주시고, 레이아웃/구조/문구 등 자유롭게 피드백 부탁드립니다.

📸 영상 / 이미지

2026-07-28.02.50.30.mov
2026-07-28.02.52.42.mov

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI 코드 리뷰 - 아키텍처 관점

네이버 지도 SDK(NMapsMap)를 SPM으로 추가하고, 지도 기반 장소 선택 기능(PlaceSelectionViewController/ViewModel)을 구현한 PR이다. Domain 계층에 Repository 프로토콜과 UseCase를 정의하고, Data 계층에 Mock 구현체를 배치한 구조는 계층 분리 원칙에 부합한다. 다만 ViewController에서 UseCase와 Repository를 직접 생성·조합하는 패턴, ViewModel이 구체 UseCase 타입에 직접 의존하는 구조, CoreLocationRepository의 completion 콜백이 단일 호출 보장 없이 중복 실행될 수 있는 설계 문제가 확인된다.

Comment thread WhereAreYou/WhereAreYou/Core/SceneDelegate.swift
Comment thread WhereAreYou/WhereAreYou/Data/CoreLocationRepository.swift

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI 코드 리뷰 - 아키텍처 관점

네이버 지도 SDK(NMapsMap) SPM 의존성 추가 및 지도 기반 장소 선택 기능을 구현한 PR이다. Domain/Data/Presentation 계층 분리는 전반적으로 유지되고 있으나, ViewController에서 UseCase와 Repository 구체 타입을 직접 생성하는 조합(composition) 방식이 계층 전반에 걸쳐 반복되고 있다. PlaceSelectionViewModel이 구체 UseCase 타입에 직접 의존하고 있어 확장성과 테스트 가능성에 구조적 제약이 있다. CoreLocationRepository의 completion 콜백은 단일 호출 후 nil로 초기화되지 않아 재사용 시 잠재적 문제가 있으나, 이는 아키텍처보다는 구현 수준 문제이므로 별도 언급하지 않는다. SceneDelegate에서 탭바 구성 로직이 직접 구현된 점은 규모가 커질수록 관리 지점이 분산될 수 있다.

Comment thread WhereAreYou/WhereAreYou/Core/SceneDelegate.swift

@sangYuLv sangYuLv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작업 내용과 리뷰 노트도 모두 확인했습니다!
수고 많으셨습니다 🦦🦦

(p.s. PR 생성할 때 AI 리뷰 라벨 달려있으면 두 번 트리거 되어서 중복으로 리뷰가 달려요. 라벨 없이 PR 생성한 뒤에 리뷰 라벨 달면 한 번만 받을 수 있습니다!)

Comment thread WhereAreYou/WhereAreYou/Core/SceneDelegate.swift
@unknown default:
break
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 두 메소드와 시그니처 패턴을 통일해도 좋겠어요!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locationManagerDidChangeAuthorization 메서드의 시그니처를 말씀하신게 맞을까요?

그렇다면 세 메서드 다 CLLocationManagerDelegate가 요구하는 고정된 프로토콜 메서드 이름이라 변경은 어려울거 같습니다🥲


import UIKit
import Combine
import NMapsMap

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

외부 라이브러리 import를 view controller에 직접하지 않고, 하나의 파일(타입)에서 지도 뷰를 래핑하는 건 어떨까요?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지도에 해당하는 UIView를 분리하겠습니다!

/// 좌표 기준 근처 장소 조회를 위임하는 UseCase
private let getNearbyPlaceUseCase: GetNearbyPlaceUseCase
/// POI가 없을 때 좌표를 주소로 변환(fallback)하는 UseCase
private let reverseGeocodeUseCase: ReverseGeocodeUseCase

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좌표 -> 근처 장소 조회는 "POI 우선, 없으면 역지오코딩 주소로 fallback" 구조라고 하셨는데,
근처 장소 조회 use case가 이 로직을 가지면 어떨까요?
근처 장소 조회와 좌표를 주소로 변환하는 로직이 각자 use case를 가지지 않고, 하나의 use case에서 처리되는 방향을 생각해봤습니다.
repository는 지금처럼 나눠져도 좋을 것 같아요.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReverseGeocodeUseCase가 독립적으로 사용되는 경우가 있을거같아서 분리했었어요
그런데, 현재 구상된 앱 기능에서는 없는거같아 하나의 use case에서 처리되게끔 수정했습니다 !!

imageView.tintColor = .customRed
imageView.contentMode = .scaleAspectFit
return imageView
}()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 이미지는 어때요?
(배경도 투명하게 하려다가 GPT 사용 한도 넘어서 못했어요 😅)

Image

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

반영했습니다 괜찮나요!? (Size: 32*32)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants