diff --git a/frontend/src/components/DashboardPage.tsx b/frontend/src/components/DashboardPage.tsx index a1456e1..88e095c 100644 --- a/frontend/src/components/DashboardPage.tsx +++ b/frontend/src/components/DashboardPage.tsx @@ -15,6 +15,7 @@ import DashboardGauge from './dashboard/DashboardGauge'; import DashboardSegmentBar from './dashboard/DashboardSegmentBar'; import DashboardSparkChart from './dashboard/DashboardSparkChart'; import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar'; +import DashboardSystemCard from './dashboard/DashboardSystemCard'; import '../styles/strategyEditorTheme.css'; import '../styles/dashboardCommand.css'; @@ -183,66 +184,30 @@ const DashboardPage: React.FC = ({ theme, onGoChart }) => {

시스템 상태

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

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

- 시스템 +
+ {/* 메모리 */} + {(() => { + const pct = mon.system?.osMemPct ?? mon.jvm.heapPct; + const used = mon.system?.osMemUsedMb ?? mon.jvm.heapUsedMb; + const total = mon.system?.osMemTotalMb ?? mon.jvm.heapMaxMb; + const fmt = (mb: number) => mb >= 1024 ? `${(mb / 1024).toFixed(1)}GB` : `${mb}MB`; + return ( + + ); + })()} + {/* 디스크 */} +
- {/* 메모리 */} - {(() => { - 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/components/dashboard/DashboardSystemCard.tsx b/frontend/src/components/dashboard/DashboardSystemCard.tsx new file mode 100644 index 0000000..1a6dfd4 --- /dev/null +++ b/frontend/src/components/dashboard/DashboardSystemCard.tsx @@ -0,0 +1,60 @@ +import React from 'react'; + +interface Props { + label: string; + pct: number; // 0–100 + usedLabel: string; // e.g. "5.8GB" + totalLabel: string; // e.g. "7.8GB" +} + +const BLOCKS = 12; + +function trafficLevel(pct: number): 'green' | 'yellow' | 'red' { + if (pct >= 90) return 'red'; + if (pct >= 70) return 'yellow'; + return 'green'; +} + +/** 리소스 카드 — 세로 블록 바 + 신호등 */ +export default function DashboardSystemCard({ label, pct, usedLabel, totalLabel }: Props) { + const clamped = Math.max(0, Math.min(100, pct)); + const filled = Math.round((clamped / 100) * BLOCKS); + const level = trafficLevel(clamped); + + return ( +
+ {label} + +
+ {/* 세로 블록 바 */} +
+ {Array.from({ length: BLOCKS }, (_, i) => { + const idx = BLOCKS - 1 - i; // 위→낮음, 아래→높음 + const on = idx < filled; + let tone = 'gc-sys-block--mid'; + if (idx >= BLOCKS - 3) tone = 'gc-sys-block--lo'; // 하위(초록) + else if (idx <= 2) tone = 'gc-sys-block--hi'; // 상위(빨강) + return ( + + ); + })} +
+ + {/* 신호등 */} +
+ + + +
+
+ +
+ {clamped.toFixed(0)}% + {usedLabel} / {totalLabel} +
+
+ ); +} diff --git a/frontend/src/styles/dashboardCommand.css b/frontend/src/styles/dashboardCommand.css index 8acdd71..c1ed7f4 100644 --- a/frontend/src/styles/dashboardCommand.css +++ b/frontend/src/styles/dashboardCommand.css @@ -326,32 +326,95 @@ margin-top: 2px; } -/* ── 시스템 상태 — 3열 신호등 (가로 배치) ── */ +/* ── 시스템 상태 — 2열 리소스 카드 (가로 배치) ── */ .gc-dash-sys-status { display: flex; flex-direction: row; - align-items: flex-start; - justify-content: space-around; - gap: 8px; - padding: 8px 4px; + gap: 10px; width: 100%; } -.gc-sys-tl-item { +/* ── DashboardSystemCard ── */ +.gc-sys-card { + flex: 1; display: flex; flex-direction: column; align-items: center; - gap: 4px; - flex: 1; + gap: 6px; + padding: 10px 8px; + border-radius: 10px; + border: 1px solid var(--se-border); + background: color-mix(in srgb, var(--se-bg) 60%, transparent); + min-width: 0; } -.gc-sys-tl-label { +.gc-sys-card-title { font-size: 0.65rem; - font-weight: 600; - color: var(--se-text-muted); - letter-spacing: 0.03em; + font-weight: 700; + letter-spacing: 0.06em; text-transform: uppercase; - margin-top: 2px; + color: var(--se-text-muted); +} + +.gc-sys-card-body { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; +} + +/* 세로 블록 바 */ +.gc-sys-bar-wrap { + display: flex; + flex-direction: column; + gap: 3px; +} + +.gc-sys-block { + width: 18px; + height: 8px; + border-radius: 2px; + background: color-mix(in srgb, var(--se-text-dim, #666) 18%, #111); + opacity: 0.28; +} + +.gc-sys-block--on { opacity: 1; } + +/* 하위 블록 (낮은 사용률 = 초록) */ +.gc-sys-block--lo.gc-sys-block--on { + background: #34d399; + box-shadow: 0 0 4px rgba(52,211,153,0.6); +} + +/* 중간 블록 (중간 사용률 = 노랑) */ +.gc-sys-block--mid.gc-sys-block--on { + background: #fbbf24; + box-shadow: 0 0 4px rgba(251,191,36,0.6); +} + +/* 상위 블록 (높은 사용률 = 빨강) */ +.gc-sys-block--hi.gc-sys-block--on { + background: #f87171; + box-shadow: 0 0 4px rgba(248,113,113,0.6); +} + +.gc-sys-card-foot { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.gc-sys-card-pct { + font-size: 0.95rem; + font-weight: 800; + color: var(--se-text); + line-height: 1; +} + +.gc-sys-card-detail { + font-size: 0.62rem; + color: var(--se-text-muted); } /* ── Indicator bars ── */