Screen Blueprints — Home
Status: Active Last reviewed: 2026-09-11
Current-state blueprints for the home 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.
Home (일간/주간/월간 브리핑)
- Route:
/(tabs)/(home) - Screen:
HomeFeedScreen—packages/features/home/src/home-feed-screen.tsx - Shell: MainTabShell · title
homeTitle.day|week|month(tracks selected granularity:일간 브리핑/주간 브리핑/월간 브리핑) · headerRight:NotificationBell - Design: ratified
- Verified: 2026-09-13
Purpose. The default landing tab — shows the user's PRESENT across three temporal slices (오늘/이번 주/이번 달), deliberately never a list of all future scheduled sessions (that belongs to the 경기 tab).
Anatomy. MainTabShell's tabs prop drives a 일/주/월 pill-nav (getPillItems()); renderTab swaps between three view components via renderTemporalContent(slice) — HomeTodayView, HomeWeekView, HomeMonthView from ./views/*, each wrapped in <FeedList> (one {kind:'temporal', slice} row) with a shared cross-pane header/footer:
├─ [MainTabShell] tabs=[일,주,월] tabValue=granularity onTabChange=setGranularity
│ ├─ header (cross-pane, only when hasDisputedMatches) ▸
│ │ └─ [HomeFeedSection] tone="danger" title=needsAttention → [HomeDisputedAlertCard]
│ ├─ [FeedList] one row per pane, renderItem → [PerfProfiler id="home-feed"] → renderTemporalContent(slice)
│ │ ├─ slice="day" → [HomeTodayView] (views/home-today-view.tsx)
│ │ ├─ slice="week" → [HomeWeekView] (views/home-week-view.tsx)
│ │ └─ slice="month" → [HomeMonthView] (views/home-month-view.tsx)
│ └─ footer (cross-pane) → [HomeRecommendationsSection]HomeTodayView (day pane): PeriodNavPanel (prev/next + DayCalendarPicker scrubber) → ▸ [HomeFeedSection] "진행 중" → LiveSessionStack (one SessionCardV2 per in_progress session anchored today) → ▸ [HomeFeedSection] "오늘 일정" → SessionCardV2×N (host-first, open|locked + endTime>now) → ▸ [HomeFeedSection] "오늘 복기" → SessionCardV2×N each carrying its post-game note row inside the card (RecapNoteBadge scoreIssue/needsAction/complete + EloDeltaBadge when the viewer's rating moved) for today's completed sessions the viewer RSVP'd to → else (all three empty) SkeletonCard | QueryErrorState | a Pressable "오늘 예정된 경기가 없어요" → routes.activity, wrapped in AttachStep for a first-idle FAB SpotlightTour coachmark (zero-club users only, MMKV-gated via useCoachmarkSeen('fab')).
HomeWeekView/HomeMonthView (structurally identical, ranged to week/month): PeriodNavPanel (WeekMonthPicker/MonthYearPicker scrubber) → [HomeFeedSection] recap → RecapSummaryCard (skeleton/error/populated-or-ghosted) → ▸ [HomeFeedSection] upcoming → SessionCardV2×N host-first (unbounded, live sessions excluded by design — those only ever show in the day pane).
States.
- Cross-pane:
hasDisputedMatchesheader banner shown/hidden; pull-to-refresh (usePullToRefresh(handleRefresh));HomeRecommendationsSectionhidden entirely unlessclubDiscovery.shouldShow || nearbyPickups.shouldShow. - Day pane: loading (no dedicated skeleton until
!hasContent, thenSkeletonCard) vs fetch-error (QueryErrorState variant="compact", checks bothsessionsQ.isErrorandrsvpdIsError) vs empty-CTA vs any-of-live/scheduled/recap populated; zero-club + empty + unseen + 3s-idle → FAB coachmark fires once. - Week/month panes: recap sub-loading
SkeletonCard/ recap fetch-errorQueryErrorState/RecapSummaryCardpopulated /RecapSummaryCardghosted (ranged query returns zero matches); upcoming section independently loading/error/populated/hidden-when-empty. - No always-on realtime: three ambient realtime channels were removed by owner decision (2026-06-01) after RLS cost measurement — freshness now carried by
staleTime(60s), focus/reconnect refetch, push-triggered invalidation, and foreground catch-up (see file header comment).
Actions.
NotificationBelltap →routes.notifications(badge toneattention(amber) when an admin obligation is open, elseunread(red), else none — viaderiveBellState).- Pill tap →
setGranularity(resets todaydefault view logic per store). PeriodNavPanel‹›/step(±1); center tap toggles the scrubber; picker day/week/month tap →pickAnchor.HomeDisputedAlertCardtap →routes.scorecard(sessionId)of the first disputed match's session.- Any
SessionCardV2tap (any partition, any pane) →routes.clubSession(clubId, sessionId). - Day-pane empty-state tap ("오늘 예정된 경기가 없어요") →
routes.activity. - Pull-to-refresh →
handleRefresh.
Gaps. none found.
Notification Center (알림)
- Route:
/home/notifications - Screen:
NotificationCenterScreen—packages/features/home/src/notification-center-screen.tsx - Shell: DetailShell · title
t().notifications.title(알림) · scroll=false · headerRight:HeaderIconButtonSettings →routes.signalPreferences - Design: ratified
- Verified: 2026-09-16
Purpose. The one place behind the bell: every active signal for the viewer plus the admin-attention rows for clubs they run, filtered by chip and grouped into 할 일 / 새 소식 / 이전 알림 bands.
Anatomy.
├─ [DetailShell] title=알림 · headerRight=[HeaderIconButton Settings → 알림 설정]
└─ [FeedList] rows = union of band labels, admin rows, signal rows, group rows (getItemType per kind)
├─ header: [XStack flexWrap] [FilterChip] × buildFilters() — wraps to a second row, never a horizontal scroll
├─ [SectionHeader] band 할 일 ▸ any admin-attention item or todo-band signal
│ ├─ [AdminAttentionRow] × visibleAdminItems — one per admin club, dismissible, → that club's 관리 tab
│ └─ [MemoSignalRow] × signals with signalBand()==='todo'
├─ [SectionHeader] band 새 소식 (trailing 모두 읽음 text action ▸ unreadSignalCount > 0) ▸ any fresh signal
│ └─ [MemoSignalRow] × fresh
└─ [SectionHeader] band 이전 알림 ▸ any earlier signal or group
├─ [MemoGroupRow] × groupEarlierBandSignals() units — title={club name alone} (via useClubsByIds), body=[새 글 N개, …latest signal's facts] rendered as MetaSegments, never glued
└─ [MemoSignalRow] × earlier — body is a single clause (Text) or several distinct facts (`readonly string[]`, MetaSegments) — never a middle-dot-glued stringStates.
- Loading (
activeSignals.isLoading || signalPreferences.isLoading) → threeSkeletonCards in the list'sloadingState. - Error →
QueryErrorStatewith retry (activeSignals.refetch). - Empty copy branches on the active chip: 안 읽음 →
emptyUnread("caught up"), a category chip →emptyCategory(label), otherwisenotifications.empty; all renderEmptyState icon=Bell variant=compact. - Infinite feed:
hasNextPage/isFetchingNextPage→onEndReachedloads more; pull-to-refresh viausePullToRefreshrefetching signals + preferences + admin attention. - Signals hidden by the viewer's preferences (
buildPreferenceMap→resolveSignals) never render. - Offline cold start (query paused, nothing fetched) →
OfflineEmptyStatewith retry, via the list'sisPausedOffline+offlineState— never the empty state (2026-09-11 offline sweep).
Actions.
- Chip tap →
setFilter(all / unread / admin / per-category). - Signal row tap → acknowledges if unread (
useAcknowledgeSignals) thenappRouter.push(getSignalRoute(signal)). - Signal row dismiss →
useDismissSignal. - 모두 읽음 (새 소식 band header) →
useAcknowledgeVisibleUnreadSignals. - Admin row tap → the club's 관리 tab; admin row dismiss → local dismiss from
useMyAdminClubAttention. - Settings icon →
routes.signalPreferences. - Back →
appRouter.back()when there is history, elsereplace(routes.home)(deep-link entry).
Gaps. none found.