feat(gift): 선물 발송 화면을 추가한다
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import SwiftUI
|
||||
|
||||
struct GiftSendView: View {
|
||||
@StateObject private var viewModel: GiftSendViewModel
|
||||
@State private var hasAppeared = false
|
||||
|
||||
init(recipientMemberId: Int, recipientCreatorNickname: String) {
|
||||
_viewModel = StateObject(wrappedValue: GiftSendViewModel(
|
||||
recipientMemberId: recipientMemberId,
|
||||
recipientCreatorNickname: recipientCreatorNickname,
|
||||
onCreated: { applicationNo in
|
||||
AppState.shared.replaceCurrentAppStep(
|
||||
step: .giftDetail(applicationNo: applicationNo),
|
||||
ifCurrentStep: { step in
|
||||
guard case .giftSend(let memberId, let nickname) = step else { return false }
|
||||
return memberId == recipientMemberId && nickname == recipientCreatorNickname
|
||||
}
|
||||
)
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
DetailNavigationBar(title: "선물 보내기")
|
||||
.frame(height: 54)
|
||||
.allowsHitTesting(!viewModel.isSubmitting)
|
||||
BaseView(isLoading: .constant(viewModel.isLoading || viewModel.isSubmitting)) {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||
GiftSendBannerView()
|
||||
recipientSection
|
||||
sectionDivider
|
||||
sizeSection
|
||||
categorySection
|
||||
sectionDivider
|
||||
GiftSenderFormView(
|
||||
name: $viewModel.senderName,
|
||||
phoneNumber: $viewModel.senderPhoneNumber,
|
||||
zipCode: $viewModel.senderZipCode,
|
||||
address: $viewModel.senderAddress,
|
||||
addressDetail: $viewModel.senderAddressDetail
|
||||
)
|
||||
sectionDivider
|
||||
agreementsSection
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s14)
|
||||
.padding(.vertical, SodaSpacing.s16)
|
||||
}
|
||||
.scrollDismissesKeyboard(.interactively)
|
||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||
Button {
|
||||
viewModel.submitGift()
|
||||
} label: {
|
||||
SelectedButtonView(
|
||||
title: viewModel.selectedSize.map { "\($0.salePriceCan)캔으로 선물 보내기" } ?? "선물 보내기",
|
||||
isActive: viewModel.canSubmit,
|
||||
isSelected: false
|
||||
)
|
||||
}
|
||||
.disabled(!viewModel.canSubmit)
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.black)
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage)
|
||||
}
|
||||
.background(Color.black)
|
||||
.onAppear {
|
||||
guard !hasAppeared else { return }
|
||||
hasAppeared = true
|
||||
viewModel.fetchFormOptions()
|
||||
}
|
||||
}
|
||||
|
||||
private var recipientSection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
sectionTitle("받는 크리에이터")
|
||||
HStack(spacing: SodaSpacing.s12) {
|
||||
Circle().fill(Color.gray800).frame(width: 38, height: 38)
|
||||
.accessibilityHidden(true)
|
||||
Text(viewModel.recipientCreatorNickname)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.gray900)
|
||||
.cornerRadius(SodaRadius.r8)
|
||||
}
|
||||
}
|
||||
|
||||
private var sizeSection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
sectionTitle("선물 사이즈")
|
||||
helper("시작은 부담 없이 작은 크기를 선택해 주세요")
|
||||
ForEach(viewModel.sizes, id: \.sizeCode) { size in
|
||||
Button {
|
||||
viewModel.selectedSize = size
|
||||
} label: {
|
||||
HStack(spacing: SodaSpacing.s12) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s6) {
|
||||
Text(sizeTitle(size.sizeCode))
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
if let description = GiftDisplayPolicy.sizeDescription(for: size.sizeCode) {
|
||||
helper(description)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
VStack(alignment: .trailing, spacing: SodaSpacing.s4) {
|
||||
if size.basePriceCan != size.salePriceCan {
|
||||
Text("\(size.basePriceCan)캔")
|
||||
.strikethrough()
|
||||
.appFont(size: 12, weight: .regular)
|
||||
.foregroundColor(.gray500)
|
||||
}
|
||||
Text("\(size.salePriceCan)캔")
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.soda400)
|
||||
}
|
||||
}
|
||||
.padding(SodaSpacing.s16)
|
||||
.frame(minHeight: 77)
|
||||
.background(Color.gray900)
|
||||
.cornerRadius(SodaRadius.r8)
|
||||
.overlay(RoundedRectangle(cornerRadius: SodaRadius.r8)
|
||||
.stroke(viewModel.selectedSize?.sizeCode == size.sizeCode ? Color.soda400 : .clear, lineWidth: 1))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityAddTraits(viewModel.selectedSize?.sizeCode == size.sizeCode ? .isSelected : [])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var categorySection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
sectionTitle("카테고리")
|
||||
helper("택배 기간에는 제한되는 카테고리의 특별한 점들을 꼭 확인합니다")
|
||||
Menu {
|
||||
ForEach(viewModel.categories, id: \.categoryId) { category in
|
||||
Button(category.name) { viewModel.selectCategory(category) }
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Text(viewModel.selectedCategory?.name ?? "카테고리 선택")
|
||||
.appFont(.body3)
|
||||
Spacer()
|
||||
Image(systemName: "chevron.down")
|
||||
}
|
||||
.foregroundColor(viewModel.selectedCategory == nil ? .gray500 : .white)
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.gray900)
|
||||
.cornerRadius(SodaRadius.r14)
|
||||
}
|
||||
.disabled(viewModel.categories.isEmpty)
|
||||
if viewModel.selectedCategory?.requiresDamageWaiver == true {
|
||||
GiftAgreementRow(title: "파손 및 분실 면책 사항에 동의합니다.", isAgreed: $viewModel.damageWaiverAgreed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var agreementsSection: some View {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s16) {
|
||||
sectionTitle("이용 약관 동의")
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
Text("선물 보내기 신청 전 꼭 확인해 주세요")
|
||||
.appFont(size: 14, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
ForEach(shippingRules, id: \.self) { rule in
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s6) {
|
||||
helper("•")
|
||||
helper(rule)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.background(Color.gray900)
|
||||
.cornerRadius(SodaRadius.r8)
|
||||
GiftAgreementRow(title: "소다라이브 크리에이터 선물 보내기 이용약관에 동의합니다.",
|
||||
isAgreed: $viewModel.senderTermsAgreed)
|
||||
GiftAgreementRow(title: "선물 보내기를 위한 개인정보 수집·이용에 동의합니다.",
|
||||
isAgreed: $viewModel.senderPrivacyAgreed)
|
||||
}
|
||||
}
|
||||
|
||||
private var shippingRules: [String] {
|
||||
[
|
||||
"일반 공산품 중 세 변 합 100cm 이하, 5kg 이하, 내품가 40만원 이하의 소형 선물만 접수합니다.",
|
||||
"신선식품은 접수할 수 없으며, 아크릴·피규어·응원봉 등 파손 위험이 있는 상품은 별도 동의가 필요할 수 있습니다.",
|
||||
"선물 보내기에 필요한 개인정보는 회원 주소록처럼 장기 저장하지 않고 전달 절차에 필요한 기간에만 일시적으로 이용합니다.",
|
||||
"선물은 반드시 배송비 선불로 발송해 주세요. 착불로 발송된 선물은 수취가 거부됩니다."
|
||||
]
|
||||
}
|
||||
|
||||
private var sectionDivider: some View {
|
||||
Color.gray900.frame(height: 7).padding(.horizontal, -SodaSpacing.s14)
|
||||
}
|
||||
|
||||
private func sectionTitle(_ title: String) -> some View {
|
||||
Text(title).appFont(.heading3).foregroundColor(.white)
|
||||
}
|
||||
|
||||
private func helper(_ text: String) -> some View {
|
||||
Text(text).appFont(size: 12, weight: .regular).foregroundColor(.gray500)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
private func sizeTitle(_ code: String) -> String {
|
||||
switch GiftSize(rawValue: code) {
|
||||
case .small: return "소형"
|
||||
case .medium: return "중형"
|
||||
case .large: return "대형"
|
||||
case nil: return code
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user