시스템 모니터
This commit is contained in:
213
frontend/components/DashboardControl.vue
Normal file
213
frontend/components/DashboardControl.vue
Normal file
@@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<div class="control-panel">
|
||||
<div class="control-row">
|
||||
<span class="control-label">조회 간격:</span>
|
||||
<div class="interval-buttons">
|
||||
<button
|
||||
v-for="min in [1, 2, 3, 4, 5]"
|
||||
:key="min"
|
||||
:class="['interval-btn', { active: autoRefresh && interval === min }]"
|
||||
@click="selectInterval(min)"
|
||||
>
|
||||
{{ min }}분
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="control-divider"></div>
|
||||
|
||||
<button
|
||||
:class="['auto-refresh-btn', { active: autoRefresh }]"
|
||||
@click="toggleAutoRefresh"
|
||||
>
|
||||
{{ autoRefresh ? '⏸ 자동갱신 ON' : '▶ 자동갱신 OFF' }}
|
||||
</button>
|
||||
|
||||
<div class="last-fetch-info">
|
||||
마지막 조회: <span class="last-fetch-time">{{ relativeTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-row">
|
||||
<span class="control-label">특정시간:</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
v-model="selectedDatetime"
|
||||
class="datetime-input"
|
||||
:disabled="autoRefresh || fetchState !== 'idle'"
|
||||
:class="{ disabled: autoRefresh || fetchState !== 'idle' }"
|
||||
/>
|
||||
<button
|
||||
class="refresh-btn"
|
||||
:disabled="autoRefresh || fetchState !== 'idle'"
|
||||
:class="{
|
||||
disabled: autoRefresh,
|
||||
loading: fetchState === 'loading',
|
||||
success: fetchState === 'success'
|
||||
}"
|
||||
@click="doRefresh"
|
||||
>
|
||||
<span v-if="fetchState === 'loading'" class="loading-spinner"></span>
|
||||
{{ buttonText }}
|
||||
</button>
|
||||
<span v-if="autoRefresh" class="hint-text">자동갱신 OFF 시 특정시간 조회 가능</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
interval: number
|
||||
autoRefresh: boolean
|
||||
lastFetchTime: Date | null
|
||||
fetchState: 'idle' | 'loading' | 'success'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:interval', value: number): void
|
||||
(e: 'update:autoRefresh', value: boolean): void
|
||||
(e: 'fetchAt', datetime: string): void
|
||||
(e: 'refresh'): void
|
||||
}>()
|
||||
|
||||
// props destructure for template
|
||||
const { interval, autoRefresh, fetchState } = toRefs(props)
|
||||
|
||||
const selectedDatetime = ref('')
|
||||
const relativeTime = ref('-')
|
||||
|
||||
const buttonText = computed(() => {
|
||||
switch (props.fetchState) {
|
||||
case 'loading': return '조회 중...'
|
||||
case 'success': return '✓ 완료'
|
||||
default: return '조회'
|
||||
}
|
||||
})
|
||||
|
||||
function getCurrentDatetimeLocal(): string {
|
||||
const now = new Date()
|
||||
const y = now.getFullYear()
|
||||
const m = String(now.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(now.getDate()).padStart(2, '0')
|
||||
const h = String(now.getHours()).padStart(2, '0')
|
||||
const min = String(now.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}T${h}:${min}`
|
||||
}
|
||||
|
||||
function getRelativeTime(date: Date | null): string {
|
||||
if (!date) return '-'
|
||||
|
||||
const now = new Date()
|
||||
const diff = Math.floor((now.getTime() - date.getTime()) / 1000)
|
||||
|
||||
if (diff < 5) return '방금 전'
|
||||
if (diff < 60) return `${diff}초 전`
|
||||
if (diff < 3600) return `${Math.floor(diff / 60)}분 전`
|
||||
if (diff < 86400) return `${Math.floor(diff / 3600)}시간 전`
|
||||
return `${Math.floor(diff / 86400)}일 전`
|
||||
}
|
||||
|
||||
function updateRelativeTime() {
|
||||
relativeTime.value = getRelativeTime(props.lastFetchTime)
|
||||
}
|
||||
|
||||
let timeTimer: ReturnType<typeof window.setInterval> | null = null
|
||||
|
||||
onMounted(() => {
|
||||
updateRelativeTime()
|
||||
timeTimer = window.setInterval(updateRelativeTime, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeTimer) {
|
||||
window.clearInterval(timeTimer)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => props.lastFetchTime, () => {
|
||||
updateRelativeTime()
|
||||
})
|
||||
|
||||
// 조회간격 선택 → 자동갱신 ON + 특정시간 초기화
|
||||
function selectInterval(min: number) {
|
||||
selectedDatetime.value = ''
|
||||
emit('update:interval', min)
|
||||
if (!props.autoRefresh) {
|
||||
emit('update:autoRefresh', true)
|
||||
}
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
function toggleAutoRefresh() {
|
||||
const newValue = !props.autoRefresh
|
||||
emit('update:autoRefresh', newValue)
|
||||
|
||||
if (newValue) {
|
||||
// 자동갱신 ON 시 특정시간 초기화 + 즉시 조회
|
||||
selectedDatetime.value = ''
|
||||
emit('refresh')
|
||||
} else {
|
||||
// 자동갱신 OFF 시 현재시간을 기본값으로 설정
|
||||
selectedDatetime.value = getCurrentDatetimeLocal()
|
||||
}
|
||||
}
|
||||
|
||||
function doRefresh() {
|
||||
// 자동갱신 OFF일 때만 동작
|
||||
if (props.autoRefresh || props.fetchState !== 'idle') return
|
||||
|
||||
// 특정시간 입력된 경우 → 해당 시간 조회
|
||||
if (selectedDatetime.value) {
|
||||
emit('fetchAt', selectedDatetime.value.replace('T', ' '))
|
||||
} else {
|
||||
// 특정시간 미입력 → 현재 시점 조회
|
||||
emit('refresh')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.datetime-input.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn.loading {
|
||||
cursor: wait;
|
||||
background: var(--btn-primary-bg);
|
||||
}
|
||||
|
||||
.refresh-btn.success {
|
||||
background: var(--success-border);
|
||||
border-color: var(--success-border);
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #fff;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
font-style: italic;
|
||||
margin-left: 8px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user