feat(dev-tools): 인증 모달에 회사 선택 및 토큰 표시 기능 추가

- 인증 모달에 회사(테넌트) 선택 드롭다운 추가
  - 헤더의 $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 <noreply@anthropic.com>
This commit is contained in:
2026-01-05 15:10:09 +09:00
parent 301f2da23e
commit f70f75fb22
4 changed files with 216 additions and 26 deletions

View File

@@ -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;