From f70f75fb220ffd21240704b98e52a2af234ee46f Mon Sep 17 00:00:00 2001 From: kent Date: Mon, 5 Jan 2026 15:10:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(dev-tools):=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=EC=97=90=20=ED=9A=8C=EC=82=AC=20=EC=84=A0?= =?UTF-8?q?=ED=83=9D=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 인증 모달에 회사(테넌트) 선택 드롭다운 추가 - 헤더의 $globalTenants 재사용 - tenant.switch 라우트와 동기화 - 회사 변경 시 사용자 목록 자동 갱신 - Bearer 토큰 표시 및 복사 기능 추가 - 토큰 발급 API 엔드포인트 추가 (POST /dev-tools/api-explorer/issue-token) - 현재 상태 영역에 토큰 표시 - 클립보드 복사 버튼 (Clipboard API + fallback) - 적용 후 모달 유지하여 토큰 복사 가능 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../DevTools/ApiExplorerController.php | 30 ++++ .../dev-tools/partials/auth-modal.blade.php | 47 ++++- .../dev-tools/partials/auth-scripts.blade.php | 164 ++++++++++++++++-- routes/web.php | 1 + 4 files changed, 216 insertions(+), 26 deletions(-) diff --git a/app/Http/Controllers/DevTools/ApiExplorerController.php b/app/Http/Controllers/DevTools/ApiExplorerController.php index d3b9286d..6acd2df8 100644 --- a/app/Http/Controllers/DevTools/ApiExplorerController.php +++ b/app/Http/Controllers/DevTools/ApiExplorerController.php @@ -493,6 +493,36 @@ public function users(): JsonResponse ]); } + /** + * 사용자 선택 시 Sanctum 토큰 발급 + */ + public function issueToken(Request $request): JsonResponse + { + $validated = $request->validate([ + 'user_id' => 'required|integer|exists:users,id', + ]); + + $user = \App\Models\User::find($validated['user_id']); + + if (!$user) { + return response()->json(['error' => '사용자를 찾을 수 없습니다.'], 404); + } + + // Sanctum 토큰 발급 + $token = $user->createToken('api-explorer', ['*'])->plainTextToken; + session(['api_explorer_token' => $token]); + + return response()->json([ + 'success' => true, + 'token' => $token, + 'user' => [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ], + ]); + } + /** * 즐겨찾기 수정 */ diff --git a/resources/views/dev-tools/partials/auth-modal.blade.php b/resources/views/dev-tools/partials/auth-modal.blade.php index 20d2f8d1..d26550da 100644 --- a/resources/views/dev-tools/partials/auth-modal.blade.php +++ b/resources/views/dev-tools/partials/auth-modal.blade.php @@ -42,14 +42,30 @@ class="w-full border rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-5 + + diff --git a/resources/views/dev-tools/partials/auth-scripts.blade.php b/resources/views/dev-tools/partials/auth-scripts.blade.php index d0452664..b1e17de0 100644 --- a/resources/views/dev-tools/partials/auth-scripts.blade.php +++ b/resources/views/dev-tools/partials/auth-scripts.blade.php @@ -18,6 +18,8 @@ const STORAGE_KEY = 'devToolsAuth'; const USERS_ENDPOINT = '{{ route("dev-tools.api-explorer.users") }}'; + const TENANT_SWITCH_ENDPOINT = '{{ route("tenant.switch") }}'; + const ISSUE_TOKEN_ENDPOINT = '{{ route("dev-tools.api-explorer.issue-token") }}'; const CSRF_TOKEN = '{{ csrf_token() }}'; // 인증 상태 @@ -25,7 +27,8 @@ token: null, userId: null, userName: null, - userEmail: null + userEmail: null, + actualToken: null // 실제 발급된 토큰 (복사용) }; // 사용자 목록 캐시 @@ -101,6 +104,19 @@ function updateUI() { if (userSelect && state.userId) { userSelect.value = state.userId; } + + // 토큰 표시 영역 업데이트 + const tokenDisplay = document.getElementById('devToolsAuthTokenDisplay'); + const tokenValue = document.getElementById('devToolsAuthTokenValue'); + if (tokenDisplay && tokenValue) { + const displayToken = state.actualToken || state.token; + if (displayToken) { + tokenValue.textContent = displayToken; + tokenDisplay.classList.remove('hidden'); + } else { + tokenDisplay.classList.add('hidden'); + } + } } // 콜백 호출 @@ -245,7 +261,7 @@ function notifyChange() { }, // 인증 저장 - save() { + async save() { const authType = document.querySelector('input[name="devToolsAuthType"]:checked')?.value || 'token'; if (authType === 'token') { @@ -263,12 +279,18 @@ function notifyChange() { token: token, userId: null, userName: null, - userEmail: null + userEmail: null, + actualToken: token // 직접 입력한 토큰도 actualToken에 저장 }; if (typeof showToast === 'function') { - showToast('토큰이 적용되었습니다.', 'success'); + showToast('토큰이 적용되었습니다. 복사 후 닫기를 눌러주세요.', 'success'); } + + saveState(); + updateUI(); + notifyChange(); + // 토큰 표시를 위해 모달을 닫지 않음 - 사용자가 직접 닫기 클릭 } else { const userSelect = document.getElementById('devToolsAuthSelectedUser'); const userId = userSelect?.value; @@ -282,22 +304,47 @@ function notifyChange() { const selectedOption = userSelect.options[userSelect.selectedIndex]; - state = { - token: null, - userId: userId, - userName: selectedOption?.dataset?.name || '', - userEmail: selectedOption?.dataset?.email || '' - }; + // 토큰 발급 API 호출 + try { + const response = await fetch(ISSUE_TOKEN_ENDPOINT, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'X-CSRF-TOKEN': CSRF_TOKEN + }, + body: JSON.stringify({ user_id: userId }) + }); - if (typeof showToast === 'function') { - showToast('사용자가 선택되었습니다. API 실행 시 토큰이 자동 발급됩니다.', 'success'); + if (!response.ok) { + throw new Error('토큰 발급 실패'); + } + + const data = await response.json(); + + state = { + token: null, + userId: userId, + userName: selectedOption?.dataset?.name || data.user?.name || '', + userEmail: selectedOption?.dataset?.email || data.user?.email || '', + actualToken: data.token // 발급된 토큰 저장 + }; + + if (typeof showToast === 'function') { + showToast('토큰이 발급되었습니다. 복사 후 닫기를 눌러주세요.', 'success'); + } + + saveState(); + updateUI(); + notifyChange(); + // 토큰 표시를 위해 모달을 닫지 않음 - 사용자가 직접 닫기 클릭 + } catch (err) { + console.error('토큰 발급 실패:', err); + if (typeof showToast === 'function') { + showToast('토큰 발급에 실패했습니다.', 'error'); + } } } - - saveState(); - updateUI(); - notifyChange(); - this.closeModal(); }, // 인증 초기화 @@ -306,7 +353,8 @@ function notifyChange() { token: null, userId: null, userName: null, - userEmail: null + userEmail: null, + actualToken: null }; const tokenInput = document.getElementById('devToolsAuthBearerToken'); @@ -324,6 +372,86 @@ function notifyChange() { } }, + // 테넌트 변경 + async switchTenant(tenantId) { + if (!tenantId) return; + + try { + const formData = new FormData(); + formData.append('tenant_id', tenantId); + + const response = await fetch(TENANT_SWITCH_ENDPOINT, { + method: 'POST', + headers: { + 'X-CSRF-TOKEN': CSRF_TOKEN, + 'Accept': 'application/json' + }, + body: formData + }); + + if (!response.ok) { + throw new Error('테넌트 변경 실패'); + } + + // 사용자 목록 새로고침 + usersLoaded = false; + await loadUsers(); + + // 헤더의 테넌트 선택도 동기화 + const headerTenantSelect = document.getElementById('tenant-select'); + if (headerTenantSelect) { + headerTenantSelect.value = tenantId; + } + + if (typeof showToast === 'function') { + showToast('회사가 변경되었습니다.', 'success'); + } + } catch (err) { + console.error('테넌트 변경 실패:', err); + if (typeof showToast === 'function') { + showToast('회사 변경에 실패했습니다.', 'error'); + } + } + }, + + // 토큰 복사 + async copyToken() { + const displayToken = state.actualToken || state.token; + if (!displayToken) { + if (typeof showToast === 'function') { + showToast('복사할 토큰이 없습니다.', 'warning'); + } + return; + } + + try { + await navigator.clipboard.writeText(displayToken); + if (typeof showToast === 'function') { + showToast('토큰이 클립보드에 복사되었습니다.', 'success'); + } + } catch (err) { + console.error('클립보드 복사 실패:', err); + // Fallback: textarea를 이용한 복사 + const textarea = document.createElement('textarea'); + textarea.value = displayToken; + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + if (typeof showToast === 'function') { + showToast('토큰이 클립보드에 복사되었습니다.', 'success'); + } + } catch (e) { + if (typeof showToast === 'function') { + showToast('복사에 실패했습니다. 직접 선택하여 복사해주세요.', 'error'); + } + } + document.body.removeChild(textarea); + } + }, + // 현재 토큰 반환 getToken() { return state.token; diff --git a/routes/web.php b/routes/web.php index c8648e40..e89da895 100644 --- a/routes/web.php +++ b/routes/web.php @@ -423,6 +423,7 @@ // 사용자 목록 (인증용) Route::get('/users', [ApiExplorerController::class, 'users'])->name('users'); + Route::post('/issue-token', [ApiExplorerController::class, 'issueToken'])->name('issue-token'); // API 사용 현황 및 폐기 관리 Route::get('/usage', [ApiExplorerController::class, 'usage'])->name('usage');