wing-ops/frontend/src/tabs/hns/components/HNSRecalcModal.tsx

219 lines
7.7 KiB
TypeScript
Executable File

import { useState, useRef, useEffect } from 'react';
import { ComboBox } from '@common/components/ui/ComboBox';
export interface RecalcParams {
substance: string;
releaseType: '연속 유출' | '순간 유출' | '밀도가스 유출';
emissionRate: number;
totalRelease: number;
algorithm: string;
predictionTime: string;
}
interface HNSRecalcModalProps {
isOpen: boolean;
onClose: () => void;
onSubmit: (params: RecalcParams) => void;
currentParams?: Partial<RecalcParams> | null;
}
export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNSRecalcModalProps) {
const backdropRef = useRef<HTMLDivElement>(null);
const [substance, setSubstance] = useState('톨루엔 (Toluene)');
const [releaseType, setReleaseType] = useState<RecalcParams['releaseType']>('연속 유출');
const [amount, setAmount] = useState('10');
const [algorithm, setAlgorithm] = useState('ALOHA (EPA)');
const [predictionTime, setPredictionTime] = useState('24시간');
// 모달 열릴 때 현재 파라미터로 초기화
useEffect(() => {
if (!isOpen || !currentParams) return;
queueMicrotask(() => {
if (currentParams.substance) setSubstance(currentParams.substance);
if (currentParams.releaseType) setReleaseType(currentParams.releaseType);
if (currentParams.releaseType === '순간 유출') {
setAmount(String(currentParams.totalRelease ?? ''));
} else {
setAmount(String(currentParams.emissionRate ?? ''));
}
if (currentParams.algorithm) setAlgorithm(currentParams.algorithm);
if (currentParams.predictionTime) setPredictionTime(currentParams.predictionTime);
});
}, [isOpen, currentParams]);
// 배경 클릭으로 닫기
useEffect(() => {
const handler = (e: MouseEvent) => {
if (e.target === backdropRef.current) onClose();
};
if (isOpen) document.addEventListener('mousedown', handler);
return () => document.removeEventListener('mousedown', handler);
}, [isOpen, onClose]);
const handleRun = () => {
const numAmount = parseFloat(amount) || 10;
onSubmit({
substance,
releaseType,
emissionRate: releaseType !== '순간 유출' ? numAmount : 10,
totalRelease: releaseType === '순간 유출' ? numAmount : 5000,
algorithm,
predictionTime,
});
onClose();
};
if (!isOpen) return null;
const amountLabel = releaseType === '순간 유출' ? '총 누출량' : '배출률';
const amountUnit = releaseType === '순간 유출' ? 'g' : 'g/s';
return (
<div
ref={backdropRef}
className="fixed inset-0 z-[9999] flex items-center justify-center"
style={{ background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(4px)' }}
>
<div
className="w-[380px] bg-bg-surface border border-stroke rounded-[14px] overflow-hidden flex flex-col"
style={{ boxShadow: '0 20px 60px rgba(0,0,0,0.5)' }}
>
{/* Header */}
<div className="px-5 py-4 border-b border-stroke flex items-center gap-3">
<div
className="w-9 h-9 rounded-[10px] border border-[rgba(6,182,212,0.3)] flex items-center justify-center text-base shrink-0"
style={{
background: 'linear-gradient(135deg, rgba(6,182,212,0.2), rgba(59,130,246,0.15))',
}}
>
🔄
</div>
<div className="flex-1">
<h2 className="text-title-2 font-bold m-0"> </h2>
<div className="text-label-2 text-fg-disabled mt-0.5"> </div>
</div>
<button
onClick={onClose}
className="w-7 h-7 rounded-md border border-stroke bg-bg-card text-fg-disabled text-label-1 cursor-pointer flex items-center justify-center shrink-0"
>
</button>
</div>
{/* Content */}
<div
className="px-5 py-4 flex flex-col gap-3"
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
>
{/* HNS 물질 */}
<FG label="HNS 물질">
<ComboBox
className="prd-i"
value={substance}
onChange={setSubstance}
options={[
{ value: '톨루엔 (Toluene)', label: '톨루엔' },
{ value: '벤젠 (Benzene)', label: '벤젠' },
{ value: '메탄올 (Methanol)', label: '메탄올' },
{ value: '암모니아 (Ammonia)', label: '암모니아' },
{ value: '염화수소 (HCl)', label: '염화수소' },
{ value: '황화수소 (H2S)', label: '황화수소' },
]}
/>
</FG>
{/* 유출 유형 + 유출량 */}
<div className="grid grid-cols-2 gap-[10px]">
<FG label="유출 유형">
<ComboBox
className="prd-i"
value={releaseType}
onChange={(v) => setReleaseType(v as RecalcParams['releaseType'])}
options={[
{ value: '연속 유출', label: '연속 유출' },
{ value: '순간 유출', label: '순간 유출' },
{ value: '밀도가스 유출', label: '밀도가스 유출' },
]}
/>
</FG>
<FG label={`${amountLabel} (${amountUnit})`}>
<input
className="prd-i font-mono"
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder={amountUnit}
/>
</FG>
</div>
{/* 확산 모델 + 예측 시간 */}
<div className="grid grid-cols-2 gap-[10px]">
<FG label="예측 알고리즘">
<ComboBox
className="prd-i"
value={algorithm}
onChange={setAlgorithm}
options={[
{ value: 'ALOHA (EPA)', label: 'ALOHA (EPA)' },
{ value: 'CAMEO', label: 'CAMEO' },
{ value: 'Gaussian Plume', label: 'Gaussian Plume' },
{ value: 'AERMOD', label: 'AERMOD' },
]}
/>
</FG>
<FG label="예측 시간">
<ComboBox
className="prd-i"
value={predictionTime}
onChange={setPredictionTime}
options={[
{ value: '6시간', label: '6시간' },
{ value: '12시간', label: '12시간' },
{ value: '24시간', label: '24시간' },
{ value: '48시간', label: '48시간' },
]}
/>
</FG>
</div>
</div>
{/* Footer */}
<div className="px-5 py-[14px] border-t border-stroke flex gap-2">
<button
onClick={onClose}
className="flex-1 py-2.5 text-label-1 font-semibold rounded-md cursor-pointer bg-bg-card border border-stroke text-fg-sub"
>
</button>
<button
onClick={handleRun}
className="flex-[2] py-2.5 text-label-1 font-bold rounded-md text-white"
style={{
cursor: 'pointer',
background: 'linear-gradient(135deg, var(--color-accent), #ef4444)',
border: 'none',
}}
>
🔄
</button>
</div>
</div>
</div>
);
}
function FG({ label, children }: { label: string; children: React.ReactNode }) {
return (
<div>
<div
style={{ fontSize: '10px', fontWeight: 700, color: 'var(--fg-sub)', marginBottom: '6px' }}
>
{label}
</div>
{children}
</div>
);
}