diff --git a/backend/utils/server-scheduler.ts b/backend/utils/server-scheduler.ts index b1e026d..ba3e0a5 100644 --- a/backend/utils/server-scheduler.ts +++ b/backend/utils/server-scheduler.ts @@ -87,8 +87,8 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuChange = prevCpuAvg > 1 ? ((currCpuAvg - prevCpuAvg) / prevCpuAvg) * 100 : currCpuAvg - prevCpuAvg const memChange = prevMemAvg > 1 ? ((currMemAvg - prevMemAvg) / prevMemAvg) * 100 : currMemAvg - prevMemAvg - // CPU 단기 변화율 체크 - if (Math.abs(cpuChange) >= SHORT_TERM_THRESHOLD) { + // CPU 단기 변화율 체크 (증가만 감지) + if (cpuChange >= SHORT_TERM_THRESHOLD) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'short-term' AND metric = 'CPU' @@ -97,20 +97,19 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(cpuChange) >= 100 ? 'danger' : 'warning' - const direction = cpuChange >= 0 ? '증가' : '감소' - const message = `CPU ${direction} 감지 (${prevCpuAvg.toFixed(1)}% → ${currCpuAvg.toFixed(1)}%)` + const level = cpuChange >= 100 ? 'danger' : 'warning' + const message = `CPU 급증 감지 (${prevCpuAvg.toFixed(1)}% → ${currCpuAvg.toFixed(1)}%)` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) VALUES ($1, $2, 'short-term', 'CPU', $3, $4, $5, $6) `, [targetId, serverName, level, currCpuAvg, cpuChange, message]) - console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: CPU ${cpuChange.toFixed(1)}% (${level})`) + console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: CPU +${cpuChange.toFixed(1)}% (${level})`) } } - // Memory 단기 변화율 체크 - if (Math.abs(memChange) >= SHORT_TERM_THRESHOLD) { + // Memory 단기 변화율 체크 (증가만 감지) + if (memChange >= SHORT_TERM_THRESHOLD) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'short-term' AND metric = 'Memory' @@ -119,15 +118,14 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(memChange) >= 100 ? 'danger' : 'warning' - const direction = memChange >= 0 ? '증가' : '감소' - const message = `Memory ${direction} 감지 (${prevMemAvg.toFixed(1)}% → ${currMemAvg.toFixed(1)}%)` + const level = memChange >= 100 ? 'danger' : 'warning' + const message = `Memory 급증 감지 (${prevMemAvg.toFixed(1)}% → ${currMemAvg.toFixed(1)}%)` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) VALUES ($1, $2, 'short-term', 'Memory', $3, $4, $5, $6) `, [targetId, serverName, level, currMemAvg, memChange, message]) - console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: Memory ${memChange.toFixed(1)}% (${level})`) + console.log(`[${now}] 🚨 [${serverName}] 단기변화율 이상감지: Memory +${memChange.toFixed(1)}% (${level})`) } } } @@ -163,8 +161,8 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuZscore = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0 const memZscore = memStd > 0.1 ? (currMem - memAvg) / memStd : 0 - // CPU Z-Score 체크 - if (Math.abs(cpuZscore) >= WARNING_Z) { + // CPU Z-Score 체크 (높은 경우만 감지) + if (cpuZscore >= WARNING_Z) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'zscore' AND metric = 'CPU' @@ -173,9 +171,8 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(cpuZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = cpuZscore >= 0 ? '높음' : '낮음' - const message = `CPU 평균 대비 ${Math.abs(cpuZscore).toFixed(1)}σ ${direction} (평균: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` + const level = cpuZscore >= DANGER_Z ? 'danger' : 'warning' + const message = `CPU 평균 대비 ${cpuZscore.toFixed(1)}σ 높음 (평균: ${cpuAvg.toFixed(1)}%, 현재: ${currCpu.toFixed(1)}%)` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) @@ -185,8 +182,8 @@ async function detectAnomalies(targetId: number, serverName: string) { } } - // Memory Z-Score 체크 - if (Math.abs(memZscore) >= WARNING_Z) { + // Memory Z-Score 체크 (높은 경우만 감지) + if (memZscore >= WARNING_Z) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'zscore' AND metric = 'Memory' @@ -195,9 +192,8 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(memZscore) >= DANGER_Z ? 'danger' : 'warning' - const direction = memZscore >= 0 ? '높음' : '낮음' - const message = `Memory 평균 대비 ${Math.abs(memZscore).toFixed(1)}σ ${direction} (평균: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` + const level = memZscore >= DANGER_Z ? 'danger' : 'warning' + const message = `Memory 평균 대비 ${memZscore.toFixed(1)}σ 높음 (평균: ${memAvg.toFixed(1)}%, 현재: ${currMem.toFixed(1)}%)` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) @@ -253,7 +249,8 @@ async function detectAnomalies(targetId: number, serverName: string) { const cpuDeviation = cpuStd > 0.1 ? (currCpu - cpuAvg) / cpuStd : 0 const memDeviation = memStd > 0.1 ? (currMem - memAvg) / memStd : 0 - if (Math.abs(cpuDeviation) >= DEVIATION_THRESHOLD) { + // CPU 베이스라인 체크 (높은 경우만 감지) + if (cpuDeviation >= DEVIATION_THRESHOLD) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'baseline' AND metric = 'CPU' @@ -262,10 +259,9 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(cpuDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = cpuDeviation >= 0 ? '높음' : '낮음' + const level = cpuDeviation >= 3.0 ? 'danger' : 'warning' const dayLabel = isWeekend ? '주말' : '평일' - const message = `CPU ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(cpuDeviation).toFixed(1)}σ ${direction}` + const message = `CPU ${dayLabel} ${currentHour}시 베이스라인 대비 ${cpuDeviation.toFixed(1)}σ 높음` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) @@ -275,7 +271,8 @@ async function detectAnomalies(targetId: number, serverName: string) { } } - if (Math.abs(memDeviation) >= DEVIATION_THRESHOLD) { + // Memory 베이스라인 체크 (높은 경우만 감지) + if (memDeviation >= DEVIATION_THRESHOLD) { const recentExists = await queryOne(` SELECT 1 FROM anomaly_logs WHERE target_id = $1 AND detect_type = 'baseline' AND metric = 'Memory' @@ -284,10 +281,9 @@ async function detectAnomalies(targetId: number, serverName: string) { `, [targetId]) if (!recentExists) { - const level = Math.abs(memDeviation) >= 3.0 ? 'danger' : 'warning' - const direction = memDeviation >= 0 ? '높음' : '낮음' + const level = memDeviation >= 3.0 ? 'danger' : 'warning' const dayLabel = isWeekend ? '주말' : '평일' - const message = `Memory ${dayLabel} ${currentHour}시 베이스라인 대비 ${Math.abs(memDeviation).toFixed(1)}σ ${direction}` + const message = `Memory ${dayLabel} ${currentHour}시 베이스라인 대비 ${memDeviation.toFixed(1)}σ 높음` await execute(` INSERT INTO anomaly_logs (target_id, server_name, detect_type, metric, level, current_value, threshold_value, message) diff --git a/frontend/anomaly/baseline.vue b/frontend/anomaly/baseline.vue index 9df2189..b5490bb 100644 --- a/frontend/anomaly/baseline.vue +++ b/frontend/anomaly/baseline.vue @@ -263,8 +263,8 @@ onUnmounted(() => { if (chart) chart.destroy() }) .pros-cons { display: flex; gap: 16px; } .pros, .cons { flex: 1; padding: 12px; border-radius: 8px; } -.pros { background: #f0fdf4; border: 1px solid #86efac; } -.cons { background: #fef2f2; border: 1px solid #fca5a5; } +.pros { background: var(--container-normal-bg, #f0fdf4); border: 1px solid var(--container-normal-border, #86efac); } +.cons { background: var(--container-danger-bg, #fef2f2); border: 1px solid var(--container-danger-border, #fca5a5); } .pros h4, .cons h4 { margin: 0 0 8px 0; font-size: 13px; } .pros ul, .cons ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--text-secondary); } @@ -304,14 +304,14 @@ onUnmounted(() => { if (chart) chart.destroy() }) .log-list { max-height: 300px; overflow-y: auto; } .log-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; } -.log-item.warning { background: #fefce8; } -.log-item.danger { background: #fef2f2; } +.log-item.warning { background: var(--log-warning-bg, #fefce8); } +.log-item.danger { background: var(--log-danger-bg, #fef2f2); } .log-time { color: var(--text-muted); font-family: monospace; min-width: 70px; } .log-level { font-size: 14px; } .log-server { font-weight: 600; color: var(--text-primary); min-width: 80px; } .log-metric { color: var(--text-secondary); min-width: 50px; } .log-value { font-weight: 600; min-width: 50px; } -.log-item.warning .log-value { color: #ca8a04; } -.log-item.danger .log-value { color: #dc2626; } +.log-item.warning .log-value { color: var(--log-warning-text, #ca8a04); } +.log-item.danger .log-value { color: var(--log-danger-text, #dc2626); } .log-msg { color: var(--text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/frontend/anomaly/short-term.vue b/frontend/anomaly/short-term.vue index f96a4ae..0ad9a5c 100644 --- a/frontend/anomaly/short-term.vue +++ b/frontend/anomaly/short-term.vue @@ -293,8 +293,8 @@ onUnmounted(() => { .pros-cons { display: flex; gap: 16px; } .pros, .cons { flex: 1; padding: 12px; border-radius: 8px; } -.pros { background: #f0fdf4; border: 1px solid #86efac; } -.cons { background: #fef2f2; border: 1px solid #fca5a5; } +.pros { background: var(--container-normal-bg, #f0fdf4); border: 1px solid var(--container-normal-border, #86efac); } +.cons { background: var(--container-danger-bg, #fef2f2); border: 1px solid var(--container-danger-border, #fca5a5); } .pros h4, .cons h4 { margin: 0 0 8px 0; font-size: 13px; } .pros ul, .cons ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--text-secondary); } .pros li, .cons li { margin-bottom: 2px; } @@ -338,14 +338,14 @@ onUnmounted(() => { .log-list { max-height: 300px; overflow-y: auto; } .log-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; } -.log-item.warning { background: #fefce8; } -.log-item.danger { background: #fef2f2; } +.log-item.warning { background: var(--log-warning-bg, #fefce8); } +.log-item.danger { background: var(--log-danger-bg, #fef2f2); } .log-time { color: var(--text-muted); font-family: monospace; min-width: 70px; } .log-level { font-size: 14px; } .log-server { font-weight: 600; color: var(--text-primary); min-width: 80px; } .log-metric { color: var(--text-secondary); min-width: 50px; } .log-value { font-weight: 600; min-width: 50px; } -.log-item.warning .log-value { color: #ca8a04; } -.log-item.danger .log-value { color: #dc2626; } +.log-item.warning .log-value { color: var(--log-warning-text, #ca8a04); } +.log-item.danger .log-value { color: var(--log-danger-text, #dc2626); } .log-msg { color: var(--text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/frontend/anomaly/trend.vue b/frontend/anomaly/trend.vue index f4705d0..810479d 100644 --- a/frontend/anomaly/trend.vue +++ b/frontend/anomaly/trend.vue @@ -263,8 +263,8 @@ onUnmounted(() => { if (chart) chart.destroy() }) .pros-cons { display: flex; gap: 16px; } .pros, .cons { flex: 1; padding: 12px; border-radius: 8px; } -.pros { background: #f0fdf4; border: 1px solid #86efac; } -.cons { background: #fef2f2; border: 1px solid #fca5a5; } +.pros { background: var(--container-normal-bg, #f0fdf4); border: 1px solid var(--container-normal-border, #86efac); } +.cons { background: var(--container-danger-bg, #fef2f2); border: 1px solid var(--container-danger-border, #fca5a5); } .pros h4, .cons h4 { margin: 0 0 8px 0; font-size: 13px; } .pros ul, .cons ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--text-secondary); } @@ -305,14 +305,14 @@ onUnmounted(() => { if (chart) chart.destroy() }) .log-list { max-height: 300px; overflow-y: auto; } .log-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; } -.log-item.warning { background: #fefce8; } -.log-item.danger { background: #fef2f2; } +.log-item.warning { background: var(--log-warning-bg, #fefce8); } +.log-item.danger { background: var(--log-danger-bg, #fef2f2); } .log-time { color: var(--text-muted); font-family: monospace; min-width: 70px; } .log-level { font-size: 14px; } .log-server { font-weight: 600; color: var(--text-primary); min-width: 80px; } .log-metric { color: var(--text-secondary); min-width: 50px; } .log-value { font-weight: 600; min-width: 70px; } -.log-item.warning .log-value { color: #ca8a04; } -.log-item.danger .log-value { color: #dc2626; } +.log-item.warning .log-value { color: var(--log-warning-text, #ca8a04); } +.log-item.danger .log-value { color: var(--log-danger-text, #dc2626); } .log-msg { color: var(--text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/frontend/anomaly/zscore.vue b/frontend/anomaly/zscore.vue index c0d7b44..9be0f83 100644 --- a/frontend/anomaly/zscore.vue +++ b/frontend/anomaly/zscore.vue @@ -258,8 +258,8 @@ onUnmounted(() => { if (chart) chart.destroy() }) .pros-cons { display: flex; gap: 16px; } .pros, .cons { flex: 1; padding: 12px; border-radius: 8px; } -.pros { background: #f0fdf4; border: 1px solid #86efac; } -.cons { background: #fef2f2; border: 1px solid #fca5a5; } +.pros { background: var(--container-normal-bg, #f0fdf4); border: 1px solid var(--container-normal-border, #86efac); } +.cons { background: var(--container-danger-bg, #fef2f2); border: 1px solid var(--container-danger-border, #fca5a5); } .pros h4, .cons h4 { margin: 0 0 8px 0; font-size: 13px; } .pros ul, .cons ul { margin: 0; padding-left: 18px; font-size: 12px; color: var(--text-secondary); } @@ -299,14 +299,14 @@ onUnmounted(() => { if (chart) chart.destroy() }) .log-list { max-height: 300px; overflow-y: auto; } .log-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 6px; margin-bottom: 4px; font-size: 12px; } -.log-item.warning { background: #fefce8; } -.log-item.danger { background: #fef2f2; } +.log-item.warning { background: var(--log-warning-bg, #fefce8); } +.log-item.danger { background: var(--log-danger-bg, #fef2f2); } .log-time { color: var(--text-muted); font-family: monospace; min-width: 70px; } .log-level { font-size: 14px; } .log-server { font-weight: 600; color: var(--text-primary); min-width: 80px; } .log-metric { color: var(--text-secondary); min-width: 50px; } .log-value { font-weight: 600; min-width: 60px; } -.log-item.warning .log-value { color: #ca8a04; } -.log-item.danger .log-value { color: #dc2626; } +.log-item.warning .log-value { color: var(--log-warning-text, #ca8a04); } +.log-item.danger .log-value { color: var(--log-danger-text, #dc2626); } .log-msg { color: var(--text-muted); flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index 3147f97..d46d57b 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -73,6 +73,28 @@ --fail-border: #f87171; --fail-text: #fca5a5; + /* 컨테이너 카드 색상 */ + --container-normal-bg: #1a3d1a; + --container-normal-border: #2d5a2d; + --container-warning-bg: #3d3a1a; + --container-warning-border: #5a5a2d; + --container-critical-bg: #3d2a1a; + --container-critical-border: #5a3d2d; + --container-danger-bg: #3d1a1a; + --container-danger-border: #5a2d2d; + + /* 로그 색상 */ + --log-warning-bg: #3d3a1a; + --log-warning-text: #fbbf24; + --log-danger-bg: #3d1a1a; + --log-danger-text: #f87171; + + /* 컨테이너 상태 텍스트 */ + --container-status-running: #86efac; + --container-status-exited: #fca5a5; + --container-status-paused: #fcd34d; + --container-status-restarting: #fdba74; + /* 버튼 */ --btn-active-bg: #555; --btn-active-border: #666; diff --git a/frontend/components/DashboardControl.vue b/frontend/components/DashboardControl.vue index bd1979c..5f6cbc2 100644 --- a/frontend/components/DashboardControl.vue +++ b/frontend/components/DashboardControl.vue @@ -22,12 +22,8 @@ {{ autoRefresh ? '⏸ 자동갱신 ON' : '▶ 자동갱신 OFF' }} -