diff --git a/public/js/fcm.js b/public/js/fcm.js index 7a4c7778..3fc6bf33 100644 --- a/public/js/fcm.js +++ b/public/js/fcm.js @@ -1,45 +1,70 @@ /** * FCM (Firebase Cloud Messaging) Push Notification Handler * Capacitor 앱에서만 동작, 웹 브라우저에서는 무시됨 - * - * 필요 조건: - * - localStorage에 'api_access_token' 저장 (API 인증용) - * - window.SAM_CONFIG.apiBaseUrl 설정 (API 서버 주소) */ -(function() { +(function () { + 'use strict'; - // 설정 + console.log('FCM.js LOADED'); + const CONFIG = { - // API Base URL (Blade에서 주입하거나 기본값 사용) apiBaseUrl: window.SAM_CONFIG?.apiBaseUrl || 'https://api.codebridge-x.com', - // localStorage 키 fcmTokenKey: 'fcm_token', apiTokenKey: 'api_access_token', apiKeyHeader: window.SAM_CONFIG?.apiKey || '', }; - /** - * FCM 초기화 (페이지 로드 시 실행) - */ - document.addEventListener('DOMContentLoaded', async () => { - // Capacitor 환경이 아니면 무시 + // DOM 준비되면 실행 (이미 로드됐으면 즉시) + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', bootstrap); + } else { + bootstrap(); + } + + async function bootstrap() { if (!window.Capacitor?.Plugins?.PushNotifications) { - console.log('[FCM] Not running in Capacitor or PushNotifications not available'); + console.log('[FCM] Not running in Capacitor'); return; } await initializeFCM(); - }); + } - /** - * FCM 초기화 - */ async function initializeFCM() { const { PushNotifications } = Capacitor.Plugins; try { - // 1. 권한 요청 + // ✅ 1. 기존 리스너 제거 + PushNotifications.removeAllListeners(); + + // ✅ 2. 리스너를 먼저 등록 (가장 중요) + PushNotifications.addListener('registration', async (token) => { + console.log('[FCM] 🔥 registration event fired'); + console.log('[FCM] Token received:', token.value?.substring(0, 20) + '...'); + await handleTokenRegistration(token.value); + }); + + PushNotifications.addListener('registrationError', (err) => { + console.error('[FCM] Registration error:', err); + }); + + PushNotifications.addListener('pushNotificationReceived', (notification) => { + console.log('[FCM] Push received (foreground):', notification); + if (typeof showToast === 'function') { + showToast(notification.body || notification.title, 'info'); + } + }); + + PushNotifications.addListener('pushNotificationActionPerformed', (action) => { + console.log('[FCM] Push action performed:', action); + const data = action.notification?.data; + if (data?.url) { + window.location.href = data.url; + } + }); + + // ✅ 3. 그 다음에 권한 요청 const perm = await PushNotifications.requestPermissions(); console.log('[FCM] Push permission:', perm.receive); @@ -48,42 +73,7 @@ return; } - // 2. 기존 리스너 제거 (중복 방지) - PushNotifications.removeAllListeners(); - - // 3. 토큰 수신 리스너 - PushNotifications.addListener('registration', async (token) => { - console.log('[FCM] Token received:', token.value?.substring(0, 20) + '...'); - await handleTokenRegistration(token.value); - }); - - // 4. 등록 에러 핸들링 - PushNotifications.addListener('registrationError', (err) => { - console.error('[FCM] Registration error:', err); - }); - - // 5. 푸시 수신 리스너 (앱이 포그라운드일 때) - PushNotifications.addListener('pushNotificationReceived', (notification) => { - console.log('[FCM] Push received (foreground):', notification); - - // Toast 알림 표시 (SweetAlert2 사용) - if (typeof showToast === 'function') { - showToast(notification.body || notification.title, 'info'); - } - }); - - // 6. 푸시 액션 리스너 (알림 클릭 시) - PushNotifications.addListener('pushNotificationActionPerformed', (action) => { - console.log('[FCM] Push action performed:', action); - - // 알림에 포함된 URL로 이동 - const data = action.notification.data; - if (data && data.url) { - window.location.href = data.url; - } - }); - - // 7. FCM 등록 시작 + // ✅ 4. 마지막에 register 호출 await PushNotifications.register(); } catch (error) { @@ -91,39 +81,27 @@ } } - /** - * 토큰 등록 처리 (중복 방지 + 변경 감지) - * @param {string} newToken - 새로 받은 FCM 토큰 - */ async function handleTokenRegistration(newToken) { - const oldToken = localStorage.getItem(CONFIG.fcmTokenKey); + const oldToken = sessionStorage.getItem(CONFIG.fcmTokenKey); - // 토큰이 동일하면 재등록 생략 if (oldToken === newToken) { - console.log('[FCM] Token unchanged, skipping registration'); + console.log('[FCM] Token unchanged, skip'); return; } - // API로 토큰 등록 const success = await registerTokenToServer(newToken); if (success) { - // 성공 시 localStorage에 저장 - localStorage.setItem(CONFIG.fcmTokenKey, newToken); - console.log('[FCM] Token saved to localStorage'); + sessionStorage.setItem(CONFIG.fcmTokenKey, newToken); + console.log('[FCM] Token saved to sessionStorage'); } } - /** - * FCM 토큰을 서버에 등록 - * @param {string} token - FCM 토큰 - * @returns {boolean} 성공 여부 - */ async function registerTokenToServer(token) { - const accessToken = localStorage.getItem(CONFIG.apiTokenKey); + const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey); if (!accessToken) { - console.warn('[FCM] No API access token found, skipping registration'); + console.warn('[FCM] No API access token'); return false; } @@ -134,16 +112,15 @@ 'Authorization': `Bearer ${accessToken}`, }; - // API Key가 있으면 추가 if (CONFIG.apiKeyHeader) { headers['X-API-KEY'] = CONFIG.apiKeyHeader; } const response = await fetch(`${CONFIG.apiBaseUrl}/api/push/register-token`, { method: 'POST', - headers: headers, + headers, body: JSON.stringify({ - token: token, + token, platform: getDevicePlatform(), device_name: getDeviceName(), app_version: getAppVersion(), @@ -151,114 +128,62 @@ }); if (response.ok) { - const result = await response.json(); - console.log('[FCM] Token registered successfully:', result); + console.log('[FCM] Token registered successfully'); return true; - } else { - const error = await response.json().catch(() => ({})); - console.error('[FCM] Token registration failed:', response.status, error); - return false; } + + console.error('[FCM] Token registration failed:', response.status); + return false; + } catch (error) { - console.error('[FCM] Failed to send token to server:', error); + console.error('[FCM] Failed to send token:', error); return false; } } - /** - * FCM 토큰 해제 (로그아웃 시 호출) - * @returns {boolean} 성공 여부 - */ async function unregisterToken() { - const token = localStorage.getItem(CONFIG.fcmTokenKey); - const accessToken = localStorage.getItem(CONFIG.apiTokenKey); + const token = sessionStorage.getItem(CONFIG.fcmTokenKey); + const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey); - if (!token) { - console.log('[FCM] No token to unregister'); - return true; - } - - if (!accessToken) { - // 토큰은 있지만 API 인증이 없으면 로컬만 삭제 - localStorage.removeItem(CONFIG.fcmTokenKey); - console.log('[FCM] Token removed from localStorage (no API auth)'); - return true; - } + if (!token) return true; try { - const headers = { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }; - - if (CONFIG.apiKeyHeader) { - headers['X-API-KEY'] = CONFIG.apiKeyHeader; + if (accessToken) { + await fetch(`${CONFIG.apiBaseUrl}/api/push/unregister-token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${accessToken}`, + }, + body: JSON.stringify({ token }), + }); } - - const response = await fetch(`${CONFIG.apiBaseUrl}/api/push/unregister-token`, { - method: 'POST', - headers: headers, - body: JSON.stringify({ - token: token, - }), - }); - - if (response.ok) { - console.log('[FCM] Token unregistered successfully'); - } else { - console.warn('[FCM] Token unregister failed, but continuing logout'); - } - - // 성공/실패와 관계없이 로컬 토큰 삭제 - localStorage.removeItem(CONFIG.fcmTokenKey); - return true; - - } catch (error) { - console.error('[FCM] Failed to unregister token:', error); - // 에러가 나도 로컬 토큰은 삭제 - localStorage.removeItem(CONFIG.fcmTokenKey); - return false; + } catch (e) { + console.warn('[FCM] Unregister failed, clearing local token'); } + + sessionStorage.removeItem(CONFIG.fcmTokenKey); + return true; } - /** - * 디바이스 플랫폼 감지 - * @returns {string} 'ios' | 'android' | 'web' - */ function getDevicePlatform() { if (window.Capacitor) { - const platform = Capacitor.getPlatform(); - if (platform === 'ios') return 'ios'; - if (platform === 'android') return 'android'; + const p = Capacitor.getPlatform(); + if (p === 'ios' || p === 'android') return p; } return 'web'; } - /** - * 디바이스명 가져오기 - * @returns {string|null} - */ function getDeviceName() { - if (window.Capacitor?.Plugins?.Device) { - // Capacitor Device 플러그인이 있으면 비동기로 가져와야 함 - // 여기서는 간단히 null 반환 (필요시 별도 구현) - return null; - } return navigator.userAgent?.substring(0, 100) || null; } - /** - * 앱 버전 가져오기 - * @returns {string|null} - */ function getAppVersion() { return window.SAM_CONFIG?.appVersion || null; } - // 전역으로 노출 (로그아웃 시 호출용) window.FCM = { - unregisterToken: unregisterToken, + unregisterToken, reinitialize: initializeFCM, }; diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 104e0aa6..cafb6f1a 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -14,7 +14,7 @@ appVersion: '{{ config('app.version', '1.0.0') }}', }; - // API 토큰 localStorage 동기화 (FCM 등에서 사용) + // API 토큰 sessionStorage 동기화 (FCM 등에서 사용) @if(session('api_access_token')) (function() { const token = '{{ session('api_access_token') }}'; @@ -23,18 +23,18 @@ // 토큰이 유효한 경우에만 저장 if (expiresAt > now) { - localStorage.setItem('api_access_token', token); - localStorage.setItem('api_token_expires_at', expiresAt); + sessionStorage.setItem('api_access_token', token); + sessionStorage.setItem('api_token_expires_at', expiresAt); } else { // 만료된 토큰 정리 - localStorage.removeItem('api_access_token'); - localStorage.removeItem('api_token_expires_at'); + sessionStorage.removeItem('api_access_token'); + sessionStorage.removeItem('api_token_expires_at'); } })(); @else - // 세션에 토큰이 없으면 localStorage도 정리 - localStorage.removeItem('api_access_token'); - localStorage.removeItem('api_token_expires_at'); + // 세션에 토큰이 없으면 sessionStorage도 정리 + sessionStorage.removeItem('api_access_token'); + sessionStorage.removeItem('api_token_expires_at'); @endif