refactor(dev-tools): 인증 시스템 통합 및 테넌트 사용자 조회 개선
## 인증 모달 통합
- api-explorer, flow-tester, api-logs 3개 페이지의 인증 UI 통합
- 공유 컴포넌트 생성: auth-modal.blade.php, auth-scripts.blade.php
- sessionStorage 기반으로 페이지 간 인증 상태 공유
- DevToolsAuth 글로벌 JavaScript API 제공
## 테넌트 사용자 조회 개선
- 시스템 헤더에서 선택한 테넌트의 사용자 목록 표시
- 관리자가 모든 테넌트의 사용자 조회 가능 (소속 무관)
- session('selected_tenant_id')로 Tenant 모델 직접 조회
- 테넌트 미선택 시 안내 메시지 표시
## 버그 수정
- /users 페이지 HTMX swap 오류 수정 (JSON→HTML 직접 반환)
- 사용자 이름 JavaScript 이스케이프 처리 (@js() 사용)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
389
resources/views/dev-tools/partials/auth-scripts.blade.php
Normal file
389
resources/views/dev-tools/partials/auth-scripts.blade.php
Normal file
@@ -0,0 +1,389 @@
|
||||
{{--
|
||||
Dev Tools 공유 인증 스크립트
|
||||
사용법: @include('dev-tools.partials.auth-scripts')
|
||||
|
||||
제공 API:
|
||||
- DevToolsAuth.openModal() : 인증 모달 열기
|
||||
- DevToolsAuth.closeModal() : 인증 모달 닫기
|
||||
- DevToolsAuth.getToken() : 현재 토큰 반환
|
||||
- DevToolsAuth.getUserId() : 현재 사용자 ID 반환
|
||||
- DevToolsAuth.isAuthenticated() : 인증 여부 반환
|
||||
- DevToolsAuth.getAuthPayload() : API 요청용 인증 페이로드 반환
|
||||
- DevToolsAuth.onAuthChange(callback) : 인증 상태 변경 콜백 등록
|
||||
--}}
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const STORAGE_KEY = 'devToolsAuth';
|
||||
const USERS_ENDPOINT = '{{ route("dev-tools.api-explorer.users") }}';
|
||||
const CSRF_TOKEN = '{{ csrf_token() }}';
|
||||
|
||||
// 인증 상태
|
||||
let state = {
|
||||
token: null,
|
||||
userId: null,
|
||||
userName: null,
|
||||
userEmail: null
|
||||
};
|
||||
|
||||
// 사용자 목록 캐시
|
||||
let usersLoaded = false;
|
||||
let usersCache = [];
|
||||
let currentTenantInfo = null;
|
||||
|
||||
// 상태 변경 콜백 목록
|
||||
let changeCallbacks = [];
|
||||
|
||||
// sessionStorage에서 상태 로드
|
||||
function loadState() {
|
||||
try {
|
||||
const saved = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (saved) {
|
||||
state = JSON.parse(saved);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load auth state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// sessionStorage에 상태 저장
|
||||
function saveState() {
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save auth state:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// UI 업데이트
|
||||
function updateUI() {
|
||||
const isAuth = !!(state.token || state.userId);
|
||||
|
||||
// 헤더의 인증 상태 버튼 업데이트 (id="auth-status" 또는 class="dev-tools-auth-status")
|
||||
document.querySelectorAll('#auth-status, .dev-tools-auth-status').forEach(el => {
|
||||
el.textContent = isAuth ? '인증됨' : '인증 필요';
|
||||
el.classList.toggle('text-green-600', isAuth);
|
||||
el.classList.toggle('text-gray-500', !isAuth);
|
||||
});
|
||||
|
||||
// 모달 내 상태 표시
|
||||
const modalStatus = document.getElementById('devToolsAuthStatusDisplay');
|
||||
if (modalStatus) {
|
||||
modalStatus.textContent = isAuth ? '인증됨' : '인증 필요';
|
||||
modalStatus.classList.toggle('text-green-600', isAuth);
|
||||
modalStatus.classList.toggle('text-gray-500', !isAuth);
|
||||
}
|
||||
|
||||
// 사용자 정보 표시
|
||||
const userInfo = document.getElementById('devToolsAuthUserInfo');
|
||||
if (userInfo) {
|
||||
if (state.userId && state.userName) {
|
||||
userInfo.textContent = `사용자: ${state.userName} (${state.userEmail})`;
|
||||
userInfo.classList.remove('hidden');
|
||||
} else if (state.token) {
|
||||
userInfo.textContent = '토큰으로 인증됨';
|
||||
userInfo.classList.remove('hidden');
|
||||
} else {
|
||||
userInfo.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 토큰 입력 필드 업데이트
|
||||
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
||||
if (tokenInput && state.token) {
|
||||
tokenInput.value = state.token;
|
||||
}
|
||||
|
||||
// 사용자 선택 업데이트
|
||||
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
||||
if (userSelect && state.userId) {
|
||||
userSelect.value = state.userId;
|
||||
}
|
||||
}
|
||||
|
||||
// 콜백 호출
|
||||
function notifyChange() {
|
||||
const isAuth = !!(state.token || state.userId);
|
||||
changeCallbacks.forEach(cb => {
|
||||
try {
|
||||
cb(isAuth, state);
|
||||
} catch (e) {
|
||||
console.error('Auth change callback error:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 사용자 목록 로드
|
||||
async function loadUsers() {
|
||||
if (usersLoaded) return usersCache;
|
||||
|
||||
const spinner = document.getElementById('devToolsAuthUserSpinner');
|
||||
const select = document.getElementById('devToolsAuthSelectedUser');
|
||||
const tenantLabel = document.getElementById('devToolsAuthTenantLabel');
|
||||
|
||||
if (spinner) spinner.classList.remove('hidden');
|
||||
|
||||
try {
|
||||
const response = await fetch(USERS_ENDPOINT, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'X-CSRF-TOKEN': CSRF_TOKEN
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to load users');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 새 응답 형식: { tenant: {...}, users: [...] }
|
||||
if (data.tenant) {
|
||||
currentTenantInfo = data.tenant;
|
||||
usersCache = data.users || [];
|
||||
} else if (Array.isArray(data) && data.length === 0) {
|
||||
// 빈 배열 응답 - 테넌트 미선택
|
||||
currentTenantInfo = null;
|
||||
usersCache = [];
|
||||
} else {
|
||||
// 기존 형식 호환성 유지
|
||||
usersCache = Array.isArray(data) ? data : (data.data || []);
|
||||
}
|
||||
usersLoaded = true;
|
||||
|
||||
// 테넌트 정보 표시
|
||||
if (tenantLabel) {
|
||||
if (currentTenantInfo) {
|
||||
tenantLabel.textContent = `테넌트: ${currentTenantInfo.name}`;
|
||||
tenantLabel.classList.remove('hidden');
|
||||
tenantLabel.className = 'mb-2 px-3 py-2 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-700';
|
||||
} else {
|
||||
tenantLabel.textContent = '시스템 헤더에서 테넌트를 먼저 선택해주세요.';
|
||||
tenantLabel.classList.remove('hidden');
|
||||
tenantLabel.className = 'mb-2 px-3 py-2 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700';
|
||||
}
|
||||
}
|
||||
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="">사용자를 선택하세요</option>';
|
||||
if (usersCache.length === 0 && currentTenantInfo) {
|
||||
select.innerHTML = '<option value="">이 테넌트에 사용자가 없습니다</option>';
|
||||
}
|
||||
usersCache.forEach(user => {
|
||||
const option = document.createElement('option');
|
||||
option.value = user.id;
|
||||
option.textContent = `${user.name || user.email} (${user.email})`;
|
||||
option.dataset.name = user.name || user.email;
|
||||
option.dataset.email = user.email;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
// 현재 선택된 사용자 복원
|
||||
if (state.userId) {
|
||||
select.value = state.userId;
|
||||
}
|
||||
}
|
||||
|
||||
return usersCache;
|
||||
} catch (err) {
|
||||
console.error('사용자 목록 로드 실패:', err);
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('사용자 목록을 불러오는데 실패했습니다.', 'error');
|
||||
}
|
||||
return [];
|
||||
} finally {
|
||||
if (spinner) spinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// 공개 API
|
||||
window.DevToolsAuth = {
|
||||
// 모달 열기
|
||||
openModal() {
|
||||
const modal = document.getElementById('devToolsAuthModal');
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
updateUI();
|
||||
|
||||
// 인증 방식 라디오 버튼 설정
|
||||
const tokenRadio = document.querySelector('input[name="devToolsAuthType"][value="token"]');
|
||||
const userRadio = document.querySelector('input[name="devToolsAuthType"][value="user"]');
|
||||
if (state.userId && !state.token) {
|
||||
if (userRadio) userRadio.checked = true;
|
||||
this.toggleType();
|
||||
} else {
|
||||
if (tokenRadio) tokenRadio.checked = true;
|
||||
this.toggleType();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 모달 닫기
|
||||
closeModal() {
|
||||
const modal = document.getElementById('devToolsAuthModal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
}
|
||||
},
|
||||
|
||||
// 인증 방식 토글
|
||||
toggleType() {
|
||||
const authType = document.querySelector('input[name="devToolsAuthType"]:checked')?.value || 'token';
|
||||
const tokenSection = document.getElementById('devToolsAuthTokenSection');
|
||||
const userSection = document.getElementById('devToolsAuthUserSection');
|
||||
|
||||
if (authType === 'token') {
|
||||
if (tokenSection) tokenSection.classList.remove('hidden');
|
||||
if (userSection) userSection.classList.add('hidden');
|
||||
} else {
|
||||
if (tokenSection) tokenSection.classList.add('hidden');
|
||||
if (userSection) userSection.classList.remove('hidden');
|
||||
// 사용자 선택 시 항상 새로 로드 (테넌트 변경 대응)
|
||||
usersLoaded = false;
|
||||
loadUsers();
|
||||
}
|
||||
},
|
||||
|
||||
// 인증 저장
|
||||
save() {
|
||||
const authType = document.querySelector('input[name="devToolsAuthType"]:checked')?.value || 'token';
|
||||
|
||||
if (authType === 'token') {
|
||||
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
||||
const token = tokenInput?.value?.trim();
|
||||
|
||||
if (!token) {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('토큰을 입력해주세요.', 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state = {
|
||||
token: token,
|
||||
userId: null,
|
||||
userName: null,
|
||||
userEmail: null
|
||||
};
|
||||
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('토큰이 적용되었습니다.', 'success');
|
||||
}
|
||||
} else {
|
||||
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
||||
const userId = userSelect?.value;
|
||||
|
||||
if (!userId) {
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('사용자를 선택해주세요.', 'warning');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedOption = userSelect.options[userSelect.selectedIndex];
|
||||
|
||||
state = {
|
||||
token: null,
|
||||
userId: userId,
|
||||
userName: selectedOption?.dataset?.name || '',
|
||||
userEmail: selectedOption?.dataset?.email || ''
|
||||
};
|
||||
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('사용자가 선택되었습니다. API 실행 시 토큰이 자동 발급됩니다.', 'success');
|
||||
}
|
||||
}
|
||||
|
||||
saveState();
|
||||
updateUI();
|
||||
notifyChange();
|
||||
this.closeModal();
|
||||
},
|
||||
|
||||
// 인증 초기화
|
||||
clear() {
|
||||
state = {
|
||||
token: null,
|
||||
userId: null,
|
||||
userName: null,
|
||||
userEmail: null
|
||||
};
|
||||
|
||||
const tokenInput = document.getElementById('devToolsAuthBearerToken');
|
||||
const userSelect = document.getElementById('devToolsAuthSelectedUser');
|
||||
|
||||
if (tokenInput) tokenInput.value = '';
|
||||
if (userSelect) userSelect.value = '';
|
||||
|
||||
saveState();
|
||||
updateUI();
|
||||
notifyChange();
|
||||
|
||||
if (typeof showToast === 'function') {
|
||||
showToast('인증이 초기화되었습니다.', 'info');
|
||||
}
|
||||
},
|
||||
|
||||
// 현재 토큰 반환
|
||||
getToken() {
|
||||
return state.token;
|
||||
},
|
||||
|
||||
// 현재 사용자 ID 반환
|
||||
getUserId() {
|
||||
return state.userId;
|
||||
},
|
||||
|
||||
// 인증 여부 반환
|
||||
isAuthenticated() {
|
||||
return !!(state.token || state.userId);
|
||||
},
|
||||
|
||||
// API 요청용 인증 페이로드 반환
|
||||
getAuthPayload() {
|
||||
if (state.token) {
|
||||
return { token: state.token };
|
||||
}
|
||||
if (state.userId) {
|
||||
return { user_id: state.userId };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
// 현재 상태 반환
|
||||
getState() {
|
||||
return { ...state };
|
||||
},
|
||||
|
||||
// 인증 상태 변경 콜백 등록
|
||||
onAuthChange(callback) {
|
||||
if (typeof callback === 'function') {
|
||||
changeCallbacks.push(callback);
|
||||
}
|
||||
},
|
||||
|
||||
// 외부에서 상태 설정 (서버에서 저장된 토큰 로드 시 사용)
|
||||
setToken(token) {
|
||||
if (token) {
|
||||
state.token = token;
|
||||
state.userId = null;
|
||||
state.userName = null;
|
||||
state.userEmail = null;
|
||||
saveState();
|
||||
updateUI();
|
||||
notifyChange();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 초기화
|
||||
loadState();
|
||||
|
||||
// DOM 로드 후 UI 업데이트
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', updateUI);
|
||||
} else {
|
||||
updateUI();
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user