wing-ops/frontend/src/tabs/prediction/components/BoomDeploymentTheoryView.tsx

1444 lines
60 KiB
TypeScript
Executable File
Raw Blame 히스토리

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 } from 'react';
const boomTabs = [
{ id: 0, label: '개요' },
{ id: 1, label: '배치 이론' },
{ id: 2, label: '최적화 알고리즘' },
{ id: 3, label: '유체역학 모델' },
{ id: 4, label: '현장 적용' },
{ id: 5, label: '참고문헌' },
];
export function BoomDeploymentTheoryView() {
const [activePanel, setActivePanel] = useState(0);
const handleExportPDF = () => {
window.print();
};
return (
<div className="flex flex-col h-full overflow-hidden bg-bg-base">
<div className="flex-1 overflow-y-auto scrollbar-thin p-5">
{/* 헤더 */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-3">
<div className="w-[42px] h-[42px] rounded-[10px] bg-bg-elevated border border-stroke flex items-center justify-center text-heading-3">
🛡
</div>
<div>
<div className="text-title-2 font-bold"> </div>
<div className="text-label-2 mt-0.5 text-fg-disabled">
Oil Boom Deployment Optimization · ·
</div>
</div>
</div>
<button
onClick={handleExportPDF}
className="px-3.5 py-1.5 rounded-sm text-label-2 font-semibold cursor-pointer text-color-accent"
style={{
border: '1px solid rgba(6,182,212,.3)',
background: 'rgba(6,182,212,.08)',
}}
>
📤 PDF
</button>
</div>
{/* 내부 네비게이션 */}
<div className="mb-5">
<div className="flex gap-0.5 rounded-lg p-1 bg-bg-card border border-stroke">
{boomTabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActivePanel(tab.id)}
className={`flex-1 py-2 px-1 text-label-1 font-medium rounded-md transition-all duration-150 cursor-pointer border ${
activePanel === tab.id
? 'border-stroke-light bg-bg-elevated text-fg'
: 'border-transparent bg-bg-card text-fg-disabled hover:text-fg-sub'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
{/* ═══ PANEL 0: 개요 ═══ */}
{activePanel === 0 && <OverviewPanel />}
{activePanel === 1 && <DeploymentTheoryPanel />}
{activePanel === 2 && <OptimizationPanel />}
{activePanel === 3 && <FluidDynamicsPanel />}
{activePanel === 4 && <FieldApplicationPanel />}
{activePanel === 5 && <ReferencesPanel />}
</div>
</div>
);
}
/* ──────────── PANEL 0: 개요 ──────────── */
function OverviewPanel() {
return (
<>
{/* 인트로 카드 */}
<div className="rounded-[10px] p-[14px] mb-4 bg-bg-card border border-stroke">
<div className="grid grid-cols-2 gap-5">
<div>
<div className="text-title-4 font-bold mb-2"> ?</div>
<div className="text-label-2 leading-[1.8] text-fg-sub">
<b className="text-color-accent"> </b>
(··) , ( ·
) <b className="text-color-accent"> </b>
·· .
</div>
</div>
<div>
<div className="text-title-4 font-bold mb-2">WING </div>
<div className="flex flex-col gap-[5px] text-label-2 text-fg-sub">
{[
{
num: '①',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화',
},
{
num: '②',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축',
},
{
num: '③',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
text: '자원 효율 최적화 — 가용 오일펜스 길이·방제정 수·이동시간 제약 충족',
},
{
num: '④',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
text: '실패 안전성 확보 — 조류 초과 시 오일펜스 이탈 방지 방향각 자동 보정',
},
].map((item, i) => (
<div
key={i}
className="px-2.5 py-1.5 rounded-md"
style={{ background: item.bg, border: `1px solid ${item.bd}` }}
>
<span className="font-bold" style={{ color: item.color }}>
{item.num}
</span>{' '}
<b>{item.text}</b>
</div>
))}
</div>
</div>
</div>
</div>
{/* 전체 흐름도 */}
<div className="rounded-md p-4 mb-4 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3.5">WING </div>
<div className="flex items-center justify-center gap-0 flex-nowrap overflow-x-auto py-2">
{[
{
icon: '🌊',
label: '확산예측',
sub: 'KOSPS/POSEIDON\nOpenDrift',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.08)',
bd: 'rgba(6,182,212,.2)',
},
{
icon: '📡',
label: '환경입력',
sub: '조류·풍향\n파고·수심',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.08)',
bd: 'rgba(6,182,212,.2)',
},
{
icon: '🗺️',
label: '차단선 후보',
sub: '격자탐색\n후보지점 생성',
color: 'var(--color-caution)',
bg: 'rgba(6,182,212,.08)',
bd: 'rgba(6,182,212,.2)',
},
{
icon: '⚙️',
label: '최적화 엔진',
sub: '다목적\n유전알고리즘',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.08)',
bd: 'rgba(6,182,212,.3)',
bold: true,
},
{
icon: '✅',
label: '배치안 출력',
sub: '좌표·형태\n방향·순서',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.08)',
bd: 'rgba(6,182,212,.2)',
},
{
icon: '🗺️',
label: '지도 표시',
sub: 'ESI 중첩\n방제자원 연계',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.08)',
bd: 'rgba(59,130,246,.2)',
},
].map((step, i) => (
<div key={i} className="flex items-center">
<div
className="text-center min-w-[80px] rounded-lg px-3 py-2.5 text-caption"
style={{
background: step.bg,
border: `${step.bold ? '2px' : '1px'} solid ${step.bd}`,
}}
>
<div className="text-title-2 mb-1">{step.icon}</div>
<div className="font-bold" style={{ color: step.color }}>
{step.label}
</div>
<div className="text-fg-disabled" style={{ whiteSpace: 'pre-line' }}>
{step.sub}
</div>
</div>
{i < 5 && <div className="px-1.5 text-fg-disabled text-title-3"></div>}
</div>
))}
</div>
</div>
{/* 오일펜스 종류 */}
<div className="grid grid-cols-3 gap-3 mb-4">
{[
{
icon: '⛽',
title: '고형 오일펜스',
color: 'var(--color-accent)',
desc: '단단한 부체와 수중커튼으로 구성. 정적 배치. 항구·좁은 수로에 적합.',
specs: [
'내조류 한계: 0.5~1.0 knot',
'높이: 30~60cm · 수중 30~60cm',
'전개속도: 30~60m/hr',
],
},
{
icon: '🌊',
title: '공기충전식 오일펜스',
color: 'var(--color-info)',
desc: '공기로 부력 확보. 이동·보관 편리. 해상 광역 차단에 주로 사용.',
specs: [
'내조류 한계: 0.7~1.5 knot',
'높이: 45~90cm · 수중 45~90cm',
'전개속도: 100~300m/hr',
],
},
{
icon: '🔄',
title: '자항식 오일펜스',
color: 'var(--color-accent)',
desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.',
specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'],
},
].map((boom, i) => (
<div key={i} className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2" style={{ color: boom.color }}>
{boom.icon} {boom.title}
</div>
<div className="text-label-2 mb-2 leading-[1.7] text-fg-sub">{boom.desc}</div>
<div className="flex flex-col gap-[3px] text-caption text-fg-sub">
{boom.specs.map((spec, j) => (
<div
key={j}
className="px-[7px] py-[3px] rounded-[3px] text-fg-sub"
style={{ background: `${boom.color}11` }}
>
{spec}
</div>
))}
</div>
</div>
))}
</div>
{/* 핵심 제약조건 */}
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2.5 text-color-caution">
</div>
<div className="grid grid-cols-3 gap-2.5 text-label-2 text-fg-sub">
{[
{
icon: '🌊',
title: '조류 제약',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.05)',
bd: 'rgba(59,130,246,.15)',
lines: [
'조류속도 > 임계유속 시',
'오일펜스 하단 통과 발생',
'U<0.7 knot 유지 필수',
'임계각도 자동 계산 적용',
],
},
{
icon: '📏',
title: '자원 제약',
color: 'var(--color-caution)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
lines: [
'가용 오일펜스 총 길이',
'방제정 척수·이동시간',
'앵커링 가능 수심 조건',
'연결부 허용 장력',
],
},
{
icon: '⏱️',
title: '시간 제약',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.05)',
bd: 'rgba(59,130,246,.15)',
lines: [
'유출유 도달 예측시간',
'오일펜스 전개 소요시간',
'방제정 현장 도착시간',
'조석 변환 주기 고려',
],
},
].map((c, i) => (
<div
key={i}
className="p-2.5 rounded-[7px]"
style={{ background: c.bg, border: `1px solid ${c.bd}` }}
>
<div className="font-bold mb-[5px]" style={{ color: c.color }}>
{c.icon} {c.title}
</div>
<div className="leading-[1.7] text-fg-sub">
{c.lines.map((l, j) => (
<span key={j}>
{j > 0 && <br />}
{l}
</span>
))}
</div>
</div>
))}
</div>
</div>
</>
);
}
/* ──────────── PANEL 1: 배치 이론 ──────────── */
function DeploymentTheoryPanel() {
return (
<>
{/* 차단 효율 이론 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">
📐 (Boom Containment Efficiency)
</div>
<div className="grid grid-cols-2 gap-3.5">
<div>
<div className="text-label-2 font-bold mb-2 text-color-accent">
E(θ, U)
</div>
<div className="text-label-2 leading-[1.8] mb-2 text-fg-sub">
<b> (U)</b> <b> (θ)</b> .
,
.
</div>
<div
className="rounded-md p-2.5 text-label-2 leading-[2.1] bg-bg-base font-mono"
style={{ border: '1px solid rgba(6,182,212,.2)' }}
>
E(θ,U) = 1 {' '}
<span className="text-color-info">
F<sub>loss</sub>(U<sub>n</sub>)
</span>
<br />U<sub>n</sub> = U · sin(θ){' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />E = 1 (U<sub>n</sub> U<sub>c</sub>)<br />E = max(0, 1 (U<sub>n</sub>/U
<sub>c</sub>)²) (U<sub>n</sub> &gt; U<sub>c</sub>)<br />
<span className="text-caption text-fg-disabled">
U<sub>c</sub>: ( 0.35m/s = 0.7 knot)
</span>
</div>
</div>
<div>
<div className="text-label-2 font-bold mb-2 text-color-accent">
θ*
</div>
<div className="text-label-2 leading-[1.8] mb-2 text-fg-sub">
. {' '}
<b></b> , {' '}
<b className="text-color-accent">30°~45° </b> .
</div>
<div
className="rounded-md p-2.5 text-label-2 leading-[2.1] bg-bg-base font-mono"
style={{ border: '1px solid rgba(6,182,212,.2)' }}
>
θ* = arcsin(U<sub>c</sub> / U){' '}
<span className="text-caption text-fg-disabled">()</span>
<br />θ<sub>opt</sub> = argmax [A<sub>block</sub>(θ) · E(θ,U)]
<br />
실용범위: 15° θ 60°
<br />
<span className="text-caption text-fg-disabled">
, θ &lt; arcsin(U<sub>c</sub>/U)
</span>
</div>
</div>
</div>
</div>
{/* V형·U형·J형 배치 패턴 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">🔷 </div>
<div className="grid grid-cols-3 gap-3">
{/* V형 */}
<div
className="rounded-lg p-3.5 bg-bg-base"
style={{
border: '1px solid rgba(6,182,212,.2)',
}}
>
<div className="text-label-2 font-bold mb-2 text-color-accent">V형 (Chevron)</div>
<div
className="flex items-center justify-center p-4 rounded-md mb-2"
style={{ background: 'rgba(6,182,212,.04)' }}
>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs>
<marker
id="arr1"
markerWidth="6"
markerHeight="6"
refX="3"
refY="3"
orient="auto"
>
<path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" />
</marker>
</defs>
<line
x1="10"
y1="15"
x2="50"
y2="55"
stroke="rgba(6,182,212,.8)"
strokeWidth="2.5"
/>
<line
x1="90"
y1="15"
x2="50"
y2="55"
stroke="rgba(6,182,212,.8)"
strokeWidth="2.5"
/>
<circle cx="50" cy="10" r="4" fill="rgba(6,182,212,.5)" />
<text
x="50"
y="7"
textAnchor="middle"
fill="rgba(6,182,212,.8)"
fontSize="7"
fontFamily="monospace"
>
</text>
<line
x1="50"
y1="58"
x2="50"
y2="65"
stroke="rgba(6,182,212,.7)"
strokeWidth="1.5"
markerEnd="url(#arr1)"
/>
<text x="58" y="64" fill="rgba(6,182,212,.7)" fontSize="6">
</text>
</svg>
</div>
<div className="text-label-2 leading-[1.7] mb-[7px] text-fg-sub">
V형. . .
</div>
<div
className="rounded-[5px] p-[7px] text-caption leading-[1.9] font-mono"
style={{ background: 'rgba(6,182,212,.05)' }}
>
A<sub>V</sub> = L²·sin(2α)/2
<br />
<span className="text-fg-disabled">α: 반개각, L: 편측 </span>
<br />
α = 30°~45°
</div>
</div>
{/* U형 */}
<div
className="rounded-lg p-3.5 bg-bg-base"
style={{
border: '1px solid rgba(6,182,212,.2)',
}}
>
<div className="text-label-2 font-bold mb-2 text-color-accent">U형 (Horseshoe)</div>
<div
className="flex items-center justify-center p-4 rounded-md mb-2"
style={{ background: 'rgba(6,182,212,.04)' }}
>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs>
<marker
id="arr2"
markerWidth="6"
markerHeight="6"
refX="3"
refY="3"
orient="auto"
>
<path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" />
</marker>
</defs>
<path
d="M15,10 L15,45 Q50,65 85,45 L85,10"
fill="none"
stroke="rgba(6,182,212,.8)"
strokeWidth="2.5"
/>
<circle cx="50" cy="52" r="4" fill="rgba(6,182,212,.5)" />
<text
x="50"
y="62"
textAnchor="middle"
fill="rgba(6,182,212,.8)"
fontSize="7"
fontFamily="monospace"
>
</text>
<line
x1="50"
y1="0"
x2="50"
y2="7"
stroke="rgba(6,182,212,.7)"
strokeWidth="1.5"
markerEnd="url(#arr2)"
/>
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">
</text>
</svg>
</div>
<div className="text-label-2 leading-[1.7] mb-[7px] text-fg-sub">
. . .
</div>
<div
className="rounded-[5px] p-[7px] text-caption leading-[1.9] font-mono"
style={{ background: 'rgba(6,182,212,.05)' }}
>
A<sub>U</sub> = π·r²/2 + 2r·h
<br />
<span className="text-fg-disabled">r: 반경, h: 직선부 </span>
<br />
전제: U &lt; 0.5 knot
</div>
</div>
{/* J형 */}
<div
className="rounded-lg p-3.5 bg-bg-base"
style={{
border: '1px solid rgba(6,182,212,.2)',
}}
>
<div className="text-label-2 font-bold mb-2 text-color-accent">J형 (Skimming)</div>
<div
className="flex items-center justify-center p-4 rounded-md mb-2"
style={{ background: 'rgba(6,182,212,.04)' }}
>
<svg width="180" height="120" viewBox="0 0 100 65" className="overflow-visible">
<defs>
<marker
id="arr3"
markerWidth="6"
markerHeight="6"
refX="3"
refY="3"
orient="auto"
>
<path d="M0,0 L0,6 L6,3 z" fill="rgba(6,182,212,.7)" />
</marker>
</defs>
<line
x1="80"
y1="8"
x2="80"
y2="48"
stroke="rgba(6,182,212,.8)"
strokeWidth="2.5"
/>
<path
d="M80,48 Q55,60 30,35"
fill="none"
stroke="rgba(6,182,212,.8)"
strokeWidth="2.5"
/>
<circle cx="79" cy="48" r="4" fill="rgba(6,182,212,.5)" />
<text x="67" y="43" fill="rgba(6,182,212,.8)" fontSize="7" fontFamily="monospace">
</text>
<line
x1="50"
y1="0"
x2="50"
y2="7"
stroke="rgba(6,182,212,.7)"
strokeWidth="1.5"
markerEnd="url(#arr3)"
/>
<text x="58" y="5" fill="rgba(6,182,212,.7)" fontSize="6">
</text>
</svg>
</div>
<div className="text-label-2 leading-[1.7] mb-[7px] text-fg-sub">
+ . . · .
</div>
<div
className="rounded-[5px] p-[7px] text-caption leading-[1.9] font-mono"
style={{ background: 'rgba(6,182,212,.05)' }}
>
θ<sub>J</sub> = arcsin(U<sub>c</sub>/U) + δ<br />
<span className="text-fg-disabled">δ: 안전여유각(5°~10°)</span>
<br />
활용: U &gt; 0.7 knot
</div>
</div>
</div>
</div>
{/* 다단 배치 이론 */}
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2.5">🔢 (Multi-Boom) </div>
<div className="grid grid-cols-2 gap-3">
<div className="text-label-2 leading-[1.8] text-fg-sub">
<b> </b> .
n개 :
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
E<sub>total</sub> = 1 (1E<sub>i</sub>)<br />
<span className="text-caption text-fg-disabled">
E<sub>i</sub>: i번째
</span>
</div>
</div>
<div className="flex flex-col gap-[5px] text-caption text-fg-sub">
{[
{
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
label: '2단 직렬',
text: ': E_total = E₁+E₂E₁·E₂ (예: 70%+70% → 91%)',
},
{
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
label: '단간 거리',
text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장',
},
{
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
label: '배치 우선순위',
text: ': ESI 고등급 구역 보호 → 취수원 → 어항 순',
},
{
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
label: '조석 변화',
text: ': 창조·낙조 전환 시 오일펜스 방향 재조정 필요',
},
].map((item, i) => (
<div
key={i}
className="px-[9px] py-1.5 rounded-[5px] text-fg-sub"
style={{ background: item.bg, border: `1px solid ${item.bd}` }}
>
<b style={{ color: item.color }}>{item.label}</b>
{item.text}
</div>
))}
</div>
</div>
</div>
</>
);
}
/* ──────────── PANEL 2: 최적화 알고리즘 ──────────── */
function OptimizationPanel() {
return (
<>
{/* 다목적 최적화 개요 */}
<div className="rounded-[10px] p-[14px] mb-4 bg-bg-card border border-stroke">
<div className="text-title-4 font-bold mb-2">
(Multi-Objective Optimization)
</div>
<div className="text-label-2 leading-[1.8] text-fg-sub">
<b className="text-color-accent"> </b>
.
,{' '}
<b className="text-color-accent"> (Pareto Optimal) </b>
.
</div>
</div>
{/* 목적함수 정의 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">📊 </div>
<div className="grid grid-cols-2 gap-3 mb-3">
<div
className="rounded-lg p-3 bg-bg-base"
style={{ border: '1px solid rgba(6,182,212,.2)' }}
>
<div className="text-label-2 font-bold mb-2 text-color-accent">🎯 F(x)</div>
<div
className="rounded-[5px] p-[9px] text-label-2 leading-[2.2] font-mono"
style={{ background: 'rgba(6,182,212,.04)' }}
>
<b className="text-color-accent">:</b>
<br />
f(x) = Σ A<sub>blocked,i</sub> · w<sub>ESI,i</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />
f(x) = T<sub>deadline</sub> T<sub>deploy</sub>{' '}
<span className="text-caption text-fg-disabled">()</span>
<br />
<b className="text-color-info">:</b>
<br />
f(x) = Σ L<sub>boom,j</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />
f(x) = Σ D<sub>vessel,k</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
</div>
</div>
<div
className="rounded-lg p-3 bg-bg-base"
style={{ border: '1px solid rgba(59,130,246,.2)' }}
>
<div className="text-label-2 font-bold mb-2 text-color-caution">🚫 G(x)</div>
<div
className="rounded-[5px] p-[9px] text-label-2 leading-[2.2] font-mono"
style={{ background: 'rgba(59,130,246,.04)' }}
>
g: U·sin(θ<sub>i</sub>) U<sub>c</sub> i{' '}
<span className="text-caption text-fg-disabled">()</span>
<br />
g: Σ L<sub>j</sub> L<sub>max</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />
g: T<sub>deploy,i</sub> T<sub>arrive,i</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />
g: d(p<sub>i</sub>, shore) d<sub>min</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
<br />
g: h(p<sub>i</sub>) h<sub>min</sub>{' '}
<span className="text-caption text-fg-disabled">( )</span>
</div>
</div>
</div>
{/* ESI 가중치 */}
<div
className="rounded-lg p-3 bg-bg-base"
style={{ border: '1px solid rgba(6,182,212,.2)' }}
>
<div className="text-label-2 font-bold mb-2 text-color-caution">
🏖 ESI w<sub>ESI</sub>
</div>
<div className="grid grid-cols-5 gap-[5px] text-caption text-fg-sub">
{[
{
grade: 'ESI 1~2',
desc: '노출암반',
w: 'w = 0.2',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.06)',
},
{
grade: 'ESI 3~4',
desc: '모래해변',
w: 'w = 0.4',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.06)',
},
{
grade: 'ESI 5~6',
desc: '자갈·조약',
w: 'w = 0.6',
color: 'var(--color-caution)',
bg: 'rgba(6,182,212,.06)',
},
{
grade: 'ESI 7~8',
desc: '갯벌·조간대',
w: 'w = 0.85',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.06)',
},
{
grade: 'ESI 9~10',
desc: '맹그로브·습지',
w: 'w = 1.0',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.08)',
bd: 'rgba(59,130,246,.2)',
},
].map((esi, i) => (
<div
key={i}
className="p-1.5 rounded text-center"
style={{ background: esi.bg, border: esi.bd ? `1px solid ${esi.bd}` : undefined }}
>
<div className="font-bold" style={{ color: esi.color }}>
{esi.grade}
</div>
<div className="text-fg-disabled">{esi.desc}</div>
<div className="font-bold">{esi.w}</div>
</div>
))}
</div>
</div>
</div>
{/* NSGA-II 알고리즘 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3 text-color-accent">
🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<div className="text-label-2 leading-[1.8] mb-2 text-fg-sub">
WING의 {' '}
<b className="text-color-accent">NSGA-II</b>(Deb et al., 2002)
. (Pareto Front)
.
</div>
<div className="flex flex-col gap-1 text-caption text-fg-sub">
{[
'염색체 구조 : [배치지점 좌표, 방향각θ, 길이L, 형태, 배치순서]',
'집단 크기 : 100~200개체 · 세대수 50~200',
'교배 연산 : SBX(Simulated Binary Crossover) · ηc=20',
'변이 연산 : 다항식 변이(Polynomial Mutation) · ηm=20',
'선택 방식 : 비지배 정렬 + 혼잡도 거리(Crowding Distance)',
].map((item, i) => (
<div
key={i}
className="px-2 py-[5px] rounded text-fg-sub"
style={{
background: 'rgba(6,182,212,.05)',
border: '1px solid rgba(6,182,212,.12)',
}}
>
<b className="text-color-accent">{item.split(' : ')[0]}</b> :{' '}
{item.split(' : ')[1]}
</div>
))}
</div>
</div>
<div>
<div className="text-label-2 font-bold mb-[7px] text-fg-sub">
NSGA-II 5
</div>
<div className="flex flex-col gap-[5px] text-caption text-fg-sub">
{[
{
step: '①',
title: '초기 집단 생성',
desc: '확산예측 결과 기반 랜덤 + 휴리스틱 배치안 혼합 초기화',
},
{
step: '②',
title: '적합도 평가',
desc: '유출유 확산 시뮬레이터로 각 배치안의 차단면적·도달시간 계산',
},
{
step: '③',
title: '비지배 정렬',
desc: '목적함수 공간에서 파레토 전면 계층(F₁, F₂, F₃…) 분류',
},
{
step: '④',
title: '교배·변이',
desc: 'SBX + 다항식 변이로 자식 세대 생성. 제약조건 위반 수리(repair)',
},
{
step: '⑤',
title: '엘리트 선택',
desc: '부모+자식 2N 집단에서 비지배 정렬+혼잡도 기준으로 N개 선택 → 수렴까지 반복',
},
].map((item, i) => (
<div
key={i}
className="flex gap-2 px-[9px] py-1.5 rounded-[5px]"
style={{
background: i === 4 ? 'rgba(6,182,212,.05)' : 'rgba(6,182,212,.04)',
border: i === 4 ? '1px solid rgba(6,182,212,.12)' : undefined,
}}
>
<span className="min-w-[20px] font-extrabold text-color-accent">{item.step}</span>
<div className="leading-[1.6] text-fg-sub">
<b>{item.title}</b> : {item.desc}
</div>
</div>
))}
</div>
</div>
</div>
</div>
{/* 보조 알고리즘 비교 */}
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2.5">🔬 </div>
<div className="overflow-x-auto">
<table className="w-full text-label-2 border-collapse">
<thead>
<tr
style={{
background: 'rgba(255,255,255,.03)',
borderBottom: '1px solid var(--stroke-light)',
}}
>
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map((h) => (
<th
key={h}
className="py-[7px] px-2.5 font-semibold text-fg-disabled"
style={{ textAlign: h === '알고리즘' ? 'left' : 'center' }}
>
{h}
</th>
))}
</tr>
</thead>
<tbody>
{[
{
name: 'NSGA-II',
color: 'var(--color-accent)',
type: '다목적 GA',
pros: '파레토 전면 탐색\n다양성 유지 우수',
cons: '계산비용 높음\n수렴 느림',
wing: '메인 엔진',
wingColor: 'var(--color-accent)',
},
{
name: 'PSO',
color: 'var(--color-accent)',
type: '입자군집',
pros: '빠른 수렴\n구현 단순',
cons: '조기수렴\n다목적 취약',
wing: '단일목적 빠른 배치',
wingColor: 'var(--fg-sub)',
},
{
name: 'SA',
color: 'var(--color-info)',
type: '모의담금질',
pros: '전역 탈출 우수\n국소최적 회피',
cons: '매개변수 민감\n느린 수렴',
wing: '긴급 단순 배치',
wingColor: 'var(--fg-sub)',
},
{
name: 'Greedy+휴리스틱',
color: 'var(--color-accent)',
type: '결정론적',
pros: '즉시 결과\n해석 용이',
cons: '전역최적 미보장',
wing: '실시간 초기 제안',
wingColor: 'var(--color-accent)',
},
].map((row, i) => (
<tr
key={i}
style={{
borderBottom: '1px solid rgba(255,255,255,.04)',
background: i % 2 === 1 ? 'rgba(255,255,255,.01)' : undefined,
}}
>
<td className="py-[7px] px-2.5 font-bold" style={{ color: row.color }}>
{row.name}
</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub">{row.type}</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub whitespace-pre-line">
{row.pros}
</td>
<td className="py-[7px] px-2.5 text-center text-fg-sub whitespace-pre-line">
{row.cons}
</td>
<td className="py-[7px] px-2.5 text-center" style={{ color: row.wingColor }}>
{row.wing}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
);
}
/* ──────────── PANEL 3: 유체역학 모델 ──────────── */
function FluidDynamicsPanel() {
return (
<>
{/* 유동 수치 모델 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">🌊 </div>
<div className="grid grid-cols-2 gap-3.5">
<div>
<div className="text-label-2 font-bold mb-2 text-color-info"> </div>
<div className="text-label-2 leading-[1.8] mb-2 text-fg-sub">
.
(catenary형태) .
</div>
<div
className="rounded-md p-2.5 text-label-2 leading-[2.1] bg-bg-base font-mono"
style={{ border: '1px solid rgba(59,130,246,.2)' }}
>
F<sub>D</sub> = ½ · ρ · C<sub>D</sub> · A · U<sub>n</sub>²<br />T = F<sub>D</sub> · L
/ (2·sin(α))
<br />
<span className="text-caption text-fg-disabled">
C<sub>D</sub>: (1.2), A: 수중
</span>
<br />
<span className="text-caption text-fg-disabled">T: 연결부 , α: 체인각도</span>
</div>
</div>
<div>
<div className="text-label-2 font-bold mb-2 text-color-accent">
(Splash-over)
</div>
<div className="text-label-2 leading-[1.8] mb-2 text-fg-sub">
Splash-over가
.
</div>
<div
className="rounded-md p-2.5 text-label-2 leading-[2.1] bg-bg-base font-mono"
style={{ border: '1px solid rgba(6,182,212,.2)' }}
>
Fr = U<sub>n</sub> / (g·Δρ/ρ·h)
<br />
Splash-over: Fr &gt; 0.5~0.6
<br />
<span className="text-caption text-fg-disabled">
Fr: 수정 Froude수, h: 오일펜스
</span>
<br />
<span className="text-caption text-fg-disabled">Δρ/ρ: 기름- (~0.15)</span>
</div>
</div>
</div>
</div>
{/* Catenary 변형 모델 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">🔗 (Catenary) </div>
<div className="grid grid-cols-2 gap-3.5">
<div className="text-label-2 leading-[1.8] text-fg-sub">
(Catenary) .
, <b> </b> L<sub>eff</sub>
.
<div className="mt-2 rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
y(x) = a·cosh(x/a) a<br />L<sub>arc</sub> = 2a·sinh(L<sub>span</sub>/(2a))
<br />L<sub>eff</sub> = L<sub>span</sub> · cos(φ<sub>max</sub>)<br />
<span className="text-caption text-fg-disabled">
a: catenary , φ: 최대
</span>
</div>
</div>
<div className="flex flex-col gap-1.5 text-caption text-fg-sub">
<div className="text-label-2 font-bold mb-1 text-fg-sub">
</div>
{[
{
cond: 'U < 0.3 knot',
result: 'L_eff ≈ L (직선 유지)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
},
{
cond: '0.3~0.7 knot',
result: 'L_eff = 0.8~0.95 L (경미 변형)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
},
{
cond: '0.7~1.0 knot',
result: 'L_eff = 0.5~0.8 L (Catenary 현저)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
},
{
cond: 'U > 1.0 knot',
result: '기름 통과 위험 · 배치 재계산',
bg: 'rgba(59,130,246,.05)',
bd: 'rgba(59,130,246,.12)',
danger: true,
},
].map((item, i) => (
<div
key={i}
className="px-[9px] py-1.5 rounded-[5px]"
style={{
background: item.bg,
border: `1px solid ${item.bd}`,
color: item.danger ? 'var(--color-info)' : 'var(--fg-sub)',
}}
>
{item.cond} {item.result}
</div>
))}
</div>
</div>
</div>
{/* 유막 포집 모델 */}
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2.5">🛢 </div>
<div className="grid grid-cols-2 gap-3 text-label-2 text-fg-sub">
<div>
<div className="font-bold mb-1.5 text-color-accent"> </div>
<div className="rounded-[5px] p-[9px] leading-[2] bg-bg-base font-mono">
dV<sub>oil</sub>/dt = Q<sub>in</sub> Q<sub>out</sub> Q<sub>loss</sub>
<br />Q<sub>in</sub> = U<sub>oil</sub>·h<sub>oil</sub>·L<sub>eff</sub>
<br />Q<sub>out</sub> = Q<sub>skim</sub> ( )
<br />Q<sub>loss</sub> = +
</div>
</div>
<div>
<div className="font-bold mb-1.5 text-color-accent"> </div>
<div className="text-label-2 leading-[1.7] text-fg-sub">
70~80% Skimmer
. overflow . WING이
.
</div>
</div>
</div>
</div>
</>
);
}
/* ──────────── PANEL 4: 현장 적용 ──────────── */
function FieldApplicationPanel() {
const steps = [
{
num: 1,
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
numBg: 'rgba(6,182,212,.15)',
numBd: 'rgba(6,182,212,.3)',
title: '확산예측 결과 분석 — 위협 구역 및 도달시간 산출',
desc: 'KOSPS·POSEIDON·OpenDrift 앙상블 예측 결과에서 유출유 확산 경계선(Pollution Boundary)과 각 ESI 구역별 도달 예상시간(T_arrive)을 산출합니다. 신뢰도 70% 이상 예측 경계를 기준으로 차단 전략 영역을 설정합니다.',
},
{
num: 2,
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
numBg: 'rgba(6,182,212,.15)',
numBd: 'rgba(6,182,212,.3)',
title: '해양환경 조건 확인 — 조류·파고·수심·기상 입력',
desc: 'CHARRY 채리모델 조류예측값, KMA UM 풍속·풍향, 수심격자, NGSST 수온을 자동 연계하여 각 후보 배치지점의 U_n(법선방향 유속)을 계산합니다. 임계유속 0.7 knot를 초과하는 지점은 자동으로 J형·다단 배치로 변환합니다.',
},
{
num: 3,
color: 'var(--color-caution)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
numBg: 'rgba(6,182,212,.15)',
numBd: 'rgba(6,182,212,.3)',
title: '후보 차단선 격자 탐색 — 배치 가능지점 생성',
desc: '확산 예측 경계를 따라 500m 간격 격자로 후보 배치지점을 생성합니다. 각 지점에서 조류 조건·수심·해안선 이격·방제정 접근 가능성을 동시 검토합니다. ESI 고등급 구역 전방 2km 이내 지점에 우선 가중치를 부여합니다.',
},
{
num: 4,
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
numBg: 'rgba(6,182,212,.15)',
numBd: 'rgba(6,182,212,.3)',
title: 'NSGA-II 최적화 실행 — 파레토 최적 배치안 산출',
desc: '후보 배치지점·방향각·형태 조합을 염색체로 인코딩하여 NSGA-II 다목적 유전알고리즘을 실행합니다. 수렴 후 파레토 전면에서 3~5개 추천 배치안을 제시하며, 의사결정자가 차단 효율과 자원 사용량의 트레이드오프를 보고 선택합니다.',
},
{
num: 5,
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.15)',
numBg: 'rgba(6,182,212,.15)',
numBd: 'rgba(6,182,212,.3)',
title: '실시간 재최적화 — 조석 변환·조류 변화 대응',
desc: '창조→낙조 전환 시(약 6시간 주기) 조류 방향 역전에 따른 오일펜스 재배치 알람을 발령합니다. 확산 예측이 갱신될 때마다 배치 최적화를 자동 재실행하여 방제대응 체계를 동적으로 업데이트합니다.',
},
];
return (
<>
{/* 배치 5단계 절차 */}
<div className="rounded-md p-4 mb-3.5 bg-bg-card border border-stroke">
<div className="text-label-1 font-bold mb-3">🗺 WING 5</div>
<div className="flex flex-col gap-2">
{steps.map((step) => (
<div
key={step.num}
className="flex gap-3 items-start p-3 rounded-lg"
style={{ background: step.bg, border: `1px solid ${step.bd}` }}
>
<div
className="min-w-[36px] h-[36px] rounded-[9px] flex items-center justify-center font-extrabold text-sm flex-shrink-0"
style={{
background: step.numBg,
border: `1px solid ${step.numBd}`,
color: step.color,
}}
>
{step.num}
</div>
<div className="text-label-2 text-fg-sub">
<div className="font-bold mb-1">{step.title}</div>
<div className="leading-[1.7] text-fg-sub">{step.desc}</div>
</div>
</div>
))}
</div>
</div>
{/* 해역별 적용 특성 */}
<div className="rounded-md p-3.5 bg-bg-card border border-stroke">
<div className="text-label-2 font-bold mb-2.5">📍 </div>
<div className="grid grid-cols-3 gap-2.5 text-caption text-fg-sub">
{[
{
icon: '🌊',
title: '서해 (조차 대형)',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.05)',
bd: 'rgba(59,130,246,.12)',
desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.',
},
{
icon: '🌿',
title: '남해 (다도해)',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.',
},
{
icon: '🏔️',
title: '동해 (심해형)',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.05)',
bd: 'rgba(6,182,212,.12)',
desc: '조차 소(0.3m), 너울·파고 높음. 조류 0.5~1 knot. V형 집중. 고파랑 시 배치 제한. 수온약층 고려.',
},
].map((area, i) => (
<div
key={i}
className="p-2.5 rounded-[7px]"
style={{ background: area.bg, border: `1px solid ${area.bd}` }}
>
<div className="font-bold mb-[5px]" style={{ color: area.color }}>
{area.icon} {area.title}
</div>
<div className="leading-[1.7] text-fg-sub">{area.desc}</div>
</div>
))}
</div>
</div>
</>
);
}
/* ──────────── PANEL 5: 참고문헌 ──────────── */
function ReferencesPanel() {
const categories = [
{
title: '⚙️ 최적화 알고리즘',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.1)',
bd: 'rgba(6,182,212,.25)',
refs: [
{
title: 'A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II',
author:
'Deb, K., Pratap, A., Agarwal, S., & Meyarivan, T. | IEEE Trans. Evol. Comput. 6(2):182197, 2002',
desc: 'NSGA-II 원전 · 비지배 정렬 + 혼잡도 거리 · 파레토 전면 탐색 알고리즘 · WING 다목적 최적화 엔진의 핵심 이론 기반',
},
{
title:
'An Emergency Scheduling Model for Oil Containment Boom in Dynamically Changing Marine Oil Spills',
author:
'Xu, Y., Zhang, L., Zheng, P., Liu, G., & Zhao, D. | Ningbo University | Systems 2025, 13, 716',
desc: 'IMOGWO 다목적 최적화 · 오일필름 동적 모델 · 경제·생태 손실 정량화 · 주산해역 케이스 스터디',
highlight: true,
},
{
title: '등록특허 10-1567431 기반 유출유 확산예측-방제 연동 시스템',
author: '이문진 외 | 한국해양과학기술원 | 2015',
desc: 'KOSPS 기반 확산예측-오일펜스 배치 연동 체계 원전 · ESI 방제정보지도 연동 · 취송류 경험식 기반 방향각 산정 근거',
},
],
},
{
title: '🌊 유체역학 이론',
color: 'var(--color-info)',
bg: 'rgba(59,130,246,.1)',
bd: 'rgba(59,130,246,.25)',
refs: [
{
title: 'Oil Boom Failure: Critical Velocity and Boom Design',
author: 'Leibovich, S. | Annual Review of Fluid Mechanics 8:177197, 1976',
desc: '오일펜스 임계유속 이론 원전 · Froude수 기반 Splash-over 조건 · 방향각-차단효율 관계식',
},
{
title: 'Dynamic Behavior of Oil Containment Booms in Currents',
author: 'Delvigne, G.A.L. | Spill Science & Technology Bulletin 5(3-4):181196, 1999',
desc: '조류 중 오일펜스 항력·변형 동역학 · Catenary 형태 해석 · 실용 배치 설계 기준',
},
{
title: 'Oil Boom Containment Efficiency in Waves and Currents',
author:
'Wicks, M. | Proceedings of the Joint Conference on Prevention and Control of Oil Spills, 1969',
desc: '파랑+조류 복합 환경에서 오일펜스 효율 실험 · V형·U형 성능 비교 기초 자료',
},
{
title: 'Experimental, Numerical and Optimisation Study of Oil Spill Containment Boom',
author:
'Muttin, F., Guyot, F., Nouchi, S. & Variot, B. | Coastal Environment V, WIT Press, 2004',
desc: '유체-구조 상호작용 · 1.5D/2.5D 구조모델 · 유전알고리즘 최적화 · SPH 수치해법 · ERIKA·PRESTIGE 사고 검증',
highlight: true,
},
],
},
{
title: '📐 오일펜스 배치 설계',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.1)',
bd: 'rgba(6,182,212,.25)',
refs: [
{
title: 'Optimization of an Oil Boom Arrangement',
author:
'Fang, J. & Wong, K.-F.V. | Department of Mechanical Engineering, University of Miami',
desc: '오일펜스 배치 형태(V형·U형·J형) 최적화 연구 · 조류 방향각과 차단효율 관계 수치 분석 · 방제정 예인각도별 성능 비교',
},
],
},
{
title: '🗺️ 방제 운용 기준',
color: 'var(--color-accent)',
bg: 'rgba(6,182,212,.1)',
bd: 'rgba(6,182,212,.25)',
refs: [
{
title: '기름오염방제시 오일펜스 사용지침 (ITOPF 방제기술정보문서 3)',
author: 'ITOPF | 해양경찰청·해양환경관리공단 번역 | © 2011 ITOPF Ltd.',
desc: '커튼형·펜스형·해안용 분류 · 6가지 기름 유실 메커니즘 · 힘 계산 공식 F=100·A·V² · 유속별 최대 설치각도 표',
highlight: true,
},
{
title: 'NOAA ESI 방제정보지도 기반 오일펜스 우선 배치 전략',
author:
'NOAA Office of Response and Restoration | Open Water Oil Identification Manual, 2013',
desc: 'ESI 1~10 등급별 방제 우선순위 · 오일펜스 가중 배치 전략 · 취수원·어항 보호 기준',
},
{
title: '해양경찰청 해양오염방제 업무매뉴얼 — 오일펜스 전개 절차',
author: '해양경찰청 해양오염대응국 | 방제업무 기본지침, 최신판',
desc: '국내 해역 오일펜스 운용 법적 기준 · 방제정 연계 전개 절차 · 서해/남해/동해 해역별 운용 특성',
},
],
},
];
return (
<>
<div className="text-label-1 font-bold mb-1">📚 </div>
<div className="text-label-2 mb-3.5 text-fg-disabled"> 12 · 4 </div>
{categories.map((cat, ci) => (
<div key={ci} className="mb-4">
<div
className="text-label-2 font-bold mb-[7px] flex items-center gap-1.5"
style={{ color: cat.color }}
>
<span
className="px-[7px] py-0.5 rounded"
style={{ background: cat.bg, border: `1px solid ${cat.bd}` }}
>
{cat.title}
</span>
</div>
<div className="flex flex-col gap-[5px] text-caption text-fg-sub">
{cat.refs.map((ref, ri) => (
<div
key={ri}
className="p-[9px] px-3 rounded-[7px] grid gap-2"
style={{
gridTemplateColumns: '24px 1fr',
background: 'var(--bg-card)',
border: ref.highlight ? `1px solid ${cat.bd}` : '1px solid var(--stroke-default)',
}}
>
<div
className="w-[22px] h-[22px] rounded flex items-center justify-center text-label-2 flex-shrink-0"
style={{
background: ref.highlight ? `${cat.bg}` : `${cat.bg.replace('.1', '.12')}`,
fontWeight: ref.highlight ? 700 : undefined,
color: ref.highlight ? cat.color : undefined,
}}
>
{ri + 1 === 1 ? '①' : ri + 1 === 2 ? '②' : ri + 1 === 3 ? '③' : '④'}
</div>
<div>
<div className="font-bold mb-0.5">{ref.title}</div>
<div className="leading-[1.6] text-fg-disabled">{ref.author}</div>
<div className="mt-0.5 text-fg-sub">{ref.desc}</div>
</div>
</div>
))}
</div>
</div>
))}
</>
);
}