스파르타 코딩 클럽 - iOS 스타터 6기/본 캠프
64. 스파르타 코딩 클럽 - 최종 팀 프로젝트 #6
seongpil Heo
2025. 6. 13. 23:14
🎯 Trouble Shooting
[ 1. API 응답 중 googleMapsUri 가 nil ]
postman 응답에는 잘 표시됨
하지만 networkManager를 통해서 네트워크 통신을 하고 받아온 데이터에는 googleMapsUri가 nil로 반환
extension GoogleMap {
struct Place: Decodable {
let displayName: DisplayName? // 가게이름
let primaryTypeDisplayName: PrimaryTypeDisplayName? // 가게 카테고리
let formattedAddress: String // 전체 주소
let location: Location // 위경도
let rating: Double? // 별점
let googleMapsURI: String? // 웹뷰 주소 <- 문제점
let userRatingCount: Int? // 리뷰 수
let photos: [Photo]? // 사진요청 쿼리
let postalAddress: PostalAddress? // 구분된 주소 (사용 안함)
let currentOpeningHours: OpeningHours? // 오픈 시간
}
}
문제는 팀원분이 작성해 주신 NetworkManager 파일의 GoogleMapResponse 모델 파일에 있었다.
googleMaps의 Response의 이름은 googleMapsUri이지만
GoogleMapResponse의 이름은 googleMapsURI로 되어 있어서 해당 키 값을 찾지 못해 값을 받아오지 못하여
nil이 반환되고 있었다.
따라서 아래와 같이 GoogleMapResponse 파일의 코드를 수정하였다.
let googleMapsURI: String?
let googleMapsUri: String?
👨🏻💻 오늘의 작업
[ GoogleMapResponse 파일 수정 - googleMapsUri ]
위의 트러블 슈팅 참고
[ 1. 2개의 키워드로 2번의 네트워크 통신 후 나온 Response를 zip을 사용해서 하나로 병합하기 ]
case .viewDidLoad:
// 네트워크 통신 하고 zip으로 병합
let userLocation = UserDeafaultsManager.shared.readLocation()
let centerLat = userLocation?.lat ?? 37.5177 // 기본값: 강남역
let centerLon = userLocation?.lon ?? 127.0473
let firstRequest = NetworkManager.shared.fetchPlacesWithCircle(textQuery: "스시", centerLat: centerLat, centerLon: centerLon)
.map { self.convertToStoreInfo(places: $0) }
.asObservable()
let secondRequest = NetworkManager.shared.fetchPlacesWithCircle(textQuery: "스테이크", centerLat: centerLat, centerLon: centerLon)
.map { self.convertToStoreInfo(places: $0) }
.asObservable()
// 이미지까지 네트워크 후 킹피셔등 사용해서 섹션 데이터 넘기기
return Observable.zip(firstRequest, secondRequest)
.map { firstRequest, secondRequest in
let mergeStoreInfo = firstRequest + secondRequest
print("첫번째 가게 수 : \(firstRequest.count)")
print("두번째 가게 수 : \(secondRequest.count)")
print("총 표시 가게 수 : \(mergeStoreInfo.count)")
// 별점 순으로 데이터 정렬
let sortedMergeStoreInfo = mergeStoreInfo.sorted { $0.rating > $1.rating}
return [StoreSection(items: sortedMergeStoreInfo)]
}
.map { .setStore($0) }
Reactor 파일에서 mutate 함수 안에 case .viewDidLoad 부분에서
두 번의 네트워크 통신을 통해 데이터를 받고
Observable.zip을 사용해서 하나로 합친 뒤
기본 정렬값인 별점순으로 sort 한 뒤
Mutation을 reduce로 보낸다
[ 2. 병합된 데이터를 테이블 뷰에 표시 ]
func reduce(state: State, mutation: Mutation) -> State {
var newState = state
switch mutation {
case .setStore(let storeInfo):
newState.storeInfo = storeInfo
case .setWebViewUrl(let uri):
newState.webViewUrl = uri
newState.shouldPresentWebView = true
case .sortStore(let storeInfo):
newState.storeInfo = storeInfo
case .dismissWebView:
newState.shouldPresentWebView = false
}
return newState
}
Reactor 파일에서 Mutation으로 받은 State를 newState로 return
func bindState(reactor: DetailReactor) {
// TableView rxDataSource
let dataSource = RxTableViewSectionedAnimatedDataSource<StoreSection>(
configureCell: { dataSource, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier: DetailTableViewCell.identifier, for: indexPath) as! DetailTableViewCell
cell.configureView(with: item)
return cell
}
)
// 테이블 뷰 State 바인딩
reactor.state
.map { $0.storeInfo }
.asDriver(onErrorDriveWith: .empty())
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
DetailViewController 파일에 Reactor에서 받은 State를 사용하여 테이블 뷰의 dataSource로 사용해서
테이블 뷰에 데이터 표시
[ 3. 데이터 변환 ]
{
"openNow": true,
"periods": [
{
"open": { "day": 1, "hour": 9, "minute": 0, "date": { "year": 2025, "month": 6, "day": 13 } },
"close": { "day": 1, "hour": 18, "minute": 0, "date": { "year": 2025, "month": 6, "day": 13 } }
}
]
}
OpeningHours(
openNow: true,
periods: [
OpeningHours.Periods(
open: OpeningHours.Close(
day: 1, hour: 9, minute: 0,
date: OpeningHours.DateClass(year: 2025, month: 6, day: 13)
),
close: OpeningHours.Close(
day: 1, hour: 18, minute: 0,
date: OpeningHours.DateClass(year: 2025, month: 6, day: 13)
)
)
]
)
Google Response로 주는 OpeningHours을 내가 사용하는 OpeningHours 형식으로 데이터 변환