wing-ops/frontend/src/tabs/rescue/components/RescueView.tsx

1613 lines
59 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Fragment, useState, useEffect, useCallback } from 'react';
import { useSubMenu } from '@common/hooks/useSubMenu';
import { RescueTheoryView } from './RescueTheoryView';
import { RescueScenarioView } from './RescueScenarioView';
import { fetchRescueOps } from '../services/rescueApi';
import type { RescueOpsItem } from '../services/rescueApi';
/* ─── Types ─── */
type AccidentType =
| 'collision'
| 'grounding'
| 'turning'
| 'capsizing'
| 'sharpTurn'
| 'flooding'
| 'sinking';
type AnalysisTab = 'rescue' | 'damageStability' | 'longitudinalStrength';
/* ─── 사고 유형 데이터 ─── */
const accidentTypes: {
id: AccidentType;
label: string;
eng: string;
icon: string;
desc: string;
}[] = [
{ id: 'collision', label: '충돌', eng: 'Collision', icon: '💥', desc: '외력에 의한 선체 손상' },
{ id: 'grounding', label: '좌초', eng: 'Grounding', icon: '🪨', desc: '해저 접촉/암초 좌초' },
{ id: 'turning', label: '선회', eng: 'Turning', icon: '🔄', desc: '급격한 방향전환 사고' },
{ id: 'capsizing', label: '전복', eng: 'Capsizing', icon: '🔃', desc: '복원력 상실에 의한 전복' },
{
id: 'sharpTurn',
label: '급선회',
eng: 'Hard Turn',
icon: '↩️',
desc: '고속 급선회에 의한 경사',
},
{ id: 'flooding', label: '침수', eng: 'Flooding', icon: '🌊', desc: '해수 유입에 의한 침수' },
{ id: 'sinking', label: '침몰', eng: 'Sinking', icon: '⬇️', desc: '부력 상실에 의한 침몰' },
];
const analysisTabs: { id: AnalysisTab; label: string; icon: string }[] = [
{ id: 'rescue', label: '구난분석', icon: '🚨' },
{ id: 'damageStability', label: '손상복원성', icon: '⚖' },
{ id: 'longitudinalStrength', label: '종강도', icon: '📏' },
];
/* ─── 사고 유형별 파라미터 ─── */
const rscTypeData: Record<
AccidentType,
{
zone: string;
gm: string;
list: string;
trim: string;
buoy: number;
incident: string;
survivors: number;
total: number;
missing: number;
oilRate: string;
}
> = {
collision: {
zone: 'PREDICTED\nDAMAGE ZONE',
gm: '0.8',
list: '15.0',
trim: '2.5',
buoy: 30,
incident: 'M/V SEA GUARDIAN 충돌 사고',
survivors: 15,
total: 20,
missing: 5,
oilRate: '100L/min',
},
grounding: {
zone: 'GROUNDING\nIMPACT AREA',
gm: '1.2',
list: '8.0',
trim: '3.8',
buoy: 45,
incident: 'M/V OCEAN STAR 좌초 사고',
survivors: 22,
total: 25,
missing: 3,
oilRate: '50L/min',
},
turning: {
zone: 'PREDICTED\nDRIFT PATH',
gm: '1.5',
list: '12.0',
trim: '0.8',
buoy: 65,
incident: 'M/V PACIFIC WAVE 선회 사고',
survivors: 18,
total: 18,
missing: 0,
oilRate: '0L/min',
},
capsizing: {
zone: 'CAPSIZING\nRISK ZONE',
gm: '0.2',
list: '45.0',
trim: '1.2',
buoy: 10,
incident: 'M/V GRAND FORTUNE 전복 사고',
survivors: 8,
total: 15,
missing: 7,
oilRate: '200L/min',
},
sharpTurn: {
zone: 'VESSEL\nTURNING CIRCLE',
gm: '0.6',
list: '25.0',
trim: '0.5',
buoy: 50,
incident: 'M/V BLUE HORIZON 급선회 사고',
survivors: 20,
total: 20,
missing: 0,
oilRate: '0L/min',
},
flooding: {
zone: 'FLOODING\nSPREAD AREA',
gm: '0.5',
list: '18.0',
trim: '4.2',
buoy: 20,
incident: 'M/V EASTERN GLORY 침수 사고',
survivors: 12,
total: 16,
missing: 4,
oilRate: '80L/min',
},
sinking: {
zone: 'PREDICTED\nSINKING AREA',
gm: '0.1',
list: '35.0',
trim: '6.0',
buoy: 5,
incident: 'M/V HARMONY 침몰 사고',
survivors: 10,
total: 22,
missing: 12,
oilRate: '350L/min',
},
};
/* ─── 색상 헬퍼 ─── */
function gmColor(v: string) {
const n = parseFloat(v);
return n < 1.0
? 'var(--color-danger)'
: n < 1.5
? 'var(--color-caution)'
: 'var(--color-success)';
}
function listColor(v: string) {
const n = parseFloat(v);
return n > 20 ? 'var(--color-danger)' : n > 10 ? 'var(--color-caution)' : 'var(--color-success)';
}
function trimColor(v: string) {
const n = parseFloat(v);
return n > 3 ? 'var(--color-danger)' : n > 1.5 ? 'var(--color-caution)' : 'var(--color-success)';
}
function buoyColor(v: number) {
return v < 30 ? 'var(--color-danger)' : v < 50 ? 'var(--color-caution)' : 'var(--color-success)';
}
/* ─── 상단 사고 정보바 ─── */
function TopInfoBar({ activeType }: { activeType: AccidentType }) {
const d = rscTypeData[activeType];
const [clock, setClock] = useState('');
useEffect(() => {
const tick = () => {
const now = new Date();
setClock(
`${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')} KST`,
);
};
tick();
const iv = setInterval(tick, 1000);
return () => clearInterval(iv);
}, []);
return (
<div className="h-9 bg-bg-base border-b border-stroke flex items-center px-4 gap-3.5 flex-shrink-0">
<div className="flex items-center gap-1.5">
<span className="text-title-3"></span>
<span className="text-label-1 font-bold text-fg font-korean"></span>
</div>
<div className="flex items-center gap-1.5 text-label-2 text-color-danger font-korean">
{/* <span className="w-2.5 h-2.5 rounded-sm bg-color-danger inline-block" /> */}
: {d.incident}
</div>
<div className="flex gap-3 text-caption font-mono text-fg-disabled ml-auto">
<span>
: <span className="text-fg">{d.survivors}</span>/{d.total}
</span>
<span>
: <span className="text-fg">{d.missing}</span>
</span>
<span>
GM: <span className="text-fg">{d.gm}m</span>
</span>
<span>
: <span className="text-fg">{d.list}°</span>
</span>
<span>
: <span className="text-fg">{d.oilRate}</span>
</span>
</div>
<div className="text-title-4 font-bold text-fg font-mono">{clock}</div>
<div className="text-caption text-fg-disabled font-korean">👤 Cmdr. KIM</div>
</div>
);
}
/* ─── 왼쪽 패널: 사고유형 + 긴급경고 + CCTV ─── */
function LeftPanel({
activeType,
onTypeChange,
}: {
activeType: AccidentType;
onTypeChange: (t: AccidentType) => void;
}) {
return (
<div className="w-[208px] min-w-[208px] bg-bg-base border-r border-stroke flex flex-col overflow-y-auto scrollbar-thin p-2 gap-0.5">
{/* 사고유형 제목 */}
<div className="text-caption font-bold text-fg-disabled font-korean mb-0.5 tracking-wider">
(INCIDENT TYPE)
</div>
{/* 사고유형 버튼 */}
{accidentTypes.map((t) => (
<button
key={t.id}
onClick={() => onTypeChange(t.id)}
className={`flex items-center gap-2 w-full text-left px-2.5 py-[7px] rounded-md border transition-all cursor-pointer ${
activeType === t.id
? 'bg-[rgba(6,182,212,0.1)] border-[rgba(6,182,212,0.35)]'
: 'bg-bg-card border-stroke hover:border-[var(--stroke-light)] hover:bg-bg-surface-hover'
}`}
>
<span className="text-label-1">{t.icon}</span>
<div>
<div
className={`text-label-2 font-bold font-korean ${activeType === t.id ? 'text-color-accent' : 'text-fg'}`}
>
{t.label} ({t.eng})
</div>
<div className="text-caption text-fg-disabled font-korean mt-px">{t.desc}</div>
</div>
</button>
))}
{/* 긴급 경고 */}
<div className="text-caption font-bold text-fg-disabled font-korean mt-2.5 mb-1">
(CRITICAL ALERTS)
</div>
<div className="py-1.5 px-2.5 border-l-2 border-l-[var(--color-danger)] rounded-r text-caption text-fg-disabled font-korean">
GM
</div>
<div className="py-1.5 px-2.5 border-l-2 border-l-[var(--color-danger)] rounded-r text-caption text-fg-disabled font-korean">
5
</div>
<div className="py-1.5 px-2.5 border-l-2 border-l-[var(--color-danger)] rounded-r text-caption text-fg-disabled font-korean">
-
</div>
<div className="py-1.5 px-2.5 border-l-2 border-l-[var(--color-danger)] rounded-r text-caption text-fg-disabled font-korean">
88%
</div>
{/* CCTV 피드 */}
<div className="w-full aspect-[4/3] bg-bg-base border border-stroke rounded-md flex items-center justify-center relative overflow-hidden mt-1.5 flex-shrink-0">
<div
className="absolute inset-0"
style={{
background: 'repeating-linear-gradient(0deg, rgba(255,0,0,.03), transparent 2px)',
}}
/>
<div className="text-caption text-[rgba(255,60,60,0.35)] font-mono">CCTV FEED #1</div>
<div className="absolute top-1 left-1.5 text-caption text-[rgba(255,60,60,0.5)] font-mono">
REC
</div>
</div>
</div>
);
}
/* ─── 중앙 지도 영역 ─── */
function CenterMap({ activeType }: { activeType: AccidentType }) {
const d = rscTypeData[activeType];
return (
<div className="flex-1 relative overflow-hidden bg-bg-base">
{/* 해양 배경 그라데이션 */}
<div
className="absolute inset-0"
style={{
background:
'radial-gradient(ellipse at 30% 40%, rgba(6,90,130,.25) 0%, transparent 60%), radial-gradient(ellipse at 70% 60%, rgba(8,60,100,.2) 0%, transparent 50%), linear-gradient(180deg, #0a1628, #0d1f35 50%, #091520)',
}}
/>
{/* 격자 */}
<div
className="absolute inset-0"
style={{
backgroundImage:
'linear-gradient(rgba(30,60,100,.12) 1px, transparent 1px), linear-gradient(90deg, rgba(30,60,100,.12) 1px, transparent 1px)',
backgroundSize: '80px 80px',
}}
/>
{/* 해안선 힌트 */}
<div
className="absolute right-0 top-0 w-[55%] h-full"
style={{
background:
'linear-gradient(225deg, rgba(30,50,70,.55), rgba(20,35,50,.25) 35%, transparent 55%)',
clipPath:
'polygon(60% 0%, 65% 5%, 70% 12%, 72% 20%, 68% 30%, 65% 40%, 60% 50%, 55% 58%, 50% 65%, 45% 72%, 42% 80%, 48% 88%, 100% 100%, 100% 0%)',
}}
/>
{/* 사고 해역 정보 */}
<div className="absolute top-2.5 left-2.5 z-20 bg-[var(--dropdown-bg)] border border-stroke rounded-md px-3 py-2 backdrop-blur-sm font-mono text-caption text-fg-disabled">
<div className="text-label-2 font-bold text-fg font-korean mb-1"> </div>
<div className="grid gap-x-1.5 gap-y-px" style={{ gridTemplateColumns: '32px 1fr' }}>
<span></span>
<b className="text-fg">37°28'N, 126°15'E</b>
<span></span>
<b className="text-fg">45m</b>
<span></span>
<b className="text-fg">2.5 knots NE</b>
</div>
</div>
{/* 선박 모형 */}
<div className="absolute z-[15] top-[42%] left-[46%] -rotate-[15deg]">
<div
className="relative w-[72px] h-5"
style={{
background: 'linear-gradient(90deg, #4a3728, #6b4c33)',
borderRadius: '3px 10px 10px 3px',
border: '1px solid rgba(255,150,50,.4)',
boxShadow: '0 0 18px rgba(255,100,0,.2)',
}}
>
<div
className="absolute w-0.5 h-[7px] bg-fg-disabled rounded-[1px]"
style={{ top: '-3px', left: '60%' }}
/>
</div>
<div className="text-caption text-center mt-0.5 font-mono text-[rgba(255,200,150,0.5)]">
M/V SEA GUARDIAN
</div>
</div>
{/* 예측 구역 원 */}
<div
className="absolute z-[5] rounded-full"
style={{
top: '32%',
left: '32%',
width: '220px',
height: '220px',
background:
'radial-gradient(circle, rgba(6,182,212,.07), rgba(6,182,212,.02) 50%, transparent 70%)',
border: '1.5px dashed rgba(6,182,212,.2)',
}}
/>
{/* 구역 라벨 */}
<div className="absolute z-[6] text-caption font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)] top-1/2 left-[36%]">
{d.zone.replace('\\n', '\n')}
</div>
{/* SAR 자산 */}
<div className="absolute z-10 text-caption font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">
ETA 5 MIN
</div>
<div className="absolute z-10 text-caption font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">
ETA 15 MIN
</div>
<div className="absolute z-[12] text-title-3 opacity-60 top-[7%] left-[52%] -rotate-[30deg]">
🚁
</div>
<div className="absolute z-[12] text-caption font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">
6M
</div>
<div className="absolute z-[12] text-label-2 opacity-45 top-[28%] left-[54%]">🚢</div>
{/* 환경 민감 구역 */}
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)] bottom-[6%] right-[6%]">
<div className="text-caption font-bold font-serif uppercase tracking-wider leading-snug text-[rgba(34,197,94,0.55)]">
ENVIRONMENTALLY SENSITIVE
<br />
AREA: AQUACULTURE FARM
</div>
</div>
{/* 지도 컨트롤 */}
<div className="absolute top-2.5 right-2.5 z-20 flex flex-col gap-0.5">
{['🗺', '🔍', '📐'].map((ico, i) => (
<button
key={i}
className="w-7 h-7 bg-[rgba(13,17,23,0.85)] border border-stroke rounded text-fg-disabled text-label-1 flex items-center justify-center cursor-pointer hover:text-fg"
>
{ico}
</button>
))}
</div>
{/* 스케일 바 */}
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-stroke rounded px-2.5 py-1 text-caption text-fg-disabled font-mono">
<div
className="w-[70px] h-0.5 mb-0.5"
style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--stroke-default) 50%)' }}
/>
5 km · Zoom: 100%
</div>
{/* 사고 유형 표시 */}
{/* <div className="absolute bottom-2.5 right-2.5 z-20 bg-[rgba(13,17,23,0.85)] border border-stroke rounded px-3 py-1.5">
<div className="text-caption text-fg-disabled font-korean">현재 사고 유형</div>
<div className="text-label-2 font-bold font-korean text-color-accent">
{at.icon} {at.label} ({at.eng})
</div>
</div> */}
{/* 타임라인 시뮬레이션 컨트롤 */}
<div className="absolute bottom-2.5 left-1/2 -translate-x-1/2 z-20 bg-[rgba(13,17,23,0.9)] border border-stroke rounded-md px-4 py-2 flex items-center gap-4 backdrop-blur-sm">
<div className="text-caption font-bold text-fg font-korean whitespace-nowrap">TIMELINE</div>
<div className="flex items-center gap-1.5 text-caption text-fg-disabled font-mono">
<span>[-6h]</span>
<span className="font-bold text-fg">[NOW]</span>
<span>[+6H]</span>
<span>[+12H]</span>
<span>[+24H]</span>
</div>
<div className="relative w-24 h-1.5 bg-bg-surface-hover rounded-sm">
<div
className="absolute rounded-full border-2 border-bg-0 bg-color-accent"
style={{
left: '35%',
top: '-3px',
width: '12px',
height: '12px',
boxShadow: '0 0 8px rgba(6,182,212,.4)',
}}
/>
</div>
<div className="flex items-center gap-1.5">
<button className="w-6 h-6 bg-bg-card border border-stroke rounded-full text-fg-disabled text-label-2 flex items-center justify-center cursor-pointer hover:text-fg">
</button>
<button
className="w-8 h-8 rounded-full text-color-accent text-title-4 flex items-center justify-center cursor-pointer hover:brightness-125"
style={{
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
border: '1px solid color-mix(in srgb, var(--color-accent) 30%, transparent)',
}}
>
</button>
<button className="w-6 h-6 bg-bg-card border border-stroke rounded-full text-fg-disabled text-label-2 flex items-center justify-center cursor-pointer hover:text-fg">
</button>
</div>
<div className="text-caption text-fg-disabled font-mono whitespace-nowrap">
<b className="text-color-accent">10:45</b> KST
</div>
</div>
</div>
);
}
/* ─── 오른쪽 분석 패널 ─── */
function RightPanel({
activeAnalysis,
onAnalysisChange,
activeType,
}: {
activeAnalysis: AnalysisTab;
onAnalysisChange: (t: AnalysisTab) => void;
activeType: AccidentType;
}) {
return (
<div className="w-[310px] min-w-[310px] bg-bg-base border-l border-stroke flex flex-col overflow-hidden">
{/* 분석 탭 */}
<div className="flex border-b border-stroke flex-shrink-0">
{analysisTabs.map((tab) => (
<button
key={tab.id}
onClick={() => onAnalysisChange(tab.id)}
className={`flex-1 py-2 text-caption font-semibold font-korean cursor-pointer text-center transition-all border-b-2 ${
activeAnalysis === tab.id
? 'text-color-accent border-b-[var(--color-accent)] bg-[rgba(6,182,212,0.04)]'
: 'text-fg-disabled border-b-transparent hover:text-fg'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
{/* 패널 콘텐츠 */}
<div className="flex-1 overflow-y-auto scrollbar-thin">
{activeAnalysis === 'rescue' && <RescuePanel activeType={activeType} />}
{activeAnalysis === 'damageStability' && <DamageStabilityPanel activeType={activeType} />}
{activeAnalysis === 'longitudinalStrength' && <LongStrengthPanel activeType={activeType} />}
</div>
{/* Bottom Action Buttons */}
<div className="flex gap-1.5 p-3 border-t border-stroke flex-shrink-0">
<button className="flex-1 py-2 px-1 rounded text-label-2 font-semibold text-color-accent font-korean cursor-pointer border border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.08)]">
💾
</button>
<button className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-bg-elevated border border-stroke text-fg font-korean cursor-pointer">
🔄
</button>
<button className="flex-1 py-2 px-1 rounded text-label-2 font-semibold text-color-accent font-korean cursor-pointer border border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.08)]">
📄
</button>
</div>
</div>
);
}
/* ─── 구난 분석 패널 ─── */
function RescuePanel({ activeType }: { activeType: AccidentType }) {
const d = rscTypeData[activeType];
const at = accidentTypes.find((t) => t.id === activeType)!;
return (
<div className="flex flex-col p-2.5 gap-2">
<div className="text-label-1 font-bold text-fg font-korean"> (RESCUE ANALYSIS)</div>
<div className="text-caption text-fg-sub font-korean px-2 py-1 rounded bg-bg-card border border-stroke">
: {at.label} ({at.eng})
</div>
{/* 선박 단면도 SVG */}
<div className="bg-bg-card border border-stroke rounded-md p-2 relative">
<div className="absolute top-1.5 right-2 text-caption text-fg-disabled font-mono">
VESSEL STATUS
</div>
<svg viewBox="0 0 260 80" className="w-full" style={{ height: '65px' }}>
<rect
x="30"
y="24"
width="170"
height="28"
rx="4"
fill="var(--bg-surface-hover)"
stroke="var(--stroke-light)"
strokeWidth=".8"
/>
<rect
x="85"
y="15"
width="55"
height="10"
rx="2"
fill="var(--bg-surface-hover)"
stroke="var(--stroke-light)"
strokeWidth=".6"
/>
<rect
x="42"
y="27"
width="30"
height="18"
rx="1"
fill="color-mix(in srgb, var(--color-danger) 12%, transparent)"
stroke="color-mix(in srgb, var(--color-danger) 35%, transparent)"
strokeWidth=".6"
/>
<text
x="57"
y="39"
fill="color-mix(in srgb, var(--color-danger) 50%, transparent)"
fontSize="4.5"
textAnchor="middle"
fontFamily="var(--font-mono)"
>
FLOODED
</text>
<circle
cx="42"
cy="38"
r="6"
fill="none"
stroke="var(--color-danger)"
strokeWidth="1"
strokeDasharray="2,1"
/>
<text
x="42"
y="50"
fill="var(--color-danger)"
fontSize="4"
textAnchor="middle"
fontFamily="var(--font-mono)"
>
IMPACT
</text>
<line
x1="30"
y1="52"
x2="200"
y2="52"
stroke="var(--stroke-default)"
strokeWidth=".4"
strokeDasharray="3,2"
/>
<text
x="205"
y="55"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
WL
</text>
<line
x1="130"
y1="8"
x2="130"
y2="60"
stroke="color-mix(in srgb, var(--fg-disabled) 15%, transparent)"
strokeWidth=".4"
strokeDasharray="2,2"
/>
<text x="230" y="20" fill="var(--fg-disabled)" fontSize="5">
LIST: {d.list}°
</text>
<text x="230" y="28" fill="var(--fg-disabled)" fontSize="5">
TRIM: {d.trim}m
</text>
</svg>
</div>
{/* 핵심 지표 2×2 */}
<div className="grid grid-cols-2 gap-1.5">
<MetricCard
label="GM (메타센터 높이)"
value={d.gm}
unit="m"
color={gmColor(d.gm)}
sub={`${parseFloat(d.gm) < 1.0 ? '위험' : parseFloat(d.gm) < 1.5 ? '주의' : '정상'} (기준: 1.5m 이상)`}
/>
<MetricCard
label="LIST (횡경사)"
value={d.list}
unit="°"
color={listColor(d.list)}
sub={`${parseFloat(d.list) > 20 ? '위험' : parseFloat(d.list) > 10 ? '주의' : '정상'} (기준: 10° 이내)`}
/>
</div>
<div className="grid grid-cols-2 gap-1.5">
<MetricCard
label="TRIM (종경사)"
value={d.trim}
unit="m"
color={trimColor(d.trim)}
sub="선미 침하 (AFT SINKAGE)"
/>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">
(Reserve Buoyancy)
</div>
<div className="text-title-2 font-bold font-mono" style={{ color: buoyColor(d.buoy) }}>
{d.buoy}
<span className="text-label-2">%</span>
</div>
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
<div
className="h-full rounded-sm transition-all"
style={{ width: `${d.buoy}%`, background: buoyColor(d.buoy) }}
/>
</div>
</div>
</div>
{/* 긴급 조치 버튼 */}
<div className="text-label-2 font-bold text-fg-disabled font-korean">
(EMERGENCY ACTIONS)
</div>
<div className="grid grid-cols-2 gap-1.5">
{[
{ en: 'BALLAST INJECT', ko: '밸러스트 주입' },
{ en: 'BALLAST DISCHARGE', ko: '밸러스트 배출' },
{ en: 'ENGINE STOP', ko: '기관 정지' },
{ en: 'ANCHOR DROP', ko: '묘 투하' },
].map((btn, i) => (
<button
key={i}
className="py-[7px] rounded-[5px] text-center cursor-pointer transition-all hover:brightness-125 bg-bg-card border border-stroke"
>
<div className="text-label-2 font-bold text-fg-sub font-mono">{btn.en}</div>
<div className="text-caption font-korean text-color-danger">{btn.ko}</div>
</button>
))}
</div>
{/* 구난 의사결정 프로세스 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
(KRISO Decision Support)
</div>
<div className="flex flex-col gap-0.5 text-caption font-korean">
<div className="flex items-center gap-1">
{['① 상태평가', '② 사례분석', '③ 장비선정'].map((label, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-fg-disabled"></span>}
<div className="px-1.5 py-0.5 rounded-sm text-center flex-shrink-0 min-w-[68px] bg-bg-card border border-stroke text-fg-sub">
{label}
</div>
</Fragment>
))}
</div>
<div className="flex items-center gap-1">
{['④ 예인력', '⑤ 이초/인양', '⑥ 유출량'].map((label, i) => (
<Fragment key={i}>
{i > 0 && <span className="text-fg-disabled"></span>}
<div className="px-1.5 py-0.5 rounded-sm text-center flex-shrink-0 min-w-[68px] bg-bg-card border border-stroke text-fg-sub">
{label}
</div>
</Fragment>
))}
</div>
</div>
</div>
{/* 유체 정역학 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
(Hydrostatics)
</div>
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
{[
{ label: '배수량(Δ)', value: '12,450 ton' },
{ label: '흘수(Draft)', value: '7.2 m' },
{ label: 'KG', value: '8.5 m' },
{ label: 'KM (횡)', value: '9.3 m' },
{ label: 'TPC', value: '22.8 t/cm' },
{ label: 'MTC', value: '185 t·m' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-card rounded-sm">
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
<br />
<b className="text-fg-sub">{r.value}</b>
</div>
))}
</div>
</div>
{/* 예인력/이초력 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
/ (Towing & Refloating)
</div>
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
{[
{ label: '필요 예인력', value: '285 kN' },
{ label: '비상 예인력', value: '420 kN' },
{ label: '이초 반력', value: '1,850 kN' },
{ label: '인양 안전성', value: 'FAIL' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-card rounded-sm">
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
<br />
<b className="text-fg-sub">{r.value}</b>
</div>
))}
</div>
<div className="text-caption text-fg-disabled font-korean mt-1">
IMO Salvage Manual / Resistance Increase Ratio
</div>
</div>
{/* 유출량 추정 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
(Oil Outflow Estimation)
</div>
<div className="grid grid-cols-3 gap-1 text-caption font-mono">
{[
{ label: '현재 유출률', value: d.oilRate },
{ label: '누적 유출량', value: '6.8 kL' },
{ label: '24h 예측', value: '145 kL' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-card rounded-sm text-center">
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
<br />
<b className="text-fg-sub">{r.value}</b>
</div>
))}
</div>
<div className="mt-1 h-1 bg-bg-surface-hover rounded-sm">
<div
className="h-full rounded-sm"
style={{
width: '68%',
background: 'var(--color-danger)',
}}
/>
</div>
<div className="text-caption text-fg-disabled font-korean mt-0.5">
연료유: 210 kL | 잔량: 68%
</div>
</div>
{/* CBR 사례기반 추론 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
CBR (Case-Based Reasoning)
</div>
<div className="flex flex-col gap-0.5">
{[
{ pct: '94%', name: 'Hebei Spirit (2007)', desc: '태안 · 충돌 · 원유 12,547kL 유출' },
{ pct: '87%', name: 'Sea Empress (1996)', desc: '밀포드 · 좌초 · 72,000t 유출' },
{ pct: '82%', name: 'Rena (2011)', desc: '타우랑가 · 좌초 · 350t HFO 유출' },
].map((c, i) => (
<div
key={i}
className="flex items-center gap-1.5 px-1.5 py-1 rounded-sm cursor-pointer hover:brightness-125 bg-bg-card border border-stroke"
>
<div className="w-7 h-4 rounded-sm flex items-center justify-center text-caption font-bold font-mono text-fg-sub bg-bg-surface-hover">
{c.pct}
</div>
<div className="flex-1 text-caption font-korean">
<b className="text-fg-sub">{c.name}</b>
<br />
<span className="text-fg-disabled">{c.desc}</span>
</div>
</div>
))}
</div>
</div>
{/* 위험도 평가 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
2
</div>
<div className="flex flex-col gap-0.5 text-caption font-korean">
{[
{ label: '침수 확대 → 전복', level: 'HIGH', danger: true },
{ label: '유류 대량 유출 → 해양오염', level: 'HIGH', danger: true },
{ label: '선체 절단 (BM 초과)', level: 'MED', danger: true },
{ label: '화재/폭발', level: 'LOW', danger: false },
].map((r, i) => (
<div key={i} className="flex items-center justify-between px-1.5 py-0.5 rounded-sm">
<span className="text-fg-sub">{r.label}</span>
<span
className={`px-1.5 py-px rounded-lg text-caption font-bold ${r.danger ? 'text-color-danger' : 'text-fg-disabled'}`}
>
{r.level}
</span>
</div>
))}
</div>
</div>
{/* 해상 e-Call */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
e-Call (GMDSS / VHF-DSC)
</div>
<div className="flex flex-col gap-0.5 text-caption font-mono text-fg-disabled">
{[
{ label: 'MMSI', value: '440123456', danger: false },
{ label: 'Nature of Distress', value: 'COLLISION', danger: true },
{ label: 'DSC Alert', value: 'SENT ✓', danger: false },
{ label: 'EPIRB', value: 'ACTIVATED ✓', danger: false },
{ label: 'VTS 인천', value: 'ACK 10:36', danger: false },
].map((r, i) => (
<div key={i} className="flex justify-between">
<span className="font-korean">{r.label}</span>
<b className={r.danger ? 'text-color-danger' : 'text-fg-sub'}>{r.value}</b>
</div>
))}
</div>
<button className="w-full mt-1 py-1 rounded text-caption font-bold cursor-pointer font-korean bg-bg-card border border-stroke text-fg-sub">
📡 DISTRESS RELAY
</button>
</div>
</div>
);
}
/* ─── 손상 복원성 패널 ─── */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function DamageStabilityPanel(_props: { activeType: AccidentType }) {
return (
<div className="flex flex-col p-2.5 gap-2">
<div className="text-label-1 font-bold text-fg font-korean">
(DAMAGE STABILITY)
</div>
<div className="text-caption text-fg-disabled font-korean leading-snug">
<br />
IMO A.749(18) / SOLAS Ch.II-1
</div>
{/* GZ Curve SVG */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
GZ (Righting Lever Curve)
</div>
<svg viewBox="0 0 260 120" className="w-full" style={{ height: '100px' }}>
<line x1="30" y1="10" x2="30" y2="100" stroke="var(--stroke-default)" strokeWidth=".5" />
<line
x1="30"
y1="100"
x2="255"
y2="100"
stroke="var(--stroke-default)"
strokeWidth=".5"
/>
<line
x1="30"
y1="55"
x2="255"
y2="55"
stroke="var(--stroke-default)"
strokeWidth=".3"
strokeDasharray="3,3"
/>
<line
x1="30"
y1="32"
x2="255"
y2="32"
stroke="var(--stroke-default)"
strokeWidth=".3"
strokeDasharray="3,3"
/>
<polyline
points="30,100 55,80 80,55 105,38 130,30 155,32 180,42 205,60 230,85 250,100"
fill="none"
stroke="var(--color-info)"
strokeWidth="1.5"
/>
<text x="135" y="25" fill="var(--color-info)" fontSize="5" textAnchor="middle">
GZ
</text>
<polyline
points="30,100 55,88 80,72 105,62 130,58 155,62 180,75 205,92 220,100"
fill="none"
stroke="var(--color-danger)"
strokeWidth="1.5"
/>
<text x="140" y="54" fill="var(--color-danger)" fontSize="5" textAnchor="middle">
GZ
</text>
<line
x1="30"
y1="78"
x2="255"
y2="78"
stroke="var(--color-danger)"
strokeWidth=".6"
strokeDasharray="4,2"
/>
<text
x="240"
y="76"
fill="var(--color-danger)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
IMO MIN
</text>
<text x="5" y="14" fill="var(--fg-disabled)" fontSize="5" fontFamily="var(--font-mono)">
GZ(m)
</text>
<text x="5" y="35" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
0.6
</text>
<text x="5" y="58" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
0.4
</text>
<text x="5" y="82" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
0.2
</text>
<text
x="5"
y="104"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
0
</text>
<text
x="30"
y="112"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
0°
</text>
<text
x="80"
y="112"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
15°
</text>
<text
x="130"
y="112"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
30°
</text>
<text
x="180"
y="112"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
45°
</text>
<text
x="230"
y="112"
fill="var(--fg-disabled)"
fontSize="4.5"
fontFamily="var(--font-mono)"
>
60°
</text>
</svg>
</div>
{/* 복원성 지표 */}
<div className="grid grid-cols-2 gap-1.5">
<MetricCard
label="GZ_max (최대복원력)"
value="0.25"
unit="m"
color="var(--color-danger)"
sub="기준: 0.2m 이상 ⚠"
/>
<MetricCard
label="θ_max (최대복원각)"
value="28"
unit="°"
color="var(--color-danger)"
sub="기준: 25° 이상 ✓"
/>
</div>
<div className="grid grid-cols-2 gap-1.5">
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean"> </div>
<div className="text-title-3 font-bold text-fg-sub font-mono">
2 <span className="text-caption"></span>
</div>
<div className="text-caption text-fg-disabled font-korean">#1 / #3 </div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">Margin Line </div>
<div className="text-title-3 font-bold text-fg-sub font-mono">
0.12 <span className="text-caption">m</span>
</div>
<div className="text-caption text-fg-disabled font-korean"> </div>
</div>
</div>
{/* SOLAS 판정 */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-label-2 font-bold text-color-danger font-korean mb-1">
SOLAS 판정: 부적합 (FAIL)
</div>
<div className="text-caption text-fg-disabled font-korean leading-snug">
· Area(0~θ_f): 0.028 m·rad ( 0.015 )
<br />
· GZ_max 0.1m: 0.25m | θ_max 15°: 28° <br />·{' '}
<span className="text-color-danger">
Margin Line 침수: 0.12m
</span>
</div>
</div>
{/* 좌초시 복원성 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1">
(Grounded Stability)
</div>
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
{[
{ label: '지반반력', value: '1,240 kN' },
{ label: '접촉 면적', value: '12.5 m²' },
{ label: '제거력(Removal)', value: '1,850 kN' },
{ label: '좌초 GM', value: '0.65 m' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-card rounded-sm">
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
<br />
<b className="text-fg-sub">{r.value}</b>
</div>
))}
</div>
</div>
{/* 탱크 상태 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption font-bold text-fg-sub font-korean mb-1">
(Tank Volume Status)
</div>
<div className="flex flex-col gap-0.5 text-caption font-korean">
{[
{ name: '#1 FP Tank', pct: 100, status: '침수', danger: true },
{ name: '#3 Port Tank', pct: 85, status: '85%', danger: true },
{ name: '#2 DB Tank', pct: 45, status: '45%', danger: false },
{ name: 'Ballast #4', pct: 72, status: '72%', danger: false },
{ name: 'Fuel Oil #5', pct: 68, status: '68%', danger: false },
].map((t, i) => (
<div key={i} className="flex items-center gap-1">
<div
className={`w-2 h-2 rounded-sm flex-shrink-0 ${t.danger ? 'bg-color-danger' : 'bg-color-info'}`}
/>
<span className="w-[65px] text-fg-disabled">{t.name}</span>
<div className="flex-1 h-1 bg-bg-surface-hover rounded-sm">
<div
className={`h-full rounded-sm ${t.danger ? 'bg-color-danger' : 'bg-color-info'}`}
style={{ width: `${t.pct}%` }}
/>
</div>
<span
className={`min-w-[35px] text-right ${t.danger ? 'text-fg-sub' : 'text-fg-disabled'}`}
>
{t.status}
</span>
</div>
))}
</div>
</div>
</div>
);
}
/* ─── 종강도 패널 ─── */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function LongStrengthPanel(_props: { activeType: AccidentType }) {
return (
<div className="flex flex-col p-2.5 gap-2">
<div className="text-label-1 font-bold text-fg font-korean">
(LONGITUDINAL STRENGTH)
</div>
<div className="text-caption text-fg-disabled font-korean leading-snug">
<br />
IACS CSR / Classification Society
</div>
{/* 전단력 분포 SVG */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
(Shear Force Distribution)
</div>
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
<line x1="25" y1="45" x2="255" y2="45" stroke="var(--stroke-default)" strokeWidth=".5" />
<line x1="25" y1="10" x2="25" y2="80" stroke="var(--stroke-default)" strokeWidth=".5" />
<line
x1="25"
y1="18"
x2="255"
y2="18"
stroke="color-mix(in srgb, var(--color-danger) 25%, transparent)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<line
x1="25"
y1="72"
x2="255"
y2="72"
stroke="color-mix(in srgb, var(--color-danger) 25%, transparent)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<text
x="240"
y="15"
fill="color-mix(in srgb, var(--color-danger) 40%, transparent)"
fontSize="4"
>
LIMIT
</text>
<polyline
points="25,45 50,38 75,28 100,22 120,20 140,25 160,35 180,45 200,52 220,58 240,55 255,50"
fill="none"
stroke="var(--color-info)"
strokeWidth="1.5"
/>
<polyline
points="25,45 50,35 75,22 100,16 120,14 140,18 160,30 180,45 200,56 220,64 240,60 255,55"
fill="none"
stroke="var(--color-danger)"
strokeWidth="1.2"
strokeDasharray="3,2"
/>
<text x="80" y="12" fill="var(--color-danger)" fontSize="4.5">
SF
</text>
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
+SF
</text>
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
0
</text>
<text x="2" y="78" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
-SF
</text>
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
AP
</text>
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
MID
</text>
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
FP
</text>
</svg>
</div>
{/* 굽힘모멘트 분포 SVG */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-caption font-bold text-fg-sub font-korean mb-1.5">
(Bending Moment Distribution)
</div>
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
<line x1="25" y1="45" x2="255" y2="45" stroke="var(--stroke-default)" strokeWidth=".5" />
<line x1="25" y1="10" x2="25" y2="80" stroke="var(--stroke-default)" strokeWidth=".5" />
<line
x1="25"
y1="15"
x2="255"
y2="15"
stroke="color-mix(in srgb, var(--color-danger) 25%, transparent)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<text
x="240"
y="12"
fill="color-mix(in srgb, var(--color-danger) 40%, transparent)"
fontSize="4"
>
LIMIT
</text>
<polyline
points="25,45 50,40 75,32 100,22 130,18 160,22 190,32 220,40 255,45"
fill="none"
stroke="var(--color-info)"
strokeWidth="1.5"
/>
<polyline
points="25,45 50,38 75,26 100,16 130,12 160,16 190,28 220,38 255,45"
fill="none"
stroke="var(--color-danger)"
strokeWidth="1.2"
strokeDasharray="3,2"
/>
<text x="115" y="8" fill="var(--color-danger)" fontSize="4.5">
BM
</text>
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
+BM
</text>
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
0
</text>
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
AP
</text>
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
MID
</text>
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
FP
</text>
</svg>
</div>
{/* 종강도 지표 */}
<div className="grid grid-cols-2 gap-1.5">
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">SF / </div>
<div className="text-title-3 font-bold text-fg-sub font-mono">
88<span className="text-caption">%</span>
</div>
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
<div className="h-full rounded-sm w-[88%] bg-color-danger" />
</div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">BM / </div>
<div className="text-title-3 font-bold text-fg-sub font-mono">
92<span className="text-caption">%</span>
</div>
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
<div className="h-full rounded-sm w-[92%] bg-color-danger" />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-1.5">
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">Section Modulus </div>
<div className="text-title-3 font-bold text-fg-sub font-mono">1.08</div>
<div className="text-caption text-fg-disabled font-korean">Req'd: 1.00 이상 ✓</div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">Hull Girder ULS</div>
<div className="text-title-3 font-bold text-fg-sub font-mono">1.12</div>
<div className="text-caption text-fg-disabled font-korean">Req'd: 1.10 </div>
</div>
</div>
{/* 판정 */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-label-2 font-bold text-color-danger font-korean mb-1">
판정: 주의 (CAUTION)
</div>
<div className="text-caption text-fg-disabled font-korean leading-snug">
· SF 최대: 허용치의 88% <span className="text-color-danger"> </span>
<br />· BM 최대: 허용치의 92% <span className="text-color-danger"> </span>
<br />
· Hogging
<br />· <span className="text-color-danger"> BM </span>
</div>
</div>
</div>
);
}
/* ─── 하단 바: 이벤트 로그 + 타임라인 ─── */
function BottomBar() {
const [isOpen, setIsOpen] = useState(true);
return (
<div className="border-t border-stroke flex flex-col bg-bg-base flex-shrink-0">
{/* 제목 바 — 항상 표시 */}
<div className="flex items-center px-3 py-1 border-b border-stroke flex-shrink-0">
<span className="text-caption font-bold text-fg-disabled font-korean">
/ (EVENT LOG / COMMUNICATION TRANSCRIPT)
</span>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex-1 text-center text-caption text-fg-disabled hover:text-fg cursor-pointer transition-colors"
>
{isOpen ? '▼' : '▲'}
</button>
<div className="flex items-center gap-1.5">
{['전체', '긴급', '통신'].map((label, i) => (
<button
key={i}
className="px-2 py-px rounded-sm text-caption cursor-pointer font-korean border border-stroke text-fg-disabled hover:text-fg"
>
{label}
</button>
))}
</div>
</div>
{/* 이벤트 로그 내용 — 토글 */}
{isOpen && (
<div className="h-[120px] overflow-y-auto px-3 py-1 font-mono text-caption leading-[1.7] scrollbar-thin">
{[
{ time: '10:35', msg: 'SOS FROM M/V SEA GUARDIAN', important: true },
{ time: '10:35', msg: 'OIL LEAK DETECTED SENSOR #3', important: false },
{ time: '10:40', msg: 'CG HELO DISPATCHED', important: true },
{ time: '10:41', msg: 'GM CRITICAL ALERT — DAMAGE STABILITY FAIL', important: true },
{ time: '10:42', msg: 'Coast Guard 123 en route — ETA 15 min', important: false },
{
time: '10:43',
msg: 'LONGITUDINAL STRENGTH WARNING — BM 92% of LIMIT',
important: false,
},
{
time: '10:45',
msg: 'BALLAST TRANSFER INITIATED — PORT #2 → STBD #3',
important: false,
},
{ time: '10:48', msg: 'LIST INCREASING — 12° → 15°', important: false },
{ time: '10:50', msg: 'RESCUE HELO ON SCENE — HOISTING OPS', important: false },
{ time: '10:55', msg: '5 SURVIVORS RECOVERED BY HELO', important: true },
].map((e, i) => (
<div key={i}>
<span className="text-fg-disabled">[{e.time}]</span>{' '}
<span className={e.important ? 'text-color-accent' : 'text-fg'}>{e.msg}</span>
</div>
))}
</div>
)}
</div>
);
}
/* ─── 공통 메트릭 카드 ─── */
function MetricCard({
label,
value,
unit,
color,
sub,
}: {
label: string;
value: string;
unit: string;
color: string;
sub: string;
}) {
return (
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-caption text-fg-disabled font-korean">{label}</div>
<div className="text-title-3 font-bold font-mono" style={{ color }}>
{value}
<span className="text-caption"> {unit}</span>
</div>
<div className="text-caption font-korean text-fg-disabled">{sub}</div>
</div>
);
}
/* ─── 긴급구난 목록 탭 ─── */
function RescueListView() {
const [opsList, setOpsList] = useState<RescueOpsItem[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const loadOps = useCallback(async () => {
setLoading(true);
try {
const items = await fetchRescueOps({ search: searchTerm || undefined });
setOpsList(items);
} catch (err) {
console.error('[rescue] 구난 작전 목록 조회 실패:', err);
} finally {
setLoading(false);
}
}, [searchTerm]);
useEffect(() => {
loadOps();
}, [loadOps]);
const getStatusLabel = (sttsCd: string) => {
switch (sttsCd) {
case 'ACTIVE':
return { label: '대응중', color: 'var(--color-danger)' };
case 'STANDBY':
return { label: '대기', color: 'var(--color-warning)' };
case 'COMPLETED':
return { label: '종료', color: 'var(--color-success)' };
default:
return { label: sttsCd, color: 'var(--fg-disabled)' };
}
};
const getTypeLabel = (tpCd: string) => {
const map: Record<string, string> = {
collision: '충돌',
grounding: '좌초',
turning: '선회',
capsizing: '전복',
sharpTurn: '급선회',
flooding: '침수',
sinking: '침몰',
};
return map[tpCd] ?? tpCd;
};
return (
<div className="flex flex-col flex-1 overflow-hidden">
<div className="px-5 py-4 flex items-center justify-between border-b border-stroke">
<span className="text-title-3 font-bold font-korean"> </span>
<div className="flex gap-2 items-center">
<input
type="text"
placeholder="선박명 / 사고번호 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-md text-fg-sub font-korean text-label-2 w-[200px] outline-none focus:border-[var(--color-accent)]"
/>
<button
className="rounded-sm text-label-2 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 font-korean"
style={{
border: '1px solid rgba(6,182,212,.3)',
background: 'rgba(6,182,212,.08)',
}}
>
+
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto px-5 pb-4">
{loading ? (
<div className="text-center py-20 text-fg-disabled text-title-3"> ...</div>
) : opsList.length === 0 ? (
<div className="text-center py-20 text-fg-disabled text-title-3">
.
</div>
) : (
<table className="w-full border-collapse text-label-2 mt-3">
<thead>
<tr className="bg-bg-card border-b border-stroke">
{['상태', '사고번호', '선박명', '사고유형', '발생일시', '위치', '인명'].map((h) => (
<th
key={h}
className="py-2 px-2.5 text-left font-korean font-semibold text-fg-disabled text-label-2"
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{opsList.map((r) => {
const status = getStatusLabel(r.sttsCd);
return (
<tr
key={r.rescueOpsSn}
className="border-b border-stroke hover:bg-bg-surface-hover cursor-pointer"
>
<td className="py-2 px-2.5">
<span
className="px-2 py-0.5 rounded-xl text-caption font-bold"
style={{
background: `color-mix(in srgb, ${status.color} 15%, transparent)`,
color: status.color,
}}
>
{status.label}
</span>
</td>
<td className="py-2 px-2.5 font-mono text-color-accent font-semibold">
{r.opsCd}
</td>
<td className="py-2 px-2.5 font-korean text-fg font-semibold">{r.vesselNm}</td>
<td className="py-2 px-2.5 font-korean">{getTypeLabel(r.acdntTpCd)}</td>
<td className="py-2 px-2.5 font-mono text-fg-disabled">
{r.regDtm ? new Date(r.regDtm).toLocaleString('ko-KR') : '—'}
</td>
<td className="py-2 px-2.5 font-mono text-fg-disabled text-label-2">
{r.locDc ?? '—'}
</td>
<td className="py-2 px-2.5 font-mono">
{r.survivors ?? 0}/{r.totalCrew ?? 0}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</div>
);
}
/* ═══ 메인 RescueView ═══ */
export function RescueView() {
const { activeSubTab } = useSubMenu('rescue');
const [activeType, setActiveType] = useState<AccidentType>('collision');
const [activeAnalysis, setActiveAnalysis] = useState<AnalysisTab>('rescue');
if (activeSubTab === 'list') {
return (
<div className="flex flex-1 overflow-hidden">
<RescueListView />
</div>
);
}
if (activeSubTab === 'scenario') {
return <RescueScenarioView />;
}
if (activeSubTab === 'theory') {
return <RescueTheoryView />;
}
return (
<div className="flex flex-col flex-1 overflow-hidden">
{/* 상단 사고 정보바 */}
<TopInfoBar activeType={activeType} />
{/* 3단 레이아웃: 사고유형 | 지도 | 분석 패널 */}
<div className="flex flex-1 overflow-hidden">
<LeftPanel activeType={activeType} onTypeChange={setActiveType} />
<CenterMap activeType={activeType} />
<RightPanel
activeAnalysis={activeAnalysis}
onAnalysisChange={setActiveAnalysis}
activeType={activeType}
/>
</div>
{/* 하단: 이벤트 로그 + 타임라인 */}
<BottomBar />
</div>
);
}