import Foundation import Combine enum GiftDetailRenderState { case sent(SentGiftDetailDisplayModel) case received(ReceivedGiftDetailDisplayModel) case canceled(SentGiftDetailDisplayModel) init?(detail: GiftDetailResponse) { switch GiftDirection(rawValue: detail.direction) { case .sent: guard let model = GiftDisplayPolicy.sentDetail(from: detail) else { return nil } self = model.status == .canceled ? .canceled(model) : .sent(model) case .received: guard let model = GiftDisplayPolicy.receivedDetail(from: detail) else { return nil } self = .received(model) case nil: return nil } } } final class GiftDetailViewModel: ObservableObject { private let applicationNo: String private let onCanceled: (String, @escaping (Bool) -> Void) -> Void private let repository = GiftRepository() private var subscription: AnyCancellable? private var cancelSubscription: AnyCancellable? private var hasCompletedCancellation = false @Published private(set) var renderState: GiftDetailRenderState? @Published private(set) var isLoading = false @Published private(set) var hasLoaded = false @Published private(set) var isShowingCancelDialog = false @Published private(set) var isCancelSubmitting = false @Published private(set) var errorMessage = "" @Published var isShowPopup = false init( applicationNo: String, onCanceled: @escaping (String, @escaping (Bool) -> Void) -> Void = { _, completion in completion(true) } ) { self.applicationNo = applicationNo self.onCanceled = onCanceled } func presentCancelDialog() { guard !isLoading, !isCancelSubmitting, !hasCompletedCancellation, canCancel else { return } errorMessage = "" isShowPopup = false isShowingCancelDialog = true } func dismissCancelDialog() { guard !isCancelSubmitting else { return } isShowingCancelDialog = false errorMessage = "" isShowPopup = false } func confirmCancel() { guard isShowingCancelDialog, !isLoading, !isCancelSubmitting, !hasCompletedCancellation, canCancel else { return } isCancelSubmitting = true errorMessage = "" isShowPopup = false cancelSubscription = repository.cancelGift(applicationNo: applicationNo) .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self, !self.hasCompletedCancellation else { return } if case .failure(let error) = result { self.showCancelError(data: error.response?.data) } } receiveValue: { [weak self] response in guard let self, !self.hasCompletedCancellation else { return } do { let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) guard decoded.success else { self.showCancelError(data: response.data) return } self.hasCompletedCancellation = true self.isShowingCancelDialog = false self.onCanceled(self.applicationNo) { [weak self] didReplace in guard let self, !didReplace else { return } self.isCancelSubmitting = false self.fetch() } } catch { self.showCancelError(data: response.data) } } } func fetch() { guard !isLoading, !isShowingCancelDialog, !isCancelSubmitting else { return } isLoading = true errorMessage = "" isShowPopup = false subscription = repository.getGift(applicationNo: applicationNo) .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self else { return } if case .failure(let error) = result { self.showError(data: error.response?.data) } self.isLoading = false } receiveValue: { [weak self] response in guard let self else { return } do { let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) guard decoded.success, let detail = decoded.data else { self.showError(data: response.data) return } guard let state = GiftDetailRenderState(detail: detail) else { self.showError(data: response.data) return } self.renderState = state self.hasLoaded = true } catch { self.showError(data: response.data) } } } private var canCancel: Bool { guard case .sent(let detail) = renderState else { return false } return detail.status == .received } private func showCancelError(data: Data?) { isCancelSubmitting = false showError(data: data) } private func showError(data: Data?) { struct ErrorResponse: Decodable { let message: String? } let decoded = data.flatMap { try? JSONDecoder().decode(ErrorResponse.self, from: $0) } let message = decoded?.message?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" errorMessage = message.isEmpty ? I18n.Common.commonError : message isShowPopup = true } }