294 lines
13 KiB
Swift
294 lines
13 KiB
Swift
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선물 보내기 규정에 따라 선물 취소 및 사용한 캔이 모두 환불됩니다."
|
|
}
|
|
}
|