Skip to content

Owner review 46 — lane E: 알림 as the one inbox (2026-10-19) ​

Status: Reference · Date: 2026-10-19

Read-only research for owner review 46, mark M14: "Alarm is perhaps the most underworked section of them all. Find best practices and improve the UI/UX of it all, making sure we can aggregate alerts from all parts of the app into here." A migration is cited by number and line (00188:184-226 = supabase/migrations/00188_*.sql, lines 184–226).

Bottom line: a leaky v1 list under a v2 board. 37 of the 101 declared kinds never fire, rows expire in days, grouping is thin, and severity does three jobs (push, band, bell), so FYI news lands in 할 일.

1. Now ​

  • Stream. 101 kinds are declared (packages/app/src/domain/entities/signal.entity.ts:79-218); 64 have an emitter and 37 never fire (8 progression, 3 interclub, 6 subscription, post_mention and post_reaction, the friend_* kinds …; club_announcement's bridge was dropped in 00514). Emitters call signals_emit (00188:184-226). The kinds are listed in § 4.
  • Push. send-push → signals_claim_pushable (00688:61-106): a category toggle (on, except social, progression and rating; 00107:279-298), quiet hours 22–08 (payments exempt), caps of 10 a day per user and 3 a day per category (00641:117-118; critical kinds bypass them). min_severity defaults to low, so low kinds (session_created, member_joined) push, against docs/specifications/signal-system.md §5. 24 of the 68 title keys have no push copy and push as the bare category word (supabase/functions/send-push/notification-copy.ts:54-129).
  • Board against app. The board (flat-v2-list-a.jsx:939-1306): a ⋯ menu, three bands, PersonRow with a dot slot, Choice → a value Sheet, Select mode, swipe with undo. The app (packages/features/home/src/notification-center-screen.tsx): v1 cards, up to 13 chips, a tint and bold for unread, a gear; no select, swipe or faces.
  • Read, retention, settings. A row is acknowledged on tap or by 모두 읽음, and dismissed per row with no undo (00107:403). The feed hides expired rows (packages/app/src/adapters/supabase/signal.supabase.ts:86-92), and about half the live kinds expire within 48 hours, so 이전 알림 spans days, not the promised 30 (signal-system.md §7). Settings: 13 push toggles and one quiet-hours window; nothing per kind or per club (packages/features/profile/src/settings-signal-preferences-screen.tsx:195).
  • Grouping is thin. The upsert's count++ is never rendered, the row keeps the first actor, and a dismissed row stays dismissed (post_reply, 00192:120-137; 00188:212). The client folds only read, low, social rows per club and day (domain/rules/signal-resolver.ts:440). No series guard: a 12-week series fans out 12 session_created per member (00239:31-58); a four-round session gives a player about 12 rows.
  • Bell against 할 일. The bell = the unread RPC minus unread critical and high rows (domain/rules/alert-state.ts:164-170), against the component library (flat-v2-component-library-2026-10-09.md:878: an urgent arrival is one unread news item). So session_cancelled (no expiry, no resolver) skips the bell and sits in 할 일 until dismissed; DMs (chat is high, 00416:110) skip it too; the 안 읽음 chip uses the un-netted count. useHomeTasks (domain/rules/home-tasks.rules.ts:23) has no screen, while the library (:1565) wants 홈's 전체 보기 to open 알림's 할 일 band, which today is severity plus admin counters.
  • No signal where there should be one (absence checked): a friend request or acceptance (00118), a club invite (00686), a score correction (00681), the host told that someone joined, a seat released when a member leaves (00322, 00698), mentions, reactions, replies to a comment, an officer's 공지 (the same post_new, 00525:122), achievements (00021), 대항전 (00098), a member leaving → the officials (00410:47 goes to the leaver); a cancellation skips the waitlist (00672:137).

2. References (read in full unless "snippet") ​

3. Proposal ​

It keeps the standing rulings: the bell counts news only, 할 일 lives on 홈, no horizontal scroll, Choice → a value Sheet.

  • Structure. Header ‹ 알림 ⋯ (선택, 모두 읽음, 알림 설정). A pinned 할 일 = 홈's 할 일 set in its order (deriveHomeTasks plus the officer counters), absent when empty. Then time sections 오늘 / 이번 주 / 이전 알림 (30 days), newest first, with no severity sort. A filter Choice on the first head: 전체, 안 읽음, 관리 (officials), 일정, 경기, 회비·입금, 클럽, 사람, 시스템 (the families under § 4; empty ones hidden). [D1] Today reading a row moves it between today's 새 소식 and 이전 bands, and 모두 읽음 empties one; the dot already carries unread (Slack and GitHub keep rows in place).
  • Row. PersonRow in a notifications context: [dot 8 + 8][40 visual][line 1][line 2][time]. The visual is a face when a person did it (an applicant, a payer, a commenter, a friend), the club's avatar for club and object events, a gray glyph disc for system, trust and rating; no thumbnails. Line 1 is one 해요체 sentence, actor first ("민지님 외 2명이 댓글을 남겼어요"); line 2 is the club, then the one specific thing. Unread = the dot only. An aggregate shows the latest actor and 외 N and opens the object; request groups fold in place (KIND-GROUPS).
  • Actions. A tap opens. An inline AnswerPair only for a pending 친구 요청 [D3: library :1855 says request lists only]; money, 가입 심사 and scores open their screen. Swipe and Select = 삭제 with 8 seconds of 되돌리기; a dismissed aggregate returns on a new event. The row menu's "이런 알림 끄기" = that family's push toggle.
  • Bell and 할 일. The bell = the unread rows here (one per group), excluding 할 일 and chat, and equal to the 안 읽음 chip; the netting goes [D4]. 할 일 is state, never severity, and never in the bell. Chat stays in 메시지 (its own badge; rows deleted on read, 00687:159), with its push kept [D2; Slack folded DMs in].
  • Retention. Visible = created within 30 days and not dismissed; expires_at only gates the push and mutes the row ("지난 알림").
  • Push. Severity → interruptionLevel (time-sensitive only within the hour: hold_expiring, a referee transfer, sudden death, match_started), ttl, threadId = the group key, badge = the bell count. [D5] The spec says low never pushes and the SQL pushes it; a per-club level 전체 / 공지만 / 끔 (Strava) is preferred to raising min_severity. Later: a daily digest for what the caps drop.
  • Settings. ⋯ → 알림 설정: the 13 categories regrouped into the six families, one line each; quiet hours and the payment exemption kept; the Android channels stay 13 until a native build.

4. Signal catalog ​

Push: Y = on by default; n = inbox only (the category is off); ! = critical (bypasses quiet hours and caps); † = no push title (falls back to the category word). Link = getSignalRoute (packages/app/src/presentation/utils/signal-routes.ts:72-163). Group key: now → proposed.

Live ​

KindSourceToPushGroup keyLink
session_cancelled !, session_updatedsessions UPDATE trigger, 00672confirmed RSVPs (an update also the waitlist)Ysession × userclubSession
starting_soon, reminder_day_before, rsvp_deadline_sooncrons 00443, 00524, 00385confirmed RSVPs; members without an RSVPYsession × user → user × dayclubSession
session_created, club_event_created †, club_member_joined, post_newinsert triggers 00239, 00524, 00314, 00525the other active membersY (post_new n)per item → club × day (a series is one)clubSession, club, clubPost
waitlist_promoted, weather_warning, underfilled, late_cancel_alert, player_forfeited †, guest_*_open †rsvps triggers 00525, 00524; the weather trigger; cron 00444; 00683; 00481–00482the member; attendees; the host and admins; guestsYsessionclubSession
matchup_published, match_started †, match_completedmatch triggers 00525, 00385playersYmatch → session recapmatchBoard
score_pending †, score_disputed, sudden_death †, referee_transfer †score-call RPCs 00240–00242, 00402participantsYmatchmatchBoard
elo_changed, tier_promoted, tier_demoted, outcome_unrated †apply_match_ratings, 00682playersnmatch → session recaprecords, scorecard
the seven dues_* kinds (severely_overdue !, waived †)dues triggers and the tick, 00405, 00524the memberYdues rowclubDues
payments: hold_created, hold_expiring, hold_expired, confirmed, refund_notice; submitted, host_refund_unsettledsession_payments triggers and cron, 00385, 00682, 00684the payer; the host and dues managersY (no quiet hours)paymentclubSession; sessionPayments
join_request_received; join_request_decided; club_application_* †trigger 00385; RPC 00537; 00216the owner and admins; the applicant; host or applicantYrequestjoinReview, club
role_changed, ownership_transferred †, archived †, unarchived †, settings_updated † (dues amount only), member_left †RPCs 00537, 00407, 00410the target; both owners; admins or members; the leaverYclubclub
post_replycomment trigger, 00192the post's authornpost (count unseen) → latest actor + 외 NclubPost
no_show, late_cancel_penalty, trust_tier_changed, attendance_* †, manner_tag †00639, 00667, 00405, 00416the member; admins (flagged → own profile)Ysession, userprofile
chat_message_receivednotify_dm_recipient, 00416thread peersYthread (deleted on read)dmThread
venue_correction_* †, owner claim †, contribution †venue RPCs 00531–00624the submitterYcorrectionvenue, claim

New ​

KindSourceToPushGroup keyLink
friend_request_received, friend_request_accepted; club_invite_receivedfriendships 00118; invite RPC 00686the addressee or requester; the inviteen; Ypair; inviteprofile friends (AnswerPair); join
rsvp_joined; rsvp_removed; feedback_requestedrsvps; the seat release 00322, 00698; cronthe host; the member; attendeesn; Y; nsession × dayparticipants; clubSession
club_notice (공지); post_mention, post_reaction; member_left → officialsclub_posts; comments; club_membersmembers; the author or the mentioned; officialsY; n; npost; post × day; club × dayclubPost; members
score_corrected; session_recap; achievement_unlocked00681; session completed; 00021players; attendees; the memberY; n; nmatch; session; achievementscorecard; records; profile
interclub_challenge_*inter_club_challenges, 00098both clubs' adminsYchallengeclub

Families: 일정 = session + rsvp; 경기 = matchup + rating + progression; 회비·입금 = dues + payment; 클럽 = club + interclub + club posts; 사람 = comments, mentions, friends, trust; 시스템 = system + venue.

5. Data gaps and migrations ​

The next migration number is 00699; order 8 → 1, 3 → 2, 4, 5 → 6, 7.

  1. The feed and count RPCs (00246, 00646; signal.supabase.ts:86): a 30-day window, not expires_at; chat excluded.
  2. signals_emit v2 (00188:184): actor ids and the latest actor; clear dismissed_at on aggregate kinds.
  3. signals_dismiss_batch and signals_undismiss.
  4. Re-key the emitters: session_created 00239, post_new 00525, member_joined 00314, the match_* kinds and elo_changed 00385/00682, rsvp_deadline_soon 00385.
  5. New emitters and 24 push titles. send-push sends only to, title, body, data, sound, priority and channelId (supabase/functions/send-push/index.ts:113-127): add ttl, interruptionLevel, threadId and badge (Time Sensitive needs an iOS entitlement, so a native build).
  6. club_members.notify_level (the board's 클럽 알림, library :771, has no column).
  7. A my_tasks RPC if 알림 rows need object ids (get_home_signals returns counts only, 00694:97-144).
  8. Client only: the bell without netting, the 할 일 band from deriveHomeTasks, the family map, and getOpenObligationCounts dropped.

Decisions needed: D1 time sections or read bands; D2 chat in 알림; D3 an inline AnswerPair; D4 urgent arrivals on the bell; D5 low-severity push or a per-club level.

Not verified ​

No 2025–26 inbox write-up was found for Toss, 당근 (2021) or Kakao; no Material 3 notification page was found (the Android design guide is used). The Instagram, LinkedIn and Facebook items are search snippets; Instagram's "님 외 N명" and the time-section labels are unconfirmed. Nothing was run: "never fires" means no emitter in the migrations (variable-typed emits were checked for tier_*, chat and club_application_*). Unverified states: dues submitted by a member, the host-remove flow, report outcomes, the iOS Time Sensitive entitlement.

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