스파르타 코딩 클럽 - 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 형식으로 데이터 변환