대시보드 시스템 상태 카드 — 세로 블록바+신호등 2열 레이아웃으로 재설계
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import DashboardGauge from './dashboard/DashboardGauge';
|
|||||||
import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
|
import DashboardSegmentBar from './dashboard/DashboardSegmentBar';
|
||||||
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
import DashboardSparkChart from './dashboard/DashboardSparkChart';
|
||||||
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
import DashboardIndicatorBar from './dashboard/DashboardIndicatorBar';
|
||||||
|
import DashboardSystemCard from './dashboard/DashboardSystemCard';
|
||||||
import '../styles/strategyEditorTheme.css';
|
import '../styles/strategyEditorTheme.css';
|
||||||
import '../styles/dashboardCommand.css';
|
import '../styles/dashboardCommand.css';
|
||||||
|
|
||||||
@@ -184,64 +185,28 @@ const DashboardPage: React.FC<Props> = ({ theme, onGoChart }) => {
|
|||||||
<h2 className="gc-dash-card-title">시스템 상태</h2>
|
<h2 className="gc-dash-card-title">시스템 상태</h2>
|
||||||
<div className="gc-dash-card-inner">
|
<div className="gc-dash-card-inner">
|
||||||
<div className="gc-dash-sys-status">
|
<div className="gc-dash-sys-status">
|
||||||
{/* 전체 시스템 */}
|
|
||||||
<div className="gc-sys-tl-item">
|
|
||||||
<div className={`gc-traffic-light gc-traffic-light--${health}`} aria-label="시스템 상태">
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
|
||||||
</div>
|
|
||||||
<p className="gc-traffic-caption">
|
|
||||||
{health === 'green' ? '정상 운영' : health === 'yellow' ? '주의 필요' : '점검 필요'}
|
|
||||||
</p>
|
|
||||||
<span className="gc-sys-tl-label">시스템</span>
|
|
||||||
</div>
|
|
||||||
{/* 메모리 */}
|
{/* 메모리 */}
|
||||||
{(() => {
|
{(() => {
|
||||||
const memPct = mon.system?.osMemPct ?? mon.jvm.heapPct;
|
const pct = mon.system?.osMemPct ?? mon.jvm.heapPct;
|
||||||
const memLevel: 'green' | 'yellow' | 'red' =
|
const used = mon.system?.osMemUsedMb ?? mon.jvm.heapUsedMb;
|
||||||
memPct >= 90 ? 'red' : memPct >= 70 ? 'yellow' : 'green';
|
const total = mon.system?.osMemTotalMb ?? mon.jvm.heapMaxMb;
|
||||||
const memUsed = mon.system?.osMemUsedMb ?? mon.jvm.heapUsedMb;
|
const fmt = (mb: number) => mb >= 1024 ? `${(mb / 1024).toFixed(1)}GB` : `${mb}MB`;
|
||||||
const memTotal = mon.system?.osMemTotalMb ?? mon.jvm.heapMaxMb;
|
|
||||||
return (
|
return (
|
||||||
<div className="gc-sys-tl-item">
|
<DashboardSystemCard
|
||||||
<div className={`gc-traffic-light gc-traffic-light--${memLevel}`} aria-label="메모리 사용량">
|
label="메모리"
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
pct={pct}
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
usedLabel={fmt(used)}
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
totalLabel={fmt(total)}
|
||||||
</div>
|
/>
|
||||||
<p className="gc-traffic-caption">{memPct.toFixed(0)}%</p>
|
|
||||||
<p className="gc-traffic-caption gc-traffic-caption--sub">
|
|
||||||
{memUsed >= 1024 ? `${(memUsed/1024).toFixed(1)}GB` : `${memUsed}MB`}
|
|
||||||
{' / '}
|
|
||||||
{memTotal >= 1024 ? `${(memTotal/1024).toFixed(1)}GB` : `${memTotal}MB`}
|
|
||||||
</p>
|
|
||||||
<span className="gc-sys-tl-label">메모리</span>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{/* 디스크 */}
|
{/* 디스크 */}
|
||||||
{(() => {
|
<DashboardSystemCard
|
||||||
const diskPct = mon.system?.diskPct ?? 0;
|
label="디스크"
|
||||||
const diskLevel: 'green' | 'yellow' | 'red' =
|
pct={mon.system?.diskPct ?? 0}
|
||||||
diskPct >= 90 ? 'red' : diskPct >= 70 ? 'yellow' : 'green';
|
usedLabel={`${(mon.system?.diskUsedGb ?? 0).toFixed(1)}GB`}
|
||||||
const diskUsed = mon.system?.diskUsedGb ?? 0;
|
totalLabel={`${(mon.system?.diskTotalGb ?? 0).toFixed(1)}GB`}
|
||||||
const diskTotal = mon.system?.diskTotalGb ?? 0;
|
/>
|
||||||
return (
|
|
||||||
<div className="gc-sys-tl-item">
|
|
||||||
<div className={`gc-traffic-light gc-traffic-light--${diskLevel}`} aria-label="디스크 사용량">
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
|
||||||
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
|
||||||
</div>
|
|
||||||
<p className="gc-traffic-caption">{diskPct.toFixed(0)}%</p>
|
|
||||||
<p className="gc-traffic-caption gc-traffic-caption--sub">
|
|
||||||
{diskUsed.toFixed(1)}GB / {diskTotal.toFixed(1)}GB
|
|
||||||
</p>
|
|
||||||
<span className="gc-sys-tl-label">디스크</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="gc-sys-card">
|
||||||
|
<span className="gc-sys-card-title">{label}</span>
|
||||||
|
|
||||||
|
<div className="gc-sys-card-body">
|
||||||
|
{/* 세로 블록 바 */}
|
||||||
|
<div className="gc-sys-bar-wrap" aria-hidden>
|
||||||
|
{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 (
|
||||||
|
<span
|
||||||
|
key={i}
|
||||||
|
className={`gc-sys-block${on ? ` ${tone} gc-sys-block--on` : ''}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 신호등 */}
|
||||||
|
<div className={`gc-traffic-light gc-traffic-light--${level}`} aria-label={`${label} 상태`}>
|
||||||
|
<span className="gc-traffic-bulb gc-traffic-bulb--r" />
|
||||||
|
<span className="gc-traffic-bulb gc-traffic-bulb--y" />
|
||||||
|
<span className="gc-traffic-bulb gc-traffic-bulb--g" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gc-sys-card-foot">
|
||||||
|
<span className="gc-sys-card-pct">{clamped.toFixed(0)}%</span>
|
||||||
|
<span className="gc-sys-card-detail">{usedLabel} / {totalLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -326,32 +326,95 @@
|
|||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 시스템 상태 — 3열 신호등 (가로 배치) ── */
|
/* ── 시스템 상태 — 2열 리소스 카드 (가로 배치) ── */
|
||||||
.gc-dash-sys-status {
|
.gc-dash-sys-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
gap: 10px;
|
||||||
justify-content: space-around;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 4px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gc-sys-tl-item {
|
/* ── DashboardSystemCard ── */
|
||||||
|
.gc-sys-card {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
flex: 1;
|
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-size: 0.65rem;
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
color: var(--se-text-muted);
|
letter-spacing: 0.06em;
|
||||||
letter-spacing: 0.03em;
|
|
||||||
text-transform: uppercase;
|
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 ── */
|
/* ── Indicator bars ── */
|
||||||
|
|||||||
Reference in New Issue
Block a user