Skip to content

Commit 87035d6

Browse files
authored
[Feat] 제보하기 화면 구현(2차), 역지오코딩 로직 구현 (#66)
* feat: 위치 정보 fetch 로직 구현 * feat: 제보하기 카테고리 tableView VC구현 * refactor: 코드리뷰 반영 * feat: asset 추가
1 parent f350a75 commit 87035d6

47 files changed

Lines changed: 845 additions & 33 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,13 @@ public struct DataSourceDependencyAssembler: DependencyAssemblerProtocol {
4040
DIContainer.shared.register(type: AppConfigRepositoryProtocol.self) { _ in
4141
return AppConfigRepository()
4242
}
43+
44+
DIContainer.shared.register(type: LocationRepositoryProtocol.self) { _ in
45+
return LocationRepository()
46+
}
47+
48+
DIContainer.shared.register(type: ReportRepositoryProtocol.self) { _ in
49+
return ReportRepository()
50+
}
4351
}
4452
}

Projects/DataSource/Sources/Common/Enum/AppProperties.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ public enum AppProperties {
1515
public static var kakaoNativeKey: String {
1616
Bundle.main.object(forInfoDictionaryKey: "KakaoNativeKey") as? String ?? ""
1717
}
18+
19+
public static var kakaoApiKey: String {
20+
Bundle.main.object(forInfoDictionaryKey: "KakaoAPIKey") as? String ?? ""
21+
}
1822
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
//
2+
// KakaoLocationResponseDTO.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 11/10/25.
6+
//
7+
8+
import Domain
9+
import Foundation
10+
11+
struct KakaoLocationResponseDTO: Codable {
12+
let meta: Meta
13+
let documents: [Document]
14+
15+
struct Meta: Codable {
16+
let totalCount: Int
17+
let pageableCount: Int?
18+
let isEnd: Bool?
19+
20+
enum CodingKeys: String, CodingKey {
21+
case totalCount = "total_count"
22+
case pageableCount = "pageable_count"
23+
case isEnd = "is_end"
24+
}
25+
}
26+
}
27+
28+
// MARK: - Document
29+
struct Document: Codable {
30+
let roadAddress: KakaoRoadAddress?
31+
let address: KakaoAddress?
32+
33+
let addressName: String?
34+
let y: String? // 위도 (latitude)
35+
let x: String? // 경도 (longitude)
36+
let addressType: String?
37+
38+
enum CodingKeys: String, CodingKey {
39+
case addressName = "address_name"
40+
case y, x
41+
case addressType = "address_type"
42+
case address
43+
case roadAddress = "road_address"
44+
}
45+
46+
var latitude: Double? {
47+
if let y, let v = Double(y) { return v }
48+
if let v = address?.latitude { return v }
49+
if let v = roadAddress?.latitude { return v }
50+
return nil
51+
}
52+
53+
var longitude: Double? {
54+
if let x, let v = Double(x) { return v }
55+
if let v = address?.longitude { return v }
56+
if let v = roadAddress?.longitude { return v }
57+
return nil
58+
}
59+
}
60+
61+
// MARK: - KakaoRoadAddress (도로명 주소)
62+
struct KakaoRoadAddress: Codable {
63+
let addressName: String
64+
let region1depthName: String
65+
let region2depthName: String
66+
let region3depthName: String
67+
let roadName: String
68+
let undergroundYn: String?
69+
let mainBuildingNo: String?
70+
let subBuildingNo: String?
71+
let buildingName: String?
72+
let zoneNo: String?
73+
let y: String?
74+
let x: String?
75+
76+
enum CodingKeys: String, CodingKey {
77+
case addressName = "address_name"
78+
case region1depthName = "region_1depth_name"
79+
case region2depthName = "region_2depth_name"
80+
case region3depthName = "region_3depth_name"
81+
case roadName = "road_name"
82+
case undergroundYn = "underground_yn"
83+
case mainBuildingNo = "main_building_no"
84+
case subBuildingNo = "sub_building_no"
85+
case buildingName = "building_name"
86+
case zoneNo = "zone_no"
87+
case y, x
88+
}
89+
90+
var latitude: Double? { y.flatMap(Double.init) }
91+
var longitude: Double? { x.flatMap(Double.init) }
92+
}
93+
94+
// MARK: - Address (지번 주소)
95+
struct KakaoAddress: Codable {
96+
let addressName: String
97+
let region1depthName: String
98+
let region2depthName: String
99+
let region3depthName: String
100+
let region3depthHName: String?
101+
let hCode: String?
102+
let bCode: String?
103+
let mountainYn: String?
104+
let mainAddressNo: String?
105+
let subAddressNo: String?
106+
let x: String?
107+
let y: String?
108+
let zipCode: String?
109+
110+
enum CodingKeys: String, CodingKey {
111+
case addressName = "address_name"
112+
case region1depthName = "region_1depth_name"
113+
case region2depthName = "region_2depth_name"
114+
case region3depthName = "region_3depth_name"
115+
case region3depthHName = "region_3depth_h_name"
116+
case hCode = "h_code"
117+
case bCode = "b_code"
118+
case mountainYn = "mountain_yn"
119+
case mainAddressNo = "main_address_no"
120+
case subAddressNo = "sub_address_no"
121+
case x, y
122+
case zipCode = "zip_code"
123+
}
124+
125+
var latitude: Double? { y.flatMap(Double.init) }
126+
var longitude: Double? { x.flatMap(Double.init) }
127+
}
128+
129+
// MARK: - Mapping
130+
extension KakaoLocationResponseDTO {
131+
func toLocationEntity(fallbackLongitude: Double? = nil, fallbackLatitude: Double? = nil) -> LocationEntity? {
132+
guard let doc = documents.first else { return nil }
133+
let longitude = doc.longitude ?? fallbackLongitude
134+
let latitude = doc.latitude ?? fallbackLatitude
135+
let address = doc.roadAddress?.addressName ?? doc.address?.addressName
136+
137+
return LocationEntity(longitude: longitude ?? 0, latitude: latitude ?? 0, address: address)
138+
}
139+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// LocationEndpoint.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 11/10/25.
6+
//
7+
8+
enum LocationEndpoint {
9+
case fetchAddress(longitude: Double, latitude: Double)
10+
}
11+
12+
extension LocationEndpoint: Endpoint {
13+
var baseURL: String {
14+
switch self {
15+
case .fetchAddress:
16+
"https://dapi.kakao.com/v2"
17+
}
18+
}
19+
20+
var path: String {
21+
switch self {
22+
case .fetchAddress:
23+
return baseURL + "/local/geo/coord2address.json"
24+
}
25+
}
26+
27+
var method: HTTPMethod {
28+
switch self {
29+
case .fetchAddress:
30+
return .get
31+
}
32+
}
33+
34+
var headers: [String : String] {
35+
let headers: [String: String] = [
36+
"Authorization": "KakaoAK \(AppProperties.kakaoApiKey)",
37+
]
38+
return headers
39+
}
40+
41+
var queryParameters: [String : String] {
42+
switch self {
43+
case .fetchAddress(let longitude, let latitude):
44+
["x": "\(longitude)", "y": "\(latitude)"]
45+
}
46+
}
47+
48+
var bodyParameters: [String : Any] {
49+
return [:]
50+
}
51+
52+
var isAuthorized: Bool {
53+
return false
54+
}
55+
}

Projects/DataSource/Sources/NetworkService/NetworkService.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,18 @@ final class NetworkService {
8181
guard !data.isEmpty else { throw NetworkError.emptyData }
8282

8383
do {
84-
let baseResponse = try decoder.decode(BaseResponse<T>.self, from: data)
84+
let bitnagilResponse = try decoder.decode(BaseResponse<T>.self, from: data)
8585

86-
guard let responseDTO = baseResponse.data else { return nil }
86+
guard let responseDTO = bitnagilResponse.data else { return nil }
8787

8888
return responseDTO
8989
} catch {
90-
throw NetworkError.decodingError
90+
do {
91+
let generalResponse = try decoder.decode(T.self, from: data)
92+
return generalResponse
93+
} catch {
94+
throw NetworkError.decodingError
95+
}
9196
}
9297
}
9398
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// LocationRepository.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 11/9/25.
6+
//
7+
8+
import CoreLocation
9+
import Domain
10+
11+
final class LocationRepository: NSObject, LocationRepositoryProtocol {
12+
private let networkService = NetworkService.shared
13+
private let locationManager = CLLocationManager()
14+
private var continuation: CheckedContinuation<LocationEntity?, Never>?
15+
private var authContinuation: CheckedContinuation<CLAuthorizationStatus, Never>?
16+
17+
override init() {
18+
super.init()
19+
20+
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
21+
locationManager.delegate = self
22+
}
23+
24+
func fetchCoordinate() async -> LocationEntity? {
25+
guard CLLocationManager.locationServicesEnabled() else { return nil }
26+
27+
let currentStatus = await requestAuthorizationIfNeeded()
28+
29+
if currentStatus == .authorizedAlways || currentStatus == .authorizedWhenInUse {
30+
return await withCheckedContinuation { continuation in
31+
self.continuation = continuation
32+
locationManager.requestLocation()
33+
}
34+
} else {
35+
return nil
36+
}
37+
}
38+
39+
func fetchAddress(coordinate: LocationEntity) async throws -> LocationEntity? {
40+
let endpoint = LocationEndpoint.fetchAddress(longitude: coordinate.longitude, latitude: coordinate.latitude)
41+
42+
guard let response = try await networkService.request(endpoint: endpoint, type: KakaoLocationResponseDTO.self)
43+
else { return nil }
44+
45+
let location = response.toLocationEntity(fallbackLongitude: coordinate.longitude, fallbackLatitude: coordinate.latitude)
46+
return location
47+
}
48+
49+
private func requestAuthorizationIfNeeded() async -> CLAuthorizationStatus {
50+
let currentStatus = locationManager.authorizationStatus
51+
52+
switch currentStatus {
53+
case .authorizedAlways, .authorizedWhenInUse:
54+
return currentStatus
55+
case .denied, .restricted:
56+
return currentStatus
57+
case .notDetermined:
58+
return await withCheckedContinuation { (continuation: CheckedContinuation<CLAuthorizationStatus, Never>) in
59+
self.authContinuation?.resume(returning: currentStatus)
60+
self.authContinuation = continuation
61+
self.locationManager.requestWhenInUseAuthorization()
62+
}
63+
default:
64+
return currentStatus
65+
}
66+
}
67+
}
68+
69+
extension LocationRepository: CLLocationManagerDelegate {
70+
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
71+
let coordinate = locations.last?.coordinate
72+
let entity = coordinate.map { LocationEntity(longitude: $0.longitude, latitude: $0.latitude, address: nil) }
73+
continuation?.resume(returning: entity)
74+
continuation = nil
75+
}
76+
77+
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
78+
continuation?.resume(returning: nil)
79+
continuation = nil
80+
}
81+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//
2+
// ReportRepository.swift
3+
// DataSource
4+
//
5+
// Created by 이동현 on 11/9/25.
6+
//
7+
8+
import Domain
9+
10+
final class ReportRepository: ReportRepositoryProtocol {
11+
func report(reportEntity: Domain.ReportEntity) async {
12+
13+
}
14+
}

Projects/Domain/Sources/DomainDependencyAssembler.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,14 @@ public struct DomainDependencyAssembler: DependencyAssemblerProtocol {
6262

6363
return RoutineUseCase(routineRepository: routineRepository)
6464
}
65+
66+
DIContainer.shared.register(type: ReportUseCaseProtocol.self) { container in
67+
guard
68+
let locationRepository = container.resolve(type: LocationRepositoryProtocol.self),
69+
let reportRepository = container.resolve(type: ReportRepositoryProtocol.self)
70+
else { fatalError("reportUseCase에 필요한 의존성이 등록되지 않았습니다.") }
71+
72+
return ReportUseCase(locationRepository: locationRepository, reportRepository: reportRepository)
73+
}
6574
}
6675
}

Projects/Domain/Sources/Entity/Enum/ReportType.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//
77

88
public enum ReportType: String, CaseIterable {
9+
case transportation
910
case lamp
10-
case road
11-
case etc
11+
case water
12+
case convenience
1213
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// LocationEntity.swift
3+
// Domain
4+
//
5+
// Created by 이동현 on 11/9/25.
6+
//
7+
8+
public struct LocationEntity {
9+
public let longitude: Double
10+
public let latitude: Double
11+
public let address: String?
12+
13+
public init(
14+
longitude: Double,
15+
latitude: Double,
16+
address: String?
17+
) {
18+
self.longitude = longitude
19+
self.latitude = latitude
20+
self.address = address
21+
}
22+
}

0 commit comments

Comments
 (0)