diff --git a/EATSSU/App/Resources/en.lproj/Localizable.strings b/EATSSU/App/Resources/en.lproj/Localizable.strings
index 6f0aefc5..10db5443 100644
--- a/EATSSU/App/Resources/en.lproj/Localizable.strings
+++ b/EATSSU/App/Resources/en.lproj/Localizable.strings
@@ -192,6 +192,10 @@
"map.pub" = "Pub";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "No Department Info";
+
+"map.kakaoMap" = "KakaoMap";
+
+"map.naverMap" = "Naver Map";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "Location Permission Required";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
diff --git a/EATSSU/App/Resources/ja.lproj/Localizable.strings b/EATSSU/App/Resources/ja.lproj/Localizable.strings
index 373573ec..f09b2801 100644
--- a/EATSSU/App/Resources/ja.lproj/Localizable.strings
+++ b/EATSSU/App/Resources/ja.lproj/Localizable.strings
@@ -193,6 +193,10 @@
"map.pub" = "居酒屋";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "学科情報なし";
+
+"map.kakaoMap" = "カカオマップ";
+
+"map.naverMap" = "NAVERマップ";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "位置情報権限が必要です";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
diff --git a/EATSSU/App/Resources/ko.lproj/Localizable.strings b/EATSSU/App/Resources/ko.lproj/Localizable.strings
index d95c634a..574bcced 100644
--- a/EATSSU/App/Resources/ko.lproj/Localizable.strings
+++ b/EATSSU/App/Resources/ko.lproj/Localizable.strings
@@ -193,6 +193,10 @@
"map.pub" = "주점";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "학과 정보 없음";
+
+"map.kakaoMap" = "카카오맵";
+
+"map.naverMap" = "네이버지도";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "위치 권한 필요";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
diff --git a/EATSSU/App/Resources/vi.lproj/Localizable.strings b/EATSSU/App/Resources/vi.lproj/Localizable.strings
index 5edb302b..db539310 100644
--- a/EATSSU/App/Resources/vi.lproj/Localizable.strings
+++ b/EATSSU/App/Resources/vi.lproj/Localizable.strings
@@ -193,6 +193,10 @@
"map.pub" = "Quán nhậu";
/// PartnershipDetailSheetVC - "학과 정보 없음"
"map.noDepartmentInfo" = "Không có thông tin ngành";
+
+"map.kakaoMap" = "KakaoMap";
+
+"map.naverMap" = "Naver Map";
/// MainMapVC+Location - "위치 권한 필요"
"map.needLocationAuth" = "Cần quyền truy cập vị trí";
/// MainMapVC+Location - "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요."
diff --git a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift
index 0a74e0de..abd7f6df 100644
--- a/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift
+++ b/EATSSU/App/Sources/Data/Network/DTO/Partnership/PartnershipDTO.swift
@@ -12,6 +12,8 @@ struct PartnershipDTO: Codable {
let longitude: Double
let latitude: Double
let restaurantType: String
+ let naverMapUrl: String?
+ let kakaoMapUrl: String?
let partnershipInfos: [PartnershipInfoDTO]
}
diff --git a/EATSSU/App/Sources/Data/Network/Service/KakaoLocalService.swift b/EATSSU/App/Sources/Data/Network/Service/KakaoLocalService.swift
new file mode 100644
index 00000000..97be62d9
--- /dev/null
+++ b/EATSSU/App/Sources/Data/Network/Service/KakaoLocalService.swift
@@ -0,0 +1,86 @@
+//
+// KakaoLocalService.swift
+// EATSSU
+//
+// Created by 황상환 on 7/18/26.
+//
+
+import Foundation
+
+/// 카카오 로컬 API - 키워드 장소 검색 (카카오맵 place id 조회용)
+final class KakaoLocalService {
+
+ // MARK: - Singleton
+
+ static let shared = KakaoLocalService()
+
+ private init() {}
+
+ // MARK: - Model
+
+ struct Place: Decodable {
+ let id: String
+ let placeName: String
+ let placeURL: String
+
+ enum CodingKeys: String, CodingKey {
+ case id
+ case placeName = "place_name"
+ case placeURL = "place_url"
+ }
+ }
+
+ private struct SearchResponse: Decodable {
+ let documents: [Place]
+ }
+
+ // MARK: - Properties
+
+ private var restAPIKey: String? {
+ guard let key = Bundle.main.object(forInfoDictionaryKey: "KAKAO REST API KEY") as? String,
+ !key.isEmpty else { return nil }
+ return key
+ }
+
+ // MARK: - Request
+
+ /// 가게명 + 좌표 기준으로 가장 가까운 장소 1건 검색 (키 미설정/실패 시 nil 반환)
+ func searchNearestPlace(
+ keyword: String,
+ latitude: Double,
+ longitude: Double,
+ completion: @escaping (Place?) -> Void
+ ) {
+ guard let key = restAPIKey else {
+ completion(nil)
+ return
+ }
+
+ var components = URLComponents(string: "https://dapi.kakao.com/v2/local/search/keyword.json")
+ components?.queryItems = [
+ URLQueryItem(name: "query", value: keyword),
+ URLQueryItem(name: "x", value: "\(longitude)"),
+ URLQueryItem(name: "y", value: "\(latitude)"),
+ URLQueryItem(name: "radius", value: "300"),
+ URLQueryItem(name: "sort", value: "distance"),
+ URLQueryItem(name: "size", value: "1")
+ ]
+
+ guard let url = components?.url else {
+ completion(nil)
+ return
+ }
+
+ var request = URLRequest(url: url)
+ request.setValue("KakaoAK \(key)", forHTTPHeaderField: "Authorization")
+
+ URLSession.shared.dataTask(with: request) { data, _, _ in
+ let place = data.flatMap {
+ try? JSONDecoder().decode(SearchResponse.self, from: $0).documents.first
+ }
+ DispatchQueue.main.async {
+ completion(place)
+ }
+ }.resume()
+ }
+}
diff --git a/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift
new file mode 100644
index 00000000..49de0d0e
--- /dev/null
+++ b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift
@@ -0,0 +1,103 @@
+//
+// PartnershipMockData.swift
+// EATSSU
+//
+// Created by 황상환 on 7/18/26.
+//
+
+#if DEBUG
+import Foundation
+
+/// 서버에 제휴 데이터가 없을 때 실기기 테스트용 Mock 데이터 (DEBUG 빌드 전용)
+enum PartnershipMockData {
+
+ /// 숭실대 주변 가게 좌표 기반 샘플
+ static let samples: [PartnershipDTO] = [
+ PartnershipDTO(
+ storeName: "맘스터치 숭실대점",
+ longitude: 126.9550,
+ latitude: 37.4959,
+ restaurantType: "RESTAURANT",
+ naverMapUrl: nil,
+ kakaoMapUrl: nil,
+ partnershipInfos: [
+ PartnershipInfoDTO(
+ id: -1,
+ collegeName: "IT대학",
+ departmentName: nil,
+ likeCount: 0,
+ isLiked: false,
+ description: "학생증 인증하면 음료수 1개 증정",
+ startDate: "2025.09.03",
+ endDate: "2025.12.18",
+ periodType: .normal
+ ),
+ PartnershipInfoDTO(
+ id: -2,
+ collegeName: nil,
+ departmentName: "컴퓨터학부",
+ likeCount: 0,
+ isLiked: false,
+ description: "학생증 인증하고 카카오페이 결제 시 10% 할인",
+ startDate: "2025.09.01",
+ endDate: "2025.12.31",
+ periodType: .normal
+ )
+ ]
+ ),
+ PartnershipDTO(
+ storeName: "스타벅스 숭실대입구역점",
+ longitude: 126.9538,
+ latitude: 37.4964,
+ restaurantType: "CAFE",
+ naverMapUrl: nil,
+ kakaoMapUrl: nil,
+ partnershipInfos: [
+ PartnershipInfoDTO(
+ id: -3,
+ collegeName: "경영대학",
+ departmentName: nil,
+ likeCount: 0,
+ isLiked: false,
+ description: "아메리카노 사이즈업 무료",
+ startDate: "2025.09.03",
+ endDate: "2025.12.18",
+ periodType: .normal
+ )
+ ]
+ ),
+ PartnershipDTO(
+ storeName: "역전할머니맥주 숭실대점",
+ longitude: 126.9532,
+ latitude: 37.4949,
+ restaurantType: "PUB",
+ naverMapUrl: nil,
+ kakaoMapUrl: nil,
+ partnershipInfos: [
+ PartnershipInfoDTO(
+ id: -4,
+ collegeName: "벤처중소기업센터",
+ departmentName: nil,
+ likeCount: 0,
+ isLiked: false,
+ description: "4인 이상 방문 시 안주 1개 서비스",
+ startDate: "2025.09.03",
+ endDate: "2025.12.18",
+ periodType: .normal
+ ),
+ PartnershipInfoDTO(
+ id: -5,
+ collegeName: "총학생회",
+ departmentName: nil,
+ likeCount: 0,
+ isLiked: false,
+ description: "축제 기간 병맥주 1+1",
+ startDate: "2025.10.06",
+ endDate: "2025.10.08",
+ periodType: .festival
+ )
+ ]
+ )
+ ]
+}
+#endif
diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift
index b2bf9b31..23543c18 100644
--- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift
+++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift
@@ -155,27 +155,22 @@ extension MainMapViewController {
partnerId: partnership.partnershipInfos.first?.id ?? -1
)
- let detailVC = PartnershipDetailSheetViewController(
- storeName: partnership.storeName,
- restaurantType: partnership.restaurantType,
- partnershipInfos: partnership.partnershipInfos
- )
+ let detailVC = PartnershipDetailSheetViewController(partnership: partnership)
// 뷰가 로드된 후 높이 계산
detailVC.loadViewIfNeeded()
if let sheet = detailVC.sheetPresentationController {
- let contentHeight = detailVC.calculatePreferredHeight()
-
// iOS 16.0 이상에서만 custom detent 사용
+ // 표시 후 safe area 확정 시 invalidateDetents()로 클로저가 재실행되어 높이가 보정됨
if #available(iOS 16.0, *) {
- let customDetent = UISheetPresentationController.Detent.custom { _ in
- return contentHeight
+ let customDetent = UISheetPresentationController.Detent.custom { [weak detailVC] _ in
+ detailVC?.calculatePreferredHeight()
}
- sheet.detents = [customDetent]
+ sheet.detents = [customDetent, .large()]
} else {
// iOS 16.0 미만에서는 medium detent 사용
- sheet.detents = [.medium()]
+ sheet.detents = [.medium(), .large()]
}
sheet.prefersGrabberVisible = true
diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift
index 0f0c0fcb..d877f66b 100644
--- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift
+++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift
@@ -29,6 +29,14 @@ extension MainMapViewController {
self.cachedAllPartnerships = []
self.displayMarkers([])
}
+
+ #if DEBUG
+ // 서버에 제휴 데이터가 없는 동안 Mock으로 대체 (DEBUG 빌드 전용)
+ if self.cachedAllPartnerships.isEmpty {
+ self.cachedAllPartnerships = PartnershipMockData.samples
+ self.applyCachedMarkers()
+ }
+ #endif
}
}
@@ -57,6 +65,8 @@ extension MainMapViewController {
longitude: partnership.longitude,
latitude: partnership.latitude,
restaurantType: partnership.restaurantType,
+ naverMapUrl: partnership.naverMapUrl,
+ kakaoMapUrl: partnership.kakaoMapUrl,
partnershipInfos: matchingInfos
)
}
diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift
index d147da6d..066d5a0a 100644
--- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift
+++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController.swift
@@ -133,7 +133,7 @@ final class MainMapViewController: BaseViewController {
collegeId: currentCollegeId,
majorId: currentDepartmentId
)
- applyCachedMarkers()
+ refreshOrApplyCachedMarkers()
}
@objc private func didTapWhole() {
@@ -146,7 +146,16 @@ final class MainMapViewController: BaseViewController {
collegeId: currentCollegeId,
majorId: currentDepartmentId
)
- applyCachedMarkers()
+ refreshOrApplyCachedMarkers()
+ }
+
+ /// 캐시가 비어있으면(내 제휴 모드로 시작해 전체 데이터를 아직 안 받은 경우) 서버에서 받아오고, 있으면 캐시로 필터링
+ private func refreshOrApplyCachedMarkers() {
+ if cachedAllPartnerships.isEmpty {
+ refreshAllPartnerships()
+ } else {
+ applyCachedMarkers()
+ }
}
@objc private func didTapMyOnly() {
diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift
index 1ecf307c..a19be2f8 100644
--- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift
+++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift
@@ -15,9 +15,7 @@ final class PartnershipDetailSheetViewController: BaseViewController {
// MARK: - Properties
- private let storeName: String
- private let restaurantType: String
- private let partnershipInfos: [PartnershipInfoDTO]
+ private let partnership: PartnershipDTO
// MARK: - UI Components
@@ -26,13 +24,15 @@ final class PartnershipDetailSheetViewController: BaseViewController {
private let typeIconImageView = UIImageView()
private let typeTextLabel = UILabel()
private let infoListStackView = UIStackView()
+ private let mapButtonBarView = UIView()
+ private let kakaoMapButton = UIButton()
+ private let naverMapButton = UIButton()
+ private let mapButtonDivider = UIView()
// MARK: - Init
- init(storeName: String, restaurantType: String, partnershipInfos: [PartnershipInfoDTO]) {
- self.storeName = storeName
- self.restaurantType = restaurantType
- self.partnershipInfos = partnershipInfos
+ init(partnership: PartnershipDTO) {
+ self.partnership = partnership
super.init(nibName: nil, bundle: nil)
modalPresentationStyle = .pageSheet
@@ -47,13 +47,13 @@ final class PartnershipDetailSheetViewController: BaseViewController {
override func configureUI() {
view.backgroundColor = .white
- storeNameLabel.font = EATSSUDesignFontFamily.Pretendard.bold.font(size: 16)
+ storeNameLabel.font = .header2
storeNameLabel.textColor = .label
typeIconImageView.contentMode = .scaleAspectFit
typeIconImageView.snp.makeConstraints { $0.width.height.equalTo(18) }
- typeTextLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 10)
+ typeTextLabel.font = .body2
typeTextLabel.textColor = .gray
typeStackView.axis = .horizontal
@@ -69,7 +69,25 @@ final class PartnershipDetailSheetViewController: BaseViewController {
infoListStackView.isLayoutMarginsRelativeArrangement = true
infoListStackView.layoutMargins = .init(top: 10, left: 0, bottom: 10, right: 0)
- [storeNameLabel, typeStackView, infoListStackView].forEach {
+ kakaoMapButton.configuration = makeMapButtonConfiguration(
+ image: EATSSUDesignAsset.Images.kakaoMapLogo.image,
+ title: TextLiteral.Map.kakaoMap
+ )
+ kakaoMapButton.addTarget(self, action: #selector(kakaoMapButtonTapped), for: .touchUpInside)
+
+ naverMapButton.configuration = makeMapButtonConfiguration(
+ image: EATSSUDesignAsset.Images.naverMapLogo.image,
+ title: TextLiteral.Map.naverMap
+ )
+ naverMapButton.addTarget(self, action: #selector(naverMapButtonTapped), for: .touchUpInside)
+
+ mapButtonDivider.backgroundColor = EATSSUDesignColors.Color.gray300
+
+ [kakaoMapButton, naverMapButton, mapButtonDivider].forEach {
+ mapButtonBarView.addSubview($0)
+ }
+
+ [storeNameLabel, typeStackView, infoListStackView, mapButtonBarView].forEach {
view.addSubview($0)
}
}
@@ -89,6 +107,30 @@ final class PartnershipDetailSheetViewController: BaseViewController {
$0.top.equalTo(typeStackView.snp.bottom).offset(10)
$0.leading.trailing.equalToSuperview().inset(24)
}
+
+ // large로 확장 시에도 버튼 바가 시트 하단에 붙어있도록 bottom 고정
+ mapButtonBarView.snp.makeConstraints {
+ $0.top.greaterThanOrEqualTo(infoListStackView.snp.bottom).offset(6)
+ $0.leading.trailing.equalToSuperview()
+ $0.height.equalTo(56)
+ $0.bottom.equalTo(view.safeAreaLayoutGuide).inset(10)
+ }
+
+ kakaoMapButton.snp.makeConstraints {
+ $0.leading.top.bottom.equalToSuperview()
+ $0.trailing.equalTo(mapButtonBarView.snp.centerX)
+ }
+
+ naverMapButton.snp.makeConstraints {
+ $0.trailing.top.bottom.equalToSuperview()
+ $0.leading.equalTo(mapButtonBarView.snp.centerX)
+ }
+
+ mapButtonDivider.snp.makeConstraints {
+ $0.center.equalToSuperview()
+ $0.width.equalTo(1)
+ $0.height.equalTo(16)
+ }
}
// MARK: - Data Config
@@ -100,15 +142,24 @@ final class PartnershipDetailSheetViewController: BaseViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
-
+
logScreenView(screenID: FirebaseScreenID.Map.map2)
}
+ /// 시트가 화면에 붙어 실제 safe area가 확정되면 detent 높이 재계산
+ override func viewSafeAreaInsetsDidChange() {
+ super.viewSafeAreaInsetsDidChange()
+
+ if #available(iOS 16.0, *) {
+ sheetPresentationController?.invalidateDetents()
+ }
+ }
+
/// 매장 정보와 제휴 내용을 화면에 반영
private func configureData() {
- storeNameLabel.text = storeName
+ storeNameLabel.text = partnership.storeName
- switch restaurantType {
+ switch partnership.restaurantType {
case "RESTAURANT":
typeIconImageView.image = EATSSUDesignAsset.Images.restaurantPin.image
typeTextLabel.text = TextLiteral.Map.restaurant
@@ -120,11 +171,11 @@ final class PartnershipDetailSheetViewController: BaseViewController {
typeTextLabel.text = TextLiteral.Map.pub
default:
typeIconImageView.image = EATSSUDesignAsset.Images.restaurantPin.image
- typeTextLabel.text = restaurantType
+ typeTextLabel.text = partnership.restaurantType
}
- for (index, info) in partnershipInfos.enumerated() {
- let isLast = index == partnershipInfos.count - 1
+ for (index, info) in partnership.partnershipInfos.enumerated() {
+ let isLast = index == partnership.partnershipInfos.count - 1
let card = makeInfoCard(info: info, isLast: isLast)
infoListStackView.addArrangedSubview(card)
}
@@ -133,12 +184,26 @@ final class PartnershipDetailSheetViewController: BaseViewController {
// MARK: - UI Helpers
/// 제휴 content 갯수에 따라 유동적으로 Height 측정
+ /// 버튼 바는 시트 하단에 고정이므로 리스트 높이 기준으로 계산
func calculatePreferredHeight() -> CGFloat {
view.layoutIfNeeded()
- let contentHeight = infoListStackView.frame.maxY
- let bottomPadding: CGFloat = view.safeAreaInsets.bottom + 20
-
- return contentHeight + bottomPadding
+ let buttonBarArea: CGFloat = 6 + 56 + 10
+ let bottomPadding: CGFloat = view.safeAreaInsets.bottom
+
+ return infoListStackView.frame.maxY + buttonBarArea + bottomPadding
+ }
+
+ /// 지도 앱 이동 버튼 공통 Configuration 생성
+ private func makeMapButtonConfiguration(image: UIImage, title: String) -> UIButton.Configuration {
+ var config = UIButton.Configuration.plain()
+ config.image = image
+ config.imagePadding = 6
+ config.baseForegroundColor = .label
+ config.attributedTitle = AttributedString(
+ title,
+ attributes: AttributeContainer([.font: UIFont.body2])
+ )
+ return config
}
/// 제휴 정보 카드 뷰 생성
@@ -155,21 +220,21 @@ final class PartnershipDetailSheetViewController: BaseViewController {
let dateRange = (fullText as NSString).range(of: "\(start) ~ \(end)")
attrText.addAttributes([
- .font: EATSSUDesignFontFamily.Pretendard.medium.font(size: 14),
+ .font: UIFont.body2,
.foregroundColor: UIColor.label
], range: collegeRange)
attrText.addAttributes([
- .font: EATSSUDesignFontFamily.Pretendard.regular.font(size: 10),
+ .font: UIFont.caption2,
.foregroundColor: EATSSUDesignColors.Color.gray700,
- .baselineOffset: +2
+ .baselineOffset: +1
], range: dateRange)
let titleDateLabel = UILabel()
titleDateLabel.attributedText = attrText
let descriptionLabel = UILabel()
- descriptionLabel.font = EATSSUDesignFontFamily.Pretendard.regular.font(size: 12)
+ descriptionLabel.font = .body3
descriptionLabel.textColor = EATSSUDesignColors.Color.gray700
descriptionLabel.numberOfLines = 0
descriptionLabel.text = info.description
@@ -179,7 +244,7 @@ final class PartnershipDetailSheetViewController: BaseViewController {
contentStack.spacing = 4
let separator = UIView()
- separator.backgroundColor = EATSSUDesignColors.Color.gray200
+ separator.backgroundColor = EATSSUDesignColors.Color.gray300
separator.isHidden = isLast
separator.snp.makeConstraints {
$0.height.equalTo(1)
@@ -198,4 +263,89 @@ final class PartnershipDetailSheetViewController: BaseViewController {
return paddedContainer
}
+ // MARK: - Actions
+
+ /// 카카오맵 장소 상세 페이지로 이동
+ /// 서버 제공 URL 우선 → 없으면 로컬 API 매칭 → 실패 시 좌표 핀 폴백
+ @objc private func kakaoMapButtonTapped() {
+ // 서버가 준 place URL(place.map.kakao.com/{id})에서 id를 추출해 앱 스킴으로 연결
+ if let urlString = partnership.kakaoMapUrl,
+ let webURL = URL(string: urlString),
+ Int(webURL.lastPathComponent) != nil {
+ let appURL = URL(string: "kakaomap://place?id=\(webURL.lastPathComponent)")
+ openMapApp(appURL: appURL, fallbackURL: webURL)
+ return
+ }
+
+ KakaoLocalService.shared.searchNearestPlace(
+ keyword: partnership.storeName,
+ latitude: partnership.latitude,
+ longitude: partnership.longitude
+ ) { [weak self] place in
+ guard let self else { return }
+
+ if let place {
+ let appURL = URL(string: "kakaomap://place?id=\(place.id)")
+ self.openMapApp(appURL: appURL, fallbackURL: URL(string: place.placeURL))
+ } else {
+ self.openKakaoMapByCoordinate()
+ }
+ }
+ }
+
+ /// 카카오맵 좌표 핀으로 이동 (미설치 시 카카오맵 웹)
+ private func openKakaoMapByCoordinate() {
+ let appURL = URL(string: "kakaomap://look?p=\(partnership.latitude),\(partnership.longitude)")
+ let encodedName = percentEncodedForMapURL(partnership.storeName)
+ let webURL = URL(
+ string: "https://map.kakao.com/link/map/\(encodedName),\(partnership.latitude),\(partnership.longitude)"
+ )
+ openMapApp(appURL: appURL, fallbackURL: webURL)
+ }
+
+ /// 지도 웹 URL 경로에 안전하게 넣을 수 있도록 경로/구분자 문자까지 인코딩
+ private func percentEncodedForMapURL(_ text: String) -> String {
+ var allowed = CharacterSet.urlPathAllowed
+ allowed.remove(charactersIn: "/?&,")
+ return text.addingPercentEncoding(withAllowedCharacters: allowed) ?? ""
+ }
+
+ /// 네이버지도 플레이스로 이동
+ /// 서버 제공 URL 우선(유니버설 링크로 앱/웹 자동 분기) → 없으면 정제된 상호명 검색 폴백
+ @objc private func naverMapButtonTapped() {
+ if let urlString = partnership.naverMapUrl,
+ let url = URL(string: urlString) {
+ UIApplication.shared.open(url)
+ return
+ }
+
+ KakaoLocalService.shared.searchNearestPlace(
+ keyword: partnership.storeName,
+ latitude: partnership.latitude,
+ longitude: partnership.longitude
+ ) { [weak self] place in
+ guard let self else { return }
+ self.openNaverMapSearch(query: place?.placeName ?? self.partnership.storeName)
+ }
+ }
+
+ private func openNaverMapSearch(query: String) {
+ var components = URLComponents(string: "nmap://search")
+ components?.queryItems = [
+ URLQueryItem(name: "query", value: query),
+ URLQueryItem(name: "appname", value: Bundle.main.bundleIdentifier ?? "")
+ ]
+ let encodedQuery = percentEncodedForMapURL(query)
+ let webURL = URL(string: "https://map.naver.com/p/search/\(encodedQuery)")
+ openMapApp(appURL: components?.url, fallbackURL: webURL)
+ }
+
+ private func openMapApp(appURL: URL?, fallbackURL: URL?) {
+ if let appURL, UIApplication.shared.canOpenURL(appURL) {
+ UIApplication.shared.open(appURL)
+ } else if let fallbackURL {
+ UIApplication.shared.open(fallbackURL)
+ }
+ }
+
}
diff --git a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift
index 3e95247b..c3a69de5 100644
--- a/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift
+++ b/EATSSU/App/Sources/Utility/Literal/TextLiteral.swift
@@ -493,6 +493,16 @@ enum TextLiteral {
Localization.localized("map.noDepartmentInfo", fallback: "학과 정보 없음")
}
+ /// PartnershipDetailSheetVC - "카카오맵"
+ static var kakaoMap: String {
+ Localization.localized("map.kakaoMap", fallback: "카카오맵")
+ }
+
+ /// PartnershipDetailSheetVC - "네이버지도"
+ static var naverMap: String {
+ Localization.localized("map.naverMap", fallback: "네이버지도")
+ }
+
/// MainMapVC+Location - "위치 권한 필요"
static var needLocationAuth: String {
Localization.localized("map.needLocationAuth", fallback: "위치 권한 필요")
diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift
index 7f55a697..ba37b330 100644
--- a/EATSSU/Project.swift
+++ b/EATSSU/Project.swift
@@ -6,6 +6,7 @@ let appInfoPlist: InfoPlist = .extendingDefault(with: [
"UILaunchStoryboardName": "LaunchScreen",
"BASE_URL": "https://$(BASE_URL)",
"KAKAO API KEY": "$(KAKAO_API_KEY)",
+ "KAKAO REST API KEY": "$(KAKAO_REST_API_KEY)",
"AppGroupID": "$(APP_GROUP_ID)",
"FirebaseAutomaticScreenReportingEnabled": false,
"UIBackgroundModes": [
@@ -20,8 +21,10 @@ let appInfoPlist: InfoPlist = .extendingDefault(with: [
"LSApplicationQueriesSchemes": [
"kakaokompassauth",
"kakaolink",
+ "kakaomap",
"kakaoplus",
"kakaotalk",
+ "nmap",
],
"NSLocationWhenInUseUsageDescription": "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요.",
"UIApplicationSceneManifest": [
diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/Contents.json
new file mode 100644
index 00000000..f51699e6
--- /dev/null
+++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "KakaoMapLogo.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/KakaoMapLogo.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/KakaoMapLogo.svg
new file mode 100644
index 00000000..2e71c881
--- /dev/null
+++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/KakaoMapLogo.svg
@@ -0,0 +1,9 @@
+
diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/Contents.json b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/Contents.json
new file mode 100644
index 00000000..1a28006b
--- /dev/null
+++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "NaverMapLogo.svg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/NaverMapLogo.svg b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/NaverMapLogo.svg
new file mode 100644
index 00000000..e3747a9c
--- /dev/null
+++ b/EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/NaverMapLogo.svg
@@ -0,0 +1,9 @@
+