Skip to content

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: DirectoryVenueDetailScreenpackages/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 generic venues.noVenues list-empty copy — the wrong copy for one specific missing venue)
  • closed venue → ClosedVenueBanner + TERMINAL_MUTE_OPACITY on 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: VenueContributionDetailScreenpackages/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/replace

States.

  • 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 only isError/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: VenueContributionsScreenpackages/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: NewVenueContributionScreenpackages/features/sessions/src/venue-contributions/new-screen.tsx
  • Shell: DetailShell · title = kind-specific (dynamic, e.g. 없는 장소 추가 for new_venue) via the internal EditorGate · 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't new_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: VenueOwnerClaimDetailScreenpackages/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) buttons

States.

  • 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 only isError was checked)
  • content with full view permissions vs summary-only vs transfer-consent-only view
  • Offline cold start (query paused, nothing fetched) → OfflineEmptyState with retry, via an early OfflineEmptyState return — 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: VenueOwnerClaimsScreenpackages/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) → OfflineEmptyState with retry, via the list's isPausedOffline + 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: NewVenueOwnerClaimScreenpackages/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: VenueDetailScreenpackages/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 generic venues.noVenues list-empty copy — the wrong copy for one specific missing venue)
  • ActionIconRow can 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) → OfflineEmptyState with retry, via an early OfflineEmptyState return — never the empty state (2026-09-11 offline sweep).

Actions.

  • 길찾기 (navigate); 지도 보기 (open map place)
  • 예약 사이트 열기 (▸ bookingUrl exists); 전화 (▸ bookingPhone exists)
  • 알림 설정/해제 toggle (▸ hasBookingAlert)

Gaps. none found.

Markdown remains the source of truth. Run yarn docs:check before handoff.