/** * 운영 현황 대시보드 — 멀티스레드 파이프라인·JVM·트래픽·모의/실거래 */ import React, { useEffect, useState, useCallback } from 'react'; import type { Theme } from '../types'; import { loadDashboardSummary, type DashboardSummaryDto, type ProcessMonitorDto, type SystemMonitorDto, } from '../utils/backendApi'; interface Props { theme: Theme; onGoChart?: (market: string) => void; } const fmtKrw = (n: number) => n >= 1e8 ? `${(n / 1e8).toFixed(2)}억` : n >= 1e4 ? `${(n / 1e4).toFixed(0)}만` : n.toLocaleString('ko-KR'); const STATUS_LABEL: Record = { healthy: '정상', degraded: '주의', down: '중단', disabled: '비활성', }; function statusClass(s: string): string { if (s === 'healthy') return 'dash-proc--ok'; if (s === 'degraded') return 'dash-proc--warn'; if (s === 'down') return 'dash-proc--down'; return 'dash-proc--off'; } function formatMetricValue(v: unknown): string { if (typeof v === 'boolean') return v ? 'Y' : 'N'; if (typeof v === 'number') { if (Number.isInteger(v)) return v.toLocaleString('ko-KR'); return v.toLocaleString('ko-KR', { maximumFractionDigits: 2 }); } return String(v ?? '—'); } function formatAgo(ms: number): string { if (!ms) return '—'; const sec = Math.floor((Date.now() - ms) / 1000); if (sec < 60) return `${sec}초 전`; if (sec < 3600) return `${Math.floor(sec / 60)}분 전`; return `${Math.floor(sec / 3600)}시간 전`; } const ProcessCard: React.FC<{ proc: ProcessMonitorDto }> = ({ proc }) => (
{proc.layer} {STATUS_LABEL[proc.status] ?? proc.status}

{proc.name}

); const DashboardPage: React.FC = ({ theme, onGoChart }) => { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const refresh = useCallback(() => { setLoading(true); loadDashboardSummary() .then(d => { setData(d); setError(null); }) .catch(e => setError((e as Error).message)) .finally(() => setLoading(false)); }, []); useEffect(() => { refresh(); const id = window.setInterval(refresh, 5_000); return () => window.clearInterval(id); }, [refresh]); const app = data?.app as Record | undefined; const mon: SystemMonitorDto | undefined = data?.systemMonitor; return (

운영 대시보드

{mon && ( 파이프라인 {mon.pipelineEnabled ? 'ON' : 'OFF'} {mon.sampledAtMs ? ` · ${formatAgo(mon.sampledAtMs)} 갱신` : ''} )}
{error &&

{error}

} {data && mon && (
{/* JVM · 트래픽 */}

JVM · 트래픽

Heap {mon.jvm.heapUsedMb} / {mon.jvm.heapMaxMb} MB ({mon.jvm.heapPct}%)
85 ? ' dash-jvm-fill--warn' : ''}`} style={{ width: `${Math.min(100, mon.jvm.heapPct)}%` }} />
  • WS 메시지/s{mon.traffic.wsMessagesPerSec ?? 0}
  • 틱 적재/s{mon.traffic.ticksEnqueuedPerSec ?? 0}
  • 틱 처리/s{mon.traffic.ticksProcessedPerSec ?? 0}
  • 주문 실행/s{mon.traffic.ordersExecutedPerSec ?? 0}
  • 스레드{mon.jvm.threadCount}
  • Non-Heap{mon.jvm.nonHeapUsedMb} MB
{/* Ta4j 메모리 */}

인메모리 캔들 (Ta4j)

  • 종목{mon.memory.markets}
  • 시리즈{mon.memory.series}
  • 총 봉 수{mon.memory.totalBars.toLocaleString('ko-KR')}
  • 시리즈 상한{mon.memory.maxBarsPerSeries}
{/* 이벤트 큐 */}

이벤트 큐

  • 대기{mon.queue.pending}
  • 용량{mon.queue.capacity?.toLocaleString('ko-KR') ?? '—'}
  • 적재율{Math.round((mon.queue.fillRatio ?? 0) * 1000) / 10}%
  • 드롭 0 ? 'warn' : ''}>{mon.queue.dropped}
{/* 멀티스레드 프로세스 */}

멀티스레드 프로세스

{mon.processes.map(p => ( ))}
{/* 스레드 목록 */}

관련 스레드

{mon.threads.length === 0 ? (

표시할 스레드 없음

) : ( {mon.threads.map(t => ( ))}
이름ID상태데몬
{t.name} {t.id} {t.state} {t.daemon ? 'Y' : 'N'}
)}
{/* 틱 정체 종목 */} {mon.staleMarkets.length > 0 && (

틱 정체 종목 ({mon.staleMarkets.length})

{mon.staleMarkets.slice(0, 12).map(s => ( ))}
종목마지막 틱 경과
{s.lastTickAgeSec}초
)}

매매 모드

  • 운영 모드{String(app?.tradingMode ?? '—')}
  • 실거래 자동{app?.liveAutoTradeEnabled ? 'ON' : 'OFF'}
  • 모의 자동{app?.paperAutoTradeEnabled ? 'ON' : 'OFF'}
  • 전략 체크{app?.liveStrategyCheck ? 'ON' : 'OFF'}

모니터링

  • 관심종목{data.watchlistCount}
  • 전략 감시{data.monitoredMarkets}
  • FCM {data.fcm.pushEnabled ? (data.fcm.available ? '활성' : '설정됨·서버 미연결') : 'OFF'}

모의투자

  • 현금{fmtKrw(data.paper.cashBalance)} KRW
  • 총자산{fmtKrw(data.paper.totalAsset)} KRW
  • 보유 종목{data.paper.positions?.length ?? 0}

실거래

  • KRW{fmtKrw(data.live.krwBalance)}
  • 보유{data.live.positions?.length ?? 0} 종목
  • 연동{data.live.configured ? 'OK' : '미설정'}

최근 시그널

{data.recentSignals.length === 0 ? (

최근 시그널 없음

) : ( {data.recentSignals.map(s => ( ))}
종목방향가격분봉시각
{s.signalType} {s.price?.toLocaleString('ko-KR')} {s.candleType} {s.createdAt?.slice(0, 19) ?? '—'}
)}
)}
); }; export default DashboardPage;