release: 2026-04-17 (320건 커밋) #190
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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
|
||||
|
||||
@ -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")
|
||||
|
||||
25
CLAUDE.md
25
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 <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)
|
||||
- **Frontend**: React 19 + Vite 7 + TypeScript 5.9 + Tailwind CSS 3
|
||||
- **Backend**: Express 4 + PostgreSQL (pg) + TypeScript
|
||||
|
||||
@ -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 서비스
|
||||
|
||||
@ -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-->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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/ 구조 시나리오
|
||||
|
||||
@ -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/ 구조 시나리오
|
||||
|
||||
@ -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]
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<typeof exifr.parse>[1])
|
||||
.then((exif) => {
|
||||
const info: ImageExif = {
|
||||
lat: exif?.latitude ?? null,
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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] },
|
||||
|
||||
@ -247,17 +247,17 @@ export function TopBar({ activeTab, onTabChange }: TopBarProps) {
|
||||
{mapTypes.map((item) => (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span className="flex items-center gap-2.5">
|
||||
<span className="text-title-4">🗺</span> {item.mapNm}
|
||||
</span>
|
||||
<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
|
||||
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>
|
||||
</button>
|
||||
|
||||
@ -273,12 +273,11 @@ export function BaseMap({
|
||||
<Map
|
||||
initialViewState={{ longitude: center[1], latitude: center[0], zoom }}
|
||||
mapStyle={mapStyle}
|
||||
className="w-full h-full"
|
||||
onClick={handleClick}
|
||||
onZoom={handleZoom}
|
||||
style={{ cursor: cursor ?? 'grab' }}
|
||||
style={{ cursor: cursor ?? 'grab', width: '100%', height: '100%' }}
|
||||
attributionControl={false}
|
||||
preserveDrawingBuffer={true}
|
||||
{...({ preserveDrawingBuffer: true } as Record<string, unknown>)}
|
||||
>
|
||||
{/* 공통 오버레이 */}
|
||||
<S57EncOverlay visible={mapToggles.s57 ?? false} />
|
||||
|
||||
@ -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<number>();
|
||||
const { current: mapRef } = useMap();
|
||||
const animRef = useRef<number | undefined>(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;
|
||||
}
|
||||
|
||||
@ -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<string, unknown>)}
|
||||
>
|
||||
{/* 지도 캡처 셋업 */}
|
||||
{mapCaptureRef && <MapCaptureSetup captureRef={mapCaptureRef} />}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -93,7 +93,7 @@ export function HNSView() {
|
||||
const [inputParams, setInputParams] = useState<HNSInputParams | null>(null);
|
||||
const [loadedParams, setLoadedParams] = useState<Partial<HNSInputParams> | null>(null);
|
||||
const hasRunOnce = useRef(false); // 최초 실행 여부
|
||||
const mapCaptureRef = useRef<(() => string | null) | null>(null);
|
||||
const mapCaptureRef = useRef<(() => Promise<string | null>) | 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 실패 무시 */
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<string, string> = {
|
||||
어장정보: '🐟',
|
||||
양식장: '🦪',
|
||||
양식어업: '🦪',
|
||||
어류양식장: '🐟',
|
||||
패류양식장: '🦪',
|
||||
해조류양식장: '🌿',
|
||||
가두리양식장: '🔲',
|
||||
갑각류양식장: '🦐',
|
||||
기타양식장: '📦',
|
||||
영세어업: '🎣',
|
||||
유어장: '🎣',
|
||||
수산시장: '🐟',
|
||||
인공어초: '🪸',
|
||||
암초: '🪨',
|
||||
침선: '🚢',
|
||||
해수욕장: '🏖',
|
||||
갯바위낚시: '🪨',
|
||||
선상낚시: '🚤',
|
||||
마리나항: '⛵',
|
||||
무역항: '🚢',
|
||||
연안항: '⛵',
|
||||
국가어항: '⚓',
|
||||
지방어항: '⚓',
|
||||
어항: '⚓',
|
||||
항만구역: '⚓',
|
||||
항로: '🚢',
|
||||
정박지: '⛵',
|
||||
항로표지: '🔴',
|
||||
해수취수시설: '💧',
|
||||
'취수구·배수구': '🚰',
|
||||
LNG: '⚡',
|
||||
발전소: '🔌',
|
||||
'발전소·산단': '🏭',
|
||||
임해공단: '🏭',
|
||||
저유시설: '🛢',
|
||||
'해저케이블·배관': '🔌',
|
||||
갯벌: '🪨',
|
||||
해안선_ESI: '🏖',
|
||||
보호지역: '🛡',
|
||||
해양보호구역: '🌿',
|
||||
철새도래지: '🐦',
|
||||
습지보호구역: '🏖',
|
||||
보호종서식지: '🐢',
|
||||
'보호종 서식지': '🐢',
|
||||
};
|
||||
|
||||
/* ── 헬퍼: 활성 모델 문자열 ─────────────────────── */
|
||||
function getActiveModels(p: PredictionAnalysis): string {
|
||||
|
||||
@ -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 (
|
||||
<svg viewBox="0 0 320 200" className="w-full h-full">
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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][] = [
|
||||
|
||||
@ -776,6 +776,8 @@ export function OilSpillView() {
|
||||
analyst: '',
|
||||
officeName: '',
|
||||
acdntSttsCd: 'ACTIVE',
|
||||
predRunSn: null,
|
||||
runDtm: null,
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -524,7 +524,7 @@ export function KospsPanel() {
|
||||
|
||||
{/* Akima 수심 보간 & NGSST 수온 */}
|
||||
<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">
|
||||
🗺️ Akima 수심 보간 기법
|
||||
</div>
|
||||
@ -539,7 +539,7 @@ export function KospsPanel() {
|
||||
<span className="text-label-2 text-fg-default">(i≤5, i+j≤5)</span>
|
||||
</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">
|
||||
🌡️ NGSST 실시간 수온자료
|
||||
</div>
|
||||
|
||||
@ -4,7 +4,7 @@ export function RoadmapPanel() {
|
||||
return (
|
||||
<div>
|
||||
<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 className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
||||
<div
|
||||
@ -42,7 +42,7 @@ export function RoadmapPanel() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${card} ${cardBg}`} className="m-0">
|
||||
<div className={`${card} ${cardBg} m-0`}>
|
||||
<div style={labelStyle('var(--color-info)')}>발전 방향</div>
|
||||
<div className="flex flex-col gap-2 text-label-2 text-fg-sub">
|
||||
{[
|
||||
|
||||
@ -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<OilSpillSummaryResponse> => {
|
||||
const response = await api.get<OilSpillSummaryResponse>(
|
||||
`/prediction/analyses/${acdntSn}/oil-summary`,
|
||||
predRunSn != null ? { params: { predRunSn } } : undefined,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const fetchSensitiveResources = async (
|
||||
acdntSn: number,
|
||||
): Promise<SensitiveResourceCategory[]> => {
|
||||
@ -134,3 +138,18 @@ export const fetchGscAccidents = async (): Promise<GscAccidentListItem[]> => {
|
||||
const response = await api.get<GscAccidentListItem[]>('/gsc/accidents');
|
||||
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: '',
|
||||
reportType: '예측보고서',
|
||||
analysisCategory: '',
|
||||
jurisdiction: jurisdiction || '',
|
||||
jurisdiction: jurisdiction ?? '남해청',
|
||||
status: '수행중',
|
||||
incident: {
|
||||
name: '',
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 레이어 아래 (예측 탭과 동일한 색상 로직)
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -9,6 +9,7 @@ export async function fetchRescueOps(params?: {
|
||||
sttsCd?: string;
|
||||
acdntTpCd?: string;
|
||||
search?: string;
|
||||
acdntSn?: number;
|
||||
}): Promise<RescueOpsItem[]> {
|
||||
const response = await api.get<RescueOpsItem[]>('/rescue/ops', { params });
|
||||
return response.data;
|
||||
|
||||
@ -34,9 +34,6 @@ export function PreScatView() {
|
||||
const [popupData, setPopupData] = useState<ScatDetail | null>(null);
|
||||
const [panelDetail, setPanelDetail] = useState<ScatDetail | null>(null);
|
||||
const [panelLoading, setPanelLoading] = useState(false);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [timelineIdx, setTimelineIdx] = useState(6);
|
||||
|
||||
// 초기 관할청 목록 로딩
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -24,6 +24,7 @@ export interface EnrichedWeatherStation extends WeatherStation {
|
||||
};
|
||||
pressure: number;
|
||||
visibility: number;
|
||||
salinity?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -45,7 +45,7 @@ export async function getUltraShortForecast(
|
||||
|
||||
// 데이터를 시간대별로 그룹화
|
||||
const forecasts: WeatherForecastData[] = [];
|
||||
const grouped = new Map<string, Record<string, unknown>>();
|
||||
const grouped = new Map<string, WeatherForecastData>();
|
||||
|
||||
items.forEach((item: Record<string, string>) => {
|
||||
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) {
|
||||
|
||||
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
@ -202,7 +202,7 @@ export interface HNSInputParams {
|
||||
/** HNS 분석 — 재계산 모달 입력 파라미터 */
|
||||
export interface RecalcParams {
|
||||
substance: string;
|
||||
releaseType: '연속 유출' | '순간 유출' | '밀도가스 유출';
|
||||
releaseType: ReleaseType;
|
||||
emissionRate: number;
|
||||
totalRelease: number;
|
||||
algorithm: string;
|
||||
|
||||
@ -29,6 +29,8 @@ export interface IncidentListItem {
|
||||
spilUnitCd: string | null;
|
||||
fcstHr: number | null;
|
||||
hasPredCompleted: boolean;
|
||||
hasHnsCompleted: boolean;
|
||||
hasRescueCompleted: boolean;
|
||||
mediaCnt: number;
|
||||
hasImgAnalysis: boolean;
|
||||
}
|
||||
|
||||
@ -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) => (
|
||||
</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 ----------
|
||||
|
||||
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) => (
|
||||
<div
|
||||
className="flex items-center gap-4 px-4 py-3"
|
||||
style={{
|
||||
|
||||
@ -16,7 +16,7 @@ export type DispersionModel = 'plume' | 'puff' | 'dense_gas';
|
||||
export type AlgorithmType = 'ALOHA (EPA)' | 'CAMEO' | 'Gaussian Plume' | 'AERMOD';
|
||||
|
||||
/** HNS 확산 — 유출 형태 UI 선택값 (연속/순간/풀증발) */
|
||||
export type ReleaseType = '연속 유출' | '순간 유출' | '풀(Pool) 증발';
|
||||
export type ReleaseType = '연속 유출' | '순간 유출' | '풀(Pool) 증발' | '밀도가스 유출';
|
||||
|
||||
/** HNS 시나리오 — 시나리오 위험도 분류 */
|
||||
export type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'RESOLVED';
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user