feat: [API Explorer] Phase 1 완성 - 히스토리 로드, 밸리데이션, 유니코드 처리

- 히스토리 로드 기능 구현 (loadFromHistory, fillFormFromHistory)
- 클라이언트 사이드 필수값 밸리데이션 추가
- 응답 본문 \xXX UTF-8 바이트 시퀀스 디코딩 (PHP 스택트레이스 한글 깨짐 해결)
- sidebar에 data-operation-id 속성 추가
- history-drawer 함수 연결 수정
- Flow Tester 변수 바인딩 개선
- 마이그레이션 파일 통합 정리
This commit is contained in:
2025-12-18 15:42:01 +09:00
parent 2ed273097e
commit a62337ef5c
15 changed files with 1328 additions and 217 deletions

265
public/js/fcm.js Normal file
View 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,
};
})();