From e75d836b9983f1d452cab29cfd0c3ba63408961a Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Sat, 4 Jul 2026 22:44:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=ED=9B=84=EC=9B=90=20=ED=83=AD?= =?UTF-8?q?=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorChannelDonationViewModel.swift | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift new file mode 100644 index 00000000..682652e1 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationViewModel.swift @@ -0,0 +1,111 @@ +import Foundation +import Combine + +final class CreatorChannelDonationViewModel: ObservableObject { + private let repository = CreatorChannelDonationRepository() + private var subscription = Set() + private let pageSize = 20 + private var latestRequestId = 0 + + @Published var isLoading = false + @Published var isLoadingNextPage = false + @Published var response: CreatorChannelDonationTabResponse? + @Published var rankings = [MemberDonationRankingResponse]() + @Published var donations = [CreatorChannelDonationResponse]() + @Published var page = 0 + @Published var size = 20 + @Published var hasNext = false + @Published var hasLoaded = false + @Published var errorMessage = "" + @Published var isShowPopup = false + + var donationCount: Int { + response?.donationCount ?? 0 + } + + func fetchFirstPage(creatorId: Int) { + fetchDonations(creatorId: creatorId, page: 0, isNextPage: false) + } + + func fetchNextPageIfNeeded(creatorId: Int, currentItem: CreatorChannelDonationResponse) { + guard let lastDonation = donations.last else { return } + guard lastDonation.createdAtUtc == currentItem.createdAtUtc, + lastDonation.nickname == currentItem.nickname, + lastDonation.can == currentItem.can else { return } + guard hasNext, isLoadingNextPage == false, isLoading == false else { return } + + fetchDonations(creatorId: creatorId, page: page + 1, isNextPage: true) + } + + private func fetchDonations(creatorId: Int, page requestPage: Int, isNextPage: Bool) { + latestRequestId += 1 + let requestId = latestRequestId + + if isNextPage { + isLoadingNextPage = true + } else { + isLoading = true + } + + repository.getDonations(creatorId: creatorId, page: requestPage, size: pageSize) + .sink { [weak self] result in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + switch result { + case .finished: + DEBUG_LOG("finish") + self.isLoading = false + self.isLoadingNextPage = false + case .failure(let error): + ERROR_LOG(error.localizedDescription) + self.applyFailureState() + } + } receiveValue: { [weak self] response in + guard let self else { return } + guard requestId == self.latestRequestId else { return } + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) + + if let data = decoded.data, decoded.success { + self.applySuccessState(data, isNextPage: isNextPage) + self.hasLoaded = true + } else { + self.errorMessage = decoded.message ?? I18n.Common.commonError + self.isShowPopup = true + } + } catch { + ERROR_LOG(error.localizedDescription) + self.errorMessage = I18n.Common.commonError + self.isShowPopup = true + } + + self.isLoading = false + self.isLoadingNextPage = false + } + .store(in: &subscription) + } + + private func applySuccessState(_ data: CreatorChannelDonationTabResponse, isNextPage: Bool) { + response = data + rankings = data.rankings + page = data.page + size = data.size + hasNext = data.hasNext + + if isNextPage { + donations.append(contentsOf: data.donations) + } else { + donations = data.donations + } + } + + private func applyFailureState() { + errorMessage = I18n.Common.commonError + isShowPopup = true + isLoading = false + isLoadingNextPage = false + } +}