diff --git a/database/auth-queries.js b/database/auth-queries.js
index fdb1409..6358072 100644
--- a/database/auth-queries.js
+++ b/database/auth-queries.js
@@ -1,5 +1,6 @@
const { getConnection } = require('./connection');
const crypto = require('crypto');
+const oracledb = require('oracledb');
// 사용자 생성
async function createUser(userData) {
@@ -177,19 +178,37 @@ async function getUserApiKeys(userId) {
FROM API_KEYS
WHERE USER_ID = :userId
ORDER BY CREATED_AT DESC`,
- [userId]
+ [userId],
+ {
+ fetchInfo: {
+ "PERMISSIONS": { type: oracledb.STRING } // CLOB을 문자열로 변환
+ }
+ }
);
- return result.rows.map(row => ({
- id: row[0],
- keyName: row[1],
- apiKey: row[2],
- permissions: typeof row[3] === 'string' ? JSON.parse(row[3] || '[]') : (row[3] || []),
- usageCount: row[4],
- lastUsed: row[5],
- createdAt: row[6],
- isActive: row[7]
- }));
+ return result.rows.map(row => {
+ let permissions = [];
+ try {
+ // CLOB이 문자열로 변환되어 있어야 함
+ if (row[3] && typeof row[3] === 'string') {
+ permissions = JSON.parse(row[3]);
+ }
+ } catch (e) {
+ console.error('권한 파싱 오류:', row[3], e);
+ permissions = [];
+ }
+
+ return {
+ id: row[0],
+ keyName: row[1],
+ apiKey: row[2],
+ permissions: permissions,
+ usageCount: row[4] || 0,
+ lastUsed: row[5],
+ createdAt: row[6],
+ isActive: row[7]
+ };
+ });
} catch (err) {
console.error('API 키 목록 조회 실패:', err);
throw err;
diff --git a/git_update.sh b/git_update.sh
index 371c9f2..f636e21 100644
--- a/git_update.sh
+++ b/git_update.sh
@@ -68,6 +68,8 @@ echo "=========================================="
echo "완료 시간: $(date)"
echo "✅ Git 최신 소스 업데이트가 완료되었습니다!"
+
+
# stash 목록이 있으면 알림
STASH_COUNT=$(git stash list | wc -l)
if [ $STASH_COUNT -gt 0 ]; then
diff --git a/middleware/logger.js b/middleware/logger.js
index e06f83a..d87ec6d 100644
--- a/middleware/logger.js
+++ b/middleware/logger.js
@@ -32,7 +32,7 @@ function apiLogger(req, res, next) {
// API 키 정보가 있으면 추가
if (req.apiKey) {
logData.apiKeyId = req.apiKey.id;
- logData.apiKeyName = req.apiKey.name;
+ logData.apiKeyName = req.apiKey.keyName;
}
// 비동기로 로그 저장 (응답 속도에 영향 주지 않도록)
diff --git a/public/css/style.css b/public/css/style.css
index a7f96ce..ce89e94 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -1455,3 +1455,45 @@ body.admin .stats-grid .admin-only {
margin-bottom: 1.5rem;
font-style: italic;
}
+
+/* 권한 배지 스타일 */
+.permissions-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+}
+
+.permission-badge {
+ display: inline-block;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ border-radius: 12px;
+ background-color: #e3f2fd;
+ color: #1565c0;
+ border: 1px solid #bbdefb;
+}
+
+.permission-badge:nth-child(1) {
+ background-color: #e8f5e8;
+ color: #2e7d32;
+ border-color: #c8e6c9;
+}
+
+.permission-badge:nth-child(2) {
+ background-color: #fff3e0;
+ color: #ef6c00;
+ border-color: #ffcc02;
+}
+
+.permission-badge:nth-child(3) {
+ background-color: #ffebee;
+ color: #c62828;
+ border-color: #ffcdd2;
+}
+
+.permission-none {
+ color: #6c757d;
+ font-style: italic;
+ font-size: 0.875rem;
+}
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
index 0975029..7c25b2f 100644
--- a/public/index.html
+++ b/public/index.html
@@ -926,22 +926,22 @@ curl -X DELETE http://localhost:5577/api/users/user005 \
-
빠른 테스트 예제
+
빠른 테스트 예제 - User CRUD
-
diff --git a/public/js/app.js b/public/js/app.js
index 04dca39..f899390 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -94,6 +94,16 @@ function showDashboard() {
// 초기 데이터 로드
loadDashboardData();
+
+ // API 키 사용 횟수 실시간 업데이트를 위한 주기적 새로고침 (30초마다)
+ if (window.apiKeyRefreshInterval) {
+ clearInterval(window.apiKeyRefreshInterval);
+ }
+ window.apiKeyRefreshInterval = setInterval(() => {
+ if (document.getElementById('apiKeysSection').style.display !== 'none') {
+ loadApiKeys();
+ }
+ }, 30000);
}
// 토큰 검증
@@ -202,6 +212,13 @@ function handleLogout() {
authToken = null;
currentUser = null;
document.body.classList.remove('admin');
+
+ // API 키 새로고침 인터벌 정리
+ if (window.apiKeyRefreshInterval) {
+ clearInterval(window.apiKeyRefreshInterval);
+ window.apiKeyRefreshInterval = null;
+ }
+
showLoginScreen();
showToast('로그아웃되었습니다', 'info');
}
@@ -311,7 +328,6 @@ async function loadApiKeys() {
if (response.ok) {
const data = await response.json();
- console.log('API 키 데이터:', data); // 디버깅용
displayApiKeys(data.apiKeys || []);
} else {
console.error('API 키 로드 실패:', response.status, response.statusText);
@@ -325,6 +341,17 @@ async function loadApiKeys() {
}
}
+// 권한 라벨 반환
+function getPermissionLabel(permission) {
+ const labels = {
+ 'read': '읽기',
+ 'write': '쓰기',
+ 'delete': '삭제',
+ 'admin': '관리자'
+ };
+ return labels[permission] || permission;
+}
+
// API 키 표시
function displayApiKeys(apiKeys) {
const tbody = document.querySelector('#apiKeysTable tbody');
@@ -366,7 +393,14 @@ function displayApiKeys(apiKeys) {
- ${Array.isArray(key.permissions) ? key.permissions.join(', ') : (key.permissions || '없음')} |
+
+
+ ${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()} |
@@ -1308,44 +1342,47 @@ function loadExample(type) {
const bodyGroup = document.getElementById('requestBodyGroup');
switch (type) {
- case 'getData':
- methodSelect.value = 'GET';
- urlInput.value = '/api/data';
- bodyGroup.style.display = 'none';
- break;
-
- case 'createData':
- methodSelect.value = 'POST';
- urlInput.value = '/api/data';
- bodyTextarea.value = JSON.stringify({
- name: "테스트 데이터",
- description: "API 테스터에서 생성한 테스트 데이터입니다.",
- dataValue: "테스트 값"
- }, null, 2);
- bodyGroup.style.display = 'block';
- break;
-
- case 'updateData':
- methodSelect.value = 'PUT';
- urlInput.value = '/api/data/1';
- bodyTextarea.value = JSON.stringify({
- name: "수정된 데이터",
- description: "API 테스터에서 수정한 데이터입니다.",
- dataValue: "수정된 값"
- }, null, 2);
- bodyGroup.style.display = 'block';
- break;
-
- case 'deleteData':
- methodSelect.value = 'DELETE';
- urlInput.value = '/api/data/1';
- bodyGroup.style.display = 'none';
- break;
-
- case 'getUserData':
+ 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;
}
}
diff --git a/routes/api.js b/routes/api.js
index db72290..7c5b748 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -10,7 +10,7 @@ const {
const { verifyApiKey, optionalAuth, requirePermission } = require('../middleware/auth');
// 모든 데이터 조회 (API 키 또는 JWT 토큰 필요)
-router.get('/data', optionalAuth, async (req, res) => {
+router.get('/data', verifyApiKey, async (req, res) => {
try {
const data = await getAllData();
res.json({
@@ -28,7 +28,7 @@ router.get('/data', optionalAuth, async (req, res) => {
});
// 특정 ID 데이터 조회 (API 키 또는 JWT 토큰 필요)
-router.get('/data/:id', optionalAuth, async (req, res) => {
+router.get('/data/:id', verifyApiKey, async (req, res) => {
try {
const { id } = req.params;
const data = await getDataById(id);
@@ -55,7 +55,7 @@ router.get('/data/:id', optionalAuth, async (req, res) => {
});
// 데이터 생성 (API 키 또는 JWT 토큰 필요)
-router.post('/data', optionalAuth, async (req, res) => {
+router.post('/data', verifyApiKey, async (req, res) => {
try {
const { name, description, dataValue } = req.body;
@@ -87,7 +87,7 @@ router.post('/data', optionalAuth, async (req, res) => {
});
// 데이터 업데이트 (API 키 또는 JWT 토큰 필요)
-router.put('/data/:id', optionalAuth, async (req, res) => {
+router.put('/data/:id', verifyApiKey, async (req, res) => {
try {
const { id } = req.params;
const { name, description, dataValue } = req.body;
@@ -127,7 +127,7 @@ router.put('/data/:id', optionalAuth, async (req, res) => {
});
// 데이터 삭제 (API 키 또는 JWT 토큰 필요)
-router.delete('/data/:id', optionalAuth, async (req, res) => {
+router.delete('/data/:id', verifyApiKey, async (req, res) => {
try {
const { id } = req.params;
const result = await deleteData(id);
diff --git a/routes/auth.js b/routes/auth.js
index 6d04c7a..c28c7c7 100644
--- a/routes/auth.js
+++ b/routes/auth.js
@@ -139,7 +139,9 @@ router.post('/api-keys', verifyToken, async (req, res) => {
});
}
- const result = await createApiKey(req.user.id, keyName, permissions || []);
+ // 기본 권한 설정 (권한이 지정되지 않은 경우)
+ const defaultPermissions = permissions && permissions.length > 0 ? permissions : ['read', 'write'];
+ const result = await createApiKey(req.user.id, keyName, defaultPermissions);
res.status(201).json({
success: true,
@@ -163,12 +165,11 @@ router.get('/api-keys', verifyToken, async (req, res) => {
const apiKeys = await getUserApiKeys(req.user.id);
// 전체 API 키를 반환 (사용자가 자신의 키를 관리할 수 있도록)
- // JSON 직렬화 안전한 데이터로 변환
const safeApiKeys = apiKeys.map(key => ({
id: key.id,
keyName: key.keyName,
apiKey: key.apiKey,
- permissions: Array.isArray(key.permissions) ? key.permissions : [],
+ permissions: key.permissions || [],
usageCount: key.usageCount || 0,
lastUsed: key.lastUsed ? new Date(key.lastUsed).toISOString() : null,
createdAt: key.createdAt ? new Date(key.createdAt).toISOString() : new Date().toISOString(),
diff --git a/routes/logs.js b/routes/logs.js
index 9a0e2a3..ef4777e 100644
--- a/routes/logs.js
+++ b/routes/logs.js
@@ -1,10 +1,10 @@
const express = require('express');
const router = express.Router();
const { getApiLogs, getApiLogStats } = require('../database/log-queries');
-const { verifyToken, requirePermission } = require('../middleware/auth');
+const { verifyToken, requireAdmin } = require('../middleware/auth');
// API 로그 목록 조회 (관리자만)
-router.get('/api-logs', verifyToken, requirePermission('admin'), async (req, res) => {
+router.get('/api-logs', verifyToken, requireAdmin, async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 50;
@@ -25,7 +25,7 @@ router.get('/api-logs', verifyToken, requirePermission('admin'), async (req, res
});
// API 로그 통계 조회 (관리자만)
-router.get('/api-logs/stats', verifyToken, requirePermission('admin'), async (req, res) => {
+router.get('/api-logs/stats', verifyToken, requireAdmin, async (req, res) => {
try {
const stats = await getApiLogStats();
res.json(stats);
diff --git a/server.js b/server.js
index 0fcf92d..a588ad0 100644
--- a/server.js
+++ b/server.js
@@ -22,17 +22,31 @@ app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
-// 정적 파일 서빙
-app.use(express.static(path.join(__dirname, 'public')));
+// 정적 파일 서빙 (개발 환경에서 캐시 비활성화)
+app.use(express.static(path.join(__dirname, 'public'), {
+ etag: false,
+ lastModified: false,
+ setHeaders: (res, path) => {
+ // 개발 환경에서 캐시 비활성화
+ if (process.env.NODE_ENV !== 'production') {
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
+ res.setHeader('Pragma', 'no-cache');
+ res.setHeader('Expires', '0');
+ }
+ }
+}));
-// API 로깅 미들웨어 (정적 파일과 헬스체크는 제외)
+// API 로깅 미들웨어 (정적 파일, 헬스체크, 로그 조회는 제외)
app.use(skipLogging([
'/favicon.ico',
'/api/health',
+ '/logs/api-logs',
+ '/logs/api-logs/stats',
/^\/css\//,
/^\/js\//,
/^\/images\//,
- /^\/fonts\//
+ /^\/fonts\//,
+ /^\/auth\// // 인증 관련 경로도 제외
]));
// 요청 로깅 미들웨어