Screen Blueprints — Venues
Status: Active Last reviewed: 2026-09-11
Current-state blueprints for the venues 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.
Venue Directory Detail
- Route:
/directory-venues/[id] - Screen:
DirectoryVenueDetailScreen—packages/features/sessions/src/directory-venue-detail-screen.tsx(re-export shim) →packages/features/sessions/src/directory-venue-detail/directory-venue-detail-screen.tsx - Shell: DetailShell · title = venue name (dynamic) · header="collapsing" · headerRight:
HeaderActionsMenu(수정 ▸canSuggestEdit/ 소유권 확인 / 신고하기) - Design: ratified
- Verified: 2026-09-16
Purpose. Read-only detail over a platform-ingested nationwide public.venues spine row — distinct from the club-scoped Venue Detail (/venues/[id]) — surfacing per-facility-kind content, trust/sources, and pending corrections.
Anatomy.
├─ [MediaHero kind="map"] title=name titleBadge=[VenueTrustBadge] facts=[🏷 kind · 🏠/🌤 indoor/outdoor
│ · 📍 address · 📏 distance] (capped at 4) onMediaPress=directions
└─ [YStack opacity=TERMINAL_MUTE_OPACITY ▸ isClosed]
└─ [FlatColumn] lead=[ClosedVenueBanner ▸ isClosed] + [StatStrip] (hero facts, non-shop: 코트/종류/
1시간 요금; shop: 결제/평점) + [ActionIconRow] 길찾기 · 전화 (disabled w/o phone) · 저장 · 공유
├─ per-kind content sections in `sectionOrderForKind(facilityKind)` order, filtered to what
│ will render (`visibleVenueDetailSections`): 기본 정보/다른 이름/운영시간/이용료/리뷰 키워드/
│ 평점/편의시설/결제 수단/바로가기/블로그/사진/시설/코트 구성 (13 keys total)
├─ [SourcesSection] ✚ 정보 출처 ▸ trust.total > 0 — [FactList] 출처 N개 + [SourceChips]
├─ [HostConfirmSection] 호스트 확인 (always present) → correction modal
├─ [VenueContributionLinks] (제안 내역 등, venue-scoped)
└─ [PendingReviewSection] 검토 대기 중인 제안 ▸ correctionRows.length > 0
└─ [BottomCtaBand] "이 테니스장에서 일정 만들기" (footer, outside scroll body)States.
- loading →
SkeletonFlatPage kind="map"; offline →OfflineEmptyState; error →QueryErrorState; missing record: venue not found →NotFoundState(common.venueNotFound; 2026-09-16 converted from the genericvenues.noVenueslist-empty copy — the wrong copy for one specific missing venue) - closed venue →
ClosedVenueBanner+TERMINAL_MUTE_OPACITYon the whole body - content varies by facility kind (full_court/practice/screen/shop) via
sectionOrderForKind - pending review present/absent; sources present/absent
Actions.
- 길찾기 / 전화 (disabled without a number) / 저장·저장 취소 / 공유
- header menu: 수정 (suggest correction, gated on
isFieldEditableFor(name|address)) / 소유권 확인 (→routes.newVenueOwnerClaim) / 신고하기 - 재개장 신고 (closed venues only, in the banner)
- per-section edit glyphs (amenities/links); 코트 구성 edit → new courts-kind venue contribution
- approve/reject a pending correction (approvers only)
- footer CTA 이 테니스장에서 일정 만들기
Gaps. none found.
Venue Contribution Detail
- Route:
/venue-contributions/[id] - Screen:
VenueContributionDetailScreen—packages/features/sessions/src/venue-contributions/detail-screen.tsx - Shell: DetailShell · title
제안 접수 내역(t().courts.contributions.receiptTitle) · headerRight: none - Design: unreviewed
- Verified: 2026-09-16
Purpose. Read-only receipt for a single venue contribution submission (fact correction, new venue, courts, lifecycle change, or a report): status, before/after diff, evidence, decision history, and review/withdraw actions for authorized reviewers.
Anatomy.
├─ [DetailShell scroll={false}] > [ScrollView] > [FlatColumn]
├─ sign-in / loading / error gate (plain Text or QueryErrorState) before the receipt loads
├─ header block: venue name (if present), kind title, status [Badge], created timestamp
├─ report-accepted note ▸ kind === 'report' && status === 'accepted'
├─ "제안된 변경" → [ContributionChanges] before/after diff
├─ "증빙 자료" → observed date / note / reference URL (+ button to open it)
├─ "결정 내역" → decision events list, or a "결정 없음" text
├─ optional nav buttons: 이전 제안 보기 (supersedesId) · 대체된 제안 보기 (replacedById) ·
│ 테니스장 보기 (venueId) · 도착지 보기 (relocation destinationVenueId)
├─ 새로고침 (manual refetch)
└─ [ReceiptActions] (BottomCtaBand) — review-decision form ▸ reviewable, destructive 철회 button,
replace/resubmit button — renders only when the viewer can review/withdraw/replaceStates.
- not signed in / loading / error (
QueryErrorState+ retry) - missing record: settled with no receipt row (bad/deleted contributionId) →
NotFoundState(2026-09-16 bug-class fix — this used to fall through to the plain-Text "불러오는 중..." loading copy forever, since onlyisError/paused were checked) - content with/without report-accepted banner; with/without decision events
- footer actions render only when the viewer has any of review/withdraw/replace permission
Actions.
- 참고자료 보기 (open reference URL)
- 이전 제안 보기 / 대체된 제안 보기 / 테니스장 보기 / 도착지 보기
- 새로고침
- review decision — 수락/거절/추가 정보 필요, required explanation, confirm dialog
- 철회 (withdraw, destructive, confirm dialog)
- 다시 제출 (replace/resubmit into a new contribution)
Gaps. none found.
Venue Contributions Inbox
- Route:
/venue-contributions - Screen:
VenueContributionsScreen—packages/features/sessions/src/venue-contributions/list-screen.tsx - Shell: DetailShell · title
장소 정보 제안(t().courts.contributions.title) · headerRight: none - Design: unreviewed
- Verified: 2026-09-13
Purpose. Inbox/list of venue contribution submissions — a viewer's own ('mine') or ones awaiting their review ('inbox') — with entry points into ownership claims and the new-contribution flow.
Anatomy.
├─ [DetailShell scroll={false}]
└─ [FeedList] (paginated, infinite scroll)
├─ header=[FlatColumn]: [ActionButton] "소유권 확인" → routes.venueOwnerClaims, then either a
│ sign-in prompt or a 내 제안/받은 제안 [SelectionChip] pair
├─ item → [ContributionRow]
└─ [BottomCtaBand] "테니스장 정보 추가하기" → routes.newVenueContribution() (footer)States.
- not signed in / loading / error (
QueryErrorState+ retry) / offline (paused fetch,QueryErrorState+ retry) - empty — scope-aware (mine vs inbox) copy
- content — paginated list
Actions.
- scope toggle (내 제안/받은 제안)
- 소유권 확인 link
- row tap → contribution detail
- infinite scroll load-more (
onEndReached) - footer CTA 테니스장 정보 추가하기 → new contribution flow
Gaps. none found.
New Venue Contribution
- Route:
/venue-contributions/new - Screen:
NewVenueContributionScreen—packages/features/sessions/src/venue-contributions/new-screen.tsx - Shell: DetailShell · title = kind-specific (dynamic, e.g.
없는 장소 추가fornew_venue) via the internalEditorGate· headerRight: none - Design: unreviewed
- Verified: 2026-09-13
Purpose. Editor flow for submitting a venue contribution — new venue, facts correction, court-operations edit, lifecycle change, or a report — resolving replacement/context data first, then rendering the kind-specific editor with evidence fields.
Anatomy.
├─ [EditorGate] ([DetailShell] + [FlatColumn]) — hosts every non-ready state
│ (sign-in prompt, loading text, error, "cannot replace", "missing target")
└─ once context/previous resolves → [ContributionEditor]
└─ [DetailShell scroll={false}] > [ScrollView] > [FlatColumn]
├─ context-snapshot hint (venue name + snapshotHint) ▸ replacing an existing target
├─ replacement hint text ▸ supersedesId set
├─ exactly one kind-specific editor: [NewVenueEditor] | [FactsEditor] |
│ [CourtOperationsEditor] | [LifecycleEditor] | [ReportEditor]
├─ [EvidenceFields] observed date / note / reference URL
├─ inline error text
├─ "이전 제안 보기" link ▸ a receipt was just created (lastReceipt)
└─ 제출 안내 delivery-hint text
└─ [BottomCtaBand] submit hint + "제출하기"/"제출 중" [ActionButton] (footer)States.
- not signed in / loading (waiting on previous/context queries) / error (
QueryErrorState+ retry) - cannot-replace (
previous.canReplace === false) / missing-target (no venueId, kind isn'tnew_venue) - content per kind (new_venue/facts/courts/lifecycle/report)
- submit pending / submit error
Actions.
- 제출하기 (submit; on success replaces the route with the new receipt's detail page)
- 이전 제안 보기 (view the previously submitted receipt when replacing)
Gaps. none found.
Venue Owner Claim Detail
- Route:
/venue-owner-claims/[id] - Screen:
VenueOwnerClaimDetailScreen—packages/features/sessions/src/venue-owner-claims/detail-screen.tsx - Shell: DetailShell · title
장소 소유권(t().courts.ownerClaims.receiptTitle) · headerRight: none - Design: unreviewed
- Verified: 2026-09-16
Purpose. Detail/receipt view for a venue ownership claim or transfer — summary, evidence, decision, tenure status, transfer-consent flow, and a paginated event history; permitted hosts/admins can review, withdraw, relinquish, or revoke.
Anatomy.
├─ [OwnerFrame] gating wrapper (invalid-route / session-not-ready states)
└─ [LoadedDetail] → [DetailShell scroll={false}]
├─ blocked/access-denied → [OwnerReadState] with recover-retry
└─ [ReceiptFeed] ([FeedList])
├─ header=[ReceiptDetails]: [ClaimSummary] + "테니스장 보기" button, either the
│ transfer-consent applicant name OR the full evidence/결정/tenure-status blocks +
│ 이전 제안/대체 제안 buttons + [TransferProposals], then an "이력" title
├─ body: [ClaimHistoryRow] events
└─ footer: "더 보기" paginated load-more, or an error retry state
└─ [ClaimActions] (BottomCtaBand) — [ReviewEditor] + "결정 제출"; consent/동의 철회 button
for transfers; destructive 철회 (withdraw); a reason field feeding 반납 (relinquish) /
취소 (revoke, destructive) buttonsStates.
- invalid route (schema validation fails) / session-scope not ready
- blocked / access-denied (with recover-retry) / loading / error (retry)
- missing record: settled with no claim row (bad/deleted claimId) →
NotFoundState(2026-09-16 bug-class fix — this used to fall through to the plain-Text "불러오는 중..." loading copy forever, since onlyisErrorwas checked) - content with full view permissions vs summary-only vs transfer-consent-only view
- Offline cold start (query paused, nothing fetched) →
OfflineEmptyStatewith retry, via an earlyOfflineEmptyStatereturn — never the empty state (2026-09-11 offline sweep).
Actions.
- 테니스장 보기; 더 보기 (load more history)
- 결정 제출 (review: 수락/거절/정보 필요)
- 동의 / 동의 철회 (transfer consent/retract)
- 철회 (withdraw); 반납 (relinquish); 취소 (revoke, destructive)
- navigate to previous/replacement/outgoing-tenure claims
Gaps. none found.
Venue Owner Claims List
- Route:
/venue-owner-claims - Screen:
VenueOwnerClaimsScreen—packages/features/sessions/src/venue-owner-claims/list-screen.tsx - Shell: DetailShell · title
소유권 신청 내역(t().courts.ownerClaims.title) · headerRight: none - Design: unreviewed
- Verified: 2026-09-13
Purpose. List of venue ownership claims — a viewer's own ('mine') or ones awaiting their review ('inbox') — with a CTA to apply for ownership of a venue.
Anatomy.
├─ [OwnerFrame] gating wrapper (invalid-route / session-not-ready states)
└─ [LoadedList] → [DetailShell scroll={false}]
└─ [FeedList] (paginated)
├─ header: 내 제안/받은 제안 [SelectionChip] pair
├─ row: [Pressable] → [ClaimSummary] + "영수증 보기" underline text, [Divider] after each row
└─ [BottomCtaBand] "신청하기" (footer)States.
- invalid route / session not ready
- loading →
OwnerReadState/ error → retry - empty — mine/inbox-specific copy
- content — paginated list
- Offline cold start (query paused, nothing fetched) →
OfflineEmptyStatewith retry, via the list'sisPausedOffline+offlineState— never the empty state (2026-09-11 offline sweep).
Actions.
- scope toggle mine/inbox
- row tap → claim detail
- footer 신청하기 → new venue owner claim
Gaps. none found.
New Venue Owner Claim
- Route:
/venue-owner-claims/new - Screen:
NewVenueOwnerClaimScreen—packages/features/sessions/src/venue-owner-claims/new-screen.tsx - Shell: DetailShell (via
OwnerFrame) · title소유권 신청(t().courts.ownerClaims.newTitle) · headerRight: none - Design: unreviewed
- Verified: 2026-09-13
Purpose. Application flow for claiming ownership (or a transfer) of a venue — picks a target venue if none is specified, then collects evidence and submits, guarding against cannot-replace/ineligible states along the way.
Anatomy.
├─ [OwnerFrame title="소유권 신청"] — wraps every state
├─ invalid-route text, OR [OwnerSessionState] gate, OR [LoadedOwnerEditor]
├─ no target venue picked (and no supersedesId) → [ExistingOwnerVenuePicker]
│ 전체/샵 [SelectionChip] toggle, search [StyledInput], fixed-height [FeedList] of venue rows
│ ([SelectionChip] + address), "정보 추가하기" fallback button
└─ once a target resolves → [ApplicationEditor]
venue name/address, hint paragraph, "테니스장 보기" button, [ClaimEvidenceFields]
(or "blocked" text ▸ ineligible), optional 받은 신청함 보기/기존 신청 보기/기존 소유권 보기
buttons, inline error text
└─ [BottomCtaBand] "제출하기" (footer, only rendered when eligible)States.
- invalid route / session not ready
- venue not yet picked →
ExistingOwnerVenuePicker - cannot-replace / loading / error (retry)
- blocked (not eligible to submit)
- content — submit form
Actions.
- pick a venue (search + tap, in the picker)
- 제출하기 (submit; replaces the route with the new claim's detail page)
- navigate to venue detail / inbox / existing claim / existing tenure
- 정보 추가하기 (add a missing venue, fallback inside the picker)
Gaps. none found.
Venue Detail
- Route:
/venues/[id] - Screen:
VenueDetailScreen—packages/features/sessions/src/venue-detail-screen.tsx - Shell: DetailShell · title = venue name (dynamic) · header="collapsing" · headerRight: none
- Design: ratified
- Verified: 2026-09-16
Purpose. Richer venue overview with booking and facility actions for a club-scoped court_venues row (admin-curated) — distinct from the platform-wide Venue Directory Detail screen.
Anatomy.
├─ [MediaHero kind="map"] title=venue.name facts=[📍 address (fallback: region/district label) ·
│ 📏 distance ▸ both viewer location and venue coords known] onMediaPress=map
├─ [FlatColumn] lead=[StatStrip] 표면 · 코트 · 요금 (each cell skipped when its fact is unknown) +
│ [ActionIconRow] 길찾기 · 지도 · 예약 사이트 (▸ bookingUrl) · 전화 (▸ bookingPhone)
│ ├─ [SectionBlock 이용 요금] [FactList] 평일/주말/야간 추가, or "요금 정보가 없어요" text
│ ├─ [SectionBlock 운영 시간] ▸ hoursLabel — plain text row
│ ├─ [SectionBlock 시설 정보] ▸ facilityItems.length > 0 — [Badge] chip wrap
│ ├─ [SectionBlock 예약 정보] [FactList] 일정(value=[MetaSegments] day+time when both present, no
│ │ middle-dot glue)/전화/링크/메모, or "예약 정보가 없어요" text
│ └─ [SectionBlock 메모] ▸ notes — paragraph
└─ [BottomCtaBand] 알림 설정/해제 ▸ hasBookingAlert (footer, variant flips secondary/primary
on the current toggle state)States.
- loading →
SkeletonFlatPage kind="map"; error →QueryErrorState; missing record: venue not found →NotFoundState(common.venueNotFound; 2026-09-16 converted from the genericvenues.noVenueslist-empty copy — the wrong copy for one specific missing venue) ActionIconRowcan shrink to 2 items (nav + map only computed) but the row itself renders ONLY at 3+ items — never a broken 2-item row (seam-audit rule E)- booking-alert enabled/disabled toggle; hours/facilities/booking-info/notes sections individually shown or hidden by data presence
- Offline cold start (query paused, nothing fetched) →
OfflineEmptyStatewith retry, via an earlyOfflineEmptyStatereturn — never the empty state (2026-09-11 offline sweep).
Actions.
- 길찾기 (navigate); 지도 보기 (open map place)
- 예약 사이트 열기 (▸ bookingUrl exists); 전화 (▸ bookingPhone exists)
- 알림 설정/해제 toggle (▸ hasBookingAlert)
Gaps. none found.