SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,223 @@
|
||||
import React from 'react';
|
||||
import { timeWeightedAverage } from './resultSync';
|
||||
|
||||
interface TimeSeriesChartProps {
|
||||
title: string;
|
||||
unit: string;
|
||||
values: number[];
|
||||
timeSteps: number[]; // in seconds
|
||||
currentIndex: number;
|
||||
onSelectTimeIndex?: (index: number) => void;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const TimeSeriesChart: React.FC<TimeSeriesChartProps> = ({
|
||||
title,
|
||||
unit,
|
||||
values,
|
||||
timeSteps,
|
||||
currentIndex,
|
||||
onSelectTimeIndex,
|
||||
color = 'var(--accent)',
|
||||
}) => {
|
||||
if (!values || values.length === 0) return null;
|
||||
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const range = max - min || 1;
|
||||
const avg = timeWeightedAverage(values, timeSteps);
|
||||
|
||||
const width = 320;
|
||||
const height = 90;
|
||||
const padLeft = 32;
|
||||
const padRight = 12;
|
||||
const padTop = 10;
|
||||
const padBottom = 18;
|
||||
|
||||
const plotWidth = width - padLeft - padRight;
|
||||
const plotHeight = height - padTop - padBottom;
|
||||
|
||||
const points = values.map((v, i) => {
|
||||
const x = padLeft + (i / (values.length - 1 || 1)) * plotWidth;
|
||||
const y = padTop + plotHeight - ((v - min) / range) * plotHeight;
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
});
|
||||
|
||||
const pathD = `M ${points.join(' L ')}`;
|
||||
|
||||
// Current time marker X
|
||||
const currentX =
|
||||
padLeft + (currentIndex / (values.length - 1 || 1)) * plotWidth;
|
||||
const currentY =
|
||||
padTop +
|
||||
plotHeight -
|
||||
(((values[currentIndex] ?? min) - min) / range) * plotHeight;
|
||||
|
||||
// Format time (seconds -> HH:00)
|
||||
const formatTime = (secs: number) => {
|
||||
const hours = Math.floor(secs / 3600);
|
||||
return `${String(hours).padStart(2, '0')}:00`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--bg-panel)',
|
||||
border: '1px solid #e2e8f0',
|
||||
borderRadius: '6px',
|
||||
padding: '8px',
|
||||
marginTop: '10px',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.03)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'baseline',
|
||||
marginBottom: '4px',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600, fontSize: '12px', color: 'var(--text)' }}>
|
||||
24h 시계열: {title}
|
||||
</span>
|
||||
<span style={{ fontSize: '11px', color: 'var(--text-secondary)' }}>
|
||||
현재:{' '}
|
||||
<strong style={{ color }}>
|
||||
{values[currentIndex]?.toFixed(2)} {unit}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', width: '100%', height: `${height}px` }}>
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
style={{ width: '100%', height: '100%', overflow: 'visible' }}
|
||||
onClick={(e) => {
|
||||
if (!onSelectTimeIndex) return;
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickRatio = (e.clientX - rect.left - (padLeft / width) * rect.width) / ((plotWidth / width) * rect.width);
|
||||
const clamped = Math.max(0, Math.min(1, clickRatio));
|
||||
const targetIdx = Math.round(clamped * (values.length - 1));
|
||||
onSelectTimeIndex(targetIdx);
|
||||
}}
|
||||
>
|
||||
{/* Background grid lines */}
|
||||
<line
|
||||
x1={padLeft}
|
||||
y1={padTop}
|
||||
x2={padLeft + plotWidth}
|
||||
y2={padTop}
|
||||
stroke="#f1f5f9"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1={padLeft}
|
||||
y1={padTop + plotHeight / 2}
|
||||
x2={padLeft + plotWidth}
|
||||
y2={padTop + plotHeight / 2}
|
||||
stroke="#f1f5f9"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
<line
|
||||
x1={padLeft}
|
||||
y1={padTop + plotHeight}
|
||||
x2={padLeft + plotWidth}
|
||||
y2={padTop + plotHeight}
|
||||
stroke="#e2e8f0"
|
||||
strokeWidth="1"
|
||||
/>
|
||||
|
||||
{/* Min / Max Labels */}
|
||||
<text
|
||||
x={padLeft - 4}
|
||||
y={padTop + 4}
|
||||
textAnchor="end"
|
||||
fontSize="8"
|
||||
fill="#94a3b8"
|
||||
>
|
||||
{max.toFixed(1)}
|
||||
</text>
|
||||
<text
|
||||
x={padLeft - 4}
|
||||
y={padTop + plotHeight}
|
||||
textAnchor="end"
|
||||
fontSize="8"
|
||||
fill="#94a3b8"
|
||||
>
|
||||
{min.toFixed(1)}
|
||||
</text>
|
||||
|
||||
{/* Trend line */}
|
||||
<path
|
||||
d={pathD}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
|
||||
{/* Current time marker line */}
|
||||
<line
|
||||
x1={currentX}
|
||||
y1={padTop}
|
||||
x2={currentX}
|
||||
y2={padTop + plotHeight}
|
||||
stroke="#ef4444"
|
||||
strokeWidth="1.5"
|
||||
strokeDasharray="2,2"
|
||||
/>
|
||||
|
||||
{/* Current value circle */}
|
||||
<circle cx={currentX} cy={currentY} r="3.5" fill="#ef4444" />
|
||||
|
||||
{/* Time axis labels */}
|
||||
<text
|
||||
x={padLeft}
|
||||
y={height - 2}
|
||||
textAnchor="start"
|
||||
fontSize="8"
|
||||
fill="#94a3b8"
|
||||
>
|
||||
{formatTime(timeSteps[0] ?? 0)}
|
||||
</text>
|
||||
<text
|
||||
x={padLeft + plotWidth / 2}
|
||||
y={height - 2}
|
||||
textAnchor="middle"
|
||||
fontSize="8"
|
||||
fill="#94a3b8"
|
||||
>
|
||||
{formatTime(timeSteps[Math.floor(timeSteps.length / 2)] ?? 43200)}
|
||||
</text>
|
||||
<text
|
||||
x={padLeft + plotWidth}
|
||||
y={height - 2}
|
||||
textAnchor="end"
|
||||
fontSize="8"
|
||||
fill="#94a3b8"
|
||||
>
|
||||
{formatTime(timeSteps[timeSteps.length - 1] ?? 86400)}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: '10px',
|
||||
color: 'var(--text-secondary)',
|
||||
marginTop: '4px',
|
||||
borderTop: '1px solid #f8fafc',
|
||||
paddingTop: '3px',
|
||||
}}
|
||||
>
|
||||
<span>최소: {min.toFixed(2)}</span>
|
||||
<span>평균: {avg.toFixed(2)}</span>
|
||||
<span>최대: {max.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user