fix: 빌드 에러 수정 - 타입 import 정리 및 미사용 코드 제거
This commit is contained in:
부모
e31cb9b764
커밋
c5dc5c60c5
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"applied_global_version": "1.6.1",
|
"applied_global_version": "1.6.1",
|
||||||
"applied_date": "2026-04-16",
|
"applied_date": "2026-04-17",
|
||||||
"project_type": "react-ts",
|
"project_type": "react-ts",
|
||||||
"gitea_url": "https://gitea.gc-si.dev",
|
"gitea_url": "https://gitea.gc-si.dev",
|
||||||
"custom_pre_commit": true
|
"custom_pre_commit": true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
# commit-msg hook
|
# commit-msg hook
|
||||||
# Conventional Commits 형식 검증 (한/영 혼용 지원)
|
# 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_FILE="$1"
|
||||||
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
|
||||||
|
|||||||
25
CLAUDE.md
25
CLAUDE.md
@ -2,31 +2,6 @@
|
|||||||
|
|
||||||
해양 오염 사고 대응 방제 운영 지원 시스템. 유류/HNS 확산 예측, 역추적 분석, 구조 시나리오, 항공 방제, 자산 관리, SCAT 조사, 기상/해상 정보를 통합 제공한다.
|
해양 오염 사고 대응 방제 운영 지원 시스템. 유류/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 <type>/<name>`)
|
|
||||||
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)
|
- **타입**: react-ts 모노레포 (frontend + backend)
|
||||||
- **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
|
- **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
|
||||||
- **Backend**: Express 4 + PostgreSQL (pg) + TypeScript
|
- **Backend**: Express 4 + PostgreSQL (pg) + TypeScript
|
||||||
|
|||||||
@ -375,7 +375,7 @@ PUT, DELETE, PATCH 등 기타 메서드는 사용하지 않는다.
|
|||||||
|
|
||||||
### 탭별 API 서비스 패턴
|
### 탭별 API 서비스 패턴
|
||||||
|
|
||||||
각 탭은 `components/{탭명}/services/{탭명}Api.ts`에 API 함수를 정의한다.
|
각 탭은 `tabs/{탭명}/services/{탭명}Api.ts`에 API 함수를 정의한다.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// frontend/src/components/board/services/boardApi.ts
|
// frontend/src/components/board/services/boardApi.ts
|
||||||
@ -1406,7 +1406,7 @@ frontend/src/
|
|||||||
| +-- cn.ts className 조합 유틸리티
|
| +-- cn.ts className 조합 유틸리티
|
||||||
| +-- sanitize.ts XSS 방지/입력 살균
|
| +-- sanitize.ts XSS 방지/입력 살균
|
||||||
| +-- coordinates.ts 좌표 변환 유틸리티
|
| +-- coordinates.ts 좌표 변환 유틸리티
|
||||||
+-- components/ 탭별 패키지 (11개, MPA 컴포넌트 구조)
|
+-- tabs/ 탭별 패키지 (11개)
|
||||||
| +-- {탭명}/
|
| +-- {탭명}/
|
||||||
| +-- components/ 탭 뷰 컴포넌트
|
| +-- components/ 탭 뷰 컴포넌트
|
||||||
| +-- services/{탭명}Api.ts 탭별 API 서비스
|
| +-- services/{탭명}Api.ts 탭별 API 서비스
|
||||||
|
|||||||
@ -21,7 +21,7 @@ DB 설계부터 백엔드 구현, 프론트엔드 연동까지 End-to-End 패턴
|
|||||||
|
|
||||||
```
|
```
|
||||||
[Frontend] [Backend] [Database]
|
[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
|
Axios (withCredentials: true) requireAuth -> requirePermission
|
||||||
--HTTP--> src/{domain}/{domain}Service.ts wingPool / authPool
|
--HTTP--> src/{domain}/{domain}Service.ts wingPool / authPool
|
||||||
wingPool.query(SQL, params) --SQL-->
|
wingPool.query(SQL, params) --SQL-->
|
||||||
|
|||||||
@ -242,7 +242,7 @@ frontend/src/
|
|||||||
│ ├── store/ authStore (Zustand), menuStore
|
│ ├── store/ authStore (Zustand), menuStore
|
||||||
│ ├── types/ backtrack, boomLine, hns, navigation
|
│ ├── types/ backtrack, boomLine, hns, navigation
|
||||||
│ └── utils/ coordinates, geo, sanitize
|
│ └── utils/ coordinates, geo, sanitize
|
||||||
└── components/ 탭 단위 패키지 (11개, MPA 컴포넌트 구조)
|
└── tabs/ 탭 단위 패키지 (11개)
|
||||||
├── prediction/ 확산 예측
|
├── prediction/ 확산 예측
|
||||||
├── hns/ HNS 분석
|
├── hns/ HNS 분석
|
||||||
├── rescue/ 구조 시나리오
|
├── rescue/ 구조 시나리오
|
||||||
@ -259,7 +259,7 @@ frontend/src/
|
|||||||
**각 탭 내부 구조 패턴:**
|
**각 탭 내부 구조 패턴:**
|
||||||
|
|
||||||
```
|
```
|
||||||
components/{탭명}/
|
tabs/{탭명}/
|
||||||
├── components/ UI 컴포넌트
|
├── components/ UI 컴포넌트
|
||||||
│ ├── {Tab}View.tsx 메인 뷰 (App.tsx에서 라우팅)
|
│ ├── {Tab}View.tsx 메인 뷰 (App.tsx에서 라우팅)
|
||||||
│ ├── {Tab}LeftPanel.tsx
|
│ ├── {Tab}LeftPanel.tsx
|
||||||
|
|||||||
@ -75,7 +75,7 @@ wing/
|
|||||||
│ │ │ ├── store/ authStore, menuStore (Zustand)
|
│ │ │ ├── store/ authStore, menuStore (Zustand)
|
||||||
│ │ │ ├── types/ backtrack, boomLine, hns, navigation
|
│ │ │ ├── types/ backtrack, boomLine, hns, navigation
|
||||||
│ │ │ └── utils/ coordinates, geo, sanitize
|
│ │ │ └── utils/ coordinates, geo, sanitize
|
||||||
│ │ └── components/ 탭 단위 패키지 (11개, MPA 구조)
|
│ │ └── tabs/ 탭 단위 패키지 (11개)
|
||||||
│ │ ├── prediction/ 확산 예측
|
│ │ ├── prediction/ 확산 예측
|
||||||
│ │ ├── hns/ HNS 분석
|
│ │ ├── hns/ HNS 분석
|
||||||
│ │ ├── rescue/ 구조 시나리오
|
│ │ ├── rescue/ 구조 시나리오
|
||||||
|
|||||||
@ -66,7 +66,7 @@ wing/
|
|||||||
│ │ ├── utils/ cn, coordinates, geo, sanitize
|
│ │ ├── utils/ cn, coordinates, geo, sanitize
|
||||||
│ │ ├── styles/ base.css, components.css, wing.css (@layer)
|
│ │ ├── styles/ base.css, components.css, wing.css (@layer)
|
||||||
│ │ └── constants/ featureIds.ts (FEATURE_ID 상수 체계)
|
│ │ └── constants/ featureIds.ts (FEATURE_ID 상수 체계)
|
||||||
│ └── components/ @components/ alias (11개 탭, MPA 구조)
|
│ └── tabs/ @components/ alias (11개 탭)
|
||||||
│ ├── prediction/ 유류 확산 예측
|
│ ├── prediction/ 유류 확산 예측
|
||||||
│ ├── hns/ HNS 분석
|
│ ├── hns/ HNS 분석
|
||||||
│ ├── rescue/ 구조 시나리오
|
│ ├── rescue/ 구조 시나리오
|
||||||
|
|||||||
@ -4,17 +4,6 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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 최신화, 디자인 시스템 준수)
|
|
||||||
|
|
||||||
## [2026-04-16]
|
## [2026-04-16]
|
||||||
|
|
||||||
### 추가
|
### 추가
|
||||||
|
|||||||
@ -217,8 +217,6 @@ export function generateAIBoomLines(
|
|||||||
const totalDist = haversineDistance(incident, centroid);
|
const totalDist = haversineDistance(incident, centroid);
|
||||||
|
|
||||||
// 입자 분산 폭 계산 (최종 시간 기준)
|
// 입자 분산 폭 계산 (최종 시간 기준)
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const perpBearing = (mainBearing + 90) % 360;
|
|
||||||
let maxSpread = 0;
|
let maxSpread = 0;
|
||||||
for (const p of finalPoints) {
|
for (const p of finalPoints) {
|
||||||
const bearing = computeBearing(incident, p);
|
const bearing = computeBearing(incident, p);
|
||||||
|
|||||||
@ -31,45 +31,6 @@ export function stripHtmlTags(html: string): string {
|
|||||||
return html.replace(/<[^>]*>/g, '');
|
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;
|
const DANGEROUS_ATTRS = /\s*on\w+\s*=|javascript\s*:|vbscript\s*:|expression\s*\(/gi;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,11 @@ import { useState, useEffect, useCallback } from 'react';
|
|||||||
import {
|
import {
|
||||||
fetchRoles,
|
fetchRoles,
|
||||||
fetchPermTree,
|
fetchPermTree,
|
||||||
|
updatePermissionsApi,
|
||||||
|
createRoleApi,
|
||||||
|
deleteRoleApi,
|
||||||
|
updateRoleApi,
|
||||||
|
updateRoleDefaultApi,
|
||||||
type RoleWithPermissions,
|
type RoleWithPermissions,
|
||||||
type PermTreeNode,
|
type PermTreeNode,
|
||||||
} from '@common/services/authApi';
|
} from '@common/services/authApi';
|
||||||
|
|||||||
@ -89,7 +89,7 @@ export function OilAreaAnalysis() {
|
|||||||
processedFilesRef.current.add(file);
|
processedFilesRef.current.add(file);
|
||||||
|
|
||||||
exifr
|
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<typeof exifr.parse>[1])
|
||||||
.then((exif) => {
|
.then((exif) => {
|
||||||
const info: ImageExif = {
|
const info: ImageExif = {
|
||||||
lat: exif?.latitude ?? null,
|
lat: exif?.latitude ?? null,
|
||||||
|
|||||||
@ -300,7 +300,7 @@ export function AoiPanel() {
|
|||||||
getLineWidth: (d: MonitorZone) => (selectedZone === d.id ? 3 : 1.5),
|
getLineWidth: (d: MonitorZone) => (selectedZone === d.id ? 3 : 1.5),
|
||||||
lineWidthUnits: 'pixels',
|
lineWidthUnits: 'pixels',
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onClick: ({ object }: { object: MonitorZone }) => {
|
onClick: ({ object }: { object?: MonitorZone }) => {
|
||||||
if (object && !isDrawing)
|
if (object && !isDrawing)
|
||||||
setSelectedZone(object.id === selectedZone ? null : object.id);
|
setSelectedZone(object.id === selectedZone ? null : object.id);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -238,7 +238,7 @@ export function DetectPanel() {
|
|||||||
lineWidthMinPixels: 2,
|
lineWidthMinPixels: 2,
|
||||||
stroked: true,
|
stroked: true,
|
||||||
pickable: true,
|
pickable: true,
|
||||||
onClick: ({ object }: { object: VesselDetection }) => {
|
onClick: ({ object }: { object?: VesselDetection }) => {
|
||||||
if (object) setSelectedId(object.id === selectedId ? null : object.id);
|
if (object) setSelectedId(object.id === selectedId ? null : object.id);
|
||||||
},
|
},
|
||||||
updateTriggers: { getRadius: [selectedId] },
|
updateTriggers: { getRadius: [selectedId] },
|
||||||
|
|||||||
@ -247,17 +247,17 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
|||||||
{mapTypes.map((item) => (
|
{mapTypes.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.mapKey}
|
key={item.mapKey}
|
||||||
onClick={() => toggleMap(item.mapKey)}
|
onClick={() => toggleMap(item.mapKey as keyof typeof mapToggles)}
|
||||||
className="w-full px-3 py-2 flex items-center justify-between text-title-5 text-fg-sub hover:bg-[var(--hover-overlay)] transition-all"
|
className="w-full px-3 py-2 flex items-center justify-between text-title-5 text-fg-sub hover:bg-[var(--hover-overlay)] transition-all"
|
||||||
>
|
>
|
||||||
<span className="flex items-center gap-2.5">
|
<span className="flex items-center gap-2.5">
|
||||||
<span className="text-title-4">🗺</span> {item.mapNm}
|
<span className="text-title-4">🗺</span> {item.mapNm}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey] ? 'bg-color-accent' : 'bg-bg-card border border-stroke'}`}
|
className={`w-[34px] h-[18px] rounded-full transition-all relative ${mapToggles[item.mapKey as keyof typeof mapToggles] ? 'bg-color-accent' : 'bg-bg-card border border-stroke'}`}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${mapToggles[item.mapKey] ? 'left-[16px]' : 'left-[2px]'}`}
|
className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white shadow transition-all ${mapToggles[item.mapKey as keyof typeof mapToggles] ? 'left-[16px]' : 'left-[2px]'}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -273,12 +273,11 @@ export function BaseMap({
|
|||||||
<Map
|
<Map
|
||||||
initialViewState={{ longitude: center[1], latitude: center[0], zoom }}
|
initialViewState={{ longitude: center[1], latitude: center[0], zoom }}
|
||||||
mapStyle={mapStyle}
|
mapStyle={mapStyle}
|
||||||
className="w-full h-full"
|
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onZoom={handleZoom}
|
onZoom={handleZoom}
|
||||||
style={{ cursor: cursor ?? 'grab' }}
|
style={{ cursor: cursor ?? 'grab', width: '100%', height: '100%' }}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
preserveDrawingBuffer={true}
|
{...({ preserveDrawingBuffer: true } as Record<string, unknown>)}
|
||||||
>
|
>
|
||||||
{/* 공통 오버레이 */}
|
{/* 공통 오버레이 */}
|
||||||
<S57EncOverlay visible={mapToggles.s57 ?? false} />
|
<S57EncOverlay visible={mapToggles.s57 ?? false} />
|
||||||
|
|||||||
@ -15,11 +15,12 @@ const PI_4 = Math.PI / 4;
|
|||||||
const FADE_ALPHA = 0.02; // 프레임당 페이드량 (낮을수록 긴 꼬리)
|
const FADE_ALPHA = 0.02; // 프레임당 페이드량 (낮을수록 긴 꼬리)
|
||||||
|
|
||||||
export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayProps) {
|
export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayProps) {
|
||||||
const { current: map } = useMap();
|
const { current: mapRef } = useMap();
|
||||||
const animRef = useRef<number>();
|
const animRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !hydrStep) return;
|
if (!mapRef || !hydrStep) return;
|
||||||
|
const map = mapRef;
|
||||||
|
|
||||||
const container = map.getContainer();
|
const container = map.getContainer();
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
@ -212,7 +213,7 @@ export default function HydrParticleOverlay({ hydrStep }: HydrParticleOverlayPro
|
|||||||
map.off('move', onMove);
|
map.off('move', onMove);
|
||||||
canvas.remove();
|
canvas.remove();
|
||||||
};
|
};
|
||||||
}, [map, hydrStep]);
|
}, [mapRef, hydrStep]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import type { SensitiveResource } from '@interfaces/prediction/PredictionInterfa
|
|||||||
import type {
|
import type {
|
||||||
HydrDataStep,
|
HydrDataStep,
|
||||||
SensitiveResourceFeatureCollection,
|
SensitiveResourceFeatureCollection,
|
||||||
} from '@components/prediction/services/predictionApi';
|
} from '@interfaces/prediction/PredictionInterface';
|
||||||
import HydrParticleOverlay from './HydrParticleOverlay';
|
import HydrParticleOverlay from './HydrParticleOverlay';
|
||||||
import { TimelineControl } from './TimelineControl';
|
import { TimelineControl } from './TimelineControl';
|
||||||
import type { BoomLine, BoomLineCoord } from '@/types/boomLine';
|
import type { BoomLine, BoomLineCoord } from '@/types/boomLine';
|
||||||
@ -1331,8 +1331,9 @@ export function MapView({
|
|||||||
zoom: zoom,
|
zoom: zoom,
|
||||||
}}
|
}}
|
||||||
mapStyle={currentMapStyle}
|
mapStyle={currentMapStyle}
|
||||||
className="w-full h-full"
|
|
||||||
style={{
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
cursor:
|
cursor:
|
||||||
isSelectingLocation || drawAnalysisMode !== null || measureMode !== null
|
isSelectingLocation || drawAnalysisMode !== null || measureMode !== null
|
||||||
? 'crosshair'
|
? 'crosshair'
|
||||||
@ -1340,7 +1341,7 @@ export function MapView({
|
|||||||
}}
|
}}
|
||||||
onClick={handleMapClick}
|
onClick={handleMapClick}
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
preserveDrawingBuffer={true}
|
{...({ preserveDrawingBuffer: true } as Record<string, unknown>)}
|
||||||
>
|
>
|
||||||
{/* 지도 캡처 셋업 */}
|
{/* 지도 캡처 셋업 */}
|
||||||
{mapCaptureRef && <MapCaptureSetup captureRef={mapCaptureRef} />}
|
{mapCaptureRef && <MapCaptureSetup captureRef={mapCaptureRef} />}
|
||||||
|
|||||||
@ -315,12 +315,6 @@ export function HNSSubstanceView() {
|
|||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
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 */
|
/* Panel 3: 물질 상세검색 state */
|
||||||
const [hmsSearchType, setHmsSearchType] = useState<
|
const [hmsSearchType, setHmsSearchType] = useState<
|
||||||
'all' | 'abbr' | 'korName' | 'engName' | 'cas' | 'un'
|
'all' | 'abbr' | 'korName' | 'engName' | 'cas' | 'un'
|
||||||
@ -439,18 +433,6 @@ ${styles}
|
|||||||
return matchCategory && matchSearch;
|
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 기반 검색 결과 */
|
/* Panel 3: HNS API 기반 검색 결과 */
|
||||||
const HMS_PER_PAGE = 10;
|
const HMS_PER_PAGE = 10;
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export function HNSView() {
|
|||||||
const [inputParams, setInputParams] = useState<HNSInputParams | null>(null);
|
const [inputParams, setInputParams] = useState<HNSInputParams | null>(null);
|
||||||
const [loadedParams, setLoadedParams] = useState<Partial<HNSInputParams> | null>(null);
|
const [loadedParams, setLoadedParams] = useState<Partial<HNSInputParams> | null>(null);
|
||||||
const hasRunOnce = useRef(false); // 최초 실행 여부
|
const hasRunOnce = useRef(false); // 최초 실행 여부
|
||||||
const mapCaptureRef = useRef<(() => string | null) | null>(null);
|
const mapCaptureRef = useRef<(() => Promise<string | null>) | null>(null);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
setDispersionResult(null);
|
setDispersionResult(null);
|
||||||
@ -376,7 +376,7 @@ export function HNSView() {
|
|||||||
|
|
||||||
/** 분석 결과 저장 */
|
/** 분석 결과 저장 */
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!dispersionResult || !inputParams || !computedResult) {
|
if (!dispersionResult || !inputParams || !computedResult || !incidentCoord) {
|
||||||
alert('저장할 분석 결과가 없습니다. 먼저 예측을 실행해주세요.');
|
alert('저장할 분석 결과가 없습니다. 먼저 예측을 실행해주세요.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -672,11 +672,11 @@ export function HNSView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** 보고서 생성 — 실 데이터 수집 + 지도 캡처 후 탭 이동 */
|
/** 보고서 생성 — 실 데이터 수집 + 지도 캡처 후 탭 이동 */
|
||||||
const handleOpenReport = () => {
|
const handleOpenReport = async () => {
|
||||||
try {
|
try {
|
||||||
let mapImage: string | null = null;
|
let mapImage: string | null = null;
|
||||||
try {
|
try {
|
||||||
mapImage = mapCaptureRef.current?.() ?? null;
|
mapImage = (await mapCaptureRef.current?.()) ?? null;
|
||||||
} catch {
|
} catch {
|
||||||
/* canvas capture 실패 무시 */
|
/* canvas capture 실패 무시 */
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,18 +18,6 @@ export function HmsDetailPanel({
|
|||||||
'🔗 화물적부도·항구별 코드',
|
'🔗 화물적부도·항구별 코드',
|
||||||
];
|
];
|
||||||
const nfpa = s.nfpa;
|
const nfpa = s.nfpa;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const sebcColor = s.sebc.startsWith('G')
|
|
||||||
? 'var(--color-accent)'
|
|
||||||
: s.sebc.startsWith('E')
|
|
||||||
? 'var(--color-accent)'
|
|
||||||
: s.sebc.startsWith('F')
|
|
||||||
? 'var(--color-caution)'
|
|
||||||
: s.sebc.startsWith('D')
|
|
||||||
? 'var(--color-accent)'
|
|
||||||
: s.sebc.startsWith('S')
|
|
||||||
? 'var(--color-accent)'
|
|
||||||
: 'var(--fg-sub)';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { fetchPredictionAnalyses } from '@components/prediction/services/predictionApi';
|
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 { fetchHnsAnalyses } from '@components/hns/services/hnsApi';
|
||||||
import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface';
|
import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface';
|
||||||
import { fetchRescueOps } from '@components/rescue/services/rescueApi';
|
import { fetchRescueOps } from '@components/rescue/services/rescueApi';
|
||||||
|
|||||||
@ -70,54 +70,6 @@ interface AnalysisItem {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 카테고리 → 이모지 매핑 (prediction LeftPanel의 CATEGORY_ICON_MAP 기반, 미사용 보존) ── */
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const CATEGORY_ICON: Record<string, string> = {
|
|
||||||
어장정보: '🐟',
|
|
||||||
양식장: '🦪',
|
|
||||||
양식어업: '🦪',
|
|
||||||
어류양식장: '🐟',
|
|
||||||
패류양식장: '🦪',
|
|
||||||
해조류양식장: '🌿',
|
|
||||||
가두리양식장: '🔲',
|
|
||||||
갑각류양식장: '🦐',
|
|
||||||
기타양식장: '📦',
|
|
||||||
영세어업: '🎣',
|
|
||||||
유어장: '🎣',
|
|
||||||
수산시장: '🐟',
|
|
||||||
인공어초: '🪸',
|
|
||||||
암초: '🪨',
|
|
||||||
침선: '🚢',
|
|
||||||
해수욕장: '🏖',
|
|
||||||
갯바위낚시: '🪨',
|
|
||||||
선상낚시: '🚤',
|
|
||||||
마리나항: '⛵',
|
|
||||||
무역항: '🚢',
|
|
||||||
연안항: '⛵',
|
|
||||||
국가어항: '⚓',
|
|
||||||
지방어항: '⚓',
|
|
||||||
어항: '⚓',
|
|
||||||
항만구역: '⚓',
|
|
||||||
항로: '🚢',
|
|
||||||
정박지: '⛵',
|
|
||||||
항로표지: '🔴',
|
|
||||||
해수취수시설: '💧',
|
|
||||||
'취수구·배수구': '🚰',
|
|
||||||
LNG: '⚡',
|
|
||||||
발전소: '🔌',
|
|
||||||
'발전소·산단': '🏭',
|
|
||||||
임해공단: '🏭',
|
|
||||||
저유시설: '🛢',
|
|
||||||
'해저케이블·배관': '🔌',
|
|
||||||
갯벌: '🪨',
|
|
||||||
해안선_ESI: '🏖',
|
|
||||||
보호지역: '🛡',
|
|
||||||
해양보호구역: '🌿',
|
|
||||||
철새도래지: '🐦',
|
|
||||||
습지보호구역: '🏖',
|
|
||||||
보호종서식지: '🐢',
|
|
||||||
'보호종 서식지': '🐢',
|
|
||||||
};
|
|
||||||
|
|
||||||
/* ── 헬퍼: 활성 모델 문자열 ─────────────────────── */
|
/* ── 헬퍼: 활성 모델 문자열 ─────────────────────── */
|
||||||
function getActiveModels(p: PredictionAnalysis): string {
|
function getActiveModels(p: PredictionAnalysis): string {
|
||||||
|
|||||||
@ -14,11 +14,14 @@ import { getVesselCacheStatus, type VesselCacheStatus } from '@common/services/v
|
|||||||
import { IncidentsLeftPanel } from './IncidentsLeftPanel';
|
import { IncidentsLeftPanel } from './IncidentsLeftPanel';
|
||||||
import { IncidentsRightPanel, type ViewMode, type AnalysisSection } from './IncidentsRightPanel';
|
import { IncidentsRightPanel, type ViewMode, type AnalysisSection } from './IncidentsRightPanel';
|
||||||
import { fetchIncidents } from '../services/incidentsApi';
|
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 { fetchHnsAnalyses } from '@components/hns/services/hnsApi';
|
||||||
import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface';
|
import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface';
|
||||||
import { buildHnsDispersionLayers } from '../utils/hnsDispersionLayers';
|
import { buildHnsDispersionLayers } from '../utils/hnsDispersionLayers';
|
||||||
import { fetchAnalysisTrajectory, fetchOilSpillSummary } from '@components/prediction/services/predictionApi';
|
import {
|
||||||
|
fetchAnalysisTrajectory,
|
||||||
|
fetchOilSpillSummary,
|
||||||
|
} from '@components/prediction/services/predictionApi';
|
||||||
import type {
|
import type {
|
||||||
TrajectoryResponse,
|
TrajectoryResponse,
|
||||||
SensitiveResourceFeatureCollection,
|
SensitiveResourceFeatureCollection,
|
||||||
@ -512,8 +515,8 @@ export function IncidentsView() {
|
|||||||
layers.push(
|
layers.push(
|
||||||
new PathLayer({
|
new PathLayer({
|
||||||
id: `traj-path-${pathId}`,
|
id: `traj-path-${pathId}`,
|
||||||
data: [{ path: sorted.map((p) => [p.lon, p.lat]) }],
|
data: [{ path: sorted.map((p) => [p.lon, p.lat] as [number, number]) }],
|
||||||
getPath: (d: { path: number[][] }) => d.path,
|
getPath: (d: { path: [number, number][] }) => d.path,
|
||||||
getColor: [...color, 230] as [number, number, number, number],
|
getColor: [...color, 230] as [number, number, number, number],
|
||||||
getWidth: 2,
|
getWidth: 2,
|
||||||
widthMinPixels: 2,
|
widthMinPixels: 2,
|
||||||
@ -572,7 +575,8 @@ export function IncidentsView() {
|
|||||||
if (filtered.features.length === 0) return null;
|
if (filtered.features.length === 0) return null;
|
||||||
return new GeoJsonLayer({
|
return new GeoJsonLayer({
|
||||||
id: 'incidents-sensitive-geojson',
|
id: 'incidents-sensitive-geojson',
|
||||||
data: filtered,
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
data: filtered as any,
|
||||||
pickable: false,
|
pickable: false,
|
||||||
stroked: true,
|
stroked: true,
|
||||||
filled: true,
|
filled: true,
|
||||||
@ -1622,8 +1626,7 @@ function SplitResultMap({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ── (미사용) 분석별 SVG placeholder — 참고용 보존 ────────────────── */
|
/* ── (미사용) 분석별 SVG placeholder — 참고용 보존 ────────────────── */
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export function SplitVisualization({ slotKey, color }: { slotKey: SplitSlotKey; color: string }) {
|
||||||
function SplitVisualization({ slotKey, color }: { slotKey: SplitSlotKey; color: string }) {
|
|
||||||
if (slotKey === 'oil') {
|
if (slotKey === 'oil') {
|
||||||
return (
|
return (
|
||||||
<svg viewBox="0 0 320 200" className="w-full h-full">
|
<svg viewBox="0 0 320 200" className="w-full h-full">
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import {
|
|||||||
fetchIncidentAerialMedia,
|
fetchIncidentAerialMedia,
|
||||||
getMediaImageUrl,
|
getMediaImageUrl,
|
||||||
} from '../services/incidentsApi';
|
} 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';
|
type MediaTab = 'all' | 'photo' | 'video' | 'satellite' | 'cctv';
|
||||||
|
|
||||||
|
|||||||
@ -8,8 +8,17 @@ import { BitmapLayer, ScatterplotLayer } from '@deck.gl/layers';
|
|||||||
import { computeDispersion } from '@components/hns/utils/dispersionEngine';
|
import { computeDispersion } from '@components/hns/utils/dispersionEngine';
|
||||||
import { getSubstanceToxicity } from '@components/hns/utils/toxicityData';
|
import { getSubstanceToxicity } from '@components/hns/utils/toxicityData';
|
||||||
import { hexToRgba } from '@components/common/map/mapUtils';
|
import { hexToRgba } from '@components/common/map/mapUtils';
|
||||||
import type { HnsAnalysisItem, MeteoParams, SourceParams, SimParams } from '@interfaces/hns/HnsInterface';
|
import type { HnsAnalysisItem } from '@interfaces/hns/HnsInterface';
|
||||||
import type { DispersionModel, AlgorithmType, StabilityClass } from '@types/hns/HnsType';
|
import type {
|
||||||
|
MeteoParams,
|
||||||
|
SourceParams,
|
||||||
|
SimParams,
|
||||||
|
} from '@interfaces/hns/HnsInterface';
|
||||||
|
import type {
|
||||||
|
DispersionModel,
|
||||||
|
AlgorithmType,
|
||||||
|
StabilityClass,
|
||||||
|
} from '@/types/hns/HnsType';
|
||||||
|
|
||||||
// MapView와 동일한 색상 정지점
|
// MapView와 동일한 색상 정지점
|
||||||
const COLOR_STOPS: [number, number, number, number][] = [
|
const COLOR_STOPS: [number, number, number, number][] = [
|
||||||
|
|||||||
@ -776,6 +776,8 @@ export function OilSpillView() {
|
|||||||
analyst: '',
|
analyst: '',
|
||||||
officeName: '',
|
officeName: '',
|
||||||
acdntSttsCd: 'ACTIVE',
|
acdntSttsCd: 'ACTIVE',
|
||||||
|
predRunSn: null,
|
||||||
|
runDtm: null,
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@ -524,7 +524,7 @@ export function KospsPanel() {
|
|||||||
|
|
||||||
{/* Akima 수심 보간 & NGSST 수온 */}
|
{/* Akima 수심 보간 & NGSST 수온 */}
|
||||||
<div className="grid grid-cols-2 gap-2.5 mb-3">
|
<div className="grid grid-cols-2 gap-2.5 mb-3">
|
||||||
<div className="rounded-lg p-3" className="bg-bg-card border border-stroke">
|
<div className="rounded-lg p-3 bg-bg-card border border-stroke">
|
||||||
<div className="text-label-2 font-bold mb-2 text-color-accent">
|
<div className="text-label-2 font-bold mb-2 text-color-accent">
|
||||||
🗺️ Akima 수심 보간 기법
|
🗺️ Akima 수심 보간 기법
|
||||||
</div>
|
</div>
|
||||||
@ -539,7 +539,7 @@ export function KospsPanel() {
|
|||||||
<span className="text-label-2 text-fg-default">(i≤5, i+j≤5)</span>
|
<span className="text-label-2 text-fg-default">(i≤5, i+j≤5)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg p-3" className="bg-bg-card border border-stroke">
|
<div className="rounded-lg p-3 bg-bg-card border border-stroke">
|
||||||
<div className="text-label-2 font-bold mb-2 text-color-accent">
|
<div className="text-label-2 font-bold mb-2 text-color-accent">
|
||||||
🌡️ NGSST 실시간 수온자료
|
🌡️ NGSST 실시간 수온자료
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export function RoadmapPanel() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
<div className={`${card} ${cardBg}`} className="m-0">
|
<div className={`${card} ${cardBg} m-0`}>
|
||||||
<div style={labelStyle('var(--color-info)')}>현재 모델 한계</div>
|
<div style={labelStyle('var(--color-info)')}>현재 모델 한계</div>
|
||||||
<div className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
<div className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
||||||
<div
|
<div
|
||||||
@ -42,7 +42,7 @@ export function RoadmapPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${card} ${cardBg}`} className="m-0">
|
<div className={`${card} ${cardBg} m-0`}>
|
||||||
<div style={labelStyle('var(--color-info)')}>발전 방향</div>
|
<div style={labelStyle('var(--color-info)')}>발전 방향</div>
|
||||||
<div className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
<div className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
||||||
{[
|
{[
|
||||||
|
|||||||
@ -4,14 +4,29 @@ import type {
|
|||||||
PredictionDetail,
|
PredictionDetail,
|
||||||
BacktrackResult,
|
BacktrackResult,
|
||||||
TrajectoryResponse,
|
TrajectoryResponse,
|
||||||
OilSpillSummaryResponse,
|
|
||||||
SensitiveResourceCategory,
|
SensitiveResourceCategory,
|
||||||
|
SensitiveResourceFeature,
|
||||||
SensitiveResourceFeatureCollection,
|
SensitiveResourceFeatureCollection,
|
||||||
SpreadParticlesGeojson,
|
SpreadParticlesGeojson,
|
||||||
|
HydrDataStep,
|
||||||
|
OilSpillSummaryResponse,
|
||||||
ImageAnalyzeResult,
|
ImageAnalyzeResult,
|
||||||
GscAccidentListItem,
|
GscAccidentListItem,
|
||||||
} from '@interfaces/prediction/PredictionInterface';
|
} from '@interfaces/prediction/PredictionInterface';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
PredictionAnalysis,
|
||||||
|
PredictionDetail,
|
||||||
|
BacktrackResult,
|
||||||
|
TrajectoryResponse,
|
||||||
|
SensitiveResourceCategory,
|
||||||
|
SensitiveResourceFeature,
|
||||||
|
SensitiveResourceFeatureCollection,
|
||||||
|
SpreadParticlesGeojson,
|
||||||
|
HydrDataStep,
|
||||||
|
OilSpillSummaryResponse,
|
||||||
|
};
|
||||||
|
|
||||||
export const fetchPredictionAnalyses = async (params?: {
|
export const fetchPredictionAnalyses = async (params?: {
|
||||||
search?: string;
|
search?: string;
|
||||||
acdntSn?: number;
|
acdntSn?: number;
|
||||||
@ -64,17 +79,6 @@ export const fetchAnalysisTrajectory = async (
|
|||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchOilSpillSummary = async (
|
|
||||||
acdntSn: number,
|
|
||||||
predRunSn?: number,
|
|
||||||
): Promise<OilSpillSummaryResponse> => {
|
|
||||||
const response = await api.get<OilSpillSummaryResponse>(
|
|
||||||
`/prediction/analyses/${acdntSn}/oil-summary`,
|
|
||||||
predRunSn != null ? { params: { predRunSn } } : undefined,
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const fetchSensitiveResources = async (
|
export const fetchSensitiveResources = async (
|
||||||
acdntSn: number,
|
acdntSn: number,
|
||||||
): Promise<SensitiveResourceCategory[]> => {
|
): Promise<SensitiveResourceCategory[]> => {
|
||||||
@ -134,3 +138,18 @@ export const fetchGscAccidents = async (): Promise<GscAccidentListItem[]> => {
|
|||||||
const response = await api.get<GscAccidentListItem[]>('/gsc/accidents');
|
const response = await api.get<GscAccidentListItem[]>('/gsc/accidents');
|
||||||
return response.data;
|
return response.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 유류 확산 요약
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
export const fetchOilSpillSummary = async (
|
||||||
|
acdntSn: number,
|
||||||
|
predRunSn?: number,
|
||||||
|
): Promise<OilSpillSummaryResponse> => {
|
||||||
|
const response = await api.get<OilSpillSummaryResponse>(
|
||||||
|
`/prediction/analyses/${acdntSn}/oil-summary`,
|
||||||
|
predRunSn != null ? { params: { predRunSn } } : undefined,
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function createEmptyReport(jurisdiction?: Jurisdiction): OilSpillReportDa
|
|||||||
author: '',
|
author: '',
|
||||||
reportType: '예측보고서',
|
reportType: '예측보고서',
|
||||||
analysisCategory: '',
|
analysisCategory: '',
|
||||||
jurisdiction: jurisdiction || '',
|
jurisdiction: jurisdiction ?? '남해청',
|
||||||
status: '수행중',
|
status: '수행중',
|
||||||
incident: {
|
incident: {
|
||||||
name: '',
|
name: '',
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import {
|
import { OilSpillReportTemplate } from './OilSpillReportTemplate';
|
||||||
OilSpillReportTemplate,
|
import type { OilSpillReportData } from '@interfaces/reports/ReportsInterface';
|
||||||
type OilSpillReportData,
|
import type { Jurisdiction } from '@/types/reports/ReportsType';
|
||||||
type Jurisdiction,
|
|
||||||
} from './OilSpillReportTemplate';
|
|
||||||
import { loadReportsFromApi, loadReportDetail, deleteReportApi } from '../services/reportsApi';
|
import { loadReportsFromApi, loadReportDetail, deleteReportApi } from '../services/reportsApi';
|
||||||
import { useSubMenu } from '@common/hooks/useSubMenu';
|
import { useSubMenu } from '@common/hooks/useSubMenu';
|
||||||
import { templateTypes } from './reportTypes';
|
import { templateTypes } from './reportTypes';
|
||||||
|
|||||||
@ -230,7 +230,7 @@ export function SensitiveResourceMapSection({
|
|||||||
center: [127.5, 35.5],
|
center: [127.5, 35.5],
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
});
|
} as maplibregl.MapOptions);
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
// 확산 파티클 — sensitive 레이어 아래 (예측 탭과 동일한 색상 로직)
|
// 확산 파티클 — sensitive 레이어 아래 (예측 탭과 동일한 색상 로직)
|
||||||
|
|||||||
@ -62,7 +62,7 @@ export function SensitivityMapSection({
|
|||||||
center: [127.5, 35.5],
|
center: [127.5, 35.5],
|
||||||
zoom: 8,
|
zoom: 8,
|
||||||
preserveDrawingBuffer: true,
|
preserveDrawingBuffer: true,
|
||||||
});
|
} as maplibregl.MapOptions);
|
||||||
mapRef.current = map;
|
mapRef.current = map;
|
||||||
map.on('load', () => {
|
map.on('load', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@ -104,7 +104,7 @@ export function SensitivityMapSection({
|
|||||||
// fitBounds
|
// fitBounds
|
||||||
const coords: [number, number][] = [];
|
const coords: [number, number][] = [];
|
||||||
displayGeojson.features.forEach((f) => {
|
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]);
|
if (geom.type === 'Point') coords.push(geom.coordinates as [number, number]);
|
||||||
else if (geom.type === 'Polygon')
|
else if (geom.type === 'Polygon')
|
||||||
(geom.coordinates as [number, number][][])[0]?.forEach((c) => coords.push(c));
|
(geom.coordinates as [number, number][][])[0]?.forEach((c) => coords.push(c));
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type {
|
|||||||
RescueOpsItem,
|
RescueOpsItem,
|
||||||
RescueScenarioItem,
|
RescueScenarioItem,
|
||||||
RescueScenario,
|
RescueScenario,
|
||||||
|
ChartDataItem,
|
||||||
} from '@interfaces/rescue/RescueInterface';
|
} from '@interfaces/rescue/RescueInterface';
|
||||||
import type { Severity } from '@/types/rescue/RescueType';
|
import type { Severity } from '@/types/rescue/RescueType';
|
||||||
import { ScenarioMapOverlay } from './contents/ScenarioMapOverlay';
|
import { ScenarioMapOverlay } from './contents/ScenarioMapOverlay';
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export async function fetchRescueOps(params?: {
|
|||||||
sttsCd?: string;
|
sttsCd?: string;
|
||||||
acdntTpCd?: string;
|
acdntTpCd?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
|
acdntSn?: number;
|
||||||
}): Promise<RescueOpsItem[]> {
|
}): Promise<RescueOpsItem[]> {
|
||||||
const response = await api.get<RescueOpsItem[]>('/rescue/ops', { params });
|
const response = await api.get<RescueOpsItem[]>('/rescue/ops', { params });
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
@ -34,9 +34,6 @@ export function PreScatView() {
|
|||||||
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
||||||
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
|
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
|
||||||
const [panelLoading, setPanelLoading] = useState(false);
|
const [panelLoading, setPanelLoading] = useState(false);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [timelineIdx, setTimelineIdx] = useState(6);
|
|
||||||
|
|
||||||
// 초기 관할청 목록 로딩
|
// 초기 관할청 목록 로딩
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|||||||
@ -113,8 +113,6 @@ function SegRow(
|
|||||||
|
|
||||||
function ScatLeftPanel({
|
function ScatLeftPanel({
|
||||||
segments,
|
segments,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
zones,
|
|
||||||
jurisdictions,
|
jurisdictions,
|
||||||
offices,
|
offices,
|
||||||
selectedOffice,
|
selectedOffice,
|
||||||
@ -125,8 +123,6 @@ function ScatLeftPanel({
|
|||||||
jurisdictionFilter,
|
jurisdictionFilter,
|
||||||
onJurisdictionChange,
|
onJurisdictionChange,
|
||||||
areaFilter,
|
areaFilter,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
onAreaChange,
|
|
||||||
phaseFilter,
|
phaseFilter,
|
||||||
onPhaseChange,
|
onPhaseChange,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export interface EnrichedWeatherStation extends WeatherStation {
|
|||||||
};
|
};
|
||||||
pressure: number;
|
pressure: number;
|
||||||
visibility: number;
|
visibility: number;
|
||||||
|
salinity?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -45,7 +45,7 @@ export async function getUltraShortForecast(
|
|||||||
|
|
||||||
// 데이터를 시간대별로 그룹화
|
// 데이터를 시간대별로 그룹화
|
||||||
const forecasts: WeatherForecastData[] = [];
|
const forecasts: WeatherForecastData[] = [];
|
||||||
const grouped = new Map<string, Record<string, unknown>>();
|
const grouped = new Map<string, WeatherForecastData>();
|
||||||
|
|
||||||
items.forEach((item: Record<string, string>) => {
|
items.forEach((item: Record<string, string>) => {
|
||||||
const key = `${item.fcstDate}-${item.fcstTime}`;
|
const key = `${item.fcstDate}-${item.fcstTime}`;
|
||||||
@ -61,10 +61,11 @@ export async function getUltraShortForecast(
|
|||||||
waveHeight: 0,
|
waveHeight: 0,
|
||||||
precipitation: 0,
|
precipitation: 0,
|
||||||
humidity: 0,
|
humidity: 0,
|
||||||
});
|
} as WeatherForecastData);
|
||||||
}
|
}
|
||||||
|
|
||||||
const forecast = grouped.get(key);
|
const forecast = grouped.get(key);
|
||||||
|
if (!forecast) return;
|
||||||
|
|
||||||
// 카테고리별 값 매핑
|
// 카테고리별 값 매핑
|
||||||
switch (item.category) {
|
switch (item.category) {
|
||||||
|
|||||||
@ -207,19 +207,3 @@ export function windDirectionToText(degree: number): string {
|
|||||||
const index = Math.round((degree % 360) / 22.5) % 16;
|
const index = Math.round((degree % 360) / 22.5) % 16;
|
||||||
return directions[index];
|
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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -202,7 +202,7 @@ export interface HNSInputParams {
|
|||||||
/** HNS 분석 — 재계산 모달 입력 파라미터 */
|
/** HNS 분석 — 재계산 모달 입력 파라미터 */
|
||||||
export interface RecalcParams {
|
export interface RecalcParams {
|
||||||
substance: string;
|
substance: string;
|
||||||
releaseType: '연속 유출' | '순간 유출' | '밀도가스 유출';
|
releaseType: ReleaseType;
|
||||||
emissionRate: number;
|
emissionRate: number;
|
||||||
totalRelease: number;
|
totalRelease: number;
|
||||||
algorithm: string;
|
algorithm: string;
|
||||||
|
|||||||
@ -29,6 +29,8 @@ export interface IncidentListItem {
|
|||||||
spilUnitCd: string | null;
|
spilUnitCd: string | null;
|
||||||
fcstHr: number | null;
|
fcstHr: number | null;
|
||||||
hasPredCompleted: boolean;
|
hasPredCompleted: boolean;
|
||||||
|
hasHnsCompleted: boolean;
|
||||||
|
hasRescueCompleted: boolean;
|
||||||
mediaCnt: number;
|
mediaCnt: number;
|
||||||
hasImgAnalysis: boolean;
|
hasImgAnalysis: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,23 +10,6 @@ interface ColorStep {
|
|||||||
color: string;
|
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 {
|
interface ColorToken {
|
||||||
name: string;
|
name: string;
|
||||||
@ -448,117 +431,6 @@ const ChipRow = ({ hex, role, gray, d, subdued = false }: ChipRowProps) => (
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
// ---------- 내부 컴포넌트: 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 (
|
|
||||||
<div>
|
|
||||||
{/* 색상 바 */}
|
|
||||||
<div
|
|
||||||
className="flex rounded-lg overflow-hidden"
|
|
||||||
style={{ border: `1px solid ${darkBg ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)'}` }}
|
|
||||||
>
|
|
||||||
{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 (
|
|
||||||
<div
|
|
||||||
key={step}
|
|
||||||
className="flex flex-col items-center justify-between"
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: color,
|
|
||||||
height: '60px',
|
|
||||||
borderLeft: isFirst
|
|
||||||
? 'none'
|
|
||||||
: `1px solid ${darkBg ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)'}`,
|
|
||||||
borderTopLeftRadius: isFirst ? '8px' : undefined,
|
|
||||||
borderBottomLeftRadius: isFirst ? '8px' : undefined,
|
|
||||||
borderTopRightRadius: isLast ? '8px' : undefined,
|
|
||||||
borderBottomRightRadius: isLast ? '8px' : undefined,
|
|
||||||
padding: '6px 0',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 상단: 단계 번호 */}
|
|
||||||
<span style={{ fontSize: '9px', color: textColor, lineHeight: 1 }}>{step}</span>
|
|
||||||
{/* 하단: 접근성 등급 */}
|
|
||||||
{rating && (
|
|
||||||
<span style={{ fontSize: '8px', color: textColor, lineHeight: 1, opacity: 0.8 }}>
|
|
||||||
{rating}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 마커 행 */}
|
|
||||||
{markers && markers.length > 0 && (
|
|
||||||
<div className="flex relative" style={{ height: '32px', marginTop: '2px' }}>
|
|
||||||
{steps.map(({ step }, idx) => {
|
|
||||||
const marker = getMarker(step);
|
|
||||||
const pct = (idx / (steps.length - 1)) * 100;
|
|
||||||
|
|
||||||
if (!marker) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={step}
|
|
||||||
className="absolute flex flex-col items-center"
|
|
||||||
style={{ left: `calc(${pct}% )`, transform: 'translateX(-50%)' }}
|
|
||||||
>
|
|
||||||
{/* 점선 */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: 0,
|
|
||||||
height: '10px',
|
|
||||||
borderLeft: '1px dashed rgba(156,163,175,0.7)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/* 뱃지 */}
|
|
||||||
<span
|
|
||||||
className="px-2 rounded"
|
|
||||||
style={{
|
|
||||||
fontSize: '10px',
|
|
||||||
lineHeight: '18px',
|
|
||||||
backgroundColor: badgeBg,
|
|
||||||
color: badgeText,
|
|
||||||
border: '1px solid rgba(255,255,255,0.1)',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{marker.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------- 내부 컴포넌트: DualModeSection ----------
|
// ---------- 내부 컴포넌트: DualModeSection ----------
|
||||||
|
|
||||||
interface DualModeSectionProps {
|
interface DualModeSectionProps {
|
||||||
@ -570,8 +442,7 @@ interface DualModeSectionProps {
|
|||||||
darkSpecs: React.ReactNode;
|
darkSpecs: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export const DualModeSection = ({
|
||||||
const DualModeSection = ({
|
|
||||||
lightBg = '#F5F5F5',
|
lightBg = '#F5F5F5',
|
||||||
darkBg = '#121418',
|
darkBg = '#121418',
|
||||||
lightContent,
|
lightContent,
|
||||||
@ -618,8 +489,7 @@ interface ColorSpecRowProps {
|
|||||||
dark?: boolean;
|
dark?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
export const ColorSpecRow = ({ role, gray, hex, dark = false }: ColorSpecRowProps) => (
|
||||||
const ColorSpecRow = ({ role, gray, hex, dark = false }: ColorSpecRowProps) => (
|
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-4 px-4 py-3"
|
className="flex items-center gap-4 px-4 py-3"
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export type DispersionModel = 'plume' | 'puff' | 'dense_gas';
|
|||||||
export type AlgorithmType = 'ALOHA (EPA)' | 'CAMEO' | 'Gaussian Plume' | 'AERMOD';
|
export type AlgorithmType = 'ALOHA (EPA)' | 'CAMEO' | 'Gaussian Plume' | 'AERMOD';
|
||||||
|
|
||||||
/** HNS 확산 — 유출 형태 UI 선택값 (연속/순간/풀증발) */
|
/** HNS 확산 — 유출 형태 UI 선택값 (연속/순간/풀증발) */
|
||||||
export type ReleaseType = '연속 유출' | '순간 유출' | '풀(Pool) 증발';
|
export type ReleaseType = '연속 유출' | '순간 유출' | '풀(Pool) 증발' | '밀도가스 유출';
|
||||||
|
|
||||||
/** HNS 시나리오 — 시나리오 위험도 분류 */
|
/** HNS 시나리오 — 시나리오 위험도 분류 */
|
||||||
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'RESOLVED';
|
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'RESOLVED';
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user