diff --git a/backend/src/main/java/com/goldenchart/trading/pipeline/PipelineMonitorService.java b/backend/src/main/java/com/goldenchart/trading/pipeline/PipelineMonitorService.java index 899b772..c389f48 100644 --- a/backend/src/main/java/com/goldenchart/trading/pipeline/PipelineMonitorService.java +++ b/backend/src/main/java/com/goldenchart/trading/pipeline/PipelineMonitorService.java @@ -5,6 +5,7 @@ import com.goldenchart.websocket.UpbitWebSocketClient; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import java.io.File; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.ThreadInfo; @@ -62,6 +63,7 @@ public class PipelineMonitorService { "gapBackfillCron", props.getGapBackfillCron() )); out.put("jvm", jvmMetrics()); + out.put("system", systemMetrics()); out.put("traffic", computeTrafficRates()); out.put("memory", ta4jStorage.storageMetrics()); out.put("queue", eventQueue.stats()); @@ -91,6 +93,40 @@ public class PipelineMonitorService { return m; } + private Map systemMetrics() { + Map m = new LinkedHashMap<>(); + // OS 물리 메모리 + try { + com.sun.management.OperatingSystemMXBean osMx = + (com.sun.management.OperatingSystemMXBean) ManagementFactory.getOperatingSystemMXBean(); + long totalPhys = osMx.getTotalMemorySize(); + long freePhys = osMx.getFreeMemorySize(); + double memPct = totalPhys > 0 ? (totalPhys - freePhys) * 100.0 / totalPhys : 0; + m.put("osMemUsedMb", roundMb(totalPhys - freePhys)); + m.put("osMemTotalMb", roundMb(totalPhys)); + m.put("osMemPct", Math.round(memPct * 10) / 10.0); + } catch (Exception e) { + m.put("osMemUsedMb", 0); + m.put("osMemTotalMb", 0); + m.put("osMemPct", 0.0); + } + // 디스크 (루트 파티션) + try { + File root = new File("/"); + long total = root.getTotalSpace(); + long free = root.getFreeSpace(); + double diskPct = total > 0 ? (total - free) * 100.0 / total : 0; + m.put("diskUsedGb", Math.round((total - free) / 1073741824.0 * 10) / 10.0); + m.put("diskTotalGb", Math.round(total / 1073741824.0 * 10) / 10.0); + m.put("diskPct", Math.round(diskPct * 10) / 10.0); + } catch (Exception e) { + m.put("diskUsedGb", 0.0); + m.put("diskTotalGb", 0.0); + m.put("diskPct", 0.0); + } + return m; + } + private Map computeTrafficRates() { long now = System.currentTimeMillis(); var q = eventQueue.stats(); diff --git a/frontend/src/components/DashboardPage.tsx b/frontend/src/components/DashboardPage.tsx index 2e603f8..e939557 100644 --- a/frontend/src/components/DashboardPage.tsx +++ b/frontend/src/components/DashboardPage.tsx @@ -182,15 +182,65 @@ const DashboardPage: React.FC = ({ theme, onGoChart }) => {

시스템 상태

-
-
- - - +
+ {/* 전체 시스템 */} +
+
+ + + +
+

+ {health === 'green' ? '정상 운영' : health === 'yellow' ? '주의 필요' : '점검 필요'} +

+ 시스템
-

- {health === 'green' ? '정상 운영' : health === 'yellow' ? '주의 필요' : '점검 필요'} -

+ {/* 메모리 */} + {(() => { + const memPct = mon.system?.osMemPct ?? mon.jvm.heapPct; + const memLevel: 'green' | 'yellow' | 'red' = + memPct >= 90 ? 'red' : memPct >= 70 ? 'yellow' : 'green'; + const memUsed = mon.system?.osMemUsedMb ?? mon.jvm.heapUsedMb; + const memTotal = mon.system?.osMemTotalMb ?? mon.jvm.heapMaxMb; + return ( +
+
+ + + +
+

{memPct.toFixed(0)}%

+

+ {memUsed >= 1024 ? `${(memUsed/1024).toFixed(1)}GB` : `${memUsed}MB`} + {' / '} + {memTotal >= 1024 ? `${(memTotal/1024).toFixed(1)}GB` : `${memTotal}MB`} +

+ 메모리 +
+ ); + })()} + {/* 디스크 */} + {(() => { + const diskPct = mon.system?.diskPct ?? 0; + const diskLevel: 'green' | 'yellow' | 'red' = + diskPct >= 90 ? 'red' : diskPct >= 70 ? 'yellow' : 'green'; + const diskUsed = mon.system?.diskUsedGb ?? 0; + const diskTotal = mon.system?.diskTotalGb ?? 0; + return ( +
+
+ + + +
+

{diskPct.toFixed(0)}%

+

+ {diskUsed.toFixed(1)}GB / {diskTotal.toFixed(1)}GB +

+ 디스크 +
+ ); + })()}
diff --git a/frontend/src/styles/dashboardCommand.css b/frontend/src/styles/dashboardCommand.css index 5089fda..4b9fcc9 100644 --- a/frontend/src/styles/dashboardCommand.css +++ b/frontend/src/styles/dashboardCommand.css @@ -321,6 +321,36 @@ font-size: 0.72rem; color: var(--se-text-muted); } +.gc-traffic-caption--sub { + font-size: 0.62rem; + margin-top: 2px; +} + +/* ── 시스템 상태 — 3열 신호등 ── */ +.gc-dash-sys-status { + display: flex; + align-items: flex-start; + justify-content: space-around; + gap: 8px; + padding: 8px 4px; +} + +.gc-sys-tl-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + flex: 1; +} + +.gc-sys-tl-label { + font-size: 0.65rem; + font-weight: 600; + color: var(--se-text-muted); + letter-spacing: 0.03em; + text-transform: uppercase; + margin-top: 2px; +} /* ── Indicator bars ── */ diff --git a/frontend/src/utils/backendApi.ts b/frontend/src/utils/backendApi.ts index 3f88cc6..23591fd 100644 --- a/frontend/src/utils/backendApi.ts +++ b/frontend/src/utils/backendApi.ts @@ -576,6 +576,14 @@ export interface SystemMonitorDto { }; traffic: Record; memory: { markets: number; series: number; totalBars: number; maxBarsPerSeries: number }; + system?: { + osMemUsedMb: number; + osMemTotalMb: number; + osMemPct: number; + diskUsedGb: number; + diskTotalGb: number; + diskPct: number; + }; queue: { pending: number; capacity: number; fillRatio: number; enqueued: number; dequeued: number; dropped: number }; workers: { processed: number; errors: number; active: boolean; workerThreads: number }; orders: {