diff --git a/app/Http/Controllers/DevTools/ApiExplorerController.php b/app/Http/Controllers/DevTools/ApiExplorerController.php index bc31d7f3..3cb65d61 100644 --- a/app/Http/Controllers/DevTools/ApiExplorerController.php +++ b/app/Http/Controllers/DevTools/ApiExplorerController.php @@ -43,12 +43,16 @@ public function index(): View $environments = $this->explorer->getEnvironments($userId); $defaultEnv = $this->explorer->getDefaultEnvironment($userId); + // 세션에 저장된 토큰 + $savedToken = session('api_explorer_token'); + return view('dev-tools.api-explorer.index', compact( 'endpoints', 'tags', 'bookmarks', 'environments', - 'defaultEnv' + 'defaultEnv', + 'savedToken' )); } @@ -119,13 +123,42 @@ public function execute(Request $request): JsonResponse 'query' => 'nullable|array', 'body' => 'nullable|array', 'environment' => 'required|string', + 'token' => 'nullable|string', + 'user_id' => 'nullable|integer', ]); + // Bearer 토큰 처리 + $token = null; + $headers = $validated['headers'] ?? []; + + // 1. 직접 입력된 토큰 + if (! empty($validated['token'])) { + $token = $validated['token']; + session(['api_explorer_token' => $token]); + } + // 2. 사용자 선택 시 Sanctum 토큰 발급 + elseif (! empty($validated['user_id'])) { + $user = \App\Models\User::find($validated['user_id']); + if ($user) { + $token = $user->createToken('api-explorer', ['*'])->plainTextToken; + session(['api_explorer_token' => $token]); + } + } + // 3. 세션에 저장된 토큰 재사용 + elseif (session('api_explorer_token')) { + $token = session('api_explorer_token'); + } + + // Authorization 헤더 추가 (사용자 입력 토큰이 우선) + if ($token) { + $headers['Authorization'] = 'Bearer ' . $token; + } + // API 실행 $result = $this->requester->execute( $validated['method'], $validated['url'], - $validated['headers'] ?? [], + $headers, $validated['query'] ?? [], $validated['body'] ); @@ -384,4 +417,26 @@ public function setDefaultEnvironment(int $id): JsonResponse return response()->json(['success' => true]); } + + /* + |-------------------------------------------------------------------------- + | Users (for Authentication) + |-------------------------------------------------------------------------- + */ + + /** + * 현재 테넌트의 사용자 목록 + */ + public function users(): JsonResponse + { + $tenantId = auth()->user()->tenant_id; + + $users = \App\Models\User::where('tenant_id', $tenantId) + ->select(['id', 'name', 'email']) + ->orderBy('name') + ->limit(100) + ->get(); + + return response()->json($users); + } } diff --git a/app/Http/Controllers/DevTools/FlowTesterController.php b/app/Http/Controllers/DevTools/FlowTesterController.php index af80aec0..711f5485 100644 --- a/app/Http/Controllers/DevTools/FlowTesterController.php +++ b/app/Http/Controllers/DevTools/FlowTesterController.php @@ -25,7 +25,10 @@ public function index(): View ->orderByDesc('created_at') ->paginate(20); - return view('dev-tools.flow-tester.index', compact('flows')); + // 세션에 저장된 토큰 + $savedToken = session('flow_tester_token'); + + return view('dev-tools.flow-tester.index', compact('flows', 'savedToken')); } /** @@ -329,4 +332,53 @@ public function runDetail(int $runId): View return view('dev-tools.flow-tester.run-detail', compact('run')); } + + /* + |-------------------------------------------------------------------------- + | Token Management + |-------------------------------------------------------------------------- + */ + + /** + * Bearer 토큰 저장 + */ + public function saveToken(Request $request) + { + $validated = $request->validate([ + 'token' => 'required|string', + ]); + + session(['flow_tester_token' => $validated['token']]); + + return response()->json([ + 'success' => true, + 'message' => '토큰이 저장되었습니다.', + ]); + } + + /** + * Bearer 토큰 초기화 + */ + public function clearToken() + { + session()->forget('flow_tester_token'); + + return response()->json([ + 'success' => true, + 'message' => '토큰이 초기화되었습니다.', + ]); + } + + /** + * 현재 토큰 상태 조회 + */ + public function tokenStatus() + { + $token = session('flow_tester_token'); + + return response()->json([ + 'has_token' => ! empty($token), + 'token_preview' => $token ? substr($token, 0, 20).'...' : null, + ]); + } } diff --git a/app/Services/FlowTester/VariableBinder.php b/app/Services/FlowTester/VariableBinder.php index 4d7a5e31..5e186337 100644 --- a/app/Services/FlowTester/VariableBinder.php +++ b/app/Services/FlowTester/VariableBinder.php @@ -16,6 +16,7 @@ * - {{$timestamp}} - 현재 타임스탬프 * - {{$uuid}} - 랜덤 UUID * - {{$random:N}} - N자리 랜덤 숫자 + * - {{$session.token}} - 세션에 저장된 Bearer 토큰 * - {{$faker.xxx}} - Faker 기반 랜덤 데이터 생성 */ class VariableBinder @@ -151,6 +152,12 @@ function ($m) { // {{$auth.apiKey}} → .env의 API Key $input = str_replace('{{$auth.apiKey}}', env('FLOW_TESTER_API_KEY', ''), $input); + // {{$session.token}} → 세션에 저장된 Bearer 토큰 + if (str_contains($input, '{{$session.token}}')) { + $token = session('flow_tester_token', ''); + $input = str_replace('{{$session.token}}', $token, $input); + } + // {{$faker.xxx}} → Faker 기반 랜덤 데이터 생성 $input = $this->resolveFaker($input); diff --git a/config/api-explorer.php b/config/api-explorer.php index 81270148..29361919 100644 --- a/config/api-explorer.php +++ b/config/api-explorer.php @@ -22,13 +22,13 @@ 'default_environments' => [ [ 'name' => '로컬', - 'base_url' => 'http://api.sam.kr', - 'api_key' => env('API_EXPLORER_LOCAL_KEY', ''), + 'base_url' => env('API_EXPLORER_LOCAL_URL', 'http://sam-api-1'), + 'api_key' => env('FLOW_TESTER_API_KEY', ''), ], [ 'name' => '개발', 'base_url' => 'https://api.codebridge-x.com', - 'api_key' => env('API_EXPLORER_DEV_KEY', ''), + 'api_key' => env('FLOW_TESTER_API_KEY', ''), ], ], @@ -46,6 +46,7 @@ 'allowed_hosts' => [ // 화이트리스트 'api.sam.kr', 'api.codebridge-x.com', + 'sam-api-1', // Docker 컨테이너 'localhost', '127.0.0.1', ], diff --git a/database/migrations/2025_12_17_000001_create_api_bookmarks_table.php b/database/migrations/2025_12_17_000001_create_api_bookmarks_table.php deleted file mode 100644 index 245f8a40..00000000 --- a/database/migrations/2025_12_17_000001_create_api_bookmarks_table.php +++ /dev/null @@ -1,36 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->onDelete('cascade')->comment('사용자 ID'); - $table->string('endpoint', 500)->comment('API 엔드포인트'); - $table->string('method', 10)->comment('HTTP 메서드'); - $table->string('display_name', 100)->nullable()->comment('표시명'); - $table->integer('display_order')->default(0)->comment('표시 순서'); - $table->string('color', 20)->nullable()->comment('색상 코드'); - $table->timestamps(); - - $table->unique(['user_id', 'endpoint', 'method'], 'api_bookmarks_unique'); - $table->index('user_id'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('api_bookmarks'); - } -}; diff --git a/database/migrations/2025_12_17_000002_create_api_templates_table.php b/database/migrations/2025_12_17_000002_create_api_templates_table.php deleted file mode 100644 index 513ea418..00000000 --- a/database/migrations/2025_12_17_000002_create_api_templates_table.php +++ /dev/null @@ -1,40 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->onDelete('cascade')->comment('사용자 ID'); - $table->string('endpoint', 500)->comment('API 엔드포인트'); - $table->string('method', 10)->comment('HTTP 메서드'); - $table->string('name', 100)->comment('템플릿명'); - $table->text('description')->nullable()->comment('설명'); - $table->json('headers')->nullable()->comment('헤더 (JSON)'); - $table->json('path_params')->nullable()->comment('경로 파라미터 (JSON)'); - $table->json('query_params')->nullable()->comment('쿼리 파라미터 (JSON)'); - $table->json('body')->nullable()->comment('요청 본문 (JSON)'); - $table->boolean('is_shared')->default(false)->comment('공유 여부'); - $table->timestamps(); - - $table->index(['user_id', 'endpoint', 'method'], 'api_templates_user_endpoint'); - $table->index('is_shared'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('api_templates'); - } -}; diff --git a/database/migrations/2025_12_17_000003_create_api_histories_table.php b/database/migrations/2025_12_17_000003_create_api_histories_table.php deleted file mode 100644 index 561744c1..00000000 --- a/database/migrations/2025_12_17_000003_create_api_histories_table.php +++ /dev/null @@ -1,40 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->onDelete('cascade')->comment('사용자 ID'); - $table->string('endpoint', 500)->comment('API 엔드포인트'); - $table->string('method', 10)->comment('HTTP 메서드'); - $table->json('request_headers')->nullable()->comment('요청 헤더 (JSON)'); - $table->json('request_body')->nullable()->comment('요청 본문 (JSON)'); - $table->integer('response_status')->comment('응답 상태 코드'); - $table->json('response_headers')->nullable()->comment('응답 헤더 (JSON)'); - $table->longText('response_body')->nullable()->comment('응답 본문'); - $table->integer('duration_ms')->comment('소요 시간 (ms)'); - $table->string('environment', 50)->comment('환경명'); - $table->timestamp('created_at')->useCurrent()->comment('생성일시'); - - $table->index(['user_id', 'created_at'], 'api_histories_user_created'); - $table->index(['endpoint', 'method'], 'api_histories_endpoint_method'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('api_histories'); - } -}; diff --git a/database/migrations/2025_12_17_000004_create_api_environments_table.php b/database/migrations/2025_12_17_000004_create_api_environments_table.php deleted file mode 100644 index 8e45b8f0..00000000 --- a/database/migrations/2025_12_17_000004_create_api_environments_table.php +++ /dev/null @@ -1,36 +0,0 @@ -id(); - $table->foreignId('user_id')->constrained()->onDelete('cascade')->comment('사용자 ID'); - $table->string('name', 50)->comment('환경명'); - $table->string('base_url', 500)->comment('기본 URL'); - $table->string('api_key', 500)->nullable()->comment('API Key (암호화 저장)'); - $table->text('auth_token')->nullable()->comment('인증 토큰 (암호화 저장)'); - $table->json('variables')->nullable()->comment('환경 변수 (JSON)'); - $table->boolean('is_default')->default(false)->comment('기본 환경 여부'); - $table->timestamps(); - - $table->index('user_id'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('api_environments'); - } -}; diff --git a/public/js/fcm.js b/public/js/fcm.js new file mode 100644 index 00000000..7a4c7778 --- /dev/null +++ b/public/js/fcm.js @@ -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, + }; + +})(); diff --git a/resources/views/dev-tools/api-explorer/index.blade.php b/resources/views/dev-tools/api-explorer/index.blade.php index 572395bd..202a9e78 100644 --- a/resources/views/dev-tools/api-explorer/index.blade.php +++ b/resources/views/dev-tools/api-explorer/index.blade.php @@ -189,12 +189,23 @@ @endforeach + + + + + + +
+ +
+ + +
+
+ + +
+ + + @if($savedToken) +

✅ 세션에 저장된 토큰이 있습니다.

+ @endif +
+ + + + + +
+
+ 현재 상태: + + {{ $savedToken ? '인증됨' : '인증 필요' }} + +
+
+ + +
+ + + +
+ + + +
@@ -306,9 +396,141 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline- @push('scripts') diff --git a/resources/views/dev-tools/api-explorer/partials/sidebar.blade.php b/resources/views/dev-tools/api-explorer/partials/sidebar.blade.php index a6932206..7e9fda64 100644 --- a/resources/views/dev-tools/api-explorer/partials/sidebar.blade.php +++ b/resources/views/dev-tools/api-explorer/partials/sidebar.blade.php @@ -48,7 +48,7 @@ class="text-yellow-500 hover:text-yellow-600"> @php $isBookmarked = $bookmarks->where('endpoint', $endpoint['path'])->where('method', $endpoint['method'])->isNotEmpty(); @endphp -
+
{{ $endpoint['method'] }} diff --git a/resources/views/dev-tools/flow-tester/index.blade.php b/resources/views/dev-tools/flow-tester/index.blade.php index 38a76209..98f32fc2 100644 --- a/resources/views/dev-tools/flow-tester/index.blade.php +++ b/resources/views/dev-tools/flow-tester/index.blade.php @@ -7,6 +7,15 @@

API 플로우 테스터

+ + +
+ + +
+ + + @if($savedToken) +

✅ 세션에 저장된 토큰이 있습니다.

+ @endif +

플로우에서 \{\{$session.token\}\}로 참조할 수 있습니다.

+
+ + +
+
+ 현재 상태: + + {{ $savedToken ? '인증됨' : '인증 필요' }} + +
+
+ + +
+ + + +
+
+
+
@endsection @push('scripts') @@ -658,5 +720,109 @@ function confirmDelete(id, name) { }); }); } + + /* + |-------------------------------------------------------------------------- + | 인증 관리 + |-------------------------------------------------------------------------- + */ + + // 현재 토큰 상태 + let currentAuthToken = @json($savedToken ?? ''); + + function openAuthModal() { + document.getElementById('authModal').classList.remove('hidden'); + document.getElementById('authBearerToken').value = currentAuthToken || ''; + } + + function closeAuthModal() { + document.getElementById('authModal').classList.add('hidden'); + } + + function saveAuth() { + const token = document.getElementById('authBearerToken').value.trim(); + + if (!token) { + showToast('토큰을 입력해주세요.', 'warning'); + return; + } + + // 서버에 토큰 저장 + fetch('{{ route("dev-tools.flow-tester.token.save") }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ token: token }), + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + currentAuthToken = token; + updateAuthStatus(true); + showToast('토큰이 저장되었습니다.', 'success'); + closeAuthModal(); + } else { + showToast(data.message || '저장 실패', 'error'); + } + }) + .catch(error => { + showToast('오류 발생: ' + error.message, 'error'); + }); + } + + function clearAuth() { + // 서버에서 토큰 삭제 + fetch('{{ route("dev-tools.flow-tester.token.clear") }}', { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': '{{ csrf_token() }}', + 'Accept': 'application/json', + }, + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + currentAuthToken = ''; + document.getElementById('authBearerToken').value = ''; + updateAuthStatus(false); + showToast('인증이 초기화되었습니다.', 'info'); + } else { + showToast(data.message || '초기화 실패', 'error'); + } + }) + .catch(error => { + showToast('오류 발생: ' + error.message, 'error'); + }); + } + + function updateAuthStatus(isAuthenticated) { + const statusEl = document.getElementById('auth-status'); + const modalStatusEl = document.getElementById('authStatusDisplay'); + + if (isAuthenticated) { + statusEl.textContent = '인증됨'; + statusEl.classList.remove('text-gray-500'); + statusEl.classList.add('text-green-600'); + + if (modalStatusEl) { + modalStatusEl.textContent = '인증됨'; + modalStatusEl.classList.remove('text-gray-500'); + modalStatusEl.classList.add('text-green-600'); + } + } else { + statusEl.textContent = '인증 필요'; + statusEl.classList.remove('text-green-600'); + statusEl.classList.add('text-gray-500'); + + if (modalStatusEl) { + modalStatusEl.textContent = '인증 필요'; + modalStatusEl.classList.remove('text-green-600'); + modalStatusEl.classList.add('text-gray-500'); + } + } + } @endpush diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php index a42aa12c..b45fbd9b 100644 --- a/resources/views/partials/header.blade.php +++ b/resources/views/partials/header.blade.php @@ -93,9 +93,9 @@ class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 hover
-
+ @csrf -
@@ -114,5 +114,23 @@ class="flex items-center gap-2 px-3 py-2 text-sm font-medium text-gray-700 hover userMenu.classList.add('hidden'); } }); + + /** + * 로그아웃 처리 (FCM 토큰 해제 후 로그아웃) + */ + async function handleLogout() { + // FCM 토큰 해제 시도 (window.FCM이 있는 경우에만) + if (window.FCM && typeof window.FCM.unregisterToken === 'function') { + try { + await window.FCM.unregisterToken(); + console.log('[Logout] FCM token unregistered'); + } catch (error) { + console.warn('[Logout] FCM unregister failed:', error); + } + } + + // 로그아웃 폼 제출 + document.getElementById('logout-form').submit(); + } @endpush diff --git a/routes/web.php b/routes/web.php index 8557136b..bf4c843d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -283,6 +283,11 @@ Route::post('/', [FlowTesterController::class, 'store'])->name('store'); Route::post('/validate-json', [FlowTesterController::class, 'validateJson'])->name('validate-json'); + // 토큰 관리 라우트 + Route::post('/token/save', [FlowTesterController::class, 'saveToken'])->name('token.save'); + Route::post('/token/clear', [FlowTesterController::class, 'clearToken'])->name('token.clear'); + Route::get('/token/status', [FlowTesterController::class, 'tokenStatus'])->name('token.status'); + // /runs/* 관련 라우트 (고정 경로) Route::get('/runs/{runId}/status', [FlowTesterController::class, 'runStatus'])->name('run-status'); Route::get('/runs/{runId}', [FlowTesterController::class, 'runDetail'])->name('run-detail'); @@ -334,6 +339,9 @@ Route::post('/environments', [ApiExplorerController::class, 'storeEnvironment'])->name('environments.store'); Route::put('/environments/{id}', [ApiExplorerController::class, 'updateEnvironment'])->name('environments.update'); Route::delete('/environments/{id}', [ApiExplorerController::class, 'deleteEnvironment'])->name('environments.destroy'); + + // 사용자 목록 (인증용) + Route::get('/users', [ApiExplorerController::class, 'users'])->name('users'); }); }); });