import Foundation import Combine final class GiftSendViewModel: ObservableObject { let recipientMemberId: Int let recipientCreatorNickname: String private let repository = GiftRepository() private let onCreated: (String) -> Void private var subscription = Set() private var hasLoaded = false @Published private(set) var sizes = [GiftSizeOptionResponse]() @Published private(set) var categories = [GiftCategoryOptionResponse]() @Published var selectedSize: GiftSizeOptionResponse? @Published private(set) var selectedCategory: GiftCategoryOptionResponse? @Published var senderName = "" @Published var senderPhoneNumber = "" @Published var senderZipCode = "" @Published var senderAddress = "" @Published var senderAddressDetail = "" @Published var senderTermsAgreed = false @Published var senderPrivacyAgreed = false @Published var damageWaiverAgreed = false @Published private(set) var isLoading = false @Published private(set) var isSubmitting = false @Published var errorMessage = "" @Published var isShowPopup = false var isRecipientContextValid: Bool { recipientMemberId > 0 && !recipientCreatorNickname.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } var canSubmit: Bool { !isSubmitting && isRecipientContextValid && selectedSize != nil && selectedCategory != nil && GiftContactInputRule.name.acceptsSubmission(senderName) && GiftContactInputRule.phoneNumber.acceptsSubmission(senderPhoneNumber) && [senderZipCode, senderAddress].allSatisfy { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } && senderTermsAgreed && senderPrivacyAgreed && (selectedCategory?.requiresDamageWaiver != true || damageWaiverAgreed) } init(recipientMemberId: Int, recipientCreatorNickname: String, onCreated: @escaping (String) -> Void) { self.recipientMemberId = recipientMemberId self.recipientCreatorNickname = recipientCreatorNickname self.onCreated = onCreated } func selectCategory(_ category: GiftCategoryOptionResponse) { if selectedCategory?.categoryId != category.categoryId || !category.requiresDamageWaiver { damageWaiverAgreed = false } selectedCategory = category } func submitGift() { guard canSubmit, let request = makeRequest() else { return } isSubmitting = true errorMessage = "" isShowPopup = false repository.createGift(request: request) .receive(on: DispatchQueue.main) .sink { [weak self] result in if case .failure(let error) = result { self?.showSubmissionError(data: error.response?.data) } } 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 data = decoded.data else { self.showSubmissionError(data: response.data) return } self.onCreated(data.applicationNo) } catch { self.showSubmissionError(data: response.data) } } .store(in: &subscription) } private func makeRequest() -> CreateGiftRequest? { guard let selectedSize, let selectedCategory else { return nil } return CreateGiftRequest( recipientMemberId: recipientMemberId, senderName: senderName.trimmingCharacters(in: .whitespacesAndNewlines), senderPhoneNumber: senderPhoneNumber.trimmingCharacters(in: .whitespacesAndNewlines), senderZipCode: senderZipCode.trimmingCharacters(in: .whitespacesAndNewlines), senderAddress: senderAddress.trimmingCharacters(in: .whitespacesAndNewlines), senderAddressDetail: senderAddressDetail.trimmingCharacters(in: .whitespacesAndNewlines), sizeCode: selectedSize.sizeCode.trimmingCharacters(in: .whitespacesAndNewlines), categoryId: selectedCategory.categoryId, senderTermsAgreed: senderTermsAgreed, senderPrivacyAgreed: senderPrivacyAgreed, damageWaiverAgreed: selectedCategory.requiresDamageWaiver && damageWaiverAgreed ) } private func showSubmissionError(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 isSubmitting = false isShowPopup = true } func fetchFormOptions() { guard isRecipientContextValid else { errorMessage = I18n.Common.commonError isShowPopup = true return } guard !isLoading, !hasLoaded else { return } isLoading = true errorMessage = "" isShowPopup = false repository.getFormOptions() .receive(on: DispatchQueue.main) .sink { [weak self] result in guard let self else { return } self.isLoading = false if case .failure = result { self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } receiveValue: { [weak self] response in guard let self else { return } do { let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { self.sizes = data.sizes self.categories = data.categories self.selectedSize = data.sizes.first self.selectedCategory = nil self.hasLoaded = true } else { self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true } } catch { self.errorMessage = I18n.Common.commonError self.isShowPopup = true } } .store(in: &subscription) } }