diff --git a/Hankkijogbo/Hankkijogbo.xcodeproj/project.pbxproj b/Hankkijogbo/Hankkijogbo.xcodeproj/project.pbxproj index e75204a5..b18a7468 100644 --- a/Hankkijogbo/Hankkijogbo.xcodeproj/project.pbxproj +++ b/Hankkijogbo/Hankkijogbo.xcodeproj/project.pbxproj @@ -163,6 +163,9 @@ A23D11972C47FFB90023480C /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23D11962C47FFB90023480C /* SearchViewModel.swift */; }; A23D119A2C4811790023480C /* HankkiDebouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23D11992C4811790023480C /* HankkiDebouncer.swift */; }; A23D119C2C4844AE0023480C /* PostHankkiValidateRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23D119B2C4844AE0023480C /* PostHankkiValidateRequestDTO.swift */; }; + A23F354F2D2211B200B6F8C8 /* NaverMapAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23F354E2D2211B200B6F8C8 /* NaverMapAPIService.swift */; }; + A23F35512D2211BF00B6F8C8 /* NaverMapTargetType.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23F35502D2211BF00B6F8C8 /* NaverMapTargetType.swift */; }; + A23F35552D22135200B6F8C8 /* GetHankkiAddressResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23F35542D22135200B6F8C8 /* GetHankkiAddressResponseDTO.swift */; }; A240EA082C3EF6E0000FF458 /* BufferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A240EA072C3EF6E0000FF458 /* BufferView.swift */; }; A240EA0C2C3EFD77000FF458 /* CompositionalLayoutFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A240EA0B2C3EFD77000FF458 /* CompositionalLayoutFactory.swift */; }; A240EA192C3F35BC000FF458 /* SearchBarCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A240EA182C3F35BC000FF458 /* SearchBarCollectionViewCell.swift */; }; @@ -400,6 +403,9 @@ A23D11962C47FFB90023480C /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; A23D11992C4811790023480C /* HankkiDebouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HankkiDebouncer.swift; sourceTree = ""; }; A23D119B2C4844AE0023480C /* PostHankkiValidateRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostHankkiValidateRequestDTO.swift; sourceTree = ""; }; + A23F354E2D2211B200B6F8C8 /* NaverMapAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaverMapAPIService.swift; sourceTree = ""; }; + A23F35502D2211BF00B6F8C8 /* NaverMapTargetType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NaverMapTargetType.swift; sourceTree = ""; }; + A23F35542D22135200B6F8C8 /* GetHankkiAddressResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetHankkiAddressResponseDTO.swift; sourceTree = ""; }; A240EA072C3EF6E0000FF458 /* BufferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferView.swift; sourceTree = ""; }; A240EA0B2C3EFD77000FF458 /* CompositionalLayoutFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompositionalLayoutFactory.swift; sourceTree = ""; }; A240EA182C3F35BC000FF458 /* SearchBarCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchBarCollectionViewCell.swift; sourceTree = ""; }; @@ -718,6 +724,7 @@ A2EC33A92C4711A000809840 /* Report */, A24A78792CB44E110049B031 /* Menu */, A2EC33AA2C4711A600809840 /* Location */, + A23F354D2D2211A000B6F8C8 /* NaverMap */, ); path = Network; sourceTree = ""; @@ -1384,6 +1391,32 @@ name = ViewModel; sourceTree = ""; }; + A23F354D2D2211A000B6F8C8 /* NaverMap */ = { + isa = PBXGroup; + children = ( + A23F35522D22134300B6F8C8 /* DTO */, + A23F35502D2211BF00B6F8C8 /* NaverMapTargetType.swift */, + A23F354E2D2211B200B6F8C8 /* NaverMapAPIService.swift */, + ); + path = NaverMap; + sourceTree = ""; + }; + A23F35522D22134300B6F8C8 /* DTO */ = { + isa = PBXGroup; + children = ( + A23F35532D22134900B6F8C8 /* Response */, + ); + path = DTO; + sourceTree = ""; + }; + A23F35532D22134900B6F8C8 /* Response */ = { + isa = PBXGroup; + children = ( + A23F35542D22135200B6F8C8 /* GetHankkiAddressResponseDTO.swift */, + ); + path = Response; + sourceTree = ""; + }; A240EA092C3EF712000FF458 /* HankkiReusableView */ = { isa = PBXGroup; children = ( @@ -2014,6 +2047,7 @@ 86880C2A2C47F2DF00CAEF58 /* HankkiListViewModel.swift in Sources */, 8641516B2C67B9AE00E2FD44 /* MyZipListCollectionViewCell.swift in Sources */, A2C9FCB12C49985800868DF7 /* GetZipListResponseDTO.swift in Sources */, + A23F35552D22135200B6F8C8 /* GetHankkiAddressResponseDTO.swift in Sources */, A240EA442C446AB6000FF458 /* HankkiDetailButton.swift in Sources */, A2AF6FDB2CE61AA000F5271D /* MenuResponseDTO.swift in Sources */, 839138DF2C4962E500611D5C /* GetHankkiThumbnailResponseDTO.swift in Sources */, @@ -2046,11 +2080,13 @@ 86880C0F2C47116900CAEF58 /* DeleteZipToHankkRequestiDTO.swift in Sources */, 83DBED942C2564A20042BA48 /* HomeView.swift in Sources */, 86880BF42C46E89C00CAEF58 /* GetMeUniversityResponseDTO.swift in Sources */, + A23F354F2D2211B200B6F8C8 /* NaverMapAPIService.swift in Sources */, A2FF94112C31660E001ADA03 /* BaseDTO.swift in Sources */, A200C66F2D19EA0D0065C749 /* DetailMapView.swift in Sources */, 837F10DC2C4A851A00E3CCE6 /* ReportCompleteViewController.swift in Sources */, 866DFE0E2D155DC6006EE662 /* CreateZipViewControllerType.swift in Sources */, 865D59C62C7B72C9004CC517 /* FullLoadingView.swift in Sources */, + A23F35512D2211BF00B6F8C8 /* NaverMapTargetType.swift in Sources */, 86B761232C3EC87A00413059 /* ZipListCollectionViewCellModel.swift in Sources */, 83DBEDCA2C256AE70042BA48 /* UIViewController+.swift in Sources */, A23D11972C47FFB90023480C /* SearchViewModel.swift in Sources */, diff --git a/Hankkijogbo/Hankkijogbo/Global/Consts/StringLiterals.swift b/Hankkijogbo/Hankkijogbo/Global/Consts/StringLiterals.swift index d187fe8c..9c2eb773 100644 --- a/Hankkijogbo/Hankkijogbo/Global/Consts/StringLiterals.swift +++ b/Hankkijogbo/Hankkijogbo/Global/Consts/StringLiterals.swift @@ -162,6 +162,7 @@ enum StringLiterals { static let myZip = "내 족보" static let address = "주소" static let copy = "복사" + static let mapLoadErrorMessage = "주소를 불러오지 못했어요" static let copyToastMessage = "주소를 복사했습니다" static let menu = "메뉴" static let editMenu = "메뉴 수정/삭제 제보하기" diff --git a/Hankkijogbo/Hankkijogbo/Global/Extensions/UIViewController+.swift b/Hankkijogbo/Hankkijogbo/Global/Extensions/UIViewController+.swift index b6a0af1a..04ab680d 100644 --- a/Hankkijogbo/Hankkijogbo/Global/Extensions/UIViewController+.swift +++ b/Hankkijogbo/Hankkijogbo/Global/Extensions/UIViewController+.swift @@ -156,4 +156,20 @@ extension UIViewController { self.view.window!.layer.add(transition, forKey: nil) self.dismiss(animated: true, completion: nil) } + + /// 한끼 네비로 세팅한 후 식당 상세로 push + func pushToDetailWithHankkiNavigation(hankkiId: Int) { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController as? HankkiNavigationController { + let type: HankkiNavigationType = HankkiNavigationType(hasBackButton: true, + hasRightButton: false, + mainTitle: .string(""), + rightButton: .string("")) + rootViewController.setupNavigationBar(forType: type) + rootViewController.isNavigationBarHidden = false + + let hankkiDetailViewController = HankkiDetailViewController(viewModel: HankkiDetailViewModel(hankkiId: hankkiId)) + rootViewController.pushViewController(hankkiDetailViewController, animated: true) + } + } } diff --git a/Hankkijogbo/Hankkijogbo/Global/Resources/Config.swift b/Hankkijogbo/Hankkijogbo/Global/Resources/Config.swift index 06664167..c571abe5 100644 --- a/Hankkijogbo/Hankkijogbo/Global/Resources/Config.swift +++ b/Hankkijogbo/Hankkijogbo/Global/Resources/Config.swift @@ -15,6 +15,8 @@ enum Config { static let baseURL = "BASE_URL" static let NMFClientId = "NMFClientId" static let Amplitude = "Amplitude" + static let reverseGeocodingClientId = "ReverseGeocodingClientId" + static let reverseGeocodingClientSecret = "ReverseGeocodingClientSecret" static let Kakao = "Kakao" static let DefaultHankkiImageURL = "DefaultHankkiImageURL" } @@ -43,6 +45,20 @@ extension Config { return key }() + static let ReverseGeocodingClientId: String = { + guard let key = Config.infoDictionary[Keys.Plist.reverseGeocodingClientId] as? String else { + fatalError("ReverseGeocodingClientID is not set in plist for this configuration.") + } + return key + }() + + static let ReverseGeocodingClientSecret: String = { + guard let key = Config.infoDictionary[Keys.Plist.reverseGeocodingClientSecret] as? String else { + fatalError("ReverseGeocodingClientSecret is not set in plist for this configuration.") + } + return key + }() + static let Amplitude: String = { guard let key = Config.infoDictionary[Keys.Plist.Amplitude] as? String else { fatalError("Amplitude is not set in plist for this configuration.") diff --git a/Hankkijogbo/Hankkijogbo/Global/Supporting Files/Info.plist b/Hankkijogbo/Hankkijogbo/Global/Supporting Files/Info.plist index 3d2d9a47..dac5eb34 100644 --- a/Hankkijogbo/Hankkijogbo/Global/Supporting Files/Info.plist +++ b/Hankkijogbo/Hankkijogbo/Global/Supporting Files/Info.plist @@ -2,10 +2,14 @@ - - + ReverseGeocodingClientId + $(ReverseGeocodingClientId) + ReverseGeocodingClientSecret + $(ReverseGeocodingClientSecret) Amplitude $(Amplitude) + UIUserInterfaceStyle + Light BASE_URL $(BASE_URL) CFBundleURLTypes diff --git a/Hankkijogbo/Hankkijogbo/Network/Base/BaseTargetType.swift b/Hankkijogbo/Hankkijogbo/Network/Base/BaseTargetType.swift index 9e0e4ff4..bcc512cd 100644 --- a/Hankkijogbo/Hankkijogbo/Network/Base/BaseTargetType.swift +++ b/Hankkijogbo/Hankkijogbo/Network/Base/BaseTargetType.swift @@ -6,7 +6,6 @@ // import Foundation -import UIKit import Moya @@ -25,6 +24,7 @@ enum HeaderType { case loginHeader(accessToken: String) case withdrawHeader(authorizationCode: String) case formdataHeader(multipartData: [MultipartFormData]) + case naverMapHeader(clientId: String, clientSecret: String) } /// 각 API에 따라 공통된 Path 값 (존재하지 않는 경우 빈 String 값) @@ -38,6 +38,7 @@ enum UtilPath: String { case university = "/v1/universities" case location = "/v1/locations" case universityStores = "/v1/university-stores" + case naverMap = "/v2/gc" } protocol BaseTargetType: TargetType { @@ -51,26 +52,33 @@ protocol BaseTargetType: TargetType { extension BaseTargetType { var baseURL: URL { - guard let baseURL = URL(string: URLConstant.baseURL) else { - fatalError("ERROR - BASEURL") + switch utilPath { + case .naverMap: + guard let reverseGeocodingBaseURL = URL(string: URLConstant.reverseGeocodingBaseURL) else { + fatalError("ERROR - NAVER MAP Reverse Geocoding BASEURL") + } + return reverseGeocodingBaseURL + default: + guard let baseURL = URL(string: URLConstant.baseURL) else { + fatalError("ERROR - BASEURL") + } + return baseURL } - return baseURL } var headers: [String: String]? { var header: [String: String] = [:] switch headerType { + case .loginHeader(let accessToken): header["Content-Type"] = "application/json" header["Authorization"] = "\(accessToken)" - return header case .refreshTokenHeader: header["Content-Type"] = "application/json" let refreshToken = UserDefaults.standard.getRefreshToken() header["Authorization"] = URLConstant.bearer + "\(refreshToken)" - return header // 이후부터는 access token이 헤더에 필요합니다. case .withdrawHeader(let authorizationCode): @@ -80,6 +88,10 @@ extension BaseTargetType { case .formdataHeader: header["Content-Type"] = "multipart/form-data" + case .naverMapHeader(let clientId, let clientSecret): + header["x-ncp-apigw-api-key-id"] = clientId + header["x-ncp-apigw-api-key"] = clientSecret + default: header["Content-Type"] = "application/json" } diff --git a/Hankkijogbo/Hankkijogbo/Network/Base/NetworkService.swift b/Hankkijogbo/Hankkijogbo/Network/Base/NetworkService.swift index ac1df0ae..bf011d4e 100644 --- a/Hankkijogbo/Hankkijogbo/Network/Base/NetworkService.swift +++ b/Hankkijogbo/Hankkijogbo/Network/Base/NetworkService.swift @@ -21,6 +21,7 @@ final class NetworkService { let locationService: LocationAPIServiceProtocol = LocationAPIService() let zipService: ZipAPIServiceProtocol = ZipAPIService() let reportService: ReportAPIServiceProtocol = ReportAPIService() + let naverMapService: NaverMapAPIServiceProtocol = NaverMapAPIService() } extension NetworkService { diff --git a/Hankkijogbo/Hankkijogbo/Network/Base/URLConstant.swift b/Hankkijogbo/Hankkijogbo/Network/Base/URLConstant.swift index 306c3459..c501bd0e 100644 --- a/Hankkijogbo/Hankkijogbo/Network/Base/URLConstant.swift +++ b/Hankkijogbo/Hankkijogbo/Network/Base/URLConstant.swift @@ -12,6 +12,7 @@ enum URLConstant { // MARK: - Base URL static let baseURL = Config.baseURL + static let reverseGeocodingBaseURL = "https://naveropenapi.apigw.ntruss.com/map-reversegeocode" // MARK: - URL Path diff --git a/Hankkijogbo/Hankkijogbo/Network/Hankki/DTO/Response/GetHankkiDetailResponseDTO.swift b/Hankkijogbo/Hankkijogbo/Network/Hankki/DTO/Response/GetHankkiDetailResponseDTO.swift index 5b238c68..0d991903 100644 --- a/Hankkijogbo/Hankkijogbo/Network/Hankki/DTO/Response/GetHankkiDetailResponseDTO.swift +++ b/Hankkijogbo/Hankkijogbo/Network/Hankki/DTO/Response/GetHankkiDetailResponseDTO.swift @@ -15,6 +15,9 @@ struct GetHankkiDetailResponseData: Codable { let isLiked: Bool let imageUrls: [String] let menus: [MenuData] + let latitude: Double + let longitude: Double + let categoryImageUrl: String } struct MenuData: Codable { diff --git a/Hankkijogbo/Hankkijogbo/Network/NaverMap/DTO/Response/GetHankkiAddressResponseDTO.swift b/Hankkijogbo/Hankkijogbo/Network/NaverMap/DTO/Response/GetHankkiAddressResponseDTO.swift new file mode 100644 index 00000000..a4ad2f4a --- /dev/null +++ b/Hankkijogbo/Hankkijogbo/Network/NaverMap/DTO/Response/GetHankkiAddressResponseDTO.swift @@ -0,0 +1,44 @@ +// +// GetHankkiAddressResponseDTO.swift +// Hankkijogbo +// +// Created by 서은수 on 12/30/24. +// + +import Foundation + +// MARK: - Naver Reverse Geocoding API Res + +struct ReverseGeocodingBaseDTO: Decodable { + let code: Int + let name: String + let message: String +} + +struct GetHankkiAddressResponseDTO: Decodable { + let status: ReverseGeocodingBaseDTO + let results: [GetHankkiAddressResult?] +} + +struct GetHankkiAddressResult: Decodable { + let region: Region? + let land: Land? +} + +struct Region: Decodable { + let area1: Area1? + let area2, area3, area4: Area? +} + +struct Area1: Decodable { + let name, alias: String? +} + +struct Area: Decodable { + let name: String? +} + +struct Land: Decodable { + let name: String? + let number1, number2: String? +} diff --git a/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapAPIService.swift b/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapAPIService.swift new file mode 100644 index 00000000..67efc618 --- /dev/null +++ b/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapAPIService.swift @@ -0,0 +1,38 @@ +// +// NaverMapAPIService.swift +// Hankkijogbo +// +// Created by 서은수 on 12/30/24. +// + +import Foundation + +import Moya + +protocol NaverMapAPIServiceProtocol { + func getHankkiAddress(latitude: Double, longitude: Double, completion: @escaping(NetworkResult) -> Void) +} + +final class NaverMapAPIService: BaseAPIService, NaverMapAPIServiceProtocol { + + private let provider = MoyaProvider(plugins: [MoyaPlugin.shared]) + + func getHankkiAddress( + latitude: Double, + longitude: Double, + completion: @escaping (NetworkResult) -> Void) { + provider.request(.getHankkiAddress(latitude: latitude, longitude: longitude)) { result in + switch result { + case .success(let response): + let networkResult: NetworkResult = self.fetchNetworkResult(statusCode: response.statusCode, data: response.data) + print(networkResult) + completion(networkResult) + case .failure(let error): + if let response = error.response { + let networkResult: NetworkResult = self.fetchNetworkResult(statusCode: response.statusCode, data: response.data) + completion(networkResult) + } + } + } + } +} diff --git a/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapTargetType.swift b/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapTargetType.swift new file mode 100644 index 00000000..b7787840 --- /dev/null +++ b/Hankkijogbo/Hankkijogbo/Network/NaverMap/NaverMapTargetType.swift @@ -0,0 +1,65 @@ +// +// NaverMapTargetType.swift +// Hankkijogbo +// +// Created by 서은수 on 12/30/24. +// + +import Foundation + +import Moya + +enum NaverMapTargetType { + + case getHankkiAddress(latitude: Double, longitude: Double) +} + +extension NaverMapTargetType: BaseTargetType { + + var loadingViewType: LoadingViewType { + switch self { + case .getHankkiAddress: return .fullView + } + } + + var headerType: HeaderType { + return .naverMapHeader(clientId: Config.ReverseGeocodingClientId, clientSecret: Config.ReverseGeocodingClientSecret) + } + + var utilPath: UtilPath { return .naverMap } + + var pathParameter: String? { + switch self { + case .getHankkiAddress: + return .none + } + } + + var queryParameter: [String: Any]? { + switch self { + case .getHankkiAddress(let latitude, let longitude): + return .some([ + "coords": "\(longitude),\(latitude)", + "orders": "roadaddr", // 도로명주소 + "output": "json" + ]) + } + } + + var requestBodyParameter: Codable? { + return .none + } + + var path: String { + switch self { + case .getHankkiAddress: + return utilPath.rawValue + } + } + + var method: Moya.Method { + switch self { + case .getHankkiAddress: .get + } + } +} diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/Cell/HankkiMenuCollectionViewCell.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/Cell/HankkiMenuCollectionViewCell.swift index fa53bf31..e2e0a0f5 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/Cell/HankkiMenuCollectionViewCell.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/Cell/HankkiMenuCollectionViewCell.swift @@ -9,7 +9,6 @@ import UIKit final class HankkiMenuCollectionViewCell: BaseCollectionViewCell { - // MARK: - Properties // MARK: - UI Components private var nameLabel: UILabel = UILabel() diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/DetailMapView.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/DetailMapView.swift index ff6d2745..9402de89 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/DetailMapView.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/DetailMapView.swift @@ -11,7 +11,6 @@ import NMapsMap final class DetailMapView: BaseView { - // MARK: - Properties // MARK: - UI Components private let mapView: NMFMapView = NMFMapView() @@ -20,6 +19,9 @@ final class DetailMapView: BaseView { private let addressLabel: UILabel = UILabel() private let copyButton: UIButton = UIButton() + private let mapErrorView: UIView = UIView() + private let mapErrorLabel: UILabel = UILabel() + // MARK: - Init override init(frame: CGRect) { @@ -59,12 +61,7 @@ final class DetailMapView: BaseView { addressGuideLabel.snp.makeConstraints { $0.leading.equalToSuperview().inset(10) - $0.centerY.equalToSuperview() - } - - addressLabel.snp.makeConstraints { - $0.leading.equalTo(addressGuideLabel.snp.trailing).offset(8) - $0.centerY.equalTo(addressGuideLabel) + $0.top.equalToSuperview().inset(11.5) } copyButton.snp.makeConstraints { @@ -73,6 +70,12 @@ final class DetailMapView: BaseView { $0.width.equalTo(36) $0.height.equalTo(25) } + + addressLabel.snp.makeConstraints { + $0.leading.equalTo(addressGuideLabel.snp.trailing).offset(8) + $0.trailing.equalTo(copyButton.snp.leading).offset(-6) + $0.centerY.equalToSuperview() + } } override func setupStyle() { @@ -81,10 +84,11 @@ final class DetailMapView: BaseView { } mapView.do { + $0.zoomLevel = 16 $0.clipsToBounds = true - $0.layer.cornerRadius = 12 + $0.layer.cornerRadius = 8 $0.layer.maskedCorners = CACornerMask(arrayLiteral: .layerMinXMinYCorner, .layerMaxXMinYCorner) - $0.makeRoundBorder(cornerRadius: 12, borderWidth: 1, borderColor: .imageLine) + $0.makeRoundBorder(cornerRadius: 8, borderWidth: 1, borderColor: .imageLine) } addressView.do { @@ -103,13 +107,12 @@ final class DetailMapView: BaseView { ) } - // TODO: - 위경도 값으로 주소 불러와야 함 addressLabel.do { $0.attributedText = UILabel.setupAttributedText( for: PretendardStyle.caption4, - withText: "경기도 수원시 아주대학교 어쩌구", color: .gray700 ) + $0.numberOfLines = 2 } copyButton.do { @@ -124,6 +127,36 @@ final class DetailMapView: BaseView { $0.setAttributedTitle(attributedTitle, for: .normal) } } + + mapErrorView.do { + $0.backgroundColor = .gray100 + $0.makeRoundCorners(corners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], radius: 8) + $0.makeRoundBorder(cornerRadius: 8, borderWidth: 1, borderColor: .imageLine) + } + + mapErrorLabel.do { + $0.attributedText = UILabel.setupAttributedText( + for: PretendardStyle.caption4, + withText: StringLiterals.HankkiDetail.mapLoadErrorMessage, + color: .gray400 + ) + } + } +} + +extension DetailMapView { + + func bindData(latitude: Double, longitude: Double, address: String) { + addressLabel.text = address + + updateAddressViewLayout() + addMapMarker(latitude: latitude, longitude: longitude) + moveMapCamera(latitude: latitude, longitude: longitude) + } + + func handleMapLoadError() { + showMapErrorView() + disableCopyButton() } } @@ -135,10 +168,64 @@ private extension DetailMapView { copyButton.addTarget(self, action: #selector(copyButtonDidTap), for: .touchUpInside) } + func updateAddressViewLayout() { + let maxSize: CGSize = CGSize(width: addressLabel.bounds.width, height: CGFloat.greatestFiniteMagnitude) + let expectedSize: CGSize = addressLabel.sizeThatFits(maxSize) + let spacing: CGFloat = expectedSize.height > 18 ? 8 : 11.5 // 줄 수를 기준으로 + + addressView.snp.updateConstraints { + $0.top.equalTo(mapView.snp.bottom).offset(-1) + $0.leading.trailing.equalTo(mapView) + $0.height.equalTo(spacing + expectedSize.height + spacing) + } + + addressGuideLabel.snp.updateConstraints { + $0.leading.equalToSuperview().inset(10) + $0.top.equalToSuperview().inset(spacing) + } + } + func copyAddressToClipboard() { UIPasteboard.general.string = addressLabel.text } + func addMapMarker(latitude: Double, longitude: Double) { + let marker = NMFMarker() + marker.position = NMGLatLng(lat: latitude, lng: longitude) + marker.iconImage = NMFOverlayImage(image: .icPin) + marker.mapView = mapView + } + + func moveMapCamera(latitude: Double, longitude: Double) { + let cameraUpdate = NMFCameraUpdate(scrollTo: NMGLatLng(lat: latitude, lng: longitude)) + mapView.moveCamera(cameraUpdate) + } + + func showMapErrorView() { + addSubview(mapErrorView) + mapErrorView.addSubview(mapErrorLabel) + + mapErrorView.snp.makeConstraints { + $0.edges.equalTo(mapView) + } + + mapErrorLabel.snp.makeConstraints { + $0.center.equalToSuperview() + } + } + + func disableCopyButton() { + copyButton.isEnabled = false + + if let attributedTitle = UILabel.setupAttributedText( + for: PretendardStyle.caption5, + withText: StringLiterals.HankkiDetail.copy, + color: .gray300 + ) { + copyButton.setAttributedTitle(attributedTitle, for: .normal) + } + } + // MARK: - @objc Func @objc func copyButtonDidTap() { diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiDetailViewController.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiDetailViewController.swift index 3de61652..1279418e 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiDetailViewController.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiDetailViewController.swift @@ -100,8 +100,8 @@ final class HankkiDetailViewController: BaseViewController, NetworkResultDelegat } backButton.snp.makeConstraints { - $0.top.equalToSuperview().inset(48.5) - $0.leading.equalToSuperview().inset(7) + $0.top.equalToSuperview().inset(57) + $0.leading.equalToSuperview().inset(8) $0.size.equalTo(40) } @@ -113,7 +113,6 @@ final class HankkiDetailViewController: BaseViewController, NetworkResultDelegat hankkiInfoView.snp.makeConstraints { $0.top.equalTo(thumbnailImageView.snp.bottom) $0.leading.trailing.equalToSuperview() - $0.height.equalTo(116) } detailMapView.snp.makeConstraints { @@ -165,13 +164,17 @@ private extension HankkiDetailViewController { hankkiInfoView.bindData( category: data.category, + categoryImageUrl: data.categoryImageUrl, name: data.name, heartCount: String(data.heartCount), isLiked: data.isLiked ) - // map view bind data 예정 + detailMapView.bindData( + latitude: data.latitude, + longitude: data.longitude, + address: viewModel.address ?? "-" + ) - menuCollectionView.updateLayout(menuSize: data.menus.count) menuCollectionView.collectionView.reloadData() } } @@ -182,8 +185,16 @@ private extension HankkiDetailViewController { primaryButtonText: StringLiterals.Alert.check) } - viewModel.dismiss = { - self.navigationController?.popViewController(animated: false) + viewModel.dismiss = { [weak self] in + self?.navigationController?.popViewController(animated: false) + } + + viewModel.handleDeletedHankki = { [weak self] in + self?.showBlackToast(message: StringLiterals.Toast.deleteAlready) + } + + viewModel.handleMapLoadError = { [weak self] in + self?.detailMapView.handleMapLoadError() } } @@ -370,10 +381,31 @@ extension HankkiDetailViewController: UICollectionViewDataSource, UICollectionVi func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HankkiMenuCollectionViewCell.className, for: indexPath) as? HankkiMenuCollectionViewCell else { return UICollectionViewCell() } + if let data = viewModel.hankkiDetailData { cell.bindData(data.menus[indexPath.item]) + menuCollectionView.updateLayout() return cell } return UICollectionViewCell() } } + +extension HankkiDetailViewController: UICollectionViewDelegateFlowLayout { + + // 메뉴명 label 길이에 따라 다르게 셀 크기 지정 + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + if let data = viewModel.hankkiDetailData { + let menuNameLabelSize = NSString(string: data.menus[indexPath.item].name) + .boundingRect( + with: CGSize(width: UIScreen.getDeviceWidth() - 44, height: CGFloat.greatestFiniteMagnitude), + options: .usesLineFragmentOrigin, + attributes: [NSAttributedString.Key.font: UIFont.setupPretendardStyle(of: .body8)], + context: nil + ) + return CGSize(width: UIScreen.getDeviceWidth(), height: menuNameLabelSize.height + 25) + } + + return .zero + } +} diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiInfoView.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiInfoView.swift index cc5d4858..7d4b98a2 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiInfoView.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiInfoView.swift @@ -45,7 +45,7 @@ final class HankkiInfoView: BaseView { nameLabel.snp.makeConstraints { $0.top.equalTo(categoryImageView.snp.bottom).offset(2) - $0.leading.equalToSuperview().inset(22) + $0.leading.trailing.equalToSuperview().inset(22) } heartButton.snp.makeConstraints { @@ -71,15 +71,12 @@ final class HankkiInfoView: BaseView { $0.backgroundColor = .hankkiWhite } - categoryImageView.do { - $0.image = .icHomeSelected - } - categoryLabel.do { $0.attributedText = UILabel.setupAttributedText(for: PretendardStyle.caption4, color: .gray900) } nameLabel.do { + $0.numberOfLines = 2 $0.attributedText = UILabel.setupAttributedText(for: PretendardStyle.h3, color: .gray900) } @@ -112,8 +109,9 @@ final class HankkiInfoView: BaseView { extension HankkiInfoView { - func bindData(category: String, name: String, heartCount: String, isLiked: Bool) { + func bindData(category: String, categoryImageUrl: String, name: String, heartCount: String, isLiked: Bool) { categoryLabel.text = category + categoryImageView.setKFImage(url: categoryImageUrl) nameLabel.text = name if let attributedTitle = UILabel.setupAttributedText( for: PretendardStyle.body8, diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiMenuCollectionView.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiMenuCollectionView.swift index e5cda280..25ecc0b0 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiMenuCollectionView.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/View/HankkiMenuCollectionView.swift @@ -12,7 +12,7 @@ final class HankkiMenuCollectionView: BaseView { // MARK: - UI Components private let flowLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout() - lazy var collectionView: UICollectionView = UICollectionView(frame: .init(x: 0, y: 0, width: UIScreen.getDeviceWidth(), height: 179), collectionViewLayout: flowLayout) + lazy var collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) // MARK: - Life Cycle @@ -20,9 +20,16 @@ final class HankkiMenuCollectionView: BaseView { addSubview(collectionView) } + override func setupLayout() { + collectionView.snp.makeConstraints { + $0.edges.equalToSuperview() + $0.width.equalTo(UIScreen.getDeviceWidth()) + $0.height.equalTo(179) + } + } + override func setupStyle() { flowLayout.do { - $0.itemSize = .init(width: UIScreen.getDeviceWidth(), height: 45) $0.scrollDirection = .vertical $0.minimumLineSpacing = 24 $0.headerReferenceSize = .init(width: UIScreen.getDeviceWidth(), height: 20 + 26) @@ -39,14 +46,11 @@ final class HankkiMenuCollectionView: BaseView { extension HankkiMenuCollectionView { - /// 메뉴 데이터 불러온 이후에 메뉴 개수에 따라 높이 동적으로 설정 - func updateLayout(menuSize: Int) { - collectionView.snp.removeConstraints() - collectionView.snp.makeConstraints { + func updateLayout() { + collectionView.snp.updateConstraints { $0.edges.equalToSuperview() $0.width.equalTo(UIScreen.getDeviceWidth()) - $0.height.equalTo(20 + 26 + 18 + (menuSize * (45 + 24)) + 48 + 18) + $0.height.equalTo(collectionView.contentSize.height) } - layoutIfNeeded() } } diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/ViewModel/HankkiDetailViewModel.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/ViewModel/HankkiDetailViewModel.swift index b3d306d1..1d9bfba2 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/ViewModel/HankkiDetailViewModel.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiDetail/ViewModel/HankkiDetailViewModel.swift @@ -8,7 +8,6 @@ import Foundation import Moya -import UIKit final class HankkiDetailViewModel { @@ -18,16 +17,24 @@ final class HankkiDetailViewModel { setHankkiDetailData?() } } + var address: String? { + didSet { + setHankkiDetailData?() + } + } var removeOptions: [String] = [ StringLiterals.RemoveHankki.optionDisappeared, StringLiterals.RemoveHankki.optionNoMore8000, StringLiterals.RemoveHankki.optionImproperHankki ] + weak var delegate: NetworkResultDelegate? var setHankkiDetailData: (() -> Void)? var showAlert: ((String) -> Void)? var dismiss: (() -> Void)? + var handleMapLoadError: (() -> Void)? + var handleDeletedHankki: (() -> Void)? init(hankkiId: Int) { self.hankkiId = hankkiId @@ -36,24 +43,22 @@ final class HankkiDetailViewModel { extension HankkiDetailViewModel { - /// 식당 세부 조회 func getHankkiDetailAPI() { NetworkService.shared.hankkiService.getHankkiDetail(id: hankkiId) { [weak self] result in guard let self = self else { return } switch result { case .notFound: - UIApplication.showBlackToast(message: StringLiterals.Toast.deleteAlready) - self.dismiss?() + handleDeletedHankki?() + dismiss?() default: result.handleNetworkResult(delegate: self.delegate) { response in self.hankkiDetailData = response.data + self.getHankkiAddressAPI() // 상세 조회 후 주소도 같이 불러옴 } } - } } - /// 식당 좋아요 추가 func postHankkiHeartAPI() { NetworkService.shared.hankkiService.postHankkiHeart(id: hankkiId) { result in result.handleNetworkResult { _ in @@ -63,7 +68,6 @@ extension HankkiDetailViewModel { } } - /// 식당 좋아요 삭제 func deleteHankkiHeartAPI() { NetworkService.shared.hankkiService.deleteHankkiHeart(id: hankkiId) { result in result.handleNetworkResult { _ in @@ -72,10 +76,49 @@ extension HankkiDetailViewModel { } } - /// 식당 삭제 func deleteHankkiAPI(completion: @escaping () -> Void) { NetworkService.shared.hankkiService.deleteHankki(id: hankkiId) { result in result.handleNetworkResult(onSuccessVoid: completion) } } } + +private extension HankkiDetailViewModel { + + func getHankkiAddressAPI() { + guard let detailData = hankkiDetailData else { return } + NetworkService.shared.naverMapService.getHankkiAddress(latitude: detailData.latitude, longitude: detailData.longitude) { result in + switch result { + case .success: + result.handleNetworkResult { response in + guard let data = response.results.first, + let data = data else { + self.handleMapLoadError?() + return + } + + self.address = self.formatAddress(from: data) + } + default: + self.handleMapLoadError?() + } + } + } + + func formatAddress(from data: GetHankkiAddressResult) -> String { + let address: [String?] = [ + data.region?.area1?.name, + data.region?.area2?.name, + data.region?.area3?.name, + data.region?.area4?.name, + data.land?.name, + data.land?.number1, + data.land?.number2 + ] + + return address + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: " ") + } +} diff --git a/Hankkijogbo/Hankkijogbo/Present/HankkiList/View/HankkiListViewController.swift b/Hankkijogbo/Hankkijogbo/Present/HankkiList/View/HankkiListViewController.swift index 770aaafc..8b0ad0cd 100644 --- a/Hankkijogbo/Hankkijogbo/Present/HankkiList/View/HankkiListViewController.swift +++ b/Hankkijogbo/Hankkijogbo/Present/HankkiList/View/HankkiListViewController.swift @@ -112,8 +112,7 @@ extension HankkiListViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let hankkiId = viewModel.hankkiList[indexPath.item].id - let hankkiDetailViewController = HankkiDetailViewController(viewModel: HankkiDetailViewModel(hankkiId: hankkiId)) - navigationController?.pushViewController(hankkiDetailViewController, animated: true) + pushToDetailWithHankkiNavigation(hankkiId: hankkiId) } } diff --git a/Hankkijogbo/Hankkijogbo/Present/MyZipList/View/MyZipListBottomSheetViewController.swift b/Hankkijogbo/Hankkijogbo/Present/MyZipList/View/MyZipListBottomSheetViewController.swift index b297e7b1..a0910ee2 100644 --- a/Hankkijogbo/Hankkijogbo/Present/MyZipList/View/MyZipListBottomSheetViewController.swift +++ b/Hankkijogbo/Hankkijogbo/Present/MyZipList/View/MyZipListBottomSheetViewController.swift @@ -207,7 +207,6 @@ private extension MyZipListBottomSheetViewController { } func setupDelegate() { - myZipCollectionView.delegate = self myZipCollectionView.dataSource = self } @@ -358,15 +357,3 @@ extension MyZipListBottomSheetViewController: UICollectionViewDataSource { return cell } } - -// MARK: - UICollectionViewDelegate - -extension MyZipListBottomSheetViewController: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if let data = viewModel.myZipListFavoriteData { - let hankkiId = data[indexPath.item].id - let hankkiDetailViewController = HankkiDetailViewController(viewModel: HankkiDetailViewModel(hankkiId: hankkiId)) - navigationController?.pushViewController(hankkiDetailViewController, animated: true) - } - } -} diff --git a/Hankkijogbo/Hankkijogbo/Present/Report/Complete/ReportCompleteViewController.swift b/Hankkijogbo/Hankkijogbo/Present/Report/Complete/ReportCompleteViewController.swift index 24e0442b..99349ab7 100644 --- a/Hankkijogbo/Hankkijogbo/Present/Report/Complete/ReportCompleteViewController.swift +++ b/Hankkijogbo/Hankkijogbo/Present/Report/Complete/ReportCompleteViewController.swift @@ -236,8 +236,7 @@ private extension ReportCompleteViewController { // MARK: - @objc Func @objc func bottomButtonPrimaryHandler() { - let hankkiDetailViewController = HankkiDetailViewController(viewModel: HankkiDetailViewModel(hankkiId: hankkiId)) - navigationController?.pushViewController(hankkiDetailViewController, animated: true) + pushToDetailWithHankkiNavigation(hankkiId: hankkiId) } @objc func addToMyZipListButtonDidTap() { diff --git a/Hankkijogbo/Hankkijogbo/Present/Report/Search/View/SearchViewController.swift b/Hankkijogbo/Hankkijogbo/Present/Report/Search/View/SearchViewController.swift index 1479e23c..c67f69bf 100644 --- a/Hankkijogbo/Hankkijogbo/Present/Report/Search/View/SearchViewController.swift +++ b/Hankkijogbo/Hankkijogbo/Present/Report/Search/View/SearchViewController.swift @@ -275,8 +275,7 @@ private extension SearchViewController { func pushToHankkiDetail() { guard let hankkiId = viewModel.storeId else { return } - let hankkiDetailViewController = HankkiDetailViewController(viewModel: HankkiDetailViewModel(hankkiId: hankkiId)) - navigationController?.pushViewController(hankkiDetailViewController, animated: true) + pushToDetailWithHankkiNavigation(hankkiId: hankkiId) } }