diff --git a/NanaLand/NanaLand.xcodeproj/project.pbxproj b/NanaLand/NanaLand.xcodeproj/project.pbxproj index cbdf1c0..4a383b7 100644 --- a/NanaLand/NanaLand.xcodeproj/project.pbxproj +++ b/NanaLand/NanaLand.xcodeproj/project.pbxproj @@ -160,6 +160,11 @@ 6D6D308F2C61FAAF00002F77 /* review_restaurant.json in Resources */ = {isa = PBXBuildFile; fileRef = 6D6D308C2C61FAAF00002F77 /* review_restaurant.json */; }; 6D6D30902C61FAAF00002F77 /* report_complete.json in Resources */ = {isa = PBXBuildFile; fileRef = 6D6D308D2C61FAAF00002F77 /* report_complete.json */; }; 6D6D30912C61FAAF00002F77 /* review_experience.json in Resources */ = {isa = PBXBuildFile; fileRef = 6D6D308E2C61FAAF00002F77 /* review_experience.json */; }; + 6D711E312D6C83DF001B3DE4 /* KakaoMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D711E302D6C83DF001B3DE4 /* KakaoMapView.swift */; }; + 6D711E342D6C8E98001B3DE4 /* KakaoMapController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D711E332D6C8E98001B3DE4 /* KakaoMapController.swift */; }; + 6D711E372D6C8EC7001B3DE4 /* KakaoMapsSDK-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 6D711E362D6C8EC7001B3DE4 /* KakaoMapsSDK-SPM */; }; + 6D711E3A2D6C9C0F001B3DE4 /* AddressEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D711E392D6C9C0F001B3DE4 /* AddressEndPoint.swift */; }; + 6D711E3C2D6C9C1B001B3DE4 /* AddressService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D711E3B2D6C9C1B001B3DE4 /* AddressService.swift */; }; 6D722C062D2F6E8C006ACD8D /* NoResultFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D722C052D2F6E8C006ACD8D /* NoResultFilterView.swift */; }; 6D76022F2CEA587B00C49EDC /* loading.json in Resources */ = {isa = PBXBuildFile; fileRef = 6D76022E2CEA587B00C49EDC /* loading.json */; }; 6D7756752C8F4E190085D747 /* SearchReviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D7756742C8F4E190085D747 /* SearchReviewView.swift */; }; @@ -190,6 +195,7 @@ 6DCFB19E2D191F7B00A622EB /* UserInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCFB19D2D191F7B00A622EB /* UserInfoModel.swift */; }; 6DD11F422C51545C00363B08 /* TypeTestProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD11F412C51545C00363B08 /* TypeTestProfileView.swift */; }; 6DD11F442C5161E500363B08 /* ReviewArticleItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD11F432C5161E500363B08 /* ReviewArticleItemView.swift */; }; + 6DF06DAF2D6D987F00465584 /* KakaoGecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF06DAE2D6D987F00465584 /* KakaoGecoder.swift */; }; 6DF18F082C6E25130021ED5B /* TestLoading.json in Resources */ = {isa = PBXBuildFile; fileRef = 6DF18F072C6E25120021ED5B /* TestLoading.json */; }; 6DFF08EE2D3BFBB80052A4E3 /* LottieWithTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DFF08ED2D3BFBB80052A4E3 /* LottieWithTextView.swift */; }; 9C6A9C3E2BCC1BAE000413AD /* Fonts+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6A9C3D2BCC1BAE000413AD /* Fonts+.swift */; }; @@ -497,6 +503,10 @@ 6D6D308C2C61FAAF00002F77 /* review_restaurant.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = review_restaurant.json; sourceTree = ""; }; 6D6D308D2C61FAAF00002F77 /* report_complete.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = report_complete.json; sourceTree = ""; }; 6D6D308E2C61FAAF00002F77 /* review_experience.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = review_experience.json; sourceTree = ""; }; + 6D711E302D6C83DF001B3DE4 /* KakaoMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoMapView.swift; sourceTree = ""; }; + 6D711E332D6C8E98001B3DE4 /* KakaoMapController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoMapController.swift; sourceTree = ""; }; + 6D711E392D6C9C0F001B3DE4 /* AddressEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressEndPoint.swift; sourceTree = ""; }; + 6D711E3B2D6C9C1B001B3DE4 /* AddressService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressService.swift; sourceTree = ""; }; 6D722C052D2F6E8C006ACD8D /* NoResultFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoResultFilterView.swift; sourceTree = ""; }; 6D76022E2CEA587B00C49EDC /* loading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = loading.json; sourceTree = ""; }; 6D7756742C8F4E190085D747 /* SearchReviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchReviewView.swift; sourceTree = ""; }; @@ -528,6 +538,7 @@ 6DCFB19D2D191F7B00A622EB /* UserInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoModel.swift; sourceTree = ""; }; 6DD11F412C51545C00363B08 /* TypeTestProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeTestProfileView.swift; sourceTree = ""; }; 6DD11F432C5161E500363B08 /* ReviewArticleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewArticleItemView.swift; sourceTree = ""; }; + 6DF06DAE2D6D987F00465584 /* KakaoGecoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KakaoGecoder.swift; sourceTree = ""; }; 6DF18F072C6E25120021ED5B /* TestLoading.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = TestLoading.json; sourceTree = ""; }; 6DFF08ED2D3BFBB80052A4E3 /* LottieWithTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieWithTextView.swift; sourceTree = ""; }; 9C6A9C3D2BCC1BAE000413AD /* Fonts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Fonts+.swift"; sourceTree = ""; }; @@ -664,6 +675,7 @@ BF85F6602CD3617A0020BDBF /* FirebaseAuth in Frameworks */, 2C80ECEA2BDAA551002F38A7 /* SwiftUICalendar in Frameworks */, 2C219B532C16FE53001F4C28 /* FirebaseRemoteConfig in Frameworks */, + 6D711E372D6C8EC7001B3DE4 /* KakaoMapsSDK-SPM in Frameworks */, BFB73BB72C05BA6A00A9A10E /* SDWebImageSwiftUI in Frameworks */, BF85F65E2CD3614F0020BDBF /* FirebaseFirestore in Frameworks */, 2C3C75AF2BCA3C5000312F26 /* Alamofire in Frameworks */, @@ -828,6 +840,7 @@ 2C3C75C02BCA540200312F26 /* Networks */ = { isa = PBXGroup; children = ( + 6D711E382D6C9BFD001B3DE4 /* Address */, BF8EF0672D07D77600A722BB /* File */, 6D799D452D1D3737003954C1 /* PreSignedUpload */, BFC7AF282C7EDBB800432ECC /* Notification */, @@ -901,6 +914,7 @@ BF0D58962C60DAE20042D6BC /* ExpandableText.swift */, 6D722C052D2F6E8C006ACD8D /* NoResultFilterView.swift */, 6D224A822D5F016F001EB871 /* PhotoModalView.swift */, + 6D711E322D6C8E8C001B3DE4 /* KakaoMap */, ); path = Component; sourceTree = ""; @@ -1268,6 +1282,25 @@ path = TypeTest; sourceTree = ""; }; + 6D711E322D6C8E8C001B3DE4 /* KakaoMap */ = { + isa = PBXGroup; + children = ( + 6D711E302D6C83DF001B3DE4 /* KakaoMapView.swift */, + 6D711E332D6C8E98001B3DE4 /* KakaoMapController.swift */, + 6DF06DAE2D6D987F00465584 /* KakaoGecoder.swift */, + ); + path = KakaoMap; + sourceTree = ""; + }; + 6D711E382D6C9BFD001B3DE4 /* Address */ = { + isa = PBXGroup; + children = ( + 6D711E392D6C9C0F001B3DE4 /* AddressEndPoint.swift */, + 6D711E3B2D6C9C1B001B3DE4 /* AddressService.swift */, + ); + path = Address; + sourceTree = ""; + }; 6D799D452D1D3737003954C1 /* PreSignedUpload */ = { isa = PBXGroup; children = ( @@ -1671,6 +1704,7 @@ BF8F85BF2C7B7BC60066432C /* FirebaseMessaging */, BF85F65D2CD3614F0020BDBF /* FirebaseFirestore */, BF85F65F2CD3617A0020BDBF /* FirebaseAuth */, + 6D711E362D6C8EC7001B3DE4 /* KakaoMapsSDK-SPM */, ); productName = NanaLand; productReference = 2C3C75802BCA3ADA00312F26 /* NanaLand.app */; @@ -1761,6 +1795,7 @@ 2C219B4F2C16FE53001F4C28 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 6D0E90D62C54659900F8D686 /* XCRemoteSwiftPackageReference "MasonryStack" */, BF0D58982C61D81F0042D6BC /* XCRemoteSwiftPackageReference "CustomAlert" */, + 6D711E352D6C8EC7001B3DE4 /* XCRemoteSwiftPackageReference "KakaoMapsSDK-SPM" */, ); productRefGroup = 2C3C75812BCA3ADA00312F26 /* Products */; projectDirPath = ""; @@ -1848,6 +1883,7 @@ 2C9CF4DC2BFE0EA800A346E8 /* ReportInfoResultView.swift in Sources */, BF3021EE2BDF8E9300CB3966 /* ShopDetailService.swift in Sources */, BF06B36E2C4AADAE000E43ED /* ExperienceDetailModel.swift in Sources */, + 6DF06DAF2D6D987F00465584 /* KakaoGecoder.swift in Sources */, BF1510D82C4502F700BC2DE6 /* ExperienceMainModel.swift in Sources */, 2C2D3D552C038A54002A38AA /* LocalizedKey.swift in Sources */, BF22B8032C460F7D0059B380 /* ExperienceService.swift in Sources */, @@ -1868,6 +1904,7 @@ BF17B5FE2C5B261B0045B6E3 /* UserProfileMainView.swift in Sources */, BF06B36A2C4AAD91000E43ED /* ExperienceDetailView.swift in Sources */, BF7F7F622BE613400000F08A /* NatureService.swift in Sources */, + 6D711E3A2D6C9C0F001B3DE4 /* AddressEndPoint.swift in Sources */, 2C80ECBA2BCFCCB7002F38A7 /* NetworkManager.swift in Sources */, 2C80ECD12BD4AEDE002F38A7 /* NanaNavigationBar.swift in Sources */, BF80FDA32BFB2BEF00C5DA2D /* ProfileUpdateModel.swift in Sources */, @@ -1976,6 +2013,7 @@ BFF8E9E92C045B830081C0A6 /* LanguageView.swift in Sources */, BF138BCA2CEEFCD10013F1C0 /* HotModel.swift in Sources */, 2C2749832C09833500ECBA71 /* String+.swift in Sources */, + 6D711E312D6C83DF001B3DE4 /* KakaoMapView.swift in Sources */, 6D9EDADE2C5B52DA003A8DE5 /* NoticeService.swift in Sources */, 6D130D002C788D61005FCD99 /* NewNanaPickDetailService.swift in Sources */, 2C40DB9A2BE64973007A3768 /* NanaHome.swift in Sources */, @@ -1991,10 +2029,12 @@ BFABD3512BE7634100C3F3A6 /* FestivalModel.swift in Sources */, BF9834182BCE700B00A5C289 /* FestivalMainView.swift in Sources */, 2C5425002BF72C3500B16439 /* TripType.swift in Sources */, + 6D711E3C2D6C9C1B001B3DE4 /* AddressService.swift in Sources */, 2C3C75D12BCAAFF300312F26 /* FavoriteMainView.swift in Sources */, BF4F6C6C2BD5560A009F070F /* NaNaPickDetailViewModel.swift in Sources */, 2C80ECAD2BCF6201002F38A7 /* SearchService.swift in Sources */, 2C9CF4E42BFEE59200A346E8 /* ArticleDetailViewType.swift in Sources */, + 6D711E342D6C8E98001B3DE4 /* KakaoMapController.swift in Sources */, 6D48AA0C2C75C65E00E16429 /* NewNanaPickMainModel.swift in Sources */, 2C80ECF02BDBE44C002F38A7 /* RoundedCorners.swift in Sources */, BFABD34F2BE75F4600C3F3A6 /* FestivalEndPoint.swift in Sources */, @@ -2548,6 +2588,14 @@ minimumVersion = 0.1.0; }; }; + 6D711E352D6C8EC7001B3DE4 /* XCRemoteSwiftPackageReference "KakaoMapsSDK-SPM" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/kakao-mapsSDK/KakaoMapsSDK-SPM.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.12.5; + }; + }; BF0D58982C61D81F0042D6BC /* XCRemoteSwiftPackageReference "CustomAlert" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/divadretlaw/CustomAlert.git"; @@ -2627,6 +2675,11 @@ package = 6D0E90D62C54659900F8D686 /* XCRemoteSwiftPackageReference "MasonryStack" */; productName = MasonryStack; }; + 6D711E362D6C8EC7001B3DE4 /* KakaoMapsSDK-SPM */ = { + isa = XCSwiftPackageProductDependency; + package = 6D711E352D6C8EC7001B3DE4 /* XCRemoteSwiftPackageReference "KakaoMapsSDK-SPM" */; + productName = "KakaoMapsSDK-SPM"; + }; BF0D58992C61D81F0042D6BC /* CustomAlert */ = { isa = XCSwiftPackageProductDependency; package = BF0D58982C61D81F0042D6BC /* XCRemoteSwiftPackageReference "CustomAlert" */; diff --git a/NanaLand/NanaLand.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NanaLand/NanaLand.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index faf3fb5..728aec2 100644 --- a/NanaLand/NanaLand.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NanaLand/NanaLand.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "39719f64a2d469eae2495c6d8a7d0f328606b43784795304ad68ef674551f9b2", + "originHash" : "e94927db15150af0f88b444f033b8e35bdb98f0458761ee054ba429120737938", "pins" : [ { "identity" : "abseil-cpp-binary", @@ -163,6 +163,15 @@ "version" : "2.23.0" } }, + { + "identity" : "kakaomapssdk-spm", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kakao-mapsSDK/KakaoMapsSDK-SPM.git", + "state" : { + "revision" : "d44bc7365bee88f66964a14168a42910ac092d38", + "version" : "2.12.5" + } + }, { "identity" : "kingfisher", "kind" : "remoteSourceControl", diff --git a/NanaLand/NanaLand/App/NanaLandApp.swift b/NanaLand/NanaLand/App/NanaLandApp.swift index e0a41c4..e288bc7 100644 --- a/NanaLand/NanaLand/App/NanaLandApp.swift +++ b/NanaLand/NanaLand/App/NanaLandApp.swift @@ -6,6 +6,7 @@ // import SwiftUI +import KakaoMapsSDK import KakaoSDKCommon import KakaoSDKAuth import GoogleSignIn @@ -57,6 +58,11 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele let gcmMessageIDKey = "gcm.message_id" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + + // kakao sdk 최기화 + SDKInitializer.InitSDK(appKey: Secrets.kakaoLoginNativeAppKey) + + // 파이어 베이스 설정 FirebaseApp.configure() // 앱 실행시 유저에게 알림 허용 권한을 받음 diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/Contents.json b/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/Contents.json new file mode 100644 index 0000000..f817d78 --- /dev/null +++ b/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icAdressArrow.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/icAdressArrow.png b/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/icAdressArrow.png new file mode 100644 index 0000000..ced9451 Binary files /dev/null and b/NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/icAdressArrow.png differ diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/Contents.json b/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/Contents.json new file mode 100644 index 0000000..16f73f5 --- /dev/null +++ b/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "icCopy.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/icCopy.png b/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/icCopy.png new file mode 100644 index 0000000..beeec49 Binary files /dev/null and b/NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/icCopy.png differ diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/Contents.json b/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/Contents.json new file mode 100644 index 0000000..4ae22e7 --- /dev/null +++ b/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "logoPin.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/logoPin.png b/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/logoPin.png new file mode 100644 index 0000000..13acc47 Binary files /dev/null and b/NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/logoPin.png differ diff --git a/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoGecoder.swift b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoGecoder.swift new file mode 100644 index 0000000..966246e --- /dev/null +++ b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoGecoder.swift @@ -0,0 +1,70 @@ +// +// KakaoGecoder.swift +// NanaLand +// +// Created by wodnd on 2/25/25. +// + +import Foundation +import SwiftUI + +struct KakaoGeocoder { + static let apiKey = "63ebadd5e9c63cbaeca239d78b3ad4f4" // 🔴 여기에 본인의 Kakao REST API 키 입력 + + static func convertAddressToCoordinates(address: String, completion: @escaping (Double?, Double?) -> Void) { + let encodedAddress = address.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "" + let urlString = "https://dapi.kakao.com/v2/local/search/address.json?query=\(encodedAddress)" + + guard let url = URL(string: urlString) else { + print("❌ URL 생성 실패") + completion(nil, nil) + return + } + + var request = URLRequest(url: url) + request.setValue("KakaoAK \(apiKey)", forHTTPHeaderField: "Authorization") // 🔴 API 키 추가 + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + if let error = error { + print("❌ 네트워크 요청 실패: \(error.localizedDescription)") + completion(nil, nil) + return + } + + guard let data = data else { + print("❌ 데이터 없음") + completion(nil, nil) + return + } + + do { + let decodedResponse = try JSONDecoder().decode(KakaoAddressResponse.self, from: data) + + if let firstAddress = decodedResponse.documents.first { + let latitude = Double(firstAddress.y) ?? 0.0 + let longitude = Double(firstAddress.x) ?? 0.0 + print("✅ 변환된 좌표: \(latitude), \(longitude)") + completion(latitude, longitude) + } else { + print("❌ 주소 검색 결과 없음") + completion(nil, nil) + } + } catch { + print("❌ JSON 파싱 오류: \(error.localizedDescription)") + completion(nil, nil) + } + } + + task.resume() + } +} + +// 🔹 API 응답 모델 +struct KakaoAddressResponse: Codable { + let documents: [KakaoAddress] +} + +struct KakaoAddress: Codable { + let x: String // 경도 + let y: String // 위도 +} diff --git a/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapController.swift b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapController.swift new file mode 100644 index 0000000..42adf44 --- /dev/null +++ b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapController.swift @@ -0,0 +1,141 @@ +// +// KakaoMap.swift +// NanaLand +// +// Created by wodnd on 2/24/25. +// + +import Foundation +import SwiftUI +import KakaoMapsSDK + +struct KakaoMapController: UIViewRepresentable { + @Binding var draw: Bool + @Binding var coordinate: (Double, Double) // 🔴 변환된 좌표 저장 + + func makeUIView(context: Self.Context) -> KMViewContainer { + let view = KMViewContainer(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)) + context.coordinator.createController(view) + return view + } + + func updateUIView(_ uiView: KMViewContainer, context: Self.Context) { + print("🔄 updateUIView 호출됨. 현재 draw 상태: \(draw)") + if draw { + DispatchQueue.main.asyncAfter(deadline: .now()) { + guard let controller = context.coordinator.controller else { + print("❌ controller가 nil 상태입니다. updateUIView에서 실행할 수 없습니다.") + return + } + + if !controller.isEnginePrepared { + print("⚠️ 엔진 준비되지 않음. 준비 시작.") + controller.prepareEngine() + } + + if !controller.isEngineActive { + print("⚠️ 엔진 비활성화됨. 활성화 시작.") + controller.activateEngine() + } + + // ✅ 지도 이동 (좌표가 업데이트된 경우) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if let mapView = controller.getView("mapview") as? KakaoMap { + print("🗺️ mapView 가져오기 성공") + let cameraUpdate = CameraUpdate.make(target: MapPoint(longitude: coordinate.0, latitude: coordinate.1), mapView: mapView) + mapView.moveCamera(cameraUpdate) + print("✅ 지도 이동 완료: \(coordinate.0), \(coordinate.1)") + + // 🟢 새로운 마커 핀 추가 + context.coordinator.addMarker(at: coordinate, to: mapView) + } else { + print("⚠️ mapView가 nil입니다. 지도 뷰가 추가되었는지 확인하세요.") + } + } + } + } else { + print("🛑 지도 엔진 중지됨") + context.coordinator.controller?.pauseEngine() + context.coordinator.controller?.resetEngine() + } + } + + func makeCoordinator() -> KakaoMapCoordinator { + return KakaoMapCoordinator() + } + + class KakaoMapCoordinator: NSObject, MapControllerDelegate { + var controller: KMController? + var container: KMViewContainer? + + func createController(_ view: KMViewContainer) { + print("🟢 createController 호출됨") + container = view + controller = KMController(viewContainer: view) + controller?.delegate = self + } + + func addViews() { + guard let controller = controller else { + print("❌ controller가 nil입니다. addView를 실행할 수 없습니다.") + return + } + + let defaultPosition = MapPoint(longitude: 126.529124, latitude: 33.362418) + let mapviewInfo = MapviewInfo(viewName: "mapview", viewInfoName: "map", defaultPosition: defaultPosition) + controller.addView(mapviewInfo) + } + + func addViewSucceeded(_ viewName: String, viewInfoName: String) { + print("✅ 지도 뷰 추가 성공: \(viewName)") + let view = controller?.getView("mapview") + view?.viewRect = container!.bounds + } + + func addMarker(at coordinate: (Double, Double), to mapView: KakaoMap) { + let defaultCoordinate: (Double, Double) = (126.529124, 33.362418) // 기본 위치 + if coordinate == defaultCoordinate { + print("⚠️ 기본 위치에서는 마커 추가 안 함") + return + } + + print("📍 마커 추가: \(coordinate.0), \(coordinate.1)") + + let manager = mapView.getLabelManager() + + // 🔴 LabelLayer가 있는지 확인 후 없으면 추가 + if manager.getLabelLayer(layerID: "PoiLayer") == nil { + print("⚠️ LabelLayer가 없음. 새로 추가합니다.") + let layerOption = LabelLayerOptions(layerID: "PoiLayer", competitionType: .none, competitionUnit: .symbolFirst, orderType: .rank, zOrder: 100000) + manager.addLabelLayer(option: layerOption) + } + + let poiOption = PoiOptions(styleID: "CustomMarkerStyle") + let layer = manager.getLabelLayer(layerID: "PoiLayer") + + guard let poi = layer?.addPoi(option: poiOption, at: MapPoint(longitude: coordinate.0, latitude: coordinate.1)) else { + print("❌ 마커 추가 실패: addPoi()가 nil 반환") + return + } + + if let originalImage = UIImage(named: "logoPin") { + let resizedImage = originalImage.resize(to: CGSize(width: 35, height: 35)) + let badge = PoiBadge(badgeID: "marker", image: resizedImage, offset: CGPoint(x: 0, y: -20), zOrder: 1) + poi.addBadge(badge) + poi.show() + poi.showBadge(badgeID: "marker") + } else { + print("⚠️ 이미지 로드 실패: custom_marker.png") + } + } + } +} + +extension UIImage { + func resize(to size: CGSize) -> UIImage? { + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: size)) + } + } +} diff --git a/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapView.swift b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapView.swift new file mode 100644 index 0000000..d55e9d4 --- /dev/null +++ b/NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapView.swift @@ -0,0 +1,168 @@ +// +// KakaoMapView.swift +// NanaLand +// +// Created by wodnd on 2/24/25. +// + +import SwiftUI +import KakaoMapsSDK + +struct KakaoMapView: View { + @State private var draw: Bool = false //지도 Appear 토글 + @State var convertedCoordinate: (Double, Double) = (0.0, 0.0) //DetailView에서 받은 좌표 + let title: String + let address: String + let koreanAddress: String + + var body: some View { + ZStack{ + + KakaoMapController(draw: $draw, coordinate: $convertedCoordinate) + .onAppear { + print("🟢 KakaoMap appeared") + self.draw = true + } + .onDisappear { + print("🔴 KakaoMap disappeared") + self.draw = false + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + VStack{ + NanaNavigationBar(title: .empty, showBackButton: true) + .frame(height: 56) + + Spacer() + } + + VStack{ + Spacer() + + KakaoMapBottomView(title: title, address: address, koreanAddress: koreanAddress) + } + } + .toolbar(.hidden) + .onAppear(){ + convertAddressWithKakaoAPI(address: koreanAddress) + } + } + + /// 📌 **카카오 API를 사용하여 도로명 주소를 위도/경도로 변환** + func convertAddressWithKakaoAPI(address: String) { + print("📍 입력한 주소: \(address)") + + KakaoGeocoder.convertAddressToCoordinates(address: address) { latitude, longitude in + DispatchQueue.main.async { + if let lat = latitude, let lon = longitude { + self.convertedCoordinate = (lon, lat) // 🔄 위도, 경도 저장 + print("✅ 변환된 좌표: \(lon), \(lat)") + } else { + print("❌ 변환된 좌표 없음") + } + } + } + } +} + + +struct KakaoMapBottomView: View { + let title: String + let address: String + let koreanAddress: String + @EnvironmentObject var localizationManager: LocalizationManager + + @State var showCopyAlert: Bool = false + @State var alertMessage: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 0){ + Text(title) + .font(.body_bold) + .foregroundColor(.black) + .frame(height: Constants.screenWidth * (26 / 360)) + + if address != "" { + Text(address) + .font(.caption01) + .multilineTextAlignment(.leading) + .foregroundColor(.gray1) + } + + Button(action: { + copyToClipboard(koreanAddress) + }, label: { + HStack(spacing: 4){ + Text(koreanAddress) + .font(.body02) + .foregroundColor(.black) + .frame(height: Constants.screenWidth * (22 / 360)) + + Image("icCopy") + .resizable() + .scaledToFit() + .frame(width: Constants.screenWidth * (14 / 360)) + + Spacer() + } + .padding(.top, Constants.screenWidth * (12 / 360)) + }) + .alert(isPresented: $showCopyAlert) { + Alert(title: Text(.notification), message: Text(alertMessage), dismissButton: .default(Text(.check))) + } + } + .padding(.leading, Constants.screenWidth * (16 / 360)) + .padding(.trailing, Constants.screenWidth * (16 / 360)) + .padding(.top, Constants.screenWidth * (16 / 360)) + .padding(.bottom, Constants.screenWidth * (16 / 360)) + .background(){ + Rectangle() + .foregroundColor(.white) + .cornerRadius(16, corners: .topLeft) + .cornerRadius(16, corners: .topRight) + .frame(width: Constants.screenWidth) + } + } + + func copyToClipboard(_ text: String){ + + // 문자열이 비어있는 경우 예외처리 + guard !text.isEmpty else { + alertMessage = ClipboardCopyStatus.emptyString.message(using: localizationManager) + showCopyAlert = true + return + } + + if UIPasteboard.general.hasStrings { + //복사 성공 + UIPasteboard.general.string = text + alertMessage = ClipboardCopyStatus.success.message(using: localizationManager) + } else { + // 접근 권한 없어 복사 실패 + alertMessage = ClipboardCopyStatus.success.message(using: localizationManager) + } + + showCopyAlert = true + } +} + +enum ClipboardCopyStatus{ + case success + case emptyString + case failAccess + + func message(using localizationManager: LocalizationManager) -> String { + switch self { + case .success: + return LocalizedKey.copySuccess.localized(for: localizationManager.language) + case .emptyString: + return LocalizedKey.emptyCopyString.localized(for: localizationManager.language) + case .failAccess: + return LocalizedKey.failAccess.localized(for: localizationManager.language) + } + } +} + +#Preview { + KakaoMapView(title: "", address: "", koreanAddress: "") +} diff --git a/NanaLand/NanaLand/Common/Localizing/LocalizedKey.swift b/NanaLand/NanaLand/Common/Localizing/LocalizedKey.swift index 2821864..20eb897 100644 --- a/NanaLand/NanaLand/Common/Localizing/LocalizedKey.swift +++ b/NanaLand/NanaLand/Common/Localizing/LocalizedKey.swift @@ -563,6 +563,13 @@ enum LocalizedKey: String { //MARK: - 찜 콘텐츠 case noFavorite + //MARK: - 지도 보기 + case detailView + case empty + case copySuccess + case emptyCopyString + case failAccess + //MARK: - localized() func localized(for language: Language) -> String { guard let path = Bundle.main.path(forResource: language.localizedName, ofType: "lproj"), diff --git a/NanaLand/NanaLand/Common/Localizing/en.lproj/Localizable.strings b/NanaLand/NanaLand/Common/Localizing/en.lproj/Localizable.strings index 2a8e6c5..4d2f6b6 100644 --- a/NanaLand/NanaLand/Common/Localizing/en.lproj/Localizable.strings +++ b/NanaLand/NanaLand/Common/Localizing/en.lproj/Localizable.strings @@ -555,3 +555,10 @@ the market is the place!"; //MARK: - 찜 콘텐츠 "noFavorite" = "You haven't favorited any content yet\nTap the heart on content you like!"; + +//MARK: - 지도 보기 +"detailView" = "Learn more"; +"empty" = ""; +"copySuccess" = "Copied to clipboard!"; +"emptyCopyString" = "Clipboard copy failed"; +"failAccess" = "Clipboard copy failed\nPlease check the permission in the settings"; diff --git a/NanaLand/NanaLand/Common/Localizing/ko.lproj/Localizable.strings b/NanaLand/NanaLand/Common/Localizing/ko.lproj/Localizable.strings index b199a18..87bde84 100644 --- a/NanaLand/NanaLand/Common/Localizing/ko.lproj/Localizable.strings +++ b/NanaLand/NanaLand/Common/Localizing/ko.lproj/Localizable.strings @@ -545,3 +545,10 @@ //MARK: - 찜 콘텐츠 "noFavorite" = "아직 찜한 컨텐츠가 없습니다\n마음에 드는 컨텐츠에 하트를 눌러보세요!"; + +//MARK: - 지도 보기 +"detailView" = "자세히 보기"; +"empty" = ""; +"copySuccess" = "클립보드에 복사되었습니다!"; +"emptyCopyString" = "클립보드 복사에 실패했습니다"; +"failAccess" = "클립보드 복사에 실패했습니다\n설정에서 권한을 확인해주세요"; diff --git a/NanaLand/NanaLand/Common/Localizing/ms.lproj/Localizable.strings b/NanaLand/NanaLand/Common/Localizing/ms.lproj/Localizable.strings index 998b0b2..741ef75 100644 --- a/NanaLand/NanaLand/Common/Localizing/ms.lproj/Localizable.strings +++ b/NanaLand/NanaLand/Common/Localizing/ms.lproj/Localizable.strings @@ -545,3 +545,10 @@ mengembalikan sebarang hasil."; //MARK: - 찜 콘텐츠 "noFavorite" = "Anda belum menandakan kandungan sebagai kegemaran\nTekan hati pada kandungan yang anda suka!"; + +//MARK: - 지도 보기 +"detailView" = "Ketahui lebih lanjut"; +"empty" = ""; +"copySuccess" = "Disalin ke papan keratan!"; +"emptyCopyString" = "Salinan papan klip gagal"; +"failAccess" = "Salinan papan klip gagal\nSila semak kebenaran dalam tetapan"; diff --git a/NanaLand/NanaLand/Common/Localizing/vi.lproj/Localizable.strings b/NanaLand/NanaLand/Common/Localizing/vi.lproj/Localizable.strings index 309da05..52a4a2a 100644 --- a/NanaLand/NanaLand/Common/Localizing/vi.lproj/Localizable.strings +++ b/NanaLand/NanaLand/Common/Localizing/vi.lproj/Localizable.strings @@ -18,7 +18,7 @@ "yes" = "Có"; "emptyString" = "This service is being prepared.\nSee you next time!"; "beingPrepared" = "This service is being prepared.\nSee you next time!"; -"notification" = "Notification"; +"notification" = "Báo động"; //MARK: - Tab "home" = "Trang chủ"; @@ -555,3 +555,10 @@ //MARK: - 찜 콘텐츠 "noFavorite" = "Bạn chưa yêu thích nội dung nào\nHãy nhấn vào biểu tượng trái tim trên nội dung bạn thích!"; + +//MARK: - 지도 보기 +"detailView" = "Tìm hiểu thêm"; +"empty" = ""; +"copySuccess" = "Đã sao chép vào bảng nháp!"; +"emptyCopyString" = "Lỗi sao chép bảng tạm"; +"failAccess" = "Lỗi sao chép bảng tạm\nVui lòng kiểm tra quyền trong cài đặt"; diff --git a/NanaLand/NanaLand/Common/Localizing/zh-Hans.lproj/Localizable.strings b/NanaLand/NanaLand/Common/Localizing/zh-Hans.lproj/Localizable.strings index 9ff4de4..b3c0176 100644 --- a/NanaLand/NanaLand/Common/Localizing/zh-Hans.lproj/Localizable.strings +++ b/NanaLand/NanaLand/Common/Localizing/zh-Hans.lproj/Localizable.strings @@ -543,3 +543,10 @@ //MARK: - 찜 콘텐츠 "noFavorite" = "您还没有收藏的内容\n请点击心形图标收藏您喜欢的内容!"; + +//MARK: - 지도 보기 +"detailView" = "了解更多"; +"empty" = ""; +"copySuccess" = "复制到剪贴板!"; +"emptyCopyString" = "剪贴板复制失败"; +"failAccess" = "剪贴板复制失败\n请检查设置中的权限"; diff --git a/NanaLand/NanaLand/Experience/ExperienceDetailView.swift b/NanaLand/NanaLand/Experience/ExperienceDetailView.swift index d837e41..01a6c67 100644 --- a/NanaLand/NanaLand/Experience/ExperienceDetailView.swift +++ b/NanaLand/NanaLand/Experience/ExperienceDetailView.swift @@ -30,8 +30,11 @@ struct ExperienceDetailView: View { @State private var reviewThumbnailModal = false @State var selectedImageURL: String = ""// 선택된 이미지 URL + @State var koreanAddress: String = "" + var id: Int64 var experienceType = "k" + var body: some View { VStack { ZStack { @@ -286,10 +289,41 @@ struct ExperienceDetailView: View { .font(.body02_bold) Text(viewModel.state.getExperienceDetailResponse.address ?? "") .font(.body02) + .padding(.bottom, Constants.screenWidth * (12 / 360)) + + Button { + if localizationManager.language == .korean { + AppState.shared.navigationPath.append(experienceDetailType.detailMap(title: viewModel.state.getExperienceDetailResponse.title ?? "" ,address: "", korean: viewModel.state.getExperienceDetailResponse.address ?? "")) + } else { + AppState.shared.navigationPath.append(experienceDetailType.detailMap(title: viewModel.state.getExperienceDetailResponse.title ?? "" ,address: viewModel.state.getExperienceDetailResponse.address ?? "", korean: koreanAddress)) + } + + } label: { + HStack(spacing: 0){ + Text(.detailView) + .font(.caption01) + .foregroundColor(.gray1) + + Image("icAdressArrow") + .resizable() + .scaledToFit() + .frame(width: Constants.screenWidth * (12 / 360)) + } + .padding(.leading, Constants.screenWidth * (8 / 360)) + .padding(.trailing, Constants.screenWidth * (8 / 360)) + .background(){ + RoundedRectangle(cornerRadius: 100) + .frame(height: Constants.screenWidth * (28 / 360)) + .foregroundColor(.gray3) + } + } + + Spacer() + } Spacer() } - .frame(width: Constants.screenWidth - 40, height: (Constants.screenWidth - 40) * (42 / 358)) + .frame(width: Constants.screenWidth - 40) } if viewModel.state.getExperienceDetailResponse.contact != "" { @@ -789,6 +823,10 @@ struct ExperienceDetailView: View { .onAppear { Task { await getExperienceDetail(id: id, isSearch: false) + if localizationManager.language != .korean { + await getKoreanAddress(id: id, category: "EXPERIENCE") + koreanAddress = viewModel.state.getKoreanAddress + } await getReviewData(id: id, category: "EXPERIENCE", page: 0, size: 12) isAPICall = true // 이미지 불러오는 데 시간이 걸림 } @@ -916,6 +954,14 @@ struct ExperienceDetailView: View { } } + .navigationDestination(for: experienceDetailType.self) { detailView in + switch detailView { + case let .detailMap(title, address, koreanAddress): + KakaoMapView(title: title, address: address, koreanAddress: koreanAddress) + + } + + } } .toolbar(.hidden) .overlay( @@ -929,6 +975,10 @@ struct ExperienceDetailView: View { } + func getKoreanAddress(id: Int64, category: String) async { + await viewModel.action(.getKoreanAddress(id: id, category: category)) + } + func getReviewData(id: Int64, category: String, page: Int, size: Int) async { await viewModel.action(.getReviewData(id: id, category: category, page: page, size: size)) } @@ -941,6 +991,7 @@ struct ExperienceDetailView: View { await viewModel.action(.toggleFavorite(body: body)) } + func reviewFavorite(id: Int64) async { await viewModel.action(.reviewFavorite(id: id)) } @@ -979,6 +1030,11 @@ enum ExperienceViewType: Hashable { case detailReivew(id: Int64, category: String) } +enum experienceDetailType: Hashable{ + case detailMap(title: String, address: String, korean: String) +} + + //#Preview { // ExperienceDetailView(id: 1) //} diff --git a/NanaLand/NanaLand/Experience/ExperienceDetailViewModel.swift b/NanaLand/NanaLand/Experience/ExperienceDetailViewModel.swift index a24fcc0..329d96e 100644 --- a/NanaLand/NanaLand/Experience/ExperienceDetailViewModel.swift +++ b/NanaLand/NanaLand/Experience/ExperienceDetailViewModel.swift @@ -15,6 +15,7 @@ class ExperienceDetailViewModel: ObservableObject { var getReviewDataResponse = ReviewModel(totalElements: 0, totalAvgRating: 0.0, data: [ReviewData(id: 0, memberId: 0, nickname: "", profileImage: ImageList(originUrl: "", thumbnailUrl: ""), memberReviewCount: 0, rating: 0, content: "", createdAt: "", heartCount: 0, images: [], reviewTypeKeywords: [], reviewHeart: false, myReview: false)]) var deleteMyReviewResponse = EmptyResponseModel() + var getKoreanAddress = "" } @@ -24,6 +25,7 @@ class ExperienceDetailViewModel: ObservableObject { case toggleFavorite(body: FavoriteToggleRequest) case reviewFavorite(id: Int64) case deleteMyReview(id: Int64) + case getKoreanAddress(id: Int64, category: String) } @Published var state: State @@ -92,6 +94,14 @@ class ExperienceDetailViewModel: ObservableObject { } else { print("Error") } + case let .getKoreanAddress(id, category): + let response = await AddressService.getKoreanAddress(id: id, category: category, number: nil) + if response != nil { + print("주소: \(response)") + state.getKoreanAddress = response?.data ?? "" + } else { + print("Error") + } } } diff --git a/NanaLand/NanaLand/Networks/Address/AddressEndPoint.swift b/NanaLand/NanaLand/Networks/Address/AddressEndPoint.swift new file mode 100644 index 0000000..883f857 --- /dev/null +++ b/NanaLand/NanaLand/Networks/Address/AddressEndPoint.swift @@ -0,0 +1,56 @@ +// +// AddressEndPoint.swift +// NanaLand +// +// Created by wodnd on 2/24/25. +// + +import Foundation +import Alamofire + +enum AddressEndPoint { + case getKoreanAddress(id: Int64, category: String, number: Int64?) +} + + +extension AddressEndPoint: EndPoint { + var baseURL: String { + return "\(Secrets.baseUrl)/address" + } + + var path: String { + switch self { + case .getKoreanAddress(let id, let category, let number): + return "/kr" + } + } + + var method: HTTPMethod { + switch self { + case .getKoreanAddress: + return .get + } + } + + var headers: HTTPHeaders? { + switch self { + case .getKoreanAddress: + return ["Content-Type": "application/json;charset=UTF-8"] + } + } + + var task: APITask { + switch self { + case let .getKoreanAddress(id, category, number): + if number != nil { + let param = ["postId": id, "category": category, "number": number] as [String: Any] + return .requestParameters(parameters: param) + } else { + let param = ["postId": id, "category": category] as [String: Any] + return .requestParameters(parameters: param) + } + + } + } +} + diff --git a/NanaLand/NanaLand/Networks/Address/AddressService.swift b/NanaLand/NanaLand/Networks/Address/AddressService.swift new file mode 100644 index 0000000..6f9f1bb --- /dev/null +++ b/NanaLand/NanaLand/Networks/Address/AddressService.swift @@ -0,0 +1,15 @@ +// +// AddressService.swift +// NanaLand +// +// Created by wodnd on 2/24/25. +// + +import Foundation + +struct AddressService { + + static func getKoreanAddress(id: Int64, category: String, number: Int64?) async -> BaseResponse? { + return await NetworkManager.shared.request(AddressEndPoint.getKoreanAddress(id: id, category: category, number: number)) + } +} diff --git a/NanaLand/NanaLand/Restaurant/RestaurantDetailView.swift b/NanaLand/NanaLand/Restaurant/RestaurantDetailView.swift index cea80f9..24bf035 100644 --- a/NanaLand/NanaLand/Restaurant/RestaurantDetailView.swift +++ b/NanaLand/NanaLand/Restaurant/RestaurantDetailView.swift @@ -31,6 +31,8 @@ struct RestaurantDetailView: View { @State private var reviewThumbnailModal = false @State var selectedImageURL: String = ""// 선택된 이미지 URL + @State var koreanAddress: String = "" + var layout: [GridItem] = [GridItem(.flexible())] var body: some View { @@ -267,9 +269,38 @@ struct RestaurantDetailView: View { VStack(alignment: .leading, spacing: 0) { Text(.address) .font(.body02_bold) + Text(viewModel.state.getRestaurantDetailResponse.address) .font(.body02) + .padding(.bottom, Constants.screenWidth * (12 / 360)) + Button { + if localizationManager.language == .korean { + AppState.shared.navigationPath.append(restaurantDetailType.detailMap(title: viewModel.state.getRestaurantDetailResponse.title ,address: "", korean: viewModel.state.getRestaurantDetailResponse.address)) + } else { + AppState.shared.navigationPath.append(restaurantDetailType.detailMap(title: viewModel.state.getRestaurantDetailResponse.title ,address: viewModel.state.getRestaurantDetailResponse.address, korean: koreanAddress)) + } + + } label: { + HStack(spacing: 0){ + Text(.detailView) + .font(.caption01) + .foregroundColor(.gray1) + + Image("icAdressArrow") + .resizable() + .scaledToFit() + .frame(width: Constants.screenWidth * (12 / 360)) + } + .padding(.leading, Constants.screenWidth * (8 / 360)) + .padding(.trailing, Constants.screenWidth * (8 / 360)) + .background(){ + RoundedRectangle(cornerRadius: 100) + .frame(height: Constants.screenWidth * (28 / 360)) + .foregroundColor(.gray3) + } + } + Spacer() } Spacer() @@ -289,6 +320,8 @@ struct RestaurantDetailView: View { VStack(alignment: .leading, spacing: 0) { Text(.phoneNumber) .font(.body02_bold) + .foregroundColor(.black) + Link(destination: URL(string: "tel://\(sanitizedNumber)")!, label: { HStack(spacing: 0) { Text(viewModel.state.getRestaurantDetailResponse.contact!) @@ -756,6 +789,10 @@ struct RestaurantDetailView: View { .onAppear { Task { await getRestaurantDetail(id: id, isSearch: false) + if localizationManager.language != .korean { + await getKoreanAddress(id: id, category: "RESTAURANT") + koreanAddress = viewModel.state.getKoreanAddress + } await getReviewData(id: id, category: "RESTAURANT", page: 0, size: 12) isAPICalled = true } @@ -858,7 +895,7 @@ struct RestaurantDetailView: View { .navigationDestination(for: ReviewType.self) { viewType in switch viewType { case .review: - ReviewWriteMain(reviewAddress: viewModel.state.getRestaurantDetailResponse.address ?? "", reviewImageUrl: viewModel.state.getRestaurantDetailResponse.images?[0].originUrl ?? "", reviewTitle: viewModel.state.getRestaurantDetailResponse.title ?? "", reviewId: viewModel.state.getRestaurantDetailResponse.id ?? 0, reviewCategory: "RESTAURANT") + ReviewWriteMain(reviewAddress: viewModel.state.getRestaurantDetailResponse.address, reviewImageUrl: viewModel.state.getRestaurantDetailResponse.images?[0].originUrl ?? "", reviewTitle: viewModel.state.getRestaurantDetailResponse.title, reviewId: viewModel.state.getRestaurantDetailResponse.id, reviewCategory: "RESTAURANT") case let .userProfile(id): UserProfileMainView(memberId: id) case let .reviewAll(id): @@ -870,6 +907,14 @@ struct RestaurantDetailView: View { .environmentObject(LocalizationManager()) } } + .navigationDestination(for: restaurantDetailType.self) { detailView in + switch detailView { + case let .detailMap(title, address, koreanAddress): + KakaoMapView(title: title, address: address, koreanAddress: koreanAddress) + + } + + } .toolbar(.hidden) } } @@ -899,6 +944,10 @@ struct RestaurantDetailView: View { await viewModel.action(.deleteMyReview(id: id)) } + func getKoreanAddress(id: Int64, category: String) async { + await viewModel.action(.getKoreanAddress(id: id, category: category)) + } + func getSafeArea() ->UIEdgeInsets { return UIApplication.shared.windows.first?.safeAreaInsets ?? UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) } @@ -929,6 +978,10 @@ enum ReviewType: Hashable { case detailReivew(id: Int64, category: String) } +enum restaurantDetailType: Hashable{ + case detailMap(title: String, address: String, korean: String) +} + #Preview { RestaurantDetailView(id: 1) .environmentObject(LocalizationManager()) diff --git a/NanaLand/NanaLand/Restaurant/RestaurantDetailViewModel.swift b/NanaLand/NanaLand/Restaurant/RestaurantDetailViewModel.swift index f3bba98..70249d8 100644 --- a/NanaLand/NanaLand/Restaurant/RestaurantDetailViewModel.swift +++ b/NanaLand/NanaLand/Restaurant/RestaurantDetailViewModel.swift @@ -12,6 +12,7 @@ class RestaurantDetailViewModel: ObservableObject { var getRestaurantDetailResponse = RestaurantDetailModel(id: 1, title: "", content: "", address: "", addressTag: "", contact: "", homepage: "", instagram: "", time: "", service: "", menus: [Menu(menuName: "", price: "", firstImage: RestaurantDetailImagesList(originUrl: "", thumbnailUrl: ""))], keywords: [""], images: [RestaurantDetailImagesList(originUrl: "", thumbnailUrl: "")], favorite: false) var getReviewDataResponse = ReviewModel(totalElements: 0, totalAvgRating: 0.0, data: [ReviewData(id: 0, memberId: 0, nickname: "", profileImage: ImageList(originUrl: "", thumbnailUrl: ""), memberReviewCount: 0, rating: 0, content: "", createdAt: "", heartCount: 0, images: [], reviewTypeKeywords: [], reviewHeart: false, myReview: false)]) var deleteMyReviewResponse = EmptyResponseModel() + var getKoreanAddress = "" } enum Action { @@ -20,6 +21,7 @@ class RestaurantDetailViewModel: ObservableObject { case toggleFavorite(body: FavoriteToggleRequest) case reviewFavorite(id: Int64) case deleteMyReview(id: Int64) + case getKoreanAddress(id: Int64, category: String) } @Published var state: State @@ -93,6 +95,14 @@ class RestaurantDetailViewModel: ObservableObject { } else { print("Error") } + case let .getKoreanAddress(id, category): + let response = await AddressService.getKoreanAddress(id: id, category: category, number: nil) + if response != nil { + print("주소: \(response)") + state.getKoreanAddress = response?.data ?? "" + } else { + print("Error") + } } } } diff --git a/NanaLand/NanaLand/Shop/ShopDetailView.swift b/NanaLand/NanaLand/Shop/ShopDetailView.swift index 73f954a..6334109 100644 --- a/NanaLand/NanaLand/Shop/ShopDetailView.swift +++ b/NanaLand/NanaLand/Shop/ShopDetailView.swift @@ -17,6 +17,9 @@ struct ShopDetailView: View { @State private var shouldScrollToTop = false @State private var thumbnailModal = false @State var selectedImageURL: String = ""// 선택된 이미지 URL + + @State var koreanAddress: String = "" + var id: Int64 var body: some View { @@ -195,12 +198,14 @@ struct ShopDetailView: View { .font(.gothicNeo(.bold, size: 14)) Text(viewModel.state.getShopDetailResponse.address) .font(.body02) + } Spacer() } .frame(width: Constants.screenWidth - 40) + if viewModel.state.getShopDetailResponse.content != "" { let sanitizedNumber = viewModel.state.getShopDetailResponse.contact.replacingOccurrences(of: "-", with: "") HStack(spacing: 10) { @@ -389,6 +394,7 @@ struct ShopDetailView: View { await viewModel.action(.toggleFavorite(body: body)) } + } struct ScrollToTopButton: View { @@ -404,6 +410,8 @@ struct ScrollToTopButton: View { } } } + + //#Preview { // ShopDetailView() //}