소스 수정

This commit is contained in:
2025-12-28 16:35:14 +09:00
parent 1fc6a9ccd9
commit c2733ca3da
2 changed files with 168 additions and 45 deletions

View File

@@ -6,54 +6,44 @@ export default defineEventHandler(async (event) => {
const type = queryParams.type as string || 'short-term'
const period = queryParams.period as string || '24h'
// 기간/간격 계산
let interval = '24 hours'
let groupFormat = 'YYYY-MM-DD HH24:00'
// 기간/간격 설정
const config = getPeriodConfig(period)
if (period === '1h') {
interval = '1 hour'
groupFormat = 'YYYY-MM-DD HH24:MI'
} else if (period === '6h') {
interval = '6 hours'
groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '12h') {
interval = '12 hours'
groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '24h') {
interval = '24 hours'
groupFormat = 'YYYY-MM-DD HH24:00'
} else if (period === '7d') {
interval = '7 days'
groupFormat = 'YYYY-MM-DD'
} else if (period === '30d') {
interval = '30 days'
groupFormat = 'YYYY-MM-DD'
}
// 시간 슬롯 생성 (연속된 X축)
const timeSlots = generateTimeSlots(config)
// 시간대별 집계 (anomaly_logs 테이블이 없으면 빈 배열 반환)
// DB에서 로그 조회
let rows: any[] = []
try {
rows = await query<any>(`
SELECT
to_char(detected_at, '${groupFormat}') as time_slot,
to_char(detected_at::timestamp, '${config.dbFormat}') as time_slot,
SUM(CASE WHEN level = 'warning' THEN 1 ELSE 0 END) as warning,
SUM(CASE WHEN level = 'danger' THEN 1 ELSE 0 END) as danger
FROM anomaly_logs
WHERE detect_type = $1
AND detected_at >= NOW() - INTERVAL '${interval}'
AND detected_at::timestamp >= NOW() - INTERVAL '${config.interval}'
GROUP BY time_slot
ORDER BY time_slot ASC
`, [type])
} catch (e) {
// 테이블이 없으면 빈 배열
rows = []
}
// 시간 포맷 변환
const data = rows.map(r => ({
time: formatTimeLabel(r.time_slot, period),
warning: Number(r.warning),
danger: Number(r.danger)
// 로그 데이터를 Map으로 변환
const logMap = new Map<string, { warning: number; danger: number }>()
for (const r of rows) {
logMap.set(r.time_slot, {
warning: Number(r.warning) || 0,
danger: Number(r.danger) || 0
})
}
// 전체 시간 슬롯에 데이터 매핑 (없으면 0)
const data = timeSlots.map(slot => ({
time: slot.label,
warning: logMap.get(slot.key)?.warning || 0,
danger: logMap.get(slot.key)?.danger || 0
}))
return {
@@ -64,17 +54,134 @@ export default defineEventHandler(async (event) => {
}
})
function formatTimeLabel(timeSlot: string, period: string): string {
if (!timeSlot) return ''
interface PeriodConfig {
interval: string
stepMinutes: number
dbFormat: string
labelFormat: (date: Date) => string
}
if (period === '7d' || period === '30d') {
const parts = timeSlot.split('-')
return `${parts[1]}/${parts[2]}`
function getPeriodConfig(period: string): PeriodConfig {
switch (period) {
case '1h':
return {
interval: '1 hour',
stepMinutes: 5,
dbFormat: 'YYYY-MM-DD HH24:MI',
labelFormat: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
case '6h':
return {
interval: '6 hours',
stepMinutes: 30,
dbFormat: 'YYYY-MM-DD HH24:MI',
labelFormat: (d) => `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
case '12h':
return {
interval: '12 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
case '24h':
return {
interval: '24 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
case '7d':
return {
interval: '7 days',
stepMinutes: 360, // 6시간
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getMonth()+1)}/${pad(d.getDate())} ${pad(d.getHours())}`
}
case '30d':
return {
interval: '30 days',
stepMinutes: 1440, // 1일
dbFormat: 'YYYY-MM-DD',
labelFormat: (d) => `${pad(d.getMonth()+1)}/${pad(d.getDate())}`
}
default:
return {
interval: '24 hours',
stepMinutes: 60,
dbFormat: 'YYYY-MM-DD HH24:00',
labelFormat: (d) => `${pad(d.getHours())}:00`
}
}
}
function generateTimeSlots(config: PeriodConfig): { key: string; label: string }[] {
const slots: { key: string; label: string }[] = []
const now = new Date()
// 현재 시간을 간격에 맞게 내림
const roundedNow = new Date(now)
if (config.stepMinutes >= 1440) {
// 일 단위: 오늘 00:00
roundedNow.setHours(0, 0, 0, 0)
} else if (config.stepMinutes >= 60) {
// 시간 단위: 현재 시간 00분
roundedNow.setMinutes(0, 0, 0)
} else {
const parts = timeSlot.split(' ')
if (parts.length === 2) {
return parts[1].substring(0, 5)
// 분 단위: 가장 가까운 간격
const mins = roundedNow.getMinutes()
const rounded = Math.floor(mins / config.stepMinutes) * config.stepMinutes
roundedNow.setMinutes(rounded, 0, 0)
}
return timeSlot.substring(11, 16)
// interval을 밀리초로 변환
const intervalMs = parseInterval(config.interval)
const startTime = new Date(roundedNow.getTime() - intervalMs)
// 시작부터 현재까지 슬롯 생성
const stepMs = config.stepMinutes * 60 * 1000
let current = new Date(startTime)
while (current <= roundedNow) {
const key = formatDbKey(current, config.dbFormat)
const label = config.labelFormat(current)
slots.push({ key, label })
current = new Date(current.getTime() + stepMs)
}
return slots
}
function parseInterval(interval: string): number {
const match = interval.match(/(\d+)\s*(hour|hours|day|days)/)
if (!match) return 24 * 60 * 60 * 1000
const num = parseInt(match[1])
const unit = match[2]
if (unit.startsWith('day')) {
return num * 24 * 60 * 60 * 1000
} else {
return num * 60 * 60 * 1000
}
}
function formatDbKey(date: Date, format: string): string {
const y = date.getFullYear()
const m = pad(date.getMonth() + 1)
const d = pad(date.getDate())
const h = pad(date.getHours())
const mi = pad(date.getMinutes())
if (format === 'YYYY-MM-DD') {
return `${y}-${m}-${d}`
} else if (format === 'YYYY-MM-DD HH24:00') {
return `${y}-${m}-${d} ${h}:00`
} else {
return `${y}-${m}-${d} ${h}:${mi}`
}
}
function pad(n: number): string {
return n.toString().padStart(2, '0')
}

View File

@@ -288,8 +288,24 @@ async function getServerDashboard() {
})
}
// 서버 정렬: 이름
serverStatuses.sort((a, b) => a.server_name.localeCompare(b.server_name))
// 서버 정렬: 장애 우선 → 컨테이너 많은 순 → 이름순
serverStatuses.sort((a, b) => {
// 1. 장애 여부 (서버 장애 또는 컨테이너에 장애)
const aHasIssue = a.level !== 'normal' ||
(a.container_summary.stopped > 0 || a.container_summary.critical > 0 || a.container_summary.warning > 0)
const bHasIssue = b.level !== 'normal' ||
(b.container_summary.stopped > 0 || b.container_summary.critical > 0 || b.container_summary.warning > 0)
if (aHasIssue !== bHasIssue) return aHasIssue ? -1 : 1
// 2. 컨테이너 수 (많은 순)
const aContainers = a.container_summary.total || 0
const bContainers = b.container_summary.total || 0
if (aContainers !== bContainers) return bContainers - aContainers
// 3. 서버 이름순
return a.server_name.localeCompare(b.server_name)
})
return {
summary: { servers: summaryServers, containers: summaryContainers },