diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100755 index 3454ce1..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,175 +0,0 @@ -/* Layer tree 3-level styles */ -.lyr-sw { - width: 28px; - height: 16px; - background: var(--bg0); - border-radius: 8px; - position: relative; - border: 1px solid var(--bd); - transition: 0.2s; - flex-shrink: 0; - cursor: pointer; -} - -.lyr-sw::after { - content: ''; - position: absolute; - top: 2px; - left: 2px; - width: 10px; - height: 10px; - background: var(--t3); - border-radius: 50%; - transition: 0.2s; -} - -.lyr-sw.on { - background: rgba(6, 182, 212, 0.3); - border-color: var(--cyan); -} - -.lyr-sw.on::after { - left: 14px; - background: var(--cyan); -} - -.lyr-sw:disabled { - opacity: 0.4; - cursor: not-allowed; -} - -/* Level 1 Group */ -.lyr-g1 { - margin-bottom: 2px; -} - -.lyr-h1 { - display: flex; - align-items: center; - gap: 6px; - padding: 7px 8px; - cursor: pointer; - border-radius: var(--rS); - transition: background 0.15s; - font-size: 12px; - font-weight: 700; - color: var(--t1); - font-family: var(--fK); -} - -.lyr-h1:hover { - background: rgba(255, 255, 255, 0.04); -} - -.lyr-h1 .lyr-arr { - font-size: 8px; - color: var(--t3); - transition: transform 0.2s; - width: 12px; - text-align: center; -} - -.lyr-h1 .lyr-arr.open { - transform: rotate(90deg); -} - -.lyr-h1-cnt { - margin-left: auto; - font-size: 10px; - font-weight: 500; - color: var(--t3); - font-family: var(--fM); -} - -.lyr-c1 { - padding-left: 6px; - overflow: hidden; - transition: max-height 0.25s ease; -} - -.lyr-c1.collapsed { - max-height: 0 !important; - overflow: hidden; -} - -/* Level 2 Group */ -.lyr-g2 { - margin-bottom: 1px; -} - -.lyr-h2 { - display: flex; - align-items: center; - gap: 6px; - padding: 5px 8px; - cursor: pointer; - border-radius: 3px; - transition: background 0.15s; - font-size: 11px; - font-weight: 600; - color: var(--t2); - font-family: var(--fK); -} - -.lyr-h2:hover { - background: rgba(255, 255, 255, 0.03); -} - -.lyr-h2 .lyr-arr { - font-size: 7px; - color: var(--t3); - transition: transform 0.2s; - width: 10px; - text-align: center; -} - -.lyr-h2 .lyr-arr.open { - transform: rotate(90deg); -} - -.lyr-h2-cnt { - margin-left: auto; - font-size: 10px; - font-weight: 500; - color: var(--t3); - font-family: var(--fM); -} - -.lyr-c2 { - padding-left: 10px; - overflow: hidden; - transition: max-height 0.25s ease; -} - -.lyr-c2.collapsed { - max-height: 0 !important; - overflow: hidden; -} - -/* Level 3 Toggle */ -.lyr-t { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 8px; - cursor: pointer; - font-size: 11px; - color: var(--t2); - transition: color 0.15s, background 0.15s; - font-family: var(--fK); - border-radius: 3px; -} - -.lyr-t:hover { - color: var(--t1); - background: rgba(255, 255, 255, 0.02); -} - -.lyr-cnt { - margin-left: auto; - font-size: 10px; - font-weight: 400; - color: var(--t3); - font-family: var(--fM); - flex-shrink: 0; -} diff --git a/frontend/src/common/components/auth/LoginPage.tsx b/frontend/src/common/components/auth/LoginPage.tsx index bfc7cce..4254ce8 100644 --- a/frontend/src/common/components/auth/LoginPage.tsx +++ b/frontend/src/common/components/auth/LoginPage.tsx @@ -107,7 +107,7 @@ export function LoginPage() {
@@ -128,8 +128,8 @@ export function LoginPage() { style={{ width: '100%', padding: '11px 14px 11px 38px', background: 'var(--bg2)', border: '1px solid var(--bd)', - borderRadius: 8, color: 'var(--t1)', fontSize: 13, - fontFamily: 'var(--fK)', outline: 'none', + borderRadius: 8, fontSize: 13, + outline: 'none', transition: 'border-color 0.2s, box-shadow 0.2s', }} onFocus={(e) => { @@ -148,7 +148,7 @@ export function LoginPage() {
@@ -168,8 +168,8 @@ export function LoginPage() { style={{ width: '100%', padding: '11px 14px 11px 38px', background: 'var(--bg2)', border: '1px solid var(--bd)', - borderRadius: 8, color: 'var(--t1)', fontSize: 13, - fontFamily: 'var(--fK)', outline: 'none', + borderRadius: 8, fontSize: 13, + outline: 'none', transition: 'border-color 0.2s, box-shadow 0.2s', }} onFocus={(e) => { @@ -191,7 +191,7 @@ export function LoginPage() { }}>
{/* ── API 설정 패널 ── */} {showConfig && (
-
⚙ 한국해운조합 API 연동 설정
+
⚙ 한국해운조합 API 연동 설정
- + setConfigEndpoint(e.target.value)} placeholder="https://api.haewoon.or.kr/v1/..." - style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} /> + style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
- + setConfigApiKey(e.target.value)} placeholder="발급받은 API Key 입력" - style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} /> + style={{ width: '100%', padding: '9px 12px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontFamily: 'var(--fM)', fontSize: 12, outline: 'none', boxSizing: 'border-box' }} />
- +
- + setSearchType(e.target.value)} className="prd-i" style={{ borderColor: 'var(--bd)', minWidth: 120 }}> @@ -172,12 +172,12 @@ function ShipInsurance() {
- + setSearchVal(e.target.value)} placeholder={placeholderMap[searchType]} - style={{ width: '100%', padding: '9px 14px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: 13, outline: 'none', boxSizing: 'border-box' }} /> + style={{ width: '100%', padding: '9px 14px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontFamily: 'var(--fM)', fontSize: 13, outline: 'none', boxSizing: 'border-box' }} />
- +
- - + +
@@ -197,15 +197,15 @@ function ShipInsurance() { {viewState === 'empty' && (
🛡
-
한국해운조합 API 연동 대기 중
-
+
한국해운조합 API 연동 대기 중
+
API 설정에서 한국해운조합 API Key를 등록하거나
MMSI·IMO·선박명으로 직접 조회하세요.
자산목록 일괄조회 시 등록된 방제자산 전체의 보험 현황을 한번에 확인할 수 있습니다.
- - + +
)} @@ -214,7 +214,7 @@ function ShipInsurance() { {viewState === 'loading' && (
-
한국해운조합 API 조회 중...
+
한국해운조합 API 조회 중...
)} @@ -231,7 +231,7 @@ function ShipInsurance() { ].map((c, i) => (
{c.val}
-
{c.label}
+
{c.label}
))}
@@ -239,14 +239,14 @@ function ShipInsurance() { {/* 테이블 */}
-
조회 결과 {resultData.length}
+
조회 결과 {resultData.length}
- - + +
- +
{[ @@ -298,7 +298,7 @@ function ShipInsurance() { {/* 경고 */} {(expiredList.length > 0 || soonList.length > 0) && ( -
+
{expiredList.length > 0 && ( <>⛔ 만료 {expiredList.length}건: {expiredList.map(r => r.shipName).join(', ')}
)} @@ -312,14 +312,14 @@ function ShipInsurance() { {/* ── API 연동 정보 푸터 ── */}
-
+
데이터 출처: 한국해운조합(KSA) · haewoon.or.kr
연동 방식: REST API (JSON) · 실시간 조회 · 캐시 TTL 1시간
- 마지막 동기화: + 마지막 동기화: {lastSync} - +
diff --git a/frontend/src/tabs/board/components/BoardView.tsx b/frontend/src/tabs/board/components/BoardView.tsx index b1db3d6..c566da4 100755 --- a/frontend/src/tabs/board/components/BoardView.tsx +++ b/frontend/src/tabs/board/components/BoardView.tsx @@ -210,16 +210,15 @@ export function BoardView() {
📘 - 해경매뉴얼 - 총 {filteredManuals.length}건 + 해경매뉴얼 + 총 {filteredManuals.length}건
{manualCategories.map(cat => (
setManualSearch(e.target.value)} - className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none' }} /> + className="px-4 py-2 text-sm rounded w-64" style={{ background: 'var(--bg2)', border: '1px solid var(--bd)', outline: 'none' }} />
@@ -243,7 +242,7 @@ export function BoardView() {
{manualLoading ? (
-

로딩 중...

+

로딩 중...

) : (
@@ -265,7 +264,7 @@ export function BoardView() { {file.version}
-
+
{file.title}
@@ -289,7 +288,7 @@ export function BoardView() { setShowUploadModal(true) }} className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all" - style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#3b82f6', fontFamily: 'var(--fK)', cursor: 'pointer' }} + style={{ background: 'rgba(59,130,246,.1)', border: '1px solid rgba(59,130,246,.2)', color: '#3b82f6', cursor: 'pointer' }} title="수정"> ✏️ 수정 @@ -305,13 +304,13 @@ export function BoardView() { } }} className="px-2 py-0.5 rounded text-[10px] font-semibold transition-all" - style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: '#ef4444', fontFamily: 'var(--fK)', cursor: 'pointer' }} + style={{ background: 'rgba(239,68,68,.1)', border: '1px solid rgba(239,68,68,.2)', color: '#ef4444', cursor: 'pointer' }} title="삭제"> 🗑️ 삭제
-
+
{file.authorNm} {new Date(file.regDtm).toLocaleDateString('ko-KR')}
@@ -353,7 +352,7 @@ export function BoardView() { }} className="px-3 py-1 rounded text-[10px] font-semibold transition-all" style={{ background: 'rgba(6,182,212,.1)', border: '1px solid rgba(6,182,212,.25)', - color: '#22d3ee', fontFamily: 'var(--fK)', cursor: 'pointer', + color: '#22d3ee', cursor: 'pointer', }}> 📥 다운로드 @@ -368,7 +367,7 @@ export function BoardView() { {!manualLoading && filteredManuals.length === 0 && (
📘
-

검색 결과가 없습니다.

+

검색 결과가 없습니다.

)}
@@ -383,14 +382,14 @@ export function BoardView() {
{editingManualId ? '✏️' : '📤'} - {editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'} + {editingManualId ? '매뉴얼 수정' : '매뉴얼 업로드'}
{ setShowUploadModal(false); setEditingManualId(null) }} style={{ cursor: 'pointer', color: 'var(--t3)', fontSize: 16, lineHeight: 1 }}>✕
- +
{['방제매뉴얼', '대응매뉴얼', '교육자료', '법령·규정'].map(cat => { const cc = catColor(cat) @@ -399,7 +398,7 @@ export function BoardView() {
- + setUploadForm(prev => ({ ...prev, title: e.target.value }))} - style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' }} /> + style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', outline: 'none', boxSizing: 'border-box' }} />
- + setUploadForm(prev => ({ ...prev, version: e.target.value }))} - style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', color: 'var(--t1)', fontFamily: 'var(--fK)', outline: 'none', boxSizing: 'border-box' }} /> + style={{ width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 12, background: 'var(--bg2)', border: '1px solid var(--bd)', outline: 'none', boxSizing: 'border-box' }} />
- +
📄
-
{uploadForm.fileName}
+
{uploadForm.fileName}
{uploadForm.fileSize}
{ e.stopPropagation(); setUploadForm(prev => ({ ...prev, fileName: '', fileSize: '' })) }} @@ -454,7 +453,7 @@ export function BoardView() { ) : ( <>
📁
-
클릭하여 파일을 선택하세요
+
클릭하여 파일을 선택하세요
PDF, DOC, HWP, XLSX (최대 100MB)
)} @@ -463,7 +462,7 @@ export function BoardView() {
diff --git a/frontend/src/tabs/hns/components/HNSAnalysisListTable.tsx b/frontend/src/tabs/hns/components/HNSAnalysisListTable.tsx index 1fc9eca..b9acd52 100755 --- a/frontend/src/tabs/hns/components/HNSAnalysisListTable.tsx +++ b/frontend/src/tabs/hns/components/HNSAnalysisListTable.tsx @@ -60,8 +60,7 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps) flexDirection: 'column', height: '100%', background: 'var(--bg0)', - fontFamily: 'var(--fK)' - }}> + }}> {/* Header */}
@@ -104,10 +102,8 @@ export function HNSAnalysisListTable({ onTabChange }: HNSAnalysisListTableProps) border: '1px solid var(--bd)', borderRadius: '6px', fontSize: '11px', - color: 'var(--t1)', - width: '200px', - fontFamily: 'var(--fK)' - }} + width: '200px', + }} />
- +
{item.hnsAnlysSn}{item.anlysNm}{item.anlysNm} 🧪
-
+
HNS 대기확산 예측
-
+
ALOHA/CAMEO 기반 대기확산 시뮬레이션
@@ -84,16 +84,16 @@ export function HNSLeftPanel({ {/* 사고 기본정보 */}
-
+
📋 사고 기본정보
{/* 사고명 */}
- + setAccidentName(e.target.value)} /> @@ -102,16 +102,16 @@ export function HNSLeftPanel({ {/* 사고일시 + 예측시간 */}
- +
- +
- +
- + onCoordChange({ ...incidentCoord, lat: parseFloat(e.target.value) || 0 })} />
- + onCoordChange({ ...incidentCoord, lon: parseFloat(e.target.value) || 0 })} />
- + setLocationName(e.target.value)} /> @@ -185,36 +184,36 @@ export function HNSLeftPanel({
- 🌬 기상정보 (자동조회) + 🌬 기상정보 (자동조회)
KMA API · 울산 AWS
5.2
-
풍속(m/s)
+
풍속(m/s)
SW 225°
-
풍향
+
풍향
8.5°C
-
기온
+
기온
62%
-
습도
+
습도
-
+
대기안정도 - D (중립) + D (중립)
-
+
지표 조도 - 해안 + 해안
@@ -222,7 +221,7 @@ export function HNSLeftPanel({ {/* 알고리즘 선택 */}
- +
- + -
+
🧪 물질 정보
{/* 물질 분류 */}
- + - + - +
@@ -317,18 +316,18 @@ export function HNSLeftPanel({ {/* 유출량 + 단위 */}
- + setAmount(e.target.value)} step="0.1" />
- + - + -
+
⚠ 물질 위험 특성
-
+
인화점 4°C
비중 - 0.867 + 0.867
증기압 - 22 mmHg + 22 mmHg
IDLH @@ -382,7 +381,7 @@ export function HNSLeftPanel({
TWA - 50 ppm + 50 ppm
AEGL-2(1h) @@ -393,10 +392,10 @@ export function HNSLeftPanel({ {/* AEGL 등급 범례 */}
-
+
📊 확산 등급 기준 (AEGL)
-
+
AEGL-3 (생명위협) — 500 ppm @@ -431,8 +430,7 @@ export function HNSLeftPanel({ fontSize: '13px', fontWeight: 700, cursor: isRunningPrediction ? 'not-allowed' : 'pointer', - fontFamily: 'var(--fK)', - transition: '0.2s', + transition: '0.2s', boxShadow: isRunningPrediction ? 'none' : '0 4px 16px rgba(249,115,22,0.25)' }} > @@ -449,8 +447,7 @@ export function HNSLeftPanel({ fontSize: '12px', fontWeight: 600, cursor: 'pointer', - fontFamily: 'var(--fK)' - }} + }} > 🔄 초기화 @@ -474,10 +471,10 @@ export function HNSLeftPanel({ fontSize: '18px' }}>📋
-
+
분석 목록
-
+
저장된 대기확산 예측 결과
@@ -485,13 +482,13 @@ export function HNSLeftPanel({ {/* 필터 섹션 */}
-
+
🔍 필터
{/* 기간 선택 */}
- + {}} @@ -506,7 +503,7 @@ export function HNSLeftPanel({ {/* 물질 분류 */}
- + {}} @@ -522,7 +519,7 @@ export function HNSLeftPanel({ {/* 위험도 */}
- + {}} @@ -539,20 +536,20 @@ export function HNSLeftPanel({ {/* 통계 요약 */}
-
+
📊 통계
- 전체 분석 + 전체 분석 8건
- 고위험 (AEGL-3) + 고위험 (AEGL-3) 3건
- 중위험 (AEGL-2) + 중위험 (AEGL-2) 5건
diff --git a/frontend/src/tabs/hns/components/HNSRecalcModal.tsx b/frontend/src/tabs/hns/components/HNSRecalcModal.tsx index cc6d065..b8e24a5 100755 --- a/frontend/src/tabs/hns/components/HNSRecalcModal.tsx +++ b/frontend/src/tabs/hns/components/HNSRecalcModal.tsx @@ -104,10 +104,10 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', }}>🔄
-

+

HNS 대기확산 재계산

-
+
물질·기상조건을 수정하여 대기확산 예측을 재실행합니다
@@ -130,7 +130,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp padding: '10px 12px', background: 'rgba(249,115,22,0.04)', border: '1px solid rgba(249,115,22,0.15)', borderRadius: '8px', }}> -
+
현재 분석 정보
@@ -205,11 +205,11 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp
-
위도 (N)
+
위도 (N)
setLat(Number(e.target.value))} style={{ fontFamily: 'var(--fM)' }} />
-
경도 (E)
+
경도 (E)
setLon(Number(e.target.value))} style={{ fontFamily: 'var(--fM)' }} />
@@ -223,7 +223,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp disabled={phase !== 'editing'} style={{ flex: 1, padding: '10px', fontSize: '12px', fontWeight: 600, - fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer', + borderRadius: '8px', cursor: 'pointer', background: 'var(--bg3)', border: '1px solid var(--bd)', color: 'var(--t2)', opacity: phase !== 'editing' ? 0.5 : 1, }} @@ -233,7 +233,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp disabled={phase !== 'editing'} style={{ flex: 2, padding: '10px', fontSize: '12px', fontWeight: 700, - fontFamily: 'var(--fK)', borderRadius: '8px', + borderRadius: '8px', cursor: phase === 'editing' ? 'pointer' : 'wait', background: phase === 'done' ? 'rgba(34,197,94,0.15)' @@ -263,7 +263,7 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit }: HNSRecalcModalProp function FG({ label, children }: { label: string; children: React.ReactNode }) { return (
-
{label}
+
{label}
{children}
) @@ -272,8 +272,8 @@ function FG({ label, children }: { label: string; children: React.ReactNode }) { function InfoRow({ label, value }: { label: string; value: string }) { return (
- {label} - {value} + {label} + {value}
) } diff --git a/frontend/src/tabs/hns/components/HNSRightPanel.tsx b/frontend/src/tabs/hns/components/HNSRightPanel.tsx index 3b84df1..ab4efd5 100755 --- a/frontend/src/tabs/hns/components/HNSRightPanel.tsx +++ b/frontend/src/tabs/hns/components/HNSRightPanel.tsx @@ -28,7 +28,6 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: borderLeft: '1px solid var(--bd)', padding: '16px', overflow: 'auto', - fontFamily: 'var(--fK)' }}>
예측 결과 @@ -157,7 +154,6 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: fontSize: '20px', fontWeight: 700, fontFamily: 'var(--fM)', - color: 'var(--t1)' }}> 5.2 m/s
@@ -180,7 +176,6 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: fontSize: '20px', fontWeight: 700, fontFamily: 'var(--fM)', - color: 'var(--t1)' }}> SW 225°
@@ -217,7 +212,6 @@ export function HNSRightPanel({ dispersionResult, onOpenRecalc, onOpenReport }: {zone.level} diff --git a/frontend/src/tabs/hns/components/HNSScenarioView.tsx b/frontend/src/tabs/hns/components/HNSScenarioView.tsx index 876c772..8d246ac 100755 --- a/frontend/src/tabs/hns/components/HNSScenarioView.tsx +++ b/frontend/src/tabs/hns/components/HNSScenarioView.tsx @@ -139,10 +139,10 @@ export function HNSScenarioView() {
📊
-
+
HNS 대기확산 시나리오 관리
-
+
시간·조건별 대기확산 예측 시나리오 비교·검토 및 대응 의사결정 지원
@@ -169,7 +169,7 @@ export function HNSScenarioView() { padding: '6px 14px', background: 'rgba(249,115,22,0.12)', border: '1px solid rgba(249,115,22,0.3)', borderRadius: '6px', color: 'var(--orange)', fontSize: '11px', fontWeight: 700, - fontFamily: 'var(--fK)', cursor: 'pointer', whiteSpace: 'nowrap', + cursor: 'pointer', whiteSpace: 'nowrap', }} > + 신규 시나리오 @@ -188,14 +188,14 @@ export function HNSScenarioView() { padding: '10px 14px', borderBottom: '1px solid var(--bd)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', }}> - + 시나리오 목록 — 톨루엔 대기확산
{['시간순', '위험도순'].map((label, i) => (
@@ -251,7 +251,7 @@ export function HNSScenarioView() { {scn.timeStep}
{scn.datetime} - {scn.wind} + {scn.wind}
{/* Metrics grid */} @@ -263,7 +263,7 @@ export function HNSScenarioView() { { label: '영향인구', value: scn.population, color: '#f87171' }, ].map((m, i) => (
-
{m.label}
+
{m.label}
{m.value}
))} @@ -272,7 +272,7 @@ export function HNSScenarioView() { {/* Description */}
{scn.description}
@@ -291,7 +291,7 @@ export function HNSScenarioView() { style={{ flex: 1, padding: '8px', borderRadius: '6px', cursor: 'pointer', background: 'rgba(249,115,22,0.1)', border: '1px solid rgba(249,115,22,0.3)', - color: 'var(--orange)', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)', + color: 'var(--orange)', fontSize: '11px', fontWeight: 700, }} > 📊 선택 시나리오 비교 @@ -299,7 +299,7 @@ export function HNSScenarioView() { @@ -367,7 +367,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) { }}>
- + {scenario.id} {scenario.name} -
{m.label}
+
{m.label}
{m.value}
))} @@ -403,7 +403,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{/* Threat Zones */}
-

+

⚠️ 위험 구역

@@ -414,7 +414,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) { { label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' }, ].map((z, i) => (
- {z.label} + {z.label} {z.value}
))} @@ -423,7 +423,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) { {/* Actions */}
-

+

🛡 대응 권고 사항

@@ -431,7 +431,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
{action} @@ -443,7 +443,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) { {/* Weather */}
-

+

🌊 기상 조건

@@ -457,8 +457,8 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) { ].map((w, i) => (
{w.icon}
-
{w.value}
-
{w.label}
+
{w.value}
+
{w.label}
))}
@@ -502,13 +502,13 @@ function ScenarioComparison() { return (
{/* Title */} -
+
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
-
+
최대 지표면 농도 (ppm) 변화 추이
@@ -545,7 +545,7 @@ function ScenarioComparison() {
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
-
+
위험 반경 (km) 변화
@@ -575,7 +575,7 @@ function ScenarioComparison() { {/* Chart 3: 영향 인구 변화 (Bar) */}
-
+
영향 인구 (명) 변화
@@ -602,10 +602,10 @@ function ScenarioComparison() { {/* ── Chart 4: 시나리오 비교표 ── */}
-
+
📋 시나리오 비교표
- +
{['지표', ...D.map(d => `${d.id} (${d.label})`)].map((h, i) => ( @@ -676,7 +676,7 @@ function ScenarioMapOverlay() { width: '80%', maxWidth: '600px', height: '300px', background: 'var(--bg2)', border: '1px solid var(--bd)', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', - color: 'var(--t3)', fontSize: '13px', fontFamily: 'var(--fK)', + color: 'var(--t3)', fontSize: '13px', }}> [시나리오별 확산범위 오버레이 지도] @@ -689,7 +689,7 @@ function ScenarioMapOverlay() { ].map((item, i) => (
- {item.label} + {item.label}
))}
@@ -758,10 +758,10 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: { display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '16px', }}>🧪
-

+

신규 HNS 대기확산 시나리오

-
+
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
@@ -816,7 +816,7 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: { { label: 'ERPG-2', value: mat.erpg2 }, ].map((p, i) => (
-
{p.label}
+
{p.label}
{p.value}
))} @@ -879,12 +879,12 @@ function NewScenarioModal({ isOpen, onClose, onSubmit }: {
@@ -213,7 +213,7 @@ ${styles} key={idx} onClick={() => setActiveTab(idx)} className={activeTab === idx ? 'wx-tbtn on' : 'wx-tbtn'} - style={{ flex: 1, padding: 8, fontSize: 11, fontFamily: 'var(--fK)' }} + style={{ flex: 1, padding: 8, fontSize: 11 }} >{tab.icon} {tab.label} ))} @@ -224,10 +224,10 @@ ${styles}
- SEBC 해양 거동 분류 체계 + SEBC 해양 거동 분류 체계 Standard European Behaviour Classification
-
+
HNS 물질이 해양에 유출되었을 때의 물리·화학적 거동에 따라 분류하는 국제 표준 체계입니다. 물질의 밀도, 증기압, 용해도에 따라 5개 주요 거동 유형과 혼합 유형으로 구분되며, 각 유형별로 대응 전략이 달라집니다.
@@ -235,47 +235,47 @@ ${styles}
💨
G
-
Gas
-
기체 상태로 대기 중 확산. 증기압이 높아 빠르게 증발
+
Gas
+
기체 상태로 대기 중 확산. 증기압이 높아 빠르게 증발
대기확산 모델 적용
{/* E: Evaporator */}
🔥
E
-
Evaporator
-
해수면에서 증발. 부유 후 기화하여 독성 가스 생성
+
Evaporator
+
해수면에서 증발. 부유 후 기화하여 독성 가스 생성
대기+해양 복합 대응
{/* F: Floater */}
🟡
F
-
Floater
-
{'해수면 위에 부유. 비중 < 1.0, 불용성 물질'}
+
Floater
+
{'해수면 위에 부유. 비중 < 1.0, 불용성 물질'}
오일펜스 유사 봉쇄
{/* D: Dissolver */}
💧
D
-
Dissolver
-
해수에 용해. 수중 확산하여 넓은 범위 오염
+
Dissolver
+
해수에 용해. 수중 확산하여 넓은 범위 오염
해양확산 모델 적용
{/* S: Sinker */}
S
-
Sinker
-
{'해저로 침강. 비중 > 1.0, 저층 오염 축적'}
+
Sinker
+
{'해저로 침강. 비중 > 1.0, 저층 오염 축적'}
저층 3D 모니터링 필수
{/* 복합 거동 */}
-
🔀 복합 거동 유형
-
+
🔀 복합 거동 유형
+
GD
기체+용해
ED
증발+용해
FE
부유+증발
@@ -295,7 +295,7 @@ ${styles} {categories.map(cat => (
+
📊 주요 HNS 물질별 위험도 기준 비교 (ppm)
+
@@ -559,7 +559,7 @@ ${styles}
물질
-
※ AEGL: 60분 기준 / ERPG: 1시간 노출 / IDLH: 30분 / LFL: 폭발하한
+
※ AEGL: 60분 기준 / ERPG: 1시간 노출 / IDLH: 30분 / LFL: 폭발하한
)} @@ -570,16 +570,16 @@ ${styles} {/* ── 검색창 ── */}
-
🔍 HNS 물질 통합 검색 (화물약어·제품명·동의어·CAS·UN번호 통합)
+
🔍 HNS 물질 통합 검색 (화물약어·제품명·동의어·CAS·UN번호 통합)
- - + +
- 구분: - { setHmsSearchType(e.target.value as typeof hmsSearchType); setHmsPage(1) }} style={{ padding: '6px 10px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', fontSize: 10, outline: 'none', minWidth: 130 }}> @@ -587,10 +587,10 @@ ${styles}
- { setHmsSearchInput(e.target.value); setHmsPage(1) }} placeholder={hmsSearchType === 'abbr' ? '검색어 입력 (부호·띄어쓰기 제외)' : hmsSearchType === 'cas' ? 'CAS번호 입력 (예: 71-43-2)' : hmsSearchType === 'un' ? 'UN번호 입력 (예: 1114)' : '검색어 입력 (* 동의어 검색)'} style={{ flex: 1, padding: '8px 12px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t1)', fontSize: 11, fontFamily: 'var(--fK)', outline: 'none' }} /> - + { setHmsSearchInput(e.target.value); setHmsPage(1) }} placeholder={hmsSearchType === 'abbr' ? '검색어 입력 (부호·띄어쓰기 제외)' : hmsSearchType === 'cas' ? 'CAS번호 입력 (예: 71-43-2)' : hmsSearchType === 'un' ? 'UN번호 입력 (예: 1114)' : '검색어 입력 (* 동의어 검색)'} style={{ flex: 1, padding: '8px 12px', borderRadius: 6, border: '1px solid var(--bd)', background: 'var(--bg0)', fontSize: 11, outline: 'none' }} /> +
-
+
※ 국문명·영문명 검색 시 동의어까지 검색  |  약자/제품명 검색 시 부호, 띄어쓰기 제외 후 검색  |  총 1,316종 등록 (화물적부도 277종 / 해양시설 56종 / 용선자 화물코드 983종)
@@ -598,13 +598,13 @@ ${styles} {/* ── 검색 결과 테이블 ── */}
-
📋 검색 결과 — {hmsTotal}건 조회
- { setHmsFilterSebc(e.target.value); setHmsPage(1) }} style={{ padding: '3px 8px', borderRadius: 4, border: '1px solid var(--bd)', background: 'var(--bg0)', color: 'var(--t2)', fontSize: 9, outline: 'none' }}>
- +
@@ -630,7 +630,7 @@ ${styles} > - + @@ -644,7 +644,7 @@ ${styles}
No.{(hmsPage - 1) * HMS_PER_PAGE + idx + 1} {s.abbreviation}{s.nameEn}{s.nameEn} {s.synonymsEn} { e.stopPropagation(); setHmsSelectedId(s.id); setHmsDetailTab(0) }}>{s.nameKr} {s.synonymsKr}
-
+
{hmsTotal.toLocaleString()}종 등록 · Port-MIS 화물적부도 연동 · 해경청 물질정보집 · IBC CODE 692종
@@ -678,7 +678,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* Tab Navigation */}
{tabLabels.map((label, i) => ( - + ))}
@@ -689,22 +689,22 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
🧪
-
{s.nameKr} ({s.nameEn})
+
{s.nameKr} ({s.nameEn})
CAS: {s.casNumber} UN: {s.unNumber} - 운송방법: {s.transportMethod} - SEBC: {s.sebc} + 운송방법: {s.transportMethod} + SEBC: {s.sebc}
-
유사명: {s.synonymsKr}  |  특성: {s.hazardClass}
+
유사명: {s.synonymsKr}  |  특성: {s.hazardClass}
{/* Left: 물리·화학적 특성 */}
-
⚗️ 물리·화학적 특성
-
+
⚗️ 물리·화학적 특성
+
{([ ['용도', s.usage, 'var(--cyan)'], ['상태', s.state, 'var(--cyan)'], @@ -729,7 +729,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* Right: NFPA + 위험등급 */}
-
⚠️ 위험등급·농도기준
+
⚠️ 위험등급·농도기준
@@ -743,15 +743,15 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {nfpa.reactivity} -
NFPA 704
+
NFPA 704
-
+
건강(적) {nfpa.health} — {nfpa.health >= 4 ? '치명적' : nfpa.health >= 3 ? '중상' : nfpa.health >= 2 ? '장해' : nfpa.health >= 1 ? '경미한 손상' : '무해'}
인화성(황) {nfpa.fire} — {nfpa.fire >= 4 ? '93°F 미만' : nfpa.fire >= 3 ? '100°F 미만' : nfpa.fire >= 2 ? '200°F 미만' : nfpa.fire >= 1 ? '200°F 이상' : '비가연'}
반응성(청) {nfpa.reactivity} — {nfpa.reactivity >= 3 ? '폭발 가능' : nfpa.reactivity >= 2 ? '격렬 반응' : nfpa.reactivity >= 1 ? '불안정 가능' : '안정'}
-
+
@@ -770,20 +770,20 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* 방제거리 */}
-
🚧 방제거리 (ERG {s.ergNumber})
+
🚧 방제거리 (ERG {s.ergNumber})
-
🔥 화재 시
-
격리거리: {s.responseDistanceFire} 이상
+
🔥 화재 시
+
격리거리: {s.responseDistanceFire} 이상
-
💨 유출 시 (비화재)
-
주간 방호활동거리: {s.responseDistanceSpillDay}
야간 방호활동거리: {s.responseDistanceSpillNight}
+
💨 유출 시 (비화재)
+
주간 방호활동거리: {s.responseDistanceSpillDay}
야간 방호활동거리: {s.responseDistanceSpillNight}
-
🌊 해상 유출 시
-
{s.marineResponse}
+
🌊 해상 유출 시
+
{s.marineResponse}
@@ -792,9 +792,9 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* PPE */}
-
🛡 개인보호장구 (PPE) 추천
+
🛡 개인보호장구 (PPE) 추천
-
+
🧑‍🚒
근거리
@@ -810,16 +810,16 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* MSDS */}
-
📄 MSDS 주요 정보
- +
📄 MSDS 주요 정보
+
-
- §2 유해성·위험성: {s.msds.hazard}
- §4 응급조치: {s.msds.firstAid}
- §5 소화방법: {s.msds.fireFighting}
- §6 누출대응: {s.msds.spillResponse}
- §8 노출방지: {s.msds.exposure}
- §15 법적규제: {s.msds.regulation} +
+ §2 유해성·위험성: {s.msds.hazard}
+ §4 응급조치: {s.msds.firstAid}
+ §5 소화방법: {s.msds.fireFighting}
+ §6 누출대응: {s.msds.spillResponse}
+ §8 노출방지: {s.msds.exposure}
+ §15 법적규제: {s.msds.regulation}
@@ -834,9 +834,9 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* IBC CODE */}
-
⚓ IBC CODE 기반 주요 내용
+
⚓ IBC CODE 기반 주요 내용
-
+
{([ ['위험성', s.ibcHazard], @@ -848,7 +848,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H ] as [string, string][]).map(([label, value]) => (
{label}
-
{value}
+
{value}
))}
@@ -873,9 +873,9 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* EmS */}
-
🆘 비상대응핸드북 (EmS) — ERG {s.ergNumber}
+
🆘 비상대응핸드북 (EmS) — ERG {s.ergNumber}
-
+
🔥 화재 대응
{s.emsFire}
@@ -889,7 +889,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
{s.emsFirstAid}
- +
@@ -904,11 +904,11 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* 화물적부도 */}
-
📋 화물적부도 화물코드
-
클릭 시 물질검색창으로 이동
+
📋 화물적부도 화물코드
+
클릭 시 물질검색창으로 이동
- +
@@ -936,11 +936,11 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H {/* 항구별 코드 */}
-
🏗 항구별 코드
-
Port-MIS 위험물반입신고현황 연동
+
🏗 항구별 코드
+
Port-MIS 위험물반입신고현황 연동
-
화물코드 약자/제품명
+
@@ -953,7 +953,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H const freqBg = p.frequency === '높음' ? 'rgba(239,68,68,.1)' : p.frequency === '중간' ? 'rgba(249,115,22,.1)' : 'rgba(34,197,94,.1)' return ( - + @@ -963,7 +963,7 @@ function HmsDetailPanel({ substance: s, activeTab, onTabChange }: { substance: H
항구 청코드
{p.port}{p.port} {p.portCode} {p.lastImport} {p.frequency}
- +
diff --git a/frontend/src/tabs/hns/components/HNSTheoryView.tsx b/frontend/src/tabs/hns/components/HNSTheoryView.tsx index 39c33bf..aed6ff5 100755 --- a/frontend/src/tabs/hns/components/HNSTheoryView.tsx +++ b/frontend/src/tabs/hns/components/HNSTheoryView.tsx @@ -1476,11 +1476,11 @@ ${styles}
📐
-
HNS 대기확산 모델 이론 및 검증
-
WRF-Chem · Gaussian Plume/Puff · ROMS · 해양환경 보정 — Based on Lee Moon-Jin et al.
+
HNS 대기확산 모델 이론 및 검증
+
WRF-Chem · Gaussian Plume/Puff · ROMS · 해양환경 보정 — Based on Lee Moon-Jin et al.
- +
{/* Sub-tabs */} @@ -1488,7 +1488,7 @@ ${styles} {theoryTabs.map((tab, i) => (
{/* SEBC 거동 분류 */}
-
SEBC 거동 분류 (Standard European Behaviour Classification)
-
물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류
+
SEBC 거동 분류 (Standard European Behaviour Classification)
+
물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요 범주 + 7가지 하위 범주로 분류
{[ { icon: '💨', label: 'G — 가스', desc: '대기 중 확산\n증기압 > 101.3kPa\n예: 암모니아, 염소', color: 'rgba(139,92,246' }, @@ -61,8 +61,8 @@ function HNSManualViewer() { ].map(s => (
{s.icon}
-
{s.label}
-
{s.desc}
+
{s.label}
+
{s.desc}
))}
@@ -70,7 +70,7 @@ function HNSManualViewer() { {/* IMDG Code 위험물 등급 */}
-
IMDG Code 위험물 등급 (Hazard Classification)
+
IMDG Code 위험물 등급 (Hazard Classification)
{[ { icon: '💥', label: 'Class 1 — 폭발물', sub: 'Explosives', bg: 'rgba(249,115,22,.15)' }, @@ -86,8 +86,8 @@ function HNSManualViewer() {
{c.icon}
-
{c.label}
-
{c.sub}
+
{c.label}
+
{c.sub}
))} @@ -96,8 +96,8 @@ function HNSManualViewer() { {/* HNS 사고 대응 프로세스 */}
-
HNS 사고 대응 프로세스
-
+
HNS 사고 대응 프로세스
+
{[ { icon: '🚨', step: '1단계: 사고 통보', desc: 'HNS 유출 감지\nGMDSS/DSC 신호\n관계기관 통보', color: 'rgba(239,68,68', textColor: '#f87171' }, { icon: '🔍', step: '2단계: 상황 평가', desc: '물질 식별 (UN번호)\nSEBC 거동 판단\n위험 구역 설정', color: 'rgba(249,115,22', textColor: '#fb923c' }, @@ -121,7 +121,7 @@ function HNSManualViewer() {
{/* 선박 중심 조치 */}
-
🚢 선박 중심 조치
+
🚢 선박 중심 조치
{[ { icon: '🚁', title: '긴급 승선 (Emergency Boarding)', desc: '전문 대응팀의 사고 선박 접근 및 상황 파악' }, @@ -132,8 +132,8 @@ function HNSManualViewer() { ].map(item => (
{item.icon} -
- {item.title}
+
+ {item.title}
{item.desc}
@@ -142,7 +142,7 @@ function HNSManualViewer() {
{/* 오염물질 중심 조치 */}
-
🧪 오염물질 중심 조치
+
🧪 오염물질 중심 조치
{[ { icon: '💨', title: '대기 확산 모니터링', desc: '가스/증발 물질의 대기 농도 감시 (ALOHA/CAMEO)' }, @@ -153,8 +153,8 @@ function HNSManualViewer() { ].map(item => (
{item.icon} -
- {item.title}
+
+ {item.title}
{item.desc}
@@ -167,8 +167,8 @@ function HNSManualViewer() {
{/* PPE */}
-
🦺 개인보호장비 (PPE)
-
+
🦺 개인보호장비 (PPE)
+
{[ { level: 'Level A', desc: '완전 밀폐형 화학보호복 + SCBA\n증기/가스 직접 노출 구역', color: '#ef4444' }, { level: 'Level B', desc: '비밀폐형 화학보호복 + SCBA\n액체 스플래시 위험 구역', color: '#f97316' }, @@ -184,8 +184,8 @@ function HNSManualViewer() {
{/* 안전구역 */}
-
🔴 안전 구역 설정
-
+
🔴 안전 구역 설정
+
HOT ZONE 직접 위험구역
Level A/B PPE 필수
@@ -202,12 +202,12 @@ function HNSManualViewer() {
{/* 노출한계 (AEGL) */}
-
📊 노출한계 (AEGL 기준)
-
Acute Exposure Guideline Levels (EPA)
암모니아(NH₃) 예시 — ppm 기준
+
📊 노출한계 (AEGL 기준)
+
Acute Exposure Guideline Levels (EPA)
암모니아(NH₃) 예시 — ppm 기준
- + @@ -221,7 +221,7 @@ function HNSManualViewer() { { label: 'AEGL-3', color: '#f87171', vals: [2700, 1600, 1100, 550] }, ].map((row, ri) => ( - + {row.vals.map((v, vi) => ( ))} @@ -229,7 +229,7 @@ function HNSManualViewer() { ))}
구분구분 10분 30분 60분
{row.label}{row.label}{v}
-
+
AEGL-1: 불쾌감 (비장애성)
AEGL-2: 심각한 건강 영향 (비가역적)
AEGL-3: 생명 위협 또는 사망 @@ -238,7 +238,7 @@ function HNSManualViewer() {
{/* 출처 */} -
+
출처: Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo Project, 2024 한국어판)
번역: 원해민, 이시연, 양보경, 강성길, 이성엽 — KRISO 선박해양플랜트연구소 / NOWPAP MERRAC
원본: Alcaro L., Brandt J., Giraud W., Mannozzi M., Nicolas-Kopec A. (2021) ISBN: 978-2-87893-147-1 diff --git a/frontend/src/tabs/incidents/components/IncidentsLeftPanel.tsx b/frontend/src/tabs/incidents/components/IncidentsLeftPanel.tsx index 20755b2..d5029bb 100755 --- a/frontend/src/tabs/incidents/components/IncidentsLeftPanel.tsx +++ b/frontend/src/tabs/incidents/components/IncidentsLeftPanel.tsx @@ -181,8 +181,8 @@ export function IncidentsLeftPanel({ onChange={(e) => { setSearchTerm(e.target.value); resetPage() }} style={{ width: '100%', padding: '8px 12px 8px 32px', background: 'var(--bg0)', - border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', - fontFamily: 'var(--fK)', fontSize: '12px', outline: 'none', + border: '1px solid var(--bd)', borderRadius: 'var(--rS)', + fontSize: '12px', outline: 'none', }} />
@@ -192,14 +192,14 @@ export function IncidentsLeftPanel({
{ setDateFrom(e.target.value); setSelectedPeriod(''); resetPage() }} - style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }} + style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }} /> ~ { setDateTo(e.target.value); setSelectedPeriod(''); resetPage() }} - style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', color: 'var(--t1)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }} + style={{ padding: '5px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', fontFamily: 'var(--fM)', fontSize: '11px', outline: 'none', flex: 1 }} /> - +
{/* Period Presets */} @@ -208,14 +208,14 @@ export function IncidentsLeftPanel({ ))}
{/* Today Summary */}
-
+
📅 오늘 ({todayLabel}) 사고 현황
@@ -224,13 +224,13 @@ export function IncidentsLeftPanel({ const isActive = selectedRegion === r return ( @@ -250,7 +250,7 @@ export function IncidentsLeftPanel({
{/* Count */} -
+
총 {filteredIncidents.length}건
{/* Incident List */}
{pagedIncidents.length === 0 ? ( -
+
검색 결과가 없습니다.
) : pagedIncidents.map(inc => { @@ -299,7 +299,7 @@ export function IncidentsLeftPanel({ > {/* Row 1: name + status */}
-
+
{inc.name}
@@ -308,7 +308,7 @@ export function IncidentsLeftPanel({
{/* Row 2: meta */} -
+
📅 {inc.date} {inc.time} 🏛 {inc.office}
@@ -316,17 +316,17 @@ export function IncidentsLeftPanel({
{inc.causeType && ( - + {inc.causeType} )} {inc.oilType && ( - + {inc.oilType} )} {inc.prediction && ( - + {inc.prediction} )} @@ -370,8 +370,8 @@ export function IncidentsLeftPanel({
-
- 총 {filteredIncidents.length}건 중 {(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filteredIncidents.length)} +
+ 총 {filteredIncidents.length}건 중 {(safePage - 1) * pageSize + 1}-{Math.min(safePage * pageSize, filteredIncidents.length)}
setCurrentPage(1)} /> @@ -383,7 +383,7 @@ export function IncidentsLeftPanel({ = totalPages} onClick={() => setCurrentPage(totalPages)} />
@@ -429,7 +429,7 @@ const WeatherPopup = forwardRef 🌤
-
{data.locNm}
+
{data.locNm}
{data.obsDtm}
@@ -442,13 +442,13 @@ const WeatherPopup = forwardRef
{data.icon}
-
{data.temp}
-
{data.weatherDc}
+
{data.temp}
+
{data.weatherDc}
{/* Detail grid */} -
+
@@ -466,7 +466,7 @@ const WeatherPopup = forwardRef
-
고조 (만조)
+
고조 (만조)
{data.highTide}
@@ -477,7 +477,7 @@ const WeatherPopup = forwardRef
-
저조 (간조)
+
저조 (간조)
{data.lowTide}
@@ -485,7 +485,7 @@ const WeatherPopup = forwardRef -
24h 예보
+
24h 예보
{data.forecast.map((f, i) => (
@@ -502,8 +502,8 @@ const WeatherPopup = forwardRef -
⚠ 방제 작업 영향
-
{data.impactDc}
+
⚠ 방제 작업 영향
+
{data.impactDc}
@@ -520,7 +520,7 @@ function WxCell({ icon, label, value }: { icon: string; label: string; value: st {icon}
{label}
-
{value}
+
{value}
) diff --git a/frontend/src/tabs/incidents/components/IncidentsRightPanel.tsx b/frontend/src/tabs/incidents/components/IncidentsRightPanel.tsx index fecdebc..322572a 100755 --- a/frontend/src/tabs/incidents/components/IncidentsRightPanel.tsx +++ b/frontend/src/tabs/incidents/components/IncidentsRightPanel.tsx @@ -129,7 +129,7 @@ export function IncidentsRightPanel({ borderLeft: '1px solid var(--bd)', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', }}> -
+
📊
좌측에서 사고를 선택하면
통합분석 조회가 표시됩니다
@@ -145,10 +145,10 @@ export function IncidentsRightPanel({ }}> {/* Header */}
-
+
🔬 통합분석 조회
-
+
선택: {incident.name}
@@ -171,13 +171,13 @@ export function IncidentsRightPanel({
{sec.icon} - + {sec.title}
diff --git a/frontend/src/tabs/incidents/components/IncidentsView.tsx b/frontend/src/tabs/incidents/components/IncidentsView.tsx index e4444e1..e41c96c 100755 --- a/frontend/src/tabs/incidents/components/IncidentsView.tsx +++ b/frontend/src/tabs/incidents/components/IncidentsView.tsx @@ -255,10 +255,10 @@ export function IncidentsView() { }} >
- + 🔬 통합 분석 비교 - + {selectedIncident?.name}
@@ -270,8 +270,7 @@ export function IncidentsView() { borderRadius: 8, fontSize: 8, fontWeight: 600, - fontFamily: 'var(--fK)', - background: `${t.color}18`, + background: `${t.color}18`, border: `1px solid ${t.color}40`, color: t.color, }} @@ -297,8 +296,7 @@ export function IncidentsView() { borderRadius: 3, fontSize: 9, fontWeight: 600, - fontFamily: 'var(--fK)', - cursor: 'pointer', + cursor: 'pointer', background: viewMode === v.mode ? 'rgba(6,182,212,0.12)' : 'var(--bg3)', border: viewMode === v.mode ? '1px solid rgba(6,182,212,0.3)' : '1px solid var(--bd)', color: viewMode === v.mode ? 'var(--cyan)' : 'var(--t3)', @@ -314,8 +312,7 @@ export function IncidentsView() { borderRadius: 3, fontSize: 9, fontWeight: 600, - fontFamily: 'var(--fK)', - cursor: 'pointer', + cursor: 'pointer', background: 'rgba(239,68,68,0.06)', border: '1px solid rgba(239,68,68,0.2)', color: '#f87171', @@ -352,7 +349,7 @@ export function IncidentsView() { >
{incidentPopup.incident.name} @@ -389,8 +386,7 @@ export function IncidentsView() { borderRadius: 8, padding: '8px 12px', boxShadow: '0 8px 24px rgba(0,0,0,0.5)', - fontFamily: 'var(--fK)', - minWidth: 150, + minWidth: 150, }} > {hoverInfo.type === 'vessel' ? ( @@ -481,7 +477,7 @@ export function IncidentsView() { animation: 'pd 1.5s infinite', }} /> - + AIS Live MarineTraffic @@ -516,7 +512,7 @@ export function IncidentsView() { gap: 6, }} > -
+
사고 상태
@@ -527,7 +523,7 @@ export function IncidentsView() { ].map(s => (
- {s.l} + {s.l}
))}
@@ -536,8 +532,7 @@ export function IncidentsView() { fontSize: 9, fontWeight: 700, color: 'var(--t2)', - fontFamily: 'var(--fK)', - marginTop: 2, + marginTop: 2, }} > AIS 선박 @@ -554,7 +549,7 @@ export function IncidentsView() { borderBottom: `7px solid ${vl.color}`, }} /> - {vl.type} + {vl.type}
))}
@@ -604,7 +599,7 @@ export function IncidentsView() { flexShrink: 0, }} > - + {analysisTags[0] ? `${analysisTags[0].icon} ${analysisTags[0].label}` : '— 분석 결과를 선택하세요 —'} @@ -636,7 +631,7 @@ export function IncidentsView() { flexShrink: 0, }} > - + {analysisTags[1] ? `${analysisTags[1].icon} ${analysisTags[1].label}` : '— 분석 결과를 선택하세요 —'} @@ -682,7 +677,7 @@ export function IncidentsView() { padding: '0 10px', }} > - + 🛢 유출유 확산예측
@@ -723,7 +718,7 @@ export function IncidentsView() { padding: '0 10px', }} > - + 🧪 HNS 대기확산
@@ -756,7 +751,7 @@ export function IncidentsView() { padding: '0 10px', }} > - + 🚨 긴급구난
@@ -794,7 +789,7 @@ export function IncidentsView() { justifyContent: 'space-between', }} > -
+
📊 {selectedIncident?.name} · {analysisTags.map(t => t.label).join(' + ')} 분석 결과 비교
@@ -804,8 +799,7 @@ export function IncidentsView() { borderRadius: 4, fontSize: 9, fontWeight: 600, - fontFamily: 'var(--fK)', - cursor: 'pointer', + cursor: 'pointer', background: 'rgba(59,130,246,0.1)', border: '1px solid rgba(59,130,246,0.2)', color: '#58a6ff', @@ -819,8 +813,7 @@ export function IncidentsView() { borderRadius: 4, fontSize: 9, fontWeight: 600, - fontFamily: 'var(--fK)', - cursor: 'pointer', + cursor: 'pointer', background: 'rgba(168,85,247,0.1)', border: '1px solid rgba(168,85,247,0.2)', color: '#a78bfa', @@ -866,8 +859,7 @@ function SplitPanelContent({ justifyContent: 'center', color: 'var(--t3)', fontSize: 11, - fontFamily: 'var(--fK)', - }} + }} > R&D 분석 결과를 선택하세요
@@ -937,13 +929,13 @@ function SplitPanelContent({ }} >
{tag.icon} {data.title}
{data.model}
{incident && ( -
+
사고: {incident.name} · {incident.date} {incident.time}
)} @@ -962,7 +954,7 @@ function SplitPanelContent({ background: i % 2 === 0 ? 'var(--bg1)' : 'var(--bg2)', }} > - {item.label} + {item.label} 💡 {data.summary} @@ -1006,7 +997,7 @@ function SplitPanelContent({ }} >
{tag.icon}
-
시각화 영역
+
시각화 영역
) @@ -1041,8 +1032,7 @@ function VesselPopupPanel({ borderRadius: 12, boxShadow: '0 16px 48px rgba(0,0,0,0.6)', overflow: 'hidden', - fontFamily: 'var(--fK)', - }} + }} > {/* Header */}
{v.typS} @@ -1129,8 +1118,7 @@ function VesselPopupPanel({ fontSize: 8, fontWeight: 700, color: statusColor, - fontFamily: 'var(--fK)', - }} + }} > {v.status} @@ -1177,8 +1165,7 @@ function VesselPopupPanel({ fontWeight: 700, cursor: 'pointer', textAlign: 'center', - fontFamily: 'var(--fK)', - background: 'rgba(59,130,246,0.12)', + background: 'rgba(59,130,246,0.12)', border: '1px solid rgba(59,130,246,0.3)', color: '#58a6ff', }} @@ -1194,8 +1181,7 @@ function VesselPopupPanel({ fontWeight: 700, cursor: 'pointer', textAlign: 'center', - fontFamily: 'var(--fK)', - background: 'rgba(168,85,247,0.1)', + background: 'rgba(168,85,247,0.1)', border: '1px solid rgba(168,85,247,0.25)', color: '#a78bfa', }} @@ -1211,8 +1197,7 @@ function VesselPopupPanel({ fontWeight: 700, cursor: 'pointer', textAlign: 'center', - fontFamily: 'var(--fK)', - background: 'rgba(6,182,212,0.1)', + background: 'rgba(6,182,212,0.1)', border: '1px solid rgba(6,182,212,0.25)', color: '#22d3ee', }} @@ -1353,8 +1338,7 @@ function VesselDetailModal({ vessel: v, onClose }: { vessel: Vessel; onClose: () color: tab === t.key ? '#58a6ff' : '#8b949e', cursor: 'pointer', borderBottom: tab === t.key ? '2px solid #58a6ff' : '2px solid transparent', - fontFamily: 'var(--fK)', - background: 'none', + background: 'none', border: 'none', whiteSpace: 'nowrap', transition: '0.15s', @@ -1738,8 +1722,7 @@ function TabInsurance(_props: { v: Vessel }) { fontSize: 9, color: '#8b949e', lineHeight: 1.6, - fontFamily: 'var(--fK)', - }} + }} > 💡 보험정보는 한국해운조합(KSA) Open API 및 해양수산부 선박정보시스템 연동 데이터입니다. 실시간 갱신 주기: 24시간
@@ -1800,8 +1783,7 @@ function TabDangerous({ v }: { v: Vessel }) { alignItems: 'center', gap: 8, fontSize: 11, - fontFamily: 'var(--fK)', - whiteSpace: 'nowrap', + whiteSpace: 'nowrap', }} > 화물창 2개이상 여부 @@ -1834,8 +1816,7 @@ function TabDangerous({ v }: { v: Vessel }) { fontWeight: 600, color: '#58a6ff', cursor: 'pointer', - fontFamily: 'var(--fK)', - whiteSpace: 'nowrap', + whiteSpace: 'nowrap', flexShrink: 0, }} > @@ -1898,8 +1879,7 @@ function TabDangerous({ v }: { v: Vessel }) { fontSize: 9, color: '#8b949e', lineHeight: 1.6, - fontFamily: 'var(--fK)', - }} + }} > 💡 위험물정보는 PORT-MIS(항만운영정보시스템) 위험물 반입신고 데이터 연동입니다. IMDG Code 최신 개정판(Amendment 42-24) 기준. @@ -1936,7 +1916,7 @@ function EmsRow({ {icon}
{label}
-
{value}
+
{value}
) @@ -1965,8 +1945,7 @@ function ActionBtn({ fontWeight: 700, cursor: 'pointer', textAlign: 'center', - fontFamily: 'var(--fK)', - background: bg, + background: bg, border: `1px solid ${bd}`, color: fg, }} diff --git a/frontend/src/tabs/incidents/components/MediaModal.tsx b/frontend/src/tabs/incidents/components/MediaModal.tsx index 5f71937..57c0305 100755 --- a/frontend/src/tabs/incidents/components/MediaModal.tsx +++ b/frontend/src/tabs/incidents/components/MediaModal.tsx @@ -59,7 +59,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: }}>
현장정보를 불러오는 중...
@@ -96,7 +96,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
📋
-
+
현장정보 — {incident.name}
@@ -110,7 +110,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: {MEDIA_TABS.map(t => ( @@ -138,7 +138,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: flexShrink: 0, padding: '6px 20px', borderBottom: '1px solid #21262d', display: 'flex', alignItems: 'center', gap: 10, }}> - TIMELINE + TIMELINE
{timelineDots.map((d, i) => ( @@ -173,7 +173,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: }}>
📷 - + 현장사진 — {str(media.photoMeta, 'title', '현장 사진')}
@@ -184,7 +184,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: {/* Photo content */}
📷
-
+
{incident.name.replace('유류유출', '유출 현장').replace('오염', '현장')} 해상 사진
@@ -205,10 +205,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: ))}
- + 📷 사진 {num(media.photoMeta, 'thumbCount')}장 · {str(media.photoMeta, 'stage')} - 🔗 R&D 연계 + 🔗 R&D 연계
@@ -223,7 +223,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: }}>
🎬 - + 드론 영상 — {str(media.droneMeta, 'title', '드론 영상')}
@@ -234,7 +234,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
🎬
-
+
드론 항공 촬영 영상
@@ -255,12 +255,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: 02:34 / {str(media.droneMeta, 'duration')}
- + 🎬 영상 {num(media.droneMeta, 'videoCount')}건 · {str(media.droneMeta, 'stage')}
- 📂 전체보기 - 🔗 R&D 연계 + 📂 전체보기 + 🔗 R&D 연계
@@ -276,7 +276,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: }}>
🛰 - + 위성영상 — {str(media.satMeta, 'title', '위성영상')}
@@ -295,7 +295,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: {str(media.satMeta, 'detection')}
🛰
-
+
{str(media.satMeta, 'title', '위성영상')} 위성영상
@@ -306,7 +306,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: {str(media.satMeta, 'detection') === '—' && (
🛰
-
위성영상 없음
+
위성영상 없음
)}
@@ -325,10 +325,10 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
)}
- + 🛰 위성 {num(media.satMeta, 'thumbCount')}장 · {str(media.satMeta, 'sensor')} - 🔍 편집/측 비교 + 🔍 편집/측 비교
@@ -343,7 +343,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: }}>
📹 - + CCTV — {str(media.cctvMeta, 'title', 'CCTV')}
@@ -364,7 +364,7 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose:
)}
📹
-
+
{str(media.cctvMeta, 'title', 'CCTV').replace('#', 'CCTV #')}
@@ -385,12 +385,12 @@ export function MediaModal({ incident, onClose }: { incident: Incident; onClose: ))}
- + 📹 CCTV {num(media.cctvMeta, 'camCount')}채널 · {str(media.cctvMeta, 'location')}
- 🔴 녹화영상 - 🎥 PTZ + 🔴 녹화영상 + 🎥 PTZ
@@ -436,7 +436,7 @@ function BottomBtn({ icon, label, bg, bd, fg }: { icon: string; label: string; b return ( diff --git a/frontend/src/tabs/prediction/components/BacktrackModal.tsx b/frontend/src/tabs/prediction/components/BacktrackModal.tsx index 86bf5ab..9b8d53b 100755 --- a/frontend/src/tabs/prediction/components/BacktrackModal.tsx +++ b/frontend/src/tabs/prediction/components/BacktrackModal.tsx @@ -64,13 +64,13 @@ export function BacktrackModal({

유출유 역추적 분석

AIS 항적 기반 유출 선박 추정
@@ -97,7 +97,7 @@ export function BacktrackModal({

분석 조건

@@ -114,10 +114,10 @@ export function BacktrackModal({ padding: '10px 12px', background: 'var(--bg3)', border: '1px solid var(--bd)', borderRadius: '8px', }}> -
+
{item.label}
-
+
{item.value}
@@ -127,7 +127,7 @@ export function BacktrackModal({ border: '1px solid rgba(168,85,247,0.3)', borderRadius: '8px', gridColumn: '1 / -1', }}> -
+
분석 대상 선박
@@ -146,14 +146,13 @@ export function BacktrackModal({ }}>

분석 결과

{conditions.totalVessels}척 중 {vessels.length}척 의심 @@ -179,7 +178,7 @@ export function BacktrackModal({ onClick={onRunAnalysis} style={{ flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700, - fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer', + borderRadius: '8px', cursor: 'pointer', background: 'linear-gradient(135deg, var(--purple), var(--cyan))', border: 'none', color: '#fff', }} @@ -192,7 +191,7 @@ export function BacktrackModal({ disabled style={{ flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700, - fontFamily: 'var(--fK)', borderRadius: '8px', + borderRadius: '8px', background: 'var(--bg3)', border: '1px solid var(--bd)', color: 'var(--purple)', cursor: 'wait', }} @@ -205,7 +204,7 @@ export function BacktrackModal({ onClick={onStartReplay} style={{ flex: 1, padding: '12px', fontSize: '13px', fontWeight: 700, - fontFamily: 'var(--fK)', borderRadius: '8px', cursor: 'pointer', + borderRadius: '8px', cursor: 'pointer', background: 'linear-gradient(135deg, var(--purple), var(--cyan))', border: 'none', color: '#fff', }} @@ -240,7 +239,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) { {vessel.rank}
-
+
{vessel.name}
@@ -251,7 +250,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) {
{vessel.probability}%
-
유출 확률
+
유출 확률
@@ -267,7 +266,7 @@ function VesselCard({ vessel }: { vessel: BacktrackVessel }) { padding: '6px', background: 'var(--bg3)', borderRadius: '6px', border: s.highlight ? '1px solid rgba(239,68,68,0.3)' : '1px solid var(--bd)', }}> -
+
{s.label}
{vessel.description} diff --git a/frontend/src/tabs/prediction/components/BoomDeploymentTheoryView.tsx b/frontend/src/tabs/prediction/components/BoomDeploymentTheoryView.tsx index 545c558..31ff7a4 100755 --- a/frontend/src/tabs/prediction/components/BoomDeploymentTheoryView.tsx +++ b/frontend/src/tabs/prediction/components/BoomDeploymentTheoryView.tsx @@ -28,13 +28,13 @@ export function BoomDeploymentTheoryView() { 🛡️
-
오일펜스 배치 최적화 알고리즘 이론
-
Oil Boom Deployment Optimization · 유출유 확산예측 연동 · 방제효율 최대화
+
오일펜스 배치 최적화 알고리즘 이론
+
Oil Boom Deployment Optimization · 유출유 확산예측 연동 · 방제효율 최대화
@@ -48,8 +48,7 @@ export function BoomDeploymentTheoryView() { onClick={() => setActivePanel(tab.id)} className="flex-1 py-2 px-2 text-[12px] font-semibold rounded-md transition-all" style={{ - fontFamily: 'var(--fK)', - background: activePanel === tab.id ? 'rgba(249,115,22,.15)' : 'var(--bg3)', + background: activePanel === tab.id ? 'rgba(249,115,22,.15)' : 'var(--bg3)', color: activePanel === tab.id ? 'var(--orange)' : 'var(--t3)', border: activePanel === tab.id ? '1px solid rgba(249,115,22,.3)' : '1px solid var(--bd)', fontWeight: 600, @@ -84,14 +83,14 @@ function OverviewPanel() {
-
🛡️ 오일펜스 배치 최적화란?
-
+
🛡️ 오일펜스 배치 최적화란?
+
해양 유류오염 발생 시 유출유 확산 예측 결과와 실시간 해양환경(조류·풍향·파고)을 연동하여, 제한된 방제자원(오일펜스 길이·방제정 수)으로 오염 확산 차단 효율을 최대화하는 최적 배치 지점·형태·순서를 자동 산출하는 수치 알고리즘 체계입니다.
-
🎯 WING 최적화 목표
-
+
🎯 WING 최적화 목표
+
{[ { num: '①', color: 'var(--orange)', bg: 'rgba(249,115,22,.05)', bd: 'rgba(249,115,22,.12)', text: '차단 면적 최대화 — 예측 유출유 확산 경계와 오일펜스 교차 면적 극대화' }, { num: '②', color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', text: '도달시간 최소화 — 유출유 해안·ESI 민감구역 도달 전 선제적 차단선 구축' }, @@ -109,7 +108,7 @@ function OverviewPanel() { {/* 전체 흐름도 */}
-
⚙️ WING 오일펜스 배치 최적화 전체 흐름
+
⚙️ WING 오일펜스 배치 최적화 전체 흐름
{[ { icon: '🌊', label: '확산예측', sub: 'KOSPS/POSEIDON\nOpenDrift', color: 'var(--orange)', bg: 'rgba(249,115,22,.08)', bd: 'rgba(249,115,22,.2)' }, @@ -123,7 +122,7 @@ function OverviewPanel() {
{step.icon}
{step.label}
@@ -143,9 +142,9 @@ function OverviewPanel() { { icon: '🔄', title: '자항식 오일펜스', color: 'var(--green)', desc: '방제정 예인 또는 자체 추진. U형·V형 동적 배치. 강조류 해역 적합.', specs: ['운용수심: 5m 이상', 'U형·V형·J형 동적 형태', '내조류: 조류각도 보정으로 극복'] }, ].map((boom, i) => (
-
{boom.icon} {boom.title}
-
{boom.desc}
-
+
{boom.icon} {boom.title}
+
{boom.desc}
+
{boom.specs.map((spec, j) => (
{spec}
))} @@ -156,8 +155,8 @@ function OverviewPanel() { {/* 핵심 제약조건 */}
-
⚠️ 최적화 핵심 제약조건
-
+
⚠️ 최적화 핵심 제약조건
+
{[ { icon: '🌊', title: '조류 제약', color: 'var(--red)', bg: 'rgba(239,68,68,.05)', bd: 'rgba(239,68,68,.15)', lines: ['조류속도 > 임계유속 시', '오일펜스 하단 통과 발생', 'U<0.7 knot 유지 필수', '임계각도 자동 계산 적용'] }, { icon: '📏', title: '자원 제약', color: 'var(--yellow)', bg: 'rgba(234,179,8,.05)', bd: 'rgba(234,179,8,.15)', lines: ['가용 오일펜스 총 길이', '방제정 척수·이동시간', '앵커링 가능 수심 조건', '연결부 허용 장력'] }, @@ -182,14 +181,14 @@ function DeploymentTheoryPanel() { <> {/* 차단 효율 이론 */}
-
📐 오일펜스 차단 효율 이론 (Boom Containment Efficiency)
+
📐 오일펜스 차단 효율 이론 (Boom Containment Efficiency)
-
① 차단 효율 함수 E(θ, U)
-
- 오일펜스의 차단 효율은 조류 유속(U)오일펜스 방향각(θ)의 함수입니다. 조류가 오일펜스에 수직으로 입사할수록 차단 효율이 낮아지며, 임계유속 초과 시 기름이 오일펜스 하부로 통과합니다. +
① 차단 효율 함수 E(θ, U)
+
+ 오일펜스의 차단 효율은 조류 유속(U)오일펜스 방향각(θ)의 함수입니다. 조류가 오일펜스에 수직으로 입사할수록 차단 효율이 낮아지며, 임계유속 초과 시 기름이 오일펜스 하부로 통과합니다.
-
+
E(θ,U) = 1 − Floss(Un)
Un = U · sin(θ) (법선방향 유속)
E = 1 (Un ≤ Uc)
@@ -198,11 +197,11 @@ function DeploymentTheoryPanel() {
-
② 최적 방향각 θ* 산정
-
- 오일펜스 방향각은 조류 방향에 따라 최적화됩니다. 차단 면적과 오일 수집 효율의 트레이드오프를 고려하여, 일반적으로 조류에 대해 30°~45° 예각 배치가 최적입니다. +
② 최적 방향각 θ* 산정
+
+ 오일펜스 방향각은 조류 방향에 따라 최적화됩니다. 차단 면적과 오일 수집 효율의 트레이드오프를 고려하여, 일반적으로 조류에 대해 30°~45° 예각 배치가 최적입니다.
-
+
θ* = arcsin(Uc / U) (임계조건)
θopt = argmax [Ablock(θ) · E(θ,U)]
실용범위: 15° ≤ θ ≤ 60°
@@ -214,11 +213,11 @@ function DeploymentTheoryPanel() { {/* V형·U형·J형 배치 패턴 */}
-
🔷 오일펜스 배치 형태별 이론
+
🔷 오일펜스 배치 형태별 이론
{/* V형 */}
-
V형 (Chevron)
+
V형 (Chevron)
@@ -230,8 +229,8 @@ function DeploymentTheoryPanel() { 조류
-
조류 방향 정면에서 양측으로 펼친 V형. 기름을 중앙 집유점으로 유도. 회수선 배치 용이.
-
+
조류 방향 정면에서 양측으로 펼친 V형. 기름을 중앙 집유점으로 유도. 회수선 배치 용이.
+
AV = L²·sin(2α)/2
α: 반개각, L: 편측 길이
최적 α = 30°~45° @@ -240,7 +239,7 @@ function DeploymentTheoryPanel() { {/* U형 */}
-
U형 (Horseshoe)
+
U형 (Horseshoe)
@@ -251,8 +250,8 @@ function DeploymentTheoryPanel() { 조류
-
말굽형으로 기름을 완전 포위. 폐쇄형 구조로 회수 효율 최고. 저조류 해역 적합.
-
+
말굽형으로 기름을 완전 포위. 폐쇄형 구조로 회수 효율 최고. 저조류 해역 적합.
+
AU = π·r²/2 + 2r·h
r: 반경, h: 직선부 길이
전제: U < 0.5 knot @@ -261,7 +260,7 @@ function DeploymentTheoryPanel() { {/* J형 */}
-
J형 (Skimming)
+
J형 (Skimming)
@@ -273,8 +272,8 @@ function DeploymentTheoryPanel() { 조류
-
직선+곡선 조합. 기름을 한쪽으로 편향 유도하여 집유. 강조류·연안 배치에 최적.
-
+
직선+곡선 조합. 기름을 한쪽으로 편향 유도하여 집유. 강조류·연안 배치에 최적.
+
θJ = arcsin(Uc/U) + δ
δ: 안전여유각(5°~10°)
활용: U > 0.7 knot @@ -285,16 +284,16 @@ function DeploymentTheoryPanel() { {/* 다단 배치 이론 */}
-
🔢 다단계 차단선(Multi-Boom) 배치 이론
+
🔢 다단계 차단선(Multi-Boom) 배치 이론
-
- 단일 오일펜스로 차단 불가한 경우 직렬 다단 배치로 누적 차단 효율을 향상합니다. n개 직렬 배치 시 누적 차단 효율: -
+
+ 단일 오일펜스로 차단 불가한 경우 직렬 다단 배치로 누적 차단 효율을 향상합니다. n개 직렬 배치 시 누적 차단 효율: +
Etotal = 1 − ∏(1−Ei)
Ei: i번째 오일펜스 단독 차단효율
-
+
{[ { color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', label: '2단 직렬', text: ': E_total = E₁+E₂−E₁·E₂ (예: 70%+70% → 91%)' }, { color: 'var(--cyan)', bg: 'rgba(6,182,212,.05)', bd: 'rgba(6,182,212,.12)', label: '단간 거리', text: ': 부표 집적 방지를 위해 ≥ 200m 이격 권장' }, @@ -320,19 +319,19 @@ function OptimizationPanel() {
-
⚙️ 다목적 최적화 문제 (Multi-Objective Optimization)
-
+
⚙️ 다목적 최적화 문제 (Multi-Objective Optimization)
+
오일펜스 배치 최적화는 상충하는 복수 목적함수를 동시에 만족해야 하는 전형적인 다목적 최적화 문제입니다. 차단 효율 최대화와 자원 사용 최소화는 서로 트레이드오프 관계를 가지며, 파레토 최적(Pareto Optimal) 해집합에서 의사결정자가 선택합니다.
{/* 목적함수 정의 */}
-
📊 목적함수 및 제약조건 정의
+
📊 목적함수 및 제약조건 정의
-
🎯 목적함수 F(x)
-
+
🎯 목적함수 F(x)
+
최대화:
f₁(x) = Σ Ablocked,i · wESI,i (가중 차단면적)
f₂(x) = Tdeadline − Tdeploy (여유시간)
@@ -342,8 +341,8 @@ function OptimizationPanel() {
-
🚫 제약조건 G(x)
-
+
🚫 제약조건 G(x)
+
g₁: U·sin(θi) ≤ Uc ∀i (임계유속)
g₂: Σ Lj ≤ Lmax (자원 한계)
g₃: Tdeploy,i ≤ Tarrive,i (시간 제약)
@@ -355,8 +354,8 @@ function OptimizationPanel() { {/* ESI 가중치 */}
-
🏖️ ESI 가중치 wESI 설계
-
+
🏖️ ESI 가중치 wESI 설계
+
{[ { grade: 'ESI 1~2', desc: '노출암반', w: 'w = 0.2', color: 'var(--green)', bg: 'rgba(34,197,94,.06)' }, { grade: 'ESI 3~4', desc: '모래해변', w: 'w = 0.4', color: 'var(--cyan)', bg: 'rgba(6,182,212,.06)' }, @@ -367,7 +366,7 @@ function OptimizationPanel() {
{esi.grade}
{esi.desc}
-
{esi.w}
+
{esi.w}
))}
@@ -376,13 +375,13 @@ function OptimizationPanel() { {/* NSGA-II 알고리즘 */}
-
🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)
+
🧬 NSGA-II (Non-dominated Sorting Genetic Algorithm II)
-
+
WING의 오일펜스 배치 최적화는 다목적 유전알고리즘 NSGA-II(Deb et al., 2002)를 핵심 엔진으로 사용합니다. 파레토 전면(Pareto Front)을 탐색하여 차단 효율과 자원 효율의 최적 해집합을 제공합니다.
-
+
{[ '염색체 구조 : [배치지점 좌표, 방향각θ, 길이L, 형태, 배치순서]', '집단 크기 : 100~200개체 · 세대수 50~200', @@ -397,8 +396,8 @@ function OptimizationPanel() {
-
NSGA-II 5단계 진화 루프
-
+
NSGA-II 5단계 진화 루프
+
{[ { step: '①', title: '초기 집단 생성', desc: '확산예측 결과 기반 랜덤 + 휴리스틱 배치안 혼합 초기화' }, { step: '②', title: '적합도 평가', desc: '유출유 확산 시뮬레이터로 각 배치안의 차단면적·도달시간 계산' }, @@ -418,9 +417,9 @@ function OptimizationPanel() { {/* 보조 알고리즘 비교 */}
-
🔬 보조 최적화 알고리즘 비교 적용
+
🔬 보조 최적화 알고리즘 비교 적용
- +
{['알고리즘', '유형', '장점', '단점', 'WING 활용'].map(h => ( @@ -457,12 +456,12 @@ function FluidDynamicsPanel() { <> {/* 유동 수치 모델 */}
-
🌊 오일펜스 주변 유동 수치 모델
+
🌊 오일펜스 주변 유동 수치 모델
-
① 오일펜스 항력 모델
-
오일펜스에 작용하는 항력은 조류속도의 제곱에 비례합니다. 오일펜스 구조 변형(catenary형태)을 고려한 동적 항력 계산.
-
+
① 오일펜스 항력 모델
+
오일펜스에 작용하는 항력은 조류속도의 제곱에 비례합니다. 오일펜스 구조 변형(catenary형태)을 고려한 동적 항력 계산.
+
FD = ½ · ρ · CD · A · Un²
T = FD · L / (2·sin(α))
CD: 항력계수(≈1.2), A: 수중 투영면적
@@ -470,9 +469,9 @@ function FluidDynamicsPanel() {
-
② 기름 통과(Splash-over) 조건
-
조류 유속이 임계값을 초과하면 기름이 파도를 타고 오일펜스를 넘어가는 Splash-over가 발생합니다.
-
+
② 기름 통과(Splash-over) 조건
+
조류 유속이 임계값을 초과하면 기름이 파도를 타고 오일펜스를 넘어가는 Splash-over가 발생합니다.
+
Fr = Un / √(g·Δρ/ρ·h)
Splash-over: Fr > 0.5~0.6
Fr: 수정 Froude수, h: 오일펜스 수중깊이
@@ -484,10 +483,10 @@ function FluidDynamicsPanel() { {/* Catenary 변형 모델 */}
-
🔗 오일펜스 현수선(Catenary) 변형 모델
+
🔗 오일펜스 현수선(Catenary) 변형 모델
-
- 조류와 바람에 의해 오일펜스는 현수선(Catenary) 형태로 변형됩니다. 실제 차단 길이가 설계 길이보다 짧아지며, 최적화 알고리즘에서 변형 후 유효 차단 길이 Leff를 계산합니다. +
+ 조류와 바람에 의해 오일펜스는 현수선(Catenary) 형태로 변형됩니다. 실제 차단 길이가 설계 길이보다 짧아지며, 최적화 알고리즘에서 변형 후 유효 차단 길이 Leff를 계산합니다.
y(x) = a·cosh(x/a) − a
Larc = 2a·sinh(Lspan/(2a))
@@ -495,7 +494,7 @@ function FluidDynamicsPanel() { a: catenary 파라미터, φ: 최대 편향각
-
+
변형 단계별 유효 차단 길이 보정
{[ { cond: 'U < 0.3 knot', result: 'L_eff ≈ L (직선 유지)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)' }, @@ -513,8 +512,8 @@ function FluidDynamicsPanel() { {/* 유막 포집 모델 */}
-
🛢️ 오일펜스 내 유막 포집 동역학
-
+
🛢️ 오일펜스 내 유막 포집 동역학
+
포집 기름 체적 변화율
@@ -548,13 +547,13 @@ function FieldApplicationPanel() { <> {/* 배치 5단계 절차 */}
-
🗺️ WING 오일펜스 배치 의사결정 5단계
+
🗺️ WING 오일펜스 배치 의사결정 5단계
{steps.map(step => (
{step.num}
-
-
{step.title}
+
+
{step.title}
{step.desc}
@@ -564,8 +563,8 @@ function FieldApplicationPanel() { {/* 해역별 적용 특성 */}
-
📍 해역별 적용 특성 및 전략
-
+
📍 해역별 적용 특성 및 전략
+
{[ { icon: '🌊', title: '서해 (조차 대형)', color: 'var(--blue)', bg: 'rgba(59,130,246,.05)', bd: 'rgba(59,130,246,.12)', desc: '최대 조차 9m (인천), 조류 최대 3~5 knot. J형 배치 주력. 조석 전환 재배치 필수. 앵커링 수심 급변화 주의.' }, { icon: '🌿', title: '남해 (다도해)', color: 'var(--green)', bg: 'rgba(34,197,94,.05)', bd: 'rgba(34,197,94,.12)', desc: '복잡한 해안선·섬. 조류 1~2 knot. V형·U형 복합 배치. 좁은 수로 통제 우선. ESI 고등급 갯벌 보호.' }, @@ -632,15 +631,15 @@ function ReferencesPanel() { return ( <> -
📚 오일펜스 배치 최적화 이론 근거 문헌
-
총 12편 · 4개 카테고리
+
📚 오일펜스 배치 최적화 이론 근거 문헌
+
총 12편 · 4개 카테고리
{categories.map((cat, ci) => (
-
+
{cat.title}
-
+
{cat.refs.map((ref, ri) => (
-
{ref.title}
+
{ref.title}
{ref.author}
{ref.desc}
diff --git a/frontend/src/tabs/prediction/components/InfoLayerSection.tsx b/frontend/src/tabs/prediction/components/InfoLayerSection.tsx index 3e45dd7..74d1338 100644 --- a/frontend/src/tabs/prediction/components/InfoLayerSection.tsx +++ b/frontend/src/tabs/prediction/components/InfoLayerSection.tsx @@ -83,8 +83,7 @@ const InfoLayerSection = ({ padding: '4px 8px', fontSize: '10px', fontWeight: 600, - fontFamily: 'var(--fK)', - border: '1px solid var(--cyan)', + border: '1px solid var(--cyan)', borderRadius: 'var(--rS)', background: 'transparent', color: 'var(--cyan)', @@ -121,8 +120,7 @@ const InfoLayerSection = ({ padding: '4px 8px', fontSize: '10px', fontWeight: 600, - fontFamily: 'var(--fK)', - border: '1px solid var(--red)', + border: '1px solid var(--red)', borderRadius: 'var(--rS)', background: 'transparent', color: 'var(--red)', diff --git a/frontend/src/tabs/prediction/components/OilBoomSection.tsx b/frontend/src/tabs/prediction/components/OilBoomSection.tsx index 9dc40cf..0296b5b 100644 --- a/frontend/src/tabs/prediction/components/OilBoomSection.tsx +++ b/frontend/src/tabs/prediction/components/OilBoomSection.tsx @@ -69,8 +69,7 @@ const OilBoomSection = ({ padding: '6px 8px', fontSize: '10px', fontWeight: 600, - fontFamily: 'var(--fK)', - borderRadius: 'var(--rS)', + borderRadius: 'var(--rS)', border: boomPlacementTab === tab.id ? '1px solid var(--orange)' : '1px solid var(--bd)', background: boomPlacementTab === tab.id ? 'rgba(245,158,11,0.1)' : 'var(--bg0)', color: boomPlacementTab === tab.id ? 'var(--orange)' : 'var(--t3)', @@ -99,8 +98,7 @@ const OilBoomSection = ({ padding: '6px 10px', fontSize: '10px', fontWeight: 600, - fontFamily: 'var(--fK)', - borderRadius: 'var(--rS)', + borderRadius: 'var(--rS)', border: '1px solid var(--bd)', background: 'var(--bg0)', color: (boomLines.length === 0 && !isDrawingBoom && !containmentResult) ? 'var(--t3)' : 'var(--red)', @@ -130,7 +128,7 @@ const OilBoomSection = ({
{metric.value}
-
+
{metric.label}
@@ -148,16 +146,16 @@ const OilBoomSection = ({ }}>
0 ? 'var(--green)' : 'var(--t3)' }} /> - 0 ? 'var(--green)' : 'var(--t3)', fontFamily: 'var(--fK)' }}> + 0 ? 'var(--green)' : 'var(--t3)' }}> {oilTrajectory.length > 0 ? '확산 데이터 준비 완료' : '확산 예측을 먼저 실행하세요'}
-

+

확산 예측 기반 최적 배치안

-

+

{oilTrajectory.length > 0 ? '확산 궤적을 분석하여 해류 직교 방향 1차 방어선, U형 포위 2차 방어선, 연안 보호 3차 방어선을 자동 생성합니다.' : '상단에서 확산 예측을 실행한 뒤 AI 배치를 적용할 수 있습니다.' @@ -179,8 +177,7 @@ const OilBoomSection = ({ padding: '10px', fontSize: '11px', fontWeight: 700, - fontFamily: 'var(--fK)', - background: oilTrajectory.length > 0 ? 'rgba(245,158,11,0.15)' : 'var(--bg0)', + background: oilTrajectory.length > 0 ? 'rgba(245,158,11,0.15)' : 'var(--bg0)', border: oilTrajectory.length > 0 ? '2px solid var(--orange)' : '1px solid var(--bd)', borderRadius: 'var(--rS)', color: oilTrajectory.length > 0 ? 'var(--orange)' : 'var(--t3)', @@ -194,7 +191,7 @@ const OilBoomSection = ({ {/* 알고리즘 설정 */}

-

+

📊 배치 알고리즘 설정

@@ -208,7 +205,7 @@ const OilBoomSection = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 8px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)' }}> - ● {setting.label} + ● {setting.label}
- {setting.unit} + {setting.unit}
))} @@ -238,7 +235,7 @@ const OilBoomSection = ({
-
+
길이 -
{line.length.toFixed(0)}m
+
{line.length.toFixed(0)}m
각도 -
{line.angle.toFixed(0)}°
+
{line.angle.toFixed(0)}°
우선순위 @@ -362,9 +359,9 @@ const OilBoomSection = ({ onBoomLinesChange(updated) }} style={{ - width: '100%', fontSize: '10px', fontWeight: 600, fontFamily: 'var(--fK)', + width: '100%', fontSize: '10px', fontWeight: 600, background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: '3px', - color: 'var(--t1)', padding: '2px', outline: 'none' + padding: '2px', outline: 'none' }} > @@ -387,7 +384,7 @@ const OilBoomSection = ({
0 ? 'var(--green)' : 'var(--red)' }} /> 0 ? 'var(--green)' : 'var(--t3)' }}> @@ -397,7 +394,7 @@ const OilBoomSection = ({
0 ? 'var(--green)' : 'var(--red)' }} /> 0 ? 'var(--green)' : 'var(--t3)' }}> @@ -414,7 +411,7 @@ const OilBoomSection = ({ }} disabled={oilTrajectory.length === 0 || boomLines.length === 0} style={{ - width: '100%', padding: '10px', fontSize: '11px', fontWeight: 700, fontFamily: 'var(--fK)', + width: '100%', padding: '10px', fontSize: '11px', fontWeight: 700, background: (oilTrajectory.length > 0 && boomLines.length > 0) ? 'rgba(6,182,212,0.15)' : 'var(--bg0)', border: (oilTrajectory.length > 0 && boomLines.length > 0) ? '2px solid var(--cyan)' : '1px solid var(--bd)', borderRadius: 'var(--rS)', @@ -437,7 +434,7 @@ const OilBoomSection = ({
{containmentResult.overallEfficiency}%
-
+
전체 차단 효율
@@ -448,13 +445,13 @@ const OilBoomSection = ({
{containmentResult.blockedParticles}
-
차단 입자
+
차단 입자
{containmentResult.passedParticles}
-
통과 입자
+
통과 입자
@@ -468,7 +465,7 @@ const OilBoomSection = ({ {/* 라인별 분석 */}
-

+

라인별 차단 분석

{containmentResult.perLineResults.map((r) => ( @@ -476,7 +473,7 @@ const OilBoomSection = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 8px', marginBottom: '4px', background: 'var(--bg0)', border: '1px solid var(--bd)', borderRadius: 'var(--rS)', - fontSize: '9px', fontFamily: 'var(--fK)' + fontSize: '9px' }}> {r.boomLineName} = 50 ? 'var(--green)' : 'var(--orange)', fontFamily: 'var(--fM)', marginLeft: '8px' }}> @@ -502,11 +499,11 @@ const OilBoomSection = ({ borderLeft: `3px solid ${priorityColor}`, borderRadius: 'var(--rS)' }}>
- + 🛡 {idx + 1}차 방어선 ({line.type}) @@ -515,21 +512,21 @@ const OilBoomSection = ({
- 길이 -
+ 길이 +
{line.length.toFixed(0)}m
- 각도 -
+ 각도 +
{line.angle.toFixed(0)}°
= 80 ? 'var(--green)' : 'var(--orange)' }} /> - = 80 ? 'var(--green)' : 'var(--orange)', fontFamily: 'var(--fK)' }}> + = 80 ? 'var(--green)' : 'var(--orange)' }}> 차단 효율 {line.efficiency}%
diff --git a/frontend/src/tabs/prediction/components/OilSpillTheoryView.tsx b/frontend/src/tabs/prediction/components/OilSpillTheoryView.tsx index 76a1fa4..584cbc5 100755 --- a/frontend/src/tabs/prediction/components/OilSpillTheoryView.tsx +++ b/frontend/src/tabs/prediction/components/OilSpillTheoryView.tsx @@ -73,20 +73,20 @@ ${styles} 📐
-
유출유 확산 모델 이론 및 검증
+
유출유 확산 모델 이론 및 검증
- 🔷 KOSPS - 🔴 POSEIDON - 🔵 OpenDrift - 앙상블 - 라그랑지안 입자추적 이론 기반 + 🔷 KOSPS + 🔴 POSEIDON + 🔵 OpenDrift + 앙상블 + 라그랑지안 입자추적 이론 기반
@@ -98,8 +98,7 @@ ${styles}
+
@@ -280,9 +279,9 @@ function SystemOverviewPanel() { ].map((row, i) => ( ))} @@ -293,8 +292,8 @@ function SystemOverviewPanel() {
-
- 3종 앙상블 운용 방식 : WING 시스템은 KOSPS·POSEIDON·OpenDrift를 동시 구동하여 각 모델의 예측 결과를 중첩·비교합니다. 3종 결과의 공통 영역을 고신뢰 오염 범위로, 최외곽 범위를 불확실성 경계로 표출하여 방제 의사결정의 위험도를 정량화합니다. +
+ 3종 앙상블 운용 방식 : WING 시스템은 KOSPS·POSEIDON·OpenDrift를 동시 구동하여 각 모델의 예측 결과를 중첩·비교합니다. 3종 결과의 공통 영역을 고신뢰 오염 범위로, 최외곽 범위를 불확실성 경계로 표출하여 방제 의사결정의 위험도를 정량화합니다.
@@ -315,14 +314,14 @@ function KospsPanel() {
🔷
-
KOSPS (Korea Oil Spill Prediction System)
-
한국해양연구원(KORDI) 개발 · 한국 해역 특화 유출유 확산 예측 상시 운용 시스템
+
KOSPS (Korea Oil Spill Prediction System)
+
한국해양연구원(KORDI) 개발 · 한국 해역 특화 유출유 확산 예측 상시 운용 시스템
-
+
김혜진·이문진·오세웅·강준묵(2011)이 개발한 KOSPS는 유류오염사고 발생 즉시 신속하게 모델을 구동할 수 있도록 상시 운용 체계를 구축한 국내 유출유 확산 예측 시스템입니다. 정적자료(수심·해안선)와 동적자료(KMA 바람·HYCOM 해류·KORDI 조류)를 외부 FTP 연계로 자동 갱신하여 실시간 예측 상시 활용을 가능하게 합니다.
-
+
📚 참고문헌: 김혜진·이문진·오세웅·강준묵, "유출유 확산 예측 모델의 상시 운용 체계 개발에 관한 연구", 해양환경안전학회지 17권 4호, pp.375-382, 2011.
@@ -345,8 +344,8 @@ function KospsPanel() {
2015.11.03
-
해양 유류오염사고 발생시 효율적인 방제방안 수립을 위한 유출유 확산 예측 방법
-
+
해양 유류오염사고 발생시 효율적인 방제방안 수립을 위한 유출유 확산 예측 방법
+
발명자 : 이문진 · 김혜진 · 이승현 · 전태병 | 특허권자 : 한국해양과학기술원
@@ -354,7 +353,7 @@ function KospsPanel() { {t} )}
-
+
국가R&D: ① 3차원 유출유 확산예측 기반 방제 지원기술 개발 (기여율 65%) ② HNS 유출 거동예측 및 대응정보 지원기술 개발 (기여율 35%) | 해양수산부
@@ -367,7 +366,7 @@ function KospsPanel() { {/* CHARRY 모델 */}
🌀 CHARRY 모델 (실시간 조류예측 핵심 알고리즘)
-
+
CHARRY는 조화분석에 의한 검조소 실시간 조위와 조석 수치모델링에 의한 조류 공간분포를 변조조석(Modulated Tide)을 매개로 결합하여, 실제 시간대의 전 해역 조류를 실시간으로 재현하는 핵심 알고리즘입니다.
@@ -376,7 +375,7 @@ function KospsPanel() { ζ(t) = A(t) cos[σt − θ(t)]
A²(t) = Σ Yᵢ² + 2Σ YᵢYⱼ cos[(σᵢ−σⱼ)t−(φᵢ−φⱼ)]
-
+
① 진폭 등비 증가
검조소 조위 진폭 f배 → 전 격자 동일 f배 증가 @@ -393,7 +392,7 @@ function KospsPanel() {
📊 동적 입력자료 체계
-
+
{[ { icon: '🌬️', label: '바람·기온', detail: 'KMA UM · ~12km · 2회/일', bg: 'rgba(6,182,212,.04)', bd: 'rgba(6,182,212,.12)' }, { icon: '🌊', label: '해류(표층)', detail: 'HYCOM · ~9km · 1회/일', bg: 'rgba(59,130,246,.04)', bd: 'rgba(59,130,246,.12)' }, @@ -402,7 +401,7 @@ function KospsPanel() { { icon: '💨', label: '취송류(풍성류)', detail: 'KMA 바람 → 경험식 계산', bg: 'rgba(168,85,247,.04)', bd: 'rgba(168,85,247,.12)' }, ].map(d => (
- {d.icon} {d.label} + {d.icon} {d.label} {d.detail}
))} @@ -410,13 +409,13 @@ function KospsPanel() {
🗂️ 정적 입력자료
-
+
-
📍 수심·해안선
+
📍 수심·해안선
전자해도(ENC) → 500m 격자 보간
-
🗺️ 격자 구성
+
🗺️ 격자 구성
좌표변환 → 영역추출 → 격자보간 표준화
@@ -437,7 +436,7 @@ function KospsPanel() { θ_WDC = θ_wind + 18.6°
-
+
V_WDC : 표면 취송류 유속 (m/s) — 바람의 약 2.9%
@@ -466,8 +465,7 @@ function KospsPanel() {
+ }}>
{node.label}
{node.sub}
@@ -475,14 +473,14 @@ function KospsPanel() {
))}
-
+
FTP 자동 갱신 → DB 정규화 → 격자 재구성 → 모델 구동 → 결과 표출
-
⚠️ 예측 신뢰도 한계
-
+
⚠️ 예측 신뢰도 한계
+
조류: 무한 예보 가능
- 해류(HYCOM): 5일치 제한
+ 해류(HYCOM): 5일치 제한
기온·취송류: 3일치 제한
72h 초과 예측 시 신뢰도 저하
@@ -497,56 +495,56 @@ function KospsPanel() {
📜
-
이문진 박사 특허 기반 핵심 기술 (등록특허 10-1567431)
-
해양 유류오염사고 발생시 효율적인 방제방안 수립을 위한 유출유 확산 예측 방법 · 한국해양과학기술원 · 2015년 등록
+
이문진 박사 특허 기반 핵심 기술 (등록특허 10-1567431)
+
해양 유류오염사고 발생시 효율적인 방제방안 수립을 위한 유출유 확산 예측 방법 · 한국해양과학기술원 · 2015년 등록
{/* 특허 개요 */} -
+
본 특허는 방제정보지도(ESI, Environmental Sensitivity Index Map)를 기반으로 실시간 조류·취송류·해수유동을 산출하고, 이를 유출유 확산 예측에 직결하는 통합 방법론을 특허화한 것입니다. 기상예측시스템·위성영상수신시스템·검조소를 인터넷으로 연결하여 실시간 기상·수온·조석 정보를 자동 수신하고, CHARRY 모델로 실시간 조류를 예측하여 유출유 확산에 적용합니다.
{/* 3단계 처리 프로세스 */} -
⚙️ 특허 청구항 3단계 처리 프로세스
+
⚙️ 특허 청구항 3단계 처리 프로세스
-
① 실시간 자료 수신 (S10)
-
- • 기상자료 : 바람·기온·기압 분포
- • 수온자료 : NGSST 위성 원격탐사 (일본 토호쿠대학, 5km 해상도)
- • 조석정보 : 검조소 실시간 조위 +
① 실시간 자료 수신 (S10)
+
+ • 기상자료 : 바람·기온·기압 분포
+ • 수온자료 : NGSST 위성 원격탐사 (일본 토호쿠대학, 5km 해상도)
+ • 조석정보 : 검조소 실시간 조위
-
FTP 자동 접속·수신·좌표변환·DB구축까지 자동화
+
FTP 자동 접속·수신·좌표변환·DB구축까지 자동화
-
② 조류·취송류 예측 (S20)
-
+
② 조류·취송류 예측 (S20)
+
CHARRY 모델로 실시간 조류 예측
• 2D 해수유동 방정식 기반 조화상수 DB 구축
• 취송류 경험식 : V_WDC=0.029×V_wind, θ_WDC=θ_wind+18.6°
• M2·S2·K1·O1 주요 4대 분조 조화분해
-
계산격자 : 15초(약 463m) 등간격 · 총 3,225,600격자
+
계산격자 : 15초(약 463m) 등간격 · 총 3,225,600격자
-
③ 유출유 확산 예측 (S30)
-
- • 클라이언트로부터 유출유량·유출지점 입력 수신
+
③ 유출유 확산 예측 (S30)
+
+ • 클라이언트로부터 유출유량·유출지점 입력 수신
• 몬테카를로(Monte Carlo) 입자추적 기법
• fBm 기반 난류확산 : σ²=Atᵐ (m≈2H)
• 풍화모델 : 퍼짐·증발·소산·유상화·침강 통합
-
ESI Map 환경민감자원 DB와 결합하여 피해 위험도 평가
+
ESI Map 환경민감자원 DB와 결합하여 피해 위험도 평가
{/* 해수유동 방정식 & 풍화 5단계 */}
-
📐 2차원 해수유동 방정식
-
극좌표계(Polar Coordinate) 적용 · 지구 구면효과 반영
-
+
📐 2차원 해수유동 방정식
+
극좌표계(Polar Coordinate) 적용 · 지구 구면효과 반영
+
{[ { label: '기조력', desc: '제1(Love Number 0.69) + 제2(α=0.9) 포함' }, { label: '4대 분조', desc: 'M2(1.405×10⁻⁴/s) · S2 · K1 · O1' }, @@ -560,8 +558,8 @@ function KospsPanel() {
-
🔁 유출유 풍화(Weathering) 5단계
-
+
🔁 유출유 풍화(Weathering) 5단계
+
① 퍼짐 : Fay(1969) + Mackay et al.(1980) · 두께별 3단계 모델
@@ -584,8 +582,8 @@ function KospsPanel() { {/* Akima 수심 보간 & NGSST 수온 */}
-
🗺️ Akima 수심 보간 기법
-
+
🗺️ Akima 수심 보간 기법
+
전자해도(ENC) 무작위 수심자료를 계산격자로 보간할 때 Akima(1978) 2차원 5차다항식(Bivariate Quintic Polynomial)을 사용합니다. 삼각망(TIN)을 구성하여 각 꼭지점의 편미분 연속성 조건으로 21개 계수를 결정합니다.
@@ -593,11 +591,11 @@ function KospsPanel() {
-
🌡️ NGSST 실시간 수온자료
-
+
🌡️ NGSST 실시간 수온자료
+
일본 토호쿠대학 Kawamura 교수팀의 New Generation SST(NGSST)를 FTP 자동수신합니다. 열적외선(AVHRR·MODIS) + 마이크로파(AMSR-E) 융합으로 구름 무관 일별 수온 제공.
-
+
영역 : 116~166°E, 13~63°N (북서태평양)
해상도 : 3분(약 5km) · 일 1회 갱신
변환식 : SST(℃) = 0.15 × DN − 3.0
@@ -607,8 +605,8 @@ function KospsPanel() { {/* 특허 기여율 */}
-
📋 특허 지원 국가연구개발사업 및 기여율
-
+
📋 특허 지원 국가연구개발사업 및 기여율
+
🔷 과제① (기여율 65%)
3차원 유출유 확산예측 기반 해양유류오염 방제 지원기술 개발
해양수산부 | 2013.01~2013.12
@@ -618,7 +616,7 @@ function KospsPanel() {
주요 위험유해물질(HNS) 유출 거동예측 및 대응정보 지원기술 개발
해양수산부 | 2013.01~2013.12
-
+
📚 특허권자: 한국해양과학기술원 | 발명자: 이문진·김혜진·이승현·전태병 | 등록번호: 10-1567431 | 공고일: 2015.11.12
@@ -630,8 +628,8 @@ function KospsPanel() {
setKospsPapersOpen(!kospsPapersOpen)}>
📄
-
KOSPS관련 유출유 확산예측 논문
-
KOSPS 개발 · 허베이스피리트 검증 · 3D 확산예측 시스템 · 방제효과 모델링 — 한국해양환경·에너지학회
+
KOSPS관련 유출유 확산예측 논문
+
KOSPS 개발 · 허베이스피리트 검증 · 3D 확산예측 시스템 · 방제효과 모델링 — 한국해양환경·에너지학회
{kospsPapersOpen &&
@@ -644,13 +642,13 @@ function KospsPanel() {
- {paper.tags.map(t => {t.label})} + {paper.tags.map(t => {t.label})}
- {paper.year} + {paper.year}
-
{paper.title}
-
{paper.authors}
-
{paper.desc}
+
{paper.title}
+
{paper.authors}
+
{paper.desc}
))}
} @@ -662,7 +660,7 @@ function KospsPanel() { 📄 국내 학술논문
- {papersOpen &&
+ {papersOpen &&
{[ { num: '①', title: '유출유 확산 예측 모델의 상시 운용 체계 개발에 관한 연구', authors: '김혜진 · 이문진 · 오세웅 · 강준묵', journal: '해양환경안전학회지', detail: '제17권 4호, pp.375-382 | 2011', desc: 'KOSPS 상시 운용 체계 구축 · 정적/동적 자료 FTP 자동 연계 · 서해·남해 시범 운용 성능 평가', color: 'var(--cyan)' }, { num: '②', title: '해양오염 방제지원시스템 구축을 위한 실시간 조류 예측 기술 개발', authors: '이문진 · 강용균 외', journal: '해양환경안전학회 춘계학술발표회', detail: '| 2008', desc: 'CHARRY 모델 인천·서해 적용 검증 · 창조/낙조 실시간 조류 재현 결과', color: 'var(--cyan)' }, @@ -678,7 +676,7 @@ function KospsPanel() {
{paper.num}
-
{paper.title}
+
{paper.title}
{paper.authors} | {paper.journal} {paper.detail}
{paper.desc}
{paper.tags && ( @@ -698,7 +696,7 @@ function KospsPanel() { 🌐 국외 핵심 논문
- {intlPapersOpen &&
+ {intlPapersOpen &&
{[ { num: '①', title: 'OpenDrift v1.0: a generic framework for trajectory modelling', authors: 'Dagestad et al.', journal: 'Geoscientific Model Development', detail: 'Vol.11, pp.1405-1420 | 2018', desc: 'OpenDrift 프레임워크 전체 설계·구현·검증 · Python 모듈화 구조 · OpenOil 유출유 모듈 포함', color: 'var(--green)' }, { num: '②', title: 'Observation-based evaluation of surface wave effects on oil spill trajectories', authors: 'Röhrs et al.', journal: 'J. Geophys. Res. Oceans', detail: '| 2013', desc: 'Stokes drift 파랑 기여 효과 · OpenOil 유출유 확산 현장 검증', color: 'var(--green)' }, @@ -711,7 +709,7 @@ function KospsPanel() {
{paper.num}
-
{paper.title}
+
{paper.title}
{paper.authors} | {paper.journal} {paper.detail}
{paper.desc}
@@ -734,14 +732,14 @@ function PoseidonPanel() {
🔵
-
POSEIDON (입자추적 최적화 예측 시스템)
-
한국환경연구원 · (주)아라종합기술 · 한국해양기상기술 공동개발 · MOHID 해양순환모델 기반 · 뜰개 관측 매개변수 자동 최적화
+
POSEIDON (입자추적 최적화 예측 시스템)
+
한국환경연구원 · (주)아라종합기술 · 한국해양기상기술 공동개발 · MOHID 해양순환모델 기반 · 뜰개 관측 매개변수 자동 최적화
-
+
김충기·김도연 외(등록특허 10-1868791, 2018)가 개발한 POSEIDON은 실제 해양에 투하한 뜰개(Drifter) 관측 경로와 수치모델 예측값을 비교하여, 입자추적 모델의 매개변수를 GA·DE·HS·PSO 알고리즘으로 자동 최적화하는 핵심 기술을 보유합니다. 해양순환 기반 모델은 포르투갈 IST/MARETEC이 개발한 MOHID 3D 순환모델을 사용합니다.
-
+
📚 참고문헌 ①: 김도연·김용혁, "유출유 확산 예측을 위한 입자 추적 모듈 최적화 방법 및 이를 이용한 예측 시스템", 등록특허 10-1868791, 2018.
📚 참고문헌 ②: 이재호·임병준·김도연 외, "2016년 동아시아 해역의 MOHID 지역 해양 순환 모델 검증", 한국지구과학회지 39(5), pp.436-457, 2018.
@@ -759,8 +757,8 @@ function PoseidonPanel() {
2018 등록
-
유출유 확산 예측을 위한 입자 추적 모듈 최적화 방법 및 이를 이용한 예측 시스템 (POSEIDON)
-
+
유출유 확산 예측을 위한 입자 추적 모듈 최적화 방법 및 이를 이용한 예측 시스템 (POSEIDON)
+
발명자 : 김도연 · 김용혁 · 김충기 외 | 출원인 : (주)아라종합기술
@@ -776,7 +774,7 @@ function PoseidonPanel() {
🌊 MOHID 해양순환모델 (기반 모델)
포르투갈 IST/MARETEC에서 1985년부터 개발한 3D 수치 해양순환모델. 60여 개 모듈을 보유하며 조류·폭풍해일·수온·염분·난류·유출유 확산 등을 통합 모의합니다.
-
+
{[ { label: '격자 구성', desc: '동아시아 9km + 한반도 3km (Nesting)' }, { label: '수직 좌표', desc: 'σ + Z-level 혼합 (GVC)' }, @@ -790,7 +788,7 @@ function PoseidonPanel() {
⚙️ 입자추적 매개변수 자동 최적화
뜰개 관측 경로와 제1 모델 예측값의 단위시간별 변화량을 비교하여 제2 모델의 매개변수를 반복 최적화합니다.
-
+
{[ { label: '최적화 알고리즘', desc: 'GA · DE · HS · PSO 중 선택' }, { label: '입력 데이터', desc: '유속·풍속 벡터 + 제1모델 변화량' }, @@ -808,21 +806,21 @@ function PoseidonPanel() {
📐 POSEIDON 입자추적 핵심 수식
-
제1 입자추적 모델 (기본)
+
제1 입자추적 모델 (기본)
Model_x = Δt × current_u + Δt × c × wind_u
Model_y = Δt × current_v + Δt × c × wind_v
-
c : 풍속 가중치 (예: c=0.3 → 바람의 30% 반영)
+
c : 풍속 가중치 (예: c=0.3 → 바람의 30% 반영)
-
제2 입자추적 모델 (최적화 후)
+
제2 입자추적 모델 (최적화 후)
Revised_x = a1·current_u + a2·current_v
           + a3·wind_u + a4·wind_v
           + a5·Model_x + a6·Model_y + a7
-
a1~a7 : GA·DE·PSO로 최적화된 매개변수
+
a1~a7 : GA·DE·PSO로 최적화된 매개변수
@@ -831,7 +829,7 @@ function PoseidonPanel() {
🔄 POSEIDON_V2 상시 운용 체계
{/* 외부 입력 자료 */} -
외부 입력 자료
+
외부 입력 자료
{[ { label: 'HYCOM', sub: '해류·수온·염분', detail: 'YYYYMMDD.nc', color: 'var(--blue)' }, @@ -841,7 +839,7 @@ function PoseidonPanel() { ].map((node, i) => (
{node.label}
{node.sub}
@@ -856,7 +854,7 @@ function PoseidonPanel() {
▼ DATA → PREP → 격자 보간/좌표 변환 ▼
{/* 4대 도메인 실행 모듈 */} -
POSEIDON 4대 실행 모듈 (EA012 대격자 → KO108 연안 상세격자)
+
POSEIDON 4대 실행 모듈 (EA012 대격자 → KO108 연안 상세격자)
{[ { label: 'HYDR', script: 'RUN-HYDR.sh', engine: 'MOHID 3D', desc: '해류·수온·염분·수위', color: 'var(--cyan)', icon: '🌊' }, @@ -866,7 +864,7 @@ function PoseidonPanel() { ].map(m => (
{m.icon}
{m.label}
@@ -891,8 +889,7 @@ function PoseidonPanel() {
+ }}>
{node.label}
{node.sub}
@@ -901,14 +898,14 @@ function PoseidonPanel() { ))}
-
+
HYCOM(.nc) + GDAPS(.grib2) → 격자 보간(EA012/KO108) → MOHID(해류) + SWAN(파랑) + 조석 → OILS 입자추적 → 뜰개 동화 최적화 → HDF5 결과 → 방제 의사결정
-
⚠️ 예측 신뢰도 한계
-
+
⚠️ 예측 신뢰도 한계
+
조류: 무한 예보 가능
- 해류(HYCOM): 5일치 제한
+ 해류(HYCOM): 5일치 제한
기온·취송류: 3일치 제한
72h 초과 예측 시 신뢰도 저하
@@ -921,8 +918,8 @@ function PoseidonPanel() {
📄
-
POSEIDON관련 유출유 확산예측 논문
-
포세이돈 시스템 소개·활용 · 최적 방제전략 · 원격탐사 연동 · MOHID 검증 — 한국해양환경·에너지학회 외
+
POSEIDON관련 유출유 확산예측 논문
+
포세이돈 시스템 소개·활용 · 최적 방제전략 · 원격탐사 연동 · MOHID 검증 — 한국해양환경·에너지학회 외
@@ -935,13 +932,13 @@ function PoseidonPanel() {
- {paper.tags.map(t => {t.label})} + {paper.tags.map(t => {t.label})}
- {paper.year} + {paper.year}
-
{paper.title}
-
{paper.authors}
-
{paper.desc}
+
{paper.title}
+
{paper.authors}
+
{paper.desc}
))}
@@ -960,16 +957,16 @@ function OpenDriftPanel() {
🟢
-
OpenDrift (오픈소스 라그랑지안 확산 프레임워크)
-
노르웨이 MET Norway · OpenOil 공개 프레임워크 · Python 기반 · IMO/IPIECA 검증
+
OpenDrift (오픈소스 라그랑지안 확산 프레임워크)
+
노르웨이 MET Norway · OpenOil 공개 프레임워크 · Python 기반 · IMO/IPIECA 검증
-
+
Dagestad et al.(2018)이 개발한 OpenDrift는 유출유·SAR·표류물·부유입자 등 다양한 해양 확산 현상을 모의하는 Python 기반 오픈소스 라그랑지안 프레임워크입니다. NEMO·ROMS·HYCOM 등 다양한 해양모델 출력값을 강제력으로 사용하며, OpenOil 모듈을 통해 유출유 거동을 상세 모의합니다.
- 🔗 공식 문서 (opendrift.github.io) - 📚 Dagestad et al., Geosci. Model Dev. 11, 2018 + 🔗 공식 문서 (opendrift.github.io) + 📚 Dagestad et al., Geosci. Model Dev. 11, 2018
@@ -992,7 +989,7 @@ function OpenDriftPanel() { ].map(s => (
{s.title}
-
+
{s.items.map((item, idx) => ( 'html' in item ?
@@ -1015,8 +1012,8 @@ function OpenDriftPanel() { ].map(w => (
{w.icon}
-
{w.title}
-
{w.desc}
+
{w.title}
+
{w.desc}
))}
@@ -1038,8 +1035,7 @@ function OpenDriftPanel() {
+ }}>
{node.label}
{node.sub}
@@ -1047,7 +1043,7 @@ function OpenDriftPanel() {
))}
-
+
해양모델(NEMO·ROMS·HYCOM) + 기상자료(ECMWF·GFS) → NOAA Oil Library 유종 매칭 → OpenDrift/OpenOil 모듈 구동 → NetCDF 결과 출력·시각화
@@ -1058,8 +1054,8 @@ function OpenDriftPanel() {
📄
-
OpenDrift / OpenOil 국내 해역 적용 연구 논문
-
한국 연안 유출유 확산 수치모의 관련 핵심 논문 3편 — WING 모델 이론 근거
+
OpenDrift / OpenOil 국내 해역 적용 연구 논문
+
한국 연안 유출유 확산 수치모의 관련 핵심 논문 3편 — WING 모델 이론 근거
@@ -1069,32 +1065,32 @@ function OpenDriftPanel() {
{[{ label: '국제저널', color: '#3b82f6' }, { label: 'OpenOil 적용', color: 'var(--green)' }, { label: '허베이 스피리트', color: 'var(--orange)' }].map(t => - {t.label} + {t.label} )}
- 2024 + 2024
-
Numerical Model Test of Spilled Oil Transport Near the Korean Coasts Using Various Input Parametric Models
-
Hai Van Dang, Suchan Joo, Junhyeok Lim, Jinhwan Hur, Sungwon Shin | Hanyang University ERICA | Journal of Ocean Engineering and Technology, 2024
+
Numerical Model Test of Spilled Oil Transport Near the Korean Coasts Using Various Input Parametric Models
+
Hai Van Dang, Suchan Joo, Junhyeok Lim, Jinhwan Hur, Sungwon Shin | Hanyang University ERICA | Journal of Ocean Engineering and Technology, 2024
-
- 연구 목적
+
+ 연구 목적
OpenOil 모델에 다양한 해양-기상 강제력(Met-Ocean) 조합을 적용하여 한국 연안 유출유 이동 예측 정확도를 비교 평가. 허베이 스피리트(2007) 사고를 대상으로 6개 조합 검증.
-
- 주요 결과
+
+ 주요 결과
CMEMS 해류모델 + ECMWF 기상 조합이 최고 성능. 동황해 조석 특성(수심 <60m 천해) 고려 시 정확도 향상. Envisat ASAR 위성 실측과 수치 비교 검증 완료.
-
적용 강제력 모델 6종 조합
-
+
적용 강제력 모델 6종 조합
+
해류 HYCOM / CMEMS
파랑 CMEMS / ECMWF-ERA5
기상 NCEP-GFS / ECMWF
-
+
WING 적용 의의 : OpenOil이 한국 해역에서 실효적으로 작동함을 검증한 국내 최신 연구. 6종 Met-Ocean 조합 비교는 WING의 OpenDrift 강제력 선택 기준(CMEMS+ECMWF)의 직접적 근거.
@@ -1104,29 +1100,29 @@ function OpenDriftPanel() {
{[{ label: '국내저널', color: '#06b6d4' }, { label: '라그랑지안 실증', color: 'var(--purple)' }, { label: '동남해역 검증', color: 'var(--yellow)' }].map(t => - {t.label} + {t.label} )}
- 1998 + 1998
-
한국 동남해역에서의 유출유 확산예측모델 (Oil Spill Behavior Forecasting Model in South-eastern Coastal Area of Korea)
-
류청로, 김종규, 설동관, 강동욱 | 부경대학교 해양공학과 | 한국해양환경공학회지 Vol.1 No.2, pp.52–59, 1998
+
한국 동남해역에서의 유출유 확산예측모델 (Oil Spill Behavior Forecasting Model in South-eastern Coastal Area of Korea)
+
류청로, 김종규, 설동관, 강동욱 | 부경대학교 해양공학과 | 한국해양환경공학회지 Vol.1 No.2, pp.52–59, 1998
-
- 연구 목적
+
+ 연구 목적
한국 동남해역(부산-울산) 긴급방제용 실시간 유출유 확산예측모델 OILSPILLFM 개발. 알렉산드리아호(1995) 실제 사고 힌드캐스팅으로 모델 검증.
-
- 핵심 수식
+
+ 핵심 수식
라그랑지안 입자추적 + 2D 해수유동 모델(ADI법). 취송류 Vw = 0.03×V10. 난류확산 Random Walk. 증발 1차 반응식.
-
+
해수유동 Navier-Stokes 2D 연속·운동방정식
조류 조화분석 실시간 조류 DB 구축
풍화 퍼짐·증발·유상화·생분해 5단계
-
+
WING 적용 의의 : OpenDrift·KOSPS의 라그랑지안 입자추적 이론적 원전. 조류 영향 미약 해역에서 취송류·해류 성분이 확산에 결정적 역할 — OpenDrift 한국 해역 적용 정당성 근거.
@@ -1136,39 +1132,39 @@ function OpenDriftPanel() {
{[{ label: '국내학술대회', color: 'var(--orange)' }, { label: '태안 허베이스피리트', color: 'var(--red)' }, { label: '서해 수치모의', color: 'var(--green)' }].map(t => - {t.label} + {t.label} )}
- 2008 + 2008
-
태안 기름유출사고의 유출유 확산특성 분석 (Analysis of Oil Spill Dispersion in Taean Coastal Zone)
-
정태성, 조형진 | 한남대학교 토목환경공학과 | 한국해안·해양공학회 학술발표논문집 제17권 pp.60–63, 2008
+
태안 기름유출사고의 유출유 확산특성 분석 (Analysis of Oil Spill Dispersion in Taean Coastal Zone)
+
정태성, 조형진 | 한남대학교 토목환경공학과 | 한국해안·해양공학회 학술발표논문집 제17권 pp.60–63, 2008
-
- 연구 목적
+
+ 연구 목적
2007년 12월 태안 허베이 스피리트 기름유출 사고 후 15일간 조류·취송류에 의한 유출유 확산패턴을 2D 수치모형으로 재현. 조류·취송류 기여도 분리 분석.
-
- 핵심 발견
+
+ 핵심 발견
취송류 풍속 비율 α = 2%가 실제 관측 확산과 가장 유사(3% 과대, 2.5% 다소 빠름). 취송류 편향각 θ = 20° 최적. 조류는 남북 확산거리, 취송류는 해안 부착에 기여.
-
+
해역 군산~영흥도(서해 중부)
격자 유한요소 삼각형, 최소 150m
방출 50입자/9시간 · 총 5일 모의
{/* 취송류 파라미터 비교 */}
-
취송류 매개변수 비교 (태안 사고 수치실험)
-
+
취송류 매개변수 비교 (태안 사고 수치실험)
+
α = 3%
과대 확산
α = 2.5%
다소 빠름
α = 2% ✓
최적 일치
θ = 20° ✓
최적 편향각
-
+
WING 적용 의의 : 서해(조류 강한 해역) 취송류 매개변수 α = 2%·θ = 20° 최적값은 OpenDrift 서해 운용 시 wind drift factor 설정의 직접 근거. 허베이 스피리트 사고는 POSEIDON(특허 10-1868791)의 검증 데이터로도 활용.
@@ -1186,7 +1182,7 @@ function LagrangianPanel() { style={{ background: 'linear-gradient(135deg,rgba(6,182,212,.05),rgba(59,130,246,.04))', border: '1px solid rgba(6,182,212,.2)' }}>
🧭 라그랑지안 입자추적법(Lagrangian Particle Tracking)
-
+
유출유를 수많은 가상 입자(Virtual Particle)로 표현하여, 각 입자의 이동 경로를 시간 단계마다 추적하는 방법입니다. KOSPS·POSEIDON·OpenDrift 3종 모두 이 방식을 채택합니다.
@@ -1198,7 +1194,7 @@ function LagrangianPanel() { dx/dt = U_c + α·U_w + U_stokes + U'
dy/dt = V_c + α·V_w + V_stokes + V'
-
+
U_c, V_c표층 해류·조류 속도 (m/s)
α·U_w풍류 기여 (α ≈ 0.03)
U_stokes스토크스 표류 (파랑 영향)
@@ -1211,7 +1207,7 @@ function LagrangianPanel() { U' = R · √(2K_h / Δt)
V' = R · √(2K_h / Δt)
-
+
R[-1, 1] 균등분포 난수
K_h수평 확산 계수 (m²/s)
Δt시간 스텝 (일반 1시간)
@@ -1233,7 +1229,7 @@ function LagrangianPanel() { R(t) = K₂ · (ΔρgV² / νw) · t¾
-
+
Δρ : 유류-해수 밀도차 (kg/m³)
@@ -1261,7 +1257,7 @@ function WeatheringPanel() { style={{ background: 'linear-gradient(135deg,rgba(249,115,22,.05),rgba(239,68,68,.04))', border: '1px solid rgba(249,115,22,.2)' }}>
🔁 유출유 풍화(Weathering) 프로세스
-
+
유출유는 해면에 유출된 직후부터 물리·화학·생물학적 풍화 과정을 거쳐 지속적으로 특성이 변화합니다. WING 시스템은 주요 풍화 프로세스를 통합 모의하여 시간 경과에 따른 유류 상태 변화를 추적합니다.
@@ -1276,7 +1272,7 @@ function WeatheringPanel() {
{w.title}
{w.desc}
{w.formula}
-
{w.note}
+
{w.note}
))}
@@ -1293,9 +1289,9 @@ function WeatheringPanel() { ].map((s, i) => (
-
{s.time}
-
{s.title}
-
{s.desc}
+
{s.time}
+
{s.title}
+
{s.desc}
{s.nextColor &&
}
@@ -1314,21 +1310,21 @@ function OceanInputPanel() { style={{ background: 'linear-gradient(135deg,rgba(34,197,94,.05),rgba(6,182,212,.04))', border: '1px solid rgba(34,197,94,.2)' }}>
🌊 해양환경 입력 데이터 체계
-
+
유출유 확산 예측의 정확도는 입력 해양·기상 데이터의 품질에 크게 좌우됩니다. WING 시스템은 국내외 수치예보 모델과 실시간 관측 데이터를 연동하여 모델 정확도를 향상시킵니다.
🌬️ 기상 입력
-
+
{[ { label: 'KMA RDAPS', desc: '동아시아 12km 해상도 · 24h 예보' }, { label: 'ECMWF ERA5', desc: '전지구 0.25° 재분석 데이터' }, { label: 'AWS 자동기상관측', desc: '실시간 10분 단위 풍향·풍속' }, ].map(t =>
-
{t.label}
+
{t.label}
{t.desc}
)} @@ -1336,14 +1332,14 @@ function OceanInputPanel() {
🌊 해양 입력
-
+
{[ { label: 'NIFS ROMS', desc: '한국 근해 1km 해류·수온 예보' }, { label: 'HYCOM', desc: '전지구 1/12° 해양 순환 모델' }, { label: 'TPXO9 조화분석', desc: '15개 분조 기반 조류 예측' }, ].map(t =>
-
{t.label}
+
{t.label}
{t.desc}
)} @@ -1363,7 +1359,7 @@ function VerificationPanel() { style={{ background: 'linear-gradient(135deg,rgba(59,130,246,.05),rgba(34,197,94,.04))', border: '1px solid rgba(59,130,246,.2)' }}>
✅ 모델 검증 사례
-
+
WING 시스템의 유출유 확산 모델은 과거 실제 사고 재현 시뮬레이션을 통해 정확도가 검증되었습니다.
@@ -1382,7 +1378,7 @@ function VerificationPanel() { ].map(s => (
{s.value}
-
{s.label}
+
{s.label}
))}
@@ -1393,7 +1389,7 @@ function VerificationPanel() {
2014년 우이산 충돌 사고 재현
검증완료
-
+
여수 오동도 인근 원유 유출 사고를 KOSPS 모델로 재현한 결과, 실제 위성 SAR 영상과 비교 시 유막 중심 이동 경로 일치율 82.1% 확인. 남해 특유의 복잡한 조류 패턴 반영이 관건.
@@ -1404,7 +1400,7 @@ function VerificationPanel() { 📄 모델 검증 관련 논문
- {papersOpen &&
+ {papersOpen &&
{[ { num: '①', title: '허베이스피리트호 유출유 확산예측 검증 분석', authors: '이문진 · 김선동 · 김혜진 · 오세웅', journal: '한국해양과학기술협의회 공동학술대회', detail: 'pp.3154 | 2010', desc: 'Radarsat 인공위성영상 비교 분석 · NOAA GNOME과 동일 입력조건 하 비교 검증', color: 'var(--red)', system: 'KOSPS' }, { num: '②', title: '3차원 유출유 확산예측 시스템 연구', authors: '이문진 · 김혜진 · 강관근', journal: '한국해양과학기술협의회 공동학술대회', detail: 'pp.17-18 | 2013', desc: 'Monte Carlo Simulation 기반 3D 확산모델 · 허베이 스피리트호 720시간 적용 검증', color: 'var(--purple)', system: 'KOSPS' }, @@ -1421,7 +1417,7 @@ function VerificationPanel() {
{paper.num}
-
{paper.title}
+
{paper.title}
{paper.system}
{paper.authors} | {paper.journal} {paper.detail}
@@ -1443,7 +1439,7 @@ function EnsemblePanel() { style={{ background: 'linear-gradient(135deg,rgba(168,85,247,.05),rgba(6,182,212,.04))', border: '1px solid rgba(168,85,247,.2)' }}>
⚡ 앙상블 예측(Ensemble Prediction)
-
+
WING 시스템은 3종의 유출유 확산 모델(KOSPS·POSEIDON·OpenDrift)을 동시 운용하여 앙상블 결과를 생산합니다. 단일 모델 대비 불확실성을 정량화하고 신뢰 구간을 제공합니다.
@@ -1471,22 +1467,22 @@ function RoadmapPanel() {
⚠️ 현재 모델 한계
-
-
2D 표층 확산 모의 위주 → 3D 수직 구조 반영 미흡
-
기상·해양 결합 피드백 미적용 → 대규모 유출 시 정확도 저하
-
조류 조화상수 분해능 한계 → 항만·연안 세부 예측 오차
+
+
2D 표층 확산 모의 위주 → 3D 수직 구조 반영 미흡
+
기상·해양 결합 피드백 미적용 → 대규모 유출 시 정확도 저하
+
조류 조화상수 분해능 한계 → 항만·연안 세부 예측 오차
🚀 발전 방향
-
+
{[ { title: 'ROMS 3D 해양모델 결합', desc: '광역→상세역 Nesting 200m급 고해상도 구현' }, { title: 'AI/ML 서로게이트 모델', desc: '수치모델 학습 기반 수초 내 근사 예측' }, { title: '위성 SAR 실시간 동화', desc: '관측 유막 데이터 모델 보정 자동화' }, ].map(r => (
-
{r.title}
+
{r.title}
{r.desc}
))} @@ -1506,9 +1502,9 @@ function RoadmapPanel() { ].map((s, i) => (
-
{s.phase}
-
{s.title}
-
{s.desc}
+
{s.phase}
+
{s.title}
+
{s.desc}
{s.nextColor &&
}
diff --git a/frontend/src/tabs/prediction/components/OilSpillView.tsx b/frontend/src/tabs/prediction/components/OilSpillView.tsx index 5d58530..246554a 100755 --- a/frontend/src/tabs/prediction/components/OilSpillView.tsx +++ b/frontend/src/tabs/prediction/components/OilSpillView.tsx @@ -603,12 +603,12 @@ export function OilSpillView() {
{[ - { label: '풍화율', value: `${Math.min(99, Math.round(timelinePosition * 0.4))}%`, color: 'var(--t1)' }, - { label: '면적', value: `${(timelinePosition * 0.08).toFixed(1)} km²`, color: 'var(--t1)' }, + { label: '풍화율', value: `${Math.min(99, Math.round(timelinePosition * 0.4))}%` }, + { label: '면적', value: `${(timelinePosition * 0.08).toFixed(1)} km²` }, { label: '차단율', value: boomLines.length > 0 ? `${Math.min(95, 70 + Math.round(timelinePosition * 0.2))}%` : '—', color: 'var(--boom)' }, ].map((s, i) => (
- {s.label} + {s.label} {s.value}
))} diff --git a/frontend/src/tabs/prediction/components/PredictionInputSection.tsx b/frontend/src/tabs/prediction/components/PredictionInputSection.tsx index 644ac54..fea7b72 100644 --- a/frontend/src/tabs/prediction/components/PredictionInputSection.tsx +++ b/frontend/src/tabs/prediction/components/PredictionInputSection.tsx @@ -81,7 +81,7 @@ const PredictionInputSection = ({ {expanded && (
{/* Input Mode Selection */} -
+
diff --git a/frontend/src/tabs/prediction/components/RecalcModal.tsx b/frontend/src/tabs/prediction/components/RecalcModal.tsx index 6e033d4..260bcd0 100755 --- a/frontend/src/tabs/prediction/components/RecalcModal.tsx +++ b/frontend/src/tabs/prediction/components/RecalcModal.tsx @@ -134,13 +134,13 @@ export function RecalcModal({

확산예측 재계산

유출유·유출량 등 파라미터를 수정하여 재실행
@@ -168,7 +168,7 @@ export function RecalcModal({ padding: '10px 12px', background: 'rgba(6,182,212,0.04)', border: '1px solid rgba(6,182,212,0.15)', borderRadius: '8px', }}> -
+
현재 분석 정보
@@ -245,7 +245,7 @@ export function RecalcModal({
-
+
위도 (N)
-
+
경도 (E)
{label}
@@ -372,8 +372,8 @@ function FieldGroup({ label, children }: { label: string; children: React.ReactN function InfoItem({ label, value }: { label: string; value: string }) { return (
- {label} - {value} + {label} + {value}
) } diff --git a/frontend/src/tabs/reports/components/OilSpillReportTemplate.tsx b/frontend/src/tabs/reports/components/OilSpillReportTemplate.tsx index 9a607ba..9e68dd1 100755 --- a/frontend/src/tabs/reports/components/OilSpillReportTemplate.tsx +++ b/frontend/src/tabs/reports/components/OilSpillReportTemplate.tsx @@ -146,7 +146,7 @@ export function createSampleReport(): OilSpillReportData { // ─── Styles ───────────────────────────────────────────────── const S = { - page: { background: 'var(--bg1)', color: 'var(--t1)', padding: '32px 40px', marginBottom: '24px', borderRadius: '6px', border: '1px solid var(--bd)', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif", fontSize: '12px', lineHeight: '1.6', position: 'relative' as const, width: '100%', boxSizing: 'border-box' as const }, + page: { background: 'var(--bg1)', padding: '32px 40px', marginBottom: '24px', borderRadius: '6px', border: '1px solid var(--bd)', fontFamily: "'Pretendard', 'Noto Sans KR', sans-serif", fontSize: '12px', lineHeight: '1.6', position: 'relative' as const, width: '100%', boxSizing: 'border-box' as const }, sectionTitle: { background: 'rgba(6,182,212,0.12)', color: 'var(--cyan)', padding: '8px 16px', fontSize: '13px', fontWeight: 700, marginBottom: '12px', borderRadius: '4px', border: '1px solid rgba(6,182,212,0.2)' }, subHeader: { fontSize: '14px', fontWeight: 700, color: 'var(--cyan)', marginBottom: '12px', borderBottom: '2px solid var(--bd)', paddingBottom: '6px' }, table: { width: '100%', tableLayout: 'fixed' as const, borderCollapse: 'collapse' as const, fontSize: '11px', marginBottom: '16px' }, @@ -160,7 +160,7 @@ const S = { // ─── Editable cell ────────────────────────────────────────── const inputStyle: React.CSSProperties = { width: '100%', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '3px', - padding: '4px 8px', fontSize: '11px', color: 'var(--t1)', outline: 'none', textAlign: 'center', + padding: '4px 8px', fontSize: '11px', outline: 'none', textAlign: 'center', } function ECell({ value, editing, onChange, align, placeholder }: { @@ -302,7 +302,7 @@ function Page3({ data, editing, onChange }: { data: OilSpillReportData; editing: value={data.analysis} onChange={e => onChange({ ...data, analysis: e.target.value })} placeholder="분석 내용을 작성하세요..." - style={{ width: '100%', minHeight: '300px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '16px', fontSize: '13px', color: 'var(--t1)', outline: 'none', resize: 'vertical', lineHeight: '1.8' }} + style={{ width: '100%', minHeight: '300px', background: 'var(--bg0)', border: '1px solid var(--bdL)', borderRadius: '4px', padding: '16px', fontSize: '13px', outline: 'none', resize: 'vertical', lineHeight: '1.8' }} /> ) : (
@@ -459,7 +459,7 @@ function Page6({ data, editing, onChange }: { data: OilSpillReportData; editing: {editing && onChange({ ...data, vessels: [...data.vessels, { name: '', org: '', dist: '', speed: '', ton: '', collectorType: '', collectorCap: '', boomType: '', boomLength: '' }] })} />}
기타 장비
{editing ? ( -
구분
')) }} /> - - - + + +