Files
system-monitor/frontend/settings/thresholds.vue
2025-12-28 16:27:46 +09:00

252 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="app-layout">
<SidebarNav />
<div class="main-content">
<header class="main-header">
<h1 class="page-title"> 임계값 설정</h1>
<div class="header-info">
<span class="current-time">{{ currentTime }}</span>
<ThemeToggle />
</div>
</header>
<main class="main-body">
<div class="settings-container">
<!-- 서버 임계값 -->
<section class="threshold-section">
<h2 class="section-title">🖥 서버 임계값</h2>
<p class="section-desc">서버의 CPU, Memory, Disk 사용률에 대한 경고 기준을 설정합니다.</p>
<div class="threshold-table">
<div class="table-header">
<div class="col-metric">지표</div>
<div class="col-value">🟡 주의 (Warning)</div>
<div class="col-value">🟠 경고 (Critical)</div>
<div class="col-value">🔴 위험 (Danger)</div>
</div>
<div class="table-row" v-for="metric in serverMetrics" :key="metric.key">
<div class="col-metric">
<span class="metric-icon">{{ metric.icon }}</span>
<span>{{ metric.label }}</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.server[metric.key].warning" min="0" max="100" />
<span class="unit">%</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.server[metric.key].critical" min="0" max="100" />
<span class="unit">%</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.server[metric.key].danger" min="0" max="100" />
<span class="unit">%</span>
</div>
</div>
</div>
</section>
<!-- 컨테이너 임계값 -->
<section class="threshold-section">
<h2 class="section-title">🐳 컨테이너 임계값</h2>
<p class="section-desc">컨테이너의 CPU, Memory 사용률에 대한 경고 기준을 설정합니다.</p>
<div class="threshold-table">
<div class="table-header">
<div class="col-metric">지표</div>
<div class="col-value">🟡 주의 (Warning)</div>
<div class="col-value">🟠 경고 (Critical)</div>
<div class="col-value">🔴 위험 (Danger)</div>
</div>
<div class="table-row" v-for="metric in containerMetrics" :key="metric.key">
<div class="col-metric">
<span class="metric-icon">{{ metric.icon }}</span>
<span>{{ metric.label }}</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.container[metric.key].warning" min="0" max="100" />
<span class="unit">%</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.container[metric.key].critical" min="0" max="100" />
<span class="unit">%</span>
</div>
<div class="col-value">
<input type="number" v-model.number="thresholds.container[metric.key].danger" min="0" max="100" />
<span class="unit">%</span>
</div>
</div>
</div>
</section>
<!-- 레벨 설명 -->
<section class="level-guide">
<h3>📋 레벨 설명</h3>
<div class="level-items">
<div class="level-item normal">🟢 정상: 모든 지표가 주의 기준 미만</div>
<div class="level-item warning">🟡 주의: 하나 이상의 지표가 주의 기준 이상</div>
<div class="level-item critical">🟠 경고: 하나 이상의 지표가 경고 기준 이상</div>
<div class="level-item danger">🔴 위험: 하나 이상의 지표가 위험 기준 이상</div>
</div>
</section>
<!-- 저장 버튼 -->
<div class="actions">
<button class="btn-reset" @click="resetDefaults" :disabled="saving">기본값 복원</button>
<button class="btn-save" @click="saveThresholds" :disabled="saving">
{{ saving ? '저장 중...' : '저장' }}
</button>
</div>
<div v-if="message" :class="['message', messageType]">{{ message }}</div>
</div>
</main>
</div>
</div>
</template>
<script setup lang="ts">
const currentTime = ref('')
const saving = ref(false)
const message = ref('')
const messageType = ref<'success' | 'error'>('success')
const serverMetrics = [
{ key: 'cpu', label: 'CPU 사용률', icon: '💻' },
{ key: 'memory', label: 'Memory 사용률', icon: '🧠' },
{ key: 'disk', label: 'Disk 사용률', icon: '💾' }
]
const containerMetrics = [
{ key: 'cpu', label: 'CPU 사용률', icon: '💻' },
{ key: 'memory', label: 'Memory 사용률', icon: '🧠' }
]
const defaultThresholds = {
server: {
cpu: { warning: 70, critical: 85, danger: 95 },
memory: { warning: 80, critical: 90, danger: 95 },
disk: { warning: 80, critical: 90, danger: 95 }
},
container: {
cpu: { warning: 80, critical: 90, danger: 95 },
memory: { warning: 80, critical: 90, danger: 95 }
}
}
const thresholds = ref(JSON.parse(JSON.stringify(defaultThresholds)))
async function fetchThresholds() {
try {
const data = await $fetch('/api/settings/thresholds') as any
if (data) {
// 기본값과 병합 (API 응답이 불완전할 수 있음)
thresholds.value = {
server: {
cpu: { ...defaultThresholds.server.cpu, ...data.server?.cpu },
memory: { ...defaultThresholds.server.memory, ...data.server?.memory },
disk: { ...defaultThresholds.server.disk, ...data.server?.disk }
},
container: {
cpu: { ...defaultThresholds.container.cpu, ...data.container?.cpu },
memory: { ...defaultThresholds.container.memory, ...data.container?.memory }
}
}
}
} catch (err) {
console.error('Failed to fetch thresholds:', err)
}
}
async function saveThresholds() {
saving.value = true
message.value = ''
try {
await $fetch('/api/settings/thresholds', {
method: 'PUT',
body: thresholds.value
})
message.value = '✅ 저장되었습니다.'
messageType.value = 'success'
} catch (err: any) {
message.value = `❌ 저장 실패: ${err.data?.message || err.message}`
messageType.value = 'error'
} finally {
saving.value = false
setTimeout(() => { message.value = '' }, 3000)
}
}
function resetDefaults() {
thresholds.value = JSON.parse(JSON.stringify(defaultThresholds))
message.value = '기본값으로 복원되었습니다. 저장 버튼을 눌러 적용하세요.'
messageType.value = 'success'
setTimeout(() => { message.value = '' }, 3000)
}
onMounted(() => {
const updateTime = () => {
currentTime.value = new Date().toLocaleString('ko-KR', {
timeZone: 'Asia/Seoul',
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit'
})
}
updateTime()
setInterval(updateTime, 1000)
fetchThresholds()
})
</script>
<style scoped>
.app-layout { display: flex; height: 100vh; background: var(--bg-primary); }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.main-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; border-bottom: 1px solid var(--border-color); background: var(--bg-secondary); }
.page-title { margin: 0; font-size: 20px; font-weight: 600; color: var(--text-primary); }
.header-info { display: flex; align-items: center; gap: 16px; }
.current-time { font-size: 14px; color: var(--text-muted); font-family: monospace; }
.main-body { flex: 1; padding: 24px; overflow-y: auto; }
.settings-container { }
.threshold-section { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
.section-title { margin: 0 0 8px 0; font-size: 18px; font-weight: 600; color: var(--text-primary); }
.section-desc { margin: 0 0 16px 0; font-size: 13px; color: var(--text-muted); }
.threshold-table { border: 1px solid var(--border-color); border-radius: 8px; overflow: hidden; }
.table-header { display: grid; grid-template-columns: 180px repeat(3, 1fr); background: var(--bg-tertiary, #f1f5f9); border-bottom: 1px solid var(--border-color); }
.table-header > div { padding: 12px 16px; font-size: 13px; font-weight: 600; color: var(--text-primary); }
.table-row { display: grid; grid-template-columns: 180px repeat(3, 1fr); border-bottom: 1px solid var(--border-color); }
.table-row:last-child { border-bottom: none; }
.table-row:hover { background: var(--bg-tertiary, #f8fafc); }
.col-metric { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 14px; color: var(--text-primary); }
.metric-icon { font-size: 16px; }
.col-value { display: flex; align-items: center; gap: 6px; padding: 10px 16px; }
.col-value input { width: 70px; padding: 8px 10px; border: 1px solid var(--border-color); border-radius: 6px; font-size: 14px; text-align: center; background: var(--bg-primary); color: var(--text-primary); }
.col-value input:focus { outline: none; border-color: var(--btn-primary-bg); }
.col-value .unit { font-size: 13px; color: var(--text-muted); }
.level-guide { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
.level-guide h3 { margin: 0 0 12px 0; font-size: 15px; font-weight: 600; color: var(--text-primary); }
.level-items { display: flex; flex-wrap: wrap; gap: 12px; }
.level-item { padding: 8px 14px; border-radius: 6px; font-size: 13px; }
.level-item.normal { background: #f0fdf4; color: #166534; border: 1px solid #86efac; }
.level-item.warning { background: #fefce8; color: #854d0e; border: 1px solid #fde047; }
.level-item.critical { background: #fff7ed; color: #c2410c; border: 1px solid #fdba74; }
.level-item.danger { background: #fef2f2; color: #991b1b; border: 1px solid #fca5a5; }
.actions { display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; }
.btn-reset { padding: 10px 20px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-primary); color: var(--text-primary); font-size: 14px; cursor: pointer; transition: all 0.2s; }
.btn-reset:hover:not(:disabled) { background: var(--bg-secondary); }
.btn-reset:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-save { padding: 10px 24px; border: none; border-radius: 8px; background: var(--btn-primary-bg); color: #fff; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-save:hover:not(:disabled) { opacity: 0.9; }
.btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
.message { margin-top: 16px; padding: 12px 16px; border-radius: 8px; font-size: 14px; text-align: center; }
.message.success { background: #f0fdf4; color: #166534; border: 1px solid #86efac; }
.message.error { background: #fef2f2; color: #991b1b; border: 1px solid #fca5a5; }
</style>