241 lines
11 KiB
Vue
241 lines
11 KiB
Vue
<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')
|
||
if (data) {
|
||
thresholds.value = data as typeof defaultThresholds
|
||
}
|
||
} 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>
|