Skip to content

[Feat] WTH-537: FCM 웹 푸시 알림 기반 설정 - #215

Open
JIN921 wants to merge 8 commits into
developfrom
setting/WTH-537-FCM-푸시-알림-웹-설정

Hidden character warning

The head ref may contain hidden characters: "setting/WTH-537-FCM-\ud478\uc2dc-\uc54c\ub9bc-\uc6f9-\uc124\uc815"
Open

JIN921 wants to merge 8 commits into
developfrom
setting/WTH-537-FCM-푸시-알림-웹-설정

Conversation

@JIN921

@JIN921 JIN921 commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

이 pr만 빼서 릴리즈하기 번거로우니까 일단 머지 안 하고 둘게요!!

✅ PR 유형

어떤 변경 사항이 있었나요?

  • 새로운 기능 추가
  • 버그 수정
  • 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
  • 코드 리팩토링
  • 주석 추가 및 수정
  • 문서 수정
  • 빌드 부분 혹은 패키지 매니저 수정
  • 파일 혹은 폴더명 수정
  • 파일 혹은 폴더 삭제

📌 관련 이슈번호

  • Closed #537

✅ Key Changes

FCM 기반 웹 푸시 알림을 도입했습니다.

기반 설정 (e7f24189)

  • Firebase SDK 초기화 및 config/VAPID 로딩 (lib/firebase/config.ts, lib/firebase/messaging.ts)
  • 백그라운드 수신 서비스워커 추가 (public/firebase-messaging-sw.js) — config는 쿼리스트링 주입
  • 토큰 발급·등록·해제 로직 (lib/push/webPush.ts), 플랫폼 판별(platform.ts), 사용 의도 저장(preference.ts)
  • 토큰 등록/해제 API (lib/apis/notification.ts)

동작 연결

  • 04b45374 웹 푸시 로직을 action 레이어 기준으로 분리
  • a47a22ec 로그인 직후 토큰 자동 등록 (FcmAutoRegister — 인증/메인 레이아웃에 마운트)
  • 8e54d928 마이페이지 고객지원에 알림설정 토글 추가 (NotificationSettingItem)
  • e39936f1 로그아웃 시 토큰 해제(revoke) 연결

문서/정리

  • FCM 웹 푸시 테스트 절차·결과·함정 문서화 (docs/로그/FCM-웹푸시-테스트-핸드오프.md)
  • prettier 포맷 정리

ℹ️ 동작 범위: 일반 브라우저(PC Chrome/Edge/Firefox, Android Chrome)에서 동작. 네이티브(Capacitor WebView)·iOS Safari(비PWA)는 미지원 — 추후 네이티브 푸시 경로에서 별도 처리.
ℹ️ 남은 작업: 백엔드 발송 로직(service account), 포그라운드 수신 핸들러 프로덕션 연결. 백엔드 발송 규격은 문서 6·7번 참고.


📸 스크린샷 or 실행영상


🎸 기타 사항 or 추가 코멘트

  • 사전 체크: typecheck ✅ / lint ✅ (0 errors) / format:check ✅ / build ✅ 통과
  • test: 817 통과 / 4 실패 — 실패는 전부 admin/member/MemberPageContent.test.tsx(어드민 멤버, 본 변경과 무관)의 waitFor 타임아웃이며 이 브랜치와 무관한 기존 이슈입니다. FCM 관련 테스트는 모두 통과.
  • 수신 검증 시 Windows 집중 지원/방해 금지가 켜져 있으면 토스트가 억제됩니다 (문서 5번 함정 참고)

JIN921 and others added 8 commits October 8, 2026 11:39
- firebase SDK 설치 및 앱 초기화(config/messaging) 모듈 추가
- 백그라운드 수신 서비스워커 추가(config는 쿼리스트링으로 주입)
- 토큰 등록/해제 API(notification) 추가
- 웹/네이티브 분기용 platform 유틸 추가
- 권한요청→토큰발급→백엔드등록 + 포그라운드 수신 useFcmToken 훅 추가

네이티브(Capacitor) 경로는 WebView Web Push 미지원으로 추후 플러그인 연동 예정

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- webPush.ts: 권한요청/토큰발급·등록/조건부 등록/해제를 React 밖에서도
  재사용 가능한 plain 함수로 분리 (로그인 자동등록·로그아웃 revoke에서 재사용)
- preference.ts: 코드로 해제 불가한 브라우저 권한을 보완할 알림 사용 의도 저장
- useFcmToken: action 레이어 위의 얇은 래퍼(enable/disable + 포그라운드 수신)로 정리

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- FcmAutoRegister: 인증 영역 진입 시 권한 상태/사용자 의도에 따라
  토큰을 조용히 재등록하거나 1회 권한 요청
- 인증 레이아웃과 클럽 메인 레이아웃에 마운트 (페이지 로드당 1회 가드)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NotificationSettingItem: 토글 ON(권한요청·토큰등록)/OFF(토큰해제) 제어
- 브라우저 권한은 코드로 해제 불가하므로 사용자 의도(preference)와 함께 판단
- 고객지원 섹션 최상단에 배치

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 쿠키/스토리지 정리 전에 revokeCurrentToken 호출 (revoke API가 인증 쿠키 필요)
- best-effort 처리로 실패해도 로그아웃 흐름은 진행

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
다른 세션/계정에서 이어서 알림 수신 테스트를 진행할 수 있도록
현재 상태·전제조건·테스트 절차·함정·백엔드 협의사항 정리

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PC Chrome 백그라운드 수신 확인 완료 (2026-10-19)
- Firebase 콘솔 UI 변경 반영 (새 알림 만들기 → 새 캠페인)
- 토큰 선택(체크/추가) 후 테스트 버튼 활성화 단계 명시
- Windows 알림 차단(집중 지원/방해 금지) 함정 추가 — 이번 테스트 실제 원인
- 세팅 완료 항목과 남은 작업(백엔드 발송, 포그라운드 핸들러) 구분

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- webPush.ts, NotificationSettingItem.tsx 줄바꿈 정리 (로직 변화 없음)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added the ⚙ Setting 개발 환경 세팅 label Oct 19, 2026
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e7a9d77c-4751-43dd-a838-7d96569c709a
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@JIN921 JIN921 self-assigned this Oct 19, 2026
@github-actions

Copy link
Copy Markdown

PR 테스트 결과

✅ Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-ec9gjw86y-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 검증 결과

✅ TypeScript: 통과
✅ ESLint: 통과
❌ Prettier: 실패
✅ Build: 통과

⚠️ 일부 검증에 실패했습니다. 확인 후 수정해주세요.

@nabbang6 nabbang6 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

확인이 늦었습니다....ㅠㅠ 전반적으로 깔끔 꼼꼼하게 잘 설정해주셔서 큰 문제는 없을 것 같아요 너무 고생하셧습니다~!!! 👍

그리고 어드민 LNB 하단 프로필 쪽에서 알림 표시되는 부분은 주언님께서 디자인 잡아주셧어서, 제가 전에 admin/layout/NotificationModal.tsx <요 컴포넌트로 UI만 구현해둿엇어요! 추후 연결 작업하실 때 요거 사용하셔두 괜찮을 것 같아서 참고용으로 적어두어요,,,ㅎㅎ 파잇띵 ✊👊

Comment on lines +13 to +27
let hasRun = false;

/**
* 로그인 직후(인증 영역 진입 시) 웹 푸시 토큰을 등록하는 비가시 컴포넌트.
*
* - 권한 granted: 조용히 토큰 재등록 (토큰 회전/계정 변경 대응)
* - 권한 default + 미결정: 1회 권한 요청 (로그인 직후 프롬프트)
* - 권한 denied 또는 사용자가 알림 끔(disabled): 아무것도 하지 않음
*
* 네이티브(Capacitor)에서는 동작하지 않으며, 추후 플러그인 경로에서 별도 처리한다.
*/
export function FcmAutoRegister() {
useEffect(() => {
if (hasRun || isNativeApp()) return;
hasRun = true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

요기 hasRun이 한 세션 동안 true로 유지되고 잇어서, 사용자가 로그아웃 -> 페이지 새로고침을 하지 않고 재로그인했을 때 notificationApi.registerToken이 호출되지 않음 -> fcm 토큰이 새로 등록되지 않는 문제가 잇는 것 같습니다!

재로그인 후 푸시 알림이 동작하지 않는 상황이 있을 수 잇으니...
아래처럼 수정해주면 조금 더 안전할 것 같아용 b

  // 모듈 플래그 제거, ref로 교체 → 컴포넌트 인스턴스 생애 기준으로 관리
  export function FcmAutoRegister() {
    const hasRun = useRef(false);

    useEffect(() => {
      if (hasRun.current || isNativeApp()) return;
      hasRun.current = true;
      // ... 이하 동일
    }, []);
    return null;
  }

Comment thread src/hooks/useLogout.ts
Comment on lines 13 to 14
function clearBrowserStorage() {
localStorage.clear();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

로그아웃할 때 로컬스토리지 전체가 초기화되면서 weeth:fcm-preference 키도 함께 제거되고 잇는 것 같아요!
요것 때문에 사용자가 알림을 끄더라도 로그아웃 -> 재로그인 시 권한 요청 알림이 반복적으로 발생하고 잇는 것 같습니닷

  // FCM 사용 의도는 디바이스 단위 설정이므로 로그아웃 시에도 보존
  const FCM_PREFERENCE_KEY = 'weeth:fcm-preference';

  function clearBrowserStorage() {
    const fcmPref = localStorage.getItem(FCM_PREFERENCE_KEY);
    localStorage.clear();
    sessionStorage.clear();
    clearClientCookies();
    if (fcmPref) localStorage.setItem(FCM_PREFERENCE_KEY,
  fcmPref);
  }

요로코롬... fcm 키는 유지되게 처리해줌 ux 측면에서 좋을 것 같아용

Comment on lines +46 to +49
if ('focus' in client) {
client.navigate(targetUrl);
return client.focus();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

포커스처럼 client.navigate(targetUrl) 부분도 await 처리 해주면 좋을 것 같습니당!

@github-actions

Copy link
Copy Markdown

PR 테스트 결과

✅ Jest: 통과

🎉 모든 테스트를 통과했습니다!

@github-actions

Copy link
Copy Markdown

구현한 기능 Preview: https://weeth-iilswag3d-weethsite-4975s-projects.vercel.app

@github-actions

Copy link
Copy Markdown

PR 검증 결과

✅ TypeScript: 통과
✅ ESLint: 통과
✅ Prettier: 통과
✅ Build: 통과

🎉 모든 검증을 통과했습니다!

@woneeeee woneeeee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셨습니다~.~ 유진님 깃허브 프사 바꾸셨네용 ㅎ.ㅎ💕

self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
for (const client of clientList) {
if ('focus' in client) {
client.navigate(targetUrl);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이 SW 스코프(/firebase-cloud-messaging-push-scope)는 앱 페이지를 제어하지 않아서 matchAll로 잡힌 클라이언트가 전부 uncontrolled라고 합니다! 이러면 client.navigate()가 타입 에러로 거절돼서 focus()까지 못 가고, 클릭해도 아무 일도 안 일어날 가능성이 잇다고 합니당,,, ㅠㅠㅠ

focus() 후 postMessage로 앱이 라우팅하거나, navigate 실패 시 openWindow로 폴백시켜주면 조을 것 같아용!! data.link는 same-origin 검증도 부탁드려요. 클로드가 이것도 하면 좋다고 합니다 ㅎㅎ

Comment thread src/lib/push/webPush.ts
Comment on lines +87 to +107
export async function revokeCurrentToken(): Promise<void> {
if (!isWebPushSupportable() || Notification.permission !== 'granted') return;
const messaging = await getMessagingInstance();
if (!messaging) return;

try {
const registration = await getExistingRegistration();
if (registration && FIREBASE_VAPID_KEY) {
const token = await getToken(messaging, {
vapidKey: FIREBASE_VAPID_KEY,
serviceWorkerRegistration: registration,
});
if (token) {
await notificationApi.revokeToken({ token });
}
}
await deleteToken(messaging);
} catch {
// 로그아웃 흐름을 막지 않도록 실패는 조용히 무시
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

토글 OFF 때 revoke가 실패해도 catch에서 삼켜서, UI는 껐다곻 하는데 서버에 토큰이 남는다구 합니다...!! revoke가 throw하면 deleteToken도 건너뛴다네용,,, 권한이 granted가 아니면 revoke 자체를 건너뛰는 것도 공용 기기에서 이전 사용자의 알림이 남는다는 큰 이슈가 잇을수도 잇어요! 코어는 throw하고 로그아웃 쪽에서만 best-effort로 감싸는 구조로 하면 더 안정성이 높아진다고 하니 그런 식으로 진행해도 좋을 것 같아요~

@dalzzy dalzzy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

수고하셧습니다~!!

Comment thread src/hooks/useFcmToken.ts
Comment on lines +56 to +63
getMessagingInstance().then((messaging) => {
if (!messaging) return;
unsubscribe = onMessage(messaging, (payload) => {
onForegroundMessageRef.current?.(payload);
});
});

return () => unsubscribe?.();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

getMessagingInstance()가 완료되기 전에 컴포넌트가 언마운트되면, cleanup 시점에는 unsubscribe가 없어 해제가 실행되지 않고 이후 .then()에서 구독이 생성될 수 있을 것 같습니다! cleanup에서 disposed 플래그를 설정하고, .then()에서 해당 값을 확인해 구독 등록을 막으면 어떨까용??

Comment thread src/lib/push/webPush.ts
Comment on lines +99 to +105
if (token) {
await notificationApi.revokeToken({ token });
}
}
await deleteToken(messaging);
} catch {
// 로그아웃 흐름을 막지 않도록 실패는 조용히 무시

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이 부분에서 백엔드 해제가 실패해도 오류를 무시해서 알림 설정 화면에서는 성공으로 처리되는 것 같습니다!
revoke 실패 시 deleteToken()도 건너뛰어 토큰이 남을 수 있을 것 같은데, 설정 화면에서는 실패를 감지할 수 있도록 오류를 전달하고, 로그아웃에서만 실패를 무시하도록 분리하면 어떨까요??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

어멋 위에 똑같은 리뷰를 남겨주셧네요 ㅎㅎ

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚙ Setting 개발 환경 세팅

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants