From a497487308aec5618e34e38556fab5a25d5795a4 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 16:43:32 +0900 Subject: [PATCH 01/24] =?UTF-8?q?:bug:[fix]:=20CSP=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index a9104b3..540e701 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,16 @@ { "routes": [ { "src": "/[^.]+", "dest": "/", "status": 200 } + ], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.gstatic.com; connect-src 'self' https://firebaseinstallations.googleapis.com https://fcmregistrations.googleapis.com https://fcm.googleapis.com https://identitytoolkit.googleapis.com https://securetoken.googleapis.com https://firestore.googleapis.com https://www.googleapis.com https://www.gstatic.com https://*.googleapis.com; img-src 'self' data: https://*.gstatic.com https://*.googleapis.com; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:;" + } + ] + } ] -} \ No newline at end of file +} From 5bbda10c3d4bdfd6774bccd6d43053a5d1d00bc1 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 16:51:41 +0900 Subject: [PATCH 02/24] =?UTF-8?q?:bug:[fix]:=20routes=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 540e701..57400f0 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { - "routes": [ - { "src": "/[^.]+", "dest": "/", "status": 200 } + "rewrites": [ + { "source": "/:path*", "destination": "/" } ], "headers": [ { From 9df7a8710a883497239199fa85b5f753be574e2d Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 17:23:35 +0900 Subject: [PATCH 03/24] =?UTF-8?q?:bug:[fix]:=20useFCM=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 105 +++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 35 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 66c15ae..9919eda 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,48 +1,83 @@ import { useEffect, useState } from 'react'; -import { getToken, onMessage } from 'firebase/messaging'; +import { getToken, isSupported, onMessage } from 'firebase/messaging'; import { messaging } from '@utils/firebase'; const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; +const BASE = (import.meta as any).env?.BASE_URL || '/'; export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); useEffect(() => { - // 서비스워커 등록 - navigator.serviceWorker - .register('/firebase-messaging-sw.js') - .then(registration => { - console.log('Service Worker registered:', registration); - - // 알림 권한 요청 - Notification.requestPermission().then(permission => { - if (permission === 'granted') { - // FCM 토큰 요청 - getToken(messaging, { - vapidKey: VAPID_KEY, - serviceWorkerRegistration: registration, - }) - .then(token => { - if (token) { - console.log('FCM Token:', token); - setFcmToken(token); - } else { - console.warn('No token available'); - } - }) - .catch(err => console.error('Token error:', err)); - } else { - console.warn('알림 권한 거부됨'); - } + let unsubscribeOnMessage: (() => void) | undefined; + + (async () => { + try { + // 0) 환경/브라우저 체크 + if (!(await isSupported())) { + console.warn('[FCM] This environment does not support FCM.'); + return; + } + if (!window.isSecureContext) { + console.error('[FCM] FCM requires HTTPS (secure context).'); + return; + } + if (!('Notification' in window)) { + console.error('[FCM] Notification API not available.'); + return; + } + if (!VAPID_KEY) { + console.error('[FCM] Missing VAPID key (VITE_FIREBASE_VAPID_KEY).'); + return; + } + + // 1) 서비스 워커 등록 (배포 베이스에 맞춰 경로/스코프 설정) + const swUrl = new URL( + 'firebase-messaging-sw.js', + window.location.origin + BASE, + ).pathname; + const registration = await navigator.serviceWorker.register(swUrl, { + scope: BASE, }); - }) - .catch(err => { - console.error('Service Worker registration failed:', err); - }); - - onMessage(messaging, payload => { - console.log('Foreground FCM message:', payload); - }); + console.log('[FCM] SW registered with scope:', registration.scope); + + // 2) SW 활성화까지 대기 (경합 방지) + await navigator.serviceWorker.ready; + console.log('[FCM] SW ready'); + + // 3) 알림 권한 요청 (가능하면 버튼 클릭 등 사용자 제스처에서 호출 권장) + const permission = await Notification.requestPermission(); + console.log('[FCM] Notification permission:', permission); + if (permission !== 'granted') { + console.warn('[FCM] Permission not granted.'); + return; + } + + // 4) 토큰 발급 + const token = await getToken(messaging, { + vapidKey: VAPID_KEY, + serviceWorkerRegistration: registration, + }); + if (token) { + console.log('[FCM] Token:', token); + setFcmToken(token); + } else { + console.warn('[FCM] No token available (getToken returned empty).'); + } + + // 5) 포그라운드 메시지 + unsubscribeOnMessage = onMessage(messaging, payload => { + console.log('[FCM] Foreground message:', payload); + }); + } catch (err) { + console.error('[FCM] getToken error:', err); + } + })(); + + // 클린업: 포그라운드 리스너 해제 + return () => { + if (unsubscribeOnMessage) unsubscribeOnMessage(); + }; }, []); return fcmToken; From 6c3edea0c5439009d8ea3631e9e9ff0ee32a1361 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:15:52 +0900 Subject: [PATCH 04/24] =?UTF-8?q?:bug:[fix]:=20debug=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/firebase-messaging-sw.js | 9 ++++ src/App.tsx | 3 ++ src/pages/DebugFCM.tsx | 74 +++++++++++++++++++++++++++++++++ src/utils/firebase.ts | 2 +- 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/pages/DebugFCM.tsx diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 5bf98f5..05f7e0a 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -46,4 +46,13 @@ messaging.onBackgroundMessage(function (payload) { }; self.registration.showNotification(title, notificationOptions); + + self.addEventListener('message', event => { + if (event?.data?.type === 'GET_SW_FIREBASE_OPTIONS') { + // SW의 firebase.app().options를 페이지로 전달 + const options = firebase.app().options; + // event.source는 같은 오리진 클라이언트 + event.source?.postMessage({ type: 'SW_FIREBASE_OPTIONS', options }); + } + }); }); diff --git a/src/App.tsx b/src/App.tsx index d716211..6be26a8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import { CallActive, } from '@pages/index'; import { ProtectedRoute, ScrollToTop } from '@components/index'; +import DebugFCM from '@pages/DebugFCM'; const App = () => { return ( @@ -47,6 +48,8 @@ const App = () => { } /> } /> } /> + + } /> } /> diff --git a/src/pages/DebugFCM.tsx b/src/pages/DebugFCM.tsx new file mode 100644 index 0000000..f4a3c1c --- /dev/null +++ b/src/pages/DebugFCM.tsx @@ -0,0 +1,74 @@ +import { useEffect, useState } from 'react'; +import { getToken, isSupported } from 'firebase/messaging'; +import { app, messaging } from '@utils/firebase'; + +type SWOptions = { [k: string]: any } | null; + +export default function DebugFCM() { + const [secure, setSecure] = useState(null); + const [support, setSupport] = useState(null); + const [permission, setPermission] = useState(null); + const [pageOptions, setPageOptions] = useState(null); + const [swOptions, setSwOptions] = useState(null); + const [vapidPrefix, setVapidPrefix] = useState(''); + const [installationsReachable, setInstallationsReachable] = useState('pending'); + const [tokenResult, setTokenResult] = useState('not-started'); + + useEffect(() => { + (async () => { + setSecure(window.isSecureContext); + setSupport(await isSupported()); + setPermission(Notification.permission); + setPageOptions(app.options); + setVapidPrefix((import.meta as any).env?.VITE_FIREBASE_VAPID_KEY?.slice(0, 16) || '(missing)'); + + // 1) SW에 프로젝트 설정 요청(페이지 콘솔 없이 화면에 뿌림) + navigator.serviceWorker.controller?.postMessage({ type: 'GET_SW_FIREBASE_OPTIONS' }); + navigator.serviceWorker.addEventListener('message', (e: MessageEvent) => { + if (e.data?.type === 'SW_FIREBASE_OPTIONS') setSwOptions(e.data.options || null); + }); + + // 2) Installations 도달성 핑 (CSP/프록시 차단 여부 대략 확인) + try { + await fetch('https://firebaseinstallations.googleapis.com/.well-known/assetlinks.json', { mode: 'no-cors' }); + setInstallationsReachable('ok (opaque)'); + } catch (e: any) { + setInstallationsReachable('blocked: ' + (e?.message || e)); + } + + // 3) 실제 토큰 시도 + try { + const reg = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/' }); + await navigator.serviceWorker.ready; + if (Notification.permission !== 'granted') { + const p = await Notification.requestPermission(); + setPermission(p); + if (p !== 'granted') { setTokenResult('permission denied'); return; } + } + const t = await getToken(messaging, { vapidKey: (import.meta as any).env?.VITE_FIREBASE_VAPID_KEY, serviceWorkerRegistration: reg }); + setTokenResult(t ? 'token OK' : 'no token'); + } catch (e: any) { + setTokenResult('getToken error: ' + (e?.message || e)); + } + })(); + }, []); + + return ( +
+

FCM Diagnostics

+
secureContext: {String(secure)}
+
isSupported: {String(support)}
+
Notification.permission: {permission}
+
VAPID key prefix: {vapidPrefix}
+
+
[Page] app.options
+
{JSON.stringify(pageOptions, null, 2)}
+
[Service Worker] app.options
+
{JSON.stringify(swOptions, null, 2)}
+
+
Installations ping: {installationsReachable}
+
getToken(): {tokenResult}
+

이 페이지 스크린샷만 주셔도 진단됩니다.

+
+ ); +} diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index c6bf02b..fd7491b 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -14,4 +14,4 @@ const firebaseConfig = { const app = initializeApp(firebaseConfig); const messaging = getMessaging(app); -export { messaging }; +export { app, messaging }; From 9010c8838821376789a853ce6830ce3d32045c66 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:20:30 +0900 Subject: [PATCH 05/24] =?UTF-8?q?:bug:[fix]:=20Debug=20=EC=9C=84=EC=B9=98?= =?UTF-8?q?=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6be26a8..14e3606 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ const App = () => { } /> } /> } /> + } /> }> {/* Call */} @@ -48,8 +49,6 @@ const App = () => { } /> } /> } /> - - } /> } /> From 3ac53353e7765a6b94c818d2674b41f34fac7719 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:32:33 +0900 Subject: [PATCH 06/24] =?UTF-8?q?:bug:[fix]:=20=EC=84=A4=EC=B9=98=20?= =?UTF-8?q?=EC=A7=81=ED=9B=84=20=EC=A0=9C=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/firebase-messaging-sw.js | 46 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 05f7e0a..499fcee 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -17,42 +17,40 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -// install event +// 새 SW가 즉시 페이지를 제어하도록 보장 self.addEventListener('install', () => { + self.skipWaiting(); console.log('[Service Worker] installed'); }); -// activate event -self.addEventListener('activate', e => { - console.log('[Service Worker] actived', e); +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()); + console.log('[Service Worker] activated'); }); -// fetch event +// ---- fetch: 진단용 로그 self.addEventListener('fetch', e => { - console.log('[Service Worker] fetched resource ' + e.request.url); + console.log('[Service Worker] fetched resource', e.request.url); }); -messaging.onBackgroundMessage(function (payload) { - console.log( - '[firebase-messaging-sw.js] Received background message ', - payload, - ); +// ---- 페이지 <-> SW 진단 메시지 +self.addEventListener('message', event => { + if (event?.data?.type === 'GET_SW_FIREBASE_OPTIONS') { + const options = firebase.app().options; + event.source?.postMessage({ type: 'SW_FIREBASE_OPTIONS', options }); + } +}); - const { title, body } = payload.notification; +// ---- FCM 백그라운드 메시지 +messaging.onBackgroundMessage(payload => { + console.log('[SW] Received background message', payload); + const { title, body, icon } = payload.notification || {}; + const notificationTitle = title || '알림'; const notificationOptions = { - body, - icon: 'icons/icon-24x24.svg', + body: body || '', + icon: icon || 'icons/icon-24x24.svg', }; - self.registration.showNotification(title, notificationOptions); - - self.addEventListener('message', event => { - if (event?.data?.type === 'GET_SW_FIREBASE_OPTIONS') { - // SW의 firebase.app().options를 페이지로 전달 - const options = firebase.app().options; - // event.source는 같은 오리진 클라이언트 - event.source?.postMessage({ type: 'SW_FIREBASE_OPTIONS', options }); - } - }); + self.registration.showNotification(notificationTitle, notificationOptions); }); From 69c6f4c5975d3972f9b7f703c173f35f1238818d Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:44:51 +0900 Subject: [PATCH 07/24] =?UTF-8?q?:bug:[fix]:=20IndexedDB=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/main.tsx b/src/main.tsx index 6a6258e..4f005dd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,42 @@ import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; import '@utils/setupFetchInterceptor'; +(async () => { + try { + // IndexedDB 테스트 + const openReq = indexedDB.open('___probe', 1); + await new Promise((res, rej) => { + openReq.onerror = () => rej(openReq.error); + openReq.onsuccess = () => res(null); + }); + console.log('[diag] IndexedDB OK'); + } catch (e) { + console.error('[diag] IndexedDB FAILED:', e); + } + + // Installations API 직접 호출 + try { + const projectId = 'alzheimerdinger-b9e53'; + const url = `https://firebaseinstallations.googleapis.com/v1/projects/${projectId}/installations`; + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // Firebase API Key + 'x-goog-api-key': 'AIzaSyClSEPibfp07m4Qvjix1nJjzEwSEyOJK54', + }, + body: JSON.stringify({ + appId: '1:832024980689:web:fead7061b8fe4378ece9f0', + sdkVersion: 'w:12.0.0', // firebase SDK 버전 + }), + }); + console.log('[diag] Installations API status:', resp.status); + console.log('[diag] Installations API body:', await resp.text()); + } catch (err) { + console.error('[diag] Installations API error:', err); + } +})(); + const GlobalStyle = createGlobalStyle` body { margin: 0; From e1add53c7b980f348876f60f4a536f50155d66b0 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:49:37 +0900 Subject: [PATCH 08/24] =?UTF-8?q?:bug:[fix]:=20setupFetchInterceptor=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index 4f005dd..64e8a11 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; -import '@utils/setupFetchInterceptor'; +// import '@utils/setupFetchInterceptor'; (async () => { try { From fb8bb2b77e1557299306680eeb4191e7dc685786 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 18:55:28 +0900 Subject: [PATCH 09/24] =?UTF-8?q?:bug:[fix]:=20FCM=20PushManager=20?= =?UTF-8?q?=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/manifest.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/public/manifest.json b/public/manifest.json index 82e4024..407449a 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -5,6 +5,8 @@ "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", + "gcm_sender_id": "103953800507", + "icons": [ { "src": "icons/icon-24x24.svg", From c0b2e56f66edc42b12c93437c338d6d47223f391 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 19:45:37 +0900 Subject: [PATCH 10/24] =?UTF-8?q?:bug:[fix]:=20firebase-messaging-sw.js=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/firebase-messaging-sw.js | 35 ++++----------------------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 499fcee..22bdbd9 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -17,40 +17,13 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -// 새 SW가 즉시 페이지를 제어하도록 보장 -self.addEventListener('install', () => { - self.skipWaiting(); - console.log('[Service Worker] installed'); -}); - -self.addEventListener('activate', event => { - event.waitUntil(self.clients.claim()); - console.log('[Service Worker] activated'); -}); - -// ---- fetch: 진단용 로그 -self.addEventListener('fetch', e => { - console.log('[Service Worker] fetched resource', e.request.url); -}); +self.addEventListener('install', () => self.skipWaiting()); +self.addEventListener('activate', e => e.waitUntil(self.clients.claim())); -// ---- 페이지 <-> SW 진단 메시지 -self.addEventListener('message', event => { - if (event?.data?.type === 'GET_SW_FIREBASE_OPTIONS') { - const options = firebase.app().options; - event.source?.postMessage({ type: 'SW_FIREBASE_OPTIONS', options }); - } -}); - -// ---- FCM 백그라운드 메시지 messaging.onBackgroundMessage(payload => { - console.log('[SW] Received background message', payload); - const { title, body, icon } = payload.notification || {}; - const notificationTitle = title || '알림'; - const notificationOptions = { + self.registration.showNotification(title || '알림', { body: body || '', icon: icon || 'icons/icon-24x24.svg', - }; - - self.registration.showNotification(notificationTitle, notificationOptions); + }); }); From 1e1bc841494776c22f40e2a56179e2ab4e779979 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 20:09:51 +0900 Subject: [PATCH 11/24] =?UTF-8?q?:bug:[fix]:=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=EA=B5=AC=EB=8F=85=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 81 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 9919eda..3d35fef 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -2,8 +2,12 @@ import { useEffect, useState } from 'react'; import { getToken, isSupported, onMessage } from 'firebase/messaging'; import { messaging } from '@utils/firebase'; -const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; -const BASE = (import.meta as any).env?.BASE_URL || '/'; +/** + * FCM 토큰을 발급하고, 포그라운드 메시지를 수신하는 훅 + * - 루트 스코프(/)에 있는 /firebase-messaging-sw.js 를 사용 + * - VAPID 키 공백/개행 문제 방지 + * - 푸시 구독(PushManager.subscribe) 선검증 → 막히면 원인 로그로 바로 확인 + */ export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); @@ -13,7 +17,7 @@ export const useFCM = () => { (async () => { try { - // 0) 환경/브라우저 체크 + // 0) 환경/브라우저 지원 체크 if (!(await isSupported())) { console.warn('[FCM] This environment does not support FCM.'); return; @@ -26,26 +30,36 @@ export const useFCM = () => { console.error('[FCM] Notification API not available.'); return; } + + // 1) VAPID 키 확인 (공백/개행 제거) + const VAPID_RAW = import.meta.env.VITE_FIREBASE_VAPID_KEY as + | string + | undefined; + const VAPID_KEY = VAPID_RAW?.trim(); if (!VAPID_KEY) { console.error('[FCM] Missing VAPID key (VITE_FIREBASE_VAPID_KEY).'); return; } - // 1) 서비스 워커 등록 (배포 베이스에 맞춰 경로/스코프 설정) - const swUrl = new URL( - 'firebase-messaging-sw.js', - window.location.origin + BASE, - ).pathname; + // 2) 서비스워커 등록 (루트 고정 권장) + const swUrl = '/firebase-messaging-sw.js'; const registration = await navigator.serviceWorker.register(swUrl, { - scope: BASE, + scope: '/', }); console.log('[FCM] SW registered with scope:', registration.scope); - // 2) SW 활성화까지 대기 (경합 방지) + // 중복/경합 확인용 + const regs = await navigator.serviceWorker.getRegistrations(); + console.log( + '[FCM] existing SW scopes:', + regs.map(r => r.scope), + ); + + // SW 활성화 대기 await navigator.serviceWorker.ready; console.log('[FCM] SW ready'); - // 3) 알림 권한 요청 (가능하면 버튼 클릭 등 사용자 제스처에서 호출 권장) + // 3) 알림 권한 요청 const permission = await Notification.requestPermission(); console.log('[FCM] Notification permission:', permission); if (permission !== 'granted') { @@ -53,11 +67,33 @@ export const useFCM = () => { return; } - // 4) 토큰 발급 - const token = await getToken(messaging, { - vapidKey: VAPID_KEY, - serviceWorkerRegistration: registration, - }); + // 4) 푸시 구독 선검증 + const appServerKey = urlBase64ToUint8Array(VAPID_KEY); + try { + const sub = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: appServerKey, + }); + console.log('[FCM] push subscribe OK:', { + endpoint: sub.endpoint, + hasAuth: !!sub.getKey('auth'), + hasP256dh: !!sub.getKey('p256dh'), + }); + } catch (e: any) { + console.error( + '[FCM] push subscribe FAILED:', + e?.name ?? 'Error', + e?.message ?? e, + ); + // 흔한 에러: + // NotAllowedError: 브라우저/OS 알림 차단 + // NotSupportedError: 브라우저/정책이 푸시 미지원 + // AbortError/InvalidStateError: SW/스코프 충돌 + return; + } + + // 5) 토큰 발급 (SDK가 루트 SW를 스스로 찾게 registration 인자 생략) + const token = await getToken(messaging, { vapidKey: VAPID_KEY }); if (token) { console.log('[FCM] Token:', token); setFcmToken(token); @@ -65,7 +101,7 @@ export const useFCM = () => { console.warn('[FCM] No token available (getToken returned empty).'); } - // 5) 포그라운드 메시지 + // 6) 포그라운드 메시지 핸들러 unsubscribeOnMessage = onMessage(messaging, payload => { console.log('[FCM] Foreground message:', payload); }); @@ -74,7 +110,6 @@ export const useFCM = () => { } })(); - // 클린업: 포그라운드 리스너 해제 return () => { if (unsubscribeOnMessage) unsubscribeOnMessage(); }; @@ -82,3 +117,13 @@ export const useFCM = () => { return fcmToken; }; + +/** Base64URL → Uint8Array */ +function urlBase64ToUint8Array(base64String: string) { + const padding = '='.repeat((4 - (base64String.length % 4)) % 4); + const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); + const raw = atob(base64); + const out = new Uint8Array(raw.length); + for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); + return out; +} From 97c50368985a1569a0c0a6e2b3ea76deb536b3d0 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 20:14:38 +0900 Subject: [PATCH 12/24] =?UTF-8?q?:bug:[fix]::bug:[fix]:=20=20fcmregistrati?= =?UTF-8?q?ons=20=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 222 +++++++++++++++++++++++++++++++++----------- 1 file changed, 170 insertions(+), 52 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 3d35fef..2617710 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,14 +1,19 @@ import { useEffect, useState } from 'react'; -import { getToken, isSupported, onMessage } from 'firebase/messaging'; -import { messaging } from '@utils/firebase'; +import { + getToken as getFcmToken, + isSupported, + onMessage, +} from 'firebase/messaging'; +import { + getInstallations, + getToken as getFisToken, +} from 'firebase/installations'; +import { messaging, app as firebaseApp } from '@utils/firebase'; /** - * FCM 토큰을 발급하고, 포그라운드 메시지를 수신하는 훅 - * - 루트 스코프(/)에 있는 /firebase-messaging-sw.js 를 사용 - * - VAPID 키 공백/개행 문제 방지 - * - 푸시 구독(PushManager.subscribe) 선검증 → 막히면 원인 로그로 바로 확인 + * 1) 표준 getToken()으로 시도 + * 2) 실패 시, 수동 흐름(푸시 구독 → FIS 토큰 → fcmregistrations POST)으로 FCM 토큰 획득 시도 */ - export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); @@ -17,7 +22,7 @@ export const useFCM = () => { (async () => { try { - // 0) 환경/브라우저 지원 체크 + // --- 환경 체크 if (!(await isSupported())) { console.warn('[FCM] This environment does not support FCM.'); return; @@ -31,7 +36,7 @@ export const useFCM = () => { return; } - // 1) VAPID 키 확인 (공백/개행 제거) + // --- VAPID 키 확보 (공백/개행 제거) const VAPID_RAW = import.meta.env.VITE_FIREBASE_VAPID_KEY as | string | undefined; @@ -41,25 +46,16 @@ export const useFCM = () => { return; } - // 2) 서비스워커 등록 (루트 고정 권장) + // --- SW 등록 (루트 고정) const swUrl = '/firebase-messaging-sw.js'; const registration = await navigator.serviceWorker.register(swUrl, { scope: '/', }); console.log('[FCM] SW registered with scope:', registration.scope); - - // 중복/경합 확인용 - const regs = await navigator.serviceWorker.getRegistrations(); - console.log( - '[FCM] existing SW scopes:', - regs.map(r => r.scope), - ); - - // SW 활성화 대기 await navigator.serviceWorker.ready; console.log('[FCM] SW ready'); - // 3) 알림 권한 요청 + // --- 권한 const permission = await Notification.requestPermission(); console.log('[FCM] Notification permission:', permission); if (permission !== 'granted') { @@ -67,46 +63,66 @@ export const useFCM = () => { return; } - // 4) 푸시 구독 선검증 - const appServerKey = urlBase64ToUint8Array(VAPID_KEY); + // --- 우선 푸시 구독(수동 경로에서도 필요) + const sub = await ensurePushSubscribed(registration, VAPID_KEY); + if (!sub) return; // 실패 로그는 함수 내부에서 출력 + + // --- 1차: 표준 getToken try { - const sub = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: appServerKey, - }); - console.log('[FCM] push subscribe OK:', { - endpoint: sub.endpoint, - hasAuth: !!sub.getKey('auth'), - hasP256dh: !!sub.getKey('p256dh'), - }); + const token = await getFcmToken(messaging, { vapidKey: VAPID_KEY }); + if (token) { + console.log('[FCM] getToken OK:', token); + setFcmToken(token); + } else { + console.warn('[FCM] getToken returned empty.'); + } } catch (e: any) { - console.error( - '[FCM] push subscribe FAILED:', - e?.name ?? 'Error', - e?.message ?? e, - ); - // 흔한 에러: - // NotAllowedError: 브라우저/OS 알림 차단 - // NotSupportedError: 브라우저/정책이 푸시 미지원 - // AbortError/InvalidStateError: SW/스코프 충돌 - return; - } + console.error('[FCM] getToken error:', e); - // 5) 토큰 발급 (SDK가 루트 SW를 스스로 찾게 registration 인자 생략) - const token = await getToken(messaging, { vapidKey: VAPID_KEY }); - if (token) { - console.log('[FCM] Token:', token); - setFcmToken(token); - } else { - console.warn('[FCM] No token available (getToken returned empty).'); + // --- 2차: 수동 경로(진단 + 우회) + const apiKey = import.meta.env.VITE_FIREBASE_API_KEY as + | string + | undefined; + if (!apiKey) { + console.error( + '[FCM] Missing API key (VITE_FIREBASE_API_KEY). Cannot try manual registration.', + ); + return; + } + + try { + const fisToken = await getFisAuthToken(firebaseApp); + if (!fisToken) { + console.error('[probe] No FIS auth token. Abort.'); + return; + } + + const manual = await manualWebpushRegistration( + sub, + fisToken, + apiKey, + ); + if (manual.ok) { + console.log('[probe] fcmregistrations OK:', manual.token); + setFcmToken(manual.token!); + } else { + console.error( + '[probe] fcmregistrations FAILED:', + manual.status, + manual.body, + ); + } + } catch (err) { + console.error('[probe] manual registration error:', err); + } } - // 6) 포그라운드 메시지 핸들러 + // --- 포그라운드 메시지 unsubscribeOnMessage = onMessage(messaging, payload => { console.log('[FCM] Foreground message:', payload); }); } catch (err) { - console.error('[FCM] getToken error:', err); + console.error('[FCM] fatal error:', err); } })(); @@ -118,7 +134,109 @@ export const useFCM = () => { return fcmToken; }; -/** Base64URL → Uint8Array */ +/** PushManager.subscribe 보장 + 상세 로그 */ +async function ensurePushSubscribed( + registration: ServiceWorkerRegistration, + vapidKey: string, +) { + try { + const existing = await registration.pushManager.getSubscription(); + if (existing) { + console.log('[FCM] push already subscribed:', { + endpoint: existing.endpoint, + hasAuth: !!existing.getKey('auth'), + hasP256dh: !!existing.getKey('p256dh'), + }); + return existing; + } + + const sub = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapidKey), + }); + console.log('[FCM] push subscribe OK:', { + endpoint: sub.endpoint, + hasAuth: !!sub.getKey('auth'), + hasP256dh: !!sub.getKey('p256dh'), + }); + return sub; + } catch (e: any) { + console.error( + '[FCM] push subscribe FAILED:', + e?.name ?? 'Error', + e?.message ?? e, + ); + return null; + } +} + +/** FIS auth 토큰 획득 (Installations) */ +async function getFisAuthToken(app: any) { + const installations = getInstallations(app); + try { + const token = await getFisToken(installations, /* forceRefresh */ true); + console.log('[probe] FIS token OK (length):', token?.length); + return token; + } catch (e) { + console.error('[probe] FIS token FAILED:', e); + return null; + } +} + +/** fcmregistrations 수동 호출 */ +async function manualWebpushRegistration( + sub: PushSubscription, + fisToken: string, + apiKey: string, +): Promise<{ ok: boolean; status?: number; body?: string; token?: string }> { + // body 구성 + const body = { + web: { + endpoint: sub.endpoint, + p256dh: b64(sub.getKey('p256dh')!), + auth: b64(sub.getKey('auth')!), + }, + }; + + const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent( + apiKey, + )}`; + + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + // 핵심: FIS 토큰을 Authorization 헤더로 + Authorization: `FIS ${fisToken}`, + }, + body: JSON.stringify(body), + }); + + const text = await resp.text(); + + // 성공 시 응답에 FCM token 포함 + if (resp.ok) { + try { + const json = JSON.parse(text); + const token = json?.token as string | undefined; + return { ok: true, token }; + } catch { + return { ok: true, body: text }; + } + } + + return { ok: false, status: resp.status, body: text }; +} + +/** util: Base64 encode Uint8Array */ +function b64(key: ArrayBuffer) { + const arr = new Uint8Array(key); + let s = ''; + for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]); + return btoa(s); +} + +/** util: Base64URL → Uint8Array */ function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); From 03970fe81ade315e3131683b945590edffa69b12 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 20:23:04 +0900 Subject: [PATCH 13/24] =?UTF-8?q?:bug:[fix]:=20SDK=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=8B=9C=20REST=20=EC=9A=B0=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 304 ++++++++++++++++++++------------------------ 1 file changed, 139 insertions(+), 165 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 2617710..8dc01bc 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -4,16 +4,11 @@ import { isSupported, onMessage, } from 'firebase/messaging'; -import { - getInstallations, - getToken as getFisToken, -} from 'firebase/installations'; -import { messaging, app as firebaseApp } from '@utils/firebase'; - -/** - * 1) 표준 getToken()으로 시도 - * 2) 실패 시, 수동 흐름(푸시 구독 → FIS 토큰 → fcmregistrations POST)으로 FCM 토큰 획득 시도 - */ +import { messaging } from '@utils/firebase'; + +// firebaseConfig는 네 utils/firebase에서 initializeApp 한 그 값 +// 필요하면 여기서 불러오거나 아래 REST 함수에서 messaging.app.options로 읽음 + export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); @@ -22,107 +17,100 @@ export const useFCM = () => { (async () => { try { - // --- 환경 체크 - if (!(await isSupported())) { - console.warn('[FCM] This environment does not support FCM.'); - return; - } - if (!window.isSecureContext) { - console.error('[FCM] FCM requires HTTPS (secure context).'); - return; - } - if (!('Notification' in window)) { - console.error('[FCM] Notification API not available.'); - return; - } - - // --- VAPID 키 확보 (공백/개행 제거) - const VAPID_RAW = import.meta.env.VITE_FIREBASE_VAPID_KEY as - | string - | undefined; - const VAPID_KEY = VAPID_RAW?.trim(); - if (!VAPID_KEY) { - console.error('[FCM] Missing VAPID key (VITE_FIREBASE_VAPID_KEY).'); + if (!(await isSupported())) return; + if (!window.isSecureContext) return; + if (!('Notification' in window)) return; + + const VAPID = ( + import.meta.env.VITE_FIREBASE_VAPID_KEY as string | undefined + )?.trim(); + if (!VAPID) { + console.error('[FCM] Missing VAPID'); return; } - // --- SW 등록 (루트 고정) - const swUrl = '/firebase-messaging-sw.js'; - const registration = await navigator.serviceWorker.register(swUrl, { - scope: '/', - }); - console.log('[FCM] SW registered with scope:', registration.scope); + // SW 등록: 루트 고정 + const reg = await navigator.serviceWorker.register( + '/firebase-messaging-sw.js', + { scope: '/' }, + ); await navigator.serviceWorker.ready; - console.log('[FCM] SW ready'); - // --- 권한 - const permission = await Notification.requestPermission(); - console.log('[FCM] Notification permission:', permission); - if (permission !== 'granted') { - console.warn('[FCM] Permission not granted.'); + // 권한 + const perm = await Notification.requestPermission(); + if (perm !== 'granted') { + console.warn('[FCM] permission not granted'); return; } - // --- 우선 푸시 구독(수동 경로에서도 필요) - const sub = await ensurePushSubscribed(registration, VAPID_KEY); - if (!sub) return; // 실패 로그는 함수 내부에서 출력 + // 푸시 구독(있으면 재사용) + const sub = + (await reg.pushManager.getSubscription()) ?? + (await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(VAPID), + })); - // --- 1차: 표준 getToken + // 1) 표준 경로 try { - const token = await getFcmToken(messaging, { vapidKey: VAPID_KEY }); + const token = await getFcmToken(messaging, { vapidKey: VAPID }); if (token) { - console.log('[FCM] getToken OK:', token); setFcmToken(token); - } else { - console.warn('[FCM] getToken returned empty.'); + console.log('[FCM] getToken OK'); } - } catch (e: any) { - console.error('[FCM] getToken error:', e); - - // --- 2차: 수동 경로(진단 + 우회) - const apiKey = import.meta.env.VITE_FIREBASE_API_KEY as - | string - | undefined; - if (!apiKey) { - console.error( - '[FCM] Missing API key (VITE_FIREBASE_API_KEY). Cannot try manual registration.', - ); + } catch (e) { + console.warn('[FCM] getToken failed → try REST fallback', e); + + // 2) REST 우회: FIS → FCM 등록 + const fb = (messaging as any).app?.options || {}; + const apiKey: string | undefined = + ( + import.meta.env.VITE_FIREBASE_API_KEY as string | undefined + )?.trim() || fb.apiKey; + const appId: string | undefined = fb.appId; + const projectId: string | undefined = fb.projectId; + + if (!apiKey || !appId || !projectId) { + console.error('[probe] missing apiKey/appId/projectId'); + return; + } + + // 2-1) Installations 생성 (authToken 획득) + const fis = await createInstallationViaRest({ + apiKey, + appId, + projectId, + }); + if (!fis?.authToken) { + console.error('[probe] FIS REST failed'); return; } - try { - const fisToken = await getFisAuthToken(firebaseApp); - if (!fisToken) { - console.error('[probe] No FIS auth token. Abort.'); - return; - } - - const manual = await manualWebpushRegistration( - sub, - fisToken, - apiKey, + // 2-2) FCM webpush registration + const fcm = await registerWebpushViaRest({ + apiKey, + fisAuthToken: fis.authToken, + subscription: sub, + }); + + if (fcm?.token) { + console.log('[probe] fcmregistrations OK'); + setFcmToken(fcm.token); + } else { + console.error( + '[probe] fcmregistrations FAILED', + fcm?.status, + fcm?.body, ); - if (manual.ok) { - console.log('[probe] fcmregistrations OK:', manual.token); - setFcmToken(manual.token!); - } else { - console.error( - '[probe] fcmregistrations FAILED:', - manual.status, - manual.body, - ); - } - } catch (err) { - console.error('[probe] manual registration error:', err); } } - // --- 포그라운드 메시지 + // 포그라운드 수신 unsubscribeOnMessage = onMessage(messaging, payload => { - console.log('[FCM] Foreground message:', payload); + console.log('[FCM] foreground message', payload); }); } catch (err) { - console.error('[FCM] fatal error:', err); + console.error('[FCM] fatal', err); } })(); @@ -134,109 +122,89 @@ export const useFCM = () => { return fcmToken; }; -/** PushManager.subscribe 보장 + 상세 로그 */ -async function ensurePushSubscribed( - registration: ServiceWorkerRegistration, - vapidKey: string, -) { - try { - const existing = await registration.pushManager.getSubscription(); - if (existing) { - console.log('[FCM] push already subscribed:', { - endpoint: existing.endpoint, - hasAuth: !!existing.getKey('auth'), - hasP256dh: !!existing.getKey('p256dh'), - }); - return existing; - } - - const sub = await registration.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(vapidKey), - }); - console.log('[FCM] push subscribe OK:', { - endpoint: sub.endpoint, - hasAuth: !!sub.getKey('auth'), - hasP256dh: !!sub.getKey('p256dh'), - }); - return sub; - } catch (e: any) { - console.error( - '[FCM] push subscribe FAILED:', - e?.name ?? 'Error', - e?.message ?? e, - ); +// Installations REST: https://firebaseinstallations.googleapis.com/v1/projects/{projectId}/installations +async function createInstallationViaRest(opts: { + apiKey: string; + appId: string; + projectId: string; +}) { + const { apiKey, appId, projectId } = opts; + const url = `https://firebaseinstallations.googleapis.com/v1/projects/${projectId}/installations`; + + const resp = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-goog-api-key': apiKey, + }, + body: JSON.stringify({ + appId, + // firebase JS SDK 식별자 (대략 맞춰주면 됨) + sdkVersion: 'w:12.0.0', + }), + }); + + const text = await resp.text(); + if (!resp.ok) { + console.error('[probe] FIS status', resp.status, text); return null; } -} -/** FIS auth 토큰 획득 (Installations) */ -async function getFisAuthToken(app: any) { - const installations = getInstallations(app); try { - const token = await getFisToken(installations, /* forceRefresh */ true); - console.log('[probe] FIS token OK (length):', token?.length); - return token; - } catch (e) { - console.error('[probe] FIS token FAILED:', e); + const json = JSON.parse(text); + return { + fid: json?.fid as string | undefined, + refreshToken: json?.refreshToken as string | undefined, + authToken: json?.authToken?.token as string | undefined, + // expiresIn: json?.authToken?.expiresIn + }; + } catch { + console.error('[probe] FIS parse error', text); return null; } } -/** fcmregistrations 수동 호출 */ -async function manualWebpushRegistration( - sub: PushSubscription, - fisToken: string, - apiKey: string, -): Promise<{ ok: boolean; status?: number; body?: string; token?: string }> { - // body 구성 +// FCM registrations REST: https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=API_KEY +async function registerWebpushViaRest(opts: { + apiKey: string; + fisAuthToken: string; + subscription: PushSubscription; +}) { + const { apiKey, fisAuthToken, subscription } = opts; + const body = { web: { - endpoint: sub.endpoint, - p256dh: b64(sub.getKey('p256dh')!), - auth: b64(sub.getKey('auth')!), + endpoint: subscription.endpoint, + p256dh: u8ToB64(subscription.getKey('p256dh')!), + auth: u8ToB64(subscription.getKey('auth')!), }, }; - const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent( - apiKey, - )}`; - + const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent(apiKey)}`; const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', - // 핵심: FIS 토큰을 Authorization 헤더로 - Authorization: `FIS ${fisToken}`, + // 중요: FIS auth 토큰을 Authorization 헤더로 + Authorization: `FIS ${fisAuthToken}`, }, body: JSON.stringify(body), }); const text = await resp.text(); - - // 성공 시 응답에 FCM token 포함 - if (resp.ok) { - try { - const json = JSON.parse(text); - const token = json?.token as string | undefined; - return { ok: true, token }; - } catch { - return { ok: true, body: text }; - } + if (!resp.ok) { + return { status: resp.status, body: text }; } - return { ok: false, status: resp.status, body: text }; -} - -/** util: Base64 encode Uint8Array */ -function b64(key: ArrayBuffer) { - const arr = new Uint8Array(key); - let s = ''; - for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]); - return btoa(s); + try { + const json = JSON.parse(text); + return { token: json?.token as string | undefined }; + } catch { + return { body: text }; + } } -/** util: Base64URL → Uint8Array */ +// ---- utils function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); @@ -245,3 +213,9 @@ function urlBase64ToUint8Array(base64String: string) { for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); return out; } +function u8ToB64(key: ArrayBuffer) { + const arr = new Uint8Array(key); + let s = ''; + for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]); + return btoa(s); +} From af8a0e323b20768e93e08fcd51612ea5335ea6fd Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 20:31:32 +0900 Subject: [PATCH 14/24] =?UTF-8?q?:bug:[fix]:=20REST=20=EC=9A=B0=ED=9A=8C(F?= =?UTF-8?q?IS=20=E2=86=92=20FCM=20registrations)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 148 +++++++++++++++++++++++++------------------- 1 file changed, 83 insertions(+), 65 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 8dc01bc..71a7df5 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -6,9 +6,6 @@ import { } from 'firebase/messaging'; import { messaging } from '@utils/firebase'; -// firebaseConfig는 네 utils/firebase에서 initializeApp 한 그 값 -// 필요하면 여기서 불러오거나 아래 REST 함수에서 messaging.app.options로 읽음 - export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); @@ -17,97 +14,121 @@ export const useFCM = () => { (async () => { try { - if (!(await isSupported())) return; - if (!window.isSecureContext) return; - if (!('Notification' in window)) return; + // 환경 체크 + if (!(await isSupported())) { + console.warn('[FCM] This environment does not support FCM.'); + return; + } + if (!window.isSecureContext) { + console.error('[FCM] Requires HTTPS (secure context).'); + return; + } + if (!('Notification' in window)) { + console.error('[FCM] Notification API not available.'); + return; + } + // .env const VAPID = ( import.meta.env.VITE_FIREBASE_VAPID_KEY as string | undefined )?.trim(); + const API_KEY = ( + import.meta.env.VITE_FIREBASE_API_KEY as string | undefined + )?.trim(); + const APP_ID = ( + import.meta.env.VITE_FIREBASE_APP_ID as string | undefined + )?.trim(); + const PROJECT_ID = ( + import.meta.env.VITE_FIREBASE_PROJECT_ID as string | undefined + )?.trim(); + if (!VAPID) { - console.error('[FCM] Missing VAPID'); + console.error('[FCM] Missing VITE_FIREBASE_VAPID_KEY'); + return; + } + if (!API_KEY || !APP_ID || !PROJECT_ID) { + console.error('[FCM] Missing apiKey/appId/projectId in .env'); return; } - // SW 등록: 루트 고정 + // SW 등록(루트 고정) & ready const reg = await navigator.serviceWorker.register( '/firebase-messaging-sw.js', { scope: '/' }, ); + console.log('[FCM] SW registered with scope:', reg.scope); await navigator.serviceWorker.ready; + console.log('[FCM] SW ready'); // 권한 const perm = await Notification.requestPermission(); + console.log('[FCM] Notification permission:', perm); if (perm !== 'granted') { - console.warn('[FCM] permission not granted'); + console.warn('[FCM] Permission not granted.'); return; } - // 푸시 구독(있으면 재사용) + // 푸시 구독 보장(있으면 재사용) const sub = (await reg.pushManager.getSubscription()) ?? (await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array(VAPID), })); + console.log('[FCM] push subscription OK:', { + endpoint: sub.endpoint, + hasAuth: !!sub.getKey('auth'), + hasP256dh: !!sub.getKey('p256dh'), + }); - // 1) 표준 경로 + // 1) 표준 경로: registration을 반드시 명시! try { - const token = await getFcmToken(messaging, { vapidKey: VAPID }); + const token = await getFcmToken(messaging, { + vapidKey: VAPID, + serviceWorkerRegistration: reg, + }); if (token) { - setFcmToken(token); console.log('[FCM] getToken OK'); + setFcmToken(token); + // 표준 경로 성공이면 종료 + return; } + console.warn('[FCM] getToken returned empty.'); } catch (e) { console.warn('[FCM] getToken failed → try REST fallback', e); + } - // 2) REST 우회: FIS → FCM 등록 - const fb = (messaging as any).app?.options || {}; - const apiKey: string | undefined = - ( - import.meta.env.VITE_FIREBASE_API_KEY as string | undefined - )?.trim() || fb.apiKey; - const appId: string | undefined = fb.appId; - const projectId: string | undefined = fb.projectId; - - if (!apiKey || !appId || !projectId) { - console.error('[probe] missing apiKey/appId/projectId'); - return; - } - - // 2-1) Installations 생성 (authToken 획득) - const fis = await createInstallationViaRest({ - apiKey, - appId, - projectId, - }); - if (!fis?.authToken) { - console.error('[probe] FIS REST failed'); - return; - } + // 2) REST 우회: FIS → FCM registrations + const fis = await createInstallationViaRest({ + apiKey: API_KEY, + appId: APP_ID, + projectId: PROJECT_ID, + }); + if (!fis?.authToken) { + console.error('[probe] FIS REST failed'); + return; + } - // 2-2) FCM webpush registration - const fcm = await registerWebpushViaRest({ - apiKey, - fisAuthToken: fis.authToken, - subscription: sub, - }); + const fcm = await registerWebpushViaRest({ + apiKey: API_KEY, + fisAuthToken: fis.authToken, + subscription: sub, + }); - if (fcm?.token) { - console.log('[probe] fcmregistrations OK'); - setFcmToken(fcm.token); - } else { - console.error( - '[probe] fcmregistrations FAILED', - fcm?.status, - fcm?.body, - ); - } + if (fcm?.token) { + console.log('[probe] fcmregistrations OK'); + setFcmToken(fcm.token); + } else { + console.error( + '[probe] fcmregistrations FAILED', + fcm?.status, + fcm?.body, + ); } - // 포그라운드 수신 + // 포그라운드 메시지 unsubscribeOnMessage = onMessage(messaging, payload => { - console.log('[FCM] foreground message', payload); + console.log('[FCM] Foreground message:', payload); }); } catch (err) { console.error('[FCM] fatal', err); @@ -122,7 +143,7 @@ export const useFCM = () => { return fcmToken; }; -// Installations REST: https://firebaseinstallations.googleapis.com/v1/projects/{projectId}/installations +// Installations REST async function createInstallationViaRest(opts: { apiKey: string; appId: string; @@ -139,7 +160,6 @@ async function createInstallationViaRest(opts: { }, body: JSON.stringify({ appId, - // firebase JS SDK 식별자 (대략 맞춰주면 됨) sdkVersion: 'w:12.0.0', }), }); @@ -156,7 +176,6 @@ async function createInstallationViaRest(opts: { fid: json?.fid as string | undefined, refreshToken: json?.refreshToken as string | undefined, authToken: json?.authToken?.token as string | undefined, - // expiresIn: json?.authToken?.expiresIn }; } catch { console.error('[probe] FIS parse error', text); @@ -164,7 +183,7 @@ async function createInstallationViaRest(opts: { } } -// FCM registrations REST: https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=API_KEY +// FCM registrations REST async function registerWebpushViaRest(opts: { apiKey: string; fisAuthToken: string; @@ -180,21 +199,21 @@ async function registerWebpushViaRest(opts: { }, }; - const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent(apiKey)}`; + const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent( + apiKey, + )}`; + const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', - // 중요: FIS auth 토큰을 Authorization 헤더로 Authorization: `FIS ${fisAuthToken}`, }, body: JSON.stringify(body), }); const text = await resp.text(); - if (!resp.ok) { - return { status: resp.status, body: text }; - } + if (!resp.ok) return { status: resp.status, body: text }; try { const json = JSON.parse(text); @@ -204,7 +223,6 @@ async function registerWebpushViaRest(opts: { } } -// ---- utils function urlBase64ToUint8Array(base64String: string) { const padding = '='.repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); From 30528533c62bf542ace875334fbc1c64ae3d1c95 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 20:37:11 +0900 Subject: [PATCH 15/24] =?UTF-8?q?:bug:[fix]:=20=EC=9B=90=EC=83=81=EB=B3=B5?= =?UTF-8?q?=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/firebase-messaging-sw.js | 38 +++-- public/manifest.json | 1 - src/App.tsx | 2 - src/hooks/useFCM.ts | 262 +++++--------------------------- src/main.tsx | 38 +---- src/pages/DebugFCM.tsx | 74 --------- vercel.json | 17 +-- 7 files changed, 69 insertions(+), 363 deletions(-) delete mode 100644 src/pages/DebugFCM.tsx diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js index 22bdbd9..5bf98f5 100644 --- a/public/firebase-messaging-sw.js +++ b/public/firebase-messaging-sw.js @@ -17,13 +17,33 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -self.addEventListener('install', () => self.skipWaiting()); -self.addEventListener('activate', e => e.waitUntil(self.clients.claim())); - -messaging.onBackgroundMessage(payload => { - const { title, body, icon } = payload.notification || {}; - self.registration.showNotification(title || '알림', { - body: body || '', - icon: icon || 'icons/icon-24x24.svg', - }); +// install event +self.addEventListener('install', () => { + console.log('[Service Worker] installed'); +}); + +// activate event +self.addEventListener('activate', e => { + console.log('[Service Worker] actived', e); +}); + +// fetch event +self.addEventListener('fetch', e => { + console.log('[Service Worker] fetched resource ' + e.request.url); +}); + +messaging.onBackgroundMessage(function (payload) { + console.log( + '[firebase-messaging-sw.js] Received background message ', + payload, + ); + + const { title, body } = payload.notification; + + const notificationOptions = { + body, + icon: 'icons/icon-24x24.svg', + }; + + self.registration.showNotification(title, notificationOptions); }); diff --git a/public/manifest.json b/public/manifest.json index 407449a..efec34f 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -5,7 +5,6 @@ "display": "standalone", "background_color": "#ffffff", "theme_color": "#000000", - "gcm_sender_id": "103953800507", "icons": [ { diff --git a/src/App.tsx b/src/App.tsx index 14e3606..d716211 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,7 +20,6 @@ import { CallActive, } from '@pages/index'; import { ProtectedRoute, ScrollToTop } from '@components/index'; -import DebugFCM from '@pages/DebugFCM'; const App = () => { return ( @@ -33,7 +32,6 @@ const App = () => { } /> } /> } /> - } /> }> {/* Call */} diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 71a7df5..66c15ae 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,239 +1,49 @@ import { useEffect, useState } from 'react'; -import { - getToken as getFcmToken, - isSupported, - onMessage, -} from 'firebase/messaging'; +import { getToken, onMessage } from 'firebase/messaging'; import { messaging } from '@utils/firebase'; +const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; + export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); useEffect(() => { - let unsubscribeOnMessage: (() => void) | undefined; - - (async () => { - try { - // 환경 체크 - if (!(await isSupported())) { - console.warn('[FCM] This environment does not support FCM.'); - return; - } - if (!window.isSecureContext) { - console.error('[FCM] Requires HTTPS (secure context).'); - return; - } - if (!('Notification' in window)) { - console.error('[FCM] Notification API not available.'); - return; - } - - // .env - const VAPID = ( - import.meta.env.VITE_FIREBASE_VAPID_KEY as string | undefined - )?.trim(); - const API_KEY = ( - import.meta.env.VITE_FIREBASE_API_KEY as string | undefined - )?.trim(); - const APP_ID = ( - import.meta.env.VITE_FIREBASE_APP_ID as string | undefined - )?.trim(); - const PROJECT_ID = ( - import.meta.env.VITE_FIREBASE_PROJECT_ID as string | undefined - )?.trim(); - - if (!VAPID) { - console.error('[FCM] Missing VITE_FIREBASE_VAPID_KEY'); - return; - } - if (!API_KEY || !APP_ID || !PROJECT_ID) { - console.error('[FCM] Missing apiKey/appId/projectId in .env'); - return; - } - - // SW 등록(루트 고정) & ready - const reg = await navigator.serviceWorker.register( - '/firebase-messaging-sw.js', - { scope: '/' }, - ); - console.log('[FCM] SW registered with scope:', reg.scope); - await navigator.serviceWorker.ready; - console.log('[FCM] SW ready'); - - // 권한 - const perm = await Notification.requestPermission(); - console.log('[FCM] Notification permission:', perm); - if (perm !== 'granted') { - console.warn('[FCM] Permission not granted.'); - return; - } - - // 푸시 구독 보장(있으면 재사용) - const sub = - (await reg.pushManager.getSubscription()) ?? - (await reg.pushManager.subscribe({ - userVisibleOnly: true, - applicationServerKey: urlBase64ToUint8Array(VAPID), - })); - console.log('[FCM] push subscription OK:', { - endpoint: sub.endpoint, - hasAuth: !!sub.getKey('auth'), - hasP256dh: !!sub.getKey('p256dh'), - }); - - // 1) 표준 경로: registration을 반드시 명시! - try { - const token = await getFcmToken(messaging, { - vapidKey: VAPID, - serviceWorkerRegistration: reg, - }); - if (token) { - console.log('[FCM] getToken OK'); - setFcmToken(token); - // 표준 경로 성공이면 종료 - return; + // 서비스워커 등록 + navigator.serviceWorker + .register('/firebase-messaging-sw.js') + .then(registration => { + console.log('Service Worker registered:', registration); + + // 알림 권한 요청 + Notification.requestPermission().then(permission => { + if (permission === 'granted') { + // FCM 토큰 요청 + getToken(messaging, { + vapidKey: VAPID_KEY, + serviceWorkerRegistration: registration, + }) + .then(token => { + if (token) { + console.log('FCM Token:', token); + setFcmToken(token); + } else { + console.warn('No token available'); + } + }) + .catch(err => console.error('Token error:', err)); + } else { + console.warn('알림 권한 거부됨'); } - console.warn('[FCM] getToken returned empty.'); - } catch (e) { - console.warn('[FCM] getToken failed → try REST fallback', e); - } - - // 2) REST 우회: FIS → FCM registrations - const fis = await createInstallationViaRest({ - apiKey: API_KEY, - appId: APP_ID, - projectId: PROJECT_ID, }); - if (!fis?.authToken) { - console.error('[probe] FIS REST failed'); - return; - } - - const fcm = await registerWebpushViaRest({ - apiKey: API_KEY, - fisAuthToken: fis.authToken, - subscription: sub, - }); - - if (fcm?.token) { - console.log('[probe] fcmregistrations OK'); - setFcmToken(fcm.token); - } else { - console.error( - '[probe] fcmregistrations FAILED', - fcm?.status, - fcm?.body, - ); - } - - // 포그라운드 메시지 - unsubscribeOnMessage = onMessage(messaging, payload => { - console.log('[FCM] Foreground message:', payload); - }); - } catch (err) { - console.error('[FCM] fatal', err); - } - })(); - - return () => { - if (unsubscribeOnMessage) unsubscribeOnMessage(); - }; + }) + .catch(err => { + console.error('Service Worker registration failed:', err); + }); + + onMessage(messaging, payload => { + console.log('Foreground FCM message:', payload); + }); }, []); return fcmToken; }; - -// Installations REST -async function createInstallationViaRest(opts: { - apiKey: string; - appId: string; - projectId: string; -}) { - const { apiKey, appId, projectId } = opts; - const url = `https://firebaseinstallations.googleapis.com/v1/projects/${projectId}/installations`; - - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'x-goog-api-key': apiKey, - }, - body: JSON.stringify({ - appId, - sdkVersion: 'w:12.0.0', - }), - }); - - const text = await resp.text(); - if (!resp.ok) { - console.error('[probe] FIS status', resp.status, text); - return null; - } - - try { - const json = JSON.parse(text); - return { - fid: json?.fid as string | undefined, - refreshToken: json?.refreshToken as string | undefined, - authToken: json?.authToken?.token as string | undefined, - }; - } catch { - console.error('[probe] FIS parse error', text); - return null; - } -} - -// FCM registrations REST -async function registerWebpushViaRest(opts: { - apiKey: string; - fisAuthToken: string; - subscription: PushSubscription; -}) { - const { apiKey, fisAuthToken, subscription } = opts; - - const body = { - web: { - endpoint: subscription.endpoint, - p256dh: u8ToB64(subscription.getKey('p256dh')!), - auth: u8ToB64(subscription.getKey('auth')!), - }, - }; - - const url = `https://fcmregistrations.googleapis.com/v1/webpush/registrations?key=${encodeURIComponent( - apiKey, - )}`; - - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `FIS ${fisAuthToken}`, - }, - body: JSON.stringify(body), - }); - - const text = await resp.text(); - if (!resp.ok) return { status: resp.status, body: text }; - - try { - const json = JSON.parse(text); - return { token: json?.token as string | undefined }; - } catch { - return { body: text }; - } -} - -function urlBase64ToUint8Array(base64String: string) { - const padding = '='.repeat((4 - (base64String.length % 4)) % 4); - const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/'); - const raw = atob(base64); - const out = new Uint8Array(raw.length); - for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i); - return out; -} -function u8ToB64(key: ArrayBuffer) { - const arr = new Uint8Array(key); - let s = ''; - for (let i = 0; i < arr.length; i++) s += String.fromCharCode(arr[i]); - return btoa(s); -} diff --git a/src/main.tsx b/src/main.tsx index 64e8a11..6a6258e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,43 +1,7 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; -// import '@utils/setupFetchInterceptor'; - -(async () => { - try { - // IndexedDB 테스트 - const openReq = indexedDB.open('___probe', 1); - await new Promise((res, rej) => { - openReq.onerror = () => rej(openReq.error); - openReq.onsuccess = () => res(null); - }); - console.log('[diag] IndexedDB OK'); - } catch (e) { - console.error('[diag] IndexedDB FAILED:', e); - } - - // Installations API 직접 호출 - try { - const projectId = 'alzheimerdinger-b9e53'; - const url = `https://firebaseinstallations.googleapis.com/v1/projects/${projectId}/installations`; - const resp = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - // Firebase API Key - 'x-goog-api-key': 'AIzaSyClSEPibfp07m4Qvjix1nJjzEwSEyOJK54', - }, - body: JSON.stringify({ - appId: '1:832024980689:web:fead7061b8fe4378ece9f0', - sdkVersion: 'w:12.0.0', // firebase SDK 버전 - }), - }); - console.log('[diag] Installations API status:', resp.status); - console.log('[diag] Installations API body:', await resp.text()); - } catch (err) { - console.error('[diag] Installations API error:', err); - } -})(); +import '@utils/setupFetchInterceptor'; const GlobalStyle = createGlobalStyle` body { diff --git a/src/pages/DebugFCM.tsx b/src/pages/DebugFCM.tsx deleted file mode 100644 index f4a3c1c..0000000 --- a/src/pages/DebugFCM.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useEffect, useState } from 'react'; -import { getToken, isSupported } from 'firebase/messaging'; -import { app, messaging } from '@utils/firebase'; - -type SWOptions = { [k: string]: any } | null; - -export default function DebugFCM() { - const [secure, setSecure] = useState(null); - const [support, setSupport] = useState(null); - const [permission, setPermission] = useState(null); - const [pageOptions, setPageOptions] = useState(null); - const [swOptions, setSwOptions] = useState(null); - const [vapidPrefix, setVapidPrefix] = useState(''); - const [installationsReachable, setInstallationsReachable] = useState('pending'); - const [tokenResult, setTokenResult] = useState('not-started'); - - useEffect(() => { - (async () => { - setSecure(window.isSecureContext); - setSupport(await isSupported()); - setPermission(Notification.permission); - setPageOptions(app.options); - setVapidPrefix((import.meta as any).env?.VITE_FIREBASE_VAPID_KEY?.slice(0, 16) || '(missing)'); - - // 1) SW에 프로젝트 설정 요청(페이지 콘솔 없이 화면에 뿌림) - navigator.serviceWorker.controller?.postMessage({ type: 'GET_SW_FIREBASE_OPTIONS' }); - navigator.serviceWorker.addEventListener('message', (e: MessageEvent) => { - if (e.data?.type === 'SW_FIREBASE_OPTIONS') setSwOptions(e.data.options || null); - }); - - // 2) Installations 도달성 핑 (CSP/프록시 차단 여부 대략 확인) - try { - await fetch('https://firebaseinstallations.googleapis.com/.well-known/assetlinks.json', { mode: 'no-cors' }); - setInstallationsReachable('ok (opaque)'); - } catch (e: any) { - setInstallationsReachable('blocked: ' + (e?.message || e)); - } - - // 3) 실제 토큰 시도 - try { - const reg = await navigator.serviceWorker.register('/firebase-messaging-sw.js', { scope: '/' }); - await navigator.serviceWorker.ready; - if (Notification.permission !== 'granted') { - const p = await Notification.requestPermission(); - setPermission(p); - if (p !== 'granted') { setTokenResult('permission denied'); return; } - } - const t = await getToken(messaging, { vapidKey: (import.meta as any).env?.VITE_FIREBASE_VAPID_KEY, serviceWorkerRegistration: reg }); - setTokenResult(t ? 'token OK' : 'no token'); - } catch (e: any) { - setTokenResult('getToken error: ' + (e?.message || e)); - } - })(); - }, []); - - return ( -
-

FCM Diagnostics

-
secureContext: {String(secure)}
-
isSupported: {String(support)}
-
Notification.permission: {permission}
-
VAPID key prefix: {vapidPrefix}
-
-
[Page] app.options
-
{JSON.stringify(pageOptions, null, 2)}
-
[Service Worker] app.options
-
{JSON.stringify(swOptions, null, 2)}
-
-
Installations ping: {installationsReachable}
-
getToken(): {tokenResult}
-

이 페이지 스크린샷만 주셔도 진단됩니다.

-
- ); -} diff --git a/vercel.json b/vercel.json index 57400f0..a9104b3 100644 --- a/vercel.json +++ b/vercel.json @@ -1,16 +1,5 @@ { - "rewrites": [ - { "source": "/:path*", "destination": "/" } - ], - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'self' 'unsafe-inline' https://www.gstatic.com; connect-src 'self' https://firebaseinstallations.googleapis.com https://fcmregistrations.googleapis.com https://fcm.googleapis.com https://identitytoolkit.googleapis.com https://securetoken.googleapis.com https://firestore.googleapis.com https://www.googleapis.com https://www.gstatic.com https://*.googleapis.com; img-src 'self' data: https://*.gstatic.com https://*.googleapis.com; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:;" - } - ] - } + "routes": [ + { "src": "/[^.]+", "dest": "/", "status": 200 } ] -} +} \ No newline at end of file From dd077d3199839221af52d4e5ecb358966d2055e7 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 21:02:21 +0900 Subject: [PATCH 16/24] =?UTF-8?q?:bug:[fix]:=20sw.js=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/firebase-messaging-sw.js | 49 ------------------------- public/sw.js | 43 ++++++++++++++++++++++ src/hooks/useFCM.ts | 64 ++++++++++++++++----------------- 3 files changed, 74 insertions(+), 82 deletions(-) delete mode 100644 public/firebase-messaging-sw.js create mode 100644 public/sw.js diff --git a/public/firebase-messaging-sw.js b/public/firebase-messaging-sw.js deleted file mode 100644 index 5bf98f5..0000000 --- a/public/firebase-messaging-sw.js +++ /dev/null @@ -1,49 +0,0 @@ -importScripts( - 'https://www.gstatic.com/firebasejs/12.0.0/firebase-app-compat.js', -); -importScripts( - 'https://www.gstatic.com/firebasejs/12.0.0/firebase-messaging-compat.js', -); - -firebase.initializeApp({ - apiKey: 'AIzaSyClSEPibfp07m4Qvjix1nJjzEwSEyOJK54', - authDomain: 'alzheimerdinger-b9e53.firebaseapp.com', - projectId: 'alzheimerdinger-b9e53', - storageBucket: 'alzheimerdinger-b9e53.firebasestorage.app', - messagingSenderId: '832024980689', - appId: '1:832024980689:web:fead7061b8fe4378ece9f0', - measurementId: 'G-DE3S7XGP8Q', -}); - -const messaging = firebase.messaging(); - -// install event -self.addEventListener('install', () => { - console.log('[Service Worker] installed'); -}); - -// activate event -self.addEventListener('activate', e => { - console.log('[Service Worker] actived', e); -}); - -// fetch event -self.addEventListener('fetch', e => { - console.log('[Service Worker] fetched resource ' + e.request.url); -}); - -messaging.onBackgroundMessage(function (payload) { - console.log( - '[firebase-messaging-sw.js] Received background message ', - payload, - ); - - const { title, body } = payload.notification; - - const notificationOptions = { - body, - icon: 'icons/icon-24x24.svg', - }; - - self.registration.showNotification(title, notificationOptions); -}); diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..1c7888a --- /dev/null +++ b/public/sw.js @@ -0,0 +1,43 @@ +// FCM SDK 로드 +importScripts( + 'https://www.gstatic.com/firebasejs/12.0.0/firebase-app-compat.js', +); +importScripts( + 'https://www.gstatic.com/firebasejs/12.0.0/firebase-messaging-compat.js', +); + +// Firebase 초기화 +firebase.initializeApp({ + apiKey: 'AIzaSyClSEPibfp07m4Qvjix1nJjzEwSEyOJK54', + authDomain: 'alzheimerdinger-b9e53.firebaseapp.com', + projectId: 'alzheimerdinger-b9e53', + storageBucket: 'alzheimerdinger-b9e53.firebasestorage.app', + messagingSenderId: '832024980689', + appId: '1:832024980689:web:fead7061b8fe4378ece9f0', + measurementId: 'G-DE3S7XGP8Q', +}); + +const messaging = firebase.messaging(); + +// PWA 이벤트 예시 (원래 Vite PWA SW 코드 여기에 병합) +self.addEventListener('install', event => { + console.log('[SW] Installed', event); +}); + +self.addEventListener('activate', event => { + console.log('[SW] Activated', event); +}); + +self.addEventListener('fetch', () => { + // PWA 캐싱 로직 등 필요시 추가 +}); + +// FCM 백그라운드 메시지 수신 +messaging.onBackgroundMessage(payload => { + console.log('[SW] Received background message ', payload); + const { title, body, icon } = payload.notification || {}; + self.registration.showNotification(title || '알림', { + body: body || '', + icon: icon || '/icons/icon-192x192.png', + }); +}); diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 66c15ae..37b39f5 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { getToken, onMessage } from 'firebase/messaging'; +import { getToken, onMessage, isSupported } from 'firebase/messaging'; import { messaging } from '@utils/firebase'; const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; @@ -8,41 +8,39 @@ export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); useEffect(() => { - // 서비스워커 등록 - navigator.serviceWorker - .register('/firebase-messaging-sw.js') - .then(registration => { - console.log('Service Worker registered:', registration); + (async () => { + if (!(await isSupported())) { + console.warn('[FCM] 브라우저 미지원'); + return; + } - // 알림 권한 요청 - Notification.requestPermission().then(permission => { - if (permission === 'granted') { - // FCM 토큰 요청 - getToken(messaging, { - vapidKey: VAPID_KEY, - serviceWorkerRegistration: registration, - }) - .then(token => { - if (token) { - console.log('FCM Token:', token); - setFcmToken(token); - } else { - console.warn('No token available'); - } - }) - .catch(err => console.error('Token error:', err)); - } else { - console.warn('알림 권한 거부됨'); - } + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + console.warn('[FCM] 알림 권한 거부'); + return; + } + + try { + // PWA에서 이미 등록된 sw.js 사용 + const registration = await navigator.serviceWorker.ready; + const token = await getToken(messaging, { + vapidKey: VAPID_KEY, + serviceWorkerRegistration: registration, }); - }) - .catch(err => { - console.error('Service Worker registration failed:', err); - }); + if (token) { + console.log('[FCM] Token:', token); + setFcmToken(token); + } else { + console.warn('[FCM] No token available'); + } + } catch (err) { + console.error('[FCM] getToken error:', err); + } - onMessage(messaging, payload => { - console.log('Foreground FCM message:', payload); - }); + onMessage(messaging, payload => { + console.log('[FCM] Foreground message:', payload); + }); + })(); }, []); return fcmToken; From ec8a20d8728f319a1cd2a7bfeb96ca6b572bbdcf Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 21:22:40 +0900 Subject: [PATCH 17/24] =?UTF-8?q?:bug:[fix]:=20sw.js=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 13 +++++++++---- src/hooks/useFCM.ts | 34 ++++++++++++++++++++-------------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/public/sw.js b/public/sw.js index 1c7888a..2b6cecf 100644 --- a/public/sw.js +++ b/public/sw.js @@ -19,22 +19,27 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -// PWA 이벤트 예시 (원래 Vite PWA SW 코드 여기에 병합) +// SW 설치 self.addEventListener('install', event => { console.log('[SW] Installed', event); + self.skipWaiting(); }); +// SW 활성화 self.addEventListener('activate', event => { console.log('[SW] Activated', event); + return self.clients.claim(); }); -self.addEventListener('fetch', () => { - // PWA 캐싱 로직 등 필요시 추가 +// fetch 이벤트 +self.addEventListener('fetch', event => { + console.log('[SW] fetched resource:', event.request.url); }); // FCM 백그라운드 메시지 수신 messaging.onBackgroundMessage(payload => { - console.log('[SW] Received background message ', payload); + console.log('[SW] Received background message', payload); + const { title, body, icon } = payload.notification || {}; self.registration.showNotification(title || '알림', { body: body || '', diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 37b39f5..064c466 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -10,36 +10,42 @@ export const useFCM = () => { useEffect(() => { (async () => { if (!(await isSupported())) { - console.warn('[FCM] 브라우저 미지원'); - return; - } - - const permission = await Notification.requestPermission(); - if (permission !== 'granted') { - console.warn('[FCM] 알림 권한 거부'); + console.warn('[FCM] 브라우저가 FCM을 지원하지 않음'); return; } try { - // PWA에서 이미 등록된 sw.js 사용 - const registration = await navigator.serviceWorker.ready; + // 새 SW 등록 + const registration = await navigator.serviceWorker.register('/sw.js'); + console.log('[FCM] Service Worker registered:', registration); + + // 알림 권한 요청 + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + console.warn('[FCM] 알림 권한 거부됨'); + return; + } + + // 토큰 발급 const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: registration, }); + if (token) { console.log('[FCM] Token:', token); setFcmToken(token); } else { - console.warn('[FCM] No token available'); + console.warn('[FCM] 토큰 없음'); } + + // 포그라운드 메시지 수신 + onMessage(messaging, payload => { + console.log('[FCM] Foreground message:', payload); + }); } catch (err) { console.error('[FCM] getToken error:', err); } - - onMessage(messaging, payload => { - console.log('[FCM] Foreground message:', payload); - }); })(); }, []); From 5a27b7bc50293ccb7d32be13280e115c33b0a1fd Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 21:26:54 +0900 Subject: [PATCH 18/24] =?UTF-8?q?:bug:[fix]:=20=ED=99=9C=EC=84=B1=ED=99=94?= =?UTF-8?q?=20=EB=8C=80=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 064c466..ba62e24 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -10,23 +10,21 @@ export const useFCM = () => { useEffect(() => { (async () => { if (!(await isSupported())) { - console.warn('[FCM] 브라우저가 FCM을 지원하지 않음'); + console.warn('[FCM] 브라우저 미지원'); return; } - try { - // 새 SW 등록 - const registration = await navigator.serviceWorker.register('/sw.js'); - console.log('[FCM] Service Worker registered:', registration); - - // 알림 권한 요청 - const permission = await Notification.requestPermission(); - if (permission !== 'granted') { - console.warn('[FCM] 알림 권한 거부됨'); - return; - } + await navigator.serviceWorker.register('/sw.js'); + await navigator.serviceWorker.ready; + + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + console.warn('[FCM] 알림 권한 거부됨'); + return; + } - // 토큰 발급 + try { + const registration = await navigator.serviceWorker.ready; const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: registration, @@ -36,10 +34,9 @@ export const useFCM = () => { console.log('[FCM] Token:', token); setFcmToken(token); } else { - console.warn('[FCM] 토큰 없음'); + console.warn('[FCM] No token available'); } - // 포그라운드 메시지 수신 onMessage(messaging, payload => { console.log('[FCM] Foreground message:', payload); }); From 72cfb75c8c6fcaea9a1ee64ef7ac1a029dcb51eb Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 21:34:20 +0900 Subject: [PATCH 19/24] =?UTF-8?q?:bug:[fix]:=20fetch=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=EB=84=88=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/public/sw.js b/public/sw.js index 2b6cecf..36a4965 100644 --- a/public/sw.js +++ b/public/sw.js @@ -19,27 +19,19 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -// SW 설치 self.addEventListener('install', event => { console.log('[SW] Installed', event); self.skipWaiting(); }); -// SW 활성화 self.addEventListener('activate', event => { console.log('[SW] Activated', event); - return self.clients.claim(); + self.clients.claim(); }); -// fetch 이벤트 -self.addEventListener('fetch', event => { - console.log('[SW] fetched resource:', event.request.url); -}); - -// FCM 백그라운드 메시지 수신 +// 백그라운드 푸시 표시 messaging.onBackgroundMessage(payload => { console.log('[SW] Received background message', payload); - const { title, body, icon } = payload.notification || {}; self.registration.showNotification(title || '알림', { body: body || '', From 05fafec180b76db358cc96fca6f591c5e0cf5521 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 21:59:02 +0900 Subject: [PATCH 20/24] =?UTF-8?q?:bug:[fix]:=20=EB=94=94=EB=B2=84=EA=B9=85?= =?UTF-8?q?=20=EC=8B=A0=ED=98=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/useFCM.ts | 24 +++++++++++++++++++++--- src/main.tsx | 15 +++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index ba62e24..4350e97 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,6 +1,9 @@ import { useEffect, useState } from 'react'; import { getToken, onMessage, isSupported } from 'firebase/messaging'; -import { messaging } from '@utils/firebase'; +import { messaging, app } from '@utils/firebase'; + +// Installations 직접 확인용 +import { getInstallations, getId } from 'firebase/installations'; const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; @@ -14,17 +17,32 @@ export const useFCM = () => { return; } + // 1) SW 등록 + 활성화 확실히 대기 await navigator.serviceWorker.register('/sw.js'); - await navigator.serviceWorker.ready; + const registration = await navigator.serviceWorker.ready; + console.log('[FCM] SW ready →', registration); + + // 2) Installations(FID) 선확인: 여기서 막히면 getToken도 실패 + try { + const installations = getInstallations(app); + const fid = await getId(installations); + console.log('[diag] FID:', fid); + } catch (e) { + console.error( + '[diag] getId(installations) error → Installations 단계에서 막힘', + e, + ); + } + // 3) 권한 요청 const permission = await Notification.requestPermission(); if (permission !== 'granted') { console.warn('[FCM] 알림 권한 거부됨'); return; } + // 4) getToken (반드시 SW ready 사용) try { - const registration = await navigator.serviceWorker.ready; const token = await getToken(messaging, { vapidKey: VAPID_KEY, serviceWorkerRegistration: registration, diff --git a/src/main.tsx b/src/main.tsx index 6a6258e..19fd6dc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,21 @@ import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; import '@utils/setupFetchInterceptor'; +// Firebase SDK 디버그 로그 +import { setLogLevel } from 'firebase/app'; +setLogLevel('debug'); // 'debug'로 SDK 내부 동작(Installations 등) 자세히 출력 + +// 온라인/오프라인 상태 및 환경 진단 +console.log('[diag] origin:', location.origin, 'protocol:', location.protocol); +console.log('[diag] navigator.onLine:', navigator.onLine); +window.addEventListener('online', () => console.log('[diag] back online')); +window.addEventListener('offline', () => console.log('[diag] offline')); + +// installations 호스트 접근 자체 테스트(차단 여부만 확인) +fetch('https://firebaseinstallations.googleapis.com/', { mode: 'no-cors' }) + .then(() => console.log('[diag] reach installations host: OK')) + .catch(e => console.log('[diag] reach installations host: blocked?', e)); + const GlobalStyle = createGlobalStyle` body { margin: 0; From 21771825c5fc30d53c7269f22bc2b2988efd7c4e Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 22:18:23 +0900 Subject: [PATCH 21/24] =?UTF-8?q?setupFetchInterceptor=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.tsx b/src/main.tsx index 19fd6dc..bc56af8 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,7 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; -import '@utils/setupFetchInterceptor'; +// import '@utils/setupFetchInterceptor'; // Firebase SDK 디버그 로그 import { setLogLevel } from 'firebase/app'; From 0f894d015536a9e63e82e99bdeda27eb414a155b Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 22:44:11 +0900 Subject: [PATCH 22/24] =?UTF-8?q?:bug:[fix]:=20CSP=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel.json | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/vercel.json b/vercel.json index a9104b3..3d49e12 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,16 @@ { - "routes": [ - { "src": "/[^.]+", "dest": "/", "status": 200 } + "rewrites": [ + { "source": "/[^.]+", "destination": "/" } + ], + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Content-Security-Policy", + "value": "default-src 'self'; script-src 'self' https://www.gstatic.com https://www.googletagmanager.com; connect-src 'self' https://firebaseinstallations.googleapis.com https://fcmregistrations.googleapis.com https://fcm.googleapis.com https://www.googleapis.com https://www.gstatic.com https://www.google.com; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; manifest-src 'self';" + } + ] + } ] -} \ No newline at end of file +} From cb9da9d87e6f1f2290e201bab422a970a45f51ff Mon Sep 17 00:00:00 2001 From: nohyewon Date: Thu, 14 Aug 2025 22:47:10 +0900 Subject: [PATCH 23/24] =?UTF-8?q?:bug:[fix]:=20vercel.json=20=EB=B0=B0?= =?UTF-8?q?=ED=8F=AC=20=EC=98=A4=EB=A5=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 3d49e12..6680f04 100644 --- a/vercel.json +++ b/vercel.json @@ -1,10 +1,10 @@ { "rewrites": [ - { "source": "/[^.]+", "destination": "/" } + { "source": "/:path((?!.*\\.).*)", "destination": "/" } ], "headers": [ { - "source": "/(.*)", + "source": "/:path*", "headers": [ { "key": "Content-Security-Policy", From 8ea8b45d8e400c2ca54b8dc71997f9f9f32ede85 Mon Sep 17 00:00:00 2001 From: nohyewon Date: Fri, 15 Aug 2025 03:17:18 +0900 Subject: [PATCH 24/24] =?UTF-8?q?:bug:[fix]:=20sw=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=9B=90=EC=83=81=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/sw.js | 39 ++++++++++++------- src/hooks/useFCM.ts | 91 ++++++++++++++++++--------------------------- src/main.tsx | 17 +-------- vercel.json | 17 ++------- 4 files changed, 65 insertions(+), 99 deletions(-) diff --git a/public/sw.js b/public/sw.js index 36a4965..535a9c6 100644 --- a/public/sw.js +++ b/public/sw.js @@ -19,22 +19,33 @@ firebase.initializeApp({ const messaging = firebase.messaging(); -self.addEventListener('install', event => { - console.log('[SW] Installed', event); - self.skipWaiting(); +// install event +self.addEventListener('install', () => { + console.log('[Service Worker] installed'); }); -self.addEventListener('activate', event => { - console.log('[SW] Activated', event); - self.clients.claim(); +// activate event +self.addEventListener('activate', e => { + console.log('[Service Worker] actived', e); }); -// 백그라운드 푸시 표시 -messaging.onBackgroundMessage(payload => { - console.log('[SW] Received background message', payload); - const { title, body, icon } = payload.notification || {}; - self.registration.showNotification(title || '알림', { - body: body || '', - icon: icon || '/icons/icon-192x192.png', - }); +// fetch event +self.addEventListener('fetch', e => { + console.log('[Service Worker] fetched resource ' + e.request.url); +}); + +messaging.onBackgroundMessage(function (payload) { + console.log( + '[firebase-messaging-sw.js] Received background message ', + payload, + ); + + const { title, body } = payload.notification; + + const notificationOptions = { + body, + icon: 'icons/icon-24x24.svg', + }; + + self.registration.showNotification(title, notificationOptions); }); diff --git a/src/hooks/useFCM.ts b/src/hooks/useFCM.ts index 4350e97..ce50582 100644 --- a/src/hooks/useFCM.ts +++ b/src/hooks/useFCM.ts @@ -1,9 +1,6 @@ import { useEffect, useState } from 'react'; -import { getToken, onMessage, isSupported } from 'firebase/messaging'; -import { messaging, app } from '@utils/firebase'; - -// Installations 직접 확인용 -import { getInstallations, getId } from 'firebase/installations'; +import { getToken, onMessage } from 'firebase/messaging'; +import { messaging } from '@utils/firebase'; const VAPID_KEY = import.meta.env.VITE_FIREBASE_VAPID_KEY; @@ -11,57 +8,41 @@ export const useFCM = () => { const [fcmToken, setFcmToken] = useState(null); useEffect(() => { - (async () => { - if (!(await isSupported())) { - console.warn('[FCM] 브라우저 미지원'); - return; - } - - // 1) SW 등록 + 활성화 확실히 대기 - await navigator.serviceWorker.register('/sw.js'); - const registration = await navigator.serviceWorker.ready; - console.log('[FCM] SW ready →', registration); - - // 2) Installations(FID) 선확인: 여기서 막히면 getToken도 실패 - try { - const installations = getInstallations(app); - const fid = await getId(installations); - console.log('[diag] FID:', fid); - } catch (e) { - console.error( - '[diag] getId(installations) error → Installations 단계에서 막힘', - e, - ); - } - - // 3) 권한 요청 - const permission = await Notification.requestPermission(); - if (permission !== 'granted') { - console.warn('[FCM] 알림 권한 거부됨'); - return; - } - - // 4) getToken (반드시 SW ready 사용) - try { - const token = await getToken(messaging, { - vapidKey: VAPID_KEY, - serviceWorkerRegistration: registration, - }); - - if (token) { - console.log('[FCM] Token:', token); - setFcmToken(token); - } else { - console.warn('[FCM] No token available'); - } - - onMessage(messaging, payload => { - console.log('[FCM] Foreground message:', payload); + // 서비스워커 등록 + navigator.serviceWorker + .register('/sw.js') + .then(registration => { + console.log('Service Worker registered:', registration); + + // 알림 권한 요청 + Notification.requestPermission().then(permission => { + if (permission === 'granted') { + // FCM 토큰 요청 + getToken(messaging, { + vapidKey: VAPID_KEY, + serviceWorkerRegistration: registration, + }) + .then(token => { + if (token) { + console.log('FCM Token:', token); + setFcmToken(token); + } else { + console.warn('No token available'); + } + }) + .catch(err => console.error('Token error:', err)); + } else { + console.warn('알림 권한 거부됨'); + } }); - } catch (err) { - console.error('[FCM] getToken error:', err); - } - })(); + }) + .catch(err => { + console.error('Service Worker registration failed:', err); + }); + + onMessage(messaging, payload => { + console.log('Foreground FCM message:', payload); + }); }, []); return fcmToken; diff --git a/src/main.tsx b/src/main.tsx index bc56af8..6a6258e 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,22 +1,7 @@ import { createRoot } from 'react-dom/client'; import App from './App.tsx'; import { createGlobalStyle } from 'styled-components'; -// import '@utils/setupFetchInterceptor'; - -// Firebase SDK 디버그 로그 -import { setLogLevel } from 'firebase/app'; -setLogLevel('debug'); // 'debug'로 SDK 내부 동작(Installations 등) 자세히 출력 - -// 온라인/오프라인 상태 및 환경 진단 -console.log('[diag] origin:', location.origin, 'protocol:', location.protocol); -console.log('[diag] navigator.onLine:', navigator.onLine); -window.addEventListener('online', () => console.log('[diag] back online')); -window.addEventListener('offline', () => console.log('[diag] offline')); - -// installations 호스트 접근 자체 테스트(차단 여부만 확인) -fetch('https://firebaseinstallations.googleapis.com/', { mode: 'no-cors' }) - .then(() => console.log('[diag] reach installations host: OK')) - .catch(e => console.log('[diag] reach installations host: blocked?', e)); +import '@utils/setupFetchInterceptor'; const GlobalStyle = createGlobalStyle` body { diff --git a/vercel.json b/vercel.json index 6680f04..a9104b3 100644 --- a/vercel.json +++ b/vercel.json @@ -1,16 +1,5 @@ { - "rewrites": [ - { "source": "/:path((?!.*\\.).*)", "destination": "/" } - ], - "headers": [ - { - "source": "/:path*", - "headers": [ - { - "key": "Content-Security-Policy", - "value": "default-src 'self'; script-src 'self' https://www.gstatic.com https://www.googletagmanager.com; connect-src 'self' https://firebaseinstallations.googleapis.com https://fcmregistrations.googleapis.com https://fcm.googleapis.com https://www.googleapis.com https://www.gstatic.com https://www.google.com; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; manifest-src 'self';" - } - ] - } + "routes": [ + { "src": "/[^.]+", "dest": "/", "status": 200 } ] -} +} \ No newline at end of file