// 전역 변수
let currentUser = null;
let authToken = null;
const API_BASE_URL = '';
// DOM 로드 완료 시 초기화
document.addEventListener('DOMContentLoaded', function() {
initializeApp();
});
// 앱 초기화
function initializeApp() {
hideLoading();
// 저장된 토큰 확인
const savedToken = localStorage.getItem('authToken');
if (savedToken) {
authToken = savedToken;
validateToken();
} else {
showLoginScreen();
}
// 이벤트 리스너 설정
setupEventListeners();
}
// 이벤트 리스너 설정
function setupEventListeners() {
// 로그인 폼
document.getElementById('loginForm').addEventListener('submit', handleLogin);
// 회원가입 폼
document.getElementById('registerForm').addEventListener('submit', handleRegister);
// 로그아웃 버튼
document.getElementById('logoutBtn').addEventListener('click', handleLogout);
// 네비게이션 링크
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const section = this.getAttribute('data-section');
showSection(section);
});
});
// API 키 생성 폼
document.getElementById('createKeyForm').addEventListener('submit', handleCreateApiKey);
// 데이터 생성 폼
document.getElementById('createDataForm').addEventListener('submit', handleCreateData);
// 모달 외부 클릭 시 닫기
window.addEventListener('click', function(e) {
if (e.target.classList.contains('modal')) {
e.target.style.display = 'none';
}
});
}
// 로딩 표시/숨김
function showLoading() {
document.getElementById('loading').style.display = 'flex';
}
function hideLoading() {
document.getElementById('loading').style.display = 'none';
}
// 로그인 화면 표시
function showLoginScreen() {
document.getElementById('loginScreen').style.display = 'flex';
document.getElementById('dashboard').style.display = 'none';
}
// 대시보드 표시
function showDashboard() {
document.getElementById('loginScreen').style.display = 'none';
document.getElementById('dashboard').style.display = 'grid';
// 사용자 정보 표시
if (currentUser) {
document.getElementById('currentUser').textContent = currentUser.username;
const roleElement = document.getElementById('userRole');
roleElement.textContent = currentUser.role;
roleElement.className = `role-badge ${currentUser.role}`;
// 관리자 권한에 따른 UI 조정
if (currentUser.role === 'admin') {
document.body.classList.add('admin');
}
}
// 초기 데이터 로드
loadDashboardData();
// API 키 사용 횟수 실시간 업데이트를 위한 주기적 새로고침 (30초마다)
if (window.apiKeyRefreshInterval) {
clearInterval(window.apiKeyRefreshInterval);
}
window.apiKeyRefreshInterval = setInterval(() => {
if (document.getElementById('apiKeysSection').style.display !== 'none') {
loadApiKeys();
}
}, 30000);
}
// 토큰 검증
async function validateToken() {
try {
showLoading();
const response = await fetch('/auth/me', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
currentUser = data.user;
showDashboard();
} else {
localStorage.removeItem('authToken');
authToken = null;
showLoginScreen();
}
} catch (error) {
console.error('토큰 검증 실패:', error);
localStorage.removeItem('authToken');
authToken = null;
showLoginScreen();
} finally {
hideLoading();
}
}
// 로그인 처리
async function handleLogin(e) {
e.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
try {
showLoading();
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
authToken = data.token;
currentUser = data.user;
localStorage.setItem('authToken', authToken);
showToast('로그인 성공!', 'success');
showDashboard();
} else {
showToast(data.message || '로그인 실패', 'error');
}
} catch (error) {
console.error('로그인 오류:', error);
showToast('로그인 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// 회원가입 처리
async function handleRegister(e) {
e.preventDefault();
const username = document.getElementById('registerUsername').value;
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
try {
showLoading();
const response = await fetch('/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ username, email, password })
});
const data = await response.json();
if (response.ok) {
showToast('회원가입 성공! 로그인해주세요.', 'success');
showLoginTab();
document.getElementById('registerForm').reset();
} else {
showToast(data.message || '회원가입 실패', 'error');
}
} catch (error) {
console.error('회원가입 오류:', error);
showToast('회원가입 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// 로그아웃 처리
function handleLogout() {
localStorage.removeItem('authToken');
authToken = null;
currentUser = null;
document.body.classList.remove('admin');
// API 키 새로고침 인터벌 정리
if (window.apiKeyRefreshInterval) {
clearInterval(window.apiKeyRefreshInterval);
window.apiKeyRefreshInterval = null;
}
showLoginScreen();
showToast('로그아웃되었습니다', 'info');
}
// 탭 전환
function showLoginTab() {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-btn')[0].classList.add('active');
document.getElementById('loginForm').style.display = 'block';
document.getElementById('registerForm').style.display = 'none';
}
function showRegisterTab() {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-btn')[1].classList.add('active');
document.getElementById('loginForm').style.display = 'none';
document.getElementById('registerForm').style.display = 'block';
}
// 섹션 표시
function showSection(sectionName) {
// 모든 섹션 숨기기
document.querySelectorAll('.content-section').forEach(section => {
section.classList.remove('active');
});
// 모든 네비게이션 링크 비활성화
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
// 선택된 섹션 표시
const targetSection = document.getElementById(`${sectionName}-section`);
if (targetSection) {
targetSection.classList.add('active');
}
// 선택된 네비게이션 링크 활성화
const targetLink = document.querySelector(`[data-section="${sectionName}"]`);
if (targetLink) {
targetLink.classList.add('active');
}
// 섹션별 데이터 로드
loadSectionData(sectionName);
}
// 대시보드 데이터 로드
async function loadDashboardData() {
await Promise.all([
loadApiKeys(),
loadStats(),
loadData()
]);
if (currentUser && currentUser.role === 'admin') {
await Promise.all([
loadUsers(),
checkSystemStatus()
]);
}
}
// 섹션별 데이터 로드
async function loadSectionData(sectionName) {
switch (sectionName) {
case 'overview':
await loadStats();
break;
case 'api-keys':
await loadApiKeys();
break;
case 'data-management':
await loadData();
break;
case 'users':
if (currentUser && currentUser.role === 'admin') {
await loadUsers();
}
break;
case 'api-logs':
if (currentUser && currentUser.role === 'admin') {
await loadApiLogs();
await loadApiLogStats();
}
break;
case 'api-tester':
await loadApiTester();
break;
case 'monitoring':
if (currentUser && currentUser.role === 'admin') {
await checkSystemStatus();
}
break;
}
}
// API 키 목록 로드
async function loadApiKeys() {
try {
showLoading();
const response = await fetch('/auth/api-keys', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayApiKeys(data.apiKeys || []);
} else {
console.error('API 키 로드 실패:', response.status, response.statusText);
showToast('API 키 목록을 불러올 수 없습니다', 'error');
}
} catch (error) {
console.error('API 키 로드 실패:', error);
showToast('API 키 로드 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// 권한 라벨 반환
function getPermissionLabel(permission) {
const labels = {
'read': '읽기',
'write': '쓰기',
'delete': '삭제',
'admin': '관리자'
};
return labels[permission] || permission;
}
// API 키 표시
function displayApiKeys(apiKeys) {
const tbody = document.querySelector('#apiKeysTable tbody');
if (!tbody) {
console.error('API 키 테이블을 찾을 수 없습니다');
return;
}
tbody.innerHTML = '';
if (!apiKeys || apiKeys.length === 0) {
tbody.innerHTML = `
| 생성된 API 키가 없습니다. |
`;
return;
}
apiKeys.forEach(key => {
const row = document.createElement('tr');
row.innerHTML = `
${key.keyName}
|
|
${Array.isArray(key.permissions) && key.permissions.length > 0
? key.permissions.map(p => `${getPermissionLabel(p)}`).join(' ')
: '권한 없음'
}
|
${key.usageCount || 0} |
${key.lastUsed ? new Date(key.lastUsed).toLocaleString() : '사용 안함'} |
${new Date(key.createdAt).toLocaleString()} |
${key.isActive ? '활성' : '비활성'} |
|
`;
tbody.appendChild(row);
});
}
// 통계 로드
async function loadStats() {
try {
if (currentUser && currentUser.role === 'admin') {
const response = await fetch('/auth/stats', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayStats(data.stats);
}
} else {
// 일반 사용자는 자신의 API 키 통계만 표시
const response = await fetch('/auth/api-keys', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const userStats = {
totalKeys: data.apiKeys.length,
totalRequests: data.apiKeys.reduce((sum, key) => sum + key.usageCount, 0),
activeKeysToday: data.apiKeys.filter(key => key.isActive).length
};
displayStats(userStats);
}
}
} catch (error) {
console.error('통계 로드 실패:', error);
}
}
// 통계 표시
function displayStats(stats) {
document.getElementById('totalKeys').textContent = stats.totalKeys || 0;
document.getElementById('totalRequests').textContent = stats.totalRequests || 0;
document.getElementById('activeKeysToday').textContent = stats.activeKeysToday || 0;
if (stats.totalUsers !== undefined) {
document.getElementById('totalUsers').textContent = stats.totalUsers;
}
}
// 데이터 로드
async function loadData() {
try {
// API 키가 있는 경우 해당 키로 요청, 없으면 JWT 토큰으로 요청
const headers = { 'Authorization': `Bearer ${authToken}` };
const response = await fetch('/api/data', { headers });
if (response.ok) {
const data = await response.json();
displayData(data.data);
}
} catch (error) {
console.error('데이터 로드 실패:', error);
}
}
// 데이터 표시
function displayData(dataList) {
const tbody = document.querySelector('#dataTable tbody');
tbody.innerHTML = '';
dataList.forEach(item => {
const row = document.createElement('tr');
row.innerHTML = `
${item[0]} |
${item[1]} |
${item[2] || ''} |
${item[3] ? item[3].substring(0, 100) + '...' : ''} |
${item[4] ? new Date(item[4]).toLocaleString() : ''} |
${item[5] ? new Date(item[5]).toLocaleString() : ''} |
|
`;
tbody.appendChild(row);
});
}
// 사용자 목록 로드 (관리자 전용)
async function loadUsers() {
try {
const response = await fetch('/auth/users', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayUsers(data.users);
}
} catch (error) {
console.error('사용자 목록 로드 실패:', error);
}
}
// 사용자 목록 표시
function displayUsers(users) {
const tbody = document.querySelector('#usersTable tbody');
tbody.innerHTML = '';
users.forEach(user => {
const row = document.createElement('tr');
row.innerHTML = `
${user.id} |
${user.username} |
${user.email} |
${user.role} |
${user.isActive ? '활성' : '비활성'} |
${new Date(user.createdAt).toLocaleString()} |
${user.lastLogin ? new Date(user.lastLogin).toLocaleString() : '없음'} |
`;
tbody.appendChild(row);
});
}
// 시스템 상태 확인
async function checkSystemStatus() {
try {
// 서버 상태 확인
const serverResponse = await fetch('/api/health');
const serverStatus = document.getElementById('serverStatus');
if (serverResponse.ok) {
serverStatus.innerHTML = ' 온라인';
serverStatus.className = 'status-indicator online';
} else {
serverStatus.innerHTML = ' 오프라인';
serverStatus.className = 'status-indicator offline';
}
// 데이터베이스 상태는 서버 응답으로 판단
const dbStatus = document.getElementById('dbStatus');
if (serverResponse.ok) {
dbStatus.innerHTML = ' 연결됨';
dbStatus.className = 'status-indicator online';
} else {
dbStatus.innerHTML = ' 연결 실패';
dbStatus.className = 'status-indicator offline';
}
} catch (error) {
console.error('시스템 상태 확인 실패:', error);
const serverStatus = document.getElementById('serverStatus');
const dbStatus = document.getElementById('dbStatus');
serverStatus.innerHTML = ' 오프라인';
serverStatus.className = 'status-indicator offline';
dbStatus.innerHTML = ' 연결 실패';
dbStatus.className = 'status-indicator offline';
}
}
// API 키 생성 모달 표시
function showCreateKeyModal() {
document.getElementById('createKeyModal').style.display = 'block';
}
// 데이터 생성 모달 표시
function showCreateDataModal() {
document.getElementById('createDataModal').style.display = 'block';
}
// 모달 닫기
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
// API 키 생성 완료 모달 표시
function showApiKeyCreatedModal(apiKey, keyName) {
// 기존 모달이 있다면 제거
const existingModal = document.getElementById('apiKeyCreatedModal');
if (existingModal) {
existingModal.remove();
}
// 새 모달 생성
const modal = document.createElement('div');
modal.id = 'apiKeyCreatedModal';
modal.className = 'modal';
modal.style.display = 'block';
modal.innerHTML = `
${keyName} API 키가 성공적으로 생성되었습니다!
중요: 이 API 키를 안전한 곳에 저장하세요.
보안상의 이유로 다시 전체 키를 확인할 수 없습니다.
`;
document.body.appendChild(modal);
// 모달 외부 클릭 시 닫기
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeApiKeyCreatedModal();
}
});
}
// 새 API 키 복사
function copyNewApiKey(apiKey) {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(apiKey).then(() => {
showToast('API 키가 클립보드에 복사되었습니다!', 'success');
}).catch(() => {
fallbackCopyText(apiKey);
});
} else {
fallbackCopyText(apiKey);
}
}
// API 키 생성 완료 모달 닫기
function closeApiKeyCreatedModal() {
const modal = document.getElementById('apiKeyCreatedModal');
if (modal) {
modal.remove();
}
}
// API 키 생성 처리
async function handleCreateApiKey(e) {
e.preventDefault();
const keyName = document.getElementById('keyName').value;
const permissions = Array.from(document.querySelectorAll('#createKeyForm input[type="checkbox"]:checked'))
.map(cb => cb.value);
try {
showLoading();
const response = await fetch('/auth/api-keys', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ keyName, permissions })
});
const data = await response.json();
if (response.ok) {
showToast('API 키가 생성되었습니다!', 'success');
closeModal('createKeyModal');
document.getElementById('createKeyForm').reset();
// 생성된 API 키를 복사 가능한 모달로 표시
showApiKeyCreatedModal(data.apiKey, data.keyName);
await loadApiKeys();
} else {
showToast(data.message || 'API 키 생성 실패', 'error');
}
} catch (error) {
console.error('API 키 생성 오류:', error);
showToast('API 키 생성 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// 데이터 생성 처리
async function handleCreateData(e) {
e.preventDefault();
const name = document.getElementById('dataName').value;
const description = document.getElementById('dataDescription').value;
const dataValue = document.getElementById('dataValue').value;
try {
showLoading();
const response = await fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ name, description, dataValue })
});
const data = await response.json();
if (response.ok) {
showToast('데이터가 생성되었습니다!', 'success');
closeModal('createDataModal');
document.getElementById('createDataForm').reset();
await loadData();
} else {
showToast(data.message || '데이터 생성 실패', 'error');
}
} catch (error) {
console.error('데이터 생성 오류:', error);
showToast('데이터 생성 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// API 키 삭제
async function deleteApiKey(keyId, keyName) {
if (!confirm(`정말로 "${keyName}" API 키를 삭제하시겠습니까?\n\n⚠️ 이 작업은 되돌릴 수 없습니다!`)) {
return;
}
try {
showLoading();
const response = await fetch(`/auth/api-keys/${keyId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (response.ok) {
showToast('API 키가 삭제되었습니다', 'success');
await loadApiKeys();
} else {
showToast(data.message || 'API 키 삭제 실패', 'error');
}
} catch (error) {
console.error('API 키 삭제 오류:', error);
showToast('API 키 삭제 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// 데이터 삭제
async function deleteData(id) {
if (!confirm('이 데이터를 삭제하시겠습니까?')) {
return;
}
try {
showLoading();
const response = await fetch(`/api/data/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${authToken}`
}
});
const data = await response.json();
if (response.ok) {
showToast('데이터가 삭제되었습니다', 'success');
await loadData();
} else {
showToast(data.message || '데이터 삭제 실패', 'error');
}
} catch (error) {
console.error('데이터 삭제 오류:', error);
showToast('데이터 삭제 중 오류가 발생했습니다', 'error');
} finally {
hideLoading();
}
}
// API 연결 테스트
async function testApiConnection() {
try {
showLoading();
const response = await fetch('/api/health');
if (response.ok) {
const data = await response.json();
showToast('API 연결이 정상입니다!', 'success');
} else {
showToast('API 연결에 문제가 있습니다', 'error');
}
} catch (error) {
console.error('API 연결 테스트 실패:', error);
showToast('API 연결 테스트 실패', 'error');
} finally {
hideLoading();
}
}
// 토스트 알림 표시
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
// 기존 타이머가 있다면 제거
if (toast.hideTimer) {
clearTimeout(toast.hideTimer);
}
toast.textContent = message;
toast.className = `toast ${type}`;
toast.classList.add('show');
// 새로운 타이머 설정
toast.hideTimer = setTimeout(() => {
toast.classList.remove('show');
// 애니메이션이 끝난 후 완전히 숨김
setTimeout(() => {
if (!toast.classList.contains('show')) {
toast.style.display = 'none';
}
}, 300);
}, 3000);
// 토스트가 보일 때는 display를 block으로 설정
toast.style.display = 'block';
// 클릭하면 즉시 닫기
toast.onclick = () => {
if (toast.hideTimer) {
clearTimeout(toast.hideTimer);
}
toast.classList.remove('show');
setTimeout(() => {
toast.style.display = 'none';
}, 300);
};
}
// API 키 복사 기능
function copyApiKey(apiKey, keyName) {
// 클립보드에 복사
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(apiKey).then(() => {
showToast(`"${keyName}" API 키가 클립보드에 복사되었습니다!`, 'success');
}).catch(() => {
// 폴백: 텍스트 선택
fallbackCopyApiKey(apiKey, keyName);
});
} else {
// 폴백: 텍스트 선택
fallbackCopyApiKey(apiKey, keyName);
}
}
// 폴백 복사 기능 (구형 브라우저용)
function fallbackCopyApiKey(apiKey, keyName) {
const textArea = document.createElement('textarea');
textArea.value = apiKey;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast(`"${keyName}" API 키가 클립보드에 복사되었습니다!`, 'success');
} catch (err) {
console.error('복사 실패:', err);
showToast('복사에 실패했습니다. 수동으로 선택해서 복사해주세요.', 'warning');
// API 키를 선택 상태로 만들기
const keyElement = document.querySelector(`#key-${keyId}`);
if (keyElement) {
const range = document.createRange();
range.selectNodeContents(keyElement);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
}
document.body.removeChild(textArea);
}
// API 키 숨기기/보이기 토글
function toggleApiKeyVisibility(keyId) {
const keyElement = document.getElementById(`key-${keyId}`);
const toggleBtn = document.getElementById(`toggle-${keyId}`);
if (keyElement.classList.contains('api-key-hidden')) {
// 보이기
keyElement.classList.remove('api-key-hidden');
toggleBtn.innerHTML = '';
toggleBtn.title = '숨기기';
} else {
// 숨기기
keyElement.classList.add('api-key-hidden');
toggleBtn.innerHTML = '';
toggleBtn.title = '보이기';
}
}
// 모든 API 키 복사 (여러 개가 있을 때)
function copyAllApiKeys() {
const apiKeyElements = document.querySelectorAll('.api-key-value');
const apiKeys = Array.from(apiKeyElements).map(el => el.textContent).join('\n');
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(apiKeys).then(() => {
showToast(`${apiKeyElements.length}개의 API 키가 모두 복사되었습니다!`, 'success');
}).catch(() => {
fallbackCopyText(apiKeys);
});
} else {
fallbackCopyText(apiKeys);
}
}
// 텍스트 복사 폴백 함수
function fallbackCopyText(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast('클립보드에 복사되었습니다!', 'success');
} catch (err) {
showToast('복사에 실패했습니다.', 'error');
}
document.body.removeChild(textArea);
}
// 탭 기능
function showTab(tabId) {
// 모든 탭 콘텐츠 숨기기
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
// 모든 탭 버튼 비활성화
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
// 선택된 탭 콘텐츠 보이기
const selectedTab = document.getElementById(tabId);
if (selectedTab) {
selectedTab.classList.add('active');
}
// 클릭된 버튼 활성화
event.target.classList.add('active');
}
// 유틸리티 함수들
function formatDate(dateString) {
return new Date(dateString).toLocaleString();
}
function formatNumber(num) {
return new Intl.NumberFormat().format(num);
}
// API 로그 관련 함수들
let currentLogPage = 1;
const logsPerPage = 50;
// API 로그 목록 로드
async function loadApiLogs(page = 1) {
try {
showLoading();
const filters = {
startDate: document.getElementById('startDate').value,
endDate: document.getElementById('endDate').value,
method: document.getElementById('methodFilter').value,
endpoint: document.getElementById('endpointFilter').value,
username: document.getElementById('usernameFilter').value
};
const queryParams = new URLSearchParams({
page: page,
limit: logsPerPage,
...Object.fromEntries(Object.entries(filters).filter(([_, v]) => v))
});
const response = await fetch(`/logs/api-logs?${queryParams}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
displayApiLogs(data.logs);
displayLogPagination(data.page, data.totalPages, data.total);
currentLogPage = page;
} else {
throw new Error('API 로그 로드 실패');
}
} catch (error) {
console.error('API 로그 로드 오류:', error);
showToast('API 로그 로드에 실패했습니다.', 'error');
} finally {
hideLoading();
}
}
// API 로그 통계 로드
async function loadApiLogStats() {
try {
const response = await fetch('/logs/api-logs/stats', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const stats = await response.json();
document.getElementById('totalApiRequests').textContent = formatNumber(stats.totalRequests);
document.getElementById('todayApiRequests').textContent = formatNumber(stats.todayRequests);
}
} catch (error) {
console.error('API 로그 통계 로드 오류:', error);
}
}
// API 로그 표시
function displayApiLogs(logs) {
const tbody = document.querySelector('#apiLogsTable tbody');
tbody.innerHTML = '';
if (logs.length === 0) {
tbody.innerHTML = '| 로그가 없습니다. |
';
return;
}
logs.forEach(log => {
const row = document.createElement('tr');
const methodClass = `method-${log.method.toLowerCase()}`;
const statusClass = getStatusClass(log.responseStatus);
row.innerHTML = `
${formatDate(log.createdAt)} |
${log.method} |
${log.endpoint} |
${log.username || '-'} |
${log.apiKeyName || '-'} |
${log.responseStatus} |
${log.responseTime}ms |
${log.ipAddress || '-'} |
`;
tbody.appendChild(row);
});
}
// 상태 코드에 따른 CSS 클래스 반환
function getStatusClass(status) {
if (status >= 200 && status < 300) return 'status-success';
if (status >= 400 && status < 500) return 'status-warning';
if (status >= 500) return 'status-error';
return 'status-success';
}
// 로그 페이지네이션 표시
function displayLogPagination(currentPage, totalPages, totalItems) {
const pagination = document.getElementById('logPagination');
pagination.innerHTML = '';
if (totalPages <= 1) return;
// 이전 버튼
const prevBtn = document.createElement('button');
prevBtn.textContent = '이전';
prevBtn.disabled = currentPage === 1;
prevBtn.onclick = () => loadApiLogs(currentPage - 1);
pagination.appendChild(prevBtn);
// 페이지 번호들
const startPage = Math.max(1, currentPage - 2);
const endPage = Math.min(totalPages, currentPage + 2);
for (let i = startPage; i <= endPage; i++) {
const pageBtn = document.createElement('button');
pageBtn.textContent = i;
pageBtn.className = i === currentPage ? 'active' : '';
pageBtn.onclick = () => loadApiLogs(i);
pagination.appendChild(pageBtn);
}
// 다음 버튼
const nextBtn = document.createElement('button');
nextBtn.textContent = '다음';
nextBtn.disabled = currentPage === totalPages;
nextBtn.onclick = () => loadApiLogs(currentPage + 1);
pagination.appendChild(nextBtn);
// 총 개수 표시
const info = document.createElement('span');
info.textContent = ` (총 ${formatNumber(totalItems)}개)`;
info.style.marginLeft = '1rem';
info.style.color = '#6c757d';
pagination.appendChild(info);
}
// 로그 필터 초기화
function clearLogFilters() {
document.getElementById('startDate').value = '';
document.getElementById('endDate').value = '';
document.getElementById('methodFilter').value = '';
document.getElementById('endpointFilter').value = '';
document.getElementById('usernameFilter').value = '';
loadApiLogs(1);
}
// API 테스터 관련 함수들
async function loadApiTester() {
// API 키 목록을 셀렉트박스에 로드
try {
const response = await fetch('/auth/api-keys', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
if (response.ok) {
const data = await response.json();
const select = document.getElementById('testApiKey');
select.innerHTML = '';
(data.apiKeys || []).forEach(key => {
const option = document.createElement('option');
option.value = key.apiKey;
option.textContent = `${key.keyName} (${key.apiKey.substring(0, 8)}...)`;
select.appendChild(option);
});
}
} catch (error) {
console.error('API 키 로드 실패:', error);
}
// 메소드 변경 시 요청 본문 표시/숨김
document.getElementById('testMethod').addEventListener('change', function() {
const method = this.value;
const bodyGroup = document.getElementById('requestBodyGroup');
if (method === 'POST' || method === 'PUT') {
bodyGroup.style.display = 'block';
} else {
bodyGroup.style.display = 'none';
}
});
}
// API 요청 전송
async function sendApiRequest() {
const method = document.getElementById('testMethod').value;
const url = document.getElementById('testUrl').value;
const customApiKey = document.getElementById('customApiKey').value;
const selectedApiKey = document.getElementById('testApiKey').value;
const body = document.getElementById('testBody').value;
const apiKey = customApiKey || selectedApiKey;
if (!apiKey) {
showToast('API 키를 선택하거나 입력해주세요.', 'error');
return;
}
if (!url) {
showToast('엔드포인트를 입력해주세요.', 'error');
return;
}
const fullUrl = `http://localhost:5577${url}`;
const startTime = Date.now();
try {
showLoading();
const requestOptions = {
method: method,
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
}
};
if ((method === 'POST' || method === 'PUT') && body) {
try {
JSON.parse(body); // JSON 유효성 검사
requestOptions.body = body;
} catch (e) {
showToast('요청 본문이 유효한 JSON이 아닙니다.', 'error');
return;
}
}
const response = await fetch(fullUrl, requestOptions);
const endTime = Date.now();
const responseTime = endTime - startTime;
// 응답 헤더 수집
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
// 응답 본문 읽기
let responseBody;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
responseBody = await response.json();
} else {
responseBody = await response.text();
}
// 결과 표시
displayApiResponse({
status: response.status,
statusText: response.statusText,
headers: headers,
body: responseBody,
responseTime: responseTime
});
showToast(`API 호출 완료 (${responseTime}ms)`, 'success');
} catch (error) {
console.error('API 요청 실패:', error);
showToast(`API 요청 실패: ${error.message}`, 'error');
// 오류 응답 표시
displayApiResponse({
status: 0,
statusText: 'Network Error',
headers: {},
body: { error: error.message },
responseTime: Date.now() - startTime
});
} finally {
hideLoading();
}
}
// API 응답 표시
function displayApiResponse(response) {
const responseDiv = document.getElementById('apiResponse');
const statusSpan = document.getElementById('responseStatus');
const timeSpan = document.getElementById('responseTime');
const headersDiv = document.getElementById('responseHeaders');
const bodyDiv = document.getElementById('responseBody');
// 상태 표시
statusSpan.textContent = `${response.status} ${response.statusText}`;
statusSpan.className = `response-status ${response.status >= 200 && response.status < 300 ? 'success' : 'error'}`;
// 응답 시간 표시
timeSpan.textContent = `${response.responseTime}ms`;
// 헤더 표시
headersDiv.textContent = JSON.stringify(response.headers, null, 2);
// 본문 표시
if (typeof response.body === 'object') {
bodyDiv.textContent = JSON.stringify(response.body, null, 2);
} else {
bodyDiv.textContent = response.body;
}
// 응답 영역 표시
responseDiv.style.display = 'block';
responseDiv.scrollIntoView({ behavior: 'smooth' });
}
// API 테스트 초기화
function clearApiTest() {
document.getElementById('testMethod').value = 'GET';
document.getElementById('testUrl').value = '/api/data';
document.getElementById('customApiKey').value = '';
document.getElementById('testApiKey').value = '';
document.getElementById('testBody').value = '';
document.getElementById('requestBodyGroup').style.display = 'none';
document.getElementById('apiResponse').style.display = 'none';
}
// 예제 로드
function loadExample(type) {
const methodSelect = document.getElementById('testMethod');
const urlInput = document.getElementById('testUrl');
const bodyTextarea = document.getElementById('testBody');
const bodyGroup = document.getElementById('requestBodyGroup');
switch (type) {
case 'getAllUsers':
methodSelect.value = 'GET';
urlInput.value = '/api/users';
bodyGroup.style.display = 'none';
break;
case 'getUser':
methodSelect.value = 'GET';
urlInput.value = '/api/users/user001';
bodyGroup.style.display = 'none';
break;
case 'createUser':
methodSelect.value = 'POST';
urlInput.value = '/api/users';
bodyTextarea.value = JSON.stringify({
USER_ID: "user999",
USER_NAME: "홍길동",
DEPT_CODE: "IT001",
EMAIL: "hong@example.com",
PHONE: "010-1234-5678"
}, null, 2);
bodyGroup.style.display = 'block';
break;
case 'updateUser':
methodSelect.value = 'PUT';
urlInput.value = '/api/users/user999';
bodyTextarea.value = JSON.stringify({
USER_NAME: "홍길동(수정)",
DEPT_CODE: "IT002",
EMAIL: "hong.updated@example.com",
PHONE: "010-9876-5432"
}, null, 2);
bodyGroup.style.display = 'block';
break;
case 'deleteUser':
methodSelect.value = 'DELETE';
urlInput.value = '/api/users/user999';
bodyGroup.style.display = 'none';
break;
}
}