Skip to content

MSG-408 feat: FCM 웹 푸시 기반 + 토큰 API 2종 실연동 — 프로필 알림 토글 옵트인·로그아웃 fcmToken 동봉 - #60

Merged
gyuhochoime merged 2 commits into
developfrom
feat/fcm-web-push-notifications
Aug 18, 2026
Merged

MSG-408 feat: FCM 웹 푸시 기반 + 토큰 API 2종 실연동 — 프로필 알림 토글 옵트인·로그아웃 fcmToken 동봉#60
gyuhochoime merged 2 commits into
developfrom
feat/fcm-web-push-notifications

Conversation

@gyuhochoime

@gyuhochoime gyuhochoime commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🎫 관련 티켓

📌 작업 내용

FCM 웹 푸시 기반 구축 + 푸시 토큰 API 2종 실연동.

  • features/notifications/ 신설 (FSD 신규 도메인): Firebase config·VAPID 공개키 상수 정본 모듈(config/firebase.ts — 공개 설계 값, SW가 env를 못 읽어 코드 상수로 커밋: VITE_ env 컨벤션의 승인된 예외), 브라우저 지원 판별·동기화 액션 판정·토글 표시 파생 순수 로직(test-first), firebase 동적 import 격리 경계, 자동 동기화·토글·포그라운드 수신 훅, 우하단 토스트 호스트
  • 프로필 "알림 받기" 토글: 자동 권한 프롬프트 없음(스펙 승인 결정) — 토글이 유일한 신규 등록 진입점. ON = 제스처 컨텍스트 권한 요청 → getToken(VAPID)POST /api/notifications/tokens → 보관 / OFF = DELETE 성공 시에만 보관 삭제(실패 시 ON 유지 + 오류 안내). 기존 "알림 설정" 준비 중 행을 대체
  • 자동 동기화(기존 등록자 한정): 권한 granted && 보관 토큰 존재일 때만 셸 렌더 시 재등록·로테이션(DELETE 구 → POST 신). 언마운트 경쟁 가드(cancelled 플래그) 포함
  • 로그아웃 fcmToken 동봉: useLogoutshared/storage.tsfcmTokenStorage(auth↔notifications 매개 어댑터) 경유로 body 동봉 + onSettled 보관 비움
  • public/firebase-messaging-sw.js: 쿼리스트링 config 파싱 → onBackgroundMessage 최소 표시. env 무의존
  • 위치동의 게이트(MSG-407) 표시 중·비로그인에서는 미동작(셸 분기 내부 마운트)

커밋 전 codex 리뷰 1회 반영: OFF 실패 시 보관 삭제 확정버그 정정(AC 7 개정), 동기화 경쟁 가드, 등록 실패 안내 — 각 RED 재현 후 수정.

✅ 체크리스트

  • pnpm lint / pnpm typecheck / pnpm --filter web test run 통과
  • 수용 기준 검증 완료 (검증 리포트 요약을 아래에 첨부)
  • 필요한 경우 문서(README, docs/) 업데이트 — docs/STATUS.md·docs/spec/MSG-408.md·docs/decisions/DECISIONS.md

🔍 검증 요약

자동 검증 (풀 게이트 6종)

게이트 결과
vitest 통과 — web 153파일/1008 테스트 (+mobile 40파일/187)
typecheck 통과
lint 통과 (SW 파일 /* eslint-disable */ 1건 — 번들 비대상 워커 스크립트 정당 판정)
format:check 통과
check:duplication 통과 (신규 테스트 스캐폴드 패밀리 베이스라인 등재)
openapi-ts 드리프트 통과 (생성 SDK 이미 최신 — 재생성 불필요)

수용 기준 판정: 13/13 통과, 실패 0

# 기준 판정 근거
1 자동 동기화·로테이션(기존 등록자 한정) vitest — 요청 관찰
2 신규 자동 등록 경로 없음(의도) vitest + 브라우저(요청 0·permission default·SW 0)
3 비로그인·위치동의 게이트 중 무발동 vitest + 구조 감사(셸 분기 내부 마운트)
4 토글 행 표시·표시 정본(granted && 보관 토큰) vitest + 브라우저
5 토글 ON 사슬(제스처 → 권한 → 등록 → 보관) vitest + 브라우저(실프롬프트 발화 관찰)
6 denied → OFF 유지 + 설정 안내 토스트 vitest + 브라우저 실렌더
7 토글 OFF — 성공 시에만 보관 삭제, 실패 시 ON 유지+안내 vitest (codex 리뷰 정정 반영)
8 미지원 브라우저 비활성·무크래시 vitest (jsdom = 실제 미지원 환경)
9 로그아웃 fcmToken 동봉 + 보관 비움 vitest 3케이스
10 포그라운드 onMessage → 우하단 토스트 vitest (onMessage 모킹 — 스펙 명시 방법)
11 SW 서빙·등록 성공·콘솔 클린 브라우저 — 200 서빙, activated, 콘솔 에러 0, dist 복사 확인
12 config 정본 1곳·SW env 무의존 코드 감사(grep) + vitest + 브라우저
13 API 실패 격리(기동·로그인·프로필 흐름 무영향) vitest

전 구간 콘솔 에러 0. a11y: switch role·접근 이름·키보드(Tab+Space) 실확인, Toast role="status". 병렬 스코프 준수(features/map-home·widgets/cell-detail 무변경).

검증 범위 밖: 백그라운드 푸시 실수신 E2E(BE/Firebase 콘솔 발송 필요 — 합동 확인 사항), 토글 granted 실완주(브라우저 크롬 "허용" 클릭 자동화 불가 — vitest가 정본 커버), 권한 변경 실시간 감지(스펙 범위 밖).

📸 스크린샷 (선택)

💡 추가 논의할 사항

  • 백그라운드 실수신 확인은 BE 발송 협조 필요 — 지라 코멘트로 환류 예정
  • SW는 gstatic CDN importScripts 의존(Vite public/ 번들 불가 제약의 표준 우회) — 차단 환경에서는 백그라운드 표시만 영향

🤖 Generated with Claude Code

@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

Claude finished @gyuhochoime's task in 5m 24s —— View job


🔍 PR #60 코드 리뷰 완료

작업 목록

  • 컨텍스트 파악 (CLAUDE.md, docs/spec/MSG-408.md, docs/STATUS.md)
  • git diff origin/develop...HEAD 변경분 확인
  • features/notifications/ 신규 도메인 리뷰 (config/model/api/ui)
  • pages/profile/ 변경분 리뷰 (ProfilePanel, SettingToggleRow)
  • features/auth/ 로그아웃 fcmToken 연동 리뷰
  • shared/storage.ts 리뷰
  • public/firebase-messaging-sw.js 리뷰
  • 인라인 코멘트 작성 (2건)
  • 요약 코멘트 게시

요약

32개 변경 파일 전체를 diff + 주변 코드까지 순차로 확인했습니다. 순수 로직/부수효과 분리, test-first 커버리지, codex 리뷰 반영(AC 7 정정·경쟁 가드)이 잘 되어 있는 견고한 구현입니다.

🟡 이슈 2건을 인라인 코멘트로 남겼습니다 (병합을 막을 수준은 아닙니다):

  1. features/notifications/api/messaging.tsmessagingPromise 싱글턴이 초기화 실패 시 rejected promise를 영구 캐싱해, 재시도 없이 새로고침 전까지 푸시 기능 전체가 고장 상태로 남을 수 있음
  2. pages/profile/ProfilePanel.tsx — 토글 안내 토스트와 PushNoticeHost의 포그라운드 토스트가 동일 좌표(fixed bottom-md right-md z-50)를 써서, OFF 실패 + 포그라운드 푸시 동시 발생 시 겹쳐 보일 수 있음

전체 리뷰는 PR 코멘트에 게시했습니다.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown

React Doctor found 2 new issues in 1 file · 2 warnings · score 91 / 100 (Great) · 1 fixed · vs develop

2 warnings

src/features/notifications/api/use-push-toggle.ts

  • ⚠️ L49 Mutation without cache invalidation query-mutation-missing-invalidation
  • ⚠️ L70 Mutation without cache invalidation query-mutation-missing-invalidation

Reviewed by React Doctor for commit 4225f7d. See inline comments for fixes.

Comment thread apps/web/src/features/notifications/api/messaging.ts
Comment thread apps/web/src/pages/profile/ProfilePanel.tsx Outdated
@claude

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — PR #60 (MSG-408 FCM 웹 푸시)

리뷰 범위: git diff origin/develop...HEAD 기준 32개 변경 파일 전부를 diff+주변 코드까지 Read로 확인. 서브에이전트/병렬 도구는 사용하지 않고 순차 진행.

👍 잘한 점

  • 순수 로직/부수효과 분리가 깔끔합니다. model/push-sync.ts·model/push-toggle.ts·model/push-support.ts가 전부 입력→출력 순수 함수로 test-first 커버되어 있고, firebase 전역 접근은 api/messaging.ts 격리 경계 1곳에만 있어 테스트 모킹 지점이 명확합니다.
  • decideSyncAction/shouldAutoSync의 AC 1·2·3 경계 조건(로그인·지원·권한 granted·보관 토큰 존재)이 각각 개별 vitest 케이스로 촘촘히 검증돼 있습니다.
  • codex 리뷰로 잡힌 AC 7 확정버그(OFF 실패 시 보관을 비우던 것)가 onSuccess/onError 분리로 올바르게 정정됐고, 회귀 테스트(use-push-toggle.test.tsx:164)로 고정돼 있습니다.
  • use-push-token-sync.ts의 언마운트 경쟁 가드(cancelled 플래그)가 로그아웃 직후 늦게 도착하는 응답이 방금 비운 보관을 재오염하는 경로를 정확히 막고, 이걸 재현하는 테스트(use-push-token-sync.test.tsx:130)까지 갖춰져 있어 인상적입니다.
  • Toast에 표시되는 FCM payload의 title/body는 ui-web ToastdangerouslySetInnerHTML을 쓰지 않아 XSS 경로가 없습니다.
  • shared/storage.tsfcmTokenStorage 어댑터로 features/authfeatures/notifications 직접 의존을 피한 설계가 기존 deviceIdStorage 관례와 일관됩니다.

🟡 발견한 이슈 (인라인 코멘트 참고)

  1. features/notifications/api/messaging.ts:39-47messagingPromise 싱글턴이 ??=로만 채워져서, 최초 초기화(동적 import·initializeApp/getMessaging)가 한 번이라도 실패하면 rejected promise가 영구히 캐싱됩니다. 이후 토글 ON·자동 동기화·포그라운드 구독 전부가 페이지 새로고침 전까지 계속 실패합니다. 실패 시 messagingPromise = null로 되돌려 재시도 가능하게 하는 걸 권장합니다. 이 파일은 스펙상 vitest가 모킹만 하는 격리 경계라 현재 테스트로는 못 잡습니다.
  2. apps/web/src/pages/profile/ProfilePanel.tsx:140-141 — 토글 안내 토스트(pushToggle.notice)와 PushNoticeHost의 포그라운드 토스트가 정확히 같은 fixed bottom-md right-md z-50 좌표를 씁니다. OFF 실패(notice: "error", 보관 토큰 유지 → 구독 계속 살아있음) 상태에서 포그라운드 푸시가 오면 두 토스트가 겹쳐 렌더될 수 있습니다. 오프셋 분리 또는 단일 스택 컴포넌트로의 통합을 권장합니다.

두 건 모두 확실한 버그라기보다 재현 조건이 좁은 엣지 케이스라 🟡(권장)로 표시했습니다 — 병합을 막을 정도는 아니라고 판단합니다.

참고 (블로킹 아님)

  • config/firebase.ts의 Firebase apiKey·VAPID 공개키는 코드 상수로 커밋되어 있는데, 이는 스펙(docs/spec/MSG-408.md 결정 2)에서 사용자 승인을 받은 명시적 예외이고 Firebase 웹 API 키는 설계상 공개 식별자라 보안 이슈로 보지 않습니다. 다만 Firebase 콘솔 쪽 App Check·리퍼러 제한이 걸려 있는지는 코드 밖의 사안이라 확인 대상은 아닙니다(확신 없음 — 추측).
  • use-push-token-sync.ts의 로테이션 경로(구토큰 DELETE 성공 후 신규 POST 실패)에서 로컬·서버 상태가 일시적으로 어긋날 수 있는 점은 스펙 리스크 절에 이미 문서화되어 있고 멱등 API로 다음 동기화에 자기 수복되는 설계라 별도로 지적하지 않았습니다.

View job run

Comment thread apps/web/src/features/notifications/api/messaging.ts
@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

Claude finished @gyuhochoime's task in 2m 45s —— View job


🔍 PR #60 코드 리뷰 완료

작업 목록

  • 컨텍스트 파악 (CLAUDE.md, docs/spec/MSG-408.md, 이전 리뷰 코멘트)
  • git diff origin/develop...HEAD 변경분 확인 (최신 커밋 4225f7d 포함)
  • 최신 fix 커밋(4225f7d)이 이전 리뷰 지적사항을 어떻게 반영했는지 확인
  • features/notifications/ 도메인 리뷰
  • pages/profile/ 변경분 리뷰
  • features/auth/ 로그아웃 fcmToken 연동 리뷰
  • shared/storage.ts 리뷰
  • public/firebase-messaging-sw.js 리뷰
  • 인라인 코멘트 (신규 이슈 없음 — 이전 인라인 코멘트가 이번 커밋으로 해소됨)
  • 요약 코멘트 게시

요약

이번 리뷰는 최신 fix 커밋(4225f7d)이 이전 라운드(0fdbe6c)에서 지적된 이슈를 어떻게 반영했는지 중심으로, 서브에이전트·병렬 도구 없이 Read/Grep으로 전체 변경분(32개 파일)을 순차 확인했습니다.

이전 지적 2건 — 모두 정정 확인

  1. features/notifications/api/messaging.tsmessagingPromise 싱글턴 영구 고장 문제가 catch 블록에서 messagingPromise = null로 되돌리는 방식으로 정정되었습니다(L45-59). 재시도 가능한 상태로 복원되어, 일시 장애 후에도 다음 호출에서 재초기화가 됩니다.
  2. pages/profile/ProfilePanel.tsx의 토스트 좌표 겹침 문제 — ProfilePanel의 자체 토스트 블록을 완전히 제거하고, 신규 push-notice-store(zustand, 단일 슬롯)로 토글 안내(denied/error)를 PushNoticeHost에 위임해 포그라운드 수신과 한 스택으로 통합했습니다. 두 안내가 동시에 뜨는 케이스까지 push-notice-host.smoke.test.tsx에 회귀 테스트로 고정되어 있습니다(L104-116).

추가로 react-doctor가 지적한 server-sequential-independent-await(SW 등록과 메시징 초기화 순차 대기)도 Promise.all로 병렬화되어 함께 해소되었습니다(fetchFcmToken, L67-77).

남은 react-doctor 경고 2건 (query-mutation-missing-invalidation, use-push-toggle.ts L58·L79) — 대응 불필요로 판단
이 두 mutation(enable/disable)은 react-query 캐시가 아니라 usePushTokenStore(zustand)로 상태를 동기화하고 있어(setToken/clearToken이 스토어와 저장소를 함께 갱신), 무효화할 관련 쿼리 캐시가 없습니다. 자동 린터의 오탐으로 판단되며 별도 수정은 불필요해 보입니다(확신 — 코드 구조상 명확).

추가로 확인한 부분 (기존에 잘 되어 있음, 신규 이슈 없음)

  • use-foreground-messages.ts의 구독 해제 경쟁 가드(disposed 플래그)와 use-push-token-sync.ts의 언마운트 경쟁 가드(cancelled 플래그) 모두 견고합니다.
  • push-sync.ts(decideSyncAction/shouldAutoSync)와 push-toggle.ts(derivePushToggleState) 순수 함수는 조건 분기가 명확하고 테스트로 경계값까지 커버되어 있습니다.
  • use-auth-mutations.ts의 로그아웃 fcmToken 동봉 로직(fcmTokenStorage.get() → body 동봉 → onSettled에서 비움)에 특이사항 없습니다.
  • config/firebase.ts의 공개 상수 커밋은 스펙 승인 예외로 문서화되어 있고 보안 이슈로 보지 않습니다.
  • firebase-messaging-sw.js는 env 무의존 설계 그대로이며 변경 없습니다.

결론: 이번 fix 커밋은 이전 리뷰에서 나온 2건의 🟡 이슈와 react-doctor 경고 1건을 모두 정확하게 해소했고, 각각 재현 테스트로 회귀를 고정했습니다. 새로 발견된 이슈는 없습니다. 병합에 지장 없는 상태로 판단합니다.

const showNotice = usePushNoticeStore((s) => s.showToggleNotice);
const dismissNotice = usePushNoticeStore((s) => s.dismissToggleNotice);

const enable = useMutation({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/query-mutation-missing-invalidation (warning)

useMutation with no cache update here can leave your users looking at stale data after it runs.

Fix → Add onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] }) so cached data stays in sync after the mutation

Docs

onError: () => showNotice("error"),
});

const disable = useMutation({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/query-mutation-missing-invalidation (warning)

useMutation with no cache update here can leave your users looking at stale data after it runs.

Fix → Add onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] }) so cached data stays in sync after the mutation

Docs

@claude

claude Bot commented Aug 18, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — PR #60 (MSG-408 FCM 웹 푸시, 재검토)

리뷰 범위: git diff origin/develop...HEAD 기준 32개 변경 파일. 이번 라운드는 최신 fix 커밋(4225f7d)이 직전 리뷰(0fdbe6c 대상)에서 나온 지적을 어떻게 반영했는지 중심으로, 서브에이전트·병렬 도구 없이 Read/Grep으로 순차 확인했습니다.

👍 잘한 점

  • 이전 리뷰 지적 2건이 모두 정확히 정정됨.
    1. features/notifications/api/messaging.tsmessagingPromise 싱글턴이 초기화 실패 시 rejected promise를 영구 캐싱하던 문제 — catch 블록에서 messagingPromise = null로 되돌려 다음 호출에서 재시도가 가능하도록 정정됐습니다(L45-59).
    2. pages/profile/ProfilePanel.tsx의 토글 안내 토스트와 PushNoticeHost 포그라운드 토스트가 같은 좌표에 겹치던 문제 — ProfilePanel의 자체 토스트 블록을 완전히 제거하고, 신규 push-notice-store(zustand 단일 슬롯)로 안내를 PushNoticeHost에 위임해 한 스택으로 통합했습니다. 두 안내 동시 발생 케이스까지 push-notice-host.smoke.test.tsx(L104-116)에 회귀 테스트로 고정했습니다.
  • react-doctor의 server-sequential-independent-await 경고(SW 등록과 메시징 초기화가 서로 독립인데 순차 대기)도 fetchFcmToken에서 Promise.all로 병렬화해 함께 해소했습니다(messaging.ts L67-77).
  • use-foreground-messages.ts의 구독 해제 경쟁 가드(disposed)와 use-push-token-sync.ts의 언마운트 경쟁 가드(cancelled)가 견고하고, 각각 재현 테스트가 있습니다.
  • use-auth-mutations.ts 로그아웃의 fcmToken 동봉(onSettled에서 성공·실패 무관 보관 비움) 로직에 특이사항 없습니다.

🟢 확인했으나 대응 불필요로 판단 (사소한 제안)

  • react-doctor가 남긴 query-mutation-missing-invalidation 경고 2건(use-push-toggle.ts L58, L79)은, 해당 mutation(enable/disable)이 react-query 캐시가 아니라 usePushTokenStore(zustand)로 상태를 동기화하고 있어 무효화할 관련 쿼리가 없습니다. 자동 린터 오탐으로 판단됩니다.

🟡/🔴 신규 이슈

  • 없습니다. 이전 라운드에서 인라인으로 남긴 2건은 이번 커밋으로 모두 해소되어 다시 지적할 내용이 없습니다.

참고 (블로킹 아님, 확신 없음)

  • config/firebase.ts의 Firebase 공개 상수 커밋은 스펙 승인 예외로 문서화되어 있어 보안 이슈로 보지 않습니다(기존 판단 유지).

결론: 이번 fix 커밋은 지적된 이슈를 정확한 근본 원인 수정 + 회귀 테스트로 해소했습니다. 병합에 지장 없는 상태로 판단합니다.

View job run

@gyuhochoime
gyuhochoime merged commit 16d7ea8 into develop Aug 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant