219 lines
7.7 KiB
TypeScript
Executable File
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>
|
|
);
|
|
}
|