222 lines
10 KiB
Swift
222 lines
10 KiB
Swift
import SwiftUI
|
|
|
|
struct GiftSendView: View {
|
|
private let recipientCreatorProfileImageUrl: String
|
|
@StateObject private var viewModel: GiftSendViewModel
|
|
@State private var hasAppeared = false
|
|
|
|
init(recipientMemberId: Int, recipientCreatorNickname: String, recipientCreatorProfileImageUrl: String) {
|
|
self.recipientCreatorProfileImageUrl = recipientCreatorProfileImageUrl
|
|
_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) {
|
|
DownsampledKFImage(url: URL(string: recipientCreatorProfileImageUrl), size: CGSize(width: 38, height: 38))
|
|
.clipShape(Circle())
|
|
.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,
|
|
policyURL: GiftConstants.damagePolicyURL)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|