207 lines
8.0 KiB
TypeScript
Executable File
207 lines
8.0 KiB
TypeScript
Executable File
import type { DispersionGridResult, WeatherFetchResult } from '../utils/dispersionTypes';
|
|
import { windDirToCompass } from '../hooks/useWeatherFetch';
|
|
|
|
interface HNSRightPanelProps {
|
|
dispersionResult: {
|
|
zones: Array<{
|
|
level: string;
|
|
color: string;
|
|
radius: number;
|
|
angle: number;
|
|
}>;
|
|
timestamp: string;
|
|
windDirection: number;
|
|
substance: string;
|
|
concentration: {
|
|
'AEGL-3': string;
|
|
'AEGL-2': string;
|
|
'AEGL-1': string;
|
|
};
|
|
} | null;
|
|
computedResult?: DispersionGridResult | null;
|
|
weatherData?: WeatherFetchResult | null;
|
|
onOpenRecalc?: () => void;
|
|
onOpenReport?: () => void;
|
|
onSave?: () => void;
|
|
}
|
|
|
|
export function HNSRightPanel({
|
|
dispersionResult,
|
|
computedResult,
|
|
weatherData,
|
|
onOpenRecalc,
|
|
onOpenReport,
|
|
onSave,
|
|
}: HNSRightPanelProps) {
|
|
if (!dispersionResult) {
|
|
return (
|
|
<div className="w-full h-full bg-bg-surface border-l border-stroke p-4 overflow-auto flex items-center justify-center">
|
|
<div className="flex flex-col gap-3 items-center text-fg-disabled text-label-1">
|
|
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
|
<div>예측 실행 후 결과가 표시됩니다</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const area = computedResult?.aeglAreas.aegl1 ?? 0;
|
|
const maxConc = computedResult?.maxConcentration ?? 0;
|
|
const windSpd = weatherData?.windSpeed ?? 5.0;
|
|
const windDir = weatherData?.windDirection ?? dispersionResult.windDirection;
|
|
const modelLabel =
|
|
computedResult?.modelType === 'plume'
|
|
? 'Gaussian Plume'
|
|
: computedResult?.modelType === 'puff'
|
|
? 'Gaussian Puff'
|
|
: computedResult?.modelType === 'dense_gas'
|
|
? 'Dense Gas (B-M)'
|
|
: 'ALOHA';
|
|
|
|
return (
|
|
<div className="w-full bg-bg-surface border-l border-stroke p-4 overflow-auto flex flex-col gap-4">
|
|
{/* Header */}
|
|
<div>
|
|
<div className="flex items-center gap-1.5 mb-2">
|
|
<div
|
|
style={{
|
|
width: '6px',
|
|
height: '6px',
|
|
borderRadius: '50%',
|
|
background: 'var(--color-accent)',
|
|
animation: 'pulse 1.5s infinite',
|
|
}}
|
|
></div>
|
|
<h3 className="text-title-4 font-bold m-0">예측 결과</h3>
|
|
</div>
|
|
<div className="text-label-2 text-fg-disabled font-mono">
|
|
{dispersionResult.substance} · {modelLabel}
|
|
</div>
|
|
</div>
|
|
|
|
{/* KPI Cards */}
|
|
<div className="flex flex-col gap-2">
|
|
{/* 최대 농도 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-label-2 text-fg-disabled mb-1.5">최대 농도</div>
|
|
<div className="text-title-1 font-bold font-mono text-color-caution">
|
|
{maxConc > 0 ? maxConc.toFixed(1) : '—'}{' '}
|
|
<span className="text-label-2 font-medium">ppm</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 확산 면적 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-label-2 text-fg-disabled mb-1.5">AEGL-1 확산 면적</div>
|
|
<div className="text-title-1 font-bold font-mono text-color-accent">
|
|
{area > 0 ? area.toFixed(2) : '—'} <span className="text-label-2 font-medium">km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 풍속 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-label-2 text-fg-disabled mb-1.5">풍속</div>
|
|
<div className="text-title-1 font-bold font-mono">
|
|
{windSpd.toFixed(1)} <span className="text-label-2 font-medium">m/s</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 풍향 */}
|
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-label-2 text-fg-disabled mb-1.5">풍향</div>
|
|
<div className="text-title-1 font-bold font-mono">
|
|
{windDirToCompass(windDir)} <span className="text-label-2 font-medium">{windDir}°</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL Zone Details */}
|
|
<div>
|
|
<h4 className="text-label-2 font-semibold text-fg-sub mt-0 mb-2.5">AEGL 구역 상세</h4>
|
|
<div className="flex flex-col gap-2">
|
|
{/* AEGL-3 */}
|
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-label-2 font-semibold">AEGL-3 (생명위협)</span>
|
|
<span className="text-label-2 font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl3 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-3']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl3 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL-2 */}
|
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-label-2 font-semibold">AEGL-2 (건강피해)</span>
|
|
<span className="text-label-2 font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl2 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-2']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl2 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* AEGL-1 */}
|
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
|
<div className="flex justify-between items-center mb-1">
|
|
<span className="text-label-2 font-semibold">AEGL-1 (불쾌감)</span>
|
|
<span className="text-label-2 font-mono text-fg-disabled">
|
|
{computedResult?.aeglDistances.aegl1 || 0}m
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
|
<span>{dispersionResult.concentration['AEGL-1']}</span>
|
|
<span className="font-mono">{computedResult?.aeglAreas.aegl1 ?? 0} km²</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 시간 정보 (puff/dense_gas) */}
|
|
{computedResult && computedResult.modelType !== 'plume' && (
|
|
<div className="p-2.5 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
|
<div className="text-label-2 text-fg-disabled mb-1">현재 시뮬레이션 시간</div>
|
|
<div className="text-title-3 font-bold font-mono text-color-accent">
|
|
t = {computedResult.timeStep}s
|
|
<span className="text-label-2 font-normal text-fg-disabled ml-1.5">
|
|
({(computedResult.timeStep / 60).toFixed(1)}분)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Timestamp */}
|
|
<div className="mt-auto pt-3 border-t border-stroke text-label-2 text-fg-disabled font-mono">
|
|
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
|
</div>
|
|
|
|
{/* Bottom Action Buttons */}
|
|
<div className="flex gap-1.5 pt-3 border-t border-stroke">
|
|
<button
|
|
onClick={onSave}
|
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
|
|
>
|
|
💾 저장
|
|
</button>
|
|
<button
|
|
onClick={onOpenRecalc}
|
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-bg-elevated border border-stroke text-fg font-korean"
|
|
>
|
|
🔄 재계산
|
|
</button>
|
|
<button
|
|
onClick={onOpenReport}
|
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
|
|
>
|
|
📄 보고서
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|