diff --git a/.claude/workflow-version.json b/.claude/workflow-version.json index 87e0806..2019a88 100644 --- a/.claude/workflow-version.json +++ b/.claude/workflow-version.json @@ -1,7 +1,7 @@ { "applied_global_version": "1.6.1", - "applied_date": "2026-04-16", + "applied_date": "2026-04-17", "project_type": "react-ts", "gitea_url": "https://gitea.gc-si.dev", "custom_pre_commit": true -} \ No newline at end of file +} diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 93bb350..b020ed5 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -3,6 +3,7 @@ # commit-msg hook # Conventional Commits 형식 검증 (한/영 혼용 지원) #============================================================================== +export LC_ALL=en_US.UTF-8 2>/dev/null || export LC_ALL=C.UTF-8 2>/dev/null || true COMMIT_MSG_FILE="$1" COMMIT_MSG=$(cat "$COMMIT_MSG_FILE") diff --git a/CLAUDE.md b/CLAUDE.md index 1199823..eb09ccd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,31 +2,6 @@ 해양 오염 사고 대응 방제 운영 지원 시스템. 유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공한다. -## 🚨 절대 지침 (Absolute Rules) - -### 1. 신규 기능 설계/구현 전 develop 최신화 필수 - -신규 기능 설계나 구현을 시작하기 전, **반드시** 다음 절차를 사용자에게 권유하고 확인을 받을 것: - -1. `git fetch origin` 으로 원격 `develop` 최신 상태 확인 -2. `origin/develop`이 로컬 `develop`보다 앞서 있으면 → 로컬 `develop`을 최신화 (`git pull --ff-only` 또는 `git checkout -B develop origin/develop`) -3. 최신화된 `develop`에서 신규 브랜치 생성 (`git checkout -b /`) -4. 해당 브랜치에서 설계·구현 진행 - -> **이유**: 구 버전 develop 기반으로 작업 시 머지 충돌·중복 구현·사라진 코드 복원 등 위험. 최신 base에서 시작해야 MR 리뷰·릴리즈 흐름이 안전. - -### 2. 프론트엔드 구성 시 디자인 시스템 준수 필수 - -모든 프론트엔드 UI 구현은 **반드시** [docs/DESIGN-SYSTEM.md](docs/DESIGN-SYSTEM.md) 규칙을 준수할 것: - -- 시맨틱 토큰(`--bg-base`, `--fg-default`, `--color-accent` 등) 사용 — 축약형/하드코딩 금지 -- 폰트: `PretendardGOV` 단일 폰트 (4웨이트). `var(--font-korean)`, `var(--font-mono)` 경유 -- 다크/라이트 테마 전환 지원 (`data-theme` 속성 기반) -- Tailwind 컬러 키는 CSS 변수 참조 (`bg.base`, `fg.DEFAULT`, `color.accent`) -- 폰트 크기 토큰: `text-caption/body-2/body-1/title-4...` — 인라인 `fontSize` 금지 - -> **이유**: 디자인 일관성·테마 전환·접근성(대비) 확보. 토큰 외 값은 리팩토링 비용을 증가시킴. - - **타입**: react-ts 모노레포 (frontend + backend) - **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3 - **Backend**: Express 4 + PostgreSQL (pg) + TypeScript diff --git a/docs/COMMON-GUIDE.md b/docs/COMMON-GUIDE.md index 216383b..660504f 100644 --- a/docs/COMMON-GUIDE.md +++ b/docs/COMMON-GUIDE.md @@ -375,7 +375,7 @@ PUT, DELETE, PATCH 등 기타 메서드는 사용하지 않는다. ### 탭별 API 서비스 패턴 -각 탭은 `components/{탭명}/services/{탭명}Api.ts`에 API 함수를 정의한다. +각 탭은 `tabs/{탭명}/services/{탭명}Api.ts`에 API 함수를 정의한다. ```typescript // frontend/src/components/board/services/boardApi.ts @@ -1406,7 +1406,7 @@ frontend/src/ | +-- cn.ts className 조합 유틸리티 | +-- sanitize.ts XSS 방지/입력 살균 | +-- coordinates.ts 좌표 변환 유틸리티 -+-- components/ 탭별 패키지 (11개, MPA 컴포넌트 구조) ++-- tabs/ 탭별 패키지 (11개) | +-- {탭명}/ | +-- components/ 탭 뷰 컴포넌트 | +-- services/{탭명}Api.ts 탭별 API 서비스 diff --git a/docs/CRUD-API-GUIDE.md b/docs/CRUD-API-GUIDE.md index 6f17bff..050d50e 100644 --- a/docs/CRUD-API-GUIDE.md +++ b/docs/CRUD-API-GUIDE.md @@ -21,7 +21,7 @@ DB 설계부터 백엔드 구현, 프론트엔드 연동까지 End-to-End 패턴 ``` [Frontend] [Backend] [Database] -components/{탭}/services/{tab}Api.ts src/{domain}/{domain}Router.ts PostgreSQL 16 +tabs/{탭}/services/{tab}Api.ts src/{domain}/{domain}Router.ts PostgreSQL 16 Axios (withCredentials: true) requireAuth -> requirePermission --HTTP--> src/{domain}/{domain}Service.ts wingPool / authPool wingPool.query(SQL, params) --SQL--> diff --git a/docs/DEVELOPMENT-GUIDE.md b/docs/DEVELOPMENT-GUIDE.md index c3ef052..ff14f54 100644 --- a/docs/DEVELOPMENT-GUIDE.md +++ b/docs/DEVELOPMENT-GUIDE.md @@ -242,7 +242,7 @@ frontend/src/ │ ├── store/ authStore (Zustand), menuStore │ ├── types/ backtrack, boomLine, hns, navigation │ └── utils/ coordinates, geo, sanitize -└── components/ 탭 단위 패키지 (11개, MPA 컴포넌트 구조) +└── tabs/ 탭 단위 패키지 (11개) ├── prediction/ 확산 예측 ├── hns/ HNS 분석 ├── rescue/ 구조 시나리오 @@ -259,7 +259,7 @@ frontend/src/ **각 탭 내부 구조 패턴:** ``` -components/{탭명}/ +tabs/{탭명}/ ├── components/ UI 컴포넌트 │ ├── {Tab}View.tsx 메인 뷰 (App.tsx에서 라우팅) │ ├── {Tab}LeftPanel.tsx diff --git a/docs/INSTALL_GUIDE.md b/docs/INSTALL_GUIDE.md index 80c71b1..243e765 100755 --- a/docs/INSTALL_GUIDE.md +++ b/docs/INSTALL_GUIDE.md @@ -75,7 +75,7 @@ wing/ │ │ │ ├── store/ authStore, menuStore (Zustand) │ │ │ ├── types/ backtrack, boomLine, hns, navigation │ │ │ └── utils/ coordinates, geo, sanitize -│ │ └── components/ 탭 단위 패키지 (11개, MPA 구조) +│ │ └── tabs/ 탭 단위 패키지 (11개) │ │ ├── prediction/ 확산 예측 │ │ ├── hns/ HNS 분석 │ │ ├── rescue/ 구조 시나리오 diff --git a/docs/README.md b/docs/README.md index 503036a..79fffc3 100755 --- a/docs/README.md +++ b/docs/README.md @@ -66,7 +66,7 @@ wing/ │ │ ├── utils/ cn, coordinates, geo, sanitize │ │ ├── styles/ base.css, components.css, wing.css (@layer) │ │ └── constants/ featureIds.ts (FEATURE_ID 상수 체계) -│ └── components/ @components/ alias (11개 탭, MPA 구조) +│ └── tabs/ @components/ alias (11개 탭) │ ├── prediction/ 유류 확산 예측 │ ├── hns/ HNS 분석 │ ├── rescue/ 구조 시나리오 diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 61f5ffb..6f6c794 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -5,15 +5,7 @@ ## [Unreleased] ### 수정 -- 사건사고: MPA 리팩토링 누락 imports 정리 — IncidentsView `SplitPanelContent` 중복 import 제거, `fetchOilSpillSummary`를 `predictionApi`로 이관(이전 위치는 `api` 미임포트로 런타임 동작 불가), `AnalysisSelectModal`·`hnsDispersionLayers`의 `@tabs/`·`@common/components/` 구 경로를 신 경로로 정정 - -### 문서 -- MPA 컴포넌트 구조 반영: 개발 가이드·공통 가이드·CRUD 가이드·설치 가이드·docs/README의 `tabs/` 경로 예시를 `components/`로 정정 - -## [2026-04-17] - -### 문서 -- CLAUDE.md 절대 지침 추가 (develop 최신화, 디자인 시스템 준수) +- 빌드 에러 수정 - 타입 import 정리 및 미사용 코드 제거 ## [2026-04-16] diff --git a/frontend/src/common/utils/geo.ts b/frontend/src/common/utils/geo.ts index 22b21d1..177561a 100755 --- a/frontend/src/common/utils/geo.ts +++ b/frontend/src/common/utils/geo.ts @@ -217,8 +217,6 @@ export function generateAIBoomLines( const totalDist = haversineDistance(incident, centroid); // 입자 분산 폭 계산 (최종 시간 기준) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const perpBearing = (mainBearing + 90) % 360; let maxSpread = 0; for (const p of finalPoints) { const bearing = computeBearing(incident, p); diff --git a/frontend/src/common/utils/sanitize.ts b/frontend/src/common/utils/sanitize.ts index a880ba5..87b4eb9 100755 --- a/frontend/src/common/utils/sanitize.ts +++ b/frontend/src/common/utils/sanitize.ts @@ -31,45 +31,6 @@ export function stripHtmlTags(html: string): string { return html.replace(/<[^>]*>/g, ''); } -/** - * 안전한 HTML 살균 - * 허용된 태그만 남기고 위험한 태그/속성 제거 - */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ALLOWED_TAGS = new Set([ - 'b', - 'i', - 'u', - 'strong', - 'em', - 'br', - 'p', - 'span', - 'div', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'ul', - 'ol', - 'li', - 'a', - 'img', - 'table', - 'thead', - 'tbody', - 'tr', - 'th', - 'td', - 'sup', - 'sub', - 'hr', - 'blockquote', - 'pre', - 'code', -]); const DANGEROUS_ATTRS = /\s*on\w+\s*=|javascript\s*:|vbscript\s*:|expression\s*\(/gi; diff --git a/frontend/src/components/admin/components/PermissionsPanel.tsx b/frontend/src/components/admin/components/PermissionsPanel.tsx index aa1d016..c627c34 100644 --- a/frontend/src/components/admin/components/PermissionsPanel.tsx +++ b/frontend/src/components/admin/components/PermissionsPanel.tsx @@ -2,6 +2,11 @@ import { useState, useEffect, useCallback } from 'react'; import { fetchRoles, fetchPermTree, + updatePermissionsApi, + createRoleApi, + deleteRoleApi, + updateRoleApi, + updateRoleDefaultApi, type RoleWithPermissions, type PermTreeNode, } from '@common/services/authApi'; diff --git a/frontend/src/components/aerial/components/OilAreaAnalysis.tsx b/frontend/src/components/aerial/components/OilAreaAnalysis.tsx index c273bc6..039ebdf 100644 --- a/frontend/src/components/aerial/components/OilAreaAnalysis.tsx +++ b/frontend/src/components/aerial/components/OilAreaAnalysis.tsx @@ -89,7 +89,7 @@ export function OilAreaAnalysis() { processedFilesRef.current.add(file); exifr - .parse(file, { gps: true, exif: true, ifd0: true, translateValues: false }) + .parse(file, { gps: true, exif: true, ifd0: true, translateValues: false } as unknown as Parameters[1]) .then((exif) => { const info: ImageExif = { lat: exif?.latitude ?? null, diff --git a/frontend/src/components/aerial/components/contents/AoiPanel.tsx b/frontend/src/components/aerial/components/contents/AoiPanel.tsx index 607d0f0..9f8ef20 100644 --- a/frontend/src/components/aerial/components/contents/AoiPanel.tsx +++ b/frontend/src/components/aerial/components/contents/AoiPanel.tsx @@ -300,7 +300,7 @@ export function AoiPanel() { getLineWidth: (d: MonitorZone) => (selectedZone === d.id ? 3 : 1.5), lineWidthUnits: 'pixels', pickable: true, - onClick: ({ object }: { object: MonitorZone }) => { + onClick: ({ object }: { object?: MonitorZone }) => { if (object && !isDrawing) setSelectedZone(object.id === selectedZone ? null : object.id); }, diff --git a/frontend/src/components/aerial/components/contents/DetectPanel.tsx b/frontend/src/components/aerial/components/contents/DetectPanel.tsx index a576518..9574798 100644 --- a/frontend/src/components/aerial/components/contents/DetectPanel.tsx +++ b/frontend/src/components/aerial/components/contents/DetectPanel.tsx @@ -238,7 +238,7 @@ export function DetectPanel() { lineWidthMinPixels: 2, stroked: true, pickable: true, - onClick: ({ object }: { object: VesselDetection }) => { + onClick: ({ object }: { object?: VesselDetection }) => { if (object) setSelectedId(object.id === selectedId ? null : object.id); }, updateTriggers: { getRadius: [selectedId] }, diff --git a/frontend/src/components/common/layout/TopBar.tsx b/frontend/src/components/common/layout/TopBar.tsx index 5dd452c..838cc97 100644 --- a/frontend/src/components/common/layout/TopBar.tsx +++ b/frontend/src/components/common/layout/TopBar.tsx @@ -247,17 +247,17 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) { {mapTypes.map((item) => ( diff --git a/frontend/src/components/common/map/BaseMap.tsx b/frontend/src/components/common/map/BaseMap.tsx index b140a9b..4464e38 100644 --- a/frontend/src/components/common/map/BaseMap.tsx +++ b/frontend/src/components/common/map/BaseMap.tsx @@ -273,12 +273,11 @@ export function BaseMap({ )} > {/* 공통 오버레이 */} diff --git a/frontend/src/components/common/map/HydrParticleOverlay.tsx b/frontend/src/components/common/map/HydrParticleOverlay.tsx index 8b7b23f..3e1d4eb 100644 --- a/frontend/src/components/common/map/HydrParticleOverlay.tsx +++ b/frontend/src/components/common/map/HydrParticleOverlay.tsx @@ -15,11 +15,12 @@ const PI_4 = Math.PI / 4; const FADE_ALPHA = 0.02; // 프레임당 페이드량 (낮을수록 긴 꼬리) export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayProps) { - const { current: map } = useMap(); - const animRef = useRef(); + const { current: mapRef } = useMap(); + const animRef = useRef(undefined); useEffect(() => { - if (!map || !hydrStep) return; + if (!mapRef || !hydrStep) return; + const map = mapRef; const container = map.getContainer(); const canvas = document.createElement('canvas'); @@ -212,7 +213,7 @@ export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayPro map.off('move', onMove); canvas.remove(); }; - }, [map, hydrStep]); + }, [mapRef, hydrStep]); return null; } diff --git a/frontend/src/components/common/map/MapView.tsx b/frontend/src/components/common/map/MapView.tsx index 9b7d423..97d4530 100644 --- a/frontend/src/components/common/map/MapView.tsx +++ b/frontend/src/components/common/map/MapView.tsx @@ -17,7 +17,7 @@ import type { SensitiveResource } from '@interfaces/prediction/PredictionInterfa import type { HydrDataStep, SensitiveResourceFeatureCollection, -} from '@components/prediction/services/predictionApi'; +} from '@interfaces/prediction/PredictionInterface'; import HydrParticleOverlay from './HydrParticleOverlay'; import { TimelineControl } from './TimelineControl'; import type { BoomLine, BoomLineCoord } from '@/types/boomLine'; @@ -1331,8 +1331,9 @@ export function MapView({ zoom: zoom, }} mapStyle={currentMapStyle} - className="w-full h-full" style={{ + width: '100%', + height: '100%', cursor: isSelectingLocation || drawAnalysisMode !== null || measureMode !== null ? 'crosshair' @@ -1340,7 +1341,7 @@ export function MapView({ }} onClick={handleMapClick} attributionControl={false} - preserveDrawingBuffer={true} + {...({ preserveDrawingBuffer: true } as Record)} > {/* 지도 캡처 셋업 */} {mapCaptureRef && } diff --git a/frontend/src/components/hns/components/HNSSubstanceView.tsx b/frontend/src/components/hns/components/HNSSubstanceView.tsx index 0c565f9..b97bbf2 100755 --- a/frontend/src/components/hns/components/HNSSubstanceView.tsx +++ b/frontend/src/components/hns/components/HNSSubstanceView.tsx @@ -315,12 +315,6 @@ export function HNSSubstanceView() { const [activeTab, setActiveTab] = useState(0); const [selectedCategory, setSelectedCategory] = useState('all'); const [searchQuery, setSearchQuery] = useState(''); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [detailSearchName, setDetailSearchName] = useState(''); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [detailSearchCas, setDetailSearchCas] = useState(''); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [detailSearchSebc, setDetailSearchSebc] = useState('전체 거동분류'); /* Panel 3: 물질 상세검색 state */ const [hmsSearchType, setHmsSearchType] = useState< 'all' | 'abbr' | 'korName' | 'engName' | 'cas' | 'un' @@ -439,18 +433,6 @@ ${styles} return matchCategory && matchSearch; }); - /* Detail search filter for Panel 3 (legacy) */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const detailFiltered = substances.filter((s) => { - const qName = detailSearchName.toLowerCase(); - const qCas = detailSearchCas.toLowerCase(); - const matchName = - !qName || s.name.toLowerCase().includes(qName) || s.nameEn.toLowerCase().includes(qName); - const matchCas = !qCas || s.casNumber.includes(qCas); - const matchSebc = - detailSearchSebc === '전체 거동분류' || s.sebc.includes(detailSearchSebc.split(' ')[0]); - return matchName && matchCas && matchSebc; - }); /* Panel 3: HNS API 기반 검색 결과 */ const HMS_PER_PAGE = 10; diff --git a/frontend/src/components/hns/components/HNSView.tsx b/frontend/src/components/hns/components/HNSView.tsx index ec33471..a1ef981 100644 --- a/frontend/src/components/hns/components/HNSView.tsx +++ b/frontend/src/components/hns/components/HNSView.tsx @@ -93,7 +93,7 @@ export function HNSView() { const [inputParams, setInputParams] = useState(null); const [loadedParams, setLoadedParams] = useState | null>(null); const hasRunOnce = useRef(false); // 최초 실행 여부 - const mapCaptureRef = useRef<(() => string | null) | null>(null); + const mapCaptureRef = useRef<(() => Promise) | null>(null); const handleReset = useCallback(() => { setDispersionResult(null); @@ -376,7 +376,7 @@ export function HNSView() { /** 분석 결과 저장 */ const handleSave = async () => { - if (!dispersionResult || !inputParams || !computedResult) { + if (!dispersionResult || !inputParams || !computedResult || !incidentCoord) { alert('저장할 분석 결과가 없습니다. 먼저 예측을 실행해주세요.'); return; } @@ -672,11 +672,11 @@ export function HNSView() { }; /** 보고서 생성 — 실 데이터 수집 + 지도 캡처 후 탭 이동 */ - const handleOpenReport = () => { + const handleOpenReport = async () => { try { let mapImage: string | null = null; try { - mapImage = mapCaptureRef.current?.() ?? null; + mapImage = (await mapCaptureRef.current?.()) ?? null; } catch { /* canvas capture 실패 무시 */ } diff --git a/frontend/src/components/incidents/components/AnalysisSelectModal.tsx b/frontend/src/components/incidents/components/AnalysisSelectModal.tsx index 5657172..a228f82 100644 --- a/frontend/src/components/incidents/components/AnalysisSelectModal.tsx +++ b/frontend/src/components/incidents/components/AnalysisSelectModal.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from 'react'; import { fetchPredictionAnalyses } from '@components/prediction/services/predictionApi'; -import type { PredictionAnalysis } from '@interfaces/prediction/PredictionInterface'; +import type { PredictionAnalysis } from '@components/prediction/services/predictionApi'; import { fetchHnsAnalyses } from '@components/hns/services/hnsApi'; import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface'; import { fetchRescueOps } from '@components/rescue/services/rescueApi'; diff --git a/frontend/src/components/incidents/components/IncidentsRightPanel.tsx b/frontend/src/components/incidents/components/IncidentsRightPanel.tsx index 901e8b5..e46cc9a 100644 --- a/frontend/src/components/incidents/components/IncidentsRightPanel.tsx +++ b/frontend/src/components/incidents/components/IncidentsRightPanel.tsx @@ -70,54 +70,6 @@ interface AnalysisItem { checked: boolean; } -/* ── 카테고리 → 이모지 매핑 (prediction LeftPanel의 CATEGORY_ICON_MAP 기반, 미사용 보존) ── */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const CATEGORY_ICON: Record = { - 어장정보: '🐟', - 양식장: '🦪', - 양식어업: '🦪', - 어류양식장: '🐟', - 패류양식장: '🦪', - 해조류양식장: '🌿', - 가두리양식장: '🔲', - 갑각류양식장: '🦐', - 기타양식장: '📦', - 영세어업: '🎣', - 유어장: '🎣', - 수산시장: '🐟', - 인공어초: '🪸', - 암초: '🪨', - 침선: '🚢', - 해수욕장: '🏖', - 갯바위낚시: '🪨', - 선상낚시: '🚤', - 마리나항: '⛵', - 무역항: '🚢', - 연안항: '⛵', - 국가어항: '⚓', - 지방어항: '⚓', - 어항: '⚓', - 항만구역: '⚓', - 항로: '🚢', - 정박지: '⛵', - 항로표지: '🔴', - 해수취수시설: '💧', - '취수구·배수구': '🚰', - LNG: '⚡', - 발전소: '🔌', - '발전소·산단': '🏭', - 임해공단: '🏭', - 저유시설: '🛢', - '해저케이블·배관': '🔌', - 갯벌: '🪨', - 해안선_ESI: '🏖', - 보호지역: '🛡', - 해양보호구역: '🌿', - 철새도래지: '🐦', - 습지보호구역: '🏖', - 보호종서식지: '🐢', - '보호종 서식지': '🐢', -}; /* ── 헬퍼: 활성 모델 문자열 ─────────────────────── */ function getActiveModels(p: PredictionAnalysis): string { diff --git a/frontend/src/components/incidents/components/IncidentsView.tsx b/frontend/src/components/incidents/components/IncidentsView.tsx index 54d0b6e..c8806ef 100644 --- a/frontend/src/components/incidents/components/IncidentsView.tsx +++ b/frontend/src/components/incidents/components/IncidentsView.tsx @@ -14,11 +14,14 @@ import { getVesselCacheStatus, type VesselCacheStatus } from '@common/services/v import { IncidentsLeftPanel } from './IncidentsLeftPanel'; import { IncidentsRightPanel, type ViewMode, type AnalysisSection } from './IncidentsRightPanel'; import { fetchIncidents } from '../services/incidentsApi'; -import type { IncidentCompat } from '@interfaces/incidents/IncidentsInterface'; +import type { Incident, IncidentCompat } from '@interfaces/incidents/IncidentsInterface'; import { fetchHnsAnalyses } from '@components/hns/services/hnsApi'; import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface'; import { buildHnsDispersionLayers } from '../utils/hnsDispersionLayers'; -import { fetchAnalysisTrajectory, fetchOilSpillSummary } from '@components/prediction/services/predictionApi'; +import { + fetchAnalysisTrajectory, + fetchOilSpillSummary, +} from '@components/prediction/services/predictionApi'; import type { TrajectoryResponse, SensitiveResourceFeatureCollection, @@ -512,8 +515,8 @@ export function IncidentsView() { layers.push( new PathLayer({ id: `traj-path-${pathId}`, - data: [{ path: sorted.map((p) => [p.lon, p.lat]) }], - getPath: (d: { path: number[][] }) => d.path, + data: [{ path: sorted.map((p) => [p.lon, p.lat] as [number, number]) }], + getPath: (d: { path: [number, number][] }) => d.path, getColor: [...color, 230] as [number, number, number, number], getWidth: 2, widthMinPixels: 2, @@ -572,7 +575,8 @@ export function IncidentsView() { if (filtered.features.length === 0) return null; return new GeoJsonLayer({ id: 'incidents-sensitive-geojson', - data: filtered, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: filtered as any, pickable: false, stroked: true, filled: true, @@ -1622,8 +1626,7 @@ function SplitResultMap({ } /* ── (미사용) 분석별 SVG placeholder — 참고용 보존 ────────────────── */ -// eslint-disable-next-line @typescript-eslint/no-unused-vars -function SplitVisualization({ slotKey, color }: { slotKey: SplitSlotKey; color: string }) { +export function SplitVisualization({ slotKey, color }: { slotKey: SplitSlotKey; color: string }) { if (slotKey === 'oil') { return ( diff --git a/frontend/src/components/incidents/components/MediaModal.tsx b/frontend/src/components/incidents/components/MediaModal.tsx index c40ec28..d6fe6cb 100644 --- a/frontend/src/components/incidents/components/MediaModal.tsx +++ b/frontend/src/components/incidents/components/MediaModal.tsx @@ -5,7 +5,8 @@ import { fetchIncidentAerialMedia, getMediaImageUrl, } from '../services/incidentsApi'; -import type { MediaInfo, AerialMediaItem } from '@interfaces/incidents/IncidentsInterface'; +import type { MediaInfo } from '@interfaces/incidents/IncidentsInterface'; +import type { AerialMediaItem } from '@interfaces/aerial/AerialInterface'; type MediaTab = 'all' | 'photo' | 'video' | 'satellite' | 'cctv'; diff --git a/frontend/src/components/incidents/utils/hnsDispersionLayers.ts b/frontend/src/components/incidents/utils/hnsDispersionLayers.ts index b9ad740..cb88aa2 100644 --- a/frontend/src/components/incidents/utils/hnsDispersionLayers.ts +++ b/frontend/src/components/incidents/utils/hnsDispersionLayers.ts @@ -8,8 +8,17 @@ import { BitmapLayer, ScatterplotLayer } from '@deck.gl/layers'; import { computeDispersion } from '@components/hns/utils/dispersionEngine'; import { getSubstanceToxicity } from '@components/hns/utils/toxicityData'; import { hexToRgba } from '@components/common/map/mapUtils'; -import type { HnsAnalysisItem, MeteoParams, SourceParams, SimParams } from '@interfaces/hns/HnsInterface'; -import type { DispersionModel, AlgorithmType, StabilityClass } from '@types/hns/HnsType'; +import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface'; +import type { + MeteoParams, + SourceParams, + SimParams, +} from '@interfaces/hns/HnsInterface'; +import type { + DispersionModel, + AlgorithmType, + StabilityClass, +} from '@/types/hns/HnsType'; // MapView와 동일한 색상 정지점 const COLOR_STOPS: [number, number, number, number][] = [ diff --git a/frontend/src/components/prediction/components/OilSpillView.tsx b/frontend/src/components/prediction/components/OilSpillView.tsx index 3c70ccd..a84ed54 100644 --- a/frontend/src/components/prediction/components/OilSpillView.tsx +++ b/frontend/src/components/prediction/components/OilSpillView.tsx @@ -776,6 +776,8 @@ export function OilSpillView() { analyst: '', officeName: '', acdntSttsCd: 'ACTIVE', + predRunSn: null, + runDtm: null, }); }, []); diff --git a/frontend/src/components/prediction/components/contents/KospsPanel.tsx b/frontend/src/components/prediction/components/contents/KospsPanel.tsx index d432e94..8443314 100644 --- a/frontend/src/components/prediction/components/contents/KospsPanel.tsx +++ b/frontend/src/components/prediction/components/contents/KospsPanel.tsx @@ -524,7 +524,7 @@ export function KospsPanel() { {/* Akima 수심 보간 & NGSST 수온 */}
-
+
🗺️ Akima 수심 보간 기법
@@ -539,7 +539,7 @@ export function KospsPanel() { (i≤5, i+j≤5)
-
+
🌡️ NGSST 실시간 수온자료
diff --git a/frontend/src/components/prediction/components/contents/RoadmapPanel.tsx b/frontend/src/components/prediction/components/contents/RoadmapPanel.tsx index ef3f2c3..a9c7bc1 100644 --- a/frontend/src/components/prediction/components/contents/RoadmapPanel.tsx +++ b/frontend/src/components/prediction/components/contents/RoadmapPanel.tsx @@ -4,7 +4,7 @@ export function RoadmapPanel() { return (
-
+
현재 모델 한계
-
+
발전 방향
{[ diff --git a/frontend/src/components/prediction/services/predictionApi.ts b/frontend/src/components/prediction/services/predictionApi.ts index 1cf6d2b..140fde7 100644 --- a/frontend/src/components/prediction/services/predictionApi.ts +++ b/frontend/src/components/prediction/services/predictionApi.ts @@ -4,14 +4,29 @@ import type { PredictionDetail, BacktrackResult, TrajectoryResponse, - OilSpillSummaryResponse, SensitiveResourceCategory, + SensitiveResourceFeature, SensitiveResourceFeatureCollection, SpreadParticlesGeojson, + HydrDataStep, + OilSpillSummaryResponse, ImageAnalyzeResult, GscAccidentListItem, } from '@interfaces/prediction/PredictionInterface'; +export type { + PredictionAnalysis, + PredictionDetail, + BacktrackResult, + TrajectoryResponse, + SensitiveResourceCategory, + SensitiveResourceFeature, + SensitiveResourceFeatureCollection, + SpreadParticlesGeojson, + HydrDataStep, + OilSpillSummaryResponse, +}; + export const fetchPredictionAnalyses = async (params?: { search?: string; acdntSn?: number; @@ -64,17 +79,6 @@ export const fetchAnalysisTrajectory = async ( return response.data; }; -export const fetchOilSpillSummary = async ( - acdntSn: number, - predRunSn?: number, -): Promise => { - const response = await api.get( - `/prediction/analyses/${acdntSn}/oil-summary`, - predRunSn != null ? { params: { predRunSn } } : undefined, - ); - return response.data; -}; - export const fetchSensitiveResources = async ( acdntSn: number, ): Promise => { @@ -134,3 +138,18 @@ export const fetchGscAccidents = async (): Promise => { const response = await api.get('/gsc/accidents'); return response.data; }; + +// ============================================================ +// 유류 확산 요약 +// ============================================================ + +export const fetchOilSpillSummary = async ( + acdntSn: number, + predRunSn?: number, +): Promise => { + const response = await api.get( + `/prediction/analyses/${acdntSn}/oil-summary`, + predRunSn != null ? { params: { predRunSn } } : undefined, + ); + return response.data; +}; diff --git a/frontend/src/components/reports/components/OilSpillReportTemplate.tsx b/frontend/src/components/reports/components/OilSpillReportTemplate.tsx index 6c8996c..7818ad7 100644 --- a/frontend/src/components/reports/components/OilSpillReportTemplate.tsx +++ b/frontend/src/components/reports/components/OilSpillReportTemplate.tsx @@ -27,7 +27,7 @@ export function createEmptyReport(jurisdiction?: Jurisdiction): OilSpillReportDa author: '', reportType: '예측보고서', analysisCategory: '', - jurisdiction: jurisdiction || '', + jurisdiction: jurisdiction ?? '남해청', status: '수행중', incident: { name: '', diff --git a/frontend/src/components/reports/components/ReportsView.tsx b/frontend/src/components/reports/components/ReportsView.tsx index c226be4..f02da36 100644 --- a/frontend/src/components/reports/components/ReportsView.tsx +++ b/frontend/src/components/reports/components/ReportsView.tsx @@ -1,9 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; -import { - OilSpillReportTemplate, - type OilSpillReportData, - type Jurisdiction, -} from './OilSpillReportTemplate'; +import { OilSpillReportTemplate } from './OilSpillReportTemplate'; +import type { OilSpillReportData } from '@interfaces/reports/ReportsInterface'; +import type { Jurisdiction } from '@/types/reports/ReportsType'; import { loadReportsFromApi, loadReportDetail, deleteReportApi } from '../services/reportsApi'; import { useSubMenu } from '@common/hooks/useSubMenu'; import { templateTypes } from './reportTypes'; diff --git a/frontend/src/components/reports/components/contents/SensitiveResourceMapSection.tsx b/frontend/src/components/reports/components/contents/SensitiveResourceMapSection.tsx index 7b70c0d..73f4a98 100644 --- a/frontend/src/components/reports/components/contents/SensitiveResourceMapSection.tsx +++ b/frontend/src/components/reports/components/contents/SensitiveResourceMapSection.tsx @@ -230,7 +230,7 @@ export function SensitiveResourceMapSection({ center: [127.5, 35.5], zoom: 8, preserveDrawingBuffer: true, - }); + } as maplibregl.MapOptions); mapRef.current = map; map.on('load', () => { // 확산 파티클 — sensitive 레이어 아래 (예측 탭과 동일한 색상 로직) diff --git a/frontend/src/components/reports/components/contents/SensitivityMapSection.tsx b/frontend/src/components/reports/components/contents/SensitivityMapSection.tsx index eac9157..e4335d6 100644 --- a/frontend/src/components/reports/components/contents/SensitivityMapSection.tsx +++ b/frontend/src/components/reports/components/contents/SensitivityMapSection.tsx @@ -62,7 +62,7 @@ export function SensitivityMapSection({ center: [127.5, 35.5], zoom: 8, preserveDrawingBuffer: true, - }); + } as maplibregl.MapOptions); mapRef.current = map; map.on('load', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -104,7 +104,7 @@ export function SensitivityMapSection({ // fitBounds const coords: [number, number][] = []; displayGeojson.features.forEach((f) => { - const geom = (f as { geometry: { type: string; coordinates: unknown } }).geometry; + const geom = (f as unknown as { geometry: { type: string; coordinates: unknown } }).geometry; if (geom.type === 'Point') coords.push(geom.coordinates as [number, number]); else if (geom.type === 'Polygon') (geom.coordinates as [number, number][][])[0]?.forEach((c) => coords.push(c)); diff --git a/frontend/src/components/rescue/components/RescueScenarioView.tsx b/frontend/src/components/rescue/components/RescueScenarioView.tsx index ee2b93e..b6af334 100644 --- a/frontend/src/components/rescue/components/RescueScenarioView.tsx +++ b/frontend/src/components/rescue/components/RescueScenarioView.tsx @@ -4,6 +4,7 @@ import type { RescueOpsItem, RescueScenarioItem, RescueScenario, + ChartDataItem, } from '@interfaces/rescue/RescueInterface'; import type { Severity } from '@/types/rescue/RescueType'; import { ScenarioMapOverlay } from './contents/ScenarioMapOverlay'; diff --git a/frontend/src/components/rescue/services/rescueApi.ts b/frontend/src/components/rescue/services/rescueApi.ts index 72db8d4..1e3a2a5 100644 --- a/frontend/src/components/rescue/services/rescueApi.ts +++ b/frontend/src/components/rescue/services/rescueApi.ts @@ -9,6 +9,7 @@ export async function fetchRescueOps(params?: { sttsCd?: string; acdntTpCd?: string; search?: string; + acdntSn?: number; }): Promise { const response = await api.get('/rescue/ops', { params }); return response.data; diff --git a/frontend/src/components/scat/components/PreScatView.tsx b/frontend/src/components/scat/components/PreScatView.tsx index a1feb31..dcc627f 100644 --- a/frontend/src/components/scat/components/PreScatView.tsx +++ b/frontend/src/components/scat/components/PreScatView.tsx @@ -34,9 +34,6 @@ export function PreScatView() { const [popupData, setPopupData] = useState(null); const [panelDetail, setPanelDetail] = useState(null); const [panelLoading, setPanelLoading] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [timelineIdx, setTimelineIdx] = useState(6); - // 초기 관할청 목록 로딩 useEffect(() => { let cancelled = false; diff --git a/frontend/src/components/scat/components/ScatLeftPanel.tsx b/frontend/src/components/scat/components/ScatLeftPanel.tsx index 184ced7..faac6ea 100644 --- a/frontend/src/components/scat/components/ScatLeftPanel.tsx +++ b/frontend/src/components/scat/components/ScatLeftPanel.tsx @@ -113,8 +113,6 @@ function SegRow( function ScatLeftPanel({ segments, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - zones, jurisdictions, offices, selectedOffice, @@ -125,8 +123,6 @@ function ScatLeftPanel({ jurisdictionFilter, onJurisdictionChange, areaFilter, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onAreaChange, phaseFilter, onPhaseChange, statusFilter, diff --git a/frontend/src/components/weather/hooks/useWeatherData.ts b/frontend/src/components/weather/hooks/useWeatherData.ts index fef6703..4c4ae08 100644 --- a/frontend/src/components/weather/hooks/useWeatherData.ts +++ b/frontend/src/components/weather/hooks/useWeatherData.ts @@ -24,6 +24,7 @@ export interface EnrichedWeatherStation extends WeatherStation { }; pressure: number; visibility: number; + salinity?: number; } /** diff --git a/frontend/src/components/weather/services/weatherApi.ts b/frontend/src/components/weather/services/weatherApi.ts index fc656cf..2d65c62 100644 --- a/frontend/src/components/weather/services/weatherApi.ts +++ b/frontend/src/components/weather/services/weatherApi.ts @@ -45,7 +45,7 @@ export async function getUltraShortForecast( // 데이터를 시간대별로 그룹화 const forecasts: WeatherForecastData[] = []; - const grouped = new Map>(); + const grouped = new Map(); items.forEach((item: Record) => { const key = `${item.fcstDate}-${item.fcstTime}`; @@ -61,10 +61,11 @@ export async function getUltraShortForecast( waveHeight: 0, precipitation: 0, humidity: 0, - }); + } as WeatherForecastData); } const forecast = grouped.get(key); + if (!forecast) return; // 카테고리별 값 매핑 switch (item.category) { diff --git a/frontend/src/components/weather/services/weatherService.ts b/frontend/src/components/weather/services/weatherService.ts index 8e935f9..82ba36b 100644 --- a/frontend/src/components/weather/services/weatherService.ts +++ b/frontend/src/components/weather/services/weatherService.ts @@ -207,19 +207,3 @@ export function windDirectionToText(degree: number): string { const index = Math.round((degree % 360) / 22.5) % 16; return directions[index]; } - -// 해상 기상 정보 (Mock - 실제로는 해양기상청 API 사용) -// eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function getMarineWeather(lat: number, lng: number) { - // TODO: 해양기상청 API 연동 - // 현재는 Mock 데이터 반환 - return { - waveHeight: 1.2, // 파고 (m) - waveDirection: 135, // 파향 (도) - wavePeriod: 5.5, // 주기 (초) - seaTemperature: 12.5, // 수온 (°C) - currentSpeed: 0.3, // 해류속도 (m/s) - currentDirection: 180, // 해류방향 (도) - visibility: 15, // 시정 (km) - }; -} diff --git a/frontend/src/interfaces/hns/HnsInterface.ts b/frontend/src/interfaces/hns/HnsInterface.ts index df63d92..ecf551c 100644 --- a/frontend/src/interfaces/hns/HnsInterface.ts +++ b/frontend/src/interfaces/hns/HnsInterface.ts @@ -202,7 +202,7 @@ export interface HNSInputParams { /** HNS 분석 — 재계산 모달 입력 파라미터 */ export interface RecalcParams { substance: string; - releaseType: '연속 유출' | '순간 유출' | '밀도가스 유출'; + releaseType: ReleaseType; emissionRate: number; totalRelease: number; algorithm: string; diff --git a/frontend/src/interfaces/incidents/IncidentsInterface.ts b/frontend/src/interfaces/incidents/IncidentsInterface.ts index 1e778f9..59e709f 100644 --- a/frontend/src/interfaces/incidents/IncidentsInterface.ts +++ b/frontend/src/interfaces/incidents/IncidentsInterface.ts @@ -29,6 +29,8 @@ export interface IncidentListItem { spilUnitCd: string | null; fcstHr: number | null; hasPredCompleted: boolean; + hasHnsCompleted: boolean; + hasRescueCompleted: boolean; mediaCnt: number; hasImgAnalysis: boolean; } diff --git a/frontend/src/pages/design/ColorPaletteContent.tsx b/frontend/src/pages/design/ColorPaletteContent.tsx index 54d0ff4..12f5576 100644 --- a/frontend/src/pages/design/ColorPaletteContent.tsx +++ b/frontend/src/pages/design/ColorPaletteContent.tsx @@ -10,23 +10,6 @@ interface ColorStep { color: string; } -interface Marker { - step: number; - label: string; -} - -interface ContrastRating { - step: number; - rating: string; -} - -interface ColorScaleBarProps { - steps: ColorStep[]; - markers?: Marker[]; - contrastRatings?: ContrastRating[]; - darkBg?: boolean; - isDark: boolean; -} interface ColorToken { name: string; @@ -448,117 +431,6 @@ const ChipRow = ({ hex, role, gray, d, subdued = false }: ChipRowProps) => (
); -// ---------- 내부 컴포넌트: ColorScaleBar ---------- -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ColorScaleBar = ({ - steps, - markers, - contrastRatings, - darkBg = false, - isDark, -}: ColorScaleBarProps) => { - const badgeBg = isDark ? '#374151' : '#374151'; - const badgeText = isDark ? '#e5e7eb' : '#fff'; - - const getContrastRating = (step: number): string | undefined => { - return contrastRatings?.find((r) => r.step === step)?.rating; - }; - - const getMarker = (step: number): Marker | undefined => { - return markers?.find((m) => m.step === step); - }; - - return ( -
- {/* 색상 바 */} -
- {steps.map(({ step, color }, idx) => { - const isFirst = idx === 0; - const isLast = idx === steps.length - 1; - const textColor = step < 50 ? (darkBg ? '#e2e8f0' : '#1e293b') : '#e2e8f0'; - - const rating = getContrastRating(step); - - return ( -
- {/* 상단: 단계 번호 */} - {step} - {/* 하단: 접근성 등급 */} - {rating && ( - - {rating} - - )} -
- ); - })} -
- - {/* 마커 행 */} - {markers && markers.length > 0 && ( -
- {steps.map(({ step }, idx) => { - const marker = getMarker(step); - const pct = (idx / (steps.length - 1)) * 100; - - if (!marker) return null; - - return ( -
- {/* 점선 */} -
- {/* 뱃지 */} - - {marker.label} - -
- ); - })} -
- )} -
- ); -}; - // ---------- 내부 컴포넌트: DualModeSection ---------- interface DualModeSectionProps { @@ -570,8 +442,7 @@ interface DualModeSectionProps { darkSpecs: React.ReactNode; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const DualModeSection = ({ +export const DualModeSection = ({ lightBg = '#F5F5F5', darkBg = '#121418', lightContent, @@ -618,8 +489,7 @@ interface ColorSpecRowProps { dark?: boolean; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const ColorSpecRow = ({ role, gray, hex, dark = false }: ColorSpecRowProps) => ( +export const ColorSpecRow = ({ role, gray, hex, dark = false }: ColorSpecRowProps) => (