- type별 토스트 스타일 분기 (error/warning/success/info) - 포그라운드 사운드 재생 (sound_key 기반) - URL 클릭 이동 지원 - 앱 상태 감지 (포그라운드/백그라운드) - sounds/ 디렉토리 추가
308 lines
9.8 KiB
JavaScript
308 lines
9.8 KiB
JavaScript
/**
|
|
* FCM (Firebase Cloud Messaging) Push Notification Handler
|
|
* Capacitor 앱에서만 동작, 웹 브라우저에서는 무시됨
|
|
*
|
|
* Payload data 스키마:
|
|
* - type: 알림 타입 (invoice_failed, order_completed 등)
|
|
* - url: 클릭 시 이동 URL
|
|
* - sound_key: 사운드 파일 키 (sounds/{sound_key}.wav)
|
|
*/
|
|
(function () {
|
|
|
|
'use strict';
|
|
|
|
console.log('[FCM] fcm.js LOADED v2');
|
|
|
|
const CONFIG = {
|
|
apiBaseUrl: window.SAM_CONFIG?.apiBaseUrl || 'https://api.codebridge-x.com',
|
|
fcmTokenKey: 'fcm_token',
|
|
apiTokenKey: 'api_access_token',
|
|
apiKeyHeader: window.SAM_CONFIG?.apiKey || '',
|
|
soundBasePath: '/sounds/',
|
|
defaultSound: 'default',
|
|
};
|
|
|
|
// 앱 상태 (포그라운드 여부)
|
|
let isAppForeground = true;
|
|
|
|
// 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');
|
|
return;
|
|
}
|
|
|
|
// 앱 상태 리스너 (포그라운드/백그라운드)
|
|
if (window.Capacitor?.Plugins?.App) {
|
|
const { App } = Capacitor.Plugins;
|
|
|
|
App.addListener('appStateChange', ({ isActive }) => {
|
|
isAppForeground = isActive;
|
|
console.log('[FCM] App state:', isActive ? 'foreground' : 'background');
|
|
});
|
|
}
|
|
|
|
await initializeFCM();
|
|
}
|
|
|
|
async function initializeFCM() {
|
|
const { PushNotifications } = Capacitor.Plugins;
|
|
|
|
try {
|
|
// ✅ 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);
|
|
handleForegroundNotification(notification);
|
|
});
|
|
|
|
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);
|
|
|
|
if (perm.receive !== 'granted') {
|
|
console.log('[FCM] Push permission not granted');
|
|
return;
|
|
}
|
|
|
|
// ✅ 4. 마지막에 register 호출
|
|
await PushNotifications.register();
|
|
|
|
} catch (error) {
|
|
console.error('[FCM] Initialization error:', error);
|
|
}
|
|
}
|
|
|
|
async function handleTokenRegistration(newToken) {
|
|
const oldToken = sessionStorage.getItem(CONFIG.fcmTokenKey);
|
|
|
|
if (oldToken === newToken) {
|
|
console.log('[FCM] Token unchanged, skip');
|
|
return;
|
|
}
|
|
|
|
const success = await registerTokenToServer(newToken);
|
|
|
|
if (success) {
|
|
sessionStorage.setItem(CONFIG.fcmTokenKey, newToken);
|
|
console.log('[FCM] Token saved to sessionStorage');
|
|
}
|
|
}
|
|
|
|
async function registerTokenToServer(token) {
|
|
const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey);
|
|
|
|
if (!accessToken) {
|
|
console.warn('[FCM] No API access token');
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
};
|
|
|
|
if (CONFIG.apiKeyHeader) {
|
|
headers['X-API-KEY'] = CONFIG.apiKeyHeader;
|
|
}
|
|
|
|
const response = await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/register-token`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({
|
|
token,
|
|
platform: getDevicePlatform(),
|
|
device_name: getDeviceName(),
|
|
app_version: getAppVersion(),
|
|
}),
|
|
});
|
|
|
|
if (response.ok) {
|
|
console.log('[FCM] Token registered successfully');
|
|
return true;
|
|
}
|
|
|
|
console.error('[FCM] Token registration failed:', response.status);
|
|
return false;
|
|
|
|
} catch (error) {
|
|
console.error('[FCM] Failed to send token:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function unregisterToken() {
|
|
const token = sessionStorage.getItem(CONFIG.fcmTokenKey);
|
|
const accessToken = sessionStorage.getItem(CONFIG.apiTokenKey);
|
|
|
|
if (!token) return true;
|
|
|
|
try {
|
|
if (accessToken) {
|
|
await fetch(`${CONFIG.apiBaseUrl}/api/v1/push/unregister-token`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${accessToken}`,
|
|
},
|
|
body: JSON.stringify({ token }),
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn('[FCM] Unregister failed, clearing local token');
|
|
}
|
|
|
|
sessionStorage.removeItem(CONFIG.fcmTokenKey);
|
|
return true;
|
|
}
|
|
|
|
function getDevicePlatform() {
|
|
if (window.Capacitor) {
|
|
const p = Capacitor.getPlatform();
|
|
if (p === 'ios' || p === 'android') return p;
|
|
}
|
|
return 'web';
|
|
}
|
|
|
|
function getDeviceName() {
|
|
return navigator.userAgent?.substring(0, 100) || null;
|
|
}
|
|
|
|
function getAppVersion() {
|
|
return window.SAM_CONFIG?.appVersion || null;
|
|
}
|
|
|
|
/**
|
|
* 포그라운드 알림 처리
|
|
*/
|
|
function handleForegroundNotification(notification) {
|
|
const data = notification.data || {};
|
|
const type = data.type || 'default';
|
|
const url = data.url;
|
|
const soundKey = data.sound_key;
|
|
|
|
console.log('[FCM] Notification data:', { type, url, soundKey });
|
|
|
|
// 1. 포그라운드에서만 사운드 재생 (백그라운드는 OS 채널 사운드)
|
|
if (isAppForeground && soundKey) {
|
|
playNotificationSound(soundKey);
|
|
}
|
|
|
|
// 2. 타입별 토스트 메시지 표시
|
|
const title = notification.title || '알림';
|
|
const body = notification.body || '';
|
|
const toastType = getToastTypeByNotificationType(type);
|
|
|
|
if (typeof showToast === 'function') {
|
|
showToast(`${title}: ${body}`, toastType);
|
|
}
|
|
|
|
// 3. 클릭 가능한 토스트 (URL이 있는 경우)
|
|
if (url && typeof showClickableToast === 'function') {
|
|
showClickableToast(title, body, () => {
|
|
window.location.href = url;
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 알림 사운드 재생 (포그라운드 전용)
|
|
*/
|
|
function playNotificationSound(soundKey) {
|
|
try {
|
|
const soundPath = `${CONFIG.soundBasePath}${soundKey}.wav`;
|
|
const audio = new Audio(soundPath);
|
|
|
|
audio.volume = 0.5;
|
|
audio.play().catch(err => {
|
|
console.warn('[FCM] Sound play failed, trying default:', err.message);
|
|
// 기본 사운드 시도
|
|
if (soundKey !== CONFIG.defaultSound) {
|
|
const defaultAudio = new Audio(`${CONFIG.soundBasePath}${CONFIG.defaultSound}.wav`);
|
|
defaultAudio.volume = 0.5;
|
|
defaultAudio.play().catch(() => {});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.warn('[FCM] Sound error:', err);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 알림 타입별 토스트 스타일 결정
|
|
*/
|
|
function getToastTypeByNotificationType(type) {
|
|
const typeMap = {
|
|
// 긴급/에러
|
|
'invoice_failed': 'error',
|
|
'payment_failed': 'error',
|
|
'order_cancelled': 'error',
|
|
|
|
// 경고
|
|
'approval_required': 'warning',
|
|
'stock_low': 'warning',
|
|
|
|
// 성공
|
|
'order_completed': 'success',
|
|
'payment_completed': 'success',
|
|
'approval_approved': 'success',
|
|
|
|
// 기본
|
|
'default': 'info',
|
|
};
|
|
|
|
return typeMap[type] || 'info';
|
|
}
|
|
|
|
/**
|
|
* 클릭 가능한 토스트 (URL 이동용)
|
|
* showClickableToast가 없으면 기본 토스트 사용
|
|
*/
|
|
if (typeof window.showClickableToast === 'undefined') {
|
|
window.showClickableToast = function(title, body, onClick) {
|
|
// 기본 구현: 일반 토스트 + 클릭 핸들러는 무시
|
|
if (typeof showToast === 'function') {
|
|
showToast(`${title}: ${body}`, 'info');
|
|
}
|
|
console.log('[FCM] showClickableToast not implemented, URL click ignored');
|
|
};
|
|
}
|
|
|
|
window.FCM = {
|
|
unregisterToken,
|
|
reinitialize: initializeFCM,
|
|
// 디버그/테스트용
|
|
playSound: playNotificationSound,
|
|
testForeground: (data) => handleForegroundNotification({ title: 'Test', body: 'Test notification', data }),
|
|
};
|
|
|
|
})();
|