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 } } }