- 11개 탭 디렉토리 생성: tabs/{prediction,hns,rescue,weather,incidents,aerial,board,reports,assets,scat,admin}/
- 51개 컴포넌트를 역할 기반(views/, analysis/, layout/) → 탭 기반(tabs/) 구조로 이동
- weather 탭에 전용 hooks/, services/ 포함
- incidents 탭에 전용 services/ 포함
- 공통 지도 컴포넌트(MapView, BacktrackReplay)를 common/components/map/으로 이동
- 각 탭에 index.ts 생성하여 View 컴포넌트 re-export
- App.tsx import를 @tabs/ alias 사용으로 변경
- 전체 import 경로 수정 (탭 내부 상대경로, 외부 @common/ alias)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
905 lines
52 KiB
TypeScript
Executable File
905 lines
52 KiB
TypeScript
Executable File
import { useState, useEffect } from 'react'
|
||
import { useSubMenu } from '@common/hooks/useSubMenu'
|
||
import { RescueTheoryView } from './RescueTheoryView'
|
||
import { RescueScenarioView } from './RescueScenarioView'
|
||
|
||
/* ─── 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(--red)' : n < 1.5 ? 'var(--yellow)' : 'var(--green)' }
|
||
function listColor(v: string) { const n = parseFloat(v); return n > 20 ? 'var(--red)' : n > 10 ? 'var(--yellow)' : 'var(--green)' }
|
||
function trimColor(v: string) { const n = parseFloat(v); return n > 3 ? 'var(--red)' : n > 1.5 ? 'var(--yellow)' : 'var(--green)' }
|
||
function buoyColor(v: number) { return v < 30 ? 'var(--red)' : v < 50 ? 'var(--yellow)' : 'var(--green)' }
|
||
|
||
/* ─── 상단 사고 정보바 ─── */
|
||
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-border 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-text-1 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-status-red font-korean">
|
||
사고: {d.incident}
|
||
</div>
|
||
<div className="flex gap-3 text-[9px] font-mono text-text-3 ml-auto">
|
||
<span>생존자: <b className="text-text-1">{d.survivors}</b>/{d.total}</span>
|
||
<span>실종: <b className="text-status-red">{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-status-orange">{d.oilRate}</b></span>
|
||
</div>
|
||
<div className="text-[13px] font-bold text-text-1 font-mono">{clock}</div>
|
||
<div className="text-[9px] text-text-3 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-0 border-r border-border flex flex-col overflow-y-auto scrollbar-thin p-2 gap-0.5">
|
||
{/* 사고유형 제목 */}
|
||
<div className="text-[9px] font-bold text-[var(--blue)] 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-3 border-border hover:border-[var(--bdL)] hover:bg-bg-hover'
|
||
}`}
|
||
>
|
||
<span className="text-[12px]">{t.icon}</span>
|
||
<div>
|
||
<div className={`text-[10px] font-bold font-korean ${activeType === t.id ? 'text-[var(--cyan)]' : 'text-text-1'}`}>
|
||
{t.label} ({t.eng})
|
||
</div>
|
||
<div className="text-[7px] text-text-3 font-korean mt-px">{t.desc}</div>
|
||
</div>
|
||
</button>
|
||
))}
|
||
|
||
{/* 긴급 경고 */}
|
||
<div className="text-[9px] font-bold text-text-3 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(--red)] rounded-r text-[9px] font-bold text-status-red 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(--red)] rounded-r text-[9px] font-bold text-status-red 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(--orange)] rounded-r text-[9px] font-bold text-status-orange font-korean">
|
||
유류 유출 감지 - 방제 필요
|
||
</div>
|
||
<div className="py-1.5 px-2.5 bg-[rgba(251,191,36,0.1)] border-l-[3px] border-l-[var(--yellow)] rounded-r text-[9px] font-bold text-status-yellow font-korean">
|
||
종강도 한계치 88% 접근
|
||
</div>
|
||
|
||
{/* CCTV 피드 */}
|
||
<div className="w-full aspect-[4/3] bg-[#1a0000] border border-border 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" style={{ background: '#0a1628' }}>
|
||
{/* 해양 배경 그라데이션 */}
|
||
<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-[rgba(13,17,23,0.92)] border border-border rounded-md px-3 py-2 font-mono text-[9px] text-text-3">
|
||
<div className="text-[10px] font-bold text-text-1 font-korean mb-1">사고 해역 정보</div>
|
||
<div className="grid gap-x-1.5 gap-y-px" style={{ gridTemplateColumns: '32px 1fr' }}>
|
||
<span>위치</span><b className="text-text-1">37°28'N, 126°15'E</b>
|
||
<span>수심</span><b className="text-text-1">45m</b>
|
||
<span>조류</span><b className="text-text-1">2.5 knots NE</b>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 선박 모형 */}
|
||
<div className="absolute z-[15]" style={{ top: '42%', left: '46%', transform: 'rotate(-15deg)' }}>
|
||
<div className="relative" style={{
|
||
width: '72px', height: '20px',
|
||
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" style={{ top: '-3px', left: '60%', width: '2px', height: '7px', background: '#888', borderRadius: '1px' }} />
|
||
</div>
|
||
<div className="text-[7px] text-center mt-0.5 font-mono" style={{ color: 'rgba(255,200,150,.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" style={{
|
||
top: '50%', left: '36%', color: 'rgba(6,182,212,.45)', whiteSpace: 'pre-line'
|
||
}}>
|
||
{d.zone.replace('\\n', '\n')}
|
||
</div>
|
||
|
||
{/* SAR 자산 */}
|
||
<div className="absolute z-10 text-[7px] font-mono" style={{ top: '10%', left: '42%', color: 'rgba(200,220,255,.35)' }}>ETA 5 MIN ─</div>
|
||
<div className="absolute z-10 text-[7px] font-mono" style={{ top: '14%', left: '56%', color: 'rgba(200,220,255,.35)' }}>ETA 15 MIN ─</div>
|
||
<div className="absolute z-[12] text-sm" style={{ top: '7%', left: '52%', opacity: 0.6, transform: 'rotate(-30deg)' }}>🚁</div>
|
||
<div className="absolute z-[12] text-[8px] font-mono" style={{ top: '20%', left: '60%', color: 'rgba(200,220,255,.45)' }}>6M</div>
|
||
<div className="absolute z-[12] text-[11px]" style={{ top: '28%', left: '54%', opacity: 0.45 }}>🚢</div>
|
||
|
||
{/* 환경 민감 구역 */}
|
||
<div className="absolute z-10 px-3.5 py-2 rounded" style={{
|
||
bottom: '6%', right: '6%',
|
||
background: 'rgba(34,197,94,.06)', border: '1px solid rgba(34,197,94,.2)'
|
||
}}>
|
||
<div className="text-[9px] font-bold font-serif uppercase tracking-wider leading-snug" style={{ color: 'rgba(34,197,94,.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-border rounded text-text-3 text-[12px] flex items-center justify-center cursor-pointer hover:text-text-1">{ico}</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 스케일 바 */}
|
||
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-border rounded px-2.5 py-1 text-[8px] text-text-3 font-mono">
|
||
<div className="w-[70px] h-0.5 mb-0.5" style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--bd) 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-border rounded px-3 py-1.5">
|
||
<div className="text-[8px] text-text-3 font-korean">현재 사고 유형</div>
|
||
<div className="text-[11px] font-bold font-korean" style={{ color: 'var(--cyan)' }}>{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-0 border-l border-border flex flex-col overflow-hidden">
|
||
{/* 분석 탭 */}
|
||
<div className="flex border-b border-border 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(--cyan)] border-b-[var(--cyan)] bg-[rgba(6,182,212,0.04)]'
|
||
: 'text-text-3 border-b-transparent hover:text-text-1'
|
||
}`}
|
||
>
|
||
{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-border 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-status-orange 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-text-1 font-korean">구난 분석 (RESCUE ANALYSIS)</div>
|
||
<div className="text-[9px] text-[var(--blue)] 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-3 border border-border rounded-md p-2 relative">
|
||
<div className="absolute top-1.5 right-2 text-[7px] text-text-3 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-hover)" stroke="var(--bdL)" strokeWidth=".8" />
|
||
<rect x="85" y="15" width="55" height="10" rx="2" fill="var(--bg-hover)" stroke="var(--bdL)" 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(--red)" strokeWidth="1" strokeDasharray="2,1" />
|
||
<text x="42" y="50" fill="var(--red)" fontSize="4" textAnchor="middle" fontFamily="monospace">IMPACT</text>
|
||
<line x1="30" y1="52" x2="200" y2="52" stroke="var(--bd)" strokeWidth=".4" strokeDasharray="3,2" />
|
||
<text x="205" y="55" fill="var(--t3)" 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(--t3)" fontSize="5">LIST: {d.list}°</text>
|
||
<text x="230" y="28" fill="var(--t3)" 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-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 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-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-text-3 font-korean">긴급 조치 (EMERGENCY ACTIONS)</div>
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
{[
|
||
{ en: 'BALLAST INJECT', ko: '밸러스트 주입', color: 'var(--red)' },
|
||
{ en: 'BALLAST DISCHARGE', ko: '밸러스트 배출', color: 'var(--red)' },
|
||
{ en: 'ENGINE STOP', ko: '기관 정지', color: 'var(--orange)' },
|
||
{ en: 'ANCHOR DROP', ko: '묘 투하', color: 'var(--orange)' },
|
||
].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-text-1 font-mono">{btn.en}</div>
|
||
<div className="text-[8px] font-korean" style={{ color: btn.color }}>{btn.ko}</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{/* 구난 의사결정 프로세스 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--cyan)' },
|
||
{ label: '② 사례분석', color: 'var(--blue)' },
|
||
{ label: '③ 장비선정', color: 'var(--t2)' },
|
||
].map((s, i) => (
|
||
<>{i > 0 && <span className="text-text-3">→</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(--yellow)' },
|
||
{ label: '⑤ 이초/인양', color: 'var(--orange)' },
|
||
{ label: '⑥ 유출량', color: 'var(--red)' },
|
||
].map((s, i) => (
|
||
<>{i > 0 && <span className="text-text-3">→</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-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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-0 rounded-sm">
|
||
<span className="text-text-3 font-korean text-[7px]">{r.label}</span><br />
|
||
<b className="text-text-1">{r.value}</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 예인력/이초력 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--yellow)' },
|
||
{ label: '비상 예인력', value: '420 kN', color: 'var(--red)' },
|
||
{ label: '이초 반력', value: '1,850 kN', color: 'var(--orange)' },
|
||
{ label: '인양 안전성', value: 'FAIL', color: 'var(--red)' },
|
||
].map((r, i) => (
|
||
<div key={i} className="px-1.5 py-1 bg-bg-0 rounded-sm">
|
||
<span className="text-text-3 font-korean text-[7px]">{r.label}</span><br />
|
||
<b style={{ color: r.color }}>{r.value}</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div className="text-[7px] text-text-3 font-korean mt-1">※ IMO Salvage Manual / Resistance Increase Ratio 기반 산출</div>
|
||
</div>
|
||
|
||
{/* 유출량 추정 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--orange)' },
|
||
{ label: '누적 유출량', value: '6.8 kL', color: 'var(--red)' },
|
||
{ label: '24h 예측', value: '145 kL', color: 'var(--red)' },
|
||
].map((r, i) => (
|
||
<div key={i} className="px-1.5 py-1 bg-bg-0 rounded-sm text-center">
|
||
<span className="text-text-3 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-hover rounded-sm">
|
||
<div className="h-full rounded-sm" style={{ width: '68%', background: 'linear-gradient(90deg, var(--orange), var(--red))' }} />
|
||
</div>
|
||
<div className="text-[7px] text-text-3 font-korean mt-0.5">잔여 연료유: 210 kL | 탱크 잔량: 68% 유출</div>
|
||
</div>
|
||
|
||
{/* CBR 사례기반 추론 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--cyan)' },
|
||
{ pct: '87%', name: 'Sea Empress (1996)', desc: '밀포드 · 좌초 · 72,000t 유출', color: 'var(--blue)' },
|
||
{ pct: '82%', name: 'Rena (2011)', desc: '타우랑가 · 좌초 · 350t HFO 유출', color: 'var(--t2)' },
|
||
].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-text-1">{c.name}</b><br />
|
||
<span className="text-text-3">{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-status-red font-korean mb-1.5">위험도 평가 — 2차사고 시나리오</div>
|
||
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
|
||
{[
|
||
{ label: '침수 확대 → 전복', level: 'HIGH', color: 'var(--red)' },
|
||
{ label: '유류 대량 유출 → 해양오염', level: 'HIGH', color: 'var(--orange)' },
|
||
{ label: '선체 절단 (BM 초과)', level: 'MED', color: 'var(--yellow)' },
|
||
{ label: '화재/폭발', level: 'LOW', color: 'var(--t2)' },
|
||
].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-text-1">{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-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 font-korean mb-1.5">해상 e-Call (GMDSS / VHF-DSC)</div>
|
||
<div className="flex flex-col gap-0.5 text-[7px] font-mono text-text-3">
|
||
{[
|
||
{ label: 'MMSI', value: '440123456', color: 'var(--t1)' },
|
||
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--red)' },
|
||
{ label: 'DSC Alert', value: 'SENT ✓', color: 'var(--green)' },
|
||
{ label: 'EPIRB', value: 'ACTIVATED ✓', color: 'var(--green)' },
|
||
{ label: 'VTS 인천', value: 'ACK 10:36', color: 'var(--blue)' },
|
||
].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-status-red 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-text-1 font-korean">손상 복원성 (DAMAGE STABILITY)</div>
|
||
<div className="text-[9px] text-text-3 font-korean leading-snug">
|
||
사고유형에 따른 손상 후 선체 복원력 분석<br />IMO A.749(18) / SOLAS Ch.II-1 기준 평가
|
||
</div>
|
||
|
||
{/* GZ Curve SVG */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2.5">
|
||
<div className="text-[9px] font-bold text-text-1 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(--bd)" strokeWidth=".5" />
|
||
<line x1="30" y1="100" x2="255" y2="100" stroke="var(--bd)" strokeWidth=".5" />
|
||
<line x1="30" y1="55" x2="255" y2="55" stroke="var(--bd)" strokeWidth=".3" strokeDasharray="3,3" />
|
||
<line x1="30" y1="32" x2="255" y2="32" stroke="var(--bd)" 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(--blue)" strokeWidth="1.5" />
|
||
<text x="135" y="25" fill="var(--blue)" 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(--red)" strokeWidth="1.5" />
|
||
<text x="140" y="54" fill="var(--red)" 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(--yellow)" fontSize="4.5" fontFamily="monospace">IMO MIN</text>
|
||
<text x="5" y="14" fill="var(--t3)" fontSize="5" fontFamily="monospace">GZ(m)</text>
|
||
<text x="5" y="35" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">0.6</text>
|
||
<text x="5" y="58" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">0.4</text>
|
||
<text x="5" y="82" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">0.2</text>
|
||
<text x="5" y="104" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">0</text>
|
||
<text x="30" y="112" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">0°</text>
|
||
<text x="80" y="112" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">15°</text>
|
||
<text x="130" y="112" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">30°</text>
|
||
<text x="180" y="112" fill="var(--t3)" fontSize="4.5" fontFamily="monospace">45°</text>
|
||
<text x="230" y="112" fill="var(--t3)" 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(--red)" sub="기준: 0.2m 이상 ⚠" subColor="var(--red)" />
|
||
<MetricCard label="θ_max (최대복원각)" value="28" unit="°" color="var(--yellow)" sub="기준: 25° 이상 ✓" subColor="var(--yellow)" />
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">침수 구획수</div>
|
||
<div className="text-lg font-bold text-status-red font-mono">2 <span className="text-[9px]">구획</span></div>
|
||
<div className="text-[7px] text-text-3 font-korean">#1 선수탱크 / #3 좌현탱크</div>
|
||
</div>
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">Margin Line 여유</div>
|
||
<div className="text-lg font-bold text-status-red font-mono">0.12 <span className="text-[9px]">m</span></div>
|
||
<div className="text-[7px] text-status-red 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-status-red font-korean mb-1">⚠ SOLAS 손상복원성 판정: 부적합 (FAIL)</div>
|
||
<div className="text-[8px] text-text-3 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-status-red">Margin Line 침수: 0.12m — 추가 침수 시 전복 위험</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 좌초시 복원성 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--yellow)' },
|
||
{ label: '접촉 면적', value: '12.5 m²', color: 'var(--t1)' },
|
||
{ label: '제거력(Removal)', value: '1,850 kN', color: 'var(--red)' },
|
||
{ label: '좌초 GM', value: '0.65 m', color: 'var(--yellow)' },
|
||
].map((r, i) => (
|
||
<div key={i} className="px-1.5 py-1 bg-bg-0 rounded-sm">
|
||
<span className="text-text-3 font-korean text-[7px]">{r.label}</span><br />
|
||
<b style={{ color: r.color }}>{r.value}</b>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 탱크 상태 */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[9px] font-bold text-text-1 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(--red)' },
|
||
{ name: '#3 Port Tank', pct: 85, status: '85%', color: 'var(--red)' },
|
||
{ name: '#2 DB Tank', pct: 45, status: '45%', color: 'var(--green)' },
|
||
{ name: 'Ballast #4', pct: 72, status: '72%', color: 'var(--blue)' },
|
||
{ name: 'Fuel Oil #5', pct: 68, status: '68%', color: 'var(--orange)' },
|
||
].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-text-3">{t.name}</span>
|
||
<div className="flex-1 h-1 bg-bg-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-text-1 font-korean">종강도 분석 (LONGITUDINAL STRENGTH)</div>
|
||
<div className="text-[9px] text-text-3 font-korean leading-snug">
|
||
선체 종방향 구조 응력 분석<br />IACS CSR / Classification Society 기준
|
||
</div>
|
||
|
||
{/* 전단력 분포 SVG */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2.5">
|
||
<div className="text-[9px] font-bold text-text-1 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(--bd)" strokeWidth=".5" />
|
||
<line x1="25" y1="10" x2="25" y2="80" stroke="var(--bd)" 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(--cyan)" 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(--red)" strokeWidth="1.2" strokeDasharray="3,2" />
|
||
<text x="80" y="12" fill="var(--red)" fontSize="4.5">손상 후 SF ▲</text>
|
||
<text x="2" y="14" fill="var(--t3)" fontSize="4" fontFamily="monospace">+SF</text>
|
||
<text x="2" y="48" fill="var(--t3)" fontSize="4" fontFamily="monospace">0</text>
|
||
<text x="2" y="78" fill="var(--t3)" fontSize="4" fontFamily="monospace">-SF</text>
|
||
<text x="25" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">AP</text>
|
||
<text x="130" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">MID</text>
|
||
<text x="245" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">FP</text>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* 굽힘모멘트 분포 SVG */}
|
||
<div className="bg-bg-3 border border-border rounded-md p-2.5">
|
||
<div className="text-[9px] font-bold text-text-1 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(--bd)" strokeWidth=".5" />
|
||
<line x1="25" y1="10" x2="25" y2="80" stroke="var(--bd)" 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(--green)" 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(--orange)" strokeWidth="1.2" strokeDasharray="3,2" />
|
||
<text x="115" y="8" fill="var(--orange)" fontSize="4.5">손상 후 BM ▲</text>
|
||
<text x="2" y="14" fill="var(--t3)" fontSize="4" fontFamily="monospace">+BM</text>
|
||
<text x="2" y="48" fill="var(--t3)" fontSize="4" fontFamily="monospace">0</text>
|
||
<text x="25" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">AP</text>
|
||
<text x="130" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">MID</text>
|
||
<text x="245" y="88" fill="var(--t3)" fontSize="4" fontFamily="monospace">FP</text>
|
||
</svg>
|
||
</div>
|
||
|
||
{/* 종강도 지표 */}
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">SF 최대/허용 비율</div>
|
||
<div className="text-lg font-bold text-status-yellow font-mono">88<span className="text-[9px]">%</span></div>
|
||
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
||
<div className="h-full rounded-sm" style={{ width: '88%', background: 'var(--yellow)' }} />
|
||
</div>
|
||
</div>
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">BM 최대/허용 비율</div>
|
||
<div className="text-lg font-bold text-status-orange font-mono">92<span className="text-[9px]">%</span></div>
|
||
<div className="h-[5px] bg-bg-hover rounded-sm mt-0.5">
|
||
<div className="h-full rounded-sm" style={{ width: '92%', background: 'var(--orange)' }} />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">Section Modulus 여유</div>
|
||
<div className="text-lg font-bold text-status-green font-mono">1.08</div>
|
||
<div className="text-[7px] text-status-green font-korean">Req'd: 1.00 이상 ✓</div>
|
||
</div>
|
||
<div className="bg-bg-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 font-korean">Hull Girder ULS</div>
|
||
<div className="text-lg font-bold text-status-yellow font-mono">1.12</div>
|
||
<div className="text-[7px] text-status-yellow 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-status-yellow font-korean mb-1">⚠ 종강도 판정: 주의 (CAUTION)</div>
|
||
<div className="text-[8px] text-text-3 font-korean leading-snug">
|
||
· SF 최대: 허용치의 88% — <span className="text-status-yellow">주의 구간</span><br />
|
||
· BM 최대: 허용치의 92% — <span className="text-status-orange">경고 구간</span><br />
|
||
· 중앙부 Hogging 모멘트 증가 — 추가 침수 시 선체 절단 위험<br />
|
||
· <span className="text-status-red">밸러스트 이동으로 BM 분산 필요</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
/* ─── 하단 바: 이벤트 로그 + 타임라인 ─── */
|
||
function BottomBar() {
|
||
return (
|
||
<div className="h-[145px] min-h-[145px] border-t border-border flex bg-bg-0 flex-shrink-0">
|
||
{/* 이벤트 로그 */}
|
||
<div className="flex-1 flex flex-col overflow-hidden border-r border-border">
|
||
<div className="flex items-center justify-between px-3 py-1 border-b border-border flex-shrink-0">
|
||
<span className="text-[9px] font-bold text-text-3 font-korean">이벤트 로그 / 통신 기록 (EVENT LOG / COMMUNICATION TRANSCRIPT)</span>
|
||
<div className="flex gap-0.5">
|
||
{[
|
||
{ label: '전체', color: 'var(--cyan)' },
|
||
{ label: '긴급', color: 'var(--orange)' },
|
||
{ label: '통신', color: 'var(--blue)' },
|
||
].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(--red)', bold: true },
|
||
{ time: '10:35', msg: 'OIL LEAK DETECTED SENSOR #3', color: 'var(--t1)', bold: false },
|
||
{ time: '10:40', msg: 'CG HELO DISPATCHED', color: 'var(--red)', bold: true },
|
||
{ time: '10:41', msg: 'GM CRITICAL ALERT — DAMAGE STABILITY FAIL', color: 'var(--red)', bold: true },
|
||
{ time: '10:42', msg: 'Coast Guard 123 en route — ETA 15 min', color: 'var(--blue)', bold: false },
|
||
{ time: '10:43', msg: 'LONGITUDINAL STRENGTH WARNING — BM 92% of LIMIT', color: 'var(--yellow)', bold: false },
|
||
{ time: '10:45', msg: 'BALLAST TRANSFER INITIATED — PORT #2 → STBD #3', color: 'var(--t1)', bold: false },
|
||
{ time: '10:48', msg: 'LIST INCREASING — 12° → 15°', color: 'var(--yellow)', bold: false },
|
||
{ time: '10:50', msg: 'RESCUE HELO ON SCENE — HOISTING OPS', color: 'var(--blue)', bold: false },
|
||
{ time: '10:55', msg: '5 SURVIVORS RECOVERED BY HELO', color: 'var(--green)', bold: false },
|
||
].map((e, i) => (
|
||
<div key={i}>
|
||
<span className="text-text-3">[{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-text-1 font-korean">시간대별 시뮬레이션 컨트롤 (TIMELINE SIMULATION CONTROL)</div>
|
||
<div className="flex items-center gap-1.5 text-[8px] text-text-3 font-mono">
|
||
<span>[-6h]</span>
|
||
<span className="flex-1 text-center font-bold text-text-1">[CURRENT]</span>
|
||
<span>[+6H]</span>
|
||
<span>[+12H]</span>
|
||
<span>[+24H]</span>
|
||
</div>
|
||
<div className="relative h-1.5 bg-bg-hover rounded-sm mx-1">
|
||
<div className="absolute rounded-full border-2 border-bg-0" style={{
|
||
left: '35%', top: '-3px', width: '12px', height: '12px',
|
||
background: 'var(--cyan)', 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-3 border border-border rounded-full text-text-3 text-[11px] flex items-center justify-center cursor-pointer hover:text-text-1">⏮</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(--cyan)] text-[13px] flex items-center justify-center cursor-pointer hover:brightness-125">▶</button>
|
||
<button className="w-7 h-7 bg-bg-3 border border-border rounded-full text-text-3 text-[11px] flex items-center justify-center cursor-pointer hover:text-text-1">⏭</button>
|
||
</div>
|
||
<div className="text-center text-[8px] text-text-3 font-mono">
|
||
현재 시간: <b style={{ color: 'var(--cyan)' }}>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-3 border border-border rounded-md p-2">
|
||
<div className="text-[8px] text-text-3 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 listData = [
|
||
{ status: '대응중', statusColor: 'var(--red)', no: 'RSC-2026-001', vessel: 'M/V SEA GUARDIAN', type: '충돌/좌초', date: '2026-02-17 10:30', location: '37°28\'N 126°15\'E', crew: '15/20' },
|
||
{ status: '대응중', statusColor: 'var(--orange)', no: 'RSC-2026-002', vessel: 'M/V EASTERN GLORY', type: '침수/전복', date: '2026-02-15 14:20', location: '35°05\'N 129°02\'E', crew: '22/28' },
|
||
{ status: '종료', statusColor: 'var(--green)', no: 'RSC-2025-048', vessel: 'M/V PACIFIC WAVE', type: '충돌', date: '2025-12-03 08:15', location: '34°45\'N 128°30\'E', crew: '18/18' },
|
||
{ status: '종료', statusColor: 'var(--green)', no: 'RSC-2025-047', vessel: 'M/V HARMONY', type: '좌초', date: '2025-11-20 22:40', location: '36°12\'N 126°50\'E', crew: '25/25' },
|
||
{ status: '종료', statusColor: 'var(--green)', no: 'RSC-2025-046', vessel: 'M/V GRAND FORTUNE', type: '침몰', date: '2025-10-08 05:30', location: '33°30\'N 127°15\'E', crew: '10/22' },
|
||
]
|
||
|
||
return (
|
||
<div className="flex flex-col flex-1 overflow-hidden">
|
||
<div className="px-5 py-4 flex items-center justify-between border-b border-border">
|
||
<span className="text-sm font-bold font-korean">긴급구난 사고 목록</span>
|
||
<div className="flex gap-2 items-center">
|
||
<input type="text" placeholder="선박명 / 사고번호 검색..." className="px-3 py-1.5 bg-bg-0 border border-border rounded-md text-text-2 font-korean text-[11px] w-[200px] outline-none focus:border-[var(--cyan)]" />
|
||
<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(--cyan)] 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">
|
||
<table className="w-full border-collapse text-[11px] mt-3">
|
||
<thead>
|
||
<tr className="bg-bg-3 border-b border-border">
|
||
{['상태', '사고번호', '선박명', '사고유형', '발생일시', '위치', '인명'].map(h => (
|
||
<th key={h} className="py-2 px-2.5 text-left font-korean font-semibold text-text-3 text-[10px]">{h}</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{listData.map((r, i) => (
|
||
<tr key={i} className="border-b border-border hover:bg-bg-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, ${r.statusColor} 15%, transparent)`, color: r.statusColor
|
||
}}>{r.status}</span>
|
||
</td>
|
||
<td className="py-2 px-2.5 font-mono text-[var(--cyan)] font-semibold">{r.no}</td>
|
||
<td className="py-2 px-2.5 font-korean text-text-1 font-semibold">{r.vessel}</td>
|
||
<td className="py-2 px-2.5 font-korean">{r.type}</td>
|
||
<td className="py-2 px-2.5 font-mono text-text-3">{r.date}</td>
|
||
<td className="py-2 px-2.5 font-mono text-text-3 text-[10px]">{r.location}</td>
|
||
<td className="py-2 px-2.5 font-mono">{r.crew}</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>
|
||
)
|
||
}
|