Files
system-monitor/frontend/index.vue
2025-12-28 17:56:09 +09:00

262 lines
7.2 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">
<DashboardControl
:interval="interval"
:auto-refresh="autoRefresh"
:last-fetch-time="lastFetchTime"
:fetch-state="fetchState"
@update:interval="updateInterval"
@update:auto-refresh="updateAutoRefresh"
@fetch-at="fetchAt"
@refresh="refresh"
/>
<div class="dashboard-layout">
<!-- 서버 현황 (좌측 90%) -->
<div class="server-section">
<ServerPortlet
v-if="serverDashboard"
:servers="serverDashboard.servers"
:summary="serverDashboard.summary"
@navigate="navigateTo"
/>
</div>
<!-- 네트워크 상태 (우측 10%) -->
<div class="network-section">
<NetworkPortlet
type="pubnet"
title="Public"
icon="🌐"
:status="pubnetStatus"
/>
<NetworkPortlet
type="privnet"
title="Private"
icon="🔒"
:status="privnetStatus"
/>
<LocationStatsPortlet
:locations="locationStats"
/>
</div>
</div>
</main>
</div>
<div class="connection-status" :class="{ connected: wsConnected }">
{{ wsConnected ? '🟢 연결됨' : '🔴 연결 끊김' }}
</div>
</div>
</template>
<script setup lang="ts">
const router = useRouter()
// 초기값 상수
const DEFAULT_INTERVAL = 1
const DEFAULT_AUTO_REFRESH = true
const MIN_LOADING_TIME = 500
const SUCCESS_DISPLAY_TIME = 800
// 상태
const interval = ref(DEFAULT_INTERVAL)
const autoRefresh = ref(DEFAULT_AUTO_REFRESH)
const wsConnected = ref(false)
const currentTime = ref('')
const lastFetchTime = ref<Date | null>(null)
const fetchState = ref<'idle' | 'loading' | 'success'>('idle')
// 로딩 타이밍 관리
let loadingStartTime = 0
// 데이터
const pubnetStatus = ref<any>(null)
const privnetStatus = ref<any>(null)
const serverDashboard = ref<any>(null)
const locationStats = ref<any[]>([])
// WebSocket
let ws: WebSocket | null = null
let timeInterval: ReturnType<typeof window.setInterval> | null = null
async function fetchLocationStats() {
try {
const data = await $fetch('/api/server/location-stats')
locationStats.value = data as any[]
} catch (err) {
console.error('Failed to fetch location stats:', err)
}
}
function formatTime(date: Date): string {
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
const s = String(date.getSeconds()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${min}:${s}`
}
function updateCurrentTime() {
currentTime.value = formatTime(new Date())
}
function sendMessage(msg: object) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('[WS] Sending:', JSON.stringify(msg))
ws.send(JSON.stringify(msg))
}
}
function handleDataReceived(data: any) {
lastFetchTime.value = new Date()
if (data.pubnet) {
pubnetStatus.value = data.pubnet.status
}
if (data.privnet) {
privnetStatus.value = data.privnet.status
}
}
function finishLoading() {
fetchState.value = 'success'
setTimeout(() => {
fetchState.value = 'idle'
}, SUCCESS_DISPLAY_TIME)
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsUrl = `${protocol}//${window.location.host}/_ws`
ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log('[WS] Connected')
wsConnected.value = true
ws!.send(JSON.stringify({ type: 'set_interval', interval: DEFAULT_INTERVAL }))
ws!.send(JSON.stringify({ type: 'set_auto_refresh', enabled: DEFAULT_AUTO_REFRESH }))
ws!.send(JSON.stringify({ type: 'refresh' }))
}
ws.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'status' || msg.type === 'historical') {
handleDataReceived(msg.data)
if (fetchState.value === 'loading') {
const elapsed = Date.now() - loadingStartTime
const remaining = Math.max(0, MIN_LOADING_TIME - elapsed)
setTimeout(() => {
finishLoading()
}, remaining)
}
}
if (msg.type === 'server') {
serverDashboard.value = msg.data
fetchLocationStats()
}
} catch (err) {
console.error('[WS] Parse error:', err)
fetchState.value = 'idle'
}
}
ws.onclose = () => {
console.log('[WS] Disconnected')
wsConnected.value = false
fetchState.value = 'idle'
setTimeout(() => {
if (autoRefresh.value) {
connectWebSocket()
}
}, 3000)
}
ws.onerror = (error) => {
console.error('[WS] Error:', error)
fetchState.value = 'idle'
}
}
function updateInterval(min: number) {
interval.value = min
sendMessage({ type: 'set_interval', interval: min })
}
function updateAutoRefresh(enabled: boolean) {
autoRefresh.value = enabled
sendMessage({ type: 'set_auto_refresh', enabled })
}
function refresh() {
sendMessage({ type: 'refresh' })
}
function fetchAt(datetime: string) {
fetchState.value = 'loading'
loadingStartTime = Date.now()
sendMessage({ type: 'fetch_at', datetime })
}
function navigateTo(path: string) {
router.push(path)
}
onMounted(() => {
connectWebSocket()
updateCurrentTime()
fetchLocationStats()
timeInterval = window.setInterval(updateCurrentTime, 1000)
})
onUnmounted(() => {
if (ws) {
ws.close()
}
if (timeInterval) {
window.clearInterval(timeInterval)
}
})
</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: 20px 24px; overflow-y: auto; }
.dashboard-layout { display: flex; gap: 16px; height: 100%; }
.server-section { flex: 9; min-width: 0; }
.network-section { flex: 1; min-width: 150px; max-width: 200px; display: flex; flex-direction: column; gap: 8px; }
.connection-status { position: fixed; bottom: 16px; right: 16px; padding: 8px 12px; background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 8px; font-size: 12px; color: var(--text-muted); }
.connection-status.connected { color: #16a34a; }
</style>