#!/bin/bash set -euo pipefail root="$(cd "$(dirname "$0")/../.." && pwd)" mode="${1:-privacy}" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT models=( "$root/SodaLive/Sources/V2/Gift/Models/GiftModels.swift" "$root/SodaLive/Sources/V2/Gift/Models/GiftDisplayModels.swift" "$root/SodaLive/Sources/V2/Gift/Models/GiftDisplayPolicy.swift" ) cat > "$tmp/DateParser.swift" <<'EOF' import Foundation enum DateParser { static func parse(_ text: String) -> Date? { nil } } EOF if [[ "$mode" == "privacy" ]]; then swiftc "${models[@]}" "$tmp/DateParser.swift" "$root/work/tests/ReceivedGiftDetailCheck.swift" -o "$tmp/privacy" "$tmp/privacy" cat > "$tmp/Allowed.swift" <<'EOF' @main enum AllowedProbe { static func main() { func detailProbe() -> GiftDetailResponse { fatalError() } let model = GiftDisplayPolicy.receivedDetail(from: detailProbe()) _ = model?.status _ = model?.statusName _ = model?.undeliverableReason _ = model?.senderNickname _ = model?.sizeName _ = model?.categoryName _ = model?.shippingRequestedDateText _ = model?.recipientAddressRequired _ = model?.recipientAddress _ = model?.progress } } EOF swiftc "${models[@]}" "$tmp/DateParser.swift" "$tmp/Allowed.swift" -o "$tmp/allowed" echo "PASS allowed-member probe" for member in senderInfo mailbox applicationNo paidCan tracking; do cat > "$tmp/Forbidden.swift" < GiftDetailResponse { fatalError() } let model = GiftDisplayPolicy.receivedDetail(from: detailProbe()) _ = model?.$member } } EOF if swiftc "${models[@]}" "$tmp/DateParser.swift" "$tmp/Forbidden.swift" -o "$tmp/forbidden" 2> "$tmp/error"; then echo "FAIL forbidden member probe: $member unexpectedly compiled" >&2 exit 1 fi if ! grep -q "has no member" "$tmp/error"; then echo "FAIL forbidden member probe: $member failed for another reason" >&2 cat "$tmp/error" >&2 exit 1 fi echo "PASS forbidden member probe: $member has no member" done exit 0 fi if [[ "$mode" == "foundation" ]]; then component="$root/SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift" if [[ -f "$component" ]]; then if ! xcrun swiftc -parse "$component"; then echo "FAIL foundation: component does not parse" >&2 exit 1 fi if ! grep -Eq 'let[[:space:]]+model:[[:space:]]*ReceivedGiftDetailDisplayModel' "$component"; then echo "FAIL foundation: component does not declare ReceivedGiftDetailDisplayModel model" >&2 exit 1 fi else echo "RED foundation: missing ReceivedGiftDetailContentView" exit 1 fi if ! grep -q 'case \.received(let model)' "$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift" || ! grep -Fq 'ReceivedGiftDetailContentView(model: model)' "$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift"; then echo "FAIL foundation RED: received branch integration probe failed" >&2 exit 1 fi echo "PASS foundation: component and received-branch integration" exit 0 fi if [[ "$mode" == "recipient-form" ]]; then form="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressFormView.swift" view_model="$root/SodaLive/Sources/V2/Gift/RecipientAddress/GiftRecipientAddressViewModel.swift" if [[ ! -f "$form" ]]; then echo "RED recipient-form: missing GiftRecipientAddressFormView.swift" exit 1 fi if [[ ! -f "$view_model" ]]; then echo "RED recipient-form: missing GiftRecipientAddressViewModel.swift" exit 1 fi xcrun swiftc -parse "$form" xcrun swiftc -parse "$view_model" for input in recipientName recipientPhoneNumber recipientZipCode recipientAddress recipientAddressDetail; do if ! grep -Eq "var[[:space:]]+$input[[:space:]]*:[[:space:]]*String" "$view_model"; then echo "FAIL recipient-form: missing String input state $input" >&2 exit 1 fi done for agreement in recipientTermsAgreed recipientPrivacyAgreed; do if ! grep -Eq "var[[:space:]]+$agreement[[:space:]]*:[[:space:]]*Bool" "$view_model"; then echo "FAIL recipient-form: missing Bool agreement state $agreement" >&2 exit 1 fi done if ! grep -Eq 'let[[:space:]]+model:[[:space:]]*ReceivedGiftDetailDisplayModel' "$form"; then echo "FAIL recipient-form: form does not declare ReceivedGiftDetailDisplayModel model" >&2 exit 1 fi detail_view="$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift" if ! grep -q 'case \.received(let model)' "$detail_view" || \ ! grep -Fq 'model.recipientAddressRequired' "$detail_view" || \ ! grep -Fq 'GiftRecipientAddressFormView(model: model, viewModel: recipientAddressViewModel)' "$detail_view" || \ ! grep -Fq 'ReceivedGiftDetailContentView(model: model)' "$detail_view"; then echo "FAIL recipient-form: received branch form/fallback integration probe failed" >&2 exit 1 fi for probe in \ '@ObservedObject var viewModel: GiftRecipientAddressViewModel' \ 'viewModel.submit()' \ 'isActive: viewModel.canSubmit' \ '.disabled(!viewModel.canSubmit)' \ '.disabled(viewModel.isSubmitting || viewModel.hasSubmitted)'; do grep -Fq "$probe" "$form" || { echo "FAIL recipient-form: missing $probe" >&2; exit 1; } done for probe in \ '@StateObject private var recipientAddressViewModel: GiftRecipientAddressViewModel' \ '_viewModel = StateObject(wrappedValue: detailViewModel)' \ 'GiftRecipientAddressViewModel(applicationNo: applicationNo, onSubmitted:' \ 'detailViewModel.fetch()' \ '.sodaToast(isPresented: $recipientAddressViewModel.isShowPopup, message: recipientAddressViewModel.errorMessage)' \ '.sodaToast(isPresented: $recipientAddressViewModel.isShowSuccessToast, message: recipientAddressViewModel.successMessage)' \ '.onChange(of: recipientAddressViewModel.isShowSuccessToast)' \ 'if !isPresented && isActive' \ 'recipientAddressViewModel.successToastDidDismiss()' \ 'isActive = true' \ 'isActive = false' \ 'if recipientAddressViewModel.hasSubmitted && !viewModel.isLoading && !viewModel.errorMessage.isEmpty' \ 'Button(action: viewModel.fetch)' \ 'recipientAddressViewModel.isSubmitting || recipientAddressViewModel.isShowSuccessToast' \ '(recipientAddressViewModel.hasSubmitted && viewModel.isLoading)'; do grep -Fq "$probe" "$detail_view" || { echo "FAIL recipient-form: missing $probe" >&2; exit 1; } done if grep -Fq 'recipientAddressViewModel.submit()' "$detail_view"; then echo "FAIL recipient-form: detail retry must not POST" >&2 exit 1 fi echo "PASS recipient-form: injection, submission, toast lifecycle, GET-only retry and fallback" exit 0 fi if [[ "$mode" == "received-states" ]]; then component="$root/SodaLive/Sources/V2/Gift/Detail/Components/ReceivedGiftDetailContentView.swift" detail="$root/SodaLive/Sources/V2/Gift/Detail/GiftDetailView.swift" xcrun swiftc -parse "$component" "$detail" python3 - "$component" "$detail" <<'PY' import re import sys from pathlib import Path component, detail = (Path(path).read_text() for path in sys.argv[1:]) def require(condition, message): if not condition: sys.exit("FAIL received-states: " + message) print("PASS received-states: " + message) def block(source, marker): require(marker in source, "structure " + marker) start = source.index("{", source.index(marker)) depth = 1 for end in range(start + 1, len(source)): depth += (source[end] == "{") - (source[end] == "}") if depth == 0: return source[start + 1:end], source[end + 1:] sys.exit("FAIL received-states: unbalanced block " + marker) content, _ = block(component, "struct ReceivedGiftDetailContentView: View") title, _ = block(content, "private var heroTitle: String") label, _ = block(content, "private var statusLabel: String?") for state in ("trackingRegistered", "arrivedAtMailbox", "inspectionCompleted", "delivered", "undeliverable"): require(re.search(r"case [^:]*\." + state + r"\b[^:]*:", title), "hero state " + state) for text in ("배송지 입력 완료", "전달 완료", "전달 불가"): require('"' + text + '"' in label, "label " + text) for text in ("배송지 입력이 완료되었어요", "선물을 전달받았어요", "전달할 수 없는 품목입니다"): require('"' + text + '"' in title, "title " + text) for state in ("arrivedAtMailbox", "inspectionCompleted"): require(re.search(r"case [^:]*\." + state + r"\b[^:]*:\s*return model.statusName", title), "backend copy " + state) body, _ = block(content, "var body: some View") stack, _ = block(body, "VStack") require(stack.strip().startswith("hero"), "hero precedes exclusive content branch") unavailable, remainder = block(stack, "if model.status == .undeliverable") require(unavailable.strip() == "undeliverableReason", "undeliverable branch contains only reason/warning") require(remainder.strip().startswith("else {"), "normal content is exclusive else") normal, trailing = block(remainder, "else") require(not trailing.strip(), "no content after exclusive branch") require("GiftProgressView(progress: model.progress)" in normal and "giftInfo" in normal and "recipientAddress" in normal, "normal progress, gift and address") reason, _ = block(content, "private var undeliverableReason: some View") require("model.undeliverableReason?.trimmingCharacters" in reason and "!reason.isEmpty" in reason, "optional nonblank reason") require("해당 선물은 보관기간(입고일 포함 최대 7일) 내에 반송 등 필요한 조치가 완료되지 않을 경우, 사전에 동의한 약관 및 폐기 등의 내용에 따라 폐기될 수 있습니다. 자세한 내용은 고객센터로 문의해 주세요." in reason, "disposal warning") require(not re.search(r"GiftProgressView|giftInfo|recipientAddress|GiftRecipientAddressFormView", unavailable + reason), "no progress/gift/address/form in undeliverable content") require("보내주신 선물은 소다라이브 선물 정책에 따라 크리에이터에게 전달할 수 없는 품목으로 확인되었습니다." in content, "existing policy explanation") address, _ = block(content, "private var recipientAddress: some View") optional, rest = block(address, "if let address = model.recipientAddress") require(not rest.strip(), "nil address renders no section or placeholder") for probe in ('GiftInfoSectionView(title: "받는 주소")', 'GiftInfoRow(title: "이름", value: address.name)', 'GiftInfoRow(title: "휴대폰 번호", value: address.phoneNumber)', 'GiftInfoRow(title: "주소", value: address.address)'): require(probe in optional, "address row " + probe) require(not re.search(r"model\.(senderInfo|mailbox|applicationNo|paidCan|tracking)\b", component), "privacy-safe component") actions, _ = block(component, "struct ReceivedGiftDetailActionBar: View") require("guard !isNavigating else { return }" in actions and "isNavigating = true" in actions and ".disabled(isNavigating)" in actions, "inquiry repeat guard") require("ServiceCenterInquiry.openTalk()" in actions and '"문의하기"' in actions, "shared TALK inquiry action") appear, _ = block(actions, ".onAppear") require("isNavigating = false" in appear, "navigation guard resets") require("let onPolicy: () -> Void" in actions and "onPolicy()" in actions and '"선물 정책 확인"' in actions, "policy callback") require(not re.search(r"https?://|openURL|URL\(", component), "no invented URL") received = detail.split("case .received(let model):", 1)[1].split("case .canceled(let model):", 1)[0] form, fallback = block(received, "if model.recipientAddressRequired") require("GiftRecipientAddressFormView(model: model, viewModel: recipientAddressViewModel)" in form, "form remains required-flag controlled") require(fallback.strip().startswith("else {"), "content remains form fallback") inset, _ = block(fallback, ".safeAreaInset(edge: .bottom, spacing: 0)") only_unavailable, trailing = block(inset, "if model.status == .undeliverable") require(not trailing.strip() and inset.strip().startswith("if model.status == .undeliverable"), "bottom actions only for received undeliverable") require("ReceivedGiftDetailActionBar(onPolicy: openGiftTerms)" in only_unavailable, "received policy action opens gift terms") require(detail.count("ReceivedGiftDetailActionBar") == 1, "no sent/canceled action insertion") sent = detail.split("case .sent(let model):", 1)[1].split("case .received(let model):", 1)[0] require("onPolicy: openGiftTerms" in sent, "sent policy action opens gift terms") require('@Environment(\\.openURL) private var openURL' in detail, "system URL opener") open_terms, _ = block(detail, "private func openGiftTerms()") require("GiftConstants.termsURL" in open_terms and "openURL(url)" in open_terms, "confirmed gift terms URL") require("isShowingPolicyToast" not in detail and "GiftConstants.unavailableActionMessage" not in detail, "policy toast fallback removed") PY exit 0 fi echo "usage: $0 [privacy|foundation|recipient-form|received-states]" >&2 exit 2