Skip to content

Screen Blueprints — Home ​

Status: Active Last reviewed: 2026-11-10

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 (page reads and states: page/; the Lead lead/, the 할 일 lane tasks/, 소식 news/, the Sheets and the FAB overlays/)
  • Shell: MainTabShell · chrome="tabs", no tabs · title common.tabHome · testID tab.home · actions = [메시지 (useDmUnreadCount badge), 알림 (useBellAction)], both coveredByWebNav
  • Design: unreviewed
  • Verified: 2026-11-15
  • Header marks: the 메시지 and 알림 marks hang by their right edge for one digit; a count of two digits or more (to 99+) keeps the one-digit mark's left edge and grows to the right, so it never covers the glyph.
  • Batch 63 (L1/N2), Wide windows (web): the tab root sits in the reading column (MainTabShell is always reading, 680) and the day-sectioned session rows are one column at every width; the 2-column packing was removed with the FeedList grid.
  • Batch 58 (ST3, 2026-11-13), States: homePageState = pageState (data in hand is the page even if a refresh failed): loading draws the page's own skeletons; cold offline and error are PageStateView (no FAB, no list); the news feed's own cold offline is the same compact offline state.

Purpose. Flat v2, page L1-01: what is next for you, what waits on you, then everything new in your world. The Lead (a Block), the 할 일 lane (a Shelf of compact cards), then 소식 (the page's one open-ended list) and the FAB by role. Three lanes by who ends an item: the Lead is the system's, a 할 일 is ended by doing it, a 소식 item by the reader (숨기기, with 되돌리기). Built W4 batch 16 to flat-v2-hub.jsx#home-ideal; it replaced the 일/주/월 panes (hub ruling H8).

Now.

  • The page's own read is get_home_snapshot (useHomeSnapshot; useHomeData stays mounted for its cache backfill). Its status picks the look: data in hand is the page (stale while the read is paused offline); no data and paused is the offline cold state; no data and failed is the error state; otherwise the skeletons (home-page-state.ts).

  • Lead (homeLead, a pure rule; clock = the minute-ticking useNowTicking): the viewer's match in play (match Block, live Tag and ring), their last match for 30 minutes after it ends (match Block, the score standing, 방금 끝남), the next confirmed joined game (day Block, with the fee row and the door 입금하기 while a fee is owed), else the empty Block (door 매치 찾기). A cancelled, waitlisted or check-in-open next game is skipped to the next confirmed one. First run (no active club, no joined game): the empty Block with no door. The match Block is the one binding matchStateBlockProps shared with 스코어카드. The players' names come through profileDisplayName (never blank: 탈퇴한 멤버 for a profile the read did not return); until the profiles read has answered the match Block draws nothing and the rest of the page stands.

  • 할 일 (useHomeTasks): one card per kind, each carrying its subject (get_home_signals: score entry, session fee, dues owed; the officer kinds over the un-dismissed admin totals), in the board's order, absent with no card. maxItems = the card count. The over line is the session (one bulk useSessionsByIds) or the club; the Tag 오늘 마감 only for a fee due today. First run: the three ways in as cards.

  • 소식 (useHomeNewsItems, one keyset-paged get_home_news, items built by the shared domain/feed builders with the club as the source): the head (SectionHead + Choice 전체 ▾) pins under the Header; the filter Sheet is 전체 / 일정 / 결과 / 게시글. A long press (an accessibility action 숨기기, no visible button) opens the item's MenuSheet: 숨기기, 이 클럽 소식 끄기. Either removes optimistically and shows the one Toast with 되돌리기. The Feed ends with the Feed end and no door.

  • FAB by role (homeFabRole): 매치 for a member, 일정 for an official of one club, the club picker (a MenuSheet, clubs then 클럽 없이 매치로) for an official of several. None in the first run, loading, error or cold states. A member sees the one-time coachmark over a Scrim (useCoachmarkSeen; back or the action marks it seen).

  • 할 일 gains the kind 사유 인정 (follow-up batch E, from the club admin snapshot's pending_excuse_requests_count and oldest_excuse_request_user_id, an id only): the card sits between 입금 확인 and 회비 미납, title 사유 인정 (en Excuse), club over line, N건. One club opens the oldest waiting member's 멤버 기록 (routes.profileTrust(userId, clubId)); several clubs open 알림's 처리할 일; none at a count of 0. A 매치's creator who is neither owner nor admin gets the notice and the 알림 row only (recorded gap).

Anatomy.

├─ [MainTabShell chrome=tabs] 홈 · [메시지] [알림]
│  └─ [PerfProfiler home-feed] → [HomeBody]
│     ▸ cold | error → [EmptyState offline | error what=홈] (no list, no FAB)
│     ├─ ▸ stale → the page unchanged under the root strip
│     ├─ [FeedList]  header = Gap 8 · Lead · 할 일 lane      (they scroll away)
│     │   ├─ Lead: [Block day | day+pay | match live | match result | day empty | first-run empty]
│     │   ├─ 할 일: [Shelf compact] head 할 일 · N개 (no door)   ▸ loading: Shelf skeleton · failed: [ZoneFailed]
│     │   ├─ head (pinned): [SectionHead 소식 · Choice ▾]
│     │   └─ rows: [ClubFeedItemRow] long press → hide menu     ▸ failed: [ZoneFailed] · empty: [EmptyState]
│     ├─ [FabLayer] Fab placed=tab · picker MenuSheet · CoachmarkOverlay
│     └─ [NewsFilterSheet] [NewsItemMenu]

States. Loading (Block, Shelf, the 소식 head and two skeleton rows; no FAB; the match Block is its loading form while the players' profiles load, and draws with profileDisplayName's words if that read failed or is paused), error, offline cold, stale (cached data and the strip), a zone that fails (the lane or the Feed: ZoneFailed, the rest stays), first run, quiet (no 할 일: the Module is absent and 소식 is the first head), live, result, fee due, officer, English.

  • Offline (F1c, the app's root strip says it): offline with a cached snapshot the page stays unchanged under the root strip (home-stale); the FAB is a write the kit holds and its coach mark is off.
  • 2026-11-13 (UX-7, the state helper): homePageState = pageState (data in hand is the page even if a refresh failed): loading draws the page's own skeletons; cold offline and error are PageStateView (no FAB, no list); the news feed's own cold offline is the same compact offline state.

Actions.

  • Block → 일정 상세 (a fee due: its door 입금하기 opens the same page; empty: 매치 찾기 → 경기 at 매치 찾기; a match in play or just played: the whole Block → 스코어카드). First-run Block → 클럽 찾기.
  • A 할 일 card → 점수 입력 → 스코어카드; 참가비 입금 → that session's page, or 참가비 내역 when several are owed; 회비 납부 → 클럽 홈 at 회비; an officer card → that club's page for the kind (멤버 관리, 클럽 일정, 회비 관리, 신고 관리) or 알림 at 처리할 일 when several clubs hold it. First run: 클럽 찾기, 초대 코드로 가입, 매치 찾기.
  • A 소식 item → a club session, 대회 or 결과 its 일정 상세; a 매치 of a club the viewer is not in its 매치 미리보기; a post its 게시글; a photo day the album; a 행사 its page.
  • 메시지 → 메시지; 알림 → 알림 (unread news only); FAB → 매치 만들기 / 일정 만들기 / the picker; pull to refresh re-reads the snapshot and 소식 with a fresh clock.

Notes. Retired with the panes (H8): the 일/주/월 views and pickers, the recap pieces, the live-session stack, the nearby-pickups and club-discovery cards, the disputed-match banner (a dispute reaches the player through 알림's score_review signals and the scorecard), useHomePeriodStore and the period nav panel. No realtime channel: the snapshot is stale after 60 s, push invalidates, a pull re-reads.

Gaps. The Block's eyebrow reads the relative day only (the board adds the date), the day Block shows no faces or weather (a roster-face read is not in the card bundle), an officer's 입금 확인 and 회비 미납 cards name the club and the count (the session, the period and the applicant need a subject the admin totals do not carry), the "소식 N개 더" collapse, the filter Sheet's muted list and interests, and the Feed end's door are not drawn on the board and not built. A 행사 has no page of its own yet.


Notification Center (알림) ​

  • Route: /home/notifications
  • Screen: NotificationCenterScreen — packages/features/home/src/notification-center-screen.tsx (data and actions: notification-center/use-notification-center.ts, composed of use-notification-feed.ts, use-notification-actions.ts, use-notification-lookups.ts)
  • Shell: DetailShell · title 알림 · scroll=false · navRoot · actions: one gear named 알림 설정 → routes.signalPreferences. No ⋯, no Select mode, no footer
  • Design: unreviewed
  • Verified: 2026-11-12

Purpose. Flat v2, page L3-13 (an L3 list page; board flat-v2-list-a #notifications, approved 2026-10-20). The one inbox behind the bell: every alert the feed holds, newest first in three time sections, and one filter (전체, 안 읽음, 처리할 일). 처리할 일 is the alerts that ask the viewer to act while their request is open; a row leaves it when the request is settled, never when it is read. There is no read control: a row's tap opens what the alert is about and clears its dot, leaving the page reads the dots still showing, and one alert is deleted by its swipe (one home per action, § 1.18). Built W4 batch 5 to flat-v2-list-a#nt-ideal (L3-13).

Anatomy.

├─ [DetailShell] 알림 · actions [gear 알림 설정]
└─ [FeedList] tail-only (SCROLL_CONTENT.list), no separators; rows = heads and alerts
   ├─ [SectionHead first] 오늘 · choice [Choice ▾ → ValueSheet 전체 · 안 읽음 · 처리할 일, with counts]
   │  └─ [SwipeAction 삭제] > [NotificationRow] — app PersonRow, context `notifications`:
   │     [marker dot][visual 40][sentence / club | thing][time]   (no ⋯, no chevron)
   │  ├─ [NotificationRow + answer] a pending 친구 신청: 수락 · 거절 under line 2 (D3)
   │  └─ [NotificationFoldRow] a request group: PersonRow `fold` (chevron where the time is), tap opens in place
   ├─ [SectionHead] 이번 주 ─ rows
   └─ [SectionHead] 이전 알림 ─ rows; the feed ends with no line

Groups (W4 batch 18). A repeat is one stored row (migration 00728/00729) and its title reads as the group, from signalGroupOf(signal): {title}.others ("박지훈님 외 2명이 댓글을 남겼어요", when the row has other actors and the latest actor's name resolved) or {title}.count ("새 일정 12개가 올라왔어요"), else the base title; the keys live in signals.ts and PAYLOAD_KEYS (signal-copy.ts) with othersCount and count derived from the group. A request group is a client fold (planRequestFolds, request-fold.ts): the open requests of one kind and club (친구 신청, 가입 신청, 입금 확인, 신고; two or more) fold into one row where the newest stands, "한소희님 외 2명이 친구 신청을 보냈어요", club only on line 2, fold="closed" (PersonRow chevron in the time's place, spoken 펼치기 / 접기, aria-expanded); a tap opens it in place and each request then has its own row under the group row. Settled requests are ordinary rows. The group row has no swipe and its dot shows while closed and any request is unread.

The visual is the actor's face, the club's mark, or a gray glyph disc (system, trust, rating, progression). Line 1 is the sentence (resolveSignalTitle), line 2 the club then the one specific thing, the time trails. Unread is the dot alone. Spoken: "읽지 않음, {sentence}, {facts}, {time}" (notifications.unreadSpoken); the swipe's verb is a custom action 삭제 on the row.

States. Loading → nine person skeletons. Error → QueryErrorState. Cold offline → OfflineEmptyState (never "no notifications"); a cached feed stays under the root strip (the app draws it once; owner decision 16, 2026-11-10). Nothing → EmptyState kind="first" 받은 알림이 없어요 (no button). A filter with nothing → the first head kept over a compact kind="filtered" with 전체 보기 (읽지 않은 알림이 없어요 / 처리할 일이 없어요).

  • Write failures (ruling EP-3): a refused swipe-delete puts the row back, a refused 되돌리기 takes it away again; acknowledgements are background writes (shown nothing, reported); never a Toast.

Data. useActiveSignalsFeed (the 30-day window, dismissed and chat left out, migration 00727) → resolveSignals → buildNotificationSections; 처리할 일 = useOpenRequestSignalIds less the requests answered here; counts 전체 useSignalVisibleCounts, 안 읽음 useUnreadSignalCount (the bell's own count, no netting), 처리할 일 the set; the open set refetches when the page regains focus. A 친구 신청 answered here (useAcceptFriendRequest / useDeclineFriendRequest, useFriendAnswers) is marked answered once the write succeeds: the pair goes, the dot clears (acknowledged with it), the sentence says what happened (friendRequestAccepted 한소희님과 친구가 됐어요, friendRequestDeclined), the row keeps its place and, in a group, its group; a failed write keeps the pair and shows the hook's own Toast.

사유 인정 rows (follow-up batch E). Four rows: the request and its 3-day reminder (its own type, so it pushes again) read to a decider as "박지훈님이 사유 인정을 요청했어요" / "박지훈님의 사유 인정 요청이 3일째 답을 기다려요" (the requester named in the row, never in the push; 처리할 일 while it waits; several in one club fold into "박지훈님 외 2명이…" as 친구 신청 do; it opens the member's record), and "사유 인정 요청이 인정됐어요" / "거절됐어요" to the requester, a quiet row sent only after the 10 minutes in which the answerer can undo it (an answer that was undone sends nothing). Line 2 is the club alone: the signal carries no date and no category or note. The push line is worded apart and names no one ("사유 인정 요청이 왔어요").

Actions.

  • Row tap → getSignalRoute(signal) and acknowledge if unread. Landings (signal-routes.ts, now an ordered rule table): 회비 → 클럽 홈 at 회비 (routes.club(clubId, {tab:'dues'})); 공지 → routes.clubPost when the context carries a post id, else 클럽 홈; 가입 신청 → the applicant's dossier (멤버 관리 has no 가입 신청 anchor); 친구 신청 and its answer → the sender's profile; 신고 → the club's 신고 관리 (routes.clubReports); 이름 elo → 기록; trust → the profile until L3-01 (N12: 가입 신청 and 신뢰 keep these landings until 멤버 관리 and 신뢰 기록 take an anchor).
  • Swipe left → 삭제 (72 wide, danger; a full swipe commits; web: mouse drag; screen reader: custom action) → the row hides, useDismissSignals, Toast 알림을 삭제했어요 · 되돌리기 (8 s → useUndismissSignals). No Dialog.
  • Leaving the page (blur or unmount) → the unread rows that were showing are acknowledged in one batch (useAcknowledgeSignals); a deleted row is not.
  • Filter Choice → ValueSheet → setFilter. Gear → 알림 설정.

Notes. 2026-11-08 (W4 batch 5): the Header ⋯, Select mode, 모두 읽음, the row ⋯ and its NotificationMenuSheet (읽음/읽지 않음으로 표시, 이런 알림 끄기, 삭제), the CtaBand footer went; the notifications PersonRow context lost menu. The long NotificationCenterScreen and useNotificationCenter bodies were split. Decisions for the owner's veto: the bell keeps counting unread rows until the page is left ("opening the page clears the badge" needs a seen timestamp, W0.7). 2026-11-10 (W4 batch 18, lane C1): the bell is useUnreadSignalCount with no netting (the obligation counts and the netting in alert-state.ts are gone); 외 N titles; the 친구 신청 pair; the request-group fold; getSignalRoute rules for 친구 신청 and 신고 placed before the category rules.

Gaps. Rows the data cannot produce yet: the 신고 row's category on line 2 (the signal names no one and carries no category), join "함께 N명" and the trust cause on line 2, and a grouped 참석 row ("강도윤님 외 3명이 참석해요": no emitter groups RSVPs yet). The board's stale, overflow, 200% and dark frames are kit-level (the offline strip, wrapping and tokens belong to the kit's tests). 이런 알림 끄기 lives only on 알림 설정. The Header gear's destination is routes.signalPreferences (/settings/signal-preferences); the board names it /settings/notifications (L4-18's rename). The ⋯ model code (menuControl, menu on ContextSpec, copy.menuOf) is now unused in person-row-model.ts.

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