Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ final class AddClimbRecordViewModel: BaseViewModel {
let currentKeywordSubject = CurrentValueSubject<String, Never>("")
let isLastPageSubject = CurrentValueSubject<Bool, Never>(false)
let currentMountainsSubject = CurrentValueSubject<[Mountain], Never>([])
let totalReceivedCountSubject = CurrentValueSubject<Int, Never>(0)

// 새로운 검색
input.searchTrigger
Expand All @@ -70,6 +71,7 @@ final class AddClimbRecordViewModel: BaseViewModel {
currentKeywordSubject.send(keyword)
currentPageSubject.send(1)
isLastPageSubject.send(false)
totalReceivedCountSubject.send(0)
isLoadingSubject.send(true)
})
.flatMap { [weak self] keyword -> AnyPublisher<Result<MountainResponse, Error>, Never> in
Expand All @@ -83,17 +85,26 @@ final class AddClimbRecordViewModel: BaseViewModel {
switch result {
case .success(let response):
let mountains = response.body.items.item.map { $0.toMountain() }
currentMountainsSubject.send(mountains)
searchResultsSubject.send(mountains)

// 마지막 페이지 체크
// API 응답 자체에 중복이 있을 수 있으므로 중복 제거 (순서 유지)
var seenIds = Set<Int>()
let uniqueMountains = mountains.filter { seenIds.insert($0.id).inserted }

currentMountainsSubject.send(uniqueMountains)
searchResultsSubject.send(uniqueMountains)

// 받은 데이터 개수 업데이트 (중복 포함)
totalReceivedCountSubject.send(mountains.count)

// 마지막 페이지 체크 (중복 포함 받은 데이터 개수 기준)
if mountains.count >= response.body.totalCount {
isLastPageSubject.send(true)
}
case .failure(let error):
errorSubject.send(("검색 실패", error.localizedDescription))
currentMountainsSubject.send([])
searchResultsSubject.send([])
totalReceivedCountSubject.send(0)
isLastPageSubject.send(true)
}
}
Expand All @@ -117,13 +128,27 @@ final class AddClimbRecordViewModel: BaseViewModel {
switch result {
case .success(let response):
let newMountains = response.body.items.item.map { $0.toMountain() }
let allMountains = currentMountainsSubject.value + newMountains

// API 응답 자체에 중복이 있을 수 있으므로 먼저 중복 제거 (순서 유지)
var seenIdsInResponse = Set<Int>()
let deduplicatedNewMountains = newMountains.filter { seenIdsInResponse.insert($0.id).inserted }

// 기존 데이터와 비교하여 중복 체크
let existingMountains = currentMountainsSubject.value
let existingIds = Set(existingMountains.map { $0.id })
let uniqueNewMountains = deduplicatedNewMountains.filter { !existingIds.contains($0.id) }

let allMountains = existingMountains + uniqueNewMountains
currentMountainsSubject.send(allMountains)
searchResultsSubject.send(allMountains)
currentPageSubject.send(currentPageSubject.value + 1)

// 마지막 페이지 체크
if allMountains.count >= response.body.totalCount {
// 누적 받은 데이터 개수 업데이트 (API 응답 내 중복 제거 후)
let totalReceived = totalReceivedCountSubject.value + deduplicatedNewMountains.count
totalReceivedCountSubject.send(totalReceived)

// 마지막 페이지 체크 (누적 개수 기준 또는 새 데이터가 없는 경우)
if totalReceived >= response.body.totalCount || uniqueNewMountains.isEmpty {
isLastPageSubject.send(true)
}
case .failure(let error):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ protocol ClimbRecordDetailViewModelDelegate: AnyObject {
func updateReview(id: String, rating: Int, comment: String)
func deleteRecord(id: String)
func updateImages(id: String, images: [String])
func refreshStats()
}

final class ClimbRecordDetailViewModel: BaseViewModel {
Expand Down Expand Up @@ -495,6 +496,7 @@ final class ClimbRecordDetailViewModel: BaseViewModel {
switch result {
case .success:
delegate?.deleteRecord(id: climbRecord.id)
delegate?.refreshStats()
popVCSubject.send(())
case .failure(let error):
errorMesssageSubject.send(("기록 삭제에 실패했습니다", error.localizedDescription))
Expand Down Expand Up @@ -555,15 +557,15 @@ final class ClimbRecordDetailViewModel: BaseViewModel {
.eraseToAnyPublisher()
}
}
.sink { result in
if let result = result {
switch result {
case .success:
NotificationCenter.default.post(name: .climbRecordDidSave, object: nil)
saveCompletedSubject.send(())
case .failure(let error):
errorMesssageSubject.send(("저장 실패", error.localizedDescription))
}
.sink { [weak self] result in
guard let self, let result else { return }
switch result {
case .success:
NotificationCenter.default.post(name: .climbRecordDidSave, object: nil)
delegate?.refreshStats()
saveCompletedSubject.send(())
case .failure(let error):
errorMesssageSubject.send(("저장 실패", error.localizedDescription))
}
}
.store(in: &cancellables)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class ClimbRecordListViewModel: BaseViewModel {
private let reloadDataSubject = PassthroughSubject<Void, Never>()
private lazy var emptyStateTextSubject = CurrentValueSubject<String, Never>(emptyText)
private let recordUpdatedSubject = PassthroughSubject<String, Never>()
private let statSubject = PassthroughSubject<(mountainCount: Int, climbCount: Int, totalHeight: Int), Never>()

init(fetchUseCase: FetchClimbRecordUseCase, toggleBookmarkUseCase: ToggleBookmarkUseCase, fetchRecordImageUseCase: FetchRecordImageUseCase, getRecordStatsUseCase: GetRecordStatsUseCase) {
self.fetchUseCase = fetchUseCase
Expand Down Expand Up @@ -55,7 +56,6 @@ final class ClimbRecordListViewModel: BaseViewModel {
let isOnlyBookmarkedSubject = CurrentValueSubject<Bool, Never>(isOnlyBookmarked)
let searchTextSubject = CurrentValueSubject<String, Never>("")
let errorMessageSubject = PassthroughSubject<(String, String), Never>()
let statSubject = PassthroughSubject<(mountainCount: Int, climbCount: Int, totalHeight: Int), Never>()

// viewDidLoad 시 전체 데이터 불러와서 통계와 일지 모두 업데이트
input.viewDidLoad
Expand Down Expand Up @@ -105,18 +105,11 @@ final class ClimbRecordListViewModel: BaseViewModel {
}
.store(in: &cancellables)

// 검색, 북마크 필터, 새 기록 저장 시 일지만 다시 불러오기
// 검색, 북마크 필터 변경 시 일지 다시 불러오기
let searchText = searchTextSubject.eraseToAnyPublisher()
let isOnlyBookmarked = isOnlyBookmarkedSubject.eraseToAnyPublisher()
let climbRecordDidSave = NotificationCenter.default
.publisher(for: .climbRecordDidSave)
.map { _ in (searchTextSubject.value, isOnlyBookmarkedSubject.value) }
.eraseToAnyPublisher()

Publishers.Merge(
Publishers.CombineLatest(searchText, isOnlyBookmarked),
climbRecordDidSave
)
Publishers.CombineLatest(searchText, isOnlyBookmarked)
.flatMap { [fetchUseCase] keyword, isOnlyBookmarked in
fetchUseCase.execute(keyword: keyword, isOnlyBookmarked: isOnlyBookmarked)
.map { result -> (Result<[ClimbRecord], Error>, String) in
Expand Down Expand Up @@ -147,6 +140,40 @@ final class ClimbRecordListViewModel: BaseViewModel {
}
}
.store(in: &cancellables)

// 새 기록 저장 시 일지 + 통계 갱신
NotificationCenter.default
.publisher(for: .climbRecordDidSave)
.flatMap { [fetchUseCase] _ in
fetchUseCase.execute(keyword: searchTextSubject.value, isOnlyBookmarked: isOnlyBookmarkedSubject.value)
}
.sink { [weak self] result in
guard let self else { return }

switch result {
case .success(let records):
climbRecordList = records
recordImageDatas = [:]
reloadDataSubject.send(())

if records.isEmpty {
if searchTextSubject.value.isEmpty {
emptyStateTextSubject.send(emptyText)
} else {
emptyStateTextSubject.send("검색 결과가 없습니다\n다른 키워드로 검색해보세요")
}
}

// 통계 갱신 (전체 데이터 기준)
self.refreshStats()

case .failure(let error):
errorMessageSubject.send(("기록 불러오기 실패", error.localizedDescription))
climbRecordList = []
reloadDataSubject.send(())
}
}
.store(in: &cancellables)

// 셀 북마크 토글
let bookmarkToggledSubject = PassthroughSubject<Int, Never>()
Expand Down Expand Up @@ -268,4 +295,22 @@ extension ClimbRecordListViewModel: ClimbRecordDetailViewModelDelegate {
recordUpdatedSubject.send(id)
}
}

// 통계 갱신
func refreshStats() {
fetchUseCase.execute(keyword: "", isOnlyBookmarked: false)
.sink { [weak self] result in
guard let self else { return }

switch result {
case .success(let records):
// 전체 데이터로 통계 재계산
let stats = self.getRecordStatsUseCase.execute(records: records)
self.statSubject.send((mountainCount: stats.mountainCount, climbCount: stats.climbCount, totalHeight: stats.totalHeight))
case .failure:
break
}
}
.store(in: &cancellables)
}
}
10 changes: 1 addition & 9 deletions Presentation/Sources/Scenes/MapScene/MapViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ import Combine
import MapKit
import Domain

final class MapViewController: UIViewController, BaseViewController, NetworkStatusObservable {
final class MapViewController: UIViewController, BaseViewController {

var pushInfoVC: ((MountainInfo) -> Void)?

let mainView = MapView()
let viewModel: MapViewModel

var networkStatusBanner: NetworkStatusBannerView?
var networkStatusCancellable: AnyCancellable?

private var cancellables = Set<AnyCancellable>()
private lazy var dataSource = createDataSource()
Expand Down Expand Up @@ -52,7 +49,6 @@ final class MapViewController: UIViewController, BaseViewController, NetworkStat
setupDelegates()
setupMapBoundary()
setupAnnotationViewBuilder()
setupNetworkStatusObserver()
bind()

viewDidLoadSubject.send(())
Expand All @@ -67,10 +63,6 @@ final class MapViewController: UIViewController, BaseViewController, NetworkStat
super.viewWillDisappear(animated)
navigationController?.setNavigationBarHidden(false, animated: animated)
}

deinit {
removeNetworkStatusObserver()
}

func bind() {
let input = MapViewModel.Input(
Expand Down
37 changes: 31 additions & 6 deletions Presentation/Sources/Scenes/MeasureScene/MeasureViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ final class MeasureViewModel: BaseViewModel {
let currentKeywordSubject = CurrentValueSubject<String, Never>("")
let isLastPageSubject = CurrentValueSubject<Bool, Never>(false)
let currentMountainsSubject = CurrentValueSubject<[MountainInfo], Never>([])
let totalReceivedCountSubject = CurrentValueSubject<Int, Never>(0)

let viewDidLoad = input.viewDidLoad
.share()
Expand Down Expand Up @@ -195,6 +196,7 @@ final class MeasureViewModel: BaseViewModel {
currentKeywordSubject.send(keyword)
currentPageSubject.send(1)
isLastPageSubject.send(false)
totalReceivedCountSubject.send(0)
isLoadingSubject.send(true)
})
.flatMap { [weak self] keyword -> AnyPublisher<Result<MountainResponse, Error>, Never> in
Expand All @@ -208,17 +210,26 @@ final class MeasureViewModel: BaseViewModel {
switch result {
case .success(let response):
let mountains = response.body.items.item
currentMountainsSubject.send(mountains)
searchResultsSubject.send(mountains)

// 마지막 페이지 체크
// API 응답 자체에 중복이 있을 수 있으므로 중복 제거 (순서 유지)
var seenIds = Set<Int>()
let uniqueMountains = mountains.filter { seenIds.insert($0.id).inserted }

currentMountainsSubject.send(uniqueMountains)
searchResultsSubject.send(uniqueMountains)

// 받은 데이터 개수 업데이트 (중복 포함)
totalReceivedCountSubject.send(mountains.count)

// 마지막 페이지 체크 (중복 포함 받은 데이터 개수 기준)
if mountains.count >= response.body.totalCount {
isLastPageSubject.send(true)
}
case .failure(let error):
errorMessageSubject.send(("검색 실패", error.localizedDescription))
currentMountainsSubject.send([])
searchResultsSubject.send([])
totalReceivedCountSubject.send(0)
isLastPageSubject.send(true)
}
}
Expand All @@ -242,13 +253,27 @@ final class MeasureViewModel: BaseViewModel {
switch result {
case .success(let response):
let newMountains = response.body.items.item
let allMountains = currentMountainsSubject.value + newMountains

// API 응답 자체에 중복이 있을 수 있으므로 먼저 중복 제거 (순서 유지)
var seenIdsInResponse = Set<Int>()
let deduplicatedNewMountains = newMountains.filter { seenIdsInResponse.insert($0.id).inserted }

// 기존 데이터와 비교하여 중복 체크
let existingMountains = currentMountainsSubject.value
let existingIds = Set(existingMountains.map { $0.id })
let uniqueNewMountains = deduplicatedNewMountains.filter { !existingIds.contains($0.id) }

let allMountains = existingMountains + uniqueNewMountains
currentMountainsSubject.send(allMountains)
searchResultsSubject.send(allMountains)
currentPageSubject.send(currentPageSubject.value + 1)

// 마지막 페이지 체크
if allMountains.count >= response.body.totalCount {
// 누적 받은 데이터 개수 업데이트 (API 응답 내 중복 제거 후)
let totalReceived = totalReceivedCountSubject.value + deduplicatedNewMountains.count
totalReceivedCountSubject.send(totalReceived)

// 마지막 페이지 체크 (누적 개수 기준 또는 새 데이터가 없는 경우)
if totalReceived >= response.body.totalCount || uniqueNewMountains.isEmpty {
isLastPageSubject.send(true)
}
case .failure(let error):
Expand Down
Loading