From 2082e9a79b12336b2efe061f148056d5de6d1e0c Mon Sep 17 00:00:00 2001 From: leedano Date: Wed, 15 Apr 2026 16:49:00 +0900 Subject: [PATCH 1/8] =?UTF-8?q?refactor(map):=20TimelineControl=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20aerial/hns=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 +- CLAUDE.md | 2 + backend/src/aerial/aerialRouter.ts | 115 ++ backend/src/aerial/aerialService.ts | 2 +- .../src/common/components/map/MapView.tsx | 126 +- .../common/components/map/TimelineControl.tsx | 152 +++ .../common/components/ui/UserManualPopup.tsx | 1079 +-------------- frontend/src/common/data/chapters.json | 1187 +++++++++++++++++ frontend/src/common/data/manualChapters.ts | 30 + .../aerial/components/MediaManagement.tsx | 334 ++++- .../aerial/components/OilAreaAnalysis.tsx | 10 +- .../src/tabs/aerial/services/aerialApi.ts | 28 + .../tabs/hns/components/HNSScenarioView.tsx | 34 +- frontend/src/tabs/hns/components/HNSView.tsx | 279 ++-- .../components/IncidentsLeftPanel.tsx | 2 +- .../incidents/components/IncidentsView.tsx | 4 +- .../prediction/components/OilSpillView.tsx | 4 +- .../src/tabs/scat/components/PreScatView.tsx | 4 +- frontend/tsconfig.app.json | 1 + .../.claude/settings.local.json | 6 + scripts/generate_manual_pdfs/README.md | 39 + scripts/generate_manual_pdfs/generate.py | 73 + scripts/generate_manual_pdfs/requirements.txt | 2 + scripts/generate_manual_pdfs/style.css | 262 ++++ scripts/generate_manual_pdfs/template.html | 92 ++ 25 files changed, 2434 insertions(+), 1438 deletions(-) create mode 100644 frontend/src/common/components/map/TimelineControl.tsx create mode 100644 frontend/src/common/data/chapters.json create mode 100644 frontend/src/common/data/manualChapters.ts create mode 100644 scripts/generate_manual_pdfs/.claude/settings.local.json create mode 100644 scripts/generate_manual_pdfs/README.md create mode 100644 scripts/generate_manual_pdfs/generate.py create mode 100644 scripts/generate_manual_pdfs/requirements.txt create mode 100644 scripts/generate_manual_pdfs/style.css create mode 100644 scripts/generate_manual_pdfs/template.html diff --git a/.gitignore b/.gitignore index a7b64e4..42533eb 100755 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,7 @@ frontend/public/hns-manual/images/ # mcp -.mcp.json \ No newline at end of file +.mcp.json + +# python +.venv \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 3cbe76c..6c428ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,6 +107,8 @@ wing/ - `naming.md` -- 네이밍 규칙 - `testing.md` -- 테스트 규칙 - `subagent-policy.md` -- 서브에이전트 활용 정책 +- `design-system.md` -- AI 에이전트 UI 디자인 시스템 규칙 (영문, 실사용) +- `design-system-ko.md` -- 디자인 시스템 규칙 (한국어 참고용) ## 개발 문서 (docs/) diff --git a/backend/src/aerial/aerialRouter.ts b/backend/src/aerial/aerialRouter.ts index 7f892e7..65d5f34 100644 --- a/backend/src/aerial/aerialRouter.ts +++ b/backend/src/aerial/aerialRouter.ts @@ -1,6 +1,8 @@ import express from 'express'; +import { mkdirSync, existsSync } from 'fs'; import multer from 'multer'; import path from 'path'; +import { randomUUID } from 'crypto'; import { listMedia, createMedia, @@ -25,6 +27,29 @@ import { requireAuth, requirePermission } from '../auth/authMiddleware.js'; const router = express.Router(); const stitchUpload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } }); +const mediaUpload = multer({ + storage: multer.diskStorage({ + destination: (_req, _file, cb) => { + const dir = path.resolve('uploads', 'aerial'); + mkdirSync(dir, { recursive: true }); + cb(null, dir); + }, + filename: (_req, file, cb) => { + const ext = path.extname(file.originalname); + cb(null, `${randomUUID()}${ext}`); + }, + }), + limits: { fileSize: 2 * 1024 * 1024 * 1024 }, // 2GB + fileFilter: (_req, file, cb) => { + const allowed = /\.(jpe?g|png|tiff?|geotiff|mp4|mov)$/i; + if (allowed.test(path.extname(file.originalname))) { + cb(null, true); + } else { + cb(new Error('허용되지 않는 파일 형식입니다.')); + } + }, +}); + // ============================================================ // AERIAL_MEDIA 라우트 // ============================================================ @@ -73,6 +98,96 @@ router.post('/media', requireAuth, requirePermission('aerial', 'CREATE'), async } }); +// POST /api/aerial/media/upload — 파일 업로드 + 메타 등록 +router.post('/media/upload', requireAuth, requirePermission('aerial', 'CREATE'), mediaUpload.single('file'), async (req, res) => { + try { + const file = req.file; + if (!file) { + res.status(400).json({ error: '파일이 필요합니다.' }); + return; + } + const { equipTpCd, equipNm, mediaTpCd, acdntSn, memo } = req.body as { + equipTpCd?: string; + equipNm?: string; + mediaTpCd?: string; + acdntSn?: string; + memo?: string; + }; + + const isVideo = file.mimetype.startsWith('video/'); + const detectedMediaType = mediaTpCd ?? (isVideo ? '영상' : '사진'); + const fileSzMb = (file.size / (1024 * 1024)).toFixed(2) + ' MB'; + + const result = await createMedia({ + fileNm: file.filename, + orgnlNm: file.originalname, + filePath: file.path, + equipTpCd: equipTpCd ?? 'drone', + equipNm: equipNm ?? '기타', + mediaTpCd: detectedMediaType, + fileSz: fileSzMb, + acdntSn: acdntSn ? parseInt(acdntSn, 10) : undefined, + locDc: memo ?? undefined, + }); + + res.status(201).json(result); + } catch (err) { + console.error('[aerial] 미디어 업로드 오류:', err); + res.status(500).json({ error: '미디어 업로드 실패' }); + } +}); + +// GET /api/aerial/media/:sn/view — 원본 이미지 뷰어용 (inline 표시) +router.get('/media/:sn/view', requireAuth, requirePermission('aerial', 'READ'), async (req, res) => { + try { + const sn = parseInt(req.params['sn'] as string, 10); + if (!isValidNumber(sn, 1, 999999)) { + res.status(400).json({ error: '유효하지 않은 미디어 번호' }); + return; + } + + const media = await getMediaBySn(sn); + if (!media) { + res.status(404).json({ error: '미디어를 찾을 수 없습니다.' }); + return; + } + + // 로컬 업로드 파일이면 직접 서빙 + if (media.filePath) { + const absPath = path.resolve(media.filePath); + if (existsSync(absPath)) { + const ext = path.extname(absPath).toLowerCase(); + const mimeMap: Record = { + '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', + '.tif': 'image/tiff', '.tiff': 'image/tiff', + '.mp4': 'video/mp4', '.mov': 'video/quicktime', + }; + res.setHeader('Content-Type', mimeMap[ext] ?? 'application/octet-stream'); + res.setHeader('Content-Disposition', 'inline'); + res.setHeader('Cache-Control', 'private, max-age=300'); + res.sendFile(absPath); + return; + } + } + + const fileId = media.fileNm.substring(0, 36); + const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!UUID_PATTERN.test(fileId) || !media.equipNm) { + res.status(404).json({ error: '표시 가능한 이미지가 없습니다.' }); + return; + } + + const buffer = await fetchOriginalImage(media.equipNm, fileId); + res.setHeader('Content-Type', 'image/jpeg'); + res.setHeader('Content-Disposition', 'inline'); + res.setHeader('Cache-Control', 'private, max-age=300'); + res.send(buffer); + } catch (err) { + console.error('[aerial] 이미지 뷰어 오류:', err); + res.status(502).json({ error: '이미지 조회 실패' }); + } +}); + // GET /api/aerial/media/:sn/download — 원본 이미지 다운로드 router.get('/media/:sn/download', requireAuth, requirePermission('aerial', 'READ'), async (req, res) => { try { diff --git a/backend/src/aerial/aerialService.ts b/backend/src/aerial/aerialService.ts index c3ec980..f8c907c 100644 --- a/backend/src/aerial/aerialService.ts +++ b/backend/src/aerial/aerialService.ts @@ -368,7 +368,7 @@ export async function updateSatRequestStatus(sn: number, sttsCd: string): Promis // OIL INFERENCE (GPU 서버 프록시) // ============================================================ -const IMAGE_API_URL = process.env.IMAGE_API_URL ?? 'http://localhost:5001'; +const IMAGE_API_URL = process.env.IMAGE_API_URL ?? 'http://211.208.115.83:5001'; const OIL_INFERENCE_URL = process.env.OIL_INFERENCE_URL || 'http://localhost:8090'; const INFERENCE_TIMEOUT_MS = 10_000; diff --git a/frontend/src/common/components/map/MapView.tsx b/frontend/src/common/components/map/MapView.tsx index 98fa5d1..81b6983 100755 --- a/frontend/src/common/components/map/MapView.tsx +++ b/frontend/src/common/components/map/MapView.tsx @@ -18,6 +18,7 @@ import type { SensitiveResourceFeatureCollection, } from '@tabs/prediction/services/predictionApi'; import HydrParticleOverlay from './HydrParticleOverlay'; +import { TimelineControl } from './TimelineControl'; import type { BoomLine, BoomLineCoord } from '@common/types/boomLine'; import type { ReplayShip, CollisionEvent, BackwardParticleStep } from '@common/types/backtrack'; import { createBacktrackLayers } from './BacktrackReplayOverlay'; @@ -1417,6 +1418,7 @@ export function MapView({ onTimeChange={setInternalCurrentTime} onPlayPause={() => setIsPlaying(!isPlaying)} onSpeedChange={setPlaybackSpeed} + simulationStartTime={simulationStartTime} /> )} @@ -1676,130 +1678,6 @@ function CoordinateDisplay({ position, zoom }: { position: [number, number]; zoo ); } -// 타임라인 컨트롤 -interface TimelineControlProps { - currentTime: number; - maxTime: number; - isPlaying: boolean; - playbackSpeed: number; - onTimeChange: (time: number) => void; - onPlayPause: () => void; - onSpeedChange: (speed: number) => void; -} - -function TimelineControl({ - currentTime, - maxTime, - isPlaying, - playbackSpeed, - onTimeChange, - onPlayPause, - onSpeedChange, -}: TimelineControlProps) { - const progressPercent = (currentTime / maxTime) * 100; - - const handleRewind = () => onTimeChange(Math.max(0, currentTime - 6)); - const handleForward = () => onTimeChange(Math.min(maxTime, currentTime + 6)); - const handleStart = () => onTimeChange(0); - const handleEnd = () => onTimeChange(maxTime); - - const toggleSpeed = () => { - const speeds = [1, 2, 4]; - const currentIndex = speeds.indexOf(playbackSpeed); - onSpeedChange(speeds[(currentIndex + 1) % speeds.length]); - }; - - const handleTimelineClick = (e: React.MouseEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - onTimeChange(Math.max(0, Math.min(maxTime, Math.round(percent * maxTime)))); - }; - - const timeLabels = []; - for (let t = 0; t <= maxTime; t += 6) { - timeLabels.push(t); - } - - return ( -
-
-
- ⏮ -
-
- ◀ -
-
- {isPlaying ? '⏸' : '▶'} -
-
- ▶▶ -
-
- ⏭ -
-
-
- {playbackSpeed}× -
-
-
-
- {timeLabels.map((t) => ( - - {t}h - - ))} -
-
-
-
- {timeLabels.map((t) => ( -
- ))} -
-
-
-
-
- {/* eslint-disable-next-line react-hooks/purity */} -
- +{currentTime.toFixed(0)}h —{' '} - {(() => { - const base = simulationStartTime ? new Date(simulationStartTime) : new Date(); - const d = new Date(base.getTime() + currentTime * 3600 * 1000); - return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} KST`; - })()} -
-
-
- 진행률 - {progressPercent.toFixed(0)}% -
-
- 속도 - {playbackSpeed}× -
-
- 시간 - - {currentTime.toFixed(0)}/{maxTime}h - -
-
-
-
- ); -} - // 기상 데이터 Mock function getWeatherData(position: [number, number]) { const [lat, lng] = position; diff --git a/frontend/src/common/components/map/TimelineControl.tsx b/frontend/src/common/components/map/TimelineControl.tsx new file mode 100644 index 0000000..bcfbc77 --- /dev/null +++ b/frontend/src/common/components/map/TimelineControl.tsx @@ -0,0 +1,152 @@ +import type { MouseEvent } from 'react'; + +export interface TimelineControlProps { + currentTime: number; + maxTime: number; + isPlaying: boolean; + playbackSpeed: number; + onTimeChange: (time: number) => void; + onPlayPause: () => void; + onSpeedChange: (speed: number) => void; + simulationStartTime?: string; + stepSize?: number; + tickInterval?: number; + majorTickEvery?: number; + timeUnitLabel?: string; + formatOffset?: (t: number) => string; + formatAbsolute?: (t: number, base: Date) => string; + showSpeedToggle?: boolean; +} + +export function TimelineControl({ + currentTime, + maxTime, + isPlaying, + playbackSpeed, + onTimeChange, + onPlayPause, + onSpeedChange, + simulationStartTime, + stepSize = 6, + tickInterval = 6, + majorTickEvery = 12, + timeUnitLabel = 'h', + formatOffset, + formatAbsolute, + showSpeedToggle = true, +}: TimelineControlProps) { + const progressPercent = maxTime > 0 ? (currentTime / maxTime) * 100 : 0; + + const handleRewind = () => onTimeChange(Math.max(0, currentTime - stepSize)); + const handleForward = () => onTimeChange(Math.min(maxTime, currentTime + stepSize)); + const handleStart = () => onTimeChange(0); + const handleEnd = () => onTimeChange(maxTime); + + const toggleSpeed = () => { + const speeds = [1, 2, 4]; + const currentIndex = speeds.indexOf(playbackSpeed); + onSpeedChange(speeds[(currentIndex + 1) % speeds.length]); + }; + + const handleTimelineClick = (e: MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + onTimeChange(Math.max(0, Math.min(maxTime, Math.round(percent * maxTime)))); + }; + + const timeLabels: number[] = []; + for (let t = 0; t <= maxTime; t += tickInterval) { + timeLabels.push(t); + } + + const defaultOffset = (t: number) => `+${t.toFixed(0)}${timeUnitLabel}`; + const defaultAbsolute = (t: number, base: Date) => { + const d = new Date(base.getTime() + t * 3600 * 1000); + return `${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')} KST`; + }; + + const offsetStr = (formatOffset ?? defaultOffset)(currentTime); + const baseDate = simulationStartTime ? new Date(simulationStartTime) : new Date(); + const absoluteStr = (formatAbsolute ?? defaultAbsolute)(currentTime, baseDate); + + return ( +
+
+
+ ⏮ +
+
+ ◀ +
+
+ {isPlaying ? '⏸' : '▶'} +
+
+ ▶▶ +
+
+ ⏭ +
+ {showSpeedToggle && ( + <> +
+
+ {playbackSpeed}× +
+ + )} +
+
+
+ {timeLabels.map((t) => ( + + {t} + {timeUnitLabel} + + ))} +
+
+
+
+ {timeLabels.map((t) => ( +
+ ))} +
+
+
+
+
+
+ {offsetStr} — {absoluteStr} +
+
+
+ 진행률 + {progressPercent.toFixed(0)}% +
+ {showSpeedToggle && ( +
+ 속도 + {playbackSpeed}× +
+ )} +
+ 시간 + + {currentTime.toFixed(0)}/{maxTime} + {timeUnitLabel} + +
+
+
+
+ ); +} diff --git a/frontend/src/common/components/ui/UserManualPopup.tsx b/frontend/src/common/components/ui/UserManualPopup.tsx index bcbe904..5ff0bda 100644 --- a/frontend/src/common/components/ui/UserManualPopup.tsx +++ b/frontend/src/common/components/ui/UserManualPopup.tsx @@ -1,1069 +1,11 @@ import { useState } from 'react'; +import { CHAPTERS } from '@common/data/manualChapters'; interface UserManualPopupProps { isOpen: boolean; onClose: () => void; } -interface InputItem { - label: string; - type: string; - required: boolean; - desc: string; -} - -interface ScreenItem { - id: string; - name: string; - menuPath: string; - imageIndex: number; - overview: string; - description?: string; - procedure?: string[]; - inputs?: InputItem[]; - notes?: string[]; -} - -interface Chapter { - id: string; - number: string; - title: string; - subtitle: string; - screens: ScreenItem[]; -} - -const CHAPTERS: Chapter[] = [ - { - id: 'ch01', - number: '01', - title: '유출유 확산예측', - subtitle: 'Oil Spill Dispersion Prediction', - screens: [ - { - id: '001', - name: '직접입력', - menuPath: '유출유확산예측 > 유출유확산분석', - imageIndex: 1, - overview: - '해양 유출유 사고 발생 시 오염원 위치와 유출 조건을 직접 입력하여 확산 범위를 예측하는 주요 분석 화면이다. KOSPS·POSEIDON·OpenDrift·앙상블의 4종 수치 모델을 선택적으로 적용할 수 있다. 실시간 기상·해양 데이터와 연계하여 즉시 예측 결과를 지도에 표출한다.', - description: - "화면 좌측에 예측정보 입력 패널(사고명, 위치, 유종, 유출량, 예측 시간)이 위치한다. 중앙 지도에서 클릭하여 사고 발생 위치를 직접 지정하거나 위·경도 좌표를 수동 입력할 수 있다. 하단 타임라인 슬라이더로 시간 경과에 따른 확산 경과를 재생할 수 있다. 우측 '분석 요약' 패널에서 예측 면적, 이동거리, 이동 방향, 풍화 비율 등을 확인할 수 있다.", - procedure: [ - "상단 메뉴에서 '유출유확산예측 > 유출유확산분석'을 클릭하여 화면을 이동한다.", - '좌측 패널에서 사고명, 날짜, 유종, 유출량, 예측 시간을 입력한다.', - '지도를 클릭하거나 좌표 입력란에 위·경도를 직접 입력하여 사고 위치를 지정한다.', - '적용할 확산 모델(KOSPS·POSEIDON·OpenDrift·앙상블) 체크박스를 선택한다.', - "'확산예측 실행' 버튼을 클릭하여 예측을 시작한다.", - '하단 타임라인 재생 버튼으로 시간별 확산 결과를 확인한다.', - ], - inputs: [ - { label: '사고명', type: '텍스트', required: true, desc: '사고 식별 명칭' }, - { label: '날짜/시간', type: '날짜+시간', required: true, desc: '사고 발생 일시' }, - { label: '위도/경도', type: '숫자', required: true, desc: '지도 클릭 자동 입력 가능' }, - { label: '유종', type: '드롭다운', required: true, desc: '벙커C유·경유·연료유 등' }, - { label: '유출량', type: '숫자 kL', required: true, desc: '유출 유류 총량' }, - { label: '예측 시간', type: '숫자 h', required: true, desc: '확산 예측 기간' }, - { - label: '적용 모델', - type: '체크박스', - required: true, - desc: 'KOSPS·POSEIDON·OpenDrift·앙상블 중 선택', - }, - ], - notes: [ - "좌표 입력 후 반드시 '적용' 버튼을 클릭해야 지도에 반영된다.", - '앙상블 모델 선택 시 3개 모델이 동시 실행되어 계산 시간이 증가할 수 있다.', - '유출량이 0 이하이거나 예측 시간이 입력되지 않으면 실행 버튼이 비활성화된다.', - ], - }, - { - id: '002', - name: '모델종류별 확산예측 실행', - menuPath: '유출유확산예측 > 유출유확산분석', - imageIndex: 2, - overview: - 'KOSPS·POSEIDON·OpenDrift 3개 모델의 예측 결과를 동시에 지도에 표출하여 모델 간 비교 분석을 지원한다. 모델별 입자 궤적(파란점·빨간점·하늘점)이 중첩 표시되어 확산 경향의 편차를 한눈에 파악할 수 있다.', - description: - "지도에 모델별 색상 구분 입자가 동시에 표시되며, 각 모델의 확산 방향과 범위 차이를 시각적으로 비교할 수 있다. 우측 '분석 요약' 패널에 예측 면적(km2), 이동거리(km), 방향, 이동속도(cm/s)가 표출된다. 풍화 상태 막대그래프(유출량·해면연소·자연분산·수중분산·잔류 비율)를 제공한다. '다각형 분석수행' 버튼으로 특정 구역 내 오염 면적을 별도 산정할 수 있다.", - procedure: [ - '확산분석 화면에서 적용 모델 체크박스를 2개 이상 선택한다.', - "'확산예측 실행' 버튼을 클릭한다.", - '지도에서 각 모델의 입자 분포와 확산 범위를 비교한다.', - "우측 '분석 요약' 탭에서 모델별 정량 지표를 확인한다.", - '하단 타임라인으로 시간 경과별 확산 변화를 재생한다.', - ], - notes: [ - '여러 모델을 동시에 선택하면 계산 자원 소모가 증가하여 결과 표출까지 수 분이 소요될 수 있다.', - '모델별 예측 결과에 차이가 있을 경우, 앙상블 결과 또는 현장 데이터와 교차 검토하여 활용한다.', - ], - }, - { - id: '003', - name: '사고정보 조회', - menuPath: '유출유확산예측 > 유출유확산분석', - imageIndex: 3, - overview: - '시스템에 등록된 실제 사고 정보를 불러와 확산분석에 자동 연동하는 기능이다. 사고코드·선박명·유종·유출량·발생 좌표 등이 자동으로 예측정보 입력란에 채워진다.', - description: - "좌측 '사고정보' 패널에 진행 중인 사고 목록이 표시된다. 사고를 선택하면 지도 중심이 해당 사고 발생 위치로 자동 이동하고 위치 핀이 표시된다. 사고 상태는 '진행중(빨간 배지)'과 '종료(회색 배지)'로 구분된다.", - procedure: [ - "좌측 '사고정보' 패널의 펼침 버튼을 클릭하여 패널을 연다.", - '목록에서 분석할 사고를 클릭한다.', - '사고 정보가 예측정보 입력란에 자동으로 입력되었는지 확인한다.', - '필요 시 유출량·예측 시간 등 추가 정보를 수정한다.', - "'확산예측 실행'을 클릭하여 분석을 시작한다.", - ], - notes: [ - "'진행중' 상태의 사고만 실시간 확산예측과 연동된다.", - '사고 데이터는 관계 기관으로부터 등록된 정보를 기반으로 하며, 입력 오류 시 관리자에게 문의한다.', - ], - }, - { - id: '004', - name: '영향 민감자원 레이어 중첩 표시', - menuPath: '유출유확산예측 > 유출유확산분석', - imageIndex: 4, - overview: - '유출유 확산 예측 결과와 해양 민감자원(환경생태·수산자원·관광지 등) 레이어를 지도에 동시 표출하는 기능이다. 잠재적 피해 자원을 사전에 파악하여 방제 우선순위 설정에 활용한다.', - description: - "좌측 하단 '정보 레이어' 패널에서 환경생태·사회경제·민감도평가·해경관할구역 등 대분류 토글을 제공한다. 각 항목 우측에 해당 레이어 데이터 건수가 표시된다. '전체 켜기/끄기' 버튼으로 모든 레이어를 일괄 제어할 수 있다.", - procedure: [ - "좌측 하단 '정보 레이어' 패널을 열고 원하는 레이어 항목을 활성화한다.", - '확산예측 실행 후 지도에 표출된 오염 범위와 레이어 분포를 비교한다.', - '오염 영향이 우려되는 자원 레이어를 클릭하여 상세 정보를 확인한다.', - '필요한 레이어 조합을 선택하여 보고서용 지도 캡처에 활용한다.', - ], - notes: [ - '레이어 수가 많을 경우 지도 로딩 속도가 느려질 수 있으므로 필요한 레이어만 선택적으로 활성화한다.', - ], - }, - { - id: '005', - name: 'AI자동추천 배치안 적용', - menuPath: '유출유확산예측 > 유출유확산분석 > 오일펜스 배치 가이드', - imageIndex: 5, - overview: - '확산예측 결과를 기반으로 AI(NSGA-II 알고리즘)가 최적 오일펜스 배치안을 자동으로 생성하는 기능이다. 최대 3개 방어선의 위치·방향·총 길이·차단율을 자동 계산하여 지도에 표출한다.', - description: - "'오일펜스 배치 가이드' 패널 상단 'AI자동 추천' 탭에서 배치 결과를 확인한다. 1차~3차 방어선별 배치 위치(좌표), 방향, 오일펜스 길이, 예상 차단율이 표시된다.", - procedure: [ - "확산예측 실행 완료 후 좌측 '오일펜스 배치 가이드' 패널을 연다.", - "'AI자동 추천' 탭을 선택하여 추천 배치안을 확인한다.", - '방어선별 차단율 및 오일펜스 총 길이를 검토한다.', - "'추천 배치안 적용하기' 버튼을 클릭하여 지도에 표출한다.", - '현장 여건(조류·지형·자원 현황)을 고려하여 최종 배치안을 확정한다.', - ], - inputs: [ - { label: '배치 수', type: '숫자', required: false, desc: '투입할 오일펜스 방어선 수' }, - { label: '최소 차단율', type: '숫자 %', required: false, desc: '목표 차단율' }, - ], - notes: [ - 'AI 추천 기능은 확산예측이 완료된 후에 활성화된다.', - '추천 배치안은 수치 모델 기반 결과로, 현장 조류 변동·수심·접근 가능성 등을 반드시 현장 담당자가 최종 확인해야 한다.', - ], - }, - { - id: '006', - name: '이미지업로드', - menuPath: '유출유확산예측 > 유출유확산분석', - imageIndex: 6, - overview: - '위성·드론·항공기에서 촬영한 이미지를 업로드하여 실제 오염 현황과 모델 예측 결과를 비교 분석하는 기능이다.', - description: - "예측정보 입력 패널 상단 '이미지 업로드' 탭을 선택하면 파일 업로드 영역이 활성화된다. 업로드된 이미지는 지도에 자동으로 오버레이 표출된다.", - procedure: [ - "예측정보 입력 패널에서 '이미지 업로드' 탭을 선택한다.", - "'파일 선택' 버튼을 클릭하거나 파일을 드래그하여 업로드한다.", - '업로드된 이미지가 지도에 올바르게 표출되는지 위치를 확인한다.', - '확산예측 결과와 이미지를 동시에 표출하여 비교한다.', - ], - inputs: [ - { - label: '업로드 파일', - type: '파일 PNG/JPG', - required: false, - desc: '위성·드론·항공기 촬영 이미지', - }, - ], - notes: [ - '지원 파일 형식은 PNG, JPG이다.', - '업로드한 이미지의 좌표 기준(GCP)이 없을 경우 지도 상 위치 정확도가 낮을 수 있다.', - ], - }, - { - id: '007', - name: '분석 목록', - menuPath: '유출유확산예측', - imageIndex: 7, - overview: - '완료 또는 진행 중인 유출유 확산예측 분석 이력을 목록 형식으로 조회하는 화면이다. 사고명·날짜·유종·유출량·모델 상태 등을 한눈에 파악할 수 있다.', - description: - "목록에는 번호·사고명·사고일시·예측시간·유종·유출량·모델별 상태(KOSPS·POSEIDON·OPENDRIFT)·담당자 등이 표시된다. 모델 상태는 '완료(녹색 배지)'와 '대기(회색 배지)'로 구분된다.", - procedure: [ - "상단 메뉴에서 '유출유확산예측'을 클릭하여 분석 목록 화면으로 이동한다.", - '검색창에 사고명을 입력하거나 페이지를 이동하여 원하는 분석을 찾는다.', - '사고명 링크를 클릭하여 해당 분석 결과 상세 화면으로 이동한다.', - "신규 분석이 필요한 경우 우측 상단 '+ 새 분석' 버튼을 클릭한다.", - ], - notes: [ - '분석 결과 삭제 시 복구가 불가하므로, 삭제 전 보고서 출력 또는 데이터 저장 여부를 확인한다.', - ], - }, - { - id: '008', - name: '분석 조회(test001)', - menuPath: '유출유확산예측 > 분석 목록', - imageIndex: 8, - overview: - '분석 목록에서 특정 분석 건을 선택하면 해당 확산분석 결과 화면으로 이동하여 상세 내용을 조회한다. 이전 분석 결과를 재검토하거나 조건 변경 후 재분석할 수 있다.', - procedure: [ - '분석 목록에서 조회할 사고명 링크를 클릭한다.', - '로드된 분석 정보를 확인한다.', - "필요 시 입력 조건을 수정하고 '확산예측 실행'을 다시 클릭하여 재분석한다.", - '결과를 보고서로 출력하거나 저장한다.', - ], - notes: [ - '기존 분석에서 모델 조건 변경 후 재실행 시 이전 결과가 덮어써질 수 있으므로, 원본 결과를 먼저 저장한다.', - ], - }, - { - id: '009', - name: '유출유확산모델 이론 - 시스템 개요', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 9, - overview: - 'Wing 시스템에 탑재된 유출유 확산 수치 모델의 개요와 운용 체계를 안내하는 이론 화면이다. KOSPS·POSEIDON·OpenDrift 3종 모델의 특징·비교·데이터 흐름을 확인할 수 있다.', - procedure: [ - "상단 메뉴에서 '유출유확산예측 > 유출유확산모델 이론'을 클릭한다.", - "'시스템 개요' 탭을 선택하여 전체 모델 체계를 확인한다.", - '각 탭(KOSPS·POSEIDON·OpenDrift 등)을 클릭하여 상세 이론을 열람한다.', - ], - }, - { - id: '010', - name: '유출유확산모델 이론 - KOSPS', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 10, - overview: - '한국해양과학기술원(KIOST)이 개발한 KOSPS 모델의 상세 이론을 안내한다. 국내 연안 특성에 최적화된 조류예측(CHARRY) 및 풍류 경험식 적용 원리를 설명한다.', - notes: [ - 'KOSPS는 국내 연안 중심으로 검증된 모델로, 원해·심해 적용 시 정확도 제한이 있을 수 있다.', - ], - }, - { - id: '011', - name: '유출유확산모델 이론 - POSEIDON', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 11, - overview: - '입자추적 최적화 예측 시스템 POSEIDON의 이론 및 MOHID 3D 해양순환모델 적용 원리를 안내한다. GA·DE·HS·PSO 등 4종 최적화 알고리즘을 통한 파라미터 자동 최적화 방법을 설명한다.', - }, - { - id: '012', - name: '유출유확산모델 이론 - OpenDrift', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 12, - overview: - '노르웨이 MET Norway가 개발한 오픈소스 라그랑지안 확산 프레임워크 OpenDrift의 이론을 안내한다. NEMO·ROMS·HYCOM·Copernicus CMEMS 등 다양한 해양 예보 모델과 연동 가능한 범용성을 설명한다.', - }, - { - id: '013', - name: '유출유확산모델 이론 - 입자추적법', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 13, - overview: - '유출유 수치 모델에 적용되는 라그랑지안 입자추적법(Lagrangian Particle Tracking Method)의 수학적 이론과 수식을 안내한다.', - }, - { - id: '014', - name: '유출유확산모델 이론 - 풍화 프로세스', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 14, - overview: - '유출유가 시간 경과에 따라 겪는 풍화(Weathering) 프로세스(증발·유화·자연분산 등)의 이론과 타임라인을 안내한다.', - notes: [ - '풍화 속도는 유종·기온·해수온·파랑 조건에 따라 크게 달라질 수 있으므로 참고용으로 활용한다.', - ], - }, - { - id: '015', - name: '유출유확산모델 이론 - 해양환경 입력', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 15, - overview: - '유출유 확산 수치 모델에 사용되는 해양환경 입력 데이터(기상·해류·조류·해수면 온도) 체계를 안내한다. KMA RDAPS·ECMWF·NIFS ROMS·HYCOM·TPXO9 등 데이터 소스 설명.', - }, - { - id: '016', - name: '유출유확산모델 이론 - 모델 검증', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 16, - overview: - '유출유 확산 모델의 정확도 검증 사례(2007 허베이스피리트, 2014 무이산 등)와 RMSE·Skill Score 통계 지표를 안내한다.', - }, - { - id: '017', - name: '유출유확산모델 이론 - 앙상블', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 17, - overview: - 'KOSPS·POSEIDON·OpenDrift 3종 모델의 결과를 통합하는 앙상블 예측 방법론과 가중평균 알고리즘을 안내한다. 최악 시나리오(Worst Case) 산출 방법도 설명한다.', - }, - { - id: '018', - name: '유출유확산모델 이론 - 발전 방향', - menuPath: '유출유확산예측 > 유출유확산모델 이론', - imageIndex: 18, - overview: '현재 유출유 확산 모델의 한계와 향후 개선 방향(4단계 로드맵)을 안내한다.', - }, - { - id: '019', - name: '오일펜스 배치 알고리즘 이론 - 개요', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 19, - overview: - '오일펜스 최적 배치 알고리즘의 전체 개요와 최적화 목표(차단 면적 최대화·도달시간 최소화·자원 효율화)를 안내한다.', - }, - { - id: '020', - name: '오일펜스 배치 알고리즘 이론 - 배치 이론', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 20, - overview: - '차단 효율 함수(E(theta, U))와 V형·U형·J형 배치 형태별 이론적 차단 원리를 안내한다. 다단계 차단선 배치 시 총 차단 효율 산출 공식도 포함한다.', - }, - { - id: '021', - name: '오일펜스 배치 알고리즘 이론 - 최적화 알고리즘', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 21, - overview: - 'NSGA-II(Non-dominated Sorting Genetic Algorithm II) 기반 다목적 최적화 알고리즘의 원리와 보조 알고리즘(PSO·Greedy) 비교를 안내한다.', - }, - { - id: '022', - name: '오일펜스 배치 알고리즘 이론 - 유체역학 모델', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 22, - overview: '오일펜스 차단 성능 평가에 사용되는 유체역학 모델의 이론을 안내한다.', - }, - { - id: '023', - name: '오일펜스 배치 알고리즘 이론 - 현장 적용', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 23, - overview: - '오일펜스 배치 알고리즘의 실제 사고 현장 적용 사례와 현장 운용 시 고려사항을 안내한다.', - }, - { - id: '024', - name: '오일펜스 배치 알고리즘 이론 - 참고문헌', - menuPath: '유출유확산예측 > 오일펜스 배치 알고리즘 이론', - imageIndex: 24, - overview: - '오일펜스 배치 알고리즘 이론에 사용된 학술 논문·기술 보고서·국제 기준 등 참고문헌 목록을 안내한다.', - }, - ], - }, - { - id: 'ch02', - number: '02', - title: 'HNS·대기확산', - subtitle: 'HNS Atmospheric Dispersion', - screens: [ - { - id: '025', - name: '대기확산예측 실행', - menuPath: 'HNS대기확산 > 대기확산분석', - imageIndex: 25, - overview: - 'HNS(위험유해물질) 해상 유출 시 대기 확산 범위, 위험 농도 구역, 영향 인구를 예측하는 핵심 분석 화면이다. ALOHA(EPA) 또는 이문진박사모델 두 가지 알고리즘 중 선택하여 가우시안 Plume/Puff 모델을 적용한다.', - description: - "화면 좌측에 사고 기본정보·물질 및 유출 조건·기상조건 입력 패널이 위치한다. 중앙 지도에 AEGL-1~3 위험 구역이 색상별 Plume 형태로 표출된다. 우측 '예측 결과' 패널에 최대 농도(ppm), AEGL-1 영향 면적, 위험 등급 등이 표시된다.", - procedure: [ - "상단 메뉴에서 'HNS대기확산 > 대기확산분석'을 클릭한다.", - '사고 기본정보(사고명·날짜·시간·위치)를 입력한다.', - 'HNS 물질 종류 및 누출 방식·유출량을 선택·입력한다.', - '기상조건(풍향·풍속·기온·대기안정도·예측 시간)을 입력한다.', - '적용 알고리즘(ALOHA/이문진박사모델)을 선택한다.', - "'확산예측 실행' 버튼을 클릭한다.", - '지도의 위험 구역 Plume과 우측 예측 결과 패널을 확인한다.', - ], - inputs: [ - { label: '사고명', type: '텍스트', required: true, desc: '사고 식별 명칭' }, - { label: '사고 일시', type: '날짜+시간', required: true, desc: '사고 발생 일시' }, - { label: '위도/경도', type: '숫자', required: true, desc: '사고 발생 지점 좌표' }, - { - label: 'HNS 물질', - type: '드롭다운', - required: true, - desc: 'AEGL/ERPG 기준값 자동 로드', - }, - { label: '누출 방식', type: '라디오', required: true, desc: '순간 유출 또는 지속 유출' }, - { label: '유출량', type: '숫자', required: true, desc: 'ton 또는 g/s 단위' }, - { label: '풍향', type: '숫자', required: true, desc: '0~360도' }, - { label: '풍속', type: '숫자 m/s', required: true, desc: '최소 0.5 m/s 이상' }, - { label: '기온', type: '숫자', required: true, desc: '현재 기온' }, - { label: '대기안정도', type: '선택 A~F', required: true, desc: 'Pasquill-Gifford 분류' }, - { label: '예측 시간', type: '숫자 h', required: true, desc: '확산 예측 기간' }, - { - label: '적용 알고리즘', - type: '라디오', - required: true, - desc: 'ALOHA 또는 이문진박사모델', - }, - ], - notes: [ - '풍속은 최소 0.5 m/s 이상 입력해야 하며 0 입력 시 오류 발생.', - 'HNS 물질 선택 후 AEGL/ERPGs 기준값이 자동 로드되었는지 반드시 확인.', - ], - }, - { - id: '026', - name: 'HNS 분석 목록', - menuPath: 'HNS대기확산', - imageIndex: 26, - overview: - '완료된 HNS 대기확산 분석 이력을 목록 형식으로 조회하는 화면이다. AEGL-1~3 거리·위험 등급·피해반경·담당자 등 주요 결과 정보를 한눈에 파악할 수 있다.', - procedure: [ - "상단 메뉴에서 'HNS대기확산'을 클릭하여 분석 목록으로 이동한다.", - '검색창에서 분석명을 검색하거나 목록을 스크롤하여 원하는 분석을 찾는다.', - '분석명 링크를 클릭하여 해당 분석 결과 지도 화면으로 이동한다.', - ], - }, - { - id: '027', - name: '시나리오 상세', - menuPath: 'HNS대기확산 > 시나리오 관리', - imageIndex: 27, - overview: - '단일 HNS 사고에 대해 여러 기상·유출 조건 시나리오(S-01~S-05 등)를 생성·비교 관리하는 화면이다. 시나리오별 위험도·위험 구역·대응 권고사항을 한눈에 비교할 수 있다.', - description: - '시나리오 목록 카드에 시나리오명·위험도 배지(CRITICAL/HIGH/MEDIUM/RESOLVED)·최대농도·IDLH 거리·ERPG-2 거리·영향인구 요약이 표시된다.', - procedure: [ - "'HNS대기확산 > 시나리오 관리'를 클릭하여 이동한다.", - '원하는 사고 건을 선택하면 시나리오 목록이 표시된다.', - '시나리오 카드를 클릭하여 상세 위험 구역과 대응 권고사항을 확인한다.', - "'비교 차트' 탭으로 이동하여 시나리오 간 시간별 변화를 비교한다.", - ], - }, - { - id: '028', - name: '비교 차트', - menuPath: 'HNS대기확산 > 시나리오 관리', - imageIndex: 28, - overview: - '생성된 여러 HNS 시나리오의 최대 지표면 농도·위험 반경·영향 인구를 시간별 변화 차트로 비교하는 화면이다.', - description: - '상단은 시나리오별 최대 지표면 농도(ppm) 시간별 변화 그래프. 중단에 위험 반경(km)과 영향 인구(명) 변화 차트. 하단에 피대농도·IDLH 거리·ERPG-2 반경·영향인구·풍향/풍속·위험등급 항목별 비교표가 제공된다.', - }, - { - id: '029', - name: '확산범위 오버레이', - menuPath: 'HNS대기확산 > 시나리오 관리', - imageIndex: 29, - overview: - 'HNS 대기확산 시나리오의 시간대별(T+0h~T+4h) 확산 범위를 지도에 오버레이 표출하는 화면이다.', - procedure: [ - "시나리오 관리 화면에서 '확산범위 오버레이' 탭을 클릭한다.", - '지도에서 시간대별 확산 범위를 확인한다.', - '슬라이더를 이동하여 원하는 시간대의 확산 상태를 조회한다.', - ], - }, - { - id: '030', - name: '신규 시나리오', - menuPath: 'HNS대기확산 > 시나리오 관리', - imageIndex: 30, - overview: - '기상·유출 조건을 변경하여 새로운 HNS 대기확산 시나리오를 생성하는 입력 모달 화면이다.', - procedure: [ - "시나리오 관리 화면 우측 상단 '신규 시나리오' 버튼을 클릭한다.", - '시나리오명과 기준 시각을 입력한다.', - 'HNS 물질·누출 구분·유출량을 입력한다.', - '기상조건(풍향·풍속·기온·대기안정도)을 입력한다.', - "'시나리오 생성 및 예측 실행'을 클릭한다.", - ], - inputs: [ - { label: '시나리오명', type: '텍스트', required: true, desc: '시나리오 식별 명칭' }, - { label: 'HNS 물질', type: '드롭다운', required: true, desc: '유출 물질 선택' }, - { label: '누출 구분', type: '라디오', required: true, desc: '순간/지속' }, - { label: '유출량', type: '숫자', required: true, desc: 'ton 또는 g/s' }, - { label: '풍향/풍속', type: '숫자', required: true, desc: '각도와 m/s' }, - { label: '기온', type: '숫자', required: true, desc: '섭씨' }, - { label: '대기안정도', type: '선택 A~F', required: true, desc: 'Pasquill-Gifford' }, - ], - }, - { - id: '031', - name: 'HNS 대응매뉴얼', - menuPath: 'HNS대기확산', - imageIndex: 31, - overview: - '해양 HNS 사고 대응 절차를 정리한 Marine HNS Response Manual(Bonn Agreement·HELCOM·REMPEC 기반)을 시스템 내에서 직접 열람하는 화면이다. 8개 챕터. SEBC 거동 분류 5유형 카드.', - procedure: [ - "상단 메뉴에서 'HNS대기확산 > HNS 대응매뉴얼'을 클릭한다.", - '원하는 챕터 카드를 클릭하여 세부 내용을 확인한다.', - '하단 SEBC 분류 카드를 클릭하여 거동 유형별 대응 방법을 확인한다.', - ], - }, - { - id: '032', - name: 'HNS 확산모델 이론 - 시스템 개요', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 32, - overview: - 'Wing에 탑재된 HNS 대기확산 모델(WRF-Chem·Gaussian Plume/Puff·ROMS 연동)의 전체 체계와 처리 흐름을 안내한다.', - }, - { - id: '033', - name: 'HNS 확산모델 이론 - 가우시안 모델', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 33, - overview: - 'HNS 대기확산 예측에 적용되는 Gaussian Plume·Puff 모델의 수학적 이론과 Pasquill-Gifford 대기안정도 분류 체계를 안내한다.', - }, - { - id: '034', - name: 'HNS 확산모델 이론 - 물질별 시나리오', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 34, - overview: - '주요 HNS 물질(NH3·CH3OH·H2·LNG 등)의 물질 특성 및 대기확산 시나리오 적용 기준을 안내한다.', - }, - { - id: '035', - name: 'HNS 확산모델 이론 - 해양환경 보정', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 35, - overview: - '해양 환경 특성(해풍·육풍 순환·해수면 거칠기·SST 영향·MABL 구조)에 따른 대기확산 모델 보정 인자를 안내한다.', - }, - { - id: '036', - name: 'HNS 확산모델 이론 - 모델 검증', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 36, - overview: - 'HNS 대기확산 모델의 실측값 대비 예측 정확도 검증 결과(ALOHA·이문진박사모델·실측값 3종 비교)를 안내한다.', - }, - { - id: '037', - name: 'HNS 확산모델 이론 - 실시간 비교', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 37, - overview: - '동일 조건에서 ALOHA와 이문진박사모델의 예측 결과를 실시간으로 비교하는 시뮬레이션 화면이다.', - }, - { - id: '038', - name: 'HNS 확산모델 이론 - WRF-Chem 발전', - menuPath: 'HNS대기확산 > 확산모델 이론', - imageIndex: 38, - overview: - 'WRF-Chem 기상-화학 연계 모델 및 ROMS 해양확산 수치모의 검증 결과와 고도화 방향을 안내한다.', - }, - { - id: '039', - name: 'HNS물질정보 - SEBC 거동분류', - menuPath: 'HNS대기확산 > HNS물질정보', - imageIndex: 39, - overview: - 'HNS 물질의 해양 거동 특성을 SEBC 기준으로 분류한 정보를 제공한다. G(기체)·E(증발)·F(부유)·D(용해)·S(침강) 5가지 기본 유형과 복합 거동 유형을 안내한다.', - }, - { - id: '040', - name: 'HNS물질정보 - 주요 물질 특성', - menuPath: 'HNS대기확산 > HNS물질정보', - imageIndex: 40, - overview: - 'NH3·CH4·H2·페놀·톨루엔 등 주요 HNS 물질의 상세 물리화학적 특성을 카드 형식으로 제공하는 화면이다.', - }, - { - id: '041', - name: 'HNS물질정보 - 위험도 기준', - menuPath: 'HNS대기확산 > HNS물질정보', - imageIndex: 41, - overview: - 'HNS 물질의 위험도 평가에 사용되는 AEGL·ERPG·IDLH·TWA 등 주요 독성 기준값 체계를 안내한다.', - }, - { - id: '042', - name: 'HNS물질정보 - 물질 상세검색', - menuPath: 'HNS대기확산 > HNS물질정보', - imageIndex: 42, - overview: - 'HNS 6,500여 종의 물질 데이터베이스에서 CAS 번호·물질명·거동 분류 조건으로 물질을 검색하는 화면이다.', - inputs: [ - { label: '검색어', type: '텍스트', required: true, desc: 'CAS 번호 또는 물질명' }, - { label: '거동 분류', type: '체크박스', required: false, desc: 'SEBC 유형 필터' }, - ], - notes: ['CAS 번호 입력 시 하이픈(-) 포함 정확한 형식으로 입력해야 검색된다.'], - }, - ], - }, - { - id: 'ch03', - number: '03', - title: '긴급구난', - subtitle: 'Emergency Rescue', - screens: [ - { - id: '043', - name: '긴급구난예측', - menuPath: '긴급구난', - imageIndex: 43, - overview: - '해상 조난자·표류 선박·표류 물체의 위치를 예측하는 긴급구난(SAR) 분석 화면이다. 해류·바람·파랑 데이터 기반 라그랑지안 표류 궤적 시뮬레이션을 수행한다.', - description: - '화면 좌측에 사고 발생 위치·일시·표류체 종류·예측 시간·기상조건 입력 패널이 위치한다. 중앙 지도에 표류 예측 궤적과 탐색 구역(최우선·일반)이 표출된다.', - procedure: [ - "상단 메뉴에서 '긴급구난'을 클릭한다.", - '사고 발생 위치(위경도)와 일시를 입력한다.', - '표류체 종류(선박/사람/컨테이너 등)를 선택한다.', - '예측 시간과 기상 조건을 입력한다.', - "'예측 실행' 버튼을 클릭하여 표류 궤적과 탐색 구역을 확인한다.", - ], - inputs: [ - { label: '사고 위치', type: '숫자', required: true, desc: '위·경도' }, - { label: '사고 일시', type: '날짜+시간', required: true, desc: '발생 일시' }, - { - label: '표류체 종류', - type: '드롭다운', - required: true, - desc: '선박·사람·컨테이너·구명정 등', - }, - { label: '예측 시간', type: '숫자 h', required: true, desc: '표류 예측 기간' }, - { label: '기상조건', type: '숫자', required: false, desc: '미입력 시 실시간 자동 적용' }, - ], - notes: [ - '표류체 종류 선택이 부적절한 경우 예측 정확도가 낮아질 수 있다.', - '신속한 대응을 위해 입력 완료 즉시 실행하고 기상 변화 시 재분석을 수행한다.', - ], - }, - { - id: '044', - name: '긴급구난 목록', - menuPath: '긴급구난', - imageIndex: 44, - overview: '완료 또는 진행 중인 긴급구난 예측 분석 이력을 목록 형식으로 조회하는 화면이다.', - procedure: [ - "상단 메뉴에서 '긴급구난'을 클릭하면 목록 화면이 표시된다.", - '원하는 분석을 검색하거나 목록에서 선택한다.', - '사고명 링크를 클릭하여 상세 분석 결과로 이동한다.', - ], - }, - { - id: '045', - name: '시나리오 상세', - menuPath: '긴급구난 > 시나리오 관리', - imageIndex: 45, - overview: - '긴급구난 사고에 대해 기상·해양 조건을 달리 설정한 복수 시나리오를 생성·비교 관리하는 화면이다.', - procedure: [ - "'긴급구난 > 시나리오 관리'를 클릭하여 이동한다.", - "생성된 시나리오 카드를 확인하거나 '신규 시나리오' 버튼으로 추가 생성한다.", - '시나리오 카드를 클릭하여 상세 내용을 확인한다.', - ], - }, - { - id: '046', - name: '비교 차트', - menuPath: '긴급구난 > 시나리오 관리', - imageIndex: 46, - overview: - '긴급구난 시나리오별 탐색 면적·표류 속도·최대 이동거리를 시간별 변화 차트로 비교하는 화면이다.', - }, - { - id: '047', - name: '지도 오버레이', - menuPath: '긴급구난 > 시나리오 관리', - imageIndex: 47, - overview: - '긴급구난 시나리오의 시간대별 표류 예측 궤적 및 탐색 구역을 지도에 오버레이 표출하는 화면이다.', - }, - { - id: '048', - name: '긴급구난모델 이론', - menuPath: '긴급구난', - imageIndex: 48, - overview: - '긴급구난 표류 예측에 적용되는 라그랑지안 표류 모델·탐색 구역(Search Area) 산출 방법·IMO SAR 기준 적용 이론을 안내한다.', - }, - ], - }, - { - id: 'ch04', - number: '04', - title: '보고자료', - subtitle: 'Reports', - screens: [ - { - id: '049', - name: '보고서 목록', - menuPath: '보고자료', - imageIndex: 49, - overview: - '시스템에서 생성된 모든 사고 대응 보고서를 목록 형식으로 관리하는 화면이다. 보고서명·사고명·생성일시·유형·작성자·상태가 표시된다.', - procedure: [ - "상단 메뉴에서 '보고자료'를 클릭하여 보고서 목록으로 이동한다.", - '검색창에서 원하는 보고서를 검색하거나 목록에서 선택한다.', - '보고서명을 클릭하여 미리보기하거나 PDF를 다운로드한다.', - ], - }, - { - id: '050', - name: '초기보고서', - menuPath: '보고자료 > 표준보고서 템플릿', - imageIndex: 50, - overview: - '사고 발생 초기 지휘부 보고를 위한 표준 보고서 템플릿을 제공한다. 확산예측 결과와 사고 기본정보가 자동 연동된다.', - procedure: [ - "보고자료 메뉴에서 '표준보고서 템플릿'을 클릭한다.", - "좌측 사이드바에서 '초기보고서'를 선택한다.", - '자동 입력된 항목을 검토하고 필요 내용을 추가 입력한다.', - "'저장' 또는 'PDF 출력' 버튼을 활용한다.", - ], - }, - { - id: '051', - name: '지휘부 보고', - menuPath: '보고자료 > 표준보고서 템플릿', - imageIndex: 51, - overview: - '사고 지휘부 보고용 표준 보고서 템플릿. 방제 대응 현황·자원 투입 현황·향후 계획이 포함된다.', - }, - { - id: '052', - name: '예측보고서', - menuPath: '보고자료 > 표준보고서 템플릿', - imageIndex: 52, - overview: - '확산예측 결과를 포함한 예측보고서 템플릿. 모델 종류·예측 시간·주요 결과가 자동 연동된다.', - }, - { - id: '053', - name: '종합보고서', - menuPath: '보고자료 > 표준보고서 템플릿', - imageIndex: 53, - overview: - '사고 종료 후 방제 대응 전 과정을 정리하는 종합보고서. 사고개요·대응경과·방제실적·환경영향평가·교훈이 포함된다.', - }, - { - id: '054', - name: '유출유 보고', - menuPath: '보고자료 > 표준보고서 템플릿', - imageIndex: 54, - overview: - '유출유 사고 전용 표준 보고서 템플릿. 확산예측·오일펜스 배치·풍화 상태가 자동 연동된다.', - }, - { - id: '055', - name: '유출유 확산예측 보고서 생성', - menuPath: '보고자료 > 보고서 생성', - imageIndex: 55, - overview: '유출유 확산예측 분석 결과를 기반으로 보고서를 자동 생성하는 화면이다.', - procedure: [ - "보고자료 메뉴에서 '보고서 생성'을 클릭한다.", - "'유출유 확산예측' 유형을 선택한다.", - '연동할 분석 결과를 드롭다운에서 선택한다.', - '보고 대상을 선택한다.', - "'보고서 생성' 버튼을 클릭한다.", - ], - inputs: [ - { label: '분석 결과', type: '드롭다운', required: true, desc: '연동할 분석 건 선택' }, - { label: '보고 대상', type: '체크박스', required: true, desc: '지휘부·현장·외부 기관' }, - ], - }, - { - id: '056', - name: 'HNS 대기확산 보고서 생성', - menuPath: '보고자료 > 보고서 생성', - imageIndex: 56, - overview: 'HNS 대기확산 분석 결과를 기반으로 보고서를 자동 생성하는 화면이다.', - }, - { - id: '057', - name: '긴급구난 보고서 생성', - menuPath: '보고자료 > 보고서 생성', - imageIndex: 57, - overview: - '긴급구난 예측 분석 결과를 기반으로 구조 현장 지휘부용 보고서를 자동 생성하는 화면이다.', - }, - ], - }, - { - id: 'ch05', - number: '05', - title: '항공탐색', - subtitle: 'Aerial Surveillance', - screens: [ - { - id: '058', - name: '영상사진관리', - menuPath: '항공탐색', - imageIndex: 58, - overview: - '드론·항공기·위성에서 수집된 영상 및 사진을 통합 관리하는 화면이다. 촬영 일시·위치·기기 유형별 분류 조회와 유출유 면적분석 연계를 지원한다.', - procedure: [ - "상단 메뉴에서 '항공탐색 > 영상사진관리'를 클릭한다.", - '목록에서 원하는 영상/사진을 선택하거나 지도 마커를 클릭하여 확인한다.', - "신규 파일 업로드 시 '파일 업로드' 버튼을 클릭한다.", - ], - inputs: [ - { label: '업로드 파일', type: '파일', required: false, desc: 'JPG·PNG·GeoTIFF·KMZ' }, - { label: '촬영 일시', type: '날짜+시간', required: false, desc: '촬영 시점' }, - { label: '장비 유형', type: '드롭다운', required: false, desc: '드론·항공기·위성·CCTV' }, - ], - notes: ['50MB 초과 파일은 업로드 전 압축하거나 관리자에게 문의한다.'], - }, - { - id: '059', - name: '유출유면적분석', - menuPath: '항공탐색', - imageIndex: 59, - overview: - '항공·위성 이미지를 기반으로 유출유 오염 면적을 AI 자동 산출하는 화면이다. 자동 추출된 오염 경계선을 수동 편집하여 정밀 면적을 산정할 수 있다.', - procedure: [ - "영상사진관리에서 분석 대상 이미지를 선택하고 '면적 분석' 버튼을 클릭한다.", - "'AI 자동 분석 실행' 버튼을 클릭하여 오염 경계선을 추출한다.", - '폴리곤 경계선을 검토하고 필요 시 편집 도구로 수동 조정한다.', - '최종 면적·둘레·중심 좌표를 확인하고 저장한다.', - ], - }, - { - id: '060', - name: '실시간드론', - menuPath: '항공탐색', - imageIndex: 60, - overview: - '현장 드론에서 실시간 전송되는 영상 스트리밍을 시스템 내에서 직접 모니터링하는 화면이다.', - procedure: [ - "상단 메뉴에서 '항공탐색 > 실시간 드론'을 클릭한다.", - '연결된 드론 목록에서 모니터링할 드론을 선택한다.', - '실시간 영상을 확인하고 필요 시 스냅샷을 저장한다.', - ], - notes: ['드론 스트리밍은 네트워크 연결 상태에 따라 화질 및 지연이 달라질 수 있다.'], - }, - { - id: '061', - name: '오염선박 3D분석', - menuPath: '항공탐색', - imageIndex: 61, - overview: - '항공·위성 이미지를 기반으로 오염 선박의 3D 모델을 생성하고 오염 분포를 입체적으로 분석하는 화면이다.', - }, - { - id: '062', - name: '위성요청', - menuPath: '항공탐색', - imageIndex: 62, - overview: 'SAR·광학 위성 촬영 요청 및 수신 결과를 관리하는 화면이다.', - inputs: [ - { label: '요청 위치', type: '숫자', required: true, desc: '위·경도' }, - { label: '촬영 희망 일시', type: '날짜+시간', required: true, desc: '촬영 필요 일시' }, - { label: '위성 종류', type: '라디오', required: true, desc: 'SAR 또는 광학' }, - { label: '해상도', type: '드롭다운', required: false, desc: '요청 이미지 해상도' }, - ], - notes: [ - '위성 촬영 가능 여부는 궤도 조건에 따라 결정되며 요청이 항상 수용되지 않을 수 있다.', - ], - }, - { - id: '063', - name: 'CCTV 조회', - menuPath: '항공탐색', - imageIndex: 63, - overview: - '해안·항만 CCTV의 실시간 영상을 조회하는 화면이다. 사고 인근 CCTV를 지도에서 선택하여 스트리밍으로 확인할 수 있다.', - procedure: [ - "상단 메뉴에서 '항공탐색 > CCTV조회'를 클릭한다.", - '지도에서 확인할 CCTV 마커를 클릭한다.', - '실시간 영상을 확인하고 필요 시 스냅샷을 저장한다.', - ], - }, - { - id: '064', - name: '항공탐색 이론 - 개요', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 64, - overview: - 'Wing 항공탐색 기능에 적용되는 원격탐사·ESI 방제지도·면적 산정·확산예측 연계 이론의 전체 개요를 안내한다.', - }, - { - id: '065', - name: '항공탐색 이론 - 탐지 장비', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 65, - overview: - '해양 오염 항공 탐지에 사용되는 주요 장비(SAR·적외선 카메라·UV 센서·광학 카메라)의 특성과 적용 원리를 안내한다.', - }, - { - id: '066', - name: '항공탐색 이론 - 원격탐사', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 66, - overview: - '위성 및 항공 원격탐사(Remote Sensing)의 기본 원리와 해양 오염 탐지 적용 방법을 안내한다.', - }, - { - id: '067', - name: '항공탐색 이론 - ESI 방제지도', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 67, - overview: - '환경민감지수(ESI) 방제지도의 해안선 민감도 등급 분류 체계와 방제 우선순위 결정 방법을 안내한다.', - }, - { - id: '068', - name: '항공탐색 이론 - 면적 산정', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 68, - overview: - '항공·위성 이미지 기반 유출유 면적 산정에 적용되는 AI 알고리즘(객체 탐지·영상 분할)의 원리를 안내한다.', - }, - { - id: '069', - name: '항공탐색 이론 - 확산예측 연계', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 69, - overview: - '항공 탐색 결과(실측 면적·위치)와 유출유 확산예측 모델 보정 연계 방법을 안내한다.', - }, - { - id: '070', - name: '항공탐색 이론 - 논문 특허', - menuPath: '항공탐색 > 항공탐색 이론', - imageIndex: 70, - overview: 'Wing 항공탐색 기능의 기반이 되는 관련 논문과 특허 목록을 안내한다.', - }, - ], - }, - { - id: 'ch06', - number: '06', - title: '게시판', - subtitle: 'Bulletin Board', - screens: [ - { - id: '071', - name: '전체 게시판', - menuPath: '게시판', - imageIndex: 71, - overview: '공지사항·자료실·Q&A·해경매뉴얼 게시물을 통합 조회하는 전체 게시판 화면이다.', - procedure: [ - "상단 메뉴에서 '게시판'을 클릭하여 전체 게시판으로 이동한다.", - '유형 필터 탭을 선택하거나 검색창에 키워드를 입력한다.', - '원하는 게시물 제목을 클릭하여 상세 내용을 확인한다.', - ], - }, - { - id: '072', - name: '공지사항', - menuPath: '게시판', - imageIndex: 72, - overview: - '시스템 공지·업데이트·장애 안내 등 운영 공지사항을 게시하는 화면이다. 관리자만 등록·수정 가능.', - }, - { - id: '073', - name: '자료실', - menuPath: '게시판', - imageIndex: 73, - overview: '매뉴얼·지침서·참고자료 등 업무 관련 문서 파일을 공유하는 게시판 화면이다.', - }, - { - id: '074', - name: 'QNA', - menuPath: '게시판', - imageIndex: 74, - overview: '시스템 사용 관련 질문을 등록하고 답변을 확인하는 Q&A 게시판 화면이다.', - inputs: [ - { label: '제목', type: '텍스트', required: true, desc: '질문 제목' }, - { - label: '카테고리', - type: '드롭다운', - required: true, - desc: '기능문의·오류신고·개선요청', - }, - { label: '내용', type: '텍스트', required: true, desc: '질문 내용' }, - { label: '첨부 파일', type: '파일', required: false, desc: '스크린샷 등' }, - ], - }, - { - id: '075', - name: '해경 매뉴얼', - menuPath: '게시판', - imageIndex: 75, - overview: - '해양경찰청 방제·대응 매뉴얼을 시스템 내에서 바로 조회할 수 있는 화면이다. 관리자만 등록·수정 가능.', - }, - ], - }, - { - id: 'ch07', - number: '07', - title: '기상정보', - subtitle: 'Weather', - screens: [ - { - id: '076', - name: '기상정보', - menuPath: '기상정보', - imageIndex: 76, - overview: - '현재 사고 지역 주변의 실시간 기상 정보(풍향·풍속·기온·파고·시정 등)를 조회하는 화면이다. 기상 데이터는 확산예측 모델 입력으로 자동 연동된다.', - description: - '사고 위치 기준 반경 내 기상 관측소 목록과 최신 관측값이 표시된다. 시계열 그래프로 과거 24시간 기상 변화를 확인할 수 있다.', - procedure: [ - "상단 메뉴에서 '기상정보'를 클릭하여 이동한다.", - '지도에서 관측소 마커를 클릭하거나 목록에서 관측소를 선택한다.', - '최신 기상값과 시계열 그래프를 확인한다.', - ], - notes: [ - '극한 기상 조건에서는 확산예측 정확도가 낮아질 수 있으므로 현장 기상 관측값과 병행 확인한다.', - ], - }, - ], - }, - { - id: 'ch08', - number: '08', - title: '통합조회', - subtitle: 'Integrated Search', - screens: [ - { - id: '077', - name: '통합조회', - menuPath: '통합조회', - imageIndex: 77, - overview: - 'Wing 시스템 전체 분석 이력(유출유·HNS·긴급구난·보고서)을 통합 조회하는 화면이다. 사고명·날짜·분석 유형·담당자 등 복합 조건으로 검색하고 결과를 지도와 목록으로 표출한다.', - description: - '화면 좌측에 날짜 범위·분석 유형·담당자·사고 지역 복합 필터 패널이 위치한다. 중앙 지도에 검색 결과 사고지점 마커가 표출된다. Excel·CSV 내보내기를 지원한다.', - procedure: [ - "상단 메뉴에서 '통합조회'를 클릭하여 이동한다.", - '날짜 범위·분석 유형 등 필터 조건을 설정한다.', - "'검색' 버튼을 클릭하여 결과를 조회한다.", - '지도 마커 또는 목록 항목을 클릭하여 해당 분석 결과 상세 화면으로 이동한다.', - 'Excel·CSV 내보내기 버튼으로 이력 자료를 저장한다.', - ], - inputs: [ - { label: '날짜 범위', type: '날짜', required: false, desc: '시작·종료 날짜' }, - { - label: '분석 유형', - type: '체크박스', - required: false, - desc: '유출유·HNS·긴급구난·보고서', - }, - { label: '담당자', type: '텍스트', required: false, desc: '이름 필터' }, - { label: '사고 지역', type: '텍스트', required: false, desc: '지역명 필터' }, - ], - }, - ], - }, -]; - const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => { const [selectedChapterId, setSelectedChapterId] = useState('ch01'); const [expandedScreenIds, setExpandedScreenIds] = useState>(new Set()); @@ -1267,6 +209,25 @@ const UserManualPopup = ({ isOpen, onClose }: UserManualPopupProps) => { > {allExpanded ? '전체 닫기' : '전체 열기'} + { + e.currentTarget.style.background = 'rgba(6,182,212,0.16)'; + }} + onMouseLeave={(e) => { + e.currentTarget.style.background = 'rgba(6,182,212,0.08)'; + }} + > + PDF 다운로드 +
diff --git a/frontend/src/common/data/chapters.json b/frontend/src/common/data/chapters.json new file mode 100644 index 0000000..eec27cb --- /dev/null +++ b/frontend/src/common/data/chapters.json @@ -0,0 +1,1187 @@ +[ + { + "id": "ch01", + "number": "01", + "title": "유출유 확산예측", + "subtitle": "Oil Spill Dispersion Prediction", + "screens": [ + { + "id": "001", + "name": "직접입력", + "menuPath": "유출유확산예측 > 유출유확산분석", + "imageIndex": 1, + "overview": "해양 유출유 사고 발생 시 오염원 위치와 유출 조건을 직접 입력하여 확산 범위를 예측하는 주요 분석 화면이다. KOSPS·POSEIDON·OpenDrift·앙상블의 4종 수치 모델을 선택적으로 적용할 수 있다. 실시간 기상·해양 데이터와 연계하여 즉시 예측 결과를 지도에 표출한다.", + "description": "화면 좌측에 예측정보 입력 패널(사고명, 위치, 유종, 유출량, 예측 시간)이 위치한다. 중앙 지도에서 클릭하여 사고 발생 위치를 직접 지정하거나 위·경도 좌표를 수동 입력할 수 있다. 하단 타임라인 슬라이더로 시간 경과에 따른 확산 경과를 재생할 수 있다. 우측 '분석 요약' 패널에서 예측 면적, 이동거리, 이동 방향, 풍화 비율 등을 확인할 수 있다.", + "procedure": [ + "상단 메뉴에서 '유출유확산예측 > 유출유확산분석'을 클릭하여 화면을 이동한다.", + "좌측 패널에서 사고명, 날짜, 유종, 유출량, 예측 시간을 입력한다.", + "지도를 클릭하거나 좌표 입력란에 위·경도를 직접 입력하여 사고 위치를 지정한다.", + "적용할 확산 모델(KOSPS·POSEIDON·OpenDrift·앙상블) 체크박스를 선택한다.", + "'확산예측 실행' 버튼을 클릭하여 예측을 시작한다.", + "하단 타임라인 재생 버튼으로 시간별 확산 결과를 확인한다." + ], + "inputs": [ + { + "label": "사고명", + "type": "텍스트", + "required": true, + "desc": "사고 식별 명칭" + }, + { + "label": "날짜/시간", + "type": "날짜+시간", + "required": true, + "desc": "사고 발생 일시" + }, + { + "label": "위도/경도", + "type": "숫자", + "required": true, + "desc": "지도 클릭 자동 입력 가능" + }, + { + "label": "유종", + "type": "드롭다운", + "required": true, + "desc": "벙커C유·경유·연료유 등" + }, + { + "label": "유출량", + "type": "숫자 kL", + "required": true, + "desc": "유출 유류 총량" + }, + { + "label": "예측 시간", + "type": "숫자 h", + "required": true, + "desc": "확산 예측 기간" + }, + { + "label": "적용 모델", + "type": "체크박스", + "required": true, + "desc": "KOSPS·POSEIDON·OpenDrift·앙상블 중 선택" + } + ], + "notes": [ + "좌표 입력 후 반드시 '적용' 버튼을 클릭해야 지도에 반영된다.", + "앙상블 모델 선택 시 3개 모델이 동시 실행되어 계산 시간이 증가할 수 있다.", + "유출량이 0 이하이거나 예측 시간이 입력되지 않으면 실행 버튼이 비활성화된다." + ] + }, + { + "id": "002", + "name": "모델종류별 확산예측 실행", + "menuPath": "유출유확산예측 > 유출유확산분석", + "imageIndex": 2, + "overview": "KOSPS·POSEIDON·OpenDrift 3개 모델의 예측 결과를 동시에 지도에 표출하여 모델 간 비교 분석을 지원한다. 모델별 입자 궤적(파란점·빨간점·하늘점)이 중첩 표시되어 확산 경향의 편차를 한눈에 파악할 수 있다.", + "description": "지도에 모델별 색상 구분 입자가 동시에 표시되며, 각 모델의 확산 방향과 범위 차이를 시각적으로 비교할 수 있다. 우측 '분석 요약' 패널에 예측 면적(km2), 이동거리(km), 방향, 이동속도(cm/s)가 표출된다. 풍화 상태 막대그래프(유출량·해면연소·자연분산·수중분산·잔류 비율)를 제공한다. '다각형 분석수행' 버튼으로 특정 구역 내 오염 면적을 별도 산정할 수 있다.", + "procedure": [ + "확산분석 화면에서 적용 모델 체크박스를 2개 이상 선택한다.", + "'확산예측 실행' 버튼을 클릭한다.", + "지도에서 각 모델의 입자 분포와 확산 범위를 비교한다.", + "우측 '분석 요약' 탭에서 모델별 정량 지표를 확인한다.", + "하단 타임라인으로 시간 경과별 확산 변화를 재생한다." + ], + "notes": [ + "여러 모델을 동시에 선택하면 계산 자원 소모가 증가하여 결과 표출까지 수 분이 소요될 수 있다.", + "모델별 예측 결과에 차이가 있을 경우, 앙상블 결과 또는 현장 데이터와 교차 검토하여 활용한다." + ] + }, + { + "id": "003", + "name": "사고정보 조회", + "menuPath": "유출유확산예측 > 유출유확산분석", + "imageIndex": 3, + "overview": "시스템에 등록된 실제 사고 정보를 불러와 확산분석에 자동 연동하는 기능이다. 사고코드·선박명·유종·유출량·발생 좌표 등이 자동으로 예측정보 입력란에 채워진다.", + "description": "좌측 '사고정보' 패널에 진행 중인 사고 목록이 표시된다. 사고를 선택하면 지도 중심이 해당 사고 발생 위치로 자동 이동하고 위치 핀이 표시된다. 사고 상태는 '진행중(빨간 배지)'과 '종료(회색 배지)'로 구분된다.", + "procedure": [ + "좌측 '사고정보' 패널의 펼침 버튼을 클릭하여 패널을 연다.", + "목록에서 분석할 사고를 클릭한다.", + "사고 정보가 예측정보 입력란에 자동으로 입력되었는지 확인한다.", + "필요 시 유출량·예측 시간 등 추가 정보를 수정한다.", + "'확산예측 실행'을 클릭하여 분석을 시작한다." + ], + "notes": [ + "'진행중' 상태의 사고만 실시간 확산예측과 연동된다.", + "사고 데이터는 관계 기관으로부터 등록된 정보를 기반으로 하며, 입력 오류 시 관리자에게 문의한다." + ] + }, + { + "id": "004", + "name": "영향 민감자원 레이어 중첩 표시", + "menuPath": "유출유확산예측 > 유출유확산분석", + "imageIndex": 4, + "overview": "유출유 확산 예측 결과와 해양 민감자원(환경생태·수산자원·관광지 등) 레이어를 지도에 동시 표출하는 기능이다. 잠재적 피해 자원을 사전에 파악하여 방제 우선순위 설정에 활용한다.", + "description": "좌측 하단 '정보 레이어' 패널에서 환경생태·사회경제·민감도평가·해경관할구역 등 대분류 토글을 제공한다. 각 항목 우측에 해당 레이어 데이터 건수가 표시된다. '전체 켜기/끄기' 버튼으로 모든 레이어를 일괄 제어할 수 있다.", + "procedure": [ + "좌측 하단 '정보 레이어' 패널을 열고 원하는 레이어 항목을 활성화한다.", + "확산예측 실행 후 지도에 표출된 오염 범위와 레이어 분포를 비교한다.", + "오염 영향이 우려되는 자원 레이어를 클릭하여 상세 정보를 확인한다.", + "필요한 레이어 조합을 선택하여 보고서용 지도 캡처에 활용한다." + ], + "notes": [ + "레이어 수가 많을 경우 지도 로딩 속도가 느려질 수 있으므로 필요한 레이어만 선택적으로 활성화한다." + ] + }, + { + "id": "005", + "name": "AI자동추천 배치안 적용", + "menuPath": "유출유확산예측 > 유출유확산분석 > 오일펜스 배치 가이드", + "imageIndex": 5, + "overview": "확산예측 결과를 기반으로 AI(NSGA-II 알고리즘)가 최적 오일펜스 배치안을 자동으로 생성하는 기능이다. 최대 3개 방어선의 위치·방향·총 길이·차단율을 자동 계산하여 지도에 표출한다.", + "description": "'오일펜스 배치 가이드' 패널 상단 'AI자동 추천' 탭에서 배치 결과를 확인한다. 1차~3차 방어선별 배치 위치(좌표), 방향, 오일펜스 길이, 예상 차단율이 표시된다.", + "procedure": [ + "확산예측 실행 완료 후 좌측 '오일펜스 배치 가이드' 패널을 연다.", + "'AI자동 추천' 탭을 선택하여 추천 배치안을 확인한다.", + "방어선별 차단율 및 오일펜스 총 길이를 검토한다.", + "'추천 배치안 적용하기' 버튼을 클릭하여 지도에 표출한다.", + "현장 여건(조류·지형·자원 현황)을 고려하여 최종 배치안을 확정한다." + ], + "inputs": [ + { + "label": "배치 수", + "type": "숫자", + "required": false, + "desc": "투입할 오일펜스 방어선 수" + }, + { + "label": "최소 차단율", + "type": "숫자 %", + "required": false, + "desc": "목표 차단율" + } + ], + "notes": [ + "AI 추천 기능은 확산예측이 완료된 후에 활성화된다.", + "추천 배치안은 수치 모델 기반 결과로, 현장 조류 변동·수심·접근 가능성 등을 반드시 현장 담당자가 최종 확인해야 한다." + ] + }, + { + "id": "006", + "name": "이미지업로드", + "menuPath": "유출유확산예측 > 유출유확산분석", + "imageIndex": 6, + "overview": "위성·드론·항공기에서 촬영한 이미지를 업로드하여 실제 오염 현황과 모델 예측 결과를 비교 분석하는 기능이다.", + "description": "예측정보 입력 패널 상단 '이미지 업로드' 탭을 선택하면 파일 업로드 영역이 활성화된다. 업로드된 이미지는 지도에 자동으로 오버레이 표출된다.", + "procedure": [ + "예측정보 입력 패널에서 '이미지 업로드' 탭을 선택한다.", + "'파일 선택' 버튼을 클릭하거나 파일을 드래그하여 업로드한다.", + "업로드된 이미지가 지도에 올바르게 표출되는지 위치를 확인한다.", + "확산예측 결과와 이미지를 동시에 표출하여 비교한다." + ], + "inputs": [ + { + "label": "업로드 파일", + "type": "파일 PNG/JPG", + "required": false, + "desc": "위성·드론·항공기 촬영 이미지" + } + ], + "notes": [ + "지원 파일 형식은 PNG, JPG이다.", + "업로드한 이미지의 좌표 기준(GCP)이 없을 경우 지도 상 위치 정확도가 낮을 수 있다." + ] + }, + { + "id": "007", + "name": "분석 목록", + "menuPath": "유출유확산예측", + "imageIndex": 7, + "overview": "완료 또는 진행 중인 유출유 확산예측 분석 이력을 목록 형식으로 조회하는 화면이다. 사고명·날짜·유종·유출량·모델 상태 등을 한눈에 파악할 수 있다.", + "description": "목록에는 번호·사고명·사고일시·예측시간·유종·유출량·모델별 상태(KOSPS·POSEIDON·OPENDRIFT)·담당자 등이 표시된다. 모델 상태는 '완료(녹색 배지)'와 '대기(회색 배지)'로 구분된다.", + "procedure": [ + "상단 메뉴에서 '유출유확산예측'을 클릭하여 분석 목록 화면으로 이동한다.", + "검색창에 사고명을 입력하거나 페이지를 이동하여 원하는 분석을 찾는다.", + "사고명 링크를 클릭하여 해당 분석 결과 상세 화면으로 이동한다.", + "신규 분석이 필요한 경우 우측 상단 '+ 새 분석' 버튼을 클릭한다." + ], + "notes": [ + "분석 결과 삭제 시 복구가 불가하므로, 삭제 전 보고서 출력 또는 데이터 저장 여부를 확인한다." + ] + }, + { + "id": "008", + "name": "분석 조회(test001)", + "menuPath": "유출유확산예측 > 분석 목록", + "imageIndex": 8, + "overview": "분석 목록에서 특정 분석 건을 선택하면 해당 확산분석 결과 화면으로 이동하여 상세 내용을 조회한다. 이전 분석 결과를 재검토하거나 조건 변경 후 재분석할 수 있다.", + "procedure": [ + "분석 목록에서 조회할 사고명 링크를 클릭한다.", + "로드된 분석 정보를 확인한다.", + "필요 시 입력 조건을 수정하고 '확산예측 실행'을 다시 클릭하여 재분석한다.", + "결과를 보고서로 출력하거나 저장한다." + ], + "notes": [ + "기존 분석에서 모델 조건 변경 후 재실행 시 이전 결과가 덮어써질 수 있으므로, 원본 결과를 먼저 저장한다." + ] + }, + { + "id": "009", + "name": "유출유확산모델 이론 - 시스템 개요", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 9, + "overview": "Wing 시스템에 탑재된 유출유 확산 수치 모델의 개요와 운용 체계를 안내하는 이론 화면이다. KOSPS·POSEIDON·OpenDrift 3종 모델의 특징·비교·데이터 흐름을 확인할 수 있다.", + "procedure": [ + "상단 메뉴에서 '유출유확산예측 > 유출유확산모델 이론'을 클릭한다.", + "'시스템 개요' 탭을 선택하여 전체 모델 체계를 확인한다.", + "각 탭(KOSPS·POSEIDON·OpenDrift 등)을 클릭하여 상세 이론을 열람한다." + ] + }, + { + "id": "010", + "name": "유출유확산모델 이론 - KOSPS", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 10, + "overview": "한국해양과학기술원(KIOST)이 개발한 KOSPS 모델의 상세 이론을 안내한다. 국내 연안 특성에 최적화된 조류예측(CHARRY) 및 풍류 경험식 적용 원리를 설명한다.", + "notes": [ + "KOSPS는 국내 연안 중심으로 검증된 모델로, 원해·심해 적용 시 정확도 제한이 있을 수 있다." + ] + }, + { + "id": "011", + "name": "유출유확산모델 이론 - POSEIDON", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 11, + "overview": "입자추적 최적화 예측 시스템 POSEIDON의 이론 및 MOHID 3D 해양순환모델 적용 원리를 안내한다. GA·DE·HS·PSO 등 4종 최적화 알고리즘을 통한 파라미터 자동 최적화 방법을 설명한다." + }, + { + "id": "012", + "name": "유출유확산모델 이론 - OpenDrift", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 12, + "overview": "노르웨이 MET Norway가 개발한 오픈소스 라그랑지안 확산 프레임워크 OpenDrift의 이론을 안내한다. NEMO·ROMS·HYCOM·Copernicus CMEMS 등 다양한 해양 예보 모델과 연동 가능한 범용성을 설명한다." + }, + { + "id": "013", + "name": "유출유확산모델 이론 - 입자추적법", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 13, + "overview": "유출유 수치 모델에 적용되는 라그랑지안 입자추적법(Lagrangian Particle Tracking Method)의 수학적 이론과 수식을 안내한다." + }, + { + "id": "014", + "name": "유출유확산모델 이론 - 풍화 프로세스", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 14, + "overview": "유출유가 시간 경과에 따라 겪는 풍화(Weathering) 프로세스(증발·유화·자연분산 등)의 이론과 타임라인을 안내한다.", + "notes": [ + "풍화 속도는 유종·기온·해수온·파랑 조건에 따라 크게 달라질 수 있으므로 참고용으로 활용한다." + ] + }, + { + "id": "015", + "name": "유출유확산모델 이론 - 해양환경 입력", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 15, + "overview": "유출유 확산 수치 모델에 사용되는 해양환경 입력 데이터(기상·해류·조류·해수면 온도) 체계를 안내한다. KMA RDAPS·ECMWF·NIFS ROMS·HYCOM·TPXO9 등 데이터 소스 설명." + }, + { + "id": "016", + "name": "유출유확산모델 이론 - 모델 검증", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 16, + "overview": "유출유 확산 모델의 정확도 검증 사례(2007 허베이스피리트, 2014 무이산 등)와 RMSE·Skill Score 통계 지표를 안내한다." + }, + { + "id": "017", + "name": "유출유확산모델 이론 - 앙상블", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 17, + "overview": "KOSPS·POSEIDON·OpenDrift 3종 모델의 결과를 통합하는 앙상블 예측 방법론과 가중평균 알고리즘을 안내한다. 최악 시나리오(Worst Case) 산출 방법도 설명한다." + }, + { + "id": "018", + "name": "유출유확산모델 이론 - 발전 방향", + "menuPath": "유출유확산예측 > 유출유확산모델 이론", + "imageIndex": 18, + "overview": "현재 유출유 확산 모델의 한계와 향후 개선 방향(4단계 로드맵)을 안내한다." + }, + { + "id": "019", + "name": "오일펜스 배치 알고리즘 이론 - 개요", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 19, + "overview": "오일펜스 최적 배치 알고리즘의 전체 개요와 최적화 목표(차단 면적 최대화·도달시간 최소화·자원 효율화)를 안내한다." + }, + { + "id": "020", + "name": "오일펜스 배치 알고리즘 이론 - 배치 이론", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 20, + "overview": "차단 효율 함수(E(theta, U))와 V형·U형·J형 배치 형태별 이론적 차단 원리를 안내한다. 다단계 차단선 배치 시 총 차단 효율 산출 공식도 포함한다." + }, + { + "id": "021", + "name": "오일펜스 배치 알고리즘 이론 - 최적화 알고리즘", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 21, + "overview": "NSGA-II(Non-dominated Sorting Genetic Algorithm II) 기반 다목적 최적화 알고리즘의 원리와 보조 알고리즘(PSO·Greedy) 비교를 안내한다." + }, + { + "id": "022", + "name": "오일펜스 배치 알고리즘 이론 - 유체역학 모델", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 22, + "overview": "오일펜스 차단 성능 평가에 사용되는 유체역학 모델의 이론을 안내한다." + }, + { + "id": "023", + "name": "오일펜스 배치 알고리즘 이론 - 현장 적용", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 23, + "overview": "오일펜스 배치 알고리즘의 실제 사고 현장 적용 사례와 현장 운용 시 고려사항을 안내한다." + }, + { + "id": "024", + "name": "오일펜스 배치 알고리즘 이론 - 참고문헌", + "menuPath": "유출유확산예측 > 오일펜스 배치 알고리즘 이론", + "imageIndex": 24, + "overview": "오일펜스 배치 알고리즘 이론에 사용된 학술 논문·기술 보고서·국제 기준 등 참고문헌 목록을 안내한다." + } + ] + }, + { + "id": "ch02", + "number": "02", + "title": "HNS·대기확산", + "subtitle": "HNS Atmospheric Dispersion", + "screens": [ + { + "id": "025", + "name": "대기확산예측 실행", + "menuPath": "HNS대기확산 > 대기확산분석", + "imageIndex": 25, + "overview": "HNS(위험유해물질) 해상 유출 시 대기 확산 범위, 위험 농도 구역, 영향 인구를 예측하는 핵심 분석 화면이다. ALOHA(EPA) 또는 이문진박사모델 두 가지 알고리즘 중 선택하여 가우시안 Plume/Puff 모델을 적용한다.", + "description": "화면 좌측에 사고 기본정보·물질 및 유출 조건·기상조건 입력 패널이 위치한다. 중앙 지도에 AEGL-1~3 위험 구역이 색상별 Plume 형태로 표출된다. 우측 '예측 결과' 패널에 최대 농도(ppm), AEGL-1 영향 면적, 위험 등급 등이 표시된다.", + "procedure": [ + "상단 메뉴에서 'HNS대기확산 > 대기확산분석'을 클릭한다.", + "사고 기본정보(사고명·날짜·시간·위치)를 입력한다.", + "HNS 물질 종류 및 누출 방식·유출량을 선택·입력한다.", + "기상조건(풍향·풍속·기온·대기안정도·예측 시간)을 입력한다.", + "적용 알고리즘(ALOHA/이문진박사모델)을 선택한다.", + "'확산예측 실행' 버튼을 클릭한다.", + "지도의 위험 구역 Plume과 우측 예측 결과 패널을 확인한다." + ], + "inputs": [ + { + "label": "사고명", + "type": "텍스트", + "required": true, + "desc": "사고 식별 명칭" + }, + { + "label": "사고 일시", + "type": "날짜+시간", + "required": true, + "desc": "사고 발생 일시" + }, + { + "label": "위도/경도", + "type": "숫자", + "required": true, + "desc": "사고 발생 지점 좌표" + }, + { + "label": "HNS 물질", + "type": "드롭다운", + "required": true, + "desc": "AEGL/ERPG 기준값 자동 로드" + }, + { + "label": "누출 방식", + "type": "라디오", + "required": true, + "desc": "순간 유출 또는 지속 유출" + }, + { + "label": "유출량", + "type": "숫자", + "required": true, + "desc": "ton 또는 g/s 단위" + }, + { + "label": "풍향", + "type": "숫자", + "required": true, + "desc": "0~360도" + }, + { + "label": "풍속", + "type": "숫자 m/s", + "required": true, + "desc": "최소 0.5 m/s 이상" + }, + { + "label": "기온", + "type": "숫자", + "required": true, + "desc": "현재 기온" + }, + { + "label": "대기안정도", + "type": "선택 A~F", + "required": true, + "desc": "Pasquill-Gifford 분류" + }, + { + "label": "예측 시간", + "type": "숫자 h", + "required": true, + "desc": "확산 예측 기간" + }, + { + "label": "적용 알고리즘", + "type": "라디오", + "required": true, + "desc": "ALOHA 또는 이문진박사모델" + } + ], + "notes": [ + "풍속은 최소 0.5 m/s 이상 입력해야 하며 0 입력 시 오류 발생.", + "HNS 물질 선택 후 AEGL/ERPGs 기준값이 자동 로드되었는지 반드시 확인." + ] + }, + { + "id": "026", + "name": "HNS 분석 목록", + "menuPath": "HNS대기확산", + "imageIndex": 26, + "overview": "완료된 HNS 대기확산 분석 이력을 목록 형식으로 조회하는 화면이다. AEGL-1~3 거리·위험 등급·피해반경·담당자 등 주요 결과 정보를 한눈에 파악할 수 있다.", + "procedure": [ + "상단 메뉴에서 'HNS대기확산'을 클릭하여 분석 목록으로 이동한다.", + "검색창에서 분석명을 검색하거나 목록을 스크롤하여 원하는 분석을 찾는다.", + "분석명 링크를 클릭하여 해당 분석 결과 지도 화면으로 이동한다." + ] + }, + { + "id": "027", + "name": "시나리오 상세", + "menuPath": "HNS대기확산 > 시나리오 관리", + "imageIndex": 27, + "overview": "단일 HNS 사고에 대해 여러 기상·유출 조건 시나리오(S-01~S-05 등)를 생성·비교 관리하는 화면이다. 시나리오별 위험도·위험 구역·대응 권고사항을 한눈에 비교할 수 있다.", + "description": "시나리오 목록 카드에 시나리오명·위험도 배지(CRITICAL/HIGH/MEDIUM/RESOLVED)·최대농도·IDLH 거리·ERPG-2 거리·영향인구 요약이 표시된다.", + "procedure": [ + "'HNS대기확산 > 시나리오 관리'를 클릭하여 이동한다.", + "원하는 사고 건을 선택하면 시나리오 목록이 표시된다.", + "시나리오 카드를 클릭하여 상세 위험 구역과 대응 권고사항을 확인한다.", + "'비교 차트' 탭으로 이동하여 시나리오 간 시간별 변화를 비교한다." + ] + }, + { + "id": "028", + "name": "비교 차트", + "menuPath": "HNS대기확산 > 시나리오 관리", + "imageIndex": 28, + "overview": "생성된 여러 HNS 시나리오의 최대 지표면 농도·위험 반경·영향 인구를 시간별 변화 차트로 비교하는 화면이다.", + "description": "상단은 시나리오별 최대 지표면 농도(ppm) 시간별 변화 그래프. 중단에 위험 반경(km)과 영향 인구(명) 변화 차트. 하단에 피대농도·IDLH 거리·ERPG-2 반경·영향인구·풍향/풍속·위험등급 항목별 비교표가 제공된다." + }, + { + "id": "029", + "name": "확산범위 오버레이", + "menuPath": "HNS대기확산 > 시나리오 관리", + "imageIndex": 29, + "overview": "HNS 대기확산 시나리오의 시간대별(T+0h~T+4h) 확산 범위를 지도에 오버레이 표출하는 화면이다.", + "procedure": [ + "시나리오 관리 화면에서 '확산범위 오버레이' 탭을 클릭한다.", + "지도에서 시간대별 확산 범위를 확인한다.", + "슬라이더를 이동하여 원하는 시간대의 확산 상태를 조회한다." + ] + }, + { + "id": "030", + "name": "신규 시나리오", + "menuPath": "HNS대기확산 > 시나리오 관리", + "imageIndex": 30, + "overview": "기상·유출 조건을 변경하여 새로운 HNS 대기확산 시나리오를 생성하는 입력 모달 화면이다.", + "procedure": [ + "시나리오 관리 화면 우측 상단 '신규 시나리오' 버튼을 클릭한다.", + "시나리오명과 기준 시각을 입력한다.", + "HNS 물질·누출 구분·유출량을 입력한다.", + "기상조건(풍향·풍속·기온·대기안정도)을 입력한다.", + "'시나리오 생성 및 예측 실행'을 클릭한다." + ], + "inputs": [ + { + "label": "시나리오명", + "type": "텍스트", + "required": true, + "desc": "시나리오 식별 명칭" + }, + { + "label": "HNS 물질", + "type": "드롭다운", + "required": true, + "desc": "유출 물질 선택" + }, + { + "label": "누출 구분", + "type": "라디오", + "required": true, + "desc": "순간/지속" + }, + { + "label": "유출량", + "type": "숫자", + "required": true, + "desc": "ton 또는 g/s" + }, + { + "label": "풍향/풍속", + "type": "숫자", + "required": true, + "desc": "각도와 m/s" + }, + { + "label": "기온", + "type": "숫자", + "required": true, + "desc": "섭씨" + }, + { + "label": "대기안정도", + "type": "선택 A~F", + "required": true, + "desc": "Pasquill-Gifford" + } + ] + }, + { + "id": "031", + "name": "HNS 대응매뉴얼", + "menuPath": "HNS대기확산", + "imageIndex": 31, + "overview": "해양 HNS 사고 대응 절차를 정리한 Marine HNS Response Manual(Bonn Agreement·HELCOM·REMPEC 기반)을 시스템 내에서 직접 열람하는 화면이다. 8개 챕터. SEBC 거동 분류 5유형 카드.", + "procedure": [ + "상단 메뉴에서 'HNS대기확산 > HNS 대응매뉴얼'을 클릭한다.", + "원하는 챕터 카드를 클릭하여 세부 내용을 확인한다.", + "하단 SEBC 분류 카드를 클릭하여 거동 유형별 대응 방법을 확인한다." + ] + }, + { + "id": "032", + "name": "HNS 확산모델 이론 - 시스템 개요", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 32, + "overview": "Wing에 탑재된 HNS 대기확산 모델(WRF-Chem·Gaussian Plume/Puff·ROMS 연동)의 전체 체계와 처리 흐름을 안내한다." + }, + { + "id": "033", + "name": "HNS 확산모델 이론 - 가우시안 모델", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 33, + "overview": "HNS 대기확산 예측에 적용되는 Gaussian Plume·Puff 모델의 수학적 이론과 Pasquill-Gifford 대기안정도 분류 체계를 안내한다." + }, + { + "id": "034", + "name": "HNS 확산모델 이론 - 물질별 시나리오", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 34, + "overview": "주요 HNS 물질(NH3·CH3OH·H2·LNG 등)의 물질 특성 및 대기확산 시나리오 적용 기준을 안내한다." + }, + { + "id": "035", + "name": "HNS 확산모델 이론 - 해양환경 보정", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 35, + "overview": "해양 환경 특성(해풍·육풍 순환·해수면 거칠기·SST 영향·MABL 구조)에 따른 대기확산 모델 보정 인자를 안내한다." + }, + { + "id": "036", + "name": "HNS 확산모델 이론 - 모델 검증", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 36, + "overview": "HNS 대기확산 모델의 실측값 대비 예측 정확도 검증 결과(ALOHA·이문진박사모델·실측값 3종 비교)를 안내한다." + }, + { + "id": "037", + "name": "HNS 확산모델 이론 - 실시간 비교", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 37, + "overview": "동일 조건에서 ALOHA와 이문진박사모델의 예측 결과를 실시간으로 비교하는 시뮬레이션 화면이다." + }, + { + "id": "038", + "name": "HNS 확산모델 이론 - WRF-Chem 발전", + "menuPath": "HNS대기확산 > 확산모델 이론", + "imageIndex": 38, + "overview": "WRF-Chem 기상-화학 연계 모델 및 ROMS 해양확산 수치모의 검증 결과와 고도화 방향을 안내한다." + }, + { + "id": "039", + "name": "HNS물질정보 - SEBC 거동분류", + "menuPath": "HNS대기확산 > HNS물질정보", + "imageIndex": 39, + "overview": "HNS 물질의 해양 거동 특성을 SEBC 기준으로 분류한 정보를 제공한다. G(기체)·E(증발)·F(부유)·D(용해)·S(침강) 5가지 기본 유형과 복합 거동 유형을 안내한다." + }, + { + "id": "040", + "name": "HNS물질정보 - 주요 물질 특성", + "menuPath": "HNS대기확산 > HNS물질정보", + "imageIndex": 40, + "overview": "NH3·CH4·H2·페놀·톨루엔 등 주요 HNS 물질의 상세 물리화학적 특성을 카드 형식으로 제공하는 화면이다." + }, + { + "id": "041", + "name": "HNS물질정보 - 위험도 기준", + "menuPath": "HNS대기확산 > HNS물질정보", + "imageIndex": 41, + "overview": "HNS 물질의 위험도 평가에 사용되는 AEGL·ERPG·IDLH·TWA 등 주요 독성 기준값 체계를 안내한다." + }, + { + "id": "042", + "name": "HNS물질정보 - 물질 상세검색", + "menuPath": "HNS대기확산 > HNS물질정보", + "imageIndex": 42, + "overview": "HNS 6,500여 종의 물질 데이터베이스에서 CAS 번호·물질명·거동 분류 조건으로 물질을 검색하는 화면이다.", + "inputs": [ + { + "label": "검색어", + "type": "텍스트", + "required": true, + "desc": "CAS 번호 또는 물질명" + }, + { + "label": "거동 분류", + "type": "체크박스", + "required": false, + "desc": "SEBC 유형 필터" + } + ], + "notes": [ + "CAS 번호 입력 시 하이픈(-) 포함 정확한 형식으로 입력해야 검색된다." + ] + } + ] + }, + { + "id": "ch03", + "number": "03", + "title": "긴급구난", + "subtitle": "Emergency Rescue", + "screens": [ + { + "id": "043", + "name": "긴급구난예측", + "menuPath": "긴급구난", + "imageIndex": 43, + "overview": "해상 조난자·표류 선박·표류 물체의 위치를 예측하는 긴급구난(SAR) 분석 화면이다. 해류·바람·파랑 데이터 기반 라그랑지안 표류 궤적 시뮬레이션을 수행한다.", + "description": "화면 좌측에 사고 발생 위치·일시·표류체 종류·예측 시간·기상조건 입력 패널이 위치한다. 중앙 지도에 표류 예측 궤적과 탐색 구역(최우선·일반)이 표출된다.", + "procedure": [ + "상단 메뉴에서 '긴급구난'을 클릭한다.", + "사고 발생 위치(위경도)와 일시를 입력한다.", + "표류체 종류(선박/사람/컨테이너 등)를 선택한다.", + "예측 시간과 기상 조건을 입력한다.", + "'예측 실행' 버튼을 클릭하여 표류 궤적과 탐색 구역을 확인한다." + ], + "inputs": [ + { + "label": "사고 위치", + "type": "숫자", + "required": true, + "desc": "위·경도" + }, + { + "label": "사고 일시", + "type": "날짜+시간", + "required": true, + "desc": "발생 일시" + }, + { + "label": "표류체 종류", + "type": "드롭다운", + "required": true, + "desc": "선박·사람·컨테이너·구명정 등" + }, + { + "label": "예측 시간", + "type": "숫자 h", + "required": true, + "desc": "표류 예측 기간" + }, + { + "label": "기상조건", + "type": "숫자", + "required": false, + "desc": "미입력 시 실시간 자동 적용" + } + ], + "notes": [ + "표류체 종류 선택이 부적절한 경우 예측 정확도가 낮아질 수 있다.", + "신속한 대응을 위해 입력 완료 즉시 실행하고 기상 변화 시 재분석을 수행한다." + ] + }, + { + "id": "044", + "name": "긴급구난 목록", + "menuPath": "긴급구난", + "imageIndex": 44, + "overview": "완료 또는 진행 중인 긴급구난 예측 분석 이력을 목록 형식으로 조회하는 화면이다.", + "procedure": [ + "상단 메뉴에서 '긴급구난'을 클릭하면 목록 화면이 표시된다.", + "원하는 분석을 검색하거나 목록에서 선택한다.", + "사고명 링크를 클릭하여 상세 분석 결과로 이동한다." + ] + }, + { + "id": "045", + "name": "시나리오 상세", + "menuPath": "긴급구난 > 시나리오 관리", + "imageIndex": 45, + "overview": "긴급구난 사고에 대해 기상·해양 조건을 달리 설정한 복수 시나리오를 생성·비교 관리하는 화면이다.", + "procedure": [ + "'긴급구난 > 시나리오 관리'를 클릭하여 이동한다.", + "생성된 시나리오 카드를 확인하거나 '신규 시나리오' 버튼으로 추가 생성한다.", + "시나리오 카드를 클릭하여 상세 내용을 확인한다." + ] + }, + { + "id": "046", + "name": "비교 차트", + "menuPath": "긴급구난 > 시나리오 관리", + "imageIndex": 46, + "overview": "긴급구난 시나리오별 탐색 면적·표류 속도·최대 이동거리를 시간별 변화 차트로 비교하는 화면이다." + }, + { + "id": "047", + "name": "지도 오버레이", + "menuPath": "긴급구난 > 시나리오 관리", + "imageIndex": 47, + "overview": "긴급구난 시나리오의 시간대별 표류 예측 궤적 및 탐색 구역을 지도에 오버레이 표출하는 화면이다." + }, + { + "id": "048", + "name": "긴급구난모델 이론", + "menuPath": "긴급구난", + "imageIndex": 48, + "overview": "긴급구난 표류 예측에 적용되는 라그랑지안 표류 모델·탐색 구역(Search Area) 산출 방법·IMO SAR 기준 적용 이론을 안내한다." + } + ] + }, + { + "id": "ch04", + "number": "04", + "title": "보고자료", + "subtitle": "Reports", + "screens": [ + { + "id": "049", + "name": "보고서 목록", + "menuPath": "보고자료", + "imageIndex": 49, + "overview": "시스템에서 생성된 모든 사고 대응 보고서를 목록 형식으로 관리하는 화면이다. 보고서명·사고명·생성일시·유형·작성자·상태가 표시된다.", + "procedure": [ + "상단 메뉴에서 '보고자료'를 클릭하여 보고서 목록으로 이동한다.", + "검색창에서 원하는 보고서를 검색하거나 목록에서 선택한다.", + "보고서명을 클릭하여 미리보기하거나 PDF를 다운로드한다." + ] + }, + { + "id": "050", + "name": "초기보고서", + "menuPath": "보고자료 > 표준보고서 템플릿", + "imageIndex": 50, + "overview": "사고 발생 초기 지휘부 보고를 위한 표준 보고서 템플릿을 제공한다. 확산예측 결과와 사고 기본정보가 자동 연동된다.", + "procedure": [ + "보고자료 메뉴에서 '표준보고서 템플릿'을 클릭한다.", + "좌측 사이드바에서 '초기보고서'를 선택한다.", + "자동 입력된 항목을 검토하고 필요 내용을 추가 입력한다.", + "'저장' 또는 'PDF 출력' 버튼을 활용한다." + ] + }, + { + "id": "051", + "name": "지휘부 보고", + "menuPath": "보고자료 > 표준보고서 템플릿", + "imageIndex": 51, + "overview": "사고 지휘부 보고용 표준 보고서 템플릿. 방제 대응 현황·자원 투입 현황·향후 계획이 포함된다." + }, + { + "id": "052", + "name": "예측보고서", + "menuPath": "보고자료 > 표준보고서 템플릿", + "imageIndex": 52, + "overview": "확산예측 결과를 포함한 예측보고서 템플릿. 모델 종류·예측 시간·주요 결과가 자동 연동된다." + }, + { + "id": "053", + "name": "종합보고서", + "menuPath": "보고자료 > 표준보고서 템플릿", + "imageIndex": 53, + "overview": "사고 종료 후 방제 대응 전 과정을 정리하는 종합보고서. 사고개요·대응경과·방제실적·환경영향평가·교훈이 포함된다." + }, + { + "id": "054", + "name": "유출유 보고", + "menuPath": "보고자료 > 표준보고서 템플릿", + "imageIndex": 54, + "overview": "유출유 사고 전용 표준 보고서 템플릿. 확산예측·오일펜스 배치·풍화 상태가 자동 연동된다." + }, + { + "id": "055", + "name": "유출유 확산예측 보고서 생성", + "menuPath": "보고자료 > 보고서 생성", + "imageIndex": 55, + "overview": "유출유 확산예측 분석 결과를 기반으로 보고서를 자동 생성하는 화면이다.", + "procedure": [ + "보고자료 메뉴에서 '보고서 생성'을 클릭한다.", + "'유출유 확산예측' 유형을 선택한다.", + "연동할 분석 결과를 드롭다운에서 선택한다.", + "보고 대상을 선택한다.", + "'보고서 생성' 버튼을 클릭한다." + ], + "inputs": [ + { + "label": "분석 결과", + "type": "드롭다운", + "required": true, + "desc": "연동할 분석 건 선택" + }, + { + "label": "보고 대상", + "type": "체크박스", + "required": true, + "desc": "지휘부·현장·외부 기관" + } + ] + }, + { + "id": "056", + "name": "HNS 대기확산 보고서 생성", + "menuPath": "보고자료 > 보고서 생성", + "imageIndex": 56, + "overview": "HNS 대기확산 분석 결과를 기반으로 보고서를 자동 생성하는 화면이다." + }, + { + "id": "057", + "name": "긴급구난 보고서 생성", + "menuPath": "보고자료 > 보고서 생성", + "imageIndex": 57, + "overview": "긴급구난 예측 분석 결과를 기반으로 구조 현장 지휘부용 보고서를 자동 생성하는 화면이다." + } + ] + }, + { + "id": "ch05", + "number": "05", + "title": "항공탐색", + "subtitle": "Aerial Surveillance", + "screens": [ + { + "id": "058", + "name": "영상사진관리", + "menuPath": "항공탐색", + "imageIndex": 58, + "overview": "드론·항공기·위성에서 수집된 영상 및 사진을 통합 관리하는 화면이다. 촬영 일시·위치·기기 유형별 분류 조회와 유출유 면적분석 연계를 지원한다.", + "procedure": [ + "상단 메뉴에서 '항공탐색 > 영상사진관리'를 클릭한다.", + "목록에서 원하는 영상/사진을 선택하거나 지도 마커를 클릭하여 확인한다.", + "신규 파일 업로드 시 '파일 업로드' 버튼을 클릭한다." + ], + "inputs": [ + { + "label": "업로드 파일", + "type": "파일", + "required": false, + "desc": "JPG·PNG·GeoTIFF·KMZ" + }, + { + "label": "촬영 일시", + "type": "날짜+시간", + "required": false, + "desc": "촬영 시점" + }, + { + "label": "장비 유형", + "type": "드롭다운", + "required": false, + "desc": "드론·항공기·위성·CCTV" + } + ], + "notes": [ + "50MB 초과 파일은 업로드 전 압축하거나 관리자에게 문의한다." + ] + }, + { + "id": "059", + "name": "유출유면적분석", + "menuPath": "항공탐색", + "imageIndex": 59, + "overview": "항공·위성 이미지를 기반으로 유출유 오염 면적을 AI 자동 산출하는 화면이다. 자동 추출된 오염 경계선을 수동 편집하여 정밀 면적을 산정할 수 있다.", + "procedure": [ + "영상사진관리에서 분석 대상 이미지를 선택하고 '면적 분석' 버튼을 클릭한다.", + "'AI 자동 분석 실행' 버튼을 클릭하여 오염 경계선을 추출한다.", + "폴리곤 경계선을 검토하고 필요 시 편집 도구로 수동 조정한다.", + "최종 면적·둘레·중심 좌표를 확인하고 저장한다." + ] + }, + { + "id": "060", + "name": "실시간드론", + "menuPath": "항공탐색", + "imageIndex": 60, + "overview": "현장 드론에서 실시간 전송되는 영상 스트리밍을 시스템 내에서 직접 모니터링하는 화면이다.", + "procedure": [ + "상단 메뉴에서 '항공탐색 > 실시간 드론'을 클릭한다.", + "연결된 드론 목록에서 모니터링할 드론을 선택한다.", + "실시간 영상을 확인하고 필요 시 스냅샷을 저장한다." + ], + "notes": [ + "드론 스트리밍은 네트워크 연결 상태에 따라 화질 및 지연이 달라질 수 있다." + ] + }, + { + "id": "061", + "name": "오염선박 3D분석", + "menuPath": "항공탐색", + "imageIndex": 61, + "overview": "항공·위성 이미지를 기반으로 오염 선박의 3D 모델을 생성하고 오염 분포를 입체적으로 분석하는 화면이다." + }, + { + "id": "062", + "name": "위성요청", + "menuPath": "항공탐색", + "imageIndex": 62, + "overview": "SAR·광학 위성 촬영 요청 및 수신 결과를 관리하는 화면이다.", + "inputs": [ + { + "label": "요청 위치", + "type": "숫자", + "required": true, + "desc": "위·경도" + }, + { + "label": "촬영 희망 일시", + "type": "날짜+시간", + "required": true, + "desc": "촬영 필요 일시" + }, + { + "label": "위성 종류", + "type": "라디오", + "required": true, + "desc": "SAR 또는 광학" + }, + { + "label": "해상도", + "type": "드롭다운", + "required": false, + "desc": "요청 이미지 해상도" + } + ], + "notes": [ + "위성 촬영 가능 여부는 궤도 조건에 따라 결정되며 요청이 항상 수용되지 않을 수 있다." + ] + }, + { + "id": "063", + "name": "CCTV 조회", + "menuPath": "항공탐색", + "imageIndex": 63, + "overview": "해안·항만 CCTV의 실시간 영상을 조회하는 화면이다. 사고 인근 CCTV를 지도에서 선택하여 스트리밍으로 확인할 수 있다.", + "procedure": [ + "상단 메뉴에서 '항공탐색 > CCTV조회'를 클릭한다.", + "지도에서 확인할 CCTV 마커를 클릭한다.", + "실시간 영상을 확인하고 필요 시 스냅샷을 저장한다." + ] + }, + { + "id": "064", + "name": "항공탐색 이론 - 개요", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 64, + "overview": "Wing 항공탐색 기능에 적용되는 원격탐사·ESI 방제지도·면적 산정·확산예측 연계 이론의 전체 개요를 안내한다." + }, + { + "id": "065", + "name": "항공탐색 이론 - 탐지 장비", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 65, + "overview": "해양 오염 항공 탐지에 사용되는 주요 장비(SAR·적외선 카메라·UV 센서·광학 카메라)의 특성과 적용 원리를 안내한다." + }, + { + "id": "066", + "name": "항공탐색 이론 - 원격탐사", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 66, + "overview": "위성 및 항공 원격탐사(Remote Sensing)의 기본 원리와 해양 오염 탐지 적용 방법을 안내한다." + }, + { + "id": "067", + "name": "항공탐색 이론 - ESI 방제지도", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 67, + "overview": "환경민감지수(ESI) 방제지도의 해안선 민감도 등급 분류 체계와 방제 우선순위 결정 방법을 안내한다." + }, + { + "id": "068", + "name": "항공탐색 이론 - 면적 산정", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 68, + "overview": "항공·위성 이미지 기반 유출유 면적 산정에 적용되는 AI 알고리즘(객체 탐지·영상 분할)의 원리를 안내한다." + }, + { + "id": "069", + "name": "항공탐색 이론 - 확산예측 연계", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 69, + "overview": "항공 탐색 결과(실측 면적·위치)와 유출유 확산예측 모델 보정 연계 방법을 안내한다." + }, + { + "id": "070", + "name": "항공탐색 이론 - 논문 특허", + "menuPath": "항공탐색 > 항공탐색 이론", + "imageIndex": 70, + "overview": "Wing 항공탐색 기능의 기반이 되는 관련 논문과 특허 목록을 안내한다." + } + ] + }, + { + "id": "ch06", + "number": "06", + "title": "게시판", + "subtitle": "Bulletin Board", + "screens": [ + { + "id": "071", + "name": "전체 게시판", + "menuPath": "게시판", + "imageIndex": 71, + "overview": "공지사항·자료실·Q&A·해경매뉴얼 게시물을 통합 조회하는 전체 게시판 화면이다.", + "procedure": [ + "상단 메뉴에서 '게시판'을 클릭하여 전체 게시판으로 이동한다.", + "유형 필터 탭을 선택하거나 검색창에 키워드를 입력한다.", + "원하는 게시물 제목을 클릭하여 상세 내용을 확인한다." + ] + }, + { + "id": "072", + "name": "공지사항", + "menuPath": "게시판", + "imageIndex": 72, + "overview": "시스템 공지·업데이트·장애 안내 등 운영 공지사항을 게시하는 화면이다. 관리자만 등록·수정 가능." + }, + { + "id": "073", + "name": "자료실", + "menuPath": "게시판", + "imageIndex": 73, + "overview": "매뉴얼·지침서·참고자료 등 업무 관련 문서 파일을 공유하는 게시판 화면이다." + }, + { + "id": "074", + "name": "QNA", + "menuPath": "게시판", + "imageIndex": 74, + "overview": "시스템 사용 관련 질문을 등록하고 답변을 확인하는 Q&A 게시판 화면이다.", + "inputs": [ + { + "label": "제목", + "type": "텍스트", + "required": true, + "desc": "질문 제목" + }, + { + "label": "카테고리", + "type": "드롭다운", + "required": true, + "desc": "기능문의·오류신고·개선요청" + }, + { + "label": "내용", + "type": "텍스트", + "required": true, + "desc": "질문 내용" + }, + { + "label": "첨부 파일", + "type": "파일", + "required": false, + "desc": "스크린샷 등" + } + ] + }, + { + "id": "075", + "name": "해경 매뉴얼", + "menuPath": "게시판", + "imageIndex": 75, + "overview": "해양경찰청 방제·대응 매뉴얼을 시스템 내에서 바로 조회할 수 있는 화면이다. 관리자만 등록·수정 가능." + } + ] + }, + { + "id": "ch07", + "number": "07", + "title": "기상정보", + "subtitle": "Weather", + "screens": [ + { + "id": "076", + "name": "기상정보", + "menuPath": "기상정보", + "imageIndex": 76, + "overview": "현재 사고 지역 주변의 실시간 기상 정보(풍향·풍속·기온·파고·시정 등)를 조회하는 화면이다. 기상 데이터는 확산예측 모델 입력으로 자동 연동된다.", + "description": "사고 위치 기준 반경 내 기상 관측소 목록과 최신 관측값이 표시된다. 시계열 그래프로 과거 24시간 기상 변화를 확인할 수 있다.", + "procedure": [ + "상단 메뉴에서 '기상정보'를 클릭하여 이동한다.", + "지도에서 관측소 마커를 클릭하거나 목록에서 관측소를 선택한다.", + "최신 기상값과 시계열 그래프를 확인한다." + ], + "notes": [ + "극한 기상 조건에서는 확산예측 정확도가 낮아질 수 있으므로 현장 기상 관측값과 병행 확인한다." + ] + } + ] + }, + { + "id": "ch08", + "number": "08", + "title": "통합조회", + "subtitle": "Integrated Search", + "screens": [ + { + "id": "077", + "name": "통합조회", + "menuPath": "통합조회", + "imageIndex": 77, + "overview": "Wing 시스템 전체 분석 이력(유출유·HNS·긴급구난·보고서)을 통합 조회하는 화면이다. 사고명·날짜·분석 유형·담당자 등 복합 조건으로 검색하고 결과를 지도와 목록으로 표출한다.", + "description": "화면 좌측에 날짜 범위·분석 유형·담당자·사고 지역 복합 필터 패널이 위치한다. 중앙 지도에 검색 결과 사고지점 마커가 표출된다. Excel·CSV 내보내기를 지원한다.", + "procedure": [ + "상단 메뉴에서 '통합조회'를 클릭하여 이동한다.", + "날짜 범위·분석 유형 등 필터 조건을 설정한다.", + "'검색' 버튼을 클릭하여 결과를 조회한다.", + "지도 마커 또는 목록 항목을 클릭하여 해당 분석 결과 상세 화면으로 이동한다.", + "Excel·CSV 내보내기 버튼으로 이력 자료를 저장한다." + ], + "inputs": [ + { + "label": "날짜 범위", + "type": "날짜", + "required": false, + "desc": "시작·종료 날짜" + }, + { + "label": "분석 유형", + "type": "체크박스", + "required": false, + "desc": "유출유·HNS·긴급구난·보고서" + }, + { + "label": "담당자", + "type": "텍스트", + "required": false, + "desc": "이름 필터" + }, + { + "label": "사고 지역", + "type": "텍스트", + "required": false, + "desc": "지역명 필터" + } + ] + } + ] + } +] diff --git a/frontend/src/common/data/manualChapters.ts b/frontend/src/common/data/manualChapters.ts new file mode 100644 index 0000000..cde842a --- /dev/null +++ b/frontend/src/common/data/manualChapters.ts @@ -0,0 +1,30 @@ +import chaptersJson from './chapters.json'; + +export interface InputItem { + label: string; + type: string; + required: boolean; + desc: string; +} + +export interface ScreenItem { + id: string; + name: string; + menuPath: string; + imageIndex: number; + overview: string; + description?: string; + procedure?: string[]; + inputs?: InputItem[]; + notes?: string[]; +} + +export interface Chapter { + id: string; + number: string; + title: string; + subtitle: string; + screens: ScreenItem[]; +} + +export const CHAPTERS = chaptersJson as Chapter[]; diff --git a/frontend/src/tabs/aerial/components/MediaManagement.tsx b/frontend/src/tabs/aerial/components/MediaManagement.tsx index b5f281c..93c21c6 100644 --- a/frontend/src/tabs/aerial/components/MediaManagement.tsx +++ b/frontend/src/tabs/aerial/components/MediaManagement.tsx @@ -1,5 +1,10 @@ import { useState, useCallback, useRef, useEffect } from 'react'; -import { fetchAerialMedia, downloadAerialMedia } from '../services/aerialApi'; +import { + fetchAerialMedia, + downloadAerialMedia, + getAerialMediaViewUrl, + uploadAerialMedia, +} from '../services/aerialApi'; import type { AerialMediaItem } from '../services/aerialApi'; import { navigateToTab } from '@common/hooks/useSubMenu'; @@ -54,7 +59,16 @@ export function MediaManagement() { const [downloadResult, setDownloadResult] = useState<{ total: number; success: number } | null>( null, ); + const [previewItem, setPreviewItem] = useState(null); const modalRef = useRef(null); + const previewRef = useRef(null); + const fileInputRef = useRef(null); + const [uploadFile, setUploadFile] = useState(null); + const [uploading, setUploading] = useState(false); + const [dragOver, setDragOver] = useState(false); + // const [uploadEquip, setUploadEquip] = useState('drone'); + // const [uploadEquipNm, setUploadEquipNm] = useState('드론 (DJI M300 RTK)'); + // const [uploadMemo, setUploadMemo] = useState(''); const loadData = useCallback(async () => { setLoading(true); @@ -82,6 +96,15 @@ export function MediaManagement() { return () => document.removeEventListener('mousedown', handler); }, [showUpload]); + useEffect(() => { + if (!previewItem) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') setPreviewItem(null); + }; + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [previewItem]); + const filtered = mediaItems.filter((f) => { if (equipFilter !== 'all' && f.equipTpCd !== equipFilter) return false; if (typeFilter.size > 0) { @@ -164,6 +187,38 @@ export function MediaManagement() { } }; + const handleUploadSubmit = async () => { + if (!uploadFile || uploading) return; + setUploading(true); + try { + await uploadAerialMedia(uploadFile, { + // equipTpCd: uploadEquip, + // equipNm: uploadEquipNm, + // memo: uploadMemo, + }); + setShowUpload(false); + setUploadFile(null); + // setUploadMemo(''); + await loadData(); + } catch { + alert('업로드 실패: 다시 시도해주세요.'); + } finally { + setUploading(false); + } + }; + + const handleFileDrop = (e: React.DragEvent) => { + e.preventDefault(); + setDragOver(false); + const file = e.dataTransfer.files[0]; + if (file) setUploadFile(file); + }; + + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) setUploadFile(file); + }; + const droneCount = mediaItems.filter((f) => f.equipTpCd === 'drone').length; const planeCount = mediaItems.filter((f) => f.equipTpCd === 'plane').length; const satCount = mediaItems.filter((f) => f.equipTpCd === 'satellite').length; @@ -224,6 +279,12 @@ export function MediaManagement() { +
@@ -266,7 +327,7 @@ export function MediaManagement() { {/* File Table */}
-
+
@@ -332,56 +393,74 @@ export function MediaManagement() { ) : ( - sorted.map((f) => ( - toggleId(f.aerialMediaSn)} - className={`border-b border-stroke cursor-pointer transition-colors hover:bg-[rgba(255,255,255,0.02)] ${ - selectedIds.has(f.aerialMediaSn) ? 'bg-[rgba(6,182,212,0.06)]' : '' - }`} - > - - - - - - - - - - - + - - )) + toggleId(f.aerialMediaSn)} + onClick={(e) => e.stopPropagation()} + className="accent-primary-blue" + /> + + + + + + + + + + + + + ); + }) )}
e.stopPropagation()}> - toggleId(f.aerialMediaSn)} - className="accent-primary-blue" - /> - {equipIcon(f.equipTpCd)} - {f.acdntSn != null ? String(f.acdntSn) : '—'} - - {f.locDc ?? '—'} - - {f.fileNm} - - - {f.equipNm} - - - - {f.mediaTpCd === '영상' ? '🎬' : '📷'} {f.mediaTpCd} - - {formatDtm(f.takngDtm)}{f.fileSz ?? '—'}{f.resolution ?? '—'} e.stopPropagation()}> -
toggleId(f.aerialMediaSn)} > - {downloadingId === f.aerialMediaSn ? '⏳' : '📥'} - -
{equipIcon(f.equipTpCd)} + {f.acdntSn != null ? String(f.acdntSn) : '—'} + + {f.locDc ?? '—'} + + {f.fileNm} + + + {f.equipNm} + + + {isPhoto ? ( + + ) : ( + + 🎬 {f.mediaTpCd} + + )} + {formatDtm(f.takngDtm)}{f.fileSz ?? '—'}{f.resolution ?? '—'} + +
@@ -398,20 +477,20 @@ export function MediaManagement() { onClick={toggleAll} className="px-3 py-1.5 text-label-2 font-semibold rounded bg-bg-card border border-stroke text-fg-sub hover:bg-bg-surface-hover transition-colors font-korean" > - ☑ 전체선택 + 전체선택
@@ -457,28 +536,78 @@ export function MediaManagement() { className="bg-bg-surface border border-stroke rounded-md w-[480px] max-h-[80vh] overflow-y-auto p-6" >
- 📤 영상·사진 업로드 + 영상·사진 업로드
-
-
📁
-
- 파일을 드래그하거나 클릭하여 업로드 -
-
- JPG, TIFF, GeoTIFF, MP4, MOV 지원 · 최대 2GB -
+
{ + e.preventDefault(); + setDragOver(true); + }} + onDragLeave={() => setDragOver(false)} + onDrop={handleFileDrop} + onClick={() => fileInputRef.current?.click()} + className={`border-2 border-dashed rounded-md py-8 px-4 text-center mb-4 cursor-pointer transition-colors ${ + dragOver + ? 'border-color-accent bg-[rgba(6,182,212,0.06)]' + : uploadFile + ? 'border-[rgba(6,182,212,0.3)] bg-[rgba(6,182,212,0.04)]' + : 'border-stroke-light hover:border-[rgba(6,182,212,0.4)]' + }`} + > + + {uploadFile ? ( + <> +
+
+ {uploadFile.name} +
+
+ {(uploadFile.size / (1024 * 1024)).toFixed(2)} MB · 클릭하여 변경 +
+ + ) : ( + <> +
📁
+
+ 파일을 드래그하거나 클릭하여 업로드 +
+
+ JPG, TIFF, GeoTIFF, MP4, MOV 지원 · 최대 2GB +
+ + )}
-
+ {/*
- { + setUploadEquipNm(e.target.value); + const v = e.target.value; + if (v.startsWith('드론')) setUploadEquip('drone'); + else if (v.startsWith('유인')) setUploadEquip('plane'); + else if (v.startsWith('위성')) setUploadEquip('satellite'); + else setUploadEquip('drone'); + }} + > @@ -505,21 +634,88 @@ export function MediaManagement() {