feat(gift): 운송장 등록을 추가한다
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GiftCourierPickerView: View {
|
||||
@Binding var selectedCourier: String?
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
Text("택배사")
|
||||
.appFont(.body5)
|
||||
.foregroundColor(.white)
|
||||
Menu {
|
||||
ForEach(GiftConstants.courierCompanyNames, id: \.self) { courier in
|
||||
Button(courier) {
|
||||
selectedCourier = courier
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
HStack(spacing: SodaSpacing.s12) {
|
||||
Text(selectedCourier ?? "택배사 선택")
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
Image(systemName: "chevron.down")
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.appFont(.body3)
|
||||
.foregroundColor(selectedCourier == nil ? .gray500 : .white)
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
|
||||
}
|
||||
.accessibilityLabel("택배사")
|
||||
.accessibilityValue(selectedCourier ?? "택배사 선택")
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GiftTrackingRegistrationView: View {
|
||||
@StateObject private var viewModel: GiftTrackingRegistrationViewModel
|
||||
@State private var hasAppeared = false
|
||||
@State private var isActive = false
|
||||
@State private var isShowingCancelDialog = false
|
||||
@FocusState private var isTrackingNumberFocused: Bool
|
||||
|
||||
init(applicationNo: String) {
|
||||
_viewModel = StateObject(wrappedValue: GiftTrackingRegistrationViewModel(applicationNo: applicationNo, onUnavailable: { unavailableApplicationNo in
|
||||
AppState.shared.replaceCurrentAppStep(
|
||||
step: .giftDetail(applicationNo: unavailableApplicationNo),
|
||||
ifCurrentStep: { step in
|
||||
guard case .giftTrackingRegistration(let currentApplicationNo) = step else { return false }
|
||||
return currentApplicationNo == unavailableApplicationNo
|
||||
}
|
||||
)
|
||||
}, onMutationCompleted: { completedApplicationNo, completion in
|
||||
AppState.shared.replaceCurrentAppStep(
|
||||
step: .giftDetail(applicationNo: completedApplicationNo),
|
||||
ifCurrentStep: { step in
|
||||
guard case .giftTrackingRegistration(let currentApplicationNo) = step else { return false }
|
||||
return currentApplicationNo == completedApplicationNo
|
||||
},
|
||||
completion: completion
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "운송장 등록")
|
||||
.disabled(viewModel.isSubmitting)
|
||||
BaseView(isLoading: .constant(viewModel.isLoading || viewModel.isSubmitting)) {
|
||||
if let summary = viewModel.summary, let mailbox = viewModel.mailbox {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||
summaryCard(summary)
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
sectionHeader("보내실 주소", helper: "아래 주소로 발송한 뒤 운송장 번호를 등록해 주세요")
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
GiftInfoRow(title: "사서함", value: mailbox.name)
|
||||
GiftInfoRow(title: "주소", value: mailbox.address)
|
||||
GiftInfoRow(title: "연락처", value: mailbox.phoneNumber)
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
|
||||
}
|
||||
sectionDivider
|
||||
trackingSection
|
||||
sectionDivider
|
||||
guidanceSection
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s14)
|
||||
.padding(.top, SodaSpacing.s24)
|
||||
.padding(.bottom, SodaSpacing.s32)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
actionBar
|
||||
}
|
||||
} else if !viewModel.isLoading && !viewModel.errorMessage.isEmpty {
|
||||
ScrollView {
|
||||
VStack(spacing: SodaSpacing.s16) {
|
||||
Text(viewModel.errorMessage)
|
||||
.appFont(.body3)
|
||||
.foregroundColor(.gray500)
|
||||
.multilineTextAlignment(.center)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if !viewModel.shouldReturnToDetail {
|
||||
Button(action: viewModel.fetch) {
|
||||
Text("다시 시도")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, SodaSpacing.s16)
|
||||
.frame(minHeight: SodaSpacing.s48)
|
||||
.background(Color.gray800)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(SodaSpacing.s24)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(viewModel.isSubmitting)
|
||||
}
|
||||
.background(Color.black.ignoresSafeArea())
|
||||
.alert("신청 취소", isPresented: $isShowingCancelDialog) {
|
||||
Button("취소하기", role: .destructive) {
|
||||
viewModel.confirmCancel()
|
||||
}
|
||||
.disabled(viewModel.isSubmitting)
|
||||
Button("나가기", role: .cancel) {
|
||||
isShowingCancelDialog = false
|
||||
}
|
||||
} message: {
|
||||
Text(cancelMessage)
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage)
|
||||
.sodaToast(isPresented: $viewModel.isShowSuccessToast, message: viewModel.successMessage)
|
||||
.onChange(of: viewModel.isShowPopup) { isPresented in
|
||||
if !isPresented && isActive {
|
||||
viewModel.errorToastDidDismiss()
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.isShowSuccessToast) { isPresented in
|
||||
if !isPresented && isActive {
|
||||
viewModel.successToastDidDismiss()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
isActive = true
|
||||
viewModel.errorToastDidDismiss()
|
||||
viewModel.successToastDidDismiss()
|
||||
guard !hasAppeared else { return }
|
||||
hasAppeared = true
|
||||
viewModel.fetch()
|
||||
}
|
||||
.onDisappear {
|
||||
isActive = false
|
||||
}
|
||||
}
|
||||
|
||||
private func summaryCard(_ summary: SentGiftDetailDisplayModel) -> some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
Text(summary.applicationNo)
|
||||
.appFont(.caption3)
|
||||
.foregroundColor(.gray500)
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s12) {
|
||||
Circle()
|
||||
.fill(Color.gray800)
|
||||
.frame(width: 42, height: 42)
|
||||
.accessibilityHidden(true)
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s6) {
|
||||
if let nickname = summary.recipientCreatorNickname {
|
||||
Text(nickname)
|
||||
.appFont(size: 14, weight: .semibold)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
Text([summary.sizeName, summary.categoryName, summary.applicationDateText.map { "\($0) 신청" }]
|
||||
.compactMap { $0 }.joined(separator: " · "))
|
||||
.appFont(.caption3)
|
||||
.foregroundColor(.gray500)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
|
||||
}
|
||||
|
||||
private func sectionHeader(_ title: String, helper: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s8) {
|
||||
Text(title)
|
||||
.appFont(.body1)
|
||||
.foregroundColor(.white)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
Text(helper)
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(.gray500)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var trackingSection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
sectionHeader("운송장 정보", helper: "등록 완료 후 수정이 불가능하니 신중히 적어주세요")
|
||||
GiftCourierPickerView(selectedCourier: $viewModel.selectedCourier)
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
Text("운송장 번호")
|
||||
.appFont(.body5)
|
||||
.foregroundColor(.white)
|
||||
TextField("운송장 번호를 입력해 주세요", text: Binding(
|
||||
get: { viewModel.trackingNumber },
|
||||
set: { value in
|
||||
guard GiftTrackingRegistrationViewModel.acceptsTrackingEditing(value) else { return }
|
||||
viewModel.trackingNumber = value
|
||||
}
|
||||
))
|
||||
.appFont(.body3)
|
||||
.foregroundColor(.white)
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.numberPad)
|
||||
.focused($isTrackingNumberFocused)
|
||||
.submitLabel(.done)
|
||||
.onSubmit { isTrackingNumberFocused = false }
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
|
||||
.accessibilityLabel("운송장 번호")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var sectionDivider: some View {
|
||||
Color.gray900
|
||||
.frame(height: 7)
|
||||
.padding(.horizontal, -SodaSpacing.s14)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
private var guidanceSection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
Text("등록 안내")
|
||||
.appFont(.body1)
|
||||
.foregroundColor(.white)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
ForEach(registrationGuidance, id: \.self) { guidance in
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s6) {
|
||||
Text("•").accessibilityHidden(true)
|
||||
Text(guidance)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.appFont(.caption2)
|
||||
.foregroundColor(.gray500)
|
||||
}
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaRadius.r14))
|
||||
}
|
||||
}
|
||||
|
||||
private var registrationGuidance: [String] {
|
||||
[
|
||||
"신청일로부터 3일 이내에 운송장 번호를 등록해 주세요.",
|
||||
"3일 이내 운송장 번호를 등록하지 않은 경우, 선물 보내기가 자동으로 취소되며 사용한 캔이 모두 환불됩니다.",
|
||||
"선물은 반드시 배송비 선불로 발송해 주세요. 착불로 발송된 선물은 수취가 거부됩니다.",
|
||||
"진행 상태는 신청 상세에서 확인할 수 있습니다.",
|
||||
"검수를 통과한 선물만 크리에이터에게 재배송됩니다."
|
||||
]
|
||||
}
|
||||
|
||||
private var actionBar: some View {
|
||||
HStack(spacing: SodaSpacing.s14) {
|
||||
Button {
|
||||
guard viewModel.canCancel else { return }
|
||||
isTrackingNumberFocused = false
|
||||
isShowingCancelDialog = true
|
||||
} label: {
|
||||
Text("신청 취소")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(viewModel.canCancel ? .white : .gray500)
|
||||
.padding(.vertical, SodaSpacing.s16)
|
||||
.frame(maxWidth: .infinity, minHeight: SodaSpacing.s48 + SodaSpacing.s4)
|
||||
.overlay(Capsule().stroke(Color.white.opacity(0.3), lineWidth: 1))
|
||||
}
|
||||
.disabled(!viewModel.canCancel)
|
||||
Button {
|
||||
isTrackingNumberFocused = false
|
||||
viewModel.registerTracking()
|
||||
} label: {
|
||||
Text("등록 완료")
|
||||
.appFont(.body2)
|
||||
.foregroundColor(viewModel.canSubmit ? .white : .gray500)
|
||||
.padding(.vertical, SodaSpacing.s16)
|
||||
.frame(maxWidth: .infinity, minHeight: SodaSpacing.s48 + SodaSpacing.s4)
|
||||
.background(viewModel.canSubmit ? Color.button : Color.gray900)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.disabled(!viewModel.canSubmit)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.black)
|
||||
}
|
||||
|
||||
private var cancelMessage: String {
|
||||
let firstLine: String
|
||||
if let nickname = viewModel.summary?.recipientCreatorNickname?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!nickname.isEmpty {
|
||||
firstLine = "\(nickname)에게 보내는 선물을 취소할까요?"
|
||||
} else {
|
||||
firstLine = "보내는 선물을 취소할까요?"
|
||||
}
|
||||
return firstLine + "\n선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
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<GiftDetailResponse>.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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user