Screen Blueprints — Legal
Status: Active Last reviewed: 2026-09-11
Current-state blueprints for the legal screens, read from the components. Conventions and the screen index live in screen-blueprint.md. History lives in git and rebuild-log.md — not here.
Contact & FAQ (문의하기)
- Route:
/legal/contact - Screen:
ContactScreen—packages/features/profile/src/contact-screen.tsx - Shell: DetailShell (via
ContentPage,ground="canvas") · titlet().common.contact· headerRight: none - Design: unreviewed
- Verified: 2026-09-17
Purpose. Gives the user whatever contact channels are configured (email always; KakaoTalk/phone once their env var validates) and a small expandable FAQ — the one legal screen with real (if light) interactivity beyond expand/collapse.
Anatomy. ContentPage (shared shell: DetailShell ground="canvas" + one YStack text column, SEAM.rail horizontal padding / SEAM.chrome top / SEAM.tail bottom / SEAM.section gap — common to all four legal screens; flat detail-page migration, 2026-09-14, retired the boxed Card per row).
├─ [DetailShell] title=common.contact onBack
└─ [ContentPage] → [YStack]
├─ [Text] subtitle (contactSubtitle)
├─ [ListRow] × supportChannels().length — kakao → phone → email order, each:
│ MessageCircle/Phone/Mail icon + title=legalScreens[channel.label] sub=address/number (kakao: none)
│ → openExternalUrl(channel.href) (@twomore/app, lib/open-external-url.ts)
└─ [SectionBlock variant="flat" title=contactFaqTitle]
└─ [RowList dividers] of ×5 [FaqRow] → [Pressable variant="row" affordance="none"] — question + Chevron(Up/Down), ▸ answer text when expandedStates. FAQ items are mutually exclusive expand/collapse (single expandedFaq id, tapping the open one collapses it); contact rows are unconditional except KakaoTalk/phone, which render only once EXPO_PUBLIC_SUPPORT_KAKAO_CHANNEL_URL/EXPO_PUBLIC_SUPPORT_PHONE validate (EXT-15, docs/external-blockers.md — neither is set today, so only the email row renders); no loading/error state — content is fully static from t().legalScreens.
Actions.
- Tap a contact row →
openExternalUrl(channel.href)(mailto:/tel:/the configuredhttps://pf.kakao.com/...URL); failure toastscommon.linkOpenFailedinstead of a silent no-op. - Tap a FAQ row → toggle its expansion.
- back.
Gaps. none found.
Announcements (공지사항)
- Route:
/legal/notices - Screen:
AnnouncementsScreen—packages/features/profile/src/announcements-screen.tsx - Shell: DetailShell (via
ContentPage,ground="canvas") · titlet().common.announcements· headerRight: none - Design: unreviewed
- Verified: 2026-09-17
Purpose. Shows a static, i18n-sourced list of announcement items as expandable rows; no backing table or query.
Anatomy.
├─ [DetailShell] title=common.announcements onBack
└─ [ContentPage] → [YStack]
├─ ▸ [EmptyState] icon=Megaphone title=announcementsNone variant="full" (when t().legalScreens.announcementItems is empty)
└─ [RowList dividers] of ×N [AnnouncementRow] → [Pressable variant="row" affordance="layer"]
├─ title + date (XStack, space-between)
└─ expanded: body paragraphs (YStack) · collapsed: static teaser text (announcementsEmpty)States. empty (zero items → full EmptyState) vs populated; each populated item independently collapsed (teaser line) or expanded (full body), one open at a time via a single local expanded id.
Actions. tap a row → toggle expand/collapse; back.
Gaps.
- The announcements list is
t().legalScreens.announcementItemsand is deliberately EMPTY — there is no posting workflow; a real notice ships as copy in an OTA. Building a posted-announcements table is an owner product decision, not a defect.
Notes. 2026-09-17: row hairline is the shared Divider (PD-05 sweep) — pixel-identical.
Privacy Policy (개인정보 처리방침)
- Route:
/legal/privacy - Screen:
PrivacyScreen—packages/features/profile/src/privacy-screen.tsx(rendersPRIVACY_DOCUMENT_KRvia the sharedPolicyDocumentPage) - Shell: DetailShell (via
ContentPage→PolicyDocumentPage,ground="canvas") · titlestrings.privacyTitle· headerRight: none - Design: unreviewed
- Verified: 2026-09-14
Purpose. Full PIPA §30 privacy policy (11 hardcoded articles + a §28-8 cross-border transfer table) plus a real PIPA §35-2 data-export action.
Anatomy. Rendered through the shared PolicyDocumentPage (also used by TermsScreen); flat detail-page migration, 2026-09-14, retired every per-section Card box in favor of SectionBlock/RowList/FactList.
├─ [DetailShell] title=privacyTitle onBack
└─ [ContentPage] → [YStack]
├─ [MetaSegments tier="caption"] segments=[effectiveDateFact, versionFact] (no middle-dot glue)
├─ of 11 document sections
│ ├─ [TextSection] → [SectionBlock variant="flat" title=heading] → body [Text] (제1,2,3,4,7,8,9,10,11조)
│ └─ [TableSection] → [SectionBlock variant="flat" title=heading] → [RowList dividers] of [TableSectionRow] → [FactList] column/value pairs (제5조 위탁 table, 제6조 국외이전 table)
├─ [SectionBlock variant="flat" title=versionHistoryTitle] → [RowList dividers] of [VersionHistoryRow] → [FactList] (버전/시행일) + note [Text] (`PRIVACY_DOCUMENT_KR.history`, 1 entry: KR-1.0.0)
└─ [SectionBlock variant="flat" title=dataExportTitle] → [YStack]
├─ [Text] subtitle
├─ ▸ [Text] error (color="$error")
└─ [ActionButton] action="neutral" 내 데이터 다운로드 / 다운로드 중States. default (static policy text); data-export isPending (button disabled, "다운로드 중" label); data-export error (error Text shown); data-export success (native Share.share sheet opens with the exported JSON — dismissal of that OS sheet is not treated as an error).
Actions. 내 데이터 다운로드 → edgeFunctions.exportUserData() → Share.share({ title, message: json }); back.
Gaps.
- (legal, not code) Copy is a counsel-unreviewed first draft — required before public launch.
- (legal, not code) §1 states the 위치기반서비스사업자 신고번호 as "신고 진행 중" — when the number arrives, editing it is a text change, so it bumps the document version and adds a
policy_versionsrow (the hash gate enforces this).
Terms of Service (이용약관)
- Route:
/legal/terms - Screen:
TermsScreen—packages/features/profile/src/terms-screen.tsx(rendersTERMS_DOCUMENT_KRvia the sharedPolicyDocumentPage) - Shell: DetailShell (via
ContentPage) · titlestrings.termsTitle· headerRight: none - Design: unreviewed
- Verified: 2026-09-13
Purpose. Full Terms of Service (13 hardcoded articles + 부칙): definitions, membership, obligations, a not-yet-applicable payments placeholder, dispute resolution.
Anatomy.
├─ [DetailShell] title=termsTitle
└─ [ContentPage] → [YStack]
├─ [Text] effective-date (strings.terms.effectiveDate)
├─ [YStack] of 13 [Card] (제1조 목적 … 제13조 부칙, heading + body, no table sections here)
└─ [SectionBlock] title=versionHistoryTitle → [Card] version/date/note rows (1 entry: KR-1.0.0)States. single static content state — no loading/error/empty branches; purely rendered text.
Actions. back only (no other interactive affordance on this screen).
Gaps.
- (legal, not code) Copy is a counsel-unreviewed first draft — required before public launch.