FCM 초기화 순서 개선 및 토큰 저장소 변경

- FCM 초기화 순서 변경: 리스너 등록 → 권한 요청 → register 호출
- localStorage → sessionStorage 변경 (보안 강화)
- 코드 간소화 및 불필요한 주석 제거
- API 토큰 동기화 로직 sessionStorage로 통일
This commit is contained in:
2025-12-18 20:27:24 +09:00
parent 15a66a345e
commit 6914ef1013
2 changed files with 87 additions and 162 deletions

View File

@@ -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,
};

View File

@@ -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
</script>
<!-- 사이드바 상태 즉시 적용 (깜빡임 방지) -->