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 @@ + + + + + + + + +