From 2e029e1868b3f9a3bc1f776a8be8136f2c87fb41 Mon Sep 17 00:00:00 2001 From: Riu Date: Tue, 25 Feb 2025 17:10:46 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=EC=9D=B4=EC=83=89=EC=B2=B4=ED=97=98,?= =?UTF-8?q?=20=EC=A0=9C=EC=A3=BC=EB=A7=9B=EC=A7=91=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=EB=A7=B5=20=EC=97=B0=EA=B2=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NanaLand/NanaLand.xcodeproj/project.pbxproj | 53 ++++++ .../xcshareddata/swiftpm/Package.resolved | 11 +- NanaLand/NanaLand/App/NanaLandApp.swift | 6 + .../Icon/icAdressArrow.imageset/Contents.json | 21 +++ .../icAdressArrow.imageset/icAdressArrow.png | Bin 0 -> 383 bytes .../Icon/icCopy.imageset/Contents.json | 21 +++ .../Icon/icCopy.imageset/icCopy.png | Bin 0 -> 366 bytes .../Icon/logoPin.imageset/Contents.json | 21 +++ .../Icon/logoPin.imageset/logoPin.png | Bin 0 -> 14605 bytes .../Component/KakaoMap/KakaoGecoder.swift | 70 ++++++++ .../KakaoMap/KakaoMapController.swift | 141 +++++++++++++++ .../Component/KakaoMap/KakaoMapView.swift | 168 ++++++++++++++++++ .../Common/Localizing/LocalizedKey.swift | 7 + .../Localizing/en.lproj/Localizable.strings | 7 + .../Localizing/ko.lproj/Localizable.strings | 7 + .../Localizing/ms.lproj/Localizable.strings | 7 + .../Localizing/vi.lproj/Localizable.strings | 9 +- .../zh-Hans.lproj/Localizable.strings | 7 + .../Experience/ExperienceDetailView.swift | 58 +++++- .../ExperienceDetailViewModel.swift | 10 ++ .../Networks/Address/AddressEndPoint.swift | 56 ++++++ .../Networks/Address/AddressService.swift | 15 ++ .../Restaurant/RestaurantDetailView.swift | 55 +++++- .../RestaurantDetailViewModel.swift | 10 ++ NanaLand/NanaLand/Shop/ShopDetailView.swift | 8 + 25 files changed, 764 insertions(+), 4 deletions(-) create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/Contents.json create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/icAdressArrow.imageset/icAdressArrow.png create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/Contents.json create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/icCopy.imageset/icCopy.png create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/Contents.json create mode 100644 NanaLand/NanaLand/Assets.xcassets/Icon/logoPin.imageset/logoPin.png create mode 100644 NanaLand/NanaLand/Common/Component/KakaoMap/KakaoGecoder.swift create mode 100644 NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapController.swift create mode 100644 NanaLand/NanaLand/Common/Component/KakaoMap/KakaoMapView.swift create mode 100644 NanaLand/NanaLand/Networks/Address/AddressEndPoint.swift create mode 100644 NanaLand/NanaLand/Networks/Address/AddressService.swift diff --git a/NanaLand/NanaLand.xcodeproj/project.pbxproj b/NanaLand/NanaLand.xcodeproj/project.pbxproj index cbdf1c06..4a383b7b 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 faf3fb59..728aec21 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 e0a41c43..e288bc7e 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 00000000..f817d78c --- /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 0000000000000000000000000000000000000000..ced9451467432a1da5eb27a521e9d0aaab5ab4dd GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBI14-?iy0UcEkKyjb(&!UP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBIT246$B+ufx7QEyHW`SpJ!rRTP~Onk=U`sI zYM#J+n&sRB4!?wcIoU=*iz=pgVWHQqzixf6Hux*dw8!iNt5BSxmgdjRAsdC)ZvCA( zE3d{PBsBHM9(9MXrk%R^(f6|WlLLGkJRJHTNVG*A_bWMfx7YBWsCn^8`?bv1*V&j& z2wLQ5d}?8!(gpp1Bmt?ZEKZ9iIckK6aWtM{%Um4dFiXSq0@JHQ&gW-jdqla1?3f#o zw@**w>K8UWH`yztcRPQ_2v4c{^`<#qc<-N%qdku^nuGLXSFLC?G00NT?cwqep3Hmc zSW2V7j1&Q`$t+GuM-)ulSRT#Z_J`F@Sm3d8rffA+a*bKj&V8BhD&9{Qa=$V^+tV{I ZPQc*y&kY``W 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 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 00000000..42adf442 --- /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 00000000..d55e9d49 --- /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 2821864c..20eb8972 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 2a8e6c56..4d2f6b64 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 b199a189..87bde847 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 998b0b24..741ef750 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 309da051..52a4a2aa 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 9ff4de40..b3c01762 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 d837e419..01a6c677 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 a24fcc0e..329d96eb 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 00000000..883f8571 --- /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 00000000..6f9f1bb0 --- /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 cea80f90..24bf0350 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 f3bba98b..70249d86 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 73f954a7..63341098 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() //}