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

1645 lines
62 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 { 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-gradient-to-r from-bg-0 to-bg-2 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-sm"></span>
<span className="text-[12px] font-bold text-fg font-korean"></span>
</div>
<div className="px-3.5 py-0.5 bg-[rgba(239,68,68,0.15)] border border-[rgba(239,68,68,0.35)] rounded-xl text-[10px] font-bold text-color-danger font-korean">
: {d.incident}
</div>
<div className="flex gap-3 text-[9px] font-mono text-fg-disabled ml-auto">
<span>
: <b className="text-fg">{d.survivors}</b>/{d.total}
</span>
<span>
: <b className="text-color-danger">{d.missing}</b>
</span>
<span>
GM: <b style={{ color: gmColor(d.gm) }}>{d.gm}m</b>
</span>
<span>
: <b style={{ color: listColor(d.list) }}>{d.list}°</b>
</span>
<span>
: <b className="text-color-warning">{d.oilRate}</b>
</span>
</div>
<div className="text-[13px] font-bold text-fg font-mono">{clock}</div>
<div className="text-[9px] 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-[9px] font-bold text-[var(--color-info)] 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-[12px]">{t.icon}</span>
<div>
<div
className={`text-[10px] font-bold font-korean ${activeType === t.id ? 'text-[var(--color-accent)]' : 'text-fg'}`}
>
{t.label} ({t.eng})
</div>
<div className="text-[7px] text-fg-disabled font-korean mt-px">{t.desc}</div>
</div>
</button>
))}
{/* 긴급 경고 */}
<div className="text-[9px] font-bold text-fg-disabled font-korean mt-2.5 mb-1">
(CRITICAL ALERTS)
</div>
<div className="py-1.5 px-2.5 bg-[rgba(239,68,68,0.15)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-[9px] font-bold text-color-danger font-korean">
GM
</div>
<div className="py-1.5 px-2.5 bg-[rgba(239,68,68,0.15)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-[9px] font-bold text-color-danger font-korean">
5
</div>
<div className="py-1.5 px-2.5 bg-[rgba(249,115,22,0.12)] border-l-[3px] border-l-[var(--color-warning)] rounded-r text-[9px] font-bold text-color-warning font-korean">
-
</div>
<div className="py-1.5 px-2.5 bg-[rgba(251,191,36,0.1)] border-l-[3px] border-l-[var(--color-caution)] rounded-r text-[9px] font-bold text-color-caution 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-[9px] text-[rgba(255,60,60,0.35)] font-mono">CCTV FEED #1</div>
<div className="absolute top-1 left-1.5 text-[7px] text-[rgba(255,60,60,0.5)] font-mono">
REC
</div>
</div>
</div>
);
}
/* ─── 중앙 지도 영역 ─── */
function CenterMap({ activeType }: { activeType: AccidentType }) {
const d = rscTypeData[activeType];
const at = accidentTypes.find((t) => t.id === 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-[9px] text-fg-disabled">
<div className="text-[10px] 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-[#888] rounded-[1px]"
style={{ top: '-3px', left: '60%' }}
/>
</div>
<div className="text-[7px] 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-[8px] 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-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">
ETA 5 MIN
</div>
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">
ETA 15 MIN
</div>
<div className="absolute z-[12] text-sm opacity-60 top-[7%] left-[52%] -rotate-[30deg]">
🚁
</div>
<div className="absolute z-[12] text-[8px] font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">
6M
</div>
<div className="absolute z-[12] text-[11px] 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-[9px] 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-[12px] 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-[8px] 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-[8px] text-fg-disabled font-korean"> </div>
<div className="text-[11px] font-bold font-korean text-color-accent">
{at.icon} {at.label} ({at.eng})
</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-[9px] font-semibold font-korean cursor-pointer text-center transition-all border-b-2 ${
activeAnalysis === tab.id
? 'text-[var(--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-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
💾
</button>
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-color-warning font-korean">
🔄
</button>
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean">
📄
</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-[12px] font-bold text-fg font-korean"> (RESCUE ANALYSIS)</div>
<div className="text-[9px] text-[var(--color-info)] font-korean px-2 py-1 bg-[rgba(88,166,255,0.08)] border border-[rgba(88,166,255,0.2)] rounded">
📌 : {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-[7px] 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="rgba(239,68,68,.12)"
stroke="rgba(239,68,68,.35)"
strokeWidth=".6"
/>
<text
x="57"
y="39"
fill="rgba(239,68,68,.5)"
fontSize="4.5"
textAnchor="middle"
fontFamily="monospace"
>
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="monospace"
>
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="monospace">
WL
</text>
<line
x1="130"
y1="8"
x2="130"
y2="60"
stroke="rgba(251,191,36,.15)"
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 이상)`}
subColor={gmColor(d.gm)}
/>
<MetricCard
label="LIST (횡경사)"
value={d.list}
unit="°"
color={listColor(d.list)}
sub={`${parseFloat(d.list) > 20 ? '위험' : parseFloat(d.list) > 10 ? '주의' : '정상'} (기준: 10° 이내)`}
subColor={listColor(d.list)}
/>
</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)"
subColor={trimColor(d.trim)}
/>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[8px] text-fg-disabled font-korean">
(Reserve Buoyancy)
</div>
<div className="text-xl font-bold font-mono" style={{ color: buoyColor(d.buoy) }}>
{d.buoy}
<span className="text-[10px]">%</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-[10px] font-bold text-fg-disabled font-korean">
(EMERGENCY ACTIONS)
</div>
<div className="grid grid-cols-2 gap-1.5">
{[
{ en: 'BALLAST INJECT', ko: '밸러스트 주입', color: 'var(--color-danger)' },
{ en: 'BALLAST DISCHARGE', ko: '밸러스트 배출', color: 'var(--color-danger)' },
{ en: 'ENGINE STOP', ko: '기관 정지', color: 'var(--color-warning)' },
{ en: 'ANCHOR DROP', ko: '묘 투하', color: 'var(--color-warning)' },
].map((btn, i) => (
<button
key={i}
className="py-[7px] rounded-[5px] text-center cursor-pointer transition-all hover:brightness-125"
style={{
background: `color-mix(in srgb, ${btn.color} 8%, transparent)`,
border: `1px solid color-mix(in srgb, ${btn.color} 25%, transparent)`,
}}
>
<div className="text-[10px] font-bold text-fg font-mono">{btn.en}</div>
<div className="text-[8px] font-korean" style={{ color: btn.color }}>
{btn.ko}
</div>
</button>
))}
</div>
{/* 구난 의사결정 프로세스 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
(KRISO Decision Support)
</div>
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
<div className="flex items-center gap-1">
{[
{ label: '① 상태평가', color: 'var(--color-accent)' },
{ label: '② 사례분석', color: 'var(--color-info)' },
{ label: '③ 장비선정', color: 'var(--fg-sub)' },
].map((s, i) => (
<>
{i > 0 && <span className="text-fg-disabled"></span>}
<div
key={i}
className="px-1.5 py-0.5 rounded-sm text-center flex-shrink-0"
style={{
background: `color-mix(in srgb, ${s.color} 12%, transparent)`,
border: `1px solid color-mix(in srgb, ${s.color} 25%, transparent)`,
color: s.color,
minWidth: '68px',
}}
>
{s.label}
</div>
</>
))}
</div>
<div className="flex items-center gap-1">
{[
{ label: '④ 예인력', color: 'var(--color-caution)' },
{ label: '⑤ 이초/인양', color: 'var(--color-warning)' },
{ label: '⑥ 유출량', color: 'var(--color-danger)' },
].map((s, i) => (
<>
{i > 0 && <span className="text-fg-disabled"></span>}
<div
key={i}
className="px-1.5 py-0.5 rounded-sm text-center flex-shrink-0"
style={{
background: `color-mix(in srgb, ${s.color} 10%, transparent)`,
border: `1px solid color-mix(in srgb, ${s.color} 25%, transparent)`,
color: s.color,
minWidth: '68px',
}}
>
{s.label}
</div>
</>
))}
</div>
</div>
</div>
{/* 유체 정역학 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
(Hydrostatics)
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] 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-base rounded-sm">
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
<br />
<b className="text-fg">{r.value}</b>
</div>
))}
</div>
</div>
{/* 예인력/이초력 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
/ (Towing & Refloating)
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
{[
{ label: '필요 예인력', value: '285 kN', color: 'var(--color-caution)' },
{ label: '비상 예인력', value: '420 kN', color: 'var(--color-danger)' },
{ label: '이초 반력', value: '1,850 kN', color: 'var(--color-warning)' },
{ label: '인양 안전성', value: 'FAIL', color: 'var(--color-danger)' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
<br />
<b style={{ color: r.color }}>{r.value}</b>
</div>
))}
</div>
<div className="text-[7px] 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-[9px] font-bold text-fg font-korean mb-1.5">
(Oil Outflow Estimation)
</div>
<div className="grid grid-cols-3 gap-1 text-[8px] font-mono">
{[
{ label: '현재 유출률', value: d.oilRate, color: 'var(--color-warning)' },
{ label: '누적 유출량', value: '6.8 kL', color: 'var(--color-danger)' },
{ label: '24h 예측', value: '145 kL', color: 'var(--color-danger)' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm text-center">
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
<br />
<b style={{ color: r.color }}>{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: 'linear-gradient(90deg, var(--color-warning), var(--color-danger))',
}}
/>
</div>
<div className="text-[7px] 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-[9px] font-bold text-fg 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 유출',
color: 'var(--color-accent)',
},
{
pct: '87%',
name: 'Sea Empress (1996)',
desc: '밀포드 · 좌초 · 72,000t 유출',
color: 'var(--color-info)',
},
{
pct: '82%',
name: 'Rena (2011)',
desc: '타우랑가 · 좌초 · 350t HFO 유출',
color: 'var(--fg-sub)',
},
].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"
style={{
background: `color-mix(in srgb, ${c.color} 6%, transparent)`,
border: `1px solid color-mix(in srgb, ${c.color} 15%, transparent)`,
}}
>
<div
className="w-7 h-4 rounded-sm flex items-center justify-center text-[7px] font-bold font-mono"
style={{
background: `color-mix(in srgb, ${c.color} 20%, transparent)`,
color: c.color,
}}
>
{c.pct}
</div>
<div className="flex-1 text-[7px] font-korean">
<b className="text-fg">{c.name}</b>
<br />
<span className="text-fg-disabled">{c.desc}</span>
</div>
</div>
))}
</div>
</div>
{/* 위험도 평가 */}
<div className="bg-[rgba(239,68,68,0.04)] border border-[rgba(239,68,68,0.15)] rounded-md p-2">
<div className="text-[9px] font-bold text-color-danger font-korean mb-1.5">
2
</div>
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
{[
{ label: '침수 확대 → 전복', level: 'HIGH', color: 'var(--color-danger)' },
{ label: '유류 대량 유출 → 해양오염', level: 'HIGH', color: 'var(--color-warning)' },
{ label: '선체 절단 (BM 초과)', level: 'MED', color: 'var(--color-caution)' },
{ label: '화재/폭발', level: 'LOW', color: 'var(--fg-sub)' },
].map((r, i) => (
<div
key={i}
className="flex items-center justify-between px-1.5 py-0.5 rounded-sm"
style={{
background: `color-mix(in srgb, ${r.color} 8%, transparent)`,
}}
>
<span className="text-fg">{r.label}</span>
<span
className="px-1.5 py-px rounded-lg text-[7px] font-bold"
style={{
background: `color-mix(in srgb, ${r.color} 20%, transparent)`,
color: r.color,
}}
>
{r.level}
</span>
</div>
))}
</div>
</div>
{/* 해상 e-Call */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
e-Call (GMDSS / VHF-DSC)
</div>
<div className="flex flex-col gap-0.5 text-[7px] font-mono text-fg-disabled">
{[
{ label: 'MMSI', value: '440123456' },
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--color-danger)' },
{ label: 'DSC Alert', value: 'SENT ✓', color: 'var(--color-success)' },
{ label: 'EPIRB', value: 'ACTIVATED ✓', color: 'var(--color-success)' },
{ label: 'VTS 인천', value: 'ACK 10:36', color: 'var(--color-info)' },
].map((r, i) => (
<div key={i} className="flex justify-between">
<span className="font-korean">{r.label}</span>
<b style={{ color: r.color }}>{r.value}</b>
</div>
))}
</div>
<button className="w-full mt-1 py-1 bg-[rgba(239,68,68,0.12)] border border-[rgba(239,68,68,0.3)] rounded text-[8px] font-bold text-color-danger cursor-pointer font-korean hover:bg-[rgba(239,68,68,0.2)]">
📡 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-[12px] font-bold text-fg font-korean">
(DAMAGE STABILITY)
</div>
<div className="text-[9px] 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-[9px] font-bold text-fg 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="rgba(251,191,36,.4)"
strokeWidth=".6"
strokeDasharray="4,2"
/>
<text x="240" y="76" fill="var(--color-caution)" fontSize="4.5" fontFamily="monospace">
IMO MIN
</text>
<text x="5" y="14" fill="var(--fg-disabled)" fontSize="5" fontFamily="monospace">
GZ(m)
</text>
<text x="5" y="35" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
0.6
</text>
<text x="5" y="58" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
0.4
</text>
<text x="5" y="82" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
0.2
</text>
<text x="5" y="104" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
0
</text>
<text x="30" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
0°
</text>
<text x="80" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
15°
</text>
<text x="130" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
30°
</text>
<text x="180" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
45°
</text>
<text x="230" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
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 이상 ⚠"
subColor="var(--color-danger)"
/>
<MetricCard
label="θ_max (최대복원각)"
value="28"
unit="°"
color="var(--color-caution)"
sub="기준: 25° 이상 ✓"
subColor="var(--color-caution)"
/>
</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-[8px] text-fg-disabled font-korean"> </div>
<div className="text-lg font-bold text-color-danger font-mono">
2 <span className="text-[9px]"></span>
</div>
<div className="text-[7px] text-fg-disabled font-korean">#1 / #3 </div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[8px] text-fg-disabled font-korean">Margin Line </div>
<div className="text-lg font-bold text-color-danger font-mono">
0.12 <span className="text-[9px]">m</span>
</div>
<div className="text-[7px] text-color-danger font-korean"> </div>
</div>
</div>
{/* SOLAS 판정 */}
<div className="bg-[rgba(239,68,68,0.06)] border border-[rgba(239,68,68,0.2)] rounded-md p-2.5">
<div className="text-[10px] font-bold text-color-danger font-korean mb-1">
SOLAS 판정: 부적합 (FAIL)
</div>
<div className="text-[8px] 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-[9px] font-bold text-fg font-korean mb-1">
(Grounded Stability)
</div>
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
{[
{ label: '지반반력', value: '1,240 kN', color: 'var(--color-caution)' },
{ label: '접촉 면적', value: '12.5 m²' },
{ label: '제거력(Removal)', value: '1,850 kN', color: 'var(--color-danger)' },
{ label: '좌초 GM', value: '0.65 m', color: 'var(--color-caution)' },
].map((r, i) => (
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
<br />
<b style={{ color: r.color }}>{r.value}</b>
</div>
))}
</div>
</div>
{/* 탱크 상태 */}
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[9px] font-bold text-fg font-korean mb-1">
(Tank Volume Status)
</div>
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
{[
{ name: '#1 FP Tank', pct: 100, status: '침수', color: 'var(--color-danger)' },
{ name: '#3 Port Tank', pct: 85, status: '85%', color: 'var(--color-danger)' },
{ name: '#2 DB Tank', pct: 45, status: '45%', color: 'var(--color-success)' },
{ name: 'Ballast #4', pct: 72, status: '72%', color: 'var(--color-info)' },
{ name: 'Fuel Oil #5', pct: 68, status: '68%', color: 'var(--color-warning)' },
].map((t, i) => (
<div key={i} className="flex items-center gap-1">
<div className="w-2 h-2 rounded-sm flex-shrink-0" style={{ background: t.color }} />
<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"
style={{ width: `${t.pct}%`, background: t.color }}
/>
</div>
<span className="min-w-[35px] text-right" style={{ color: t.color }}>
{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-[12px] font-bold text-fg font-korean">
(LONGITUDINAL STRENGTH)
</div>
<div className="text-[9px] 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-[9px] font-bold text-fg 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="rgba(239,68,68,.25)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<line
x1="25"
y1="72"
x2="255"
y2="72"
stroke="rgba(239,68,68,.25)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<text x="240" y="15" fill="rgba(239,68,68,.4)" 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-accent)"
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="monospace">
+SF
</text>
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
0
</text>
<text x="2" y="78" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
-SF
</text>
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
AP
</text>
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
MID
</text>
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
FP
</text>
</svg>
</div>
{/* 굽힘모멘트 분포 SVG */}
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
<div className="text-[9px] font-bold text-fg 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="rgba(239,68,68,.25)"
strokeWidth=".5"
strokeDasharray="3,2"
/>
<text x="240" y="12" fill="rgba(239,68,68,.4)" 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-success)"
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-warning)"
strokeWidth="1.2"
strokeDasharray="3,2"
/>
<text x="115" y="8" fill="var(--color-warning)" fontSize="4.5">
BM
</text>
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
+BM
</text>
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
0
</text>
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
AP
</text>
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
MID
</text>
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
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-[8px] text-fg-disabled font-korean">SF / </div>
<div className="text-lg font-bold text-color-caution font-mono">
88<span className="text-[9px]">%</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-caution" />
</div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[8px] text-fg-disabled font-korean">BM / </div>
<div className="text-lg font-bold text-color-warning font-mono">
92<span className="text-[9px]">%</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-warning" />
</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-[8px] text-fg-disabled font-korean">Section Modulus </div>
<div className="text-lg font-bold text-color-success font-mono">1.08</div>
<div className="text-[7px] text-color-success font-korean">Req'd: 1.00 이상 ✓</div>
</div>
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[8px] text-fg-disabled font-korean">Hull Girder ULS</div>
<div className="text-lg font-bold text-color-caution font-mono">1.12</div>
<div className="text-[7px] text-color-caution font-korean">Req'd: 1.10 </div>
</div>
</div>
{/* 판정 */}
<div className="bg-[rgba(251,191,36,0.06)] border border-[rgba(251,191,36,0.2)] rounded-md p-2.5">
<div className="text-[10px] font-bold text-color-caution font-korean mb-1">
판정: 주의 (CAUTION)
</div>
<div className="text-[8px] text-fg-disabled font-korean leading-snug">
· SF 최대: 허용치의 88% <span className="text-color-caution"> </span>
<br />· BM 최대: 허용치의 92% <span className="text-color-warning"> </span>
<br />
· Hogging
<br />· <span className="text-color-danger"> BM </span>
</div>
</div>
</div>
);
}
/* ─── 하단 바: 이벤트 로그 + 타임라인 ─── */
function BottomBar() {
return (
<div className="h-[145px] min-h-[145px] border-t border-stroke flex bg-bg-base flex-shrink-0">
{/* 이벤트 로그 */}
<div className="flex-1 flex flex-col overflow-hidden border-r border-stroke">
<div className="flex items-center justify-between px-3 py-1 border-b border-stroke flex-shrink-0">
<span className="text-[9px] font-bold text-fg-disabled font-korean">
/ (EVENT LOG / COMMUNICATION TRANSCRIPT)
</span>
<div className="flex gap-0.5">
{[
{ label: '전체', color: 'var(--color-accent)' },
{ label: '긴급', color: 'var(--color-warning)' },
{ label: '통신', color: 'var(--color-info)' },
].map((f, i) => (
<button
key={i}
className="px-2 py-px rounded-sm text-[8px] font-bold cursor-pointer font-korean"
style={{
background: `color-mix(in srgb, ${f.color} 15%, transparent)`,
border: `1px solid color-mix(in srgb, ${f.color} 30%, transparent)`,
color: f.color,
}}
>
{f.label}
</button>
))}
</div>
</div>
<div className="flex-1 overflow-y-auto px-3 py-1 font-mono text-[9px] leading-[1.7] scrollbar-thin">
{[
{
time: '10:35',
msg: 'SOS FROM M/V SEA GUARDIAN',
color: 'var(--color-danger)',
bold: true,
},
{ time: '10:35', msg: 'OIL LEAK DETECTED SENSOR #3', bold: false },
{ time: '10:40', msg: 'CG HELO DISPATCHED', color: 'var(--color-danger)', bold: true },
{
time: '10:41',
msg: 'GM CRITICAL ALERT — DAMAGE STABILITY FAIL',
color: 'var(--color-danger)',
bold: true,
},
{
time: '10:42',
msg: 'Coast Guard 123 en route — ETA 15 min',
color: 'var(--color-info)',
bold: false,
},
{
time: '10:43',
msg: 'LONGITUDINAL STRENGTH WARNING — BM 92% of LIMIT',
color: 'var(--color-caution)',
bold: false,
},
{ time: '10:45', msg: 'BALLAST TRANSFER INITIATED — PORT #2 → STBD #3', bold: false },
{
time: '10:48',
msg: 'LIST INCREASING — 12° → 15°',
color: 'var(--color-caution)',
bold: false,
},
{
time: '10:50',
msg: 'RESCUE HELO ON SCENE — HOISTING OPS',
color: 'var(--color-info)',
bold: false,
},
{
time: '10:55',
msg: '5 SURVIVORS RECOVERED BY HELO',
color: 'var(--color-success)',
bold: false,
},
].map((e, i) => (
<div key={i}>
<span className="text-fg-disabled">[{e.time}]</span>{' '}
<span style={{ color: e.color, fontWeight: e.bold ? 700 : 400 }}>{e.msg}</span>
</div>
))}
</div>
</div>
{/* 타임라인 시뮬레이션 */}
<div className="w-[330px] min-w-[330px] flex flex-col px-3.5 py-2 gap-1.5">
<div className="text-[9px] font-bold text-fg font-korean">
(TIMELINE SIMULATION CONTROL)
</div>
<div className="flex items-center gap-1.5 text-[8px] text-fg-disabled font-mono">
<span>[-6h]</span>
<span className="flex-1 text-center font-bold text-fg">[CURRENT]</span>
<span>[+6H]</span>
<span>[+12H]</span>
<span>[+24H]</span>
</div>
<div className="relative h-1.5 bg-bg-surface-hover rounded-sm mx-1">
<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 justify-center gap-2 mt-0.5">
<button className="w-7 h-7 bg-bg-card border border-stroke rounded-full text-fg-disabled text-[11px] flex items-center justify-center cursor-pointer hover:text-fg">
</button>
<button className="w-[34px] h-[34px] bg-[rgba(6,182,212,0.15)] border border-[rgba(6,182,212,0.3)] rounded-full text-[var(--color-accent)] text-[13px] flex items-center justify-center cursor-pointer hover:brightness-125">
</button>
<button className="w-7 h-7 bg-bg-card border border-stroke rounded-full text-fg-disabled text-[11px] flex items-center justify-center cursor-pointer hover:text-fg">
</button>
</div>
<div className="text-center text-[8px] text-fg-disabled font-mono">
: <b className="text-color-accent">10:45 KST</b>
</div>
</div>
</div>
);
}
/* ─── 공통 메트릭 카드 ─── */
function MetricCard({
label,
value,
unit,
color,
sub,
subColor,
}: {
label: string;
value: string;
unit: string;
color: string;
sub: string;
subColor: string;
}) {
return (
<div className="bg-bg-card border border-stroke rounded-md p-2">
<div className="text-[8px] text-fg-disabled font-korean">{label}</div>
<div className="text-xl font-bold font-mono" style={{ color }}>
{value}
<span className="text-[10px]"> {unit}</span>
</div>
<div className="text-[8px] font-korean" style={{ color: subColor }}>
{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-sm 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-[11px] w-[200px] outline-none focus:border-[var(--color-accent)]"
/>
<button className="px-3.5 py-1.5 bg-[rgba(6,182,212,0.12)] border border-[rgba(6,182,212,0.3)] rounded-md text-[var(--color-accent)] text-[11px] font-semibold cursor-pointer font-korean hover:bg-[rgba(6,182,212,0.2)]">
+
</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-sm"> ...</div>
) : opsList.length === 0 ? (
<div className="text-center py-20 text-fg-disabled text-sm">
.
</div>
) : (
<table className="w-full border-collapse text-[11px] 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-[10px]"
>
{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-[9px] 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-[var(--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-[10px]">
{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>
);
}