feat: [API Explorer] Phase 1 완성 - 히스토리 로드, 밸리데이션, 유니코드 처리
- 히스토리 로드 기능 구현 (loadFromHistory, fillFormFromHistory) - 클라이언트 사이드 필수값 밸리데이션 추가 - 응답 본문 \xXX UTF-8 바이트 시퀀스 디코딩 (PHP 스택트레이스 한글 깨짐 해결) - sidebar에 data-operation-id 속성 추가 - history-drawer 함수 연결 수정 - Flow Tester 변수 바인딩 개선 - 마이그레이션 파일 통합 정리
This commit is contained in:
265
public/js/fcm.js
Normal file
265
public/js/fcm.js
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* FCM (Firebase Cloud Messaging) Push Notification Handler
|
||||
* Capacitor 앱에서만 동작, 웹 브라우저에서는 무시됨
|
||||
*
|
||||
* 필요 조건:
|
||||
* - localStorage에 'api_access_token' 저장 (API 인증용)
|
||||
* - window.SAM_CONFIG.apiBaseUrl 설정 (API 서버 주소)
|
||||
*/
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 설정
|
||||
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 환경이 아니면 무시
|
||||
if (!window.Capacitor?.Plugins?.PushNotifications) {
|
||||
console.log('[FCM] Not running in Capacitor or PushNotifications not available');
|
||||
return;
|
||||
}
|
||||
|
||||
await initializeFCM();
|
||||
});
|
||||
|
||||
/**
|
||||
* FCM 초기화
|
||||
*/
|
||||
async function initializeFCM() {
|
||||
const { PushNotifications } = Capacitor.Plugins;
|
||||
|
||||
try {
|
||||
// 1. 권한 요청
|
||||
const perm = await PushNotifications.requestPermissions();
|
||||
console.log('[FCM] Push permission:', perm.receive);
|
||||
|
||||
if (perm.receive !== 'granted') {
|
||||
console.log('[FCM] Push permission not granted');
|
||||
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 등록 시작
|
||||
await PushNotifications.register();
|
||||
|
||||
} catch (error) {
|
||||
console.error('[FCM] Initialization error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰 등록 처리 (중복 방지 + 변경 감지)
|
||||
* @param {string} newToken - 새로 받은 FCM 토큰
|
||||
*/
|
||||
async function handleTokenRegistration(newToken) {
|
||||
const oldToken = localStorage.getItem(CONFIG.fcmTokenKey);
|
||||
|
||||
// 토큰이 동일하면 재등록 생략
|
||||
if (oldToken === newToken) {
|
||||
console.log('[FCM] Token unchanged, skipping registration');
|
||||
return;
|
||||
}
|
||||
|
||||
// API로 토큰 등록
|
||||
const success = await registerTokenToServer(newToken);
|
||||
|
||||
if (success) {
|
||||
// 성공 시 localStorage에 저장
|
||||
localStorage.setItem(CONFIG.fcmTokenKey, newToken);
|
||||
console.log('[FCM] Token saved to localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 토큰을 서버에 등록
|
||||
* @param {string} token - FCM 토큰
|
||||
* @returns {boolean} 성공 여부
|
||||
*/
|
||||
async function registerTokenToServer(token) {
|
||||
const accessToken = localStorage.getItem(CONFIG.apiTokenKey);
|
||||
|
||||
if (!accessToken) {
|
||||
console.warn('[FCM] No API access token found, skipping registration');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'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,
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
platform: getDevicePlatform(),
|
||||
device_name: getDeviceName(),
|
||||
app_version: getAppVersion(),
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
console.log('[FCM] Token registered successfully:', result);
|
||||
return true;
|
||||
} else {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
console.error('[FCM] Token registration failed:', response.status, error);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[FCM] Failed to send token to server:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FCM 토큰 해제 (로그아웃 시 호출)
|
||||
* @returns {boolean} 성공 여부
|
||||
*/
|
||||
async function unregisterToken() {
|
||||
const token = localStorage.getItem(CONFIG.fcmTokenKey);
|
||||
const accessToken = localStorage.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;
|
||||
}
|
||||
|
||||
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/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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 디바이스 플랫폼 감지
|
||||
* @returns {string} 'ios' | 'android' | 'web'
|
||||
*/
|
||||
function getDevicePlatform() {
|
||||
if (window.Capacitor) {
|
||||
const platform = Capacitor.getPlatform();
|
||||
if (platform === 'ios') return 'ios';
|
||||
if (platform === 'android') return 'android';
|
||||
}
|
||||
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,
|
||||
reinitialize: initializeFCM,
|
||||
};
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user