From aa8103ecda2b2489cc819b4778952bd6e5889e7c Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:50:58 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[#432]=20=EC=B9=B4=EC=B9=B4=EC=98=A4?= =?UTF-8?q?=EB=A7=B5/=EB=84=A4=EC=9D=B4=EB=B2=84=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?=EB=A1=9C=EA=B3=A0=20=EC=97=90=EC=85=8B=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../KakaoMapLogo.imageset/Contents.json | 12 ++++++++++++ .../KakaoMapLogo.imageset/KakaoMapLogo.svg | 9 +++++++++ .../NaverMapLogo.imageset/Contents.json | 12 ++++++++++++ .../NaverMapLogo.imageset/NaverMapLogo.svg | 9 +++++++++ 4 files changed, 42 insertions(+) create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/KakaoMapLogo.imageset/KakaoMapLogo.svg create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/Contents.json create mode 100644 EATSSUDesign/EATSSUDesign/Resources/Images.xcassets/NaverMapLogo.imageset/NaverMapLogo.svg 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 @@ + + + + + + + + + From ebd09a9bc89f8549ecb5771767719a9fb301b9be Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:51:06 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[#432]=20=EC=A0=9C=ED=9C=B4=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=8B=9C=ED=8A=B8=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=EB=A7=B5/=EB=84=A4=EC=9D=B4=EB=B2=84=EC=A7=80?= =?UTF-8?q?=EB=8F=84=20=EC=97=B0=EA=B2=B0=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/en.lproj/Localizable.strings | 4 + .../Resources/ja.lproj/Localizable.strings | 4 + .../Resources/ko.lproj/Localizable.strings | 4 + .../Resources/vi.lproj/Localizable.strings | 4 + .../MainMapViewController+Marker.swift | 6 +- ...PartnershipDetailSheetViewController.swift | 133 +++++++++++++++--- .../Sources/Utility/Literal/TextLiteral.swift | 10 ++ EATSSU/Project.swift | 2 + 8 files changed, 140 insertions(+), 27 deletions(-) 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/Presentation/Map/ViewController/MainMapViewController+Marker.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift index b2bf9b31..cc490f8d 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift @@ -155,11 +155,7 @@ 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() diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 1ecf307c..0cc0cb53 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.gray200 + + [kakaoMapButton, naverMapButton, mapButtonDivider].forEach { + mapButtonBarView.addSubview($0) + } + + [storeNameLabel, typeStackView, infoListStackView, mapButtonBarView].forEach { view.addSubview($0) } } @@ -89,6 +107,28 @@ final class PartnershipDetailSheetViewController: BaseViewController { $0.top.equalTo(typeStackView.snp.bottom).offset(10) $0.leading.trailing.equalToSuperview().inset(24) } + + mapButtonBarView.snp.makeConstraints { + $0.top.equalTo(infoListStackView.snp.bottom).offset(6) + $0.leading.trailing.equalToSuperview() + $0.height.equalTo(56) + } + + 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 @@ -106,9 +146,9 @@ final class PartnershipDetailSheetViewController: BaseViewController { /// 매장 정보와 제휴 내용을 화면에 반영 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 +160,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) } @@ -135,11 +175,24 @@ final class PartnershipDetailSheetViewController: BaseViewController { /// 제휴 content 갯수에 따라 유동적으로 Height 측정 func calculatePreferredHeight() -> CGFloat { view.layoutIfNeeded() - let contentHeight = infoListStackView.frame.maxY - let bottomPadding: CGFloat = view.safeAreaInsets.bottom + 20 - + let contentHeight = mapButtonBarView.frame.maxY + let bottomPadding: CGFloat = view.safeAreaInsets.bottom + 10 + return contentHeight + 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 + } /// 제휴 정보 카드 뷰 생성 private func makeInfoCard(info: PartnershipInfoDTO, isLast: Bool) -> UIView { @@ -155,21 +208,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 @@ -198,4 +251,40 @@ final class PartnershipDetailSheetViewController: BaseViewController { return paddedContainer } + // MARK: - Actions + + /// 카카오맵 앱으로 이동 (미설치 시 카카오맵 웹) + @objc private func kakaoMapButtonTapped() { + let appURL = URL(string: "kakaomap://look?p=\(partnership.latitude),\(partnership.longitude)") + let encodedName = partnership.storeName + .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let webURL = URL( + string: "https://map.kakao.com/link/map/\(encodedName),\(partnership.latitude),\(partnership.longitude)" + ) + openMapApp(appURL: appURL, fallbackURL: webURL) + } + + /// 네이버지도 앱으로 이동 (미설치 시 네이버지도 웹 검색) + @objc private func naverMapButtonTapped() { + var components = URLComponents(string: "nmap://place") + components?.queryItems = [ + URLQueryItem(name: "lat", value: "\(partnership.latitude)"), + URLQueryItem(name: "lng", value: "\(partnership.longitude)"), + URLQueryItem(name: "name", value: partnership.storeName), + URLQueryItem(name: "appname", value: Bundle.main.bundleIdentifier ?? "") + ] + let encodedName = partnership.storeName + .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let webURL = URL(string: "https://map.naver.com/p/search/\(encodedName)") + 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..a650e84d 100644 --- a/EATSSU/Project.swift +++ b/EATSSU/Project.swift @@ -20,8 +20,10 @@ let appInfoPlist: InfoPlist = .extendingDefault(with: [ "LSApplicationQueriesSchemes": [ "kakaokompassauth", "kakaolink", + "kakaomap", "kakaoplus", "kakaotalk", + "nmap", ], "NSLocationWhenInUseUsageDescription": "지도에서 내 위치를 바로 확인하고, 현재 위치 주변의 제휴점들을 손쉽게 찾아볼 수 있도록 위치 권한을 허용해 주세요.", "UIApplicationSceneManifest": [ From 50e738b6fc11dbdb319b897d86bc102cb0dc719f Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:51:11 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[#432]=20=EC=8B=A4=EA=B8=B0=EA=B8=B0=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=EC=A0=9C=ED=9C=B4=20Moc?= =?UTF-8?q?k=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/PartnershipMockData.swift | 97 +++++++++++++++++++ .../MainMapViewController+Network.swift | 8 ++ 2 files changed, 105 insertions(+) create mode 100644 EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift diff --git a/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift new file mode 100644 index 00000000..9c718b5d --- /dev/null +++ b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift @@ -0,0 +1,97 @@ +// +// 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", + 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", + 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", + 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+Network.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift index 0f0c0fcb..ea784210 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 } } From be2c0326c803dc26f373a70fda4b90ff536b17be Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:17:07 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[#432]=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EB=A1=9C=EC=BB=AC=20API=20=EA=B8=B0=EB=B0=98=20=EC=B9=B4?= =?UTF-8?q?=EC=B9=B4=EC=98=A4=EB=A7=B5=20=EC=9E=A5=EC=86=8C=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20=EB=84=A4=EC=9D=B4?= =?UTF-8?q?=EB=B2=84=EC=A7=80=EB=8F=84=20=EA=B2=80=EC=83=89=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Network/Service/KakaoLocalService.swift | 86 +++++++++++++++++++ ...PartnershipDetailSheetViewController.swift | 44 ++++++++-- EATSSU/Project.swift | 1 + 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 EATSSU/App/Sources/Data/Network/Service/KakaoLocalService.swift 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/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 0cc0cb53..911fe10c 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -253,8 +253,26 @@ final class PartnershipDetailSheetViewController: BaseViewController { // MARK: - Actions - /// 카카오맵 앱으로 이동 (미설치 시 카카오맵 웹) + /// 카카오맵 장소 상세 페이지로 이동 (place id 조회 실패 시 좌표 핀으로 폴백) @objc private func kakaoMapButtonTapped() { + 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 = partnership.storeName .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" @@ -264,18 +282,28 @@ final class PartnershipDetailSheetViewController: BaseViewController { openMapApp(appURL: appURL, fallbackURL: webURL) } - /// 네이버지도 앱으로 이동 (미설치 시 네이버지도 웹 검색) + /// 네이버지도 검색으로 이동 (미설치 시 네이버지도 웹 검색) + /// 카카오 로컬 API의 정제된 상호명(지점명 포함)으로 검색해 명중률을 높임 @objc private func naverMapButtonTapped() { - var components = URLComponents(string: "nmap://place") + 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: "lat", value: "\(partnership.latitude)"), - URLQueryItem(name: "lng", value: "\(partnership.longitude)"), - URLQueryItem(name: "name", value: partnership.storeName), + URLQueryItem(name: "query", value: query), URLQueryItem(name: "appname", value: Bundle.main.bundleIdentifier ?? "") ] - let encodedName = partnership.storeName + let encodedQuery = query .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" - let webURL = URL(string: "https://map.naver.com/p/search/\(encodedName)") + let webURL = URL(string: "https://map.naver.com/p/search/\(encodedQuery)") openMapApp(appURL: components?.url, fallbackURL: webURL) } diff --git a/EATSSU/Project.swift b/EATSSU/Project.swift index a650e84d..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": [ From 0e4c510ef25705dc54e52fdbc18298710d67ea74 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:49:31 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[#432]=20=EC=84=9C=EB=B2=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B3=B5=20=EC=B9=B4=EC=B9=B4=EC=98=A4=EB=A7=B5/=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B2=84=EC=A7=80=EB=8F=84=20URL=20=EC=9A=B0=EC=84=A0?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0=20=EB=B0=8F=20DTO=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DTO/Partnership/PartnershipDTO.swift | 2 ++ .../Map/PartnershipMockData.swift | 6 +++++ .../MainMapViewController+Network.swift | 2 ++ ...PartnershipDetailSheetViewController.swift | 22 ++++++++++++++++--- 4 files changed, 29 insertions(+), 3 deletions(-) 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/Presentation/Map/PartnershipMockData.swift b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift index 9c718b5d..49de0d0e 100644 --- a/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift +++ b/EATSSU/App/Sources/Presentation/Map/PartnershipMockData.swift @@ -18,6 +18,8 @@ enum PartnershipMockData { longitude: 126.9550, latitude: 37.4959, restaurantType: "RESTAURANT", + naverMapUrl: nil, + kakaoMapUrl: nil, partnershipInfos: [ PartnershipInfoDTO( id: -1, @@ -48,6 +50,8 @@ enum PartnershipMockData { longitude: 126.9538, latitude: 37.4964, restaurantType: "CAFE", + naverMapUrl: nil, + kakaoMapUrl: nil, partnershipInfos: [ PartnershipInfoDTO( id: -3, @@ -67,6 +71,8 @@ enum PartnershipMockData { longitude: 126.9532, latitude: 37.4949, restaurantType: "PUB", + naverMapUrl: nil, + kakaoMapUrl: nil, partnershipInfos: [ PartnershipInfoDTO( id: -4, diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift index ea784210..d877f66b 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Network.swift @@ -65,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/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 911fe10c..2cdbfaa5 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -253,8 +253,18 @@ final class PartnershipDetailSheetViewController: BaseViewController { // MARK: - Actions - /// 카카오맵 장소 상세 페이지로 이동 (place id 조회 실패 시 좌표 핀으로 폴백) + /// 카카오맵 장소 상세 페이지로 이동 + /// 서버 제공 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, @@ -282,9 +292,15 @@ final class PartnershipDetailSheetViewController: BaseViewController { openMapApp(appURL: appURL, fallbackURL: webURL) } - /// 네이버지도 검색으로 이동 (미설치 시 네이버지도 웹 검색) - /// 카카오 로컬 API의 정제된 상호명(지점명 포함)으로 검색해 명중률을 높임 + /// 네이버지도 플레이스로 이동 + /// 서버 제공 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, From 0164c6d65c16ecf3b00bbf9b31087e1b2fda1d28 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:49:31 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[#432]=20=EB=82=B4=20=EC=A0=9C=ED=9C=B4=20?= =?UTF-8?q?=EB=AA=A8=EB=93=9C=EB=A1=9C=20=EC=8B=9C=EC=9E=91=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=ED=83=AD=20=EB=A7=88=EC=BB=A4=20=EB=AF=B8?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Map/ViewController/MainMapViewController.swift | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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() { From 1c522bfd75443d5ed06a5bb7f00a8502a1cd2ada Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:59:16 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[#432]=20=EC=A0=9C=ED=9C=B4=20=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=EB=8C=80=EB=B9=84=20?= =?UTF-8?q?=EB=B3=B4=EC=A0=95=20=EB=B0=8F=20large=20=ED=99=95=EC=9E=A5=20?= =?UTF-8?q?=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController+Marker.swift | 4 ++-- .../PartnershipDetailSheetViewController.swift | 15 +++++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift index cc490f8d..06ba34a6 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift @@ -168,10 +168,10 @@ extension MainMapViewController { let customDetent = UISheetPresentationController.Detent.custom { _ in return contentHeight } - 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/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 2cdbfaa5..201cddc3 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -81,7 +81,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { ) naverMapButton.addTarget(self, action: #selector(naverMapButtonTapped), for: .touchUpInside) - mapButtonDivider.backgroundColor = EATSSUDesignColors.Color.gray200 + mapButtonDivider.backgroundColor = EATSSUDesignColors.Color.gray300 [kakaoMapButton, naverMapButton, mapButtonDivider].forEach { mapButtonBarView.addSubview($0) @@ -108,10 +108,12 @@ final class PartnershipDetailSheetViewController: BaseViewController { $0.leading.trailing.equalToSuperview().inset(24) } + // large로 확장 시에도 버튼 바가 시트 하단에 붙어있도록 bottom 고정 mapButtonBarView.snp.makeConstraints { - $0.top.equalTo(infoListStackView.snp.bottom).offset(6) + $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 { @@ -173,12 +175,13 @@ final class PartnershipDetailSheetViewController: BaseViewController { // MARK: - UI Helpers /// 제휴 content 갯수에 따라 유동적으로 Height 측정 + /// 버튼 바는 시트 하단에 고정이므로 리스트 높이 기준으로 계산 func calculatePreferredHeight() -> CGFloat { view.layoutIfNeeded() - let contentHeight = mapButtonBarView.frame.maxY - let bottomPadding: CGFloat = view.safeAreaInsets.bottom + 10 + let buttonBarArea: CGFloat = 6 + 56 + 10 + let bottomPadding: CGFloat = view.safeAreaInsets.bottom - return contentHeight + bottomPadding + return infoListStackView.frame.maxY + buttonBarArea + bottomPadding } /// 지도 앱 이동 버튼 공통 Configuration 생성 @@ -232,7 +235,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) From 33947880e09882da90fe40664775c737d63e52e5 Mon Sep 17 00:00:00 2001 From: Hwang Sang Hwan <137605270+Hrepay@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:57:28 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[#432]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81:=20=EC=8B=9C=ED=8A=B8=20?= =?UTF-8?q?=EB=86=92=EC=9D=B4=20safe=20area=20=EC=8B=A4=EC=B8=A1=20?= =?UTF-8?q?=EC=9E=AC=EA=B3=84=EC=82=B0=20=EB=B0=8F=20=EC=A7=80=EB=8F=84=20?= =?UTF-8?q?URL=20=EC=9D=B8=EC=BD=94=EB=94=A9=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MainMapViewController+Marker.swift | 7 +++--- ...PartnershipDetailSheetViewController.swift | 24 +++++++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift index 06ba34a6..23543c18 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/MainMapViewController+Marker.swift @@ -161,12 +161,11 @@ extension MainMapViewController { 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, .large()] } else { diff --git a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift index 201cddc3..a19be2f8 100644 --- a/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift +++ b/EATSSU/App/Sources/Presentation/Map/ViewController/PartnershipDetailSheetViewController.swift @@ -142,10 +142,19 @@ 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 = partnership.storeName @@ -287,14 +296,20 @@ final class PartnershipDetailSheetViewController: BaseViewController { /// 카카오맵 좌표 핀으로 이동 (미설치 시 카카오맵 웹) private func openKakaoMapByCoordinate() { let appURL = URL(string: "kakaomap://look?p=\(partnership.latitude),\(partnership.longitude)") - let encodedName = partnership.storeName - .addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + 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() { @@ -320,8 +335,7 @@ final class PartnershipDetailSheetViewController: BaseViewController { URLQueryItem(name: "query", value: query), URLQueryItem(name: "appname", value: Bundle.main.bundleIdentifier ?? "") ] - let encodedQuery = query - .addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" + let encodedQuery = percentEncodedForMapURL(query) let webURL = URL(string: "https://map.naver.com/p/search/\(encodedQuery)") openMapApp(appURL: components?.url, fallbackURL: webURL) }