import Foundation import Combine final class GiftTrackingRegistrationViewModel: ObservableObject { private let applicationNo: String private let onUnavailable: (String) -> Void private let onMutationCompleted: (String, @escaping (Bool) -> Void) -> Void private let repository = GiftRepository() private var subscription: AnyCancellable? private var mutationSubscription: AnyCancellable? private var hasCompletedMutation = false private var hasStartedMutationNavigation = false @Published private(set) var summary: SentGiftDetailDisplayModel? @Published private(set) var mailbox: GiftAddressDisplayModel? @Published private(set) var isLoading = false @Published private(set) var isSubmitting = false @Published private(set) var errorMessage = "" @Published private(set) var successMessage = "운송장이 등록되었습니다." @Published private(set) var shouldReturnToDetail = false @Published var isShowPopup = false @Published var isShowSuccessToast = false @Published var selectedCourier: String? @Published var trackingNumber = "" static func acceptsTrackingEditing(_ value: String) -> Bool { value.utf8.count <= 14 && value.utf8.allSatisfy { (48...57).contains($0) } } static func acceptsTrackingSubmission(_ value: String) -> Bool { value.utf8.count >= 10 && acceptsTrackingEditing(value) } var canRegister: Bool { mailbox != nil && !isLoading && !shouldReturnToDetail } var canSubmit: Bool { guard let selectedCourier else { return false } return canRegister && !isSubmitting && !hasCompletedMutation && !selectedCourier.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && Self.acceptsTrackingSubmission(trackingNumber) } var canCancel: Bool { canRegister && summary?.status == .received && !isSubmitting && !hasCompletedMutation } init( applicationNo: String, onUnavailable: @escaping (String) -> Void = { _ in } ) { self.applicationNo = applicationNo self.onUnavailable = onUnavailable self.onMutationCompleted = { _, completion in completion(true) } } init( applicationNo: String, onUnavailable: @escaping (String) -> Void, onMutationCompleted: @escaping (String, @escaping (Bool) -> Void) -> Void ) { self.applicationNo = applicationNo self.onUnavailable = onUnavailable self.onMutationCompleted = onMutationCompleted } func fetch() { guard !isLoading, !isSubmitting, !shouldReturnToDetail else { return } summary = nil mailbox = nil errorMessage = "" isShowPopup = false isLoading = true 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 sentDetail = GiftDisplayPolicy.sentDetail(from: detail) else { self.showUnavailable(data: response.data) return } guard sentDetail.status != .canceled else { self.showUnavailable(data: response.data) return } guard detail.trackingRequired else { self.showUnavailable(data: response.data) return } guard let mailbox = detail.mailbox else { self.showMissingMailboxError() return } self.summary = sentDetail self.mailbox = GiftAddressDisplayModel( name: mailbox.name, phoneNumber: mailbox.phoneNumber, address: mailbox.address ) } catch { self.showError(data: response.data) } } } func registerTracking() { guard canSubmit, let selectedCourier else { return } let request = RegisterGiftTrackingRequest( courierCompanyName: selectedCourier, trackingNumber: trackingNumber.trimmingCharacters(in: .whitespacesAndNewlines) ) beginMutation() mutationSubscription = repository.registerTracking(applicationNo: applicationNo, request: request) .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self, !self.hasCompletedMutation else { return } if case .failure(let error) = result { self.finishMutationFailure(data: error.response?.data) } } receiveValue: { [weak self] response in guard let self, !self.hasCompletedMutation else { return } do { let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) guard decoded.success else { self.finishMutationFailure(data: response.data) return } self.finishRegistrationSuccess() } catch { self.finishMutationFailure(data: response.data) } } } func confirmCancel() { guard canCancel else { return } beginMutation() mutationSubscription = repository.cancelGift(applicationNo: applicationNo) .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self, !self.hasCompletedMutation else { return } if case .failure(let error) = result { self.finishMutationFailure(data: error.response?.data) } } receiveValue: { [weak self] response in guard let self, !self.hasCompletedMutation else { return } do { let decoded = try JSONDecoder().decode(ApiResponseWithoutData.self, from: response.data) guard decoded.success else { self.finishMutationFailure(data: response.data) return } self.finishCancellationSuccess() } catch { self.finishMutationFailure(data: response.data) } } } func successToastDidDismiss() { guard hasCompletedMutation, !isShowSuccessToast else { return } navigateAfterMutation() } func errorToastDidDismiss() { guard !isShowPopup, shouldReturnToDetail else { return } shouldReturnToDetail = false onUnavailable(applicationNo) } private func beginMutation() { isSubmitting = true errorMessage = "" isShowPopup = false } private func finishRegistrationSuccess() { hasCompletedMutation = true isShowSuccessToast = true } private func finishCancellationSuccess() { hasCompletedMutation = true navigateAfterMutation() } private func finishMutationFailure(data: Data?) { isSubmitting = false showError(data: data) } private func navigateAfterMutation() { guard hasCompletedMutation, !hasStartedMutationNavigation else { return } hasStartedMutationNavigation = true onMutationCompleted(applicationNo) { [weak self] didReplace in guard let self, !didReplace, self.isSubmitting else { return } self.isSubmitting = false self.fetch() } } private func showUnavailable(data: Data?) { showError(data: data) shouldReturnToDetail = true } private func showMissingMailboxError() { errorMessage = "받을 주소가 등록되어 있지 않습니다." isShowPopup = true } 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 } }