feat(design): HNS/예측/구조 탭 디자인 시스템 폰트 및 색상 토큰 전환
This commit is contained in:
부모
a0be19d060
커밋
c7b0b7a3c2
4
.gitignore
vendored
4
.gitignore
vendored
@ -99,3 +99,7 @@ frontend/public/hns-manual/images/
|
|||||||
# Lock files (keep for reproducible builds)
|
# Lock files (keep for reproducible builds)
|
||||||
!frontend/package-lock.json
|
!frontend/package-lock.json
|
||||||
!backend/package-lock.json
|
!backend/package-lock.json
|
||||||
|
|
||||||
|
|
||||||
|
# mcp
|
||||||
|
.mcp.json
|
||||||
@ -7,6 +7,158 @@ WING-OPS UI 디자인 시스템의 비주얼 레퍼런스 카탈로그.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 버전 히스토리
|
||||||
|
|
||||||
|
### v1.1 — 시맨틱 토큰 & 테마 시스템 (2026-03-28~)
|
||||||
|
|
||||||
|
> 브랜치: `feature/predict-develop`, `feature/design-system-font`
|
||||||
|
|
||||||
|
v1.0의 축약형 토큰 시스템을 시맨틱 네이밍으로 전면 전환하고, 다크/라이트 테마 전환 기능을 도입한 구조적 리팩토링.
|
||||||
|
|
||||||
|
#### 변경된 점
|
||||||
|
|
||||||
|
| 항목 | v1.0 | v1.1 |
|
||||||
|
|------|------|------|
|
||||||
|
| 토큰 네이밍 | 축약형 (`--bg0`, `--t1`, `--cyan`) | 시맨틱 (`--bg-base`, `--fg-default`, `--color-accent`) |
|
||||||
|
| 폰트 | Outfit + Noto Sans KR + JetBrains Mono (3종) | PretendardGOV 단일 폰트 (4웨이트) |
|
||||||
|
| 테마 | 다크 모드 전용 (하드코딩) | 다크/라이트 전환 지원 (`data-theme` 속성) |
|
||||||
|
| 프리미티브 팔레트 | 7그룹 11단계 (Navy/Cyan/Blue/Red/Green/Orange/Yellow, 00~100) | 6그룹 10단계 (Gray/Blue/Green/Yellow/Red/Purple, 100~1000) |
|
||||||
|
| 텍스트 대비 | `--t2: #b0b8cc`, `--t3: #8690a6` | `--fg-sub: #c0c8dc`, `--fg-disabled: #9ba3b8` (대비 향상) |
|
||||||
|
| 버튼 스타일 | `.prd-btn.pri` 그라데이션 (cyan→blue) | 아웃라인/고스트 스타일 |
|
||||||
|
| Tailwind 컬러 키 | 하드코딩 hex (`bg.0`, `text.1`, `primary.cyan`) | CSS 변수 참조 (`bg.base`, `fg.DEFAULT`, `color.accent`) |
|
||||||
|
| 폰트 유틸리티 | 하드코딩 (`font-family: 'JetBrains Mono'`) | CSS 변수 경유 (`font-family: var(--font-mono)`) |
|
||||||
|
|
||||||
|
#### 토큰 마이그레이션 매핑
|
||||||
|
|
||||||
|
| v1.0 | v1.1 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| `--bg0` | `--bg-base` | 페이지 배경 |
|
||||||
|
| `--bg1` | `--bg-surface` | 사이드바, 패널 |
|
||||||
|
| `--bg2` | `--bg-elevated` | 테이블 헤더, 상위 요소 |
|
||||||
|
| `--bg3` | `--bg-card` | 카드 배경 |
|
||||||
|
| `--bgH` | `--bg-surface-hover` | 호버 상태 |
|
||||||
|
| `--bd` | `--stroke-default` | 기본 구분선 |
|
||||||
|
| `--bdL` | `--stroke-light` | 연한 구분선 |
|
||||||
|
| `--t1` | `--fg-default` | 기본 텍스트 |
|
||||||
|
| `--t2` | `--fg-sub` | 보조 텍스트 |
|
||||||
|
| `--t3` | `--fg-disabled` | 비활성 텍스트 |
|
||||||
|
| `--cyan` | `--color-accent` | 주요 강조 |
|
||||||
|
| `--blue` | `--color-info` | 정보, 링크 |
|
||||||
|
| `--red` | `--color-danger` | 위험, 삭제 |
|
||||||
|
| `--orange` | `--color-warning` | 주의 |
|
||||||
|
| `--yellow` | `--color-caution` | 경고 |
|
||||||
|
| `--green` | `--color-success` | 성공, 정상 |
|
||||||
|
| `--purple` | `--color-tertiary` | 3차 강조 |
|
||||||
|
| `--boom` | `--color-boom` | 오일붐 전용 |
|
||||||
|
| `--fK` | `--font-korean` | 한국어 폰트 스택 |
|
||||||
|
| `--fM` | `--font-mono` | 모노 폰트 스택 |
|
||||||
|
| `--rS` | `--radius-sm` | 소형 border radius |
|
||||||
|
| `--rM` | `--radius-md` | 중형 border radius |
|
||||||
|
|
||||||
|
#### 추가된 기능
|
||||||
|
|
||||||
|
- **다크/라이트 테마 전환**: `themeStore.ts` (Zustand) + `data-theme` DOM 속성 + FOUC 방지 인라인 스크립트
|
||||||
|
- **시맨틱 오버레이 토큰**: `--hover-overlay`, `--dropdown-bg` (테마별 불투명도 차별화)
|
||||||
|
- **Navy 액센트 토큰**: `--color-navy`, `--color-navy-hover`, `--color-accent-muted`
|
||||||
|
- **정적 컬러 토큰**: `--static-black`, `--static-white` (테마 무관 고정값)
|
||||||
|
- **타이포그래피 스케일 (17개 토큰)**: Display 3종, Heading 3종, Title 6종, Body 2종, Label 2종, Caption 1종
|
||||||
|
- **Letter-spacing 토큰 5종**: `--letter-spacing-display` (0.06em) ~ `--letter-spacing-label` (0.04em)
|
||||||
|
- **Font weight 토큰 4종**: thin(300) / regular(400) / medium(500) / bold(700)
|
||||||
|
- **Line height 토큰 4종**: tight(1.3) / snug(1.4) / normal(1.5) / relaxed(1.6)
|
||||||
|
- **Tailwind tracking 유틸리티**: `tracking-display`, `tracking-heading`, `tracking-body`, `tracking-navigation`, `tracking-label`
|
||||||
|
- **라이트 테마 컴포넌트 오버라이드**: CCTV 팝업, 날짜피커, 타임라인, 콤보박스, 좌표 표시 등
|
||||||
|
- **폰트 렌더링 최적화**: `-webkit-font-smoothing: antialiased`, `text-rendering: optimizeLegibility`
|
||||||
|
|
||||||
|
#### 업데이트된 부분
|
||||||
|
|
||||||
|
- 92개+ 컴포넌트 파일의 토큰 마이그레이션 (축약형 → 시맨틱)
|
||||||
|
- Stitch MCP 프로젝트 참조 제거 (독립 토큰 시스템으로 전환)
|
||||||
|
- DESIGN-SYSTEM.md 문서 전면 재작성
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### v1.0 — 초기 디자인 시스템 (2026-03-24~25)
|
||||||
|
|
||||||
|
> 브랜치: `feature/stitch-mcp` | 도구: Google Stitch MCP
|
||||||
|
|
||||||
|
WING-OPS 전용 디자인 시스템의 첫 구축. CSS 변수 기반 토큰 시스템, `.wing-*` 컴포넌트 클래스, 라이브 카탈로그 뷰어를 도입.
|
||||||
|
|
||||||
|
#### 컬러 시스템
|
||||||
|
|
||||||
|
- **프리미티브 팔레트**: 7개 그룹 (Navy/Cyan/Blue/Red/Green/Orange/Yellow), 각 11단계 스케일 (00~100)
|
||||||
|
- **시맨틱 컬러**: 축약형 CSS 변수 — Background(`--bg0`~`--bgH`), Text(`--t1`~`--t3`), Border(`--bd`, `--bdL`)
|
||||||
|
- **액센트 컬러**: `--cyan`(#06b6d4), `--blue`(#3b82f6), `--red`, `--green`, `--orange`, `--yellow`, `--purple`
|
||||||
|
- **특수 컬러**: `--boom`(#f59e0b) — 오일붐 전용 Amber
|
||||||
|
|
||||||
|
#### 타이포그래피
|
||||||
|
|
||||||
|
- **3종 폰트 패밀리**: Outfit (영문 본문), Noto Sans KR (한국어), JetBrains Mono (좌표/수치)
|
||||||
|
- **10개 `.wing-*` 타이포 클래스**: `.wing-title`(15px) ~ `.wing-badge`(9px)
|
||||||
|
|
||||||
|
#### 컴포넌트 클래스
|
||||||
|
|
||||||
|
| 카테고리 | 클래스 |
|
||||||
|
|----------|--------|
|
||||||
|
| 레이아웃 셸 | `.wing-panel`, `.wing-panel-scroll`, `.wing-header-bar`, `.wing-sidebar` |
|
||||||
|
| 컨테이너 | `.wing-card`, `.wing-card-sm`, `.wing-section` |
|
||||||
|
| 버튼 | `.wing-btn`, `.wing-btn-primary` (cyan→blue 그라데이션), `-secondary`, `-outline`, `-pdf`, `-danger` |
|
||||||
|
| 입력 | `.wing-input` (cyan 포커스 링) |
|
||||||
|
| 테이블 | `.wing-table`, `.wing-th`, `.wing-td`, `.wing-tr-hover` |
|
||||||
|
| 탭 | `.wing-tab-bar`, `.wing-tab` (cyan 틴트 + 보더 활성) |
|
||||||
|
| 모달 | `.wing-overlay` (블러 백드롭), `.wing-modal`, `.wing-modal-header` |
|
||||||
|
| 유틸리티 | `.wing-divider`, `.wing-kv-row`, `.wing-kv-label`, `.wing-kv-value` |
|
||||||
|
| 뱃지/아이콘 | `.wing-badge`, `.wing-icon-badge`, `.wing-icon-badge-sm` |
|
||||||
|
|
||||||
|
#### 특수 컴포넌트
|
||||||
|
|
||||||
|
| 영역 | 클래스 접두사 | 설명 |
|
||||||
|
|------|--------------|------|
|
||||||
|
| 예측 패널 | `.prd-*` | 폼 입력, 버튼, 맵 버튼 |
|
||||||
|
| 콤보박스 | `.combo-*` | 커스텀 드롭다운 (검색 + 리스트) |
|
||||||
|
| 타임라인 | `.tlb`, `.tlt`, `.tlr`, `.tlth` | 지도 하단 재생 컨트롤 |
|
||||||
|
| 레이어 트리 | `.lyr-*` | 3단계 접이식 레이어 (색상 스와치 + 투명도) |
|
||||||
|
| 오일붐 | `.boom-*` | 오일붐 드로잉 인디케이터 |
|
||||||
|
| 역추적 | `.bt-*` | 역추적 리플레이 마커 + 속도 버튼 |
|
||||||
|
| HNS | `.hns-scn-card` | HNS 시나리오 선택 카드 |
|
||||||
|
| 모델 칩 | `.prd-mc` | 모델 선택 칩 (활성 `✓` 인디케이터) |
|
||||||
|
|
||||||
|
#### 레이아웃
|
||||||
|
|
||||||
|
- **데스크톱 전용** (≥ 1280px), Tablet/Mobile 미지원
|
||||||
|
- **7단계 z-index 레이어**: Base(0) ~ Tooltip(60)
|
||||||
|
- **Spacing**: Tailwind 기본 스케일 사용 (`gap-1`~`gap-8`)
|
||||||
|
|
||||||
|
#### Border Radius
|
||||||
|
|
||||||
|
- `rounded-sm`: **6px** (커스텀 오버라이드)
|
||||||
|
- `rounded-md`: **10px** (커스텀 오버라이드)
|
||||||
|
- 나머지 Tailwind 기본값 유지
|
||||||
|
|
||||||
|
#### 애니메이션
|
||||||
|
|
||||||
|
`fadeIn`, `fadeSlideDown`, `pulse-dot`, `pulse-border`, `comboIn`, `lyrPopIn`, `bt-collision-pulse`
|
||||||
|
|
||||||
|
#### 디자인 카탈로그
|
||||||
|
|
||||||
|
`/design` 라우트에 라이브 카탈로그 뷰어 배포:
|
||||||
|
- **Foundations 탭**: Color Palette, Typography, Radius, Layout, Overview
|
||||||
|
- **Components 탭**: Button, TextField, Overview (Button Catalog, Card, Icon Badge 섹션)
|
||||||
|
- **다크/라이트 모드 토글** 내장
|
||||||
|
- **테마 엔진**: `designTheme.ts` — 타입 안전한 `DesignTheme` 인터페이스
|
||||||
|
|
||||||
|
#### SVG 아이콘 에셋
|
||||||
|
|
||||||
|
23개 커스텀 아이콘 (`wing-` 접두사): `wing-anchor`, `wing-cargo`, `wing-chart-bar`, `wing-color-palette`, `wing-documentation`, `wing-elevation`, `wing-foundations`, `wing-layout-grid`, `wing-notification`, `wing-pdf-file`, `wing-settings`, `wing-typography`, `wing-wave-graph` 등
|
||||||
|
|
||||||
|
#### Stitch MCP 연동
|
||||||
|
|
||||||
|
Google Stitch 프로젝트 7개 스크린 참조:
|
||||||
|
- Design Tokens, Component Catalog (Buttons/Badges), Form Components
|
||||||
|
- Table & List Patterns, Modal Catalog, Operational Shell (Layout)
|
||||||
|
- Container & Navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 테마 (Theme)
|
## 테마 (Theme)
|
||||||
|
|
||||||
### 테마 전환 메커니즘
|
### 테마 전환 메커니즘
|
||||||
@ -121,6 +273,7 @@ Primitive Tokens (정적) Semantic Tokens (테마 반응형)
|
|||||||
| CSS 변수 | Tailwind 클래스 | Hex | 용도 |
|
| CSS 변수 | Tailwind 클래스 | Hex | 용도 |
|
||||||
|----------|----------------|-----|------|
|
|----------|----------------|-----|------|
|
||||||
| `--color-accent` | `text-color-accent` | `#06b6d4` | 주요 강조 (Cyan) |
|
| `--color-accent` | `text-color-accent` | `#06b6d4` | 주요 강조 (Cyan) |
|
||||||
|
| `--color-accent-muted` | `bg-color-accent-muted` | `#0e7490` / `#0891b2`(light) | 차분한 강조 (버튼 배경 등) |
|
||||||
| `--color-info` | `text-color-info` | `#3b82f6` | 정보, 링크 (Blue) |
|
| `--color-info` | `text-color-info` | `#3b82f6` | 정보, 링크 (Blue) |
|
||||||
| `--color-tertiary` | `text-color-tertiary` | `#a855f7` | 3차 강조 (Purple) |
|
| `--color-tertiary` | `text-color-tertiary` | `#a855f7` | 3차 강조 (Purple) |
|
||||||
| `--color-danger` | `text-color-danger` | `#ef4444` | 위험, 삭제 (Red) |
|
| `--color-danger` | `text-color-danger` | `#ef4444` | 위험, 삭제 (Red) |
|
||||||
@ -129,6 +282,8 @@ Primitive Tokens (정적) Semantic Tokens (테마 반응형)
|
|||||||
| `--color-success` | `text-color-success` | `#22c55e` | 성공, 정상 (Green) |
|
| `--color-success` | `text-color-success` | `#22c55e` | 성공, 정상 (Green) |
|
||||||
| `--color-boom` | `text-color-boom` | `#f59e0b` | 오일붐 전용 (Amber) |
|
| `--color-boom` | `text-color-boom` | `#f59e0b` | 오일붐 전용 (Amber) |
|
||||||
| `--color-boom-hover` | — | `#fbbf24` | 오일붐 호버 |
|
| `--color-boom-hover` | — | `#fbbf24` | 오일붐 호버 |
|
||||||
|
| `--color-navy` | `bg-color-navy` | `#1e40af` / `#1d4ed8`(light) | Navy 버튼 배경 |
|
||||||
|
| `--color-navy-hover` | `bg-color-navy-hover` | `#1d4ed8` / `#2563eb`(light) | Navy 호버 |
|
||||||
|
|
||||||
#### Static Colors
|
#### Static Colors
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="ko">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
@ -10,7 +10,7 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;900&family=JetBrains+Mono:wght@400;500;600&family=Outfit:wght@300;400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<title>frontend</title>
|
<title>해양환경 위기대응 통합지원 시스템</title>
|
||||||
<script>
|
<script>
|
||||||
document.documentElement.setAttribute(
|
document.documentElement.setAttribute(
|
||||||
'data-theme',
|
'data-theme',
|
||||||
|
|||||||
@ -547,7 +547,9 @@ function MapCaptureSetup({
|
|||||||
composite.width = Math.round(src.width * scale);
|
composite.width = Math.round(src.width * scale);
|
||||||
composite.height = Math.round(src.height * scale);
|
composite.height = Math.round(src.height * scale);
|
||||||
const ctx = composite.getContext('2d')!;
|
const ctx = composite.getContext('2d')!;
|
||||||
ctx.fillStyle = '#0f1117';
|
ctx.fillStyle =
|
||||||
|
getComputedStyle(document.documentElement).getPropertyValue('--bg-base').trim() ||
|
||||||
|
'#0f1117';
|
||||||
ctx.fillRect(0, 0, composite.width, composite.height);
|
ctx.fillRect(0, 0, composite.width, composite.height);
|
||||||
ctx.drawImage(src, 0, 0, composite.width, composite.height);
|
ctx.drawImage(src, 0, 0, composite.width, composite.height);
|
||||||
resolve(composite.toDataURL('image/jpeg', 0.82));
|
resolve(composite.toDataURL('image/jpeg', 0.82));
|
||||||
@ -1037,14 +1039,6 @@ export function MapView({
|
|||||||
...dispersionHeatmap.filter((p) => p.concentration > 0.01).map((p) => p.concentration),
|
...dispersionHeatmap.filter((p) => p.concentration > 0.01).map((p) => p.concentration),
|
||||||
);
|
);
|
||||||
const filtered = dispersionHeatmap.filter((p) => p.concentration > 0.01);
|
const filtered = dispersionHeatmap.filter((p) => p.concentration > 0.01);
|
||||||
console.log(
|
|
||||||
'[MapView] HNS 히트맵:',
|
|
||||||
dispersionHeatmap.length,
|
|
||||||
'→ filtered:',
|
|
||||||
filtered.length,
|
|
||||||
'maxConc:',
|
|
||||||
maxConc.toFixed(2),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filtered.length > 0) {
|
if (filtered.length > 0) {
|
||||||
// 경위도 바운드 계산
|
// 경위도 바운드 계산
|
||||||
@ -1108,10 +1102,11 @@ export function MapView({
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageUrl = canvas.toDataURL();
|
||||||
result.push(
|
result.push(
|
||||||
new BitmapLayer({
|
new BitmapLayer({
|
||||||
id: 'hns-dispersion-bitmap',
|
id: 'hns-dispersion-bitmap',
|
||||||
image: canvas,
|
image: imageUrl,
|
||||||
bounds: [minLon, minLat, maxLon, maxLat],
|
bounds: [minLon, minLat, maxLon, maxLat],
|
||||||
opacity: 1.0,
|
opacity: 1.0,
|
||||||
pickable: false,
|
pickable: false,
|
||||||
@ -1690,21 +1685,15 @@ function MapControls({ center, zoom }: { center: [number, number]; zoom: number
|
|||||||
return (
|
return (
|
||||||
<div className="absolute top-[80px] left-[10px] z-10">
|
<div className="absolute top-[80px] left-[10px] z-10">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<button
|
<button onClick={() => map?.zoomIn()} className="map-ctrl-btn">
|
||||||
onClick={() => map?.zoomIn()}
|
|
||||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
|
|
||||||
>
|
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button onClick={() => map?.zoomOut()} className="map-ctrl-btn">
|
||||||
onClick={() => map?.zoomOut()}
|
|
||||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:bg-bg-surface-hover hover:text-fg transition-all text-xs"
|
|
||||||
>
|
|
||||||
−
|
−
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => map?.flyTo({ center: [center[1], center[0]], zoom, duration: 1000 })}
|
onClick={() => map?.flyTo({ center: [center[1], center[0]], zoom, duration: 1000 })}
|
||||||
className="w-[28px] h-[28px] bg-[rgba(18,25,41,0.65)] backdrop-blur-sm border border-[rgba(30,42,66,0.5)] rounded-sm text-fg-sub flex items-center justify-center hover:text-fg transition-all text-[10px]"
|
className="map-ctrl-btn text-[10px]"
|
||||||
>
|
>
|
||||||
🎯
|
🎯
|
||||||
</button>
|
</button>
|
||||||
@ -1732,16 +1721,16 @@ function MapLegend({
|
|||||||
|
|
||||||
if (dispersionResult && incidentCoord) {
|
if (dispersionResult && incidentCoord) {
|
||||||
return (
|
return (
|
||||||
<div className="absolute top-4 right-4 bg-[rgba(18,25,41,0.95)] backdrop-blur-xl border border-stroke rounded-lg min-w-[200px] z-[20]">
|
<div className="absolute top-4 right-4 bg-bg-surface/95 backdrop-blur-xl border border-stroke rounded-lg min-w-[200px] z-[20]">
|
||||||
{/* 헤더 + 최소화 버튼 */}
|
{/* 헤더 + 최소화 버튼 */}
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-between px-3.5 pt-3 pb-1 cursor-pointer"
|
className="flex items-center justify-between px-3.5 pt-3 pb-1 cursor-pointer"
|
||||||
onClick={() => setMinimized(!minimized)}
|
onClick={() => setMinimized(!minimized)}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-bold text-fg-disabled uppercase tracking-wider">
|
<span className="text-label-2 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
범례
|
범례
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[10px] text-fg-disabled hover:text-fg transition-colors">
|
<span className="text-label-2 text-fg-disabled hover:text-fg transition-colors">
|
||||||
{minimized ? '▶' : '▼'}
|
{minimized ? '▶' : '▼'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1750,53 +1739,49 @@ function MapLegend({
|
|||||||
<div className="flex items-center gap-1.5 mb-2.5">
|
<div className="flex items-center gap-1.5 mb-2.5">
|
||||||
<div className="text-base">📍</div>
|
<div className="text-base">📍</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-[11px] font-bold text-color-warning">사고 위치</h4>
|
<h4 className="text-label-2 font-bold text-color-warning">사고 위치</h4>
|
||||||
<div className="text-[8px] text-fg-disabled font-mono">
|
<div className="text-caption text-fg-disabled font-mono">
|
||||||
{incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
|
{incidentCoord.lat.toFixed(4)}°N, {incidentCoord.lon.toFixed(4)}°E
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="text-[9px] text-fg-sub mb-2 rounded"
|
className="text-caption text-fg-sub mb-2 rounded"
|
||||||
style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}
|
style={{ background: 'rgba(249,115,22,0.08)', padding: '8px' }}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between mb-[3px]">
|
<div className="flex justify-between mb-[3px]">
|
||||||
<span className="text-fg-disabled">물질</span>
|
<span className="text-fg-disabled">물질</span>
|
||||||
<span className="font-semibold text-color-warning">
|
<span className="font-medium text-color-warning">{dispersionResult.substance}</span>
|
||||||
{dispersionResult.substance}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mb-[3px]">
|
<div className="flex justify-between mb-[3px]">
|
||||||
<span className="text-fg-disabled">풍향</span>
|
<span className="text-fg-disabled">풍향</span>
|
||||||
<span className="font-semibold font-mono">
|
<span className="font-medium font-mono">SW {dispersionResult.windDirection}°</span>
|
||||||
SW {dispersionResult.windDirection}°
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-fg-disabled">확산 구역</span>
|
<span className="text-fg-disabled">확산 구역</span>
|
||||||
<span className="font-semibold text-color-accent">
|
<span className="font-medium text-color-accent">
|
||||||
{dispersionResult.zones.length}개
|
{dispersionResult.zones.length}개
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-[9px] font-bold text-fg-disabled mb-2">위험 구역</h5>
|
<h5 className="text-caption font-bold text-fg-disabled mb-2">위험 구역</h5>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ background: 'rgba(239,68,68,0.7)' }}
|
style={{ background: 'rgba(239,68,68,0.7)' }}
|
||||||
/>
|
/>
|
||||||
<span>치명적 위험 구역 (AEGL-3)</span>
|
<span>치명적 위험 구역 (AEGL-3)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ background: 'rgba(249,115,22,0.7)' }}
|
style={{ background: 'rgba(249,115,22,0.7)' }}
|
||||||
/>
|
/>
|
||||||
<span>높은 위험 구역 (AEGL-2)</span>
|
<span>높은 위험 구역 (AEGL-2)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded-full"
|
className="w-3 h-3 rounded-full"
|
||||||
style={{ background: 'rgba(234,179,8,0.7)' }}
|
style={{ background: 'rgba(234,179,8,0.7)' }}
|
||||||
@ -1809,8 +1794,8 @@ function MapLegend({
|
|||||||
className="flex items-center gap-1.5 mt-2 rounded"
|
className="flex items-center gap-1.5 mt-2 rounded"
|
||||||
style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}
|
style={{ padding: '6px', background: 'rgba(168,85,247,0.08)' }}
|
||||||
>
|
>
|
||||||
<div className="text-xs">🧭</div>
|
<div className="text-label-1">🧭</div>
|
||||||
<span className="text-[9px] text-fg-disabled">풍향 (방사형)</span>
|
<span className="text-caption text-fg-disabled">풍향 (방사형)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -1821,7 +1806,7 @@ function MapLegend({
|
|||||||
if (oilTrajectory.length > 0) {
|
if (oilTrajectory.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute top-4 right-4 bg-[rgba(18,25,41,0.92)] backdrop-blur-xl border border-stroke rounded-md z-[20]"
|
className="absolute top-4 right-4 bg-bg-surface/95 backdrop-blur-xl border border-stroke rounded-md z-[20]"
|
||||||
style={{ minWidth: 155 }}
|
style={{ minWidth: 155 }}
|
||||||
>
|
>
|
||||||
{/* 헤더 + 접기/펼치기 */}
|
{/* 헤더 + 접기/펼치기 */}
|
||||||
@ -1829,8 +1814,8 @@ function MapLegend({
|
|||||||
className="flex items-center justify-between px-3 py-2 cursor-pointer select-none"
|
className="flex items-center justify-between px-3 py-2 cursor-pointer select-none"
|
||||||
onClick={() => setMinimized(!minimized)}
|
onClick={() => setMinimized(!minimized)}
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-bold text-fg-sub font-korean">범례</span>
|
<span className="text-label-2 font-bold text-fg-sub font-korean">범례</span>
|
||||||
<span className="text-[9px] text-fg-disabled hover:text-fg transition-colors ml-3">
|
<span className="text-caption text-fg-disabled hover:text-fg transition-colors ml-3">
|
||||||
{minimized ? '▶' : '▼'}
|
{minimized ? '▶' : '▼'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -1839,7 +1824,7 @@ function MapLegend({
|
|||||||
<div className="px-3 pb-2.5 flex flex-col gap-[5px]">
|
<div className="px-3 pb-2.5 flex flex-col gap-[5px]">
|
||||||
{/* 모델별 색상 */}
|
{/* 모델별 색상 */}
|
||||||
{Array.from(selectedModels).map((model) => (
|
{Array.from(selectedModels).map((model) => (
|
||||||
<div key={model} className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div key={model} className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div
|
<div
|
||||||
className="w-[14px] h-[3px] rounded-sm"
|
className="w-[14px] h-[3px] rounded-sm"
|
||||||
style={{ background: MODEL_COLORS[model] }}
|
style={{ background: MODEL_COLORS[model] }}
|
||||||
@ -1848,14 +1833,14 @@ function MapLegend({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* 앙상블 */}
|
{/* 앙상블 */}
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="w-[14px] h-[3px] rounded-sm" style={{ background: '#a855f7' }} />
|
<div className="w-[14px] h-[3px] rounded-sm" style={{ background: '#a855f7' }} />
|
||||||
<span className="font-korean">앙상블</span>
|
<span className="font-korean">앙상블</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 오일펜스 라인 */}
|
{/* 오일펜스 라인 */}
|
||||||
<div className="h-px bg-border my-0.5" />
|
<div className="h-px bg-border my-0.5" />
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="flex gap-px">
|
<div className="flex gap-px">
|
||||||
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
|
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
|
||||||
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
|
<div className="w-[4px] h-[4px] rounded-full bg-[#f97316]" />
|
||||||
@ -1866,19 +1851,19 @@ function MapLegend({
|
|||||||
|
|
||||||
{/* 도달시간별 선종 */}
|
{/* 도달시간별 선종 */}
|
||||||
<div className="h-px bg-border my-0.5" />
|
<div className="h-px bg-border my-0.5" />
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="w-[14px] h-[3px] rounded-sm bg-[#ef4444]" />
|
<div className="w-[14px] h-[3px] rounded-sm bg-[#ef4444]" />
|
||||||
<span className="font-korean">위험 (<6h)</span>
|
<span className="font-korean">위험 (<6h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="w-[14px] h-[3px] rounded-sm bg-[#f97316]" />
|
<div className="w-[14px] h-[3px] rounded-sm bg-[#f97316]" />
|
||||||
<span className="font-korean">경고 (6~12h)</span>
|
<span className="font-korean">경고 (6~12h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="w-[14px] h-[3px] rounded-sm bg-[#eab308]" />
|
<div className="w-[14px] h-[3px] rounded-sm bg-[#eab308]" />
|
||||||
<span className="font-korean">주의 (12~24h)</span>
|
<span className="font-korean">주의 (12~24h)</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 text-[10px] text-fg-sub">
|
<div className="flex items-center gap-2 text-label-2 text-fg-sub">
|
||||||
<div className="w-[14px] h-[3px] rounded-sm bg-[#22c55e]" />
|
<div className="w-[14px] h-[3px] rounded-sm bg-[#22c55e]" />
|
||||||
<span className="font-korean">안전</span>
|
<span className="font-korean">안전</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -46,6 +46,7 @@
|
|||||||
/* color — Palette */
|
/* color — Palette */
|
||||||
--color-info: #3b82f6;
|
--color-info: #3b82f6;
|
||||||
--color-accent: #06b6d4;
|
--color-accent: #06b6d4;
|
||||||
|
--color-accent-muted: #0e7490;
|
||||||
--color-danger: #ef4444;
|
--color-danger: #ef4444;
|
||||||
--color-warning: #f97316;
|
--color-warning: #f97316;
|
||||||
--color-caution: #eab308;
|
--color-caution: #eab308;
|
||||||
@ -53,6 +54,8 @@
|
|||||||
--color-tertiary: #a855f7;
|
--color-tertiary: #a855f7;
|
||||||
--color-boom: #f59e0b;
|
--color-boom: #f59e0b;
|
||||||
--color-boom-hover: #fbbf24;
|
--color-boom-hover: #fbbf24;
|
||||||
|
--color-navy: #1e40af;
|
||||||
|
--color-navy-hover: #1d4ed8;
|
||||||
/* font */
|
/* font */
|
||||||
--font-korean:
|
--font-korean:
|
||||||
'PretendardGOV', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo',
|
'PretendardGOV', -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo',
|
||||||
@ -197,6 +200,9 @@
|
|||||||
--fg-disabled: #94a3b8;
|
--fg-disabled: #94a3b8;
|
||||||
--hover-overlay: rgba(0, 0, 0, 0.04);
|
--hover-overlay: rgba(0, 0, 0, 0.04);
|
||||||
--dropdown-bg: rgba(255, 255, 255, 0.97);
|
--dropdown-bg: rgba(255, 255, 255, 0.97);
|
||||||
|
--color-accent-muted: #0891b2;
|
||||||
|
--color-navy: #1d4ed8;
|
||||||
|
--color-navy-hover: #2563eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|||||||
@ -372,6 +372,41 @@
|
|||||||
box-shadow: 0 0 0 1px rgba(6, 182, 212, 0.3);
|
box-shadow: 0 0 0 1px rgba(6, 182, 212, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Map Zoom/Reset Controls */
|
||||||
|
.map-ctrl-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.15s;
|
||||||
|
background: rgba(15, 21, 36, 0.75);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-ctrl-btn:hover {
|
||||||
|
background: rgba(15, 21, 36, 0.9);
|
||||||
|
color: #ffffff;
|
||||||
|
border-color: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='light'] .map-ctrl-btn {
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
color: rgba(0, 0, 0, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme='light'] .map-ctrl-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
color: rgba(0, 0, 0, 0.9);
|
||||||
|
border-color: rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
/* ═══ Coordinate Display ═══ */
|
/* ═══ Coordinate Display ═══ */
|
||||||
.cod {
|
.cod {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -951,7 +986,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: #8b949e;
|
color: #8b949e;
|
||||||
font-family: var(--font-korean);
|
font-family: var(--font-korean);
|
||||||
font-size: 9px;
|
font-size: var(--font-size-label-2);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@ -15,9 +15,9 @@ const RISK_LABEL: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const RISK_STYLE: Record<string, { bg: string; color: string }> = {
|
const RISK_STYLE: Record<string, { bg: string; color: string }> = {
|
||||||
CRITICAL: { bg: 'rgba(239,68,68,0.2)', color: 'var(--color-danger)' },
|
CRITICAL: { bg: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' },
|
||||||
HIGH: { bg: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' },
|
HIGH: { bg: 'rgba(249,115,22,0.15)', color: 'var(--color-warning)' },
|
||||||
MEDIUM: { bg: 'rgba(249,115,22,0.15)', color: 'var(--color-warning)' },
|
MEDIUM: { bg: 'rgba(234,179,8,0.15)', color: 'var(--color-caution)' },
|
||||||
LOW: { bg: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' },
|
LOW: { bg: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' },
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,31 +101,28 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full bg-bg-base">
|
<div className="flex flex-col h-full bg-bg-base">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-5 py-4 border-b border-stroke flex justify-between items-center bg-bg-surface">
|
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-heading-3 font-bold text-fg">HNS 대기확산 분석 목록</h1>
|
||||||
|
<p className="text-title-3 text-fg-disabled mt-1">총 {analyses.length}건</p>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="text-base font-bold flex items-center gap-2">
|
<div className="relative">
|
||||||
<span className="text-[18px]">📋</span>
|
|
||||||
HNS 대기확산 분석 목록
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-fg-disabled bg-bg-card font-mono px-[10px] py-1 rounded-xl">
|
|
||||||
총 {analyses.length}건
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="검색..."
|
placeholder="검색..."
|
||||||
className="bg-bg-card border border-stroke rounded-sm text-[11px] px-3 py-2 w-[200px]"
|
className="w-64 px-4 py-2 text-title-3 bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onTabChange('analysis')}
|
onClick={() => onTabChange('analysis')}
|
||||||
className="text-color-warning text-[11px] font-semibold cursor-pointer flex items-center gap-1 px-4 py-2 rounded-sm"
|
className="px-4 py-2 text-title-3 font-semibold rounded-sm cursor-pointer text-color-accent"
|
||||||
style={{
|
style={{
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
border: '1px solid rgba(6,182,212,.3)',
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(239,68,68,0.1))',
|
background: 'rgba(6,182,212,.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="text-sm">+</span> 새 분석
|
+ 새 분석
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -133,69 +130,69 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
{/* Table */}
|
{/* Table */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center text-fg-disabled text-[12px] py-20">로딩 중...</div>
|
<div className="text-center text-fg-disabled text-label-1 py-20">로딩 중...</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full text-[11px] border-collapse">
|
<table className="w-full text-label-2 border-collapse">
|
||||||
<thead className="sticky top-0 z-10">
|
<thead className="sticky top-0 z-10">
|
||||||
<tr className="bg-bg-elevated border-b border-stroke">
|
<tr className="bg-bg-elevated border-b border-stroke">
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[50px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
번호
|
번호
|
||||||
</th>
|
</th>
|
||||||
<th className="text-left text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[180px]">
|
<th className="text-left text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
분석명
|
분석명
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[100px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
물질
|
물질
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[130px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
사고일시
|
사고일시
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[100px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
분석날짜
|
분석날짜
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[120px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
사고지점
|
사고지점
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[90px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
유출량
|
유출량
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[80px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
알고리즘
|
알고리즘
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
예측시간
|
예측시간
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
<div className="flex flex-col items-center gap-0.5">
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<span>AEGL-3</span>
|
<span>AEGL-3</span>
|
||||||
<span className="text-[8px] text-fg-disabled">생명위협</span>
|
<span className="text-caption text-fg-disabled">생명위협</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
<div className="flex flex-col items-center gap-0.5">
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<span>AEGL-2</span>
|
<span>AEGL-2</span>
|
||||||
<span className="text-[8px] text-fg-disabled">건강피해</span>
|
<span className="text-caption text-fg-disabled">건강피해</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 w-[70px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
<div className="flex flex-col items-center gap-0.5">
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
<span>AEGL-1</span>
|
<span>AEGL-1</span>
|
||||||
<span className="text-[8px] text-fg-disabled">불쾌감</span>
|
<span className="text-caption text-fg-disabled">불쾌감</span>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[80px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
위험등급
|
위험등급
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[90px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
피해반경
|
피해반경
|
||||||
</th>
|
</th>
|
||||||
<th className="text-center text-[10px] font-semibold text-fg-sub px-4 py-3 min-w-[120px]">
|
<th className="text-center text-label-1 font-bold text-fg-disabled px-4 py-3">
|
||||||
분석자
|
분석자
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{analyses.map((item, index) => {
|
{analyses.map((item) => {
|
||||||
const rslt = item.rsltData as Record<string, unknown> | null;
|
const rslt = item.rsltData as Record<string, unknown> | null;
|
||||||
const isLocal = !!(item as HnsAnalysisItem & { _isLocal?: boolean })._isLocal;
|
const isLocal = !!(item as HnsAnalysisItem & { _isLocal?: boolean })._isLocal;
|
||||||
const riskLabel = RISK_LABEL[item.riskCd || ''] || item.riskCd || '—';
|
const riskLabel = RISK_LABEL[item.riskCd || ''] || item.riskCd || '—';
|
||||||
@ -213,19 +210,10 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={item.hnsAnlysSn}
|
key={item.hnsAnlysSn}
|
||||||
className="border-b border-stroke cursor-pointer"
|
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onSelectAnalysis?.(item.hnsAnlysSn, isLocal && rslt ? rslt : undefined)
|
onSelectAnalysis?.(item.hnsAnlysSn, isLocal && rslt ? rslt : undefined)
|
||||||
}
|
}
|
||||||
style={{
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
background: index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)',
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'var(--bg-elevated)')}
|
|
||||||
onMouseLeave={(e) =>
|
|
||||||
(e.currentTarget.style.background =
|
|
||||||
index % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.01)')
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<td className="text-center text-fg-disabled font-mono px-4 py-3">
|
<td className="text-center text-fg-disabled font-mono px-4 py-3">
|
||||||
{item.hnsAnlysSn}
|
{item.hnsAnlysSn}
|
||||||
@ -233,23 +221,23 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
|
<td className="font-medium px-4 py-3">{item.anlysNm}</td>
|
||||||
<td className="text-center px-4 py-3">
|
<td className="text-center px-4 py-3">
|
||||||
<span
|
<span
|
||||||
className="text-[9px] font-semibold text-color-warning px-2 py-1 rounded"
|
className="text-caption font-semibold text-color-accent px-2 py-1 rounded"
|
||||||
style={{ background: 'rgba(249,115,22,0.12)' }}
|
style={{ background: 'rgba(6,182,212,0.12)' }}
|
||||||
>
|
>
|
||||||
{substanceTag(item.sbstNm)}
|
{substanceTag(item.sbstNm)}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center text-fg-sub font-mono text-[10px] px-4 py-3">
|
<td className="text-center text-fg-sub font-mono text-label-2 px-4 py-3">
|
||||||
{formatDate(item.acdntDtm, 'full')}
|
{formatDate(item.acdntDtm, 'full')}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center text-fg-disabled font-mono text-[10px] px-4 py-3">
|
<td className="text-center text-fg-disabled font-mono text-label-2 px-4 py-3">
|
||||||
{formatDate(item.regDtm, 'date')}
|
{formatDate(item.regDtm, 'date')}
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center text-fg-sub px-4 py-3">{item.locNm || '—'}</td>
|
<td className="text-center text-fg-sub px-4 py-3">{item.locNm || '—'}</td>
|
||||||
<td className="text-center text-fg-sub font-mono px-4 py-3">{amount}</td>
|
<td className="text-center text-fg-sub font-mono px-4 py-3">{amount}</td>
|
||||||
<td className="text-center px-4 py-3">
|
<td className="text-center px-4 py-3">
|
||||||
<span
|
<span
|
||||||
className="text-[9px] font-semibold text-color-accent px-2 py-1 rounded"
|
className="text-caption font-semibold text-color-accent px-2 py-1 rounded"
|
||||||
style={{ background: 'rgba(6,182,212,0.12)' }}
|
style={{ background: 'rgba(6,182,212,0.12)' }}
|
||||||
>
|
>
|
||||||
{item.algoCd || '—'}
|
{item.algoCd || '—'}
|
||||||
@ -262,7 +250,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full mx-auto"
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
style={{
|
style={{
|
||||||
background: aegl3 ? 'rgba(239,68,68,0.8)' : 'rgba(255,255,255,0.1)',
|
background: aegl3 ? 'var(--color-danger)' : 'rgba(255,255,255,0.1)',
|
||||||
border: aegl3 ? 'none' : '1px solid var(--stroke-default)',
|
border: aegl3 ? 'none' : '1px solid var(--stroke-default)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -271,7 +259,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full mx-auto"
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
style={{
|
style={{
|
||||||
background: aegl2 ? 'rgba(249,115,22,0.8)' : 'rgba(255,255,255,0.1)',
|
background: aegl2 ? 'var(--color-warning)' : 'rgba(255,255,255,0.1)',
|
||||||
border: aegl2 ? 'none' : '1px solid var(--stroke-default)',
|
border: aegl2 ? 'none' : '1px solid var(--stroke-default)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -280,21 +268,21 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
<div
|
<div
|
||||||
className="w-6 h-6 rounded-full mx-auto"
|
className="w-6 h-6 rounded-full mx-auto"
|
||||||
style={{
|
style={{
|
||||||
background: aegl1 ? 'rgba(234,179,8,0.8)' : 'rgba(255,255,255,0.1)',
|
background: aegl1 ? 'var(--color-caution)' : 'rgba(255,255,255,0.1)',
|
||||||
border: aegl1 ? 'none' : '1px solid var(--stroke-default)',
|
border: aegl1 ? 'none' : '1px solid var(--stroke-default)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center px-4 py-3">
|
<td className="text-center px-4 py-3">
|
||||||
<span
|
<span
|
||||||
className="text-[9px] font-semibold px-[10px] py-1 rounded"
|
className="text-caption font-semibold px-[10px] py-1 rounded"
|
||||||
style={{ background: riskStyle.bg, color: riskStyle.color }}
|
style={{ background: riskStyle.bg, color: riskStyle.color }}
|
||||||
>
|
>
|
||||||
{riskLabel}
|
{riskLabel}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="text-center text-fg-sub font-mono px-4 py-3">{damageRadius}</td>
|
<td className="text-center text-fg-sub font-mono px-4 py-3">{damageRadius}</td>
|
||||||
<td className="text-center text-fg-disabled text-[10px] px-4 py-3">
|
<td className="text-center text-fg-disabled text-label-2 px-4 py-3">
|
||||||
{item.analystNm || '—'}
|
{item.analystNm || '—'}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -305,7 +293,7 @@ export function HNSAnalysisListTable({ onTabChange, onSelectAnalysis }: HNSAnaly
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && analyses.length === 0 && (
|
{!loading && analyses.length === 0 && (
|
||||||
<div className="text-center text-fg-disabled text-[12px] py-20">
|
<div className="text-center text-fg-disabled text-label-1 py-20">
|
||||||
분석 데이터가 없습니다.
|
분석 데이터가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -70,6 +70,9 @@ export function HNSLeftPanel({
|
|||||||
}: HNSLeftPanelProps) {
|
}: HNSLeftPanelProps) {
|
||||||
const [incidents, setIncidents] = useState<IncidentListItem[]>([]);
|
const [incidents, setIncidents] = useState<IncidentListItem[]>([]);
|
||||||
const [selectedIncidentSn, setSelectedIncidentSn] = useState('');
|
const [selectedIncidentSn, setSelectedIncidentSn] = useState('');
|
||||||
|
const [expandedSections, setExpandedSections] = useState({ accident: true, params: true });
|
||||||
|
const toggleSection = (key: 'accident' | 'params') =>
|
||||||
|
setExpandedSections((prev) => ({ ...prev, [key]: !prev[key] }));
|
||||||
|
|
||||||
const [accidentName, setAccidentName] = useState('');
|
const [accidentName, setAccidentName] = useState('');
|
||||||
const [accidentDate, setAccidentDate] = useState<string>(() => {
|
const [accidentDate, setAccidentDate] = useState<string>(() => {
|
||||||
@ -218,40 +221,35 @@ export function HNSLeftPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="w-80 min-w-[320px] flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden">
|
<div className="w-80 min-w-[320px] flex flex-col h-full bg-bg-surface border-r border-stroke overflow-hidden">
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div className="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-base">
|
|
||||||
{activeSubTab === 'analysis' && (
|
|
||||||
<div className="p-4">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex items-center justify-between mb-[14px]">
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<div
|
<div
|
||||||
className="w-9 h-9 rounded-md flex items-center justify-center text-[18px]"
|
className="flex-1 overflow-y-scroll scrollbar-thin scrollbar-thumb-border-light scrollbar-track-transparent bg-bg-base"
|
||||||
style={{
|
style={{ scrollbarGutter: 'stable' }}
|
||||||
background:
|
|
||||||
'linear-gradient(135deg, rgba(249,115,22,0.15), rgba(168,85,247,0.1))',
|
|
||||||
border: '1px solid rgba(249,115,22,0.25)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
🧪
|
{activeSubTab === 'analysis' && (
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean">
|
{/* 헤더 */}
|
||||||
|
<div className="p-4 border-b border-stroke">
|
||||||
|
<div className="text-title-4 font-bold text-fg-sub font-korean">
|
||||||
HNS 대기확산 예측
|
HNS 대기확산 예측
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-fg-disabled">
|
<div className="text-label-2 text-fg-disabled mt-0.5">
|
||||||
ALOHA/CAMEO 기반 대기확산 시뮬레이션
|
ALOHA/CAMEO 기반 대기확산 시뮬레이션
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Single Column Layout */}
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
{/* 사고 기본정보 */}
|
{/* 사고 기본정보 */}
|
||||||
<div>
|
<div className="border-b border-stroke">
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
<div
|
||||||
📋 사고 기본정보
|
onClick={() => toggleSection('accident')}
|
||||||
|
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
|
||||||
|
>
|
||||||
|
<h3 className="text-title-4 font-bold text-fg-sub font-korean">사고 기본정보</h3>
|
||||||
|
<span className="text-label-2 text-fg-disabled">
|
||||||
|
{expandedSections.accident ? '▼' : '▶'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{expandedSections.accident && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
<div className="flex flex-col gap-[6px]">
|
<div className="flex flex-col gap-[6px]">
|
||||||
{/* 사고명 직접 입력 */}
|
{/* 사고명 직접 입력 */}
|
||||||
<input
|
<input
|
||||||
@ -275,7 +273,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 사고 발생 일시 */}
|
{/* 사고 발생 일시 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
사고 발생 일시
|
사고 발생 일시
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
@ -328,7 +326,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* DMS 표시 */}
|
{/* DMS 표시 */}
|
||||||
<div
|
<div
|
||||||
className="text-[9px] text-fg-disabled font-mono border border-stroke bg-bg-base"
|
className="text-caption text-fg-disabled font-mono border border-stroke bg-bg-base"
|
||||||
style={{ padding: '4px 8px', borderRadius: 'var(--radius-sm)' }}
|
style={{ padding: '4px 8px', borderRadius: 'var(--radius-sm)' }}
|
||||||
>
|
>
|
||||||
{incidentCoord
|
{incidentCoord
|
||||||
@ -417,7 +415,7 @@ export function HNSLeftPanel({
|
|||||||
{/* 알고리즘 선택 */}
|
{/* 알고리즘 선택 */}
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
예측 알고리즘
|
예측 알고리즘
|
||||||
</label>
|
</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
@ -433,7 +431,7 @@ export function HNSLeftPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
확산 등급 기준
|
확산 등급 기준
|
||||||
</label>
|
</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
@ -451,18 +449,29 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 모델 파라미터 & 물질 정보 */}
|
{/* 모델 파라미터 & 물질 정보 */}
|
||||||
<div>
|
<div className="border-b border-stroke">
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
<div
|
||||||
🧪{' '}
|
onClick={() => toggleSection('params')}
|
||||||
|
className="flex items-center justify-between p-4 cursor-pointer hover:bg-[rgba(255,255,255,0.02)]"
|
||||||
|
>
|
||||||
|
<h3 className="text-title-4 font-bold text-fg-sub font-korean">
|
||||||
{releaseType === '연속 유출'
|
{releaseType === '연속 유출'
|
||||||
? 'Plume'
|
? 'Plume'
|
||||||
: releaseType === '순간 유출'
|
: releaseType === '순간 유출'
|
||||||
? 'Puff'
|
? 'Puff'
|
||||||
: 'Dense Gas'}{' '}
|
: 'Dense Gas'}{' '}
|
||||||
파라미터
|
파라미터
|
||||||
|
</h3>
|
||||||
|
<span className="text-label-2 text-fg-disabled">
|
||||||
|
{expandedSections.params ? '▼' : '▶'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
{expandedSections.params && (
|
||||||
|
<div className="px-4 pb-4">
|
||||||
<div className="flex flex-col gap-[6px]">
|
<div className="flex flex-col gap-[6px]">
|
||||||
{/* 모델별 입력 파라미터 */}
|
{/* 모델별 입력 파라미터 */}
|
||||||
<div
|
<div
|
||||||
@ -477,7 +486,7 @@ export function HNSLeftPanel({
|
|||||||
<div className="flex flex-col gap-[6px]">
|
<div className="flex flex-col gap-[6px]">
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
배출률 (g/s)
|
배출률 (g/s)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -490,7 +499,7 @@ export function HNSLeftPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
지속시간 (s)
|
지속시간 (s)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -504,7 +513,7 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
누출 높이 (m)
|
누출 높이 (m)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -523,7 +532,7 @@ export function HNSLeftPanel({
|
|||||||
{releaseType === '순간 유출' && (
|
{releaseType === '순간 유출' && (
|
||||||
<div className="flex flex-col gap-[6px]">
|
<div className="flex flex-col gap-[6px]">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
총 누출량 (g)
|
총 누출량 (g)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -536,7 +545,7 @@ export function HNSLeftPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
누출 높이 (m)
|
누출 높이 (m)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -556,7 +565,7 @@ export function HNSLeftPanel({
|
|||||||
<div className="flex flex-col gap-[6px]">
|
<div className="flex flex-col gap-[6px]">
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
배출률 (g/s)
|
배출률 (g/s)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -569,7 +578,7 @@ export function HNSLeftPanel({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
풀 반경 (m)
|
풀 반경 (m)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -583,7 +592,7 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">
|
<label className="text-label-2 text-fg-disabled block mb-0.5">
|
||||||
누출 높이 (m)
|
누출 높이 (m)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -599,7 +608,7 @@ export function HNSLeftPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 모델 설명 */}
|
{/* 모델 설명 */}
|
||||||
<div className="text-[9px] text-fg-disabled mt-1 leading-[1.4]">
|
<div className="text-caption text-fg-disabled mt-1 leading-[1.4]">
|
||||||
{releaseType === '연속 유출' &&
|
{releaseType === '연속 유출' &&
|
||||||
'정상상태 연속 배출. 바람 방향으로 플룸이 형성됩니다.'}
|
'정상상태 연속 배출. 바람 방향으로 플룸이 형성됩니다.'}
|
||||||
{releaseType === '순간 유출' &&
|
{releaseType === '순간 유출' &&
|
||||||
@ -613,14 +622,14 @@ export function HNSLeftPanel({
|
|||||||
<div
|
<div
|
||||||
className="p-2 rounded-sm mt-0.5"
|
className="p-2 rounded-sm mt-0.5"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(249,115,22,0.05)',
|
background: 'rgba(6,182,212,0.05)',
|
||||||
border: '1px solid rgba(249,115,22,0.12)',
|
border: '1px solid rgba(6,182,212,0.12)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[10px] font-bold text-color-warning mb-1">
|
<div className="text-label-2 font-bold text-color-accent mb-1">
|
||||||
⚠ 물질 위험 특성
|
⚠ 물질 위험 특성
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-[3px] text-[9px]">
|
<div className="grid grid-cols-2 gap-[3px] text-caption">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-fg-disabled">분자량</span>
|
<span className="text-fg-disabled">분자량</span>
|
||||||
<span className="font-mono">{tox.mw} g/mol</span>
|
<span className="font-mono">{tox.mw} g/mol</span>
|
||||||
@ -635,7 +644,7 @@ export function HNSLeftPanel({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<span className="text-fg-disabled">IDLH</span>
|
<span className="text-fg-disabled">IDLH</span>
|
||||||
<span className="text-color-danger font-semibold font-mono">
|
<span className="text-color-caution font-semibold font-mono">
|
||||||
{tox.idlh} ppm
|
{tox.idlh} ppm
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -646,14 +655,14 @@ export function HNSLeftPanel({
|
|||||||
<div
|
<div
|
||||||
className="p-2 rounded-sm"
|
className="p-2 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(168,85,247,0.05)',
|
background: 'rgba(6,182,212,0.05)',
|
||||||
border: '1px solid rgba(168,85,247,0.12)',
|
border: '1px solid rgba(6,182,212,0.12)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[10px] font-bold text-color-tertiary mb-1">
|
<div className="text-label-2 font-bold text-color-accent mb-1">
|
||||||
📊 확산 등급 기준 (AEGL)
|
📊 확산 등급 기준 (AEGL)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[9px]">
|
<div className="flex flex-col gap-0.5 text-caption">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-[2px]"
|
className="w-2 h-2 rounded-[2px]"
|
||||||
@ -666,7 +675,7 @@ export function HNSLeftPanel({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-[2px]"
|
className="w-2 h-2 rounded-[2px]"
|
||||||
style={{ background: 'rgba(249,115,22,0.7)' }}
|
style={{ background: 'rgba(6,182,212,0.7)' }}
|
||||||
></div>
|
></div>
|
||||||
<span className="text-fg-disabled">
|
<span className="text-fg-disabled">
|
||||||
AEGL-2 (건강피해) — {tox.aegl2} ppm
|
AEGL-2 (건강피해) — {tox.aegl2} ppm
|
||||||
@ -675,29 +684,32 @@ export function HNSLeftPanel({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<div
|
<div
|
||||||
className="w-2 h-2 rounded-[2px]"
|
className="w-2 h-2 rounded-[2px]"
|
||||||
style={{ background: 'rgba(234,179,8,0.7)' }}
|
style={{ background: 'rgba(6,182,212,0.7)' }}
|
||||||
></div>
|
></div>
|
||||||
<span className="text-fg-disabled">AEGL-1 (불쾌감) — {tox.aegl1} ppm</span>
|
<span className="text-fg-disabled">
|
||||||
|
AEGL-1 (불쾌감) — {tox.aegl1} ppm
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 실행 버튼 */}
|
{/* 실행 버튼 */}
|
||||||
<div className="flex flex-col gap-1 mt-2">
|
<div className="flex flex-col gap-1 px-4 py-3">
|
||||||
<button
|
<button
|
||||||
className="prd-btn pri"
|
className="prd-btn pri"
|
||||||
style={{ padding: '7px', fontSize: '11px' }}
|
style={{ padding: '7px', fontSize: 'var(--font-size-label-2)' }}
|
||||||
onClick={onRunPrediction}
|
onClick={onRunPrediction}
|
||||||
disabled={isRunningPrediction}
|
disabled={isRunningPrediction}
|
||||||
>
|
>
|
||||||
{isRunningPrediction ? '⏳ 실행 중...' : '🧪 대기확산 예측 실행'}
|
{isRunningPrediction ? '⏳ 실행 중...' : '대기확산 예측 실행'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="prd-btn sec"
|
className="prd-btn sec"
|
||||||
style={{ padding: '7px', fontSize: '11px' }}
|
style={{ padding: '7px', fontSize: 'var(--font-size-label-2)' }}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
>
|
>
|
||||||
초기화
|
초기화
|
||||||
@ -711,29 +723,29 @@ export function HNSLeftPanel({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-2.5 mb-[14px]">
|
<div className="flex items-center gap-2.5 mb-[14px]">
|
||||||
<div
|
<div
|
||||||
className="w-9 h-9 rounded-md flex items-center justify-center text-[18px]"
|
className="w-9 h-9 rounded-md flex items-center justify-center text-title-1"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, rgba(6,182,212,0.15), rgba(168,85,247,0.1))',
|
background: 'var(--bg-elevated)',
|
||||||
border: '1px solid rgba(6,182,212,0.25)',
|
border: '1px solid rgba(6,182,212,0.25)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
📋
|
📋
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean">분석 목록</div>
|
<div className="text-title-4 font-bold text-fg-sub font-korean">분석 목록</div>
|
||||||
<div className="text-[10px] text-fg-disabled">저장된 대기확산 예측 결과</div>
|
<div className="text-label-2 text-fg-disabled">저장된 대기확산 예측 결과</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 필터 섹션 */}
|
{/* 필터 섹션 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-[14px] mb-3">
|
<div className="bg-bg-card border border-stroke rounded-md p-[14px] mb-3">
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
<div className="text-title-4 font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
||||||
🔍 필터
|
🔍 필터
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* 기간 선택 */}
|
{/* 기간 선택 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">기간</label>
|
<label className="text-label-2 text-fg-disabled block mb-0.5">기간</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="최근 7일"
|
value="최근 7일"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -748,7 +760,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 물질 분류 */}
|
{/* 물질 분류 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">물질 분류</label>
|
<label className="text-label-2 text-fg-disabled block mb-0.5">물질 분류</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="전체"
|
value="전체"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -764,7 +776,7 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 위험도 */}
|
{/* 위험도 */}
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] text-fg-disabled block mb-0.5">위험도</label>
|
<label className="text-label-2 text-fg-disabled block mb-0.5">위험도</label>
|
||||||
<ComboBox
|
<ComboBox
|
||||||
value="전체"
|
value="전체"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@ -781,21 +793,21 @@ export function HNSLeftPanel({
|
|||||||
|
|
||||||
{/* 통계 요약 */}
|
{/* 통계 요약 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-[14px]">
|
<div className="bg-bg-card border border-stroke rounded-md p-[14px]">
|
||||||
<div className="text-[13px] font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
<div className="text-title-4 font-bold text-fg-sub font-korean mb-3 flex items-center gap-1.5">
|
||||||
📊 통계
|
📊 통계
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||||
<span className="text-[10px] text-fg-disabled">전체 분석</span>
|
<span className="text-label-2 text-fg-disabled">전체 분석</span>
|
||||||
<span className="text-sm font-bold text-color-accent font-mono">8건</span>
|
<span className="text-sm font-bold text-color-accent font-mono">8건</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||||
<span className="text-[10px] text-fg-disabled">고위험 (AEGL-3)</span>
|
<span className="text-label-2 text-fg-disabled">고위험 (AEGL-3)</span>
|
||||||
<span className="text-sm font-bold text-color-danger font-mono">3건</span>
|
<span className="text-sm font-bold text-color-caution font-mono">3건</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
<div className="flex justify-between items-center p-2 bg-bg-base rounded">
|
||||||
<span className="text-[10px] text-fg-disabled">중위험 (AEGL-2)</span>
|
<span className="text-label-2 text-fg-disabled">중위험 (AEGL-2)</span>
|
||||||
<span className="text-sm font-bold text-color-warning font-mono">5건</span>
|
<span className="text-sm font-bold text-color-accent font-mono">5건</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -82,20 +82,20 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="px-5 py-4 border-b border-stroke flex items-center gap-3">
|
<div className="px-5 py-4 border-b border-stroke flex items-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="w-9 h-9 rounded-[10px] border border-[rgba(249,115,22,0.3)] flex items-center justify-center text-base shrink-0"
|
className="w-9 h-9 rounded-[10px] border border-[rgba(6,182,212,0.3)] flex items-center justify-center text-base shrink-0"
|
||||||
style={{
|
style={{
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
background: 'linear-gradient(135deg, rgba(6,182,212,0.2), rgba(59,130,246,0.15))',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🔄
|
🔄
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-[15px] font-bold m-0">대기확산 재계산</h2>
|
<h2 className="text-title-2 font-bold m-0">대기확산 재계산</h2>
|
||||||
<div className="text-[10px] text-fg-disabled mt-0.5">조건을 변경하여 재계산합니다</div>
|
<div className="text-label-2 text-fg-disabled mt-0.5">조건을 변경하여 재계산합니다</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="w-7 h-7 rounded-md border border-stroke bg-bg-card text-fg-disabled text-xs cursor-pointer flex items-center justify-center shrink-0"
|
className="w-7 h-7 rounded-md border border-stroke bg-bg-card text-fg-disabled text-label-1 cursor-pointer flex items-center justify-center shrink-0"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -183,16 +183,16 @@ export function HNSRecalcModal({ isOpen, onClose, onSubmit, currentParams }: HNS
|
|||||||
<div className="px-5 py-[14px] border-t border-stroke flex gap-2">
|
<div className="px-5 py-[14px] border-t border-stroke flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex-1 py-2.5 text-xs font-semibold rounded-md cursor-pointer bg-bg-card border border-stroke text-fg-sub"
|
className="flex-1 py-2.5 text-label-1 font-semibold rounded-md cursor-pointer bg-bg-card border border-stroke text-fg-sub"
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
onClick={handleRun}
|
||||||
className="flex-[2] py-2.5 text-xs font-bold rounded-md text-white"
|
className="flex-[2] py-2.5 text-label-1 font-bold rounded-md text-white"
|
||||||
style={{
|
style={{
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
background: 'linear-gradient(135deg, var(--color-warning), #ef4444)',
|
background: 'linear-gradient(135deg, var(--color-accent), #ef4444)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -36,7 +36,7 @@ export function HNSRightPanel({
|
|||||||
if (!dispersionResult) {
|
if (!dispersionResult) {
|
||||||
return (
|
return (
|
||||||
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto">
|
<div className="w-[300px] bg-bg-surface border-l border-stroke p-4 overflow-auto">
|
||||||
<div className="flex flex-col gap-3 items-center justify-center h-full text-fg-disabled text-xs">
|
<div className="flex flex-col gap-3 items-center justify-center h-full text-fg-disabled text-label-1">
|
||||||
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
<div style={{ fontSize: '32px', opacity: 0.3 }}>📊</div>
|
||||||
<div>예측 실행 후 결과가 표시됩니다</div>
|
<div>예측 실행 후 결과가 표시됩니다</div>
|
||||||
</div>
|
</div>
|
||||||
@ -67,13 +67,13 @@ export function HNSRightPanel({
|
|||||||
width: '6px',
|
width: '6px',
|
||||||
height: '6px',
|
height: '6px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'var(--color-warning)',
|
background: 'var(--color-accent)',
|
||||||
animation: 'pulse 1.5s infinite',
|
animation: 'pulse 1.5s infinite',
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<h3 className="text-[13px] font-bold m-0">예측 결과</h3>
|
<h3 className="text-title-4 font-bold m-0">예측 결과</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[10px] text-fg-disabled font-mono">
|
<div className="text-label-2 text-fg-disabled font-mono">
|
||||||
{dispersionResult.substance} · {modelLabel}
|
{dispersionResult.substance} · {modelLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -81,89 +81,80 @@ export function HNSRightPanel({
|
|||||||
{/* KPI Cards */}
|
{/* KPI Cards */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* 최대 농도 */}
|
{/* 최대 농도 */}
|
||||||
<div className="p-3 bg-bg-card border border-[rgba(239,68,68,0.2)] rounded-[var(--radius-sm)]">
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
||||||
<div className="text-[10px] text-fg-disabled mb-1.5">최대 농도</div>
|
<div className="text-label-2 text-fg-disabled mb-1.5">최대 농도</div>
|
||||||
<div className="text-[20px] font-bold font-mono text-color-danger">
|
<div className="text-title-1 font-bold font-mono text-color-caution">
|
||||||
{maxConc > 0 ? maxConc.toFixed(1) : '—'}{' '}
|
{maxConc > 0 ? maxConc.toFixed(1) : '—'}{' '}
|
||||||
<span className="text-[10px] font-medium">ppm</span>
|
<span className="text-label-2 font-medium">ppm</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 확산 면적 */}
|
{/* 확산 면적 */}
|
||||||
<div className="p-3 bg-bg-card border border-[rgba(6,182,212,0.2)] rounded-[var(--radius-sm)]">
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
||||||
<div className="text-[10px] text-fg-disabled mb-1.5">AEGL-1 확산 면적</div>
|
<div className="text-label-2 text-fg-disabled mb-1.5">AEGL-1 확산 면적</div>
|
||||||
<div className="text-[20px] font-bold font-mono text-color-accent">
|
<div className="text-title-1 font-bold font-mono text-color-accent">
|
||||||
{area > 0 ? area.toFixed(2) : '—'} <span className="text-[10px] font-medium">km²</span>
|
{area > 0 ? area.toFixed(2) : '—'} <span className="text-label-2 font-medium">km²</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 풍속 */}
|
{/* 풍속 */}
|
||||||
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
||||||
<div className="text-[10px] text-fg-disabled mb-1.5">풍속</div>
|
<div className="text-label-2 text-fg-disabled mb-1.5">풍속</div>
|
||||||
<div className="text-[20px] font-bold font-mono">
|
<div className="text-title-1 font-bold font-mono">
|
||||||
{windSpd.toFixed(1)} <span className="text-[10px] font-medium">m/s</span>
|
{windSpd.toFixed(1)} <span className="text-label-2 font-medium">m/s</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 풍향 */}
|
{/* 풍향 */}
|
||||||
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
<div className="p-3 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
||||||
<div className="text-[10px] text-fg-disabled mb-1.5">풍향</div>
|
<div className="text-label-2 text-fg-disabled mb-1.5">풍향</div>
|
||||||
<div className="text-[20px] font-bold font-mono">
|
<div className="text-title-1 font-bold font-mono">
|
||||||
{windDirToCompass(windDir)} <span className="text-[10px] font-medium">{windDir}°</span>
|
{windDirToCompass(windDir)} <span className="text-label-2 font-medium">{windDir}°</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AEGL Zone Details */}
|
{/* AEGL Zone Details */}
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-[11px] font-semibold text-fg-sub mt-0 mb-2.5">AEGL 구역 상세</h4>
|
<h4 className="text-label-2 font-semibold text-fg-sub mt-0 mb-2.5">AEGL 구역 상세</h4>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{/* AEGL-3 */}
|
{/* AEGL-3 */}
|
||||||
<div
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
||||||
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
||||||
style={{ borderLeft: '3px solid rgba(239,68,68,1)' }}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span className="text-[11px] font-semibold">AEGL-3 (생명위협)</span>
|
<span className="text-label-2 font-semibold">AEGL-3 (생명위협)</span>
|
||||||
<span className="text-[10px] font-mono text-fg-disabled">
|
<span className="text-label-2 font-mono text-fg-disabled">
|
||||||
{computedResult?.aeglDistances.aegl3 || 0}m
|
{computedResult?.aeglDistances.aegl3 || 0}m
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[10px] text-fg-disabled">
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
||||||
<span>{dispersionResult.concentration['AEGL-3']}</span>
|
<span>{dispersionResult.concentration['AEGL-3']}</span>
|
||||||
<span className="font-mono">{computedResult?.aeglAreas.aegl3 ?? 0} km²</span>
|
<span className="font-mono">{computedResult?.aeglAreas.aegl3 ?? 0} km²</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AEGL-2 */}
|
{/* AEGL-2 */}
|
||||||
<div
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
||||||
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
||||||
style={{ borderLeft: '3px solid rgba(249,115,22,1)' }}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span className="text-[11px] font-semibold">AEGL-2 (건강피해)</span>
|
<span className="text-label-2 font-semibold">AEGL-2 (건강피해)</span>
|
||||||
<span className="text-[10px] font-mono text-fg-disabled">
|
<span className="text-label-2 font-mono text-fg-disabled">
|
||||||
{computedResult?.aeglDistances.aegl2 || 0}m
|
{computedResult?.aeglDistances.aegl2 || 0}m
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[10px] text-fg-disabled">
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
||||||
<span>{dispersionResult.concentration['AEGL-2']}</span>
|
<span>{dispersionResult.concentration['AEGL-2']}</span>
|
||||||
<span className="font-mono">{computedResult?.aeglAreas.aegl2 ?? 0} km²</span>
|
<span className="font-mono">{computedResult?.aeglAreas.aegl2 ?? 0} km²</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AEGL-1 */}
|
{/* AEGL-1 */}
|
||||||
<div
|
<div className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]">
|
||||||
className="py-2.5 px-3 bg-bg-elevated rounded-[var(--radius-sm)]"
|
|
||||||
style={{ borderLeft: '3px solid rgba(234,179,8,1)' }}
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-1">
|
<div className="flex justify-between items-center mb-1">
|
||||||
<span className="text-[11px] font-semibold">AEGL-1 (불쾌감)</span>
|
<span className="text-label-2 font-semibold">AEGL-1 (불쾌감)</span>
|
||||||
<span className="text-[10px] font-mono text-fg-disabled">
|
<span className="text-label-2 font-mono text-fg-disabled">
|
||||||
{computedResult?.aeglDistances.aegl1 || 0}m
|
{computedResult?.aeglDistances.aegl1 || 0}m
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-[10px] text-fg-disabled">
|
<div className="flex justify-between text-label-2 text-fg-disabled">
|
||||||
<span>{dispersionResult.concentration['AEGL-1']}</span>
|
<span>{dispersionResult.concentration['AEGL-1']}</span>
|
||||||
<span className="font-mono">{computedResult?.aeglAreas.aegl1 ?? 0} km²</span>
|
<span className="font-mono">{computedResult?.aeglAreas.aegl1 ?? 0} km²</span>
|
||||||
</div>
|
</div>
|
||||||
@ -174,10 +165,10 @@ export function HNSRightPanel({
|
|||||||
{/* 시간 정보 (puff/dense_gas) */}
|
{/* 시간 정보 (puff/dense_gas) */}
|
||||||
{computedResult && computedResult.modelType !== 'plume' && (
|
{computedResult && computedResult.modelType !== 'plume' && (
|
||||||
<div className="p-2.5 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
<div className="p-2.5 bg-bg-card border border-stroke rounded-[var(--radius-sm)]">
|
||||||
<div className="text-[10px] text-fg-disabled mb-1">현재 시뮬레이션 시간</div>
|
<div className="text-label-2 text-fg-disabled mb-1">현재 시뮬레이션 시간</div>
|
||||||
<div className="text-[14px] font-bold font-mono text-color-accent">
|
<div className="text-title-3 font-bold font-mono text-color-accent">
|
||||||
t = {computedResult.timeStep}s
|
t = {computedResult.timeStep}s
|
||||||
<span className="text-[10px] font-normal text-fg-disabled ml-1.5">
|
<span className="text-label-2 font-normal text-fg-disabled ml-1.5">
|
||||||
({(computedResult.timeStep / 60).toFixed(1)}분)
|
({(computedResult.timeStep / 60).toFixed(1)}분)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -185,7 +176,7 @@ export function HNSRightPanel({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Timestamp */}
|
{/* Timestamp */}
|
||||||
<div className="mt-auto pt-3 border-t border-stroke text-[10px] text-fg-disabled font-mono">
|
<div className="mt-auto pt-3 border-t border-stroke text-label-2 text-fg-disabled font-mono">
|
||||||
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
예측 시각: {new Date(dispersionResult.timestamp).toLocaleString('ko-KR')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -193,19 +184,19 @@ export function HNSRightPanel({
|
|||||||
<div className="flex gap-1.5 pt-3 border-t border-stroke">
|
<div className="flex gap-1.5 pt-3 border-t border-stroke">
|
||||||
<button
|
<button
|
||||||
onClick={onSave}
|
onClick={onSave}
|
||||||
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean"
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
|
||||||
>
|
>
|
||||||
💾 저장
|
💾 저장
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onOpenRecalc}
|
onClick={onOpenRecalc}
|
||||||
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-color-warning font-korean"
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-bg-elevated border border-stroke text-fg font-korean"
|
||||||
>
|
>
|
||||||
🔄 재계산
|
🔄 재계산
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={onOpenReport}
|
onClick={onOpenReport}
|
||||||
className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean"
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-color-navy text-static-white hover:bg-color-navy-hover font-korean"
|
||||||
>
|
>
|
||||||
📄 보고서
|
📄 보고서
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -49,10 +49,10 @@ interface HnsMaterial {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEVERITY_STYLE: Record<Severity, { bg: string; color: string }> = {
|
const SEVERITY_STYLE: Record<Severity, { bg: string; color: string }> = {
|
||||||
CRITICAL: { bg: 'rgba(239,68,68,0.2)', color: '#f87171' },
|
CRITICAL: { bg: 'rgba(239,68,68,0.15)', color: 'var(--color-danger)' },
|
||||||
HIGH: { bg: 'rgba(249,115,22,0.15)', color: '#fb923c' },
|
HIGH: { bg: 'rgba(249,115,22,0.15)', color: 'var(--color-warning)' },
|
||||||
MEDIUM: { bg: 'rgba(251,191,36,0.15)', color: '#fbbf24' },
|
MEDIUM: { bg: 'rgba(234,179,8,0.15)', color: 'var(--color-caution)' },
|
||||||
RESOLVED: { bg: 'rgba(34,197,94,0.15)', color: '#22c55e' },
|
RESOLVED: { bg: 'rgba(34,197,94,0.15)', color: 'var(--color-success)' },
|
||||||
};
|
};
|
||||||
|
|
||||||
// ─── Mock Data (시나리오 시뮬레이션 엔진 미구현 — 프론트 상수 유지) ──
|
// ─── Mock Data (시나리오 시뮬레이션 엔진 미구현 — 프론트 상수 유지) ──
|
||||||
@ -352,8 +352,8 @@ export function HNSScenarioView() {
|
|||||||
<div className="flex items-center gap-2.5">
|
<div className="flex items-center gap-2.5">
|
||||||
<span className="text-base">📊</span>
|
<span className="text-base">📊</span>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-bold">HNS 대기확산 시나리오 관리</div>
|
<div className="text-title-4 font-bold">HNS 대기확산 시나리오 관리</div>
|
||||||
<div className="text-[10px] text-fg-disabled">
|
<div className="text-label-2 text-fg-disabled">
|
||||||
시간·조건별 대기확산 예측 시나리오 비교·검토 및 대응 의사결정 지원
|
시간·조건별 대기확산 예측 시나리오 비교·검토 및 대응 의사결정 지원
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -362,7 +362,7 @@ export function HNSScenarioView() {
|
|||||||
<select
|
<select
|
||||||
value={selectedIncident}
|
value={selectedIncident}
|
||||||
onChange={(e) => setSelectedIncident(Number(e.target.value))}
|
onChange={(e) => setSelectedIncident(Number(e.target.value))}
|
||||||
className="prd-i w-[280px] text-[11px]"
|
className="prd-i w-[280px] text-label-2"
|
||||||
>
|
>
|
||||||
{incidents.length === 0 ? (
|
{incidents.length === 0 ? (
|
||||||
<option value={0}>분석 데이터 없음</option>
|
<option value={0}>분석 데이터 없음</option>
|
||||||
@ -376,10 +376,10 @@ export function HNSScenarioView() {
|
|||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
onClick={() => setModalOpen(true)}
|
onClick={() => setModalOpen(true)}
|
||||||
className="cursor-pointer whitespace-nowrap font-bold text-color-warning text-[11px] px-[14px] py-1.5 rounded-sm"
|
className="cursor-pointer whitespace-nowrap font-semibold text-color-accent text-label-2 px-[14px] py-1.5 rounded-sm"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(249,115,22,0.12)',
|
border: '1px solid rgba(6,182,212,.3)',
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
background: 'rgba(6,182,212,.08)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
+ 신규 시나리오
|
+ 신규 시나리오
|
||||||
@ -395,18 +395,18 @@ export function HNSScenarioView() {
|
|||||||
style={{ width: '370px', minWidth: '370px' }}
|
style={{ width: '370px', minWidth: '370px' }}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between border-b border-stroke px-[14px] py-2.5">
|
<div className="flex items-center justify-between border-b border-stroke px-[14px] py-2.5">
|
||||||
<span className="text-[11px] font-bold text-fg-disabled">
|
<span className="text-label-2 font-bold text-fg-disabled">
|
||||||
시나리오 목록 — 톨루엔 대기확산
|
시나리오 목록 — 톨루엔 대기확산
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{['시간순', '위험도순'].map((label, i) => (
|
{['시간순', '위험도순'].map((label, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
className="cursor-pointer px-2 py-[3px] text-[9px] font-semibold rounded-sm border border-stroke"
|
className={`cursor-pointer px-2 py-[3px] text-caption font-semibold rounded-sm border border-stroke ${
|
||||||
style={{
|
i === 0
|
||||||
background: i === 0 ? 'rgba(249,115,22,0.08)' : 'var(--bg-card)',
|
? 'bg-[rgba(6,182,212,0.08)] text-color-accent'
|
||||||
color: i === 0 ? 'var(--color-warning)' : 'var(--fg-disabled)',
|
: 'bg-bg-card text-fg-disabled'
|
||||||
}}
|
}`}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
@ -439,14 +439,14 @@ export function HNSScenarioView() {
|
|||||||
checked={checked.has(idx)}
|
checked={checked.has(idx)}
|
||||||
onChange={() => toggleCheck(idx)}
|
onChange={() => toggleCheck(idx)}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
style={{ accentColor: 'var(--color-warning)' }}
|
style={{ accentColor: 'var(--color-accent)' }}
|
||||||
/>
|
/>
|
||||||
<span className="text-[12px] font-bold">
|
<span className="text-label-1 font-bold">
|
||||||
{scn.id} {scn.name}
|
{scn.id} {scn.name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
className="font-bold px-2 py-[2px] rounded-lg text-[8px]"
|
className="font-bold px-2 py-[2px] rounded-lg text-caption"
|
||||||
style={{ background: sev.bg, color: sev.color }}
|
style={{ background: sev.bg, color: sev.color }}
|
||||||
>
|
>
|
||||||
{scn.severity}
|
{scn.severity}
|
||||||
@ -456,25 +456,25 @@ export function HNSScenarioView() {
|
|||||||
{/* Time row */}
|
{/* Time row */}
|
||||||
<div className="flex items-center gap-1.5 mb-1.5">
|
<div className="flex items-center gap-1.5 mb-1.5">
|
||||||
<span
|
<span
|
||||||
className="font-bold font-mono text-color-warning text-[9px] px-1.5 py-[2px] rounded-[3px]"
|
className="font-bold font-mono text-color-accent text-caption px-1.5 py-[2px] rounded-[3px]"
|
||||||
style={{ background: 'rgba(249,115,22,0.1)' }}
|
style={{ background: 'rgba(6,182,212,0.1)' }}
|
||||||
>
|
>
|
||||||
{scn.timeStep}
|
{scn.timeStep}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-[9px] text-fg-disabled font-mono">{scn.datetime}</span>
|
<span className="text-caption text-fg-disabled font-mono">{scn.datetime}</span>
|
||||||
<span className="ml-auto text-fg-disabled text-[8px]">{scn.wind}</span>
|
<span className="ml-auto text-fg-disabled text-caption">{scn.wind}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Metrics grid */}
|
{/* Metrics grid */}
|
||||||
<div className="grid grid-cols-4 gap-1 font-mono text-[8px]">
|
<div className="grid grid-cols-4 gap-1 font-mono text-caption">
|
||||||
{[
|
{[
|
||||||
{ label: '최대농도', value: scn.maxConc, color: '#f87171' },
|
{ label: '최대농도', value: scn.maxConc, color: 'var(--color-accent)' },
|
||||||
{ label: 'IDLH반경', value: scn.idlhRadius, color: '#f87171' },
|
{ label: 'IDLH반경', value: scn.idlhRadius, color: 'var(--color-accent)' },
|
||||||
{ label: 'ERPG-2', value: scn.erpg2, color: '#f97316' },
|
{ label: 'ERPG-2', value: scn.erpg2, color: 'var(--color-accent)' },
|
||||||
{ label: '영향인구', value: scn.population, color: '#f87171' },
|
{ label: '영향인구', value: scn.population, color: 'var(--color-accent)' },
|
||||||
].map((m, i) => (
|
].map((m, i) => (
|
||||||
<div key={i} className="text-center p-[3px] bg-bg-base rounded-[3px]">
|
<div key={i} className="text-center p-[3px] bg-bg-base rounded-[3px]">
|
||||||
<div className="text-fg-disabled text-[7px]">{m.label}</div>
|
<div className="text-fg-disabled text-caption">{m.label}</div>
|
||||||
<div className="font-bold" style={{ color: m.color }}>
|
<div className="font-bold" style={{ color: m.color }}>
|
||||||
{m.value}
|
{m.value}
|
||||||
</div>
|
</div>
|
||||||
@ -483,7 +483,7 @@ export function HNSScenarioView() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
<div className="text-fg-sub mt-1.5 text-[8px] leading-[1.4]">
|
<div className="text-fg-sub mt-1.5 text-caption leading-[1.4]">
|
||||||
{scn.description}
|
{scn.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -495,15 +495,11 @@ export function HNSScenarioView() {
|
|||||||
<div className="flex gap-2 border-t border-stroke px-[14px] py-2.5">
|
<div className="flex gap-2 border-t border-stroke px-[14px] py-2.5">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveView(1)}
|
onClick={() => setActiveView(1)}
|
||||||
className="flex-1 cursor-pointer font-bold text-color-warning text-[11px] p-2 rounded-sm"
|
className="flex-1 cursor-pointer font-bold text-static-white text-label-2 p-2 rounded-sm bg-color-navy hover:bg-color-navy-hover"
|
||||||
style={{
|
|
||||||
background: 'rgba(249,115,22,0.1)',
|
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
📊 선택 시나리오 비교
|
📊 선택 시나리오 비교
|
||||||
</button>
|
</button>
|
||||||
<button className="cursor-pointer font-semibold text-fg-sub text-[11px] px-[14px] py-2 rounded-sm bg-bg-card border border-stroke">
|
<button className="cursor-pointer font-semibold text-fg-sub text-label-2 px-[14px] py-2 rounded-sm bg-bg-card border border-stroke">
|
||||||
📄 보고서
|
📄 보고서
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -563,23 +559,13 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
style={{ scrollbarWidth: 'thin' }}
|
style={{ scrollbarWidth: 'thin' }}
|
||||||
>
|
>
|
||||||
{/* Hero card */}
|
{/* Hero card */}
|
||||||
<div
|
<div className="rounded-md p-4 bg-bg-card border border-stroke">
|
||||||
className="relative overflow-hidden rounded-md p-4"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.06), rgba(239,68,68,0.04))',
|
|
||||||
border: '1px solid rgba(249,115,22,0.2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="absolute top-0 left-0 right-0 h-0.5"
|
|
||||||
style={{ background: 'linear-gradient(90deg, #f97316, #ef4444, #a855f7)' }}
|
|
||||||
/>
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
<div className="flex items-center gap-2 mb-3">
|
||||||
<span className="text-sm font-bold">
|
<span className="text-title-4 font-bold">
|
||||||
{scenario.id} {scenario.name}
|
{scenario.id} {scenario.name}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="font-bold px-2 py-[2px] rounded-lg text-[9px]"
|
className="font-bold px-2 py-[2px] rounded-lg text-caption"
|
||||||
style={{
|
style={{
|
||||||
background: SEVERITY_STYLE[scenario.severity].bg,
|
background: SEVERITY_STYLE[scenario.severity].bg,
|
||||||
color: SEVERITY_STYLE[scenario.severity].color,
|
color: SEVERITY_STYLE[scenario.severity].color,
|
||||||
@ -587,31 +573,27 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
>
|
>
|
||||||
{scenario.severity}
|
{scenario.severity}
|
||||||
</span>
|
</span>
|
||||||
<span className="ml-auto text-[10px] text-fg-disabled font-mono">
|
<span className="ml-auto text-label-2 text-fg-disabled font-mono">
|
||||||
{scenario.datetime}
|
{scenario.datetime}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '최대농도', value: d.maxConc, color: '#f87171' },
|
{ label: '최대농도', value: d.maxConc, color: 'var(--color-accent)' },
|
||||||
{ label: 'IDLH 반경', value: d.idlhRadius, color: '#f87171' },
|
{ label: 'IDLH 반경', value: d.idlhRadius, color: 'var(--color-accent)' },
|
||||||
{ label: 'ERPG-2', value: d.erpg2, color: '#f97316' },
|
{ label: 'ERPG-2', value: d.erpg2, color: 'var(--color-accent)' },
|
||||||
{
|
{
|
||||||
label: '풍향/풍속',
|
label: '풍향/풍속',
|
||||||
value: `${d.windDir}\n${d.windSpeed}`,
|
value: `${d.windDir}\n${d.windSpeed}`,
|
||||||
color: 'var(--color-accent)',
|
color: 'var(--color-accent)',
|
||||||
},
|
},
|
||||||
{ label: '영향인구', value: d.population, color: '#f87171' },
|
{ label: '영향인구', value: d.population, color: 'var(--color-accent)' },
|
||||||
{ label: '유출량', value: d.spillAmount, color: 'var(--color-warning)' },
|
{ label: '유출량', value: d.spillAmount, color: 'var(--color-accent)' },
|
||||||
].map((m, i) => (
|
].map((m, i) => (
|
||||||
|
<div key={i} className="text-center rounded-sm p-2 bg-bg-base">
|
||||||
|
<div className="text-fg-disabled text-caption">{m.label}</div>
|
||||||
<div
|
<div
|
||||||
key={i}
|
className="text-title-3 font-bold font-mono whitespace-pre-line mt-[2px]"
|
||||||
className="text-center rounded-sm p-2"
|
|
||||||
style={{ background: 'rgba(0,0,0,0.15)' }}
|
|
||||||
>
|
|
||||||
<div className="text-fg-disabled text-[8px]">{m.label}</div>
|
|
||||||
<div
|
|
||||||
className="text-base font-bold font-mono whitespace-pre-line mt-[2px]"
|
|
||||||
style={{ color: m.color, lineHeight: 1.2 }}
|
style={{ color: m.color, lineHeight: 1.2 }}
|
||||||
>
|
>
|
||||||
{m.value}
|
{m.value}
|
||||||
@ -625,21 +607,29 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
{/* Threat Zones */}
|
{/* Threat Zones */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
||||||
<h4 className="text-[12px] font-bold mb-2.5">⚠️ 위험 구역</h4>
|
<h4 className="text-label-1 font-bold mb-2.5">⚠️ 위험 구역</h4>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{[
|
{[
|
||||||
{ label: 'IDLH (즉시위험)', value: scenario.zones.idlh, color: '#ef4444' },
|
{
|
||||||
{ label: 'ERPG-2 (대피권고)', value: scenario.zones.erpg2, color: '#f97316' },
|
label: 'IDLH (즉시위험)',
|
||||||
{ label: 'ERPG-1 (주의권고)', value: scenario.zones.erpg1, color: '#fbbf24' },
|
value: scenario.zones.idlh,
|
||||||
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: '#22c55e' },
|
color: 'var(--color-danger)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ERPG-2 (대피권고)',
|
||||||
|
value: scenario.zones.erpg2,
|
||||||
|
color: 'var(--color-warning)',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'ERPG-1 (주의권고)',
|
||||||
|
value: scenario.zones.erpg1,
|
||||||
|
color: 'var(--color-caution)',
|
||||||
|
},
|
||||||
|
{ label: 'TWA (작업허용)', value: scenario.zones.twa, color: 'var(--color-success)' },
|
||||||
].map((z, i) => (
|
].map((z, i) => (
|
||||||
<div
|
<div key={i} className="flex justify-between items-center bg-bg-base rounded-sm">
|
||||||
key={i}
|
<span className="text-label-2 text-fg-sub">{z.label}</span>
|
||||||
className="flex justify-between items-center bg-bg-base rounded-sm"
|
<span className="text-label-2 font-bold font-mono" style={{ color: z.color }}>
|
||||||
style={{ padding: '6px 8px', borderLeft: `3px solid ${z.color}` }}
|
|
||||||
>
|
|
||||||
<span className="text-[10px] text-fg-sub">{z.label}</span>
|
|
||||||
<span className="text-[11px] font-bold font-mono" style={{ color: z.color }}>
|
|
||||||
{z.value}
|
{z.value}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -649,14 +639,14 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
||||||
<h4 className="text-[12px] font-bold mb-2.5">🛡 대응 권고 사항</h4>
|
<h4 className="text-label-1 font-bold mb-2.5">🛡 대응 권고 사항</h4>
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
{scenario.actions.map((action, i) => (
|
{scenario.actions.map((action, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex items-start gap-1.5 text-[10px] text-fg-sub bg-bg-base rounded-sm leading-[1.4] py-[5px] px-2"
|
className="flex items-start gap-1.5 text-label-2 text-fg-sub bg-bg-base rounded-sm leading-[1.4] py-[5px] px-2"
|
||||||
>
|
>
|
||||||
<span className="text-color-warning font-bold shrink-0">•</span>
|
<span className="text-color-accent font-bold shrink-0">•</span>
|
||||||
{action}
|
{action}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@ -666,7 +656,7 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
|
|
||||||
{/* Weather */}
|
{/* Weather */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-elevated p-[14px]">
|
||||||
<h4 className="text-[12px] font-bold mb-2.5">🌊 기상 조건</h4>
|
<h4 className="text-label-1 font-bold mb-2.5">🌊 기상 조건</h4>
|
||||||
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
<div className="grid gap-2" style={{ gridTemplateColumns: 'repeat(6, 1fr)' }}>
|
||||||
{[
|
{[
|
||||||
{ label: '풍향', value: scenario.weather.dir, icon: '🌬' },
|
{ label: '풍향', value: scenario.weather.dir, icon: '🌬' },
|
||||||
@ -677,9 +667,9 @@ function ScenarioDetail({ scenario }: { scenario: HnsScenario }) {
|
|||||||
{ label: '혼합층', value: scenario.weather.mixHeight, icon: '📏' },
|
{ label: '혼합층', value: scenario.weather.mixHeight, icon: '📏' },
|
||||||
].map((w, i) => (
|
].map((w, i) => (
|
||||||
<div key={i} className="text-center p-2 rounded-sm bg-bg-base">
|
<div key={i} className="text-center p-2 rounded-sm bg-bg-base">
|
||||||
<div className="text-sm mb-0.5">{w.icon}</div>
|
<div className="text-title-3 mb-0.5">{w.icon}</div>
|
||||||
<div className="text-[12px] font-bold font-mono">{w.value}</div>
|
<div className="text-label-1 font-bold font-mono">{w.value}</div>
|
||||||
<div className="text-fg-disabled mt-0.5 text-[8px]">{w.label}</div>
|
<div className="text-fg-disabled mt-0.5 text-caption">{w.label}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -744,10 +734,10 @@ const CHART_DATA = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const SEV_COLOR: Record<Severity, string> = {
|
const SEV_COLOR: Record<Severity, string> = {
|
||||||
CRITICAL: '#f87171',
|
CRITICAL: 'var(--color-danger)',
|
||||||
HIGH: '#fb923c',
|
HIGH: 'var(--color-warning)',
|
||||||
MEDIUM: '#fbbf24',
|
MEDIUM: 'var(--color-caution)',
|
||||||
RESOLVED: '#22c55e',
|
RESOLVED: 'var(--color-success)',
|
||||||
};
|
};
|
||||||
|
|
||||||
function ScenarioComparison() {
|
function ScenarioComparison() {
|
||||||
@ -776,13 +766,13 @@ function ScenarioComparison() {
|
|||||||
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
|
style={{ scrollbarWidth: 'thin', scrollbarColor: 'var(--stroke-light) transparent' }}
|
||||||
>
|
>
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div className="text-[13px] font-bold mb-0.5">
|
<div className="text-title-4 font-bold mb-0.5">
|
||||||
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
|
📊 시나리오 비교 — 시간대별 대기확산 지표 추이
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
|
{/* ── Chart 1: 최대 지표면 농도 추이 (Line + Area) ── */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
||||||
<div className="text-[11px] font-bold mb-2.5">최대 지표면 농도 (ppm) 변화 추이</div>
|
<div className="text-label-2 font-bold mb-2.5">최대 지표면 농도 (ppm) 변화 추이</div>
|
||||||
<svg viewBox="0 0 500 140" className="w-full" style={{ height: '130px' }}>
|
<svg viewBox="0 0 500 140" className="w-full" style={{ height: '130px' }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="hnsGrad" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="hnsGrad" x1="0" y1="0" x2="0" y2="1">
|
||||||
@ -799,7 +789,7 @@ function ScenarioComparison() {
|
|||||||
y1={10 + (1 - 500 / concMax) * 110}
|
y1={10 + (1 - 500 / concMax) * 110}
|
||||||
x2="480"
|
x2="480"
|
||||||
y2={10 + (1 - 500 / concMax) * 110}
|
y2={10 + (1 - 500 / concMax) * 110}
|
||||||
stroke="rgba(239,68,68,.2)"
|
stroke="rgba(59,130,246,.2)"
|
||||||
strokeWidth={0.5}
|
strokeWidth={0.5}
|
||||||
strokeDasharray="4,3"
|
strokeDasharray="4,3"
|
||||||
/>
|
/>
|
||||||
@ -817,7 +807,7 @@ function ScenarioComparison() {
|
|||||||
y1={10 + (1 - 300 / concMax) * 110}
|
y1={10 + (1 - 300 / concMax) * 110}
|
||||||
x2="480"
|
x2="480"
|
||||||
y2={10 + (1 - 300 / concMax) * 110}
|
y2={10 + (1 - 300 / concMax) * 110}
|
||||||
stroke="rgba(249,115,22,.2)"
|
stroke="rgba(6,182,212,.2)"
|
||||||
strokeWidth={0.5}
|
strokeWidth={0.5}
|
||||||
strokeDasharray="4,3"
|
strokeDasharray="4,3"
|
||||||
/>
|
/>
|
||||||
@ -874,7 +864,7 @@ function ScenarioComparison() {
|
|||||||
<div className="grid grid-cols-2 gap-[14px]">
|
<div className="grid grid-cols-2 gap-[14px]">
|
||||||
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
|
{/* Chart 2: 위험 반경 변화 (Multi-line) */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
||||||
<div className="text-[11px] font-bold mb-2.5">위험 반경 (km) 변화</div>
|
<div className="text-label-2 font-bold mb-2.5">위험 반경 (km) 변화</div>
|
||||||
<svg viewBox="0 0 260 100" className="w-full" style={{ height: '85px' }}>
|
<svg viewBox="0 0 260 100" className="w-full" style={{ height: '85px' }}>
|
||||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
@ -946,7 +936,7 @@ function ScenarioComparison() {
|
|||||||
|
|
||||||
{/* Chart 3: 영향 인구 변화 (Bar) */}
|
{/* Chart 3: 영향 인구 변화 (Bar) */}
|
||||||
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
<div className="rounded-md border border-stroke bg-bg-card p-[14px]">
|
||||||
<div className="text-[11px] font-bold mb-2.5">영향 인구 (명) 변화</div>
|
<div className="text-label-2 font-bold mb-2.5">영향 인구 (명) 변화</div>
|
||||||
<svg viewBox="0 0 240 100" className="w-full" style={{ height: '85px' }}>
|
<svg viewBox="0 0 240 100" className="w-full" style={{ height: '85px' }}>
|
||||||
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="10" x2="30" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
<line x1="30" y1="85" x2="230" y2="85" stroke="#21262d" strokeWidth={0.5} />
|
||||||
@ -987,14 +977,14 @@ function ScenarioComparison() {
|
|||||||
|
|
||||||
{/* ── Chart 4: 시나리오 비교표 ── */}
|
{/* ── Chart 4: 시나리오 비교표 ── */}
|
||||||
<div className="rounded-md border border-stroke overflow-x-auto bg-bg-card p-[14px]">
|
<div className="rounded-md border border-stroke overflow-x-auto bg-bg-card p-[14px]">
|
||||||
<div className="text-[11px] font-bold mb-2.5">📋 시나리오 비교표</div>
|
<div className="text-label-2 font-bold mb-2.5">📋 시나리오 비교표</div>
|
||||||
<table className="w-full text-[10px] border-collapse">
|
<table className="w-full text-label-2 border-collapse">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-bg-base">
|
<tr className="bg-bg-base">
|
||||||
{['지표', ...D.map((d) => `${d.id} (${d.label})`)].map((h, i) => (
|
{['지표', ...D.map((d) => `${d.id} (${d.label})`)].map((h, i) => (
|
||||||
<th
|
<th
|
||||||
key={i}
|
key={i}
|
||||||
className="text-fg-disabled border-b border-stroke text-[9px] px-[10px] py-2"
|
className="text-fg-disabled border-b border-stroke text-caption px-[10px] py-2"
|
||||||
style={{ textAlign: i === 0 ? 'left' : 'center' }}
|
style={{ textAlign: i === 0 ? 'left' : 'center' }}
|
||||||
>
|
>
|
||||||
{h}
|
{h}
|
||||||
@ -1023,7 +1013,7 @@ function ScenarioComparison() {
|
|||||||
<td
|
<td
|
||||||
key={d.id}
|
key={d.id}
|
||||||
className="text-center font-mono font-semibold p-1.5"
|
className="text-center font-mono font-semibold p-1.5"
|
||||||
style={{ color: d.idlh > 0 ? '#f87171' : '#22c55e' }}
|
style={{ color: d.idlh > 0 ? 'var(--color-danger)' : 'var(--color-success)' }}
|
||||||
>
|
>
|
||||||
{d.idlh || 0}
|
{d.idlh || 0}
|
||||||
</td>
|
</td>
|
||||||
@ -1036,7 +1026,7 @@ function ScenarioComparison() {
|
|||||||
<td
|
<td
|
||||||
key={d.id}
|
key={d.id}
|
||||||
className="text-center font-mono font-semibold p-1.5"
|
className="text-center font-mono font-semibold p-1.5"
|
||||||
style={{ color: d.erpg2 > 0 ? '#f97316' : '#22c55e' }}
|
style={{ color: d.erpg2 > 0 ? 'var(--color-warning)' : 'var(--color-success)' }}
|
||||||
>
|
>
|
||||||
{d.erpg2 || 0}
|
{d.erpg2 || 0}
|
||||||
</td>
|
</td>
|
||||||
@ -1089,7 +1079,7 @@ function ScenarioMapOverlay() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex-1 flex items-center justify-center flex-col gap-4">
|
<div className="flex-1 flex items-center justify-center flex-col gap-4">
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center rounded-md border border-stroke text-fg-disabled text-[13px] bg-bg-elevated"
|
className="flex items-center justify-center rounded-md border border-stroke text-fg-disabled text-title-4 bg-bg-elevated"
|
||||||
style={{
|
style={{
|
||||||
width: '80%',
|
width: '80%',
|
||||||
maxWidth: '600px',
|
maxWidth: '600px',
|
||||||
@ -1100,14 +1090,14 @@ function ScenarioMapOverlay() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{[
|
{[
|
||||||
{ label: 'T+0h SW방향', color: '#ef4444' },
|
{ label: 'T+0h SW방향', color: 'var(--color-danger)' },
|
||||||
{ label: 'T+1h SE 전환', color: '#f97316' },
|
{ label: 'T+1h SE 전환', color: 'var(--color-warning)' },
|
||||||
{ label: 'T+3h S방향', color: '#fbbf24' },
|
{ label: 'T+3h S방향', color: 'var(--color-caution)' },
|
||||||
{ label: 'T+6h 차단 후', color: '#22c55e' },
|
{ label: 'T+6h 차단 후', color: 'var(--color-success)' },
|
||||||
].map((item, i) => (
|
].map((item, i) => (
|
||||||
<div key={i} className="flex items-center gap-1.5">
|
<div key={i} className="flex items-center gap-1.5">
|
||||||
<div className="w-3 h-3 rounded-full opacity-50" style={{ background: item.color }} />
|
<div className="w-3 h-3 rounded-full opacity-50" style={{ background: item.color }} />
|
||||||
<span className="text-[10px] text-fg-sub">{item.label}</span>
|
<span className="text-label-2 text-fg-sub">{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1172,24 +1162,18 @@ function NewScenarioModal({
|
|||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3 border-b border-stroke px-5 py-4">
|
<div className="flex items-center gap-3 border-b border-stroke px-5 py-4">
|
||||||
<div
|
<div className="flex items-center justify-center text-base w-9 h-9 rounded-[10px] bg-bg-elevated border border-stroke">
|
||||||
className="flex items-center justify-center text-base w-9 h-9 rounded-[10px]"
|
|
||||||
style={{
|
|
||||||
background: 'linear-gradient(135deg, rgba(249,115,22,0.2), rgba(239,68,68,0.15))',
|
|
||||||
border: '1px solid rgba(249,115,22,0.3)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🧪
|
🧪
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h2 className="text-[15px] font-bold m-0">신규 HNS 대기확산 시나리오</h2>
|
<h2 className="text-title-2 font-bold m-0">신규 HNS 대기확산 시나리오</h2>
|
||||||
<div className="text-[10px] text-fg-disabled mt-0.5">
|
<div className="text-label-2 text-fg-disabled mt-0.5">
|
||||||
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
|
물질·기상·유출조건을 설정하여 새 시나리오를 생성합니다
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex items-center justify-center w-7 h-7 cursor-pointer text-fg-disabled text-[12px] rounded-sm border border-stroke bg-bg-card"
|
className="flex items-center justify-center w-7 h-7 cursor-pointer text-fg-disabled text-label-1 rounded-sm border border-stroke bg-bg-card"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@ -1244,12 +1228,10 @@ function NewScenarioModal({
|
|||||||
</ModalField>
|
</ModalField>
|
||||||
{/* Material properties card */}
|
{/* Material properties card */}
|
||||||
<div
|
<div
|
||||||
className="grid p-2 rounded-sm"
|
className="grid p-2 rounded-sm bg-bg-elevated border border-stroke"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: 'repeat(5, 1fr)',
|
gridTemplateColumns: 'repeat(5, 1fr)',
|
||||||
gap: '4px',
|
gap: '4px',
|
||||||
background: 'rgba(249,115,22,0.04)',
|
|
||||||
border: '1px solid rgba(249,115,22,0.15)',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
@ -1260,8 +1242,8 @@ function NewScenarioModal({
|
|||||||
{ label: 'ERPG-2', value: mat.erpg2 },
|
{ label: 'ERPG-2', value: mat.erpg2 },
|
||||||
].map((p, i) => (
|
].map((p, i) => (
|
||||||
<div key={i} className="text-center">
|
<div key={i} className="text-center">
|
||||||
<div className="text-fg-disabled text-[8px]">{p.label}</div>
|
<div className="text-fg-disabled text-caption">{p.label}</div>
|
||||||
<div className="text-[10px] font-bold text-color-warning font-mono">
|
<div className="text-label-2 font-bold text-color-accent font-mono">
|
||||||
{p.value}
|
{p.value}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1406,17 +1388,15 @@ function NewScenarioModal({
|
|||||||
<div className="flex gap-2 border-t border-stroke px-5 py-[14px]">
|
<div className="flex gap-2 border-t border-stroke px-5 py-[14px]">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="flex-1 text-[12px] font-semibold cursor-pointer rounded-md text-fg-sub p-[10px] bg-bg-card border border-stroke"
|
className="flex-1 text-label-1 font-semibold cursor-pointer rounded-md text-fg-sub p-[10px] bg-bg-card border border-stroke"
|
||||||
>
|
>
|
||||||
취소
|
취소
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
className="cursor-pointer rounded-md text-[12px] font-bold text-white p-[10px]"
|
className="cursor-pointer rounded-md text-label-1 font-bold text-static-white p-[10px] bg-color-navy hover:bg-color-navy-hover"
|
||||||
style={{
|
style={{
|
||||||
flex: 2,
|
flex: 2,
|
||||||
background: 'linear-gradient(135deg, var(--color-warning), #ef4444)',
|
|
||||||
border: 'none',
|
|
||||||
opacity: name.trim() ? 1 : 0.5,
|
opacity: name.trim() ? 1 : 0.5,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -1432,10 +1412,7 @@ function NewScenarioModal({
|
|||||||
function ModalSection({ title, children }: { title: string; children: React.ReactNode }) {
|
function ModalSection({ title, children }: { title: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div className="text-label-2 font-bold text-color-accent mb-2 pb-1 border-b border-stroke">
|
||||||
className="text-[11px] font-bold text-color-warning mb-2 pb-1"
|
|
||||||
style={{ borderBottom: '1px solid rgba(249,115,22,0.15)' }}
|
|
||||||
>
|
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">{children}</div>
|
<div className="flex flex-col gap-2">{children}</div>
|
||||||
@ -1446,7 +1423,7 @@ function ModalSection({ title, children }: { title: string; children: React.Reac
|
|||||||
function ModalField({ label, children }: { label: string; children: React.ReactNode }) {
|
function ModalField({ label, children }: { label: string; children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-[9px] font-semibold text-fg-disabled mb-1">{label}</div>
|
<div className="text-caption font-semibold text-fg-disabled mb-1">{label}</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -44,7 +44,7 @@ function HNSManualViewer() {
|
|||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-base font-bold">📖 해양 HNS 대응 매뉴얼</div>
|
<div className="text-base font-bold">📖 해양 HNS 대응 매뉴얼</div>
|
||||||
<div className="text-[10px] text-fg-disabled mt-0.5">
|
<div className="text-label-2 text-fg-disabled mt-0.5">
|
||||||
Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024
|
Marine HNS Response Manual — Bonn Agreement / HELCOM / REMPEC (WestMOPoCo 2024
|
||||||
한국어판)
|
한국어판)
|
||||||
</div>
|
</div>
|
||||||
@ -70,19 +70,19 @@ function HNSManualViewer() {
|
|||||||
icon: '🔬',
|
icon: '🔬',
|
||||||
title: '3. HNS 거동 및 유해요소',
|
title: '3. HNS 거동 및 유해요소',
|
||||||
desc: 'SEBC 거동분류 · MSDS · GESAMP · 물리화학적 특성',
|
desc: 'SEBC 거동분류 · MSDS · GESAMP · 물리화학적 특성',
|
||||||
color: 'var(--color-tertiary)',
|
color: 'var(--color-accent)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
title: '4. 대비',
|
title: '4. 대비',
|
||||||
desc: '위험 평가 · 비상 계획 · 교육훈련 · 장비 비축',
|
desc: '위험 평가 · 비상 계획 · 교육훈련 · 장비 비축',
|
||||||
color: 'var(--color-warning)',
|
color: 'var(--color-accent)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🚨',
|
icon: '🚨',
|
||||||
title: '5. 대응',
|
title: '5. 대응',
|
||||||
desc: '최초 조치 · 안전구역 · PPE · 모니터링 · 대응 기술',
|
desc: '최초 조치 · 안전구역 · PPE · 모니터링 · 대응 기술',
|
||||||
color: 'var(--color-danger)',
|
color: 'var(--color-info)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🔄',
|
icon: '🔄',
|
||||||
@ -114,18 +114,18 @@ function HNSManualViewer() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[20px] mb-1.5">{ch.icon}</div>
|
<div className="text-[20px] mb-1.5">{ch.icon}</div>
|
||||||
<div className="text-[11px] font-bold">{ch.title}</div>
|
<div className="text-label-2 font-bold">{ch.title}</div>
|
||||||
<div className="text-[9px] text-fg-disabled mt-1 leading-[1.4]">{ch.desc}</div>
|
<div className="text-caption text-fg-disabled mt-1 leading-[1.4]">{ch.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SEBC 거동 분류 */}
|
{/* SEBC 거동 분류 */}
|
||||||
<div className={`${card} bg-bg-card border border-stroke`}>
|
<div className={`${card} bg-bg-card border border-stroke`}>
|
||||||
<div className="text-[13px] font-bold mb-2.5">
|
<div className="text-title-4 font-bold mb-2.5">
|
||||||
SEBC 거동 분류 (Standard European Behaviour Classification)
|
SEBC 거동 분류 (Standard European Behaviour Classification)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-fg-disabled mb-2.5 leading-normal">
|
<div className="text-caption text-fg-disabled mb-2.5 leading-normal">
|
||||||
물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요
|
물질의 물리적·화학적 특성(용해도, 밀도, 증기압, 점도)에 따라 이론적 거동을 5가지 주요
|
||||||
범주 + 7가지 하위 범주로 분류
|
범주 + 7가지 하위 범주로 분류
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +141,7 @@ function HNSManualViewer() {
|
|||||||
icon: '🌫️',
|
icon: '🌫️',
|
||||||
label: 'E — 증발',
|
label: 'E — 증발',
|
||||||
desc: '수면→대기 증발\n증기압 > 3kPa\n예: 벤젠, 톨루엔',
|
desc: '수면→대기 증발\n증기압 > 3kPa\n예: 벤젠, 톨루엔',
|
||||||
color: 'rgba(249,115,22',
|
color: 'rgba(6,182,212',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: '🟡',
|
icon: '🟡',
|
||||||
@ -168,7 +168,7 @@ function HNSManualViewer() {
|
|||||||
style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}
|
style={{ background: `${s.color},.08)`, border: `1px solid ${s.color},.2)` }}
|
||||||
>
|
>
|
||||||
<div className="text-[22px] mb-1">{s.icon}</div>
|
<div className="text-[22px] mb-1">{s.icon}</div>
|
||||||
<div className="text-[11px] font-bold" style={{ color: `${s.color},1)` }}>
|
<div className="text-label-2 font-bold" style={{ color: `${s.color},1)` }}>
|
||||||
{s.label}
|
{s.label}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-fg-disabled whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">
|
<div className="text-fg-disabled whitespace-pre-line text-[8px] mt-[3px] leading-[1.3]">
|
||||||
@ -225,10 +225,10 @@ function DispersionTimeSlider({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onClick={onPlayPause}
|
onClick={onPlayPause}
|
||||||
className="flex items-center justify-center w-7 h-7 rounded-full text-[14px]"
|
className="flex items-center justify-center w-7 h-7 rounded-full text-title-3"
|
||||||
style={{
|
style={{
|
||||||
background: isPlaying ? 'rgba(239,68,68,0.2)' : 'rgba(6,182,212,0.2)',
|
background: isPlaying ? 'rgba(59,130,246,0.2)' : 'rgba(6,182,212,0.2)',
|
||||||
border: `1px solid ${isPlaying ? 'rgba(239,68,68,0.4)' : 'rgba(6,182,212,0.4)'}`,
|
border: `1px solid ${isPlaying ? 'rgba(59,130,246,0.4)' : 'rgba(6,182,212,0.4)'}`,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -236,7 +236,7 @@ function DispersionTimeSlider({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col gap-1">
|
<div className="flex-1 flex flex-col gap-1">
|
||||||
<div className="flex items-center justify-between text-[9px]">
|
<div className="flex items-center justify-between text-caption">
|
||||||
<span className="text-color-accent font-mono font-bold">t = {currentTime}s</span>
|
<span className="text-color-accent font-mono font-bold">t = {currentTime}s</span>
|
||||||
<span className="text-fg-disabled font-mono">{endTime}s</span>
|
<span className="text-fg-disabled font-mono">{endTime}s</span>
|
||||||
</div>
|
</div>
|
||||||
@ -251,7 +251,7 @@ function DispersionTimeSlider({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-[9px] text-fg-disabled font-mono whitespace-nowrap">
|
<div className="text-caption text-fg-disabled font-mono whitespace-nowrap">
|
||||||
{currentFrame + 1}/{totalFrames}
|
{currentFrame + 1}/{totalFrames}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -777,19 +777,19 @@ export function HNSView() {
|
|||||||
zones: [
|
zones: [
|
||||||
{
|
{
|
||||||
level: 'AEGL-3',
|
level: 'AEGL-3',
|
||||||
color: 'rgba(239,68,68,0.4)',
|
color: 'rgba(59,130,246,0.4)',
|
||||||
radius: resultForZones.aeglDistances.aegl3,
|
radius: resultForZones.aeglDistances.aegl3,
|
||||||
angle: meteo.windDirDeg,
|
angle: meteo.windDirDeg,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'AEGL-2',
|
level: 'AEGL-2',
|
||||||
color: 'rgba(249,115,22,0.3)',
|
color: 'rgba(6,182,212,0.3)',
|
||||||
radius: resultForZones.aeglDistances.aegl2,
|
radius: resultForZones.aeglDistances.aegl2,
|
||||||
angle: meteo.windDirDeg,
|
angle: meteo.windDirDeg,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
level: 'AEGL-1',
|
level: 'AEGL-1',
|
||||||
color: 'rgba(234,179,8,0.25)',
|
color: 'rgba(6,182,212,0.25)',
|
||||||
radius: resultForZones.aeglDistances.aegl1,
|
radius: resultForZones.aeglDistances.aegl1,
|
||||||
angle: meteo.windDirDeg,
|
angle: meteo.windDirDeg,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -36,25 +36,25 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(34,197,94,0.15)] text-green-400">
|
<span className="px-2 py-1 text-label-2 font-medium rounded-md bg-[rgba(34,197,94,0.15)] text-color-success">
|
||||||
완료
|
완료
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'running':
|
case 'running':
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(249,115,22,0.15)] text-orange-400">
|
<span className="px-2 py-1 text-label-2 font-medium rounded-md bg-[rgba(249,115,22,0.15)] text-color-warning">
|
||||||
실행중
|
실행중
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'pending':
|
case 'pending':
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(138,150,168,0.15)] text-fg-disabled">
|
<span className="px-2 py-1 text-label-2 font-medium rounded-md bg-[rgba(138,150,168,0.15)] text-fg-disabled">
|
||||||
대기
|
대기
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
case 'error':
|
case 'error':
|
||||||
return (
|
return (
|
||||||
<span className="px-2 py-1 text-[10px] font-semibold rounded-md bg-[rgba(239,68,68,0.15)] text-color-danger">
|
<span className="px-2 py-1 text-label-2 font-medium rounded-md bg-[rgba(239,68,68,0.15)] text-color-danger">
|
||||||
오류
|
오류
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -106,7 +106,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
<button
|
<button
|
||||||
key={page}
|
key={page}
|
||||||
onClick={() => setCurrentPage(page as number)}
|
onClick={() => setCurrentPage(page as number)}
|
||||||
className={`px-3 py-1 text-sm font-medium rounded transition-colors ${
|
className={`px-3 py-1 text-title-3 font-medium rounded transition-colors ${
|
||||||
currentPage === page ? 'bg-color-accent text-bg-0' : 'text-fg-sub hover:bg-bg-elevated'
|
currentPage === page ? 'bg-color-accent text-bg-0' : 'text-fg-sub hover:bg-bg-elevated'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -121,8 +121,8 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
|
<div className="flex items-center justify-between px-5 py-4 border-b border-stroke">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-fg">유출유 확산 예측 목록</h1>
|
<h1 className="text-heading-3 font-bold text-fg">유출유 확산 예측 목록</h1>
|
||||||
<p className="text-sm text-fg-disabled mt-1">총 {analyses.length}건</p>
|
<p className="text-title-3 text-fg-disabled mt-1">총 {analyses.length}건</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -131,12 +131,16 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
placeholder="검색..."
|
placeholder="검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="w-64 px-4 py-2 text-sm bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
className="w-64 px-4 py-2 text-title-3 bg-bg-elevated border border-stroke rounded-md text-fg placeholder-fg-disabled focus:border-color-accent focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onTabChange('analysis')}
|
onClick={() => onTabChange('analysis')}
|
||||||
className="px-4 py-2 text-sm font-semibold rounded-md bg-color-accent text-bg-0 hover:shadow-[0_0_16px_rgba(6,182,212,0.3)] transition-all"
|
className="px-4 py-2 text-title-3 font-semibold rounded-sm cursor-pointer text-color-accent"
|
||||||
|
style={{
|
||||||
|
border: '1px solid rgba(6,182,212,.3)',
|
||||||
|
background: 'rgba(6,182,212,.08)',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
+ 새 분석
|
+ 새 분석
|
||||||
</button>
|
</button>
|
||||||
@ -146,64 +150,66 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
{/* 테이블 */}
|
{/* 테이블 */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-20 text-fg-disabled text-sm">로딩 중...</div>
|
<div className="text-center py-20 text-fg-disabled text-title-3">로딩 중...</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="sticky top-0 bg-bg-surface border-b border-stroke z-10">
|
<thead className="sticky top-0 bg-bg-surface border-b border-stroke z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
번호
|
번호
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
사고명
|
사고명
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
사고일시
|
사고일시
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
예측 실행
|
예측 실행
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
예측시간
|
예측시간
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
유종
|
유종
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-right text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-right text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
유출량
|
유출량
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-center text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
KOSPS
|
KOSPS
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-center text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
POSEIDON
|
POSEIDON
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-center text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
OpenDrift
|
OpenDrift
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-center text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-center text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
역추적
|
역추적
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
담당자
|
담당자
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-3 text-left text-xs font-bold text-fg-disabled uppercase tracking-wider">
|
<th className="px-4 py-3 text-left text-label-1 font-bold text-fg-disabled uppercase tracking-label">
|
||||||
소속
|
소속
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-border">
|
<tbody className="divide-y divide-stroke">
|
||||||
{currentAnalyses.map((analysis) => (
|
{currentAnalyses.map((analysis) => (
|
||||||
<tr
|
<tr
|
||||||
key={analysis.predRunSn ?? analysis.acdntSn}
|
key={analysis.predRunSn ?? analysis.acdntSn}
|
||||||
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
|
className="hover:bg-bg-elevated transition-colors cursor-pointer group"
|
||||||
>
|
>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{analysis.acdntSn}</td>
|
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||||
|
{analysis.acdntSn}
|
||||||
|
</td>
|
||||||
<td className="px-4 py-3">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-2 h-2 rounded-full bg-color-danger animate-pulse" />
|
<span className="w-2 h-2 rounded-full bg-color-danger animate-pulse" />
|
||||||
<span
|
<span
|
||||||
className="text-sm font-semibold text-color-accent hover:underline transition-all cursor-pointer"
|
className="text-title-3 font-medium text-color-accent hover:underline transition-all cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onSelectAnalysis) {
|
if (onSelectAnalysis) {
|
||||||
@ -215,7 +221,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">
|
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||||
{analysis.occurredAt
|
{analysis.occurredAt
|
||||||
? new Date(analysis.occurredAt).toLocaleString('ko-KR', {
|
? new Date(analysis.occurredAt).toLocaleString('ko-KR', {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@ -225,7 +231,7 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
})
|
})
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">
|
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||||
{analysis.runDtm
|
{analysis.runDtm
|
||||||
? new Date(analysis.runDtm).toLocaleString('ko-KR', {
|
? new Date(analysis.runDtm).toLocaleString('ko-KR', {
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@ -235,9 +241,11 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
})
|
})
|
||||||
: '—'}
|
: '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub font-mono">{analysis.duration}</td>
|
<td className="px-4 py-3 text-title-3 text-fg-sub font-mono">
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.oilType}</td>
|
{analysis.duration}
|
||||||
<td className="px-4 py-3 text-sm text-fg font-mono text-right font-semibold">
|
</td>
|
||||||
|
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.oilType}</td>
|
||||||
|
<td className="px-4 py-3 text-title-3 text-fg font-mono text-right font-medium">
|
||||||
{analysis.volume != null ? analysis.volume.toFixed(2) : '—'}
|
{analysis.volume != null ? analysis.volume.toFixed(2) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.kospsStatus)}</td>
|
<td className="px-4 py-3 text-center">{getStatusBadge(analysis.kospsStatus)}</td>
|
||||||
@ -250,8 +258,8 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
<td className="px-4 py-3 text-center">
|
<td className="px-4 py-3 text-center">
|
||||||
{getStatusBadge(analysis.backtrackStatus)}
|
{getStatusBadge(analysis.backtrackStatus)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.analyst}</td>
|
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.analyst}</td>
|
||||||
<td className="px-4 py-3 text-sm text-fg-sub">{analysis.officeName}</td>
|
<td className="px-4 py-3 text-title-3 text-fg-sub">{analysis.officeName}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -259,7 +267,9 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && analyses.length === 0 && (
|
{!loading && analyses.length === 0 && (
|
||||||
<div className="text-center py-20 text-fg-disabled text-sm">분석 데이터가 없습니다.</div>
|
<div className="text-center py-20 text-fg-disabled text-title-3">
|
||||||
|
분석 데이터가 없습니다.
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -268,14 +278,14 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(1)}
|
onClick={() => setCurrentPage(1)}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
≪
|
≪
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
|
||||||
disabled={currentPage === 1}
|
disabled={currentPage === 1}
|
||||||
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
‹
|
‹
|
||||||
</button>
|
</button>
|
||||||
@ -285,14 +295,14 @@ export function AnalysisListTable({ onTabChange, onSelectAnalysis }: AnalysisLis
|
|||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
›
|
›
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCurrentPage(totalPages)}
|
onClick={() => setCurrentPage(totalPages)}
|
||||||
disabled={currentPage === totalPages}
|
disabled={currentPage === totalPages}
|
||||||
className="px-3 py-1 text-sm font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 py-1 text-title-3 font-medium text-fg-sub hover:bg-bg-elevated rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
≫
|
≫
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
Load Diff
@ -1291,7 +1291,7 @@ export function OilSpillView() {
|
|||||||
<div
|
<div
|
||||||
className="absolute bottom-0 left-0 right-0 h-[72px] flex items-center px-5 gap-4"
|
className="absolute bottom-0 left-0 right-0 h-[72px] flex items-center px-5 gap-4"
|
||||||
style={{
|
style={{
|
||||||
background: 'rgba(15,21,36,0.95)',
|
background: 'var(--bg-surface)',
|
||||||
backdropFilter: 'blur(16px)',
|
backdropFilter: 'blur(16px)',
|
||||||
borderTop: '1px solid var(--stroke-default)',
|
borderTop: '1px solid var(--stroke-default)',
|
||||||
zIndex: 1100,
|
zIndex: 1100,
|
||||||
|
|||||||
@ -185,15 +185,21 @@ function TopInfoBar({ activeType }: { activeType: AccidentType }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-9 bg-gradient-to-r from-bg-0 to-bg-2 border-b border-stroke flex items-center px-4 gap-3.5 flex-shrink-0">
|
<div className="h-9 bg-bg-base border-b border-stroke flex items-center px-4 gap-3.5 flex-shrink-0">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-sm">⚓</span>
|
<span className="text-title-3">⚓</span>
|
||||||
<span className="text-[12px] font-bold text-fg font-korean">긴급구난지원</span>
|
<span className="text-label-1 font-bold text-fg font-korean">긴급구난지원</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3.5 py-0.5 bg-[rgba(239,68,68,0.15)] border border-[rgba(239,68,68,0.35)] rounded-xl text-[10px] font-bold text-color-danger font-korean">
|
<div
|
||||||
|
className="px-3.5 py-0.5 rounded-xl text-label-2 font-bold text-color-danger font-korean"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-danger) 15%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-danger) 35%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
사고: {d.incident}
|
사고: {d.incident}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 text-[9px] font-mono text-fg-disabled ml-auto">
|
<div className="flex gap-3 text-caption font-mono text-fg-disabled ml-auto">
|
||||||
<span>
|
<span>
|
||||||
생존자: <b className="text-fg">{d.survivors}</b>/{d.total}
|
생존자: <b className="text-fg">{d.survivors}</b>/{d.total}
|
||||||
</span>
|
</span>
|
||||||
@ -210,8 +216,8 @@ function TopInfoBar({ activeType }: { activeType: AccidentType }) {
|
|||||||
유출량: <b className="text-color-warning">{d.oilRate}</b>
|
유출량: <b className="text-color-warning">{d.oilRate}</b>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[13px] font-bold text-fg font-mono">{clock}</div>
|
<div className="text-title-4 font-bold text-fg font-mono">{clock}</div>
|
||||||
<div className="text-[9px] text-fg-disabled font-korean">👤 Cmdr. KIM</div>
|
<div className="text-caption text-fg-disabled font-korean">👤 Cmdr. KIM</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -227,7 +233,7 @@ function LeftPanel({
|
|||||||
return (
|
return (
|
||||||
<div className="w-[208px] min-w-[208px] bg-bg-base border-r border-stroke flex flex-col overflow-y-auto scrollbar-thin p-2 gap-0.5">
|
<div className="w-[208px] min-w-[208px] bg-bg-base border-r border-stroke flex flex-col overflow-y-auto scrollbar-thin p-2 gap-0.5">
|
||||||
{/* 사고유형 제목 */}
|
{/* 사고유형 제목 */}
|
||||||
<div className="text-[9px] font-bold text-[var(--color-info)] font-korean mb-0.5 tracking-wider">
|
<div className="text-caption font-bold text-color-info font-korean mb-0.5 tracking-wider">
|
||||||
사고 유형 (INCIDENT TYPE)
|
사고 유형 (INCIDENT TYPE)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -242,32 +248,32 @@ function LeftPanel({
|
|||||||
: 'bg-bg-card border-stroke hover:border-[var(--stroke-light)] hover:bg-bg-surface-hover'
|
: 'bg-bg-card border-stroke hover:border-[var(--stroke-light)] hover:bg-bg-surface-hover'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="text-[12px]">{t.icon}</span>
|
<span className="text-label-1">{t.icon}</span>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
className={`text-[10px] font-bold font-korean ${activeType === t.id ? 'text-[var(--color-accent)]' : 'text-fg'}`}
|
className={`text-label-2 font-bold font-korean ${activeType === t.id ? 'text-color-accent' : 'text-fg'}`}
|
||||||
>
|
>
|
||||||
{t.label} ({t.eng})
|
{t.label} ({t.eng})
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-fg-disabled font-korean mt-px">{t.desc}</div>
|
<div className="text-caption text-fg-disabled font-korean mt-px">{t.desc}</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* 긴급 경고 */}
|
{/* 긴급 경고 */}
|
||||||
<div className="text-[9px] font-bold text-fg-disabled font-korean mt-2.5 mb-1">
|
<div className="text-caption font-bold text-fg-disabled font-korean mt-2.5 mb-1">
|
||||||
긴급 경고 (CRITICAL ALERTS)
|
긴급 경고 (CRITICAL ALERTS)
|
||||||
</div>
|
</div>
|
||||||
<div className="py-1.5 px-2.5 bg-[rgba(239,68,68,0.15)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-[9px] font-bold text-color-danger font-korean">
|
<div className="py-1.5 px-2.5 bg-[color-mix(in_srgb,var(--color-danger)_15%,transparent)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-caption font-bold text-color-danger font-korean">
|
||||||
GM 위험 수준 — 전복 위험
|
GM 위험 수준 — 전복 위험
|
||||||
</div>
|
</div>
|
||||||
<div className="py-1.5 px-2.5 bg-[rgba(239,68,68,0.15)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-[9px] font-bold text-color-danger font-korean">
|
<div className="py-1.5 px-2.5 bg-[color-mix(in_srgb,var(--color-danger)_15%,transparent)] border-l-[3px] border-l-[var(--color-danger)] rounded-r text-caption font-bold text-color-danger font-korean">
|
||||||
승선자 5명 미확인
|
승선자 5명 미확인
|
||||||
</div>
|
</div>
|
||||||
<div className="py-1.5 px-2.5 bg-[rgba(249,115,22,0.12)] border-l-[3px] border-l-[var(--color-warning)] rounded-r text-[9px] font-bold text-color-warning font-korean">
|
<div className="py-1.5 px-2.5 bg-[color-mix(in_srgb,var(--color-warning)_12%,transparent)] border-l-[3px] border-l-[var(--color-warning)] rounded-r text-caption font-bold text-color-warning font-korean">
|
||||||
유류 유출 감지 - 방제 필요
|
유류 유출 감지 - 방제 필요
|
||||||
</div>
|
</div>
|
||||||
<div className="py-1.5 px-2.5 bg-[rgba(251,191,36,0.1)] border-l-[3px] border-l-[var(--color-caution)] rounded-r text-[9px] font-bold text-color-caution font-korean">
|
<div className="py-1.5 px-2.5 bg-[color-mix(in_srgb,var(--color-caution)_10%,transparent)] border-l-[3px] border-l-[var(--color-caution)] rounded-r text-caption font-bold text-color-caution font-korean">
|
||||||
종강도 한계치 88% 접근
|
종강도 한계치 88% 접근
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -279,8 +285,8 @@ function LeftPanel({
|
|||||||
background: 'repeating-linear-gradient(0deg, rgba(255,0,0,.03), transparent 2px)',
|
background: 'repeating-linear-gradient(0deg, rgba(255,0,0,.03), transparent 2px)',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="text-[9px] text-[rgba(255,60,60,0.35)] font-mono">CCTV FEED #1</div>
|
<div className="text-caption text-[rgba(255,60,60,0.35)] font-mono">CCTV FEED #1</div>
|
||||||
<div className="absolute top-1 left-1.5 text-[7px] text-[rgba(255,60,60,0.5)] font-mono">
|
<div className="absolute top-1 left-1.5 text-caption text-[rgba(255,60,60,0.5)] font-mono">
|
||||||
● REC
|
● REC
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -324,8 +330,8 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 사고 해역 정보 */}
|
{/* 사고 해역 정보 */}
|
||||||
<div className="absolute top-2.5 left-2.5 z-20 bg-[var(--dropdown-bg)] border border-stroke rounded-md px-3 py-2 backdrop-blur-sm font-mono text-[9px] text-fg-disabled">
|
<div className="absolute top-2.5 left-2.5 z-20 bg-[var(--dropdown-bg)] border border-stroke rounded-md px-3 py-2 backdrop-blur-sm font-mono text-caption text-fg-disabled">
|
||||||
<div className="text-[10px] font-bold text-fg font-korean mb-1">사고 해역 정보</div>
|
<div className="text-label-2 font-bold text-fg font-korean mb-1">사고 해역 정보</div>
|
||||||
<div className="grid gap-x-1.5 gap-y-px" style={{ gridTemplateColumns: '32px 1fr' }}>
|
<div className="grid gap-x-1.5 gap-y-px" style={{ gridTemplateColumns: '32px 1fr' }}>
|
||||||
<span>위치</span>
|
<span>위치</span>
|
||||||
<b className="text-fg">37°28'N, 126°15'E</b>
|
<b className="text-fg">37°28'N, 126°15'E</b>
|
||||||
@ -348,11 +354,11 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="absolute w-0.5 h-[7px] bg-[#888] rounded-[1px]"
|
className="absolute w-0.5 h-[7px] bg-fg-disabled rounded-[1px]"
|
||||||
style={{ top: '-3px', left: '60%' }}
|
style={{ top: '-3px', left: '60%' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-center mt-0.5 font-mono text-[rgba(255,200,150,0.5)]">
|
<div className="text-caption text-center mt-0.5 font-mono text-[rgba(255,200,150,0.5)]">
|
||||||
M/V SEA GUARDIAN
|
M/V SEA GUARDIAN
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -371,28 +377,28 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/* 구역 라벨 */}
|
{/* 구역 라벨 */}
|
||||||
<div className="absolute z-[6] text-[8px] font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)] top-1/2 left-[36%]">
|
<div className="absolute z-[6] text-caption font-korean font-semibold tracking-wider uppercase whitespace-pre-line text-[rgba(6,182,212,0.45)] top-1/2 left-[36%]">
|
||||||
{d.zone.replace('\\n', '\n')}
|
{d.zone.replace('\\n', '\n')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SAR 자산 */}
|
{/* SAR 자산 */}
|
||||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">
|
<div className="absolute z-10 text-caption font-mono text-[rgba(200,220,255,0.35)] top-[10%] left-[42%]">
|
||||||
ETA 5 MIN ─
|
ETA 5 MIN ─
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-10 text-[7px] font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">
|
<div className="absolute z-10 text-caption font-mono text-[rgba(200,220,255,0.35)] top-[14%] left-[56%]">
|
||||||
ETA 15 MIN ─
|
ETA 15 MIN ─
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-[12] text-sm opacity-60 top-[7%] left-[52%] -rotate-[30deg]">
|
<div className="absolute z-[12] text-title-3 opacity-60 top-[7%] left-[52%] -rotate-[30deg]">
|
||||||
🚁
|
🚁
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-[12] text-[8px] font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">
|
<div className="absolute z-[12] text-caption font-mono text-[rgba(200,220,255,0.45)] top-[20%] left-[60%]">
|
||||||
6M
|
6M
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute z-[12] text-[11px] opacity-45 top-[28%] left-[54%]">🚢</div>
|
<div className="absolute z-[12] text-label-2 opacity-45 top-[28%] left-[54%]">🚢</div>
|
||||||
|
|
||||||
{/* 환경 민감 구역 */}
|
{/* 환경 민감 구역 */}
|
||||||
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)] bottom-[6%] right-[6%]">
|
<div className="absolute z-10 px-3.5 py-2 rounded bg-[rgba(34,197,94,0.06)] border border-[rgba(34,197,94,0.2)] bottom-[6%] right-[6%]">
|
||||||
<div className="text-[9px] font-bold font-serif uppercase tracking-wider leading-snug text-[rgba(34,197,94,0.55)]">
|
<div className="text-caption font-bold font-serif uppercase tracking-wider leading-snug text-[rgba(34,197,94,0.55)]">
|
||||||
ENVIRONMENTALLY SENSITIVE
|
ENVIRONMENTALLY SENSITIVE
|
||||||
<br />
|
<br />
|
||||||
AREA: AQUACULTURE FARM
|
AREA: AQUACULTURE FARM
|
||||||
@ -404,7 +410,7 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
{['🗺', '🔍', '📐'].map((ico, i) => (
|
{['🗺', '🔍', '📐'].map((ico, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
className="w-7 h-7 bg-[rgba(13,17,23,0.85)] border border-stroke rounded text-fg-disabled text-[12px] flex items-center justify-center cursor-pointer hover:text-fg"
|
className="w-7 h-7 bg-[rgba(13,17,23,0.85)] border border-stroke rounded text-fg-disabled text-label-1 flex items-center justify-center cursor-pointer hover:text-fg"
|
||||||
>
|
>
|
||||||
{ico}
|
{ico}
|
||||||
</button>
|
</button>
|
||||||
@ -412,7 +418,7 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 스케일 바 */}
|
{/* 스케일 바 */}
|
||||||
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-stroke rounded px-2.5 py-1 text-[8px] text-fg-disabled font-mono">
|
<div className="absolute bottom-2.5 left-2.5 z-20 bg-[rgba(13,17,23,0.8)] border border-stroke rounded px-2.5 py-1 text-caption text-fg-disabled font-mono">
|
||||||
<div
|
<div
|
||||||
className="w-[70px] h-0.5 mb-0.5"
|
className="w-[70px] h-0.5 mb-0.5"
|
||||||
style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--stroke-default) 50%)' }}
|
style={{ background: 'linear-gradient(90deg, #e4e8f1 50%, var(--stroke-default) 50%)' }}
|
||||||
@ -422,11 +428,55 @@ function CenterMap({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 사고 유형 표시 */}
|
{/* 사고 유형 표시 */}
|
||||||
<div className="absolute bottom-2.5 right-2.5 z-20 bg-[rgba(13,17,23,0.85)] border border-stroke rounded px-3 py-1.5">
|
<div className="absolute bottom-2.5 right-2.5 z-20 bg-[rgba(13,17,23,0.85)] border border-stroke rounded px-3 py-1.5">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">현재 사고 유형</div>
|
<div className="text-caption text-fg-disabled font-korean">현재 사고 유형</div>
|
||||||
<div className="text-[11px] font-bold font-korean text-color-accent">
|
<div className="text-label-2 font-bold font-korean text-color-accent">
|
||||||
{at.icon} {at.label} ({at.eng})
|
{at.icon} {at.label} ({at.eng})
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 타임라인 시뮬레이션 컨트롤 */}
|
||||||
|
<div className="absolute bottom-2.5 left-1/2 -translate-x-1/2 z-20 bg-[rgba(13,17,23,0.9)] border border-stroke rounded-md px-4 py-2 flex items-center gap-4 backdrop-blur-sm">
|
||||||
|
<div className="text-caption font-bold text-fg font-korean whitespace-nowrap">TIMELINE</div>
|
||||||
|
<div className="flex items-center gap-1.5 text-caption text-fg-disabled font-mono">
|
||||||
|
<span>[-6h]</span>
|
||||||
|
<span className="font-bold text-fg">[NOW]</span>
|
||||||
|
<span>[+6H]</span>
|
||||||
|
<span>[+12H]</span>
|
||||||
|
<span>[+24H]</span>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-24 h-1.5 bg-bg-surface-hover rounded-sm">
|
||||||
|
<div
|
||||||
|
className="absolute rounded-full border-2 border-bg-0 bg-color-accent"
|
||||||
|
style={{
|
||||||
|
left: '35%',
|
||||||
|
top: '-3px',
|
||||||
|
width: '12px',
|
||||||
|
height: '12px',
|
||||||
|
boxShadow: '0 0 8px rgba(6,182,212,.4)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button className="w-6 h-6 bg-bg-card border border-stroke rounded-full text-fg-disabled text-label-2 flex items-center justify-center cursor-pointer hover:text-fg">
|
||||||
|
⏮
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-8 h-8 rounded-full text-color-accent text-title-4 flex items-center justify-center cursor-pointer hover:brightness-125"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-accent) 15%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-accent) 30%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
▶
|
||||||
|
</button>
|
||||||
|
<button className="w-6 h-6 bg-bg-card border border-stroke rounded-full text-fg-disabled text-label-2 flex items-center justify-center cursor-pointer hover:text-fg">
|
||||||
|
⏭
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-caption text-fg-disabled font-mono whitespace-nowrap">
|
||||||
|
<b className="text-color-accent">10:45</b> KST
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -449,9 +499,9 @@ function RightPanel({
|
|||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
onClick={() => onAnalysisChange(tab.id)}
|
onClick={() => onAnalysisChange(tab.id)}
|
||||||
className={`flex-1 py-2 text-[9px] font-semibold font-korean cursor-pointer text-center transition-all border-b-2 ${
|
className={`flex-1 py-2 text-caption font-semibold font-korean cursor-pointer text-center transition-all border-b-2 ${
|
||||||
activeAnalysis === tab.id
|
activeAnalysis === tab.id
|
||||||
? 'text-[var(--color-accent)] border-b-[var(--color-accent)] bg-[rgba(6,182,212,0.04)]'
|
? 'text-color-accent border-b-[var(--color-accent)] bg-[rgba(6,182,212,0.04)]'
|
||||||
: 'text-fg-disabled border-b-transparent hover:text-fg'
|
: 'text-fg-disabled border-b-transparent hover:text-fg'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -469,13 +519,19 @@ function RightPanel({
|
|||||||
|
|
||||||
{/* Bottom Action Buttons */}
|
{/* Bottom Action Buttons */}
|
||||||
<div className="flex gap-1.5 p-3 border-t border-stroke flex-shrink-0">
|
<div className="flex gap-1.5 p-3 border-t border-stroke flex-shrink-0">
|
||||||
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-boom to-[#d97706] text-black font-korean">
|
<button
|
||||||
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold text-color-accent font-korean cursor-pointer"
|
||||||
|
style={{ border: '1px solid rgba(6,182,212,.3)', background: 'rgba(6,182,212,.08)' }}
|
||||||
|
>
|
||||||
💾 저장
|
💾 저장
|
||||||
</button>
|
</button>
|
||||||
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-[rgba(249,115,22,0.1)] border border-[rgba(249,115,22,0.3)] text-color-warning font-korean">
|
<button className="flex-1 py-2 px-1 rounded text-label-2 font-semibold bg-bg-elevated border border-stroke text-fg font-korean cursor-pointer">
|
||||||
🔄 재계산
|
🔄 재계산
|
||||||
</button>
|
</button>
|
||||||
<button className="flex-1 py-2 px-1 rounded text-[11px] font-semibold bg-gradient-to-r from-primary-cyan to-primary-blue text-white font-korean">
|
<button
|
||||||
|
className="flex-1 py-2 px-1 rounded text-label-2 font-semibold text-color-accent font-korean cursor-pointer"
|
||||||
|
style={{ border: '1px solid rgba(6,182,212,.3)', background: 'rgba(6,182,212,.08)' }}
|
||||||
|
>
|
||||||
📄 보고서
|
📄 보고서
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -490,14 +546,20 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-2.5 gap-2">
|
<div className="flex flex-col p-2.5 gap-2">
|
||||||
<div className="text-[12px] font-bold text-fg font-korean">구난 분석 (RESCUE ANALYSIS)</div>
|
<div className="text-label-1 font-bold text-fg font-korean">구난 분석 (RESCUE ANALYSIS)</div>
|
||||||
<div className="text-[9px] text-[var(--color-info)] font-korean px-2 py-1 bg-[rgba(88,166,255,0.08)] border border-[rgba(88,166,255,0.2)] rounded">
|
<div
|
||||||
|
className="text-caption text-color-info font-korean px-2 py-1 rounded"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-info) 8%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-info) 20%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
📌 현재 사고유형: {at.label} ({at.eng})
|
📌 현재 사고유형: {at.label} ({at.eng})
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 선박 단면도 SVG */}
|
{/* 선박 단면도 SVG */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2 relative">
|
<div className="bg-bg-card border border-stroke rounded-md p-2 relative">
|
||||||
<div className="absolute top-1.5 right-2 text-[7px] text-fg-disabled font-mono">
|
<div className="absolute top-1.5 right-2 text-caption text-fg-disabled font-mono">
|
||||||
VESSEL STATUS
|
VESSEL STATUS
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 260 80" className="w-full" style={{ height: '65px' }}>
|
<svg viewBox="0 0 260 80" className="w-full" style={{ height: '65px' }}>
|
||||||
@ -537,7 +599,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
fill="rgba(239,68,68,.5)"
|
fill="rgba(239,68,68,.5)"
|
||||||
fontSize="4.5"
|
fontSize="4.5"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontFamily="monospace"
|
fontFamily="var(--font-mono)"
|
||||||
>
|
>
|
||||||
FLOODED
|
FLOODED
|
||||||
</text>
|
</text>
|
||||||
@ -556,7 +618,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
fill="var(--color-danger)"
|
fill="var(--color-danger)"
|
||||||
fontSize="4"
|
fontSize="4"
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fontFamily="monospace"
|
fontFamily="var(--font-mono)"
|
||||||
>
|
>
|
||||||
IMPACT
|
IMPACT
|
||||||
</text>
|
</text>
|
||||||
@ -569,7 +631,13 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
strokeWidth=".4"
|
strokeWidth=".4"
|
||||||
strokeDasharray="3,2"
|
strokeDasharray="3,2"
|
||||||
/>
|
/>
|
||||||
<text x="205" y="55" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="205"
|
||||||
|
y="55"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
WL
|
WL
|
||||||
</text>
|
</text>
|
||||||
<line
|
<line
|
||||||
@ -619,12 +687,12 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
subColor={trimColor(d.trim)}
|
subColor={trimColor(d.trim)}
|
||||||
/>
|
/>
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">
|
<div className="text-caption text-fg-disabled font-korean">
|
||||||
잔존 부력 (Reserve Buoyancy)
|
잔존 부력 (Reserve Buoyancy)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-bold font-mono" style={{ color: buoyColor(d.buoy) }}>
|
<div className="text-title-2 font-bold font-mono" style={{ color: buoyColor(d.buoy) }}>
|
||||||
{d.buoy}
|
{d.buoy}
|
||||||
<span className="text-[10px]">%</span>
|
<span className="text-label-2">%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
||||||
<div
|
<div
|
||||||
@ -636,7 +704,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 긴급 조치 버튼 */}
|
{/* 긴급 조치 버튼 */}
|
||||||
<div className="text-[10px] font-bold text-fg-disabled font-korean">
|
<div className="text-label-2 font-bold text-fg-disabled font-korean">
|
||||||
긴급 조치 (EMERGENCY ACTIONS)
|
긴급 조치 (EMERGENCY ACTIONS)
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
@ -654,8 +722,8 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
border: `1px solid color-mix(in srgb, ${btn.color} 25%, transparent)`,
|
border: `1px solid color-mix(in srgb, ${btn.color} 25%, transparent)`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="text-[10px] font-bold text-fg font-mono">{btn.en}</div>
|
<div className="text-label-2 font-bold text-fg font-mono">{btn.en}</div>
|
||||||
<div className="text-[8px] font-korean" style={{ color: btn.color }}>
|
<div className="text-caption font-korean" style={{ color: btn.color }}>
|
||||||
{btn.ko}
|
{btn.ko}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@ -664,10 +732,10 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 구난 의사결정 프로세스 */}
|
{/* 구난 의사결정 프로세스 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
구난 의사결정 프로세스 (KRISO Decision Support)
|
구난 의사결정 프로세스 (KRISO Decision Support)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
|
<div className="flex flex-col gap-0.5 text-caption font-korean">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{[
|
{[
|
||||||
{ label: '① 상태평가', color: 'var(--color-accent)' },
|
{ label: '① 상태평가', color: 'var(--color-accent)' },
|
||||||
@ -719,10 +787,10 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 유체 정역학 */}
|
{/* 유체 정역학 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
유체 정역학 (Hydrostatics)
|
유체 정역학 (Hydrostatics)
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
|
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
|
||||||
{[
|
{[
|
||||||
{ label: '배수량(Δ)', value: '12,450 ton' },
|
{ label: '배수량(Δ)', value: '12,450 ton' },
|
||||||
{ label: '흘수(Draft)', value: '7.2 m' },
|
{ label: '흘수(Draft)', value: '7.2 m' },
|
||||||
@ -732,7 +800,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
{ label: 'MTC', value: '185 t·m' },
|
{ label: 'MTC', value: '185 t·m' },
|
||||||
].map((r, i) => (
|
].map((r, i) => (
|
||||||
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
||||||
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
|
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
|
||||||
<br />
|
<br />
|
||||||
<b className="text-fg">{r.value}</b>
|
<b className="text-fg">{r.value}</b>
|
||||||
</div>
|
</div>
|
||||||
@ -742,10 +810,10 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 예인력/이초력 */}
|
{/* 예인력/이초력 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
예인력 / 이초력 (Towing & Refloating)
|
예인력 / 이초력 (Towing & Refloating)
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
|
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
|
||||||
{[
|
{[
|
||||||
{ label: '필요 예인력', value: '285 kN', color: 'var(--color-caution)' },
|
{ label: '필요 예인력', value: '285 kN', color: 'var(--color-caution)' },
|
||||||
{ label: '비상 예인력', value: '420 kN', color: 'var(--color-danger)' },
|
{ label: '비상 예인력', value: '420 kN', color: 'var(--color-danger)' },
|
||||||
@ -753,30 +821,30 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
{ label: '인양 안전성', value: 'FAIL', color: 'var(--color-danger)' },
|
{ label: '인양 안전성', value: 'FAIL', color: 'var(--color-danger)' },
|
||||||
].map((r, i) => (
|
].map((r, i) => (
|
||||||
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
||||||
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
|
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
|
||||||
<br />
|
<br />
|
||||||
<b style={{ color: r.color }}>{r.value}</b>
|
<b style={{ color: r.color }}>{r.value}</b>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-fg-disabled font-korean mt-1">
|
<div className="text-caption text-fg-disabled font-korean mt-1">
|
||||||
※ IMO Salvage Manual / Resistance Increase Ratio 기반 산출
|
※ IMO Salvage Manual / Resistance Increase Ratio 기반 산출
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 유출량 추정 */}
|
{/* 유출량 추정 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
유출량 추정 (Oil Outflow Estimation)
|
유출량 추정 (Oil Outflow Estimation)
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1 text-[8px] font-mono">
|
<div className="grid grid-cols-3 gap-1 text-caption font-mono">
|
||||||
{[
|
{[
|
||||||
{ label: '현재 유출률', value: d.oilRate, color: 'var(--color-warning)' },
|
{ label: '현재 유출률', value: d.oilRate, color: 'var(--color-warning)' },
|
||||||
{ label: '누적 유출량', value: '6.8 kL', color: 'var(--color-danger)' },
|
{ label: '누적 유출량', value: '6.8 kL', color: 'var(--color-danger)' },
|
||||||
{ label: '24h 예측', value: '145 kL', color: 'var(--color-danger)' },
|
{ label: '24h 예측', value: '145 kL', color: 'var(--color-danger)' },
|
||||||
].map((r, i) => (
|
].map((r, i) => (
|
||||||
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm text-center">
|
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm text-center">
|
||||||
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
|
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
|
||||||
<br />
|
<br />
|
||||||
<b style={{ color: r.color }}>{r.value}</b>
|
<b style={{ color: r.color }}>{r.value}</b>
|
||||||
</div>
|
</div>
|
||||||
@ -791,14 +859,14 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-fg-disabled font-korean mt-0.5">
|
<div className="text-caption text-fg-disabled font-korean mt-0.5">
|
||||||
잔여 연료유: 210 kL | 탱크 잔량: 68% 유출
|
잔여 연료유: 210 kL | 탱크 잔량: 68% 유출
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CBR 사례기반 추론 */}
|
{/* CBR 사례기반 추론 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
CBR 유사 사고 사례 (Case-Based Reasoning)
|
CBR 유사 사고 사례 (Case-Based Reasoning)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
@ -831,7 +899,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-7 h-4 rounded-sm flex items-center justify-center text-[7px] font-bold font-mono"
|
className="w-7 h-4 rounded-sm flex items-center justify-center text-caption font-bold font-mono"
|
||||||
style={{
|
style={{
|
||||||
background: `color-mix(in srgb, ${c.color} 20%, transparent)`,
|
background: `color-mix(in srgb, ${c.color} 20%, transparent)`,
|
||||||
color: c.color,
|
color: c.color,
|
||||||
@ -839,7 +907,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
>
|
>
|
||||||
{c.pct}
|
{c.pct}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 text-[7px] font-korean">
|
<div className="flex-1 text-caption font-korean">
|
||||||
<b className="text-fg">{c.name}</b>
|
<b className="text-fg">{c.name}</b>
|
||||||
<br />
|
<br />
|
||||||
<span className="text-fg-disabled">{c.desc}</span>
|
<span className="text-fg-disabled">{c.desc}</span>
|
||||||
@ -850,11 +918,17 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 위험도 평가 */}
|
{/* 위험도 평가 */}
|
||||||
<div className="bg-[rgba(239,68,68,0.04)] border border-[rgba(239,68,68,0.15)] rounded-md p-2">
|
<div
|
||||||
<div className="text-[9px] font-bold text-color-danger font-korean mb-1.5">
|
className="rounded-md p-2"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-danger) 4%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-danger) 15%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-caption font-bold text-color-danger font-korean mb-1.5">
|
||||||
위험도 평가 — 2차사고 시나리오
|
위험도 평가 — 2차사고 시나리오
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
|
<div className="flex flex-col gap-0.5 text-caption font-korean">
|
||||||
{[
|
{[
|
||||||
{ label: '침수 확대 → 전복', level: 'HIGH', color: 'var(--color-danger)' },
|
{ label: '침수 확대 → 전복', level: 'HIGH', color: 'var(--color-danger)' },
|
||||||
{ label: '유류 대량 유출 → 해양오염', level: 'HIGH', color: 'var(--color-warning)' },
|
{ label: '유류 대량 유출 → 해양오염', level: 'HIGH', color: 'var(--color-warning)' },
|
||||||
@ -870,7 +944,7 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
>
|
>
|
||||||
<span className="text-fg">{r.label}</span>
|
<span className="text-fg">{r.label}</span>
|
||||||
<span
|
<span
|
||||||
className="px-1.5 py-px rounded-lg text-[7px] font-bold"
|
className="px-1.5 py-px rounded-lg text-caption font-bold"
|
||||||
style={{
|
style={{
|
||||||
background: `color-mix(in srgb, ${r.color} 20%, transparent)`,
|
background: `color-mix(in srgb, ${r.color} 20%, transparent)`,
|
||||||
color: r.color,
|
color: r.color,
|
||||||
@ -885,10 +959,10 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 해상 e-Call */}
|
{/* 해상 e-Call */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
해상 e-Call (GMDSS / VHF-DSC)
|
해상 e-Call (GMDSS / VHF-DSC)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[7px] font-mono text-fg-disabled">
|
<div className="flex flex-col gap-0.5 text-caption font-mono text-fg-disabled">
|
||||||
{[
|
{[
|
||||||
{ label: 'MMSI', value: '440123456' },
|
{ label: 'MMSI', value: '440123456' },
|
||||||
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--color-danger)' },
|
{ label: 'Nature of Distress', value: 'COLLISION', color: 'var(--color-danger)' },
|
||||||
@ -902,7 +976,13 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button className="w-full mt-1 py-1 bg-[rgba(239,68,68,0.12)] border border-[rgba(239,68,68,0.3)] rounded text-[8px] font-bold text-color-danger cursor-pointer font-korean hover:bg-[rgba(239,68,68,0.2)]">
|
<button
|
||||||
|
className="w-full mt-1 py-1 rounded text-caption font-bold text-color-danger cursor-pointer font-korean"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-danger) 12%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-danger) 30%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
📡 DISTRESS RELAY 전송
|
📡 DISTRESS RELAY 전송
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -915,10 +995,10 @@ function RescuePanel({ activeType }: { activeType: AccidentType }) {
|
|||||||
function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-2.5 gap-2">
|
<div className="flex flex-col p-2.5 gap-2">
|
||||||
<div className="text-[12px] font-bold text-fg font-korean">
|
<div className="text-label-1 font-bold text-fg font-korean">
|
||||||
손상 복원성 (DAMAGE STABILITY)
|
손상 복원성 (DAMAGE STABILITY)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-fg-disabled font-korean leading-snug">
|
<div className="text-caption text-fg-disabled font-korean leading-snug">
|
||||||
사고유형에 따른 손상 후 선체 복원력 분석
|
사고유형에 따른 손상 후 선체 복원력 분석
|
||||||
<br />
|
<br />
|
||||||
IMO A.749(18) / SOLAS Ch.II-1 기준 평가
|
IMO A.749(18) / SOLAS Ch.II-1 기준 평가
|
||||||
@ -926,7 +1006,7 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* GZ Curve SVG */}
|
{/* GZ Curve SVG */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
GZ 복원력 곡선 (Righting Lever Curve)
|
GZ 복원력 곡선 (Righting Lever Curve)
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 260 120" className="w-full" style={{ height: '100px' }}>
|
<svg viewBox="0 0 260 120" className="w-full" style={{ height: '100px' }}>
|
||||||
@ -984,37 +1064,79 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
strokeWidth=".6"
|
strokeWidth=".6"
|
||||||
strokeDasharray="4,2"
|
strokeDasharray="4,2"
|
||||||
/>
|
/>
|
||||||
<text x="240" y="76" fill="var(--color-caution)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="240"
|
||||||
|
y="76"
|
||||||
|
fill="var(--color-caution)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
IMO MIN
|
IMO MIN
|
||||||
</text>
|
</text>
|
||||||
<text x="5" y="14" fill="var(--fg-disabled)" fontSize="5" fontFamily="monospace">
|
<text x="5" y="14" fill="var(--fg-disabled)" fontSize="5" fontFamily="var(--font-mono)">
|
||||||
GZ(m)
|
GZ(m)
|
||||||
</text>
|
</text>
|
||||||
<text x="5" y="35" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text x="5" y="35" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
|
||||||
0.6
|
0.6
|
||||||
</text>
|
</text>
|
||||||
<text x="5" y="58" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text x="5" y="58" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
|
||||||
0.4
|
0.4
|
||||||
</text>
|
</text>
|
||||||
<text x="5" y="82" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text x="5" y="82" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="var(--font-mono)">
|
||||||
0.2
|
0.2
|
||||||
</text>
|
</text>
|
||||||
<text x="5" y="104" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="5"
|
||||||
|
y="104"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
0
|
0
|
||||||
</text>
|
</text>
|
||||||
<text x="30" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="30"
|
||||||
|
y="112"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
0°
|
0°
|
||||||
</text>
|
</text>
|
||||||
<text x="80" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="80"
|
||||||
|
y="112"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
15°
|
15°
|
||||||
</text>
|
</text>
|
||||||
<text x="130" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="130"
|
||||||
|
y="112"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
30°
|
30°
|
||||||
</text>
|
</text>
|
||||||
<text x="180" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="180"
|
||||||
|
y="112"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
45°
|
45°
|
||||||
</text>
|
</text>
|
||||||
<text x="230" y="112" fill="var(--fg-disabled)" fontSize="4.5" fontFamily="monospace">
|
<text
|
||||||
|
x="230"
|
||||||
|
y="112"
|
||||||
|
fill="var(--fg-disabled)"
|
||||||
|
fontSize="4.5"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
>
|
||||||
60°
|
60°
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
@ -1041,27 +1163,33 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">침수 구획수</div>
|
<div className="text-caption text-fg-disabled font-korean">침수 구획수</div>
|
||||||
<div className="text-lg font-bold text-color-danger font-mono">
|
<div className="text-title-3 font-bold text-color-danger font-mono">
|
||||||
2 <span className="text-[9px]">구획</span>
|
2 <span className="text-caption">구획</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-fg-disabled font-korean">#1 선수탱크 / #3 좌현탱크</div>
|
<div className="text-caption text-fg-disabled font-korean">#1 선수탱크 / #3 좌현탱크</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">Margin Line 여유</div>
|
<div className="text-caption text-fg-disabled font-korean">Margin Line 여유</div>
|
||||||
<div className="text-lg font-bold text-color-danger font-mono">
|
<div className="text-title-3 font-bold text-color-danger font-mono">
|
||||||
0.12 <span className="text-[9px]">m</span>
|
0.12 <span className="text-caption">m</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[7px] text-color-danger font-korean">침수 임계점 임박</div>
|
<div className="text-caption text-color-danger font-korean">침수 임계점 임박</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SOLAS 판정 */}
|
{/* SOLAS 판정 */}
|
||||||
<div className="bg-[rgba(239,68,68,0.06)] border border-[rgba(239,68,68,0.2)] rounded-md p-2.5">
|
<div
|
||||||
<div className="text-[10px] font-bold text-color-danger font-korean mb-1">
|
className="rounded-md p-2.5"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-danger) 6%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-danger) 20%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-label-2 font-bold text-color-danger font-korean mb-1">
|
||||||
⚠ SOLAS 손상복원성 판정: 부적합 (FAIL)
|
⚠ SOLAS 손상복원성 판정: 부적합 (FAIL)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[8px] text-fg-disabled font-korean leading-snug">
|
<div className="text-caption text-fg-disabled font-korean leading-snug">
|
||||||
· Area(0~θ_f): 0.028 m·rad (기준 0.015 ✓)
|
· Area(0~θ_f): 0.028 m·rad (기준 0.015 ✓)
|
||||||
<br />
|
<br />
|
||||||
· GZ_max ≥ 0.1m: 0.25m ✓ | θ_max ≥ 15°: 28° ✓<br />·{' '}
|
· GZ_max ≥ 0.1m: 0.25m ✓ | θ_max ≥ 15°: 28° ✓<br />·{' '}
|
||||||
@ -1073,10 +1201,10 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 좌초시 복원성 */}
|
{/* 좌초시 복원성 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1">
|
<div className="text-caption font-bold text-fg font-korean mb-1">
|
||||||
좌초시 복원성 (Grounded Stability)
|
좌초시 복원성 (Grounded Stability)
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1 text-[8px] font-mono">
|
<div className="grid grid-cols-2 gap-1 text-caption font-mono">
|
||||||
{[
|
{[
|
||||||
{ label: '지반반력', value: '1,240 kN', color: 'var(--color-caution)' },
|
{ label: '지반반력', value: '1,240 kN', color: 'var(--color-caution)' },
|
||||||
{ label: '접촉 면적', value: '12.5 m²' },
|
{ label: '접촉 면적', value: '12.5 m²' },
|
||||||
@ -1084,7 +1212,7 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
{ label: '좌초 GM', value: '0.65 m', color: 'var(--color-caution)' },
|
{ label: '좌초 GM', value: '0.65 m', color: 'var(--color-caution)' },
|
||||||
].map((r, i) => (
|
].map((r, i) => (
|
||||||
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
<div key={i} className="px-1.5 py-1 bg-bg-base rounded-sm">
|
||||||
<span className="text-fg-disabled font-korean text-[7px]">{r.label}</span>
|
<span className="text-fg-disabled font-korean text-caption">{r.label}</span>
|
||||||
<br />
|
<br />
|
||||||
<b style={{ color: r.color }}>{r.value}</b>
|
<b style={{ color: r.color }}>{r.value}</b>
|
||||||
</div>
|
</div>
|
||||||
@ -1094,10 +1222,10 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 탱크 상태 */}
|
{/* 탱크 상태 */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1">
|
<div className="text-caption font-bold text-fg font-korean mb-1">
|
||||||
탱크 상태 (Tank Volume Status)
|
탱크 상태 (Tank Volume Status)
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-0.5 text-[7px] font-korean">
|
<div className="flex flex-col gap-0.5 text-caption font-korean">
|
||||||
{[
|
{[
|
||||||
{ name: '#1 FP Tank', pct: 100, status: '침수', color: 'var(--color-danger)' },
|
{ name: '#1 FP Tank', pct: 100, status: '침수', color: 'var(--color-danger)' },
|
||||||
{ name: '#3 Port Tank', pct: 85, status: '85%', color: 'var(--color-danger)' },
|
{ name: '#3 Port Tank', pct: 85, status: '85%', color: 'var(--color-danger)' },
|
||||||
@ -1130,10 +1258,10 @@ function DamageStabilityPanel(_props: { activeType: AccidentType }) {
|
|||||||
function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col p-2.5 gap-2">
|
<div className="flex flex-col p-2.5 gap-2">
|
||||||
<div className="text-[12px] font-bold text-fg font-korean">
|
<div className="text-label-1 font-bold text-fg font-korean">
|
||||||
종강도 분석 (LONGITUDINAL STRENGTH)
|
종강도 분석 (LONGITUDINAL STRENGTH)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[9px] text-fg-disabled font-korean leading-snug">
|
<div className="text-caption text-fg-disabled font-korean leading-snug">
|
||||||
선체 종방향 구조 응력 분석
|
선체 종방향 구조 응력 분석
|
||||||
<br />
|
<br />
|
||||||
IACS CSR / Classification Society 기준
|
IACS CSR / Classification Society 기준
|
||||||
@ -1141,7 +1269,7 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 전단력 분포 SVG */}
|
{/* 전단력 분포 SVG */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
전단력 분포 (Shear Force Distribution)
|
전단력 분포 (Shear Force Distribution)
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
|
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
|
||||||
@ -1184,22 +1312,22 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
<text x="80" y="12" fill="var(--color-danger)" fontSize="4.5">
|
<text x="80" y="12" fill="var(--color-danger)" fontSize="4.5">
|
||||||
손상 후 SF ▲
|
손상 후 SF ▲
|
||||||
</text>
|
</text>
|
||||||
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
+SF
|
+SF
|
||||||
</text>
|
</text>
|
||||||
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
0
|
0
|
||||||
</text>
|
</text>
|
||||||
<text x="2" y="78" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="2" y="78" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
-SF
|
-SF
|
||||||
</text>
|
</text>
|
||||||
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
AP
|
AP
|
||||||
</text>
|
</text>
|
||||||
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
MID
|
MID
|
||||||
</text>
|
</text>
|
||||||
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
FP
|
FP
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
@ -1207,7 +1335,7 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
|
|
||||||
{/* 굽힘모멘트 분포 SVG */}
|
{/* 굽힘모멘트 분포 SVG */}
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
<div className="bg-bg-card border border-stroke rounded-md p-2.5">
|
||||||
<div className="text-[9px] font-bold text-fg font-korean mb-1.5">
|
<div className="text-caption font-bold text-fg font-korean mb-1.5">
|
||||||
굽힘모멘트 분포 (Bending Moment Distribution)
|
굽힘모멘트 분포 (Bending Moment Distribution)
|
||||||
</div>
|
</div>
|
||||||
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
|
<svg viewBox="0 0 260 90" className="w-full" style={{ height: '75px' }}>
|
||||||
@ -1241,19 +1369,19 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
<text x="115" y="8" fill="var(--color-warning)" fontSize="4.5">
|
<text x="115" y="8" fill="var(--color-warning)" fontSize="4.5">
|
||||||
손상 후 BM ▲
|
손상 후 BM ▲
|
||||||
</text>
|
</text>
|
||||||
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="2" y="14" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
+BM
|
+BM
|
||||||
</text>
|
</text>
|
||||||
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="2" y="48" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
0
|
0
|
||||||
</text>
|
</text>
|
||||||
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="25" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
AP
|
AP
|
||||||
</text>
|
</text>
|
||||||
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="130" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
MID
|
MID
|
||||||
</text>
|
</text>
|
||||||
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="monospace">
|
<text x="245" y="88" fill="var(--fg-disabled)" fontSize="4" fontFamily="var(--font-mono)">
|
||||||
FP
|
FP
|
||||||
</text>
|
</text>
|
||||||
</svg>
|
</svg>
|
||||||
@ -1262,18 +1390,18 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
{/* 종강도 지표 */}
|
{/* 종강도 지표 */}
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">SF 최대/허용 비율</div>
|
<div className="text-caption text-fg-disabled font-korean">SF 최대/허용 비율</div>
|
||||||
<div className="text-lg font-bold text-color-caution font-mono">
|
<div className="text-title-3 font-bold text-color-caution font-mono">
|
||||||
88<span className="text-[9px]">%</span>
|
88<span className="text-caption">%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
||||||
<div className="h-full rounded-sm w-[88%] bg-color-caution" />
|
<div className="h-full rounded-sm w-[88%] bg-color-caution" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">BM 최대/허용 비율</div>
|
<div className="text-caption text-fg-disabled font-korean">BM 최대/허용 비율</div>
|
||||||
<div className="text-lg font-bold text-color-warning font-mono">
|
<div className="text-title-3 font-bold text-color-warning font-mono">
|
||||||
92<span className="text-[9px]">%</span>
|
92<span className="text-caption">%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
<div className="h-[5px] bg-bg-surface-hover rounded-sm mt-0.5">
|
||||||
<div className="h-full rounded-sm w-[92%] bg-color-warning" />
|
<div className="h-full rounded-sm w-[92%] bg-color-warning" />
|
||||||
@ -1282,23 +1410,29 @@ function LongStrengthPanel(_props: { activeType: AccidentType }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1.5">
|
<div className="grid grid-cols-2 gap-1.5">
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">Section Modulus 여유</div>
|
<div className="text-caption text-fg-disabled font-korean">Section Modulus 여유</div>
|
||||||
<div className="text-lg font-bold text-color-success font-mono">1.08</div>
|
<div className="text-title-3 font-bold text-color-success font-mono">1.08</div>
|
||||||
<div className="text-[7px] text-color-success font-korean">Req'd: 1.00 이상 ✓</div>
|
<div className="text-caption text-color-success font-korean">Req'd: 1.00 이상 ✓</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">Hull Girder ULS</div>
|
<div className="text-caption text-fg-disabled font-korean">Hull Girder ULS</div>
|
||||||
<div className="text-lg font-bold text-color-caution font-mono">1.12</div>
|
<div className="text-title-3 font-bold text-color-caution font-mono">1.12</div>
|
||||||
<div className="text-[7px] text-color-caution font-korean">Req'd: 1.10 이상 ⚠</div>
|
<div className="text-caption text-color-caution font-korean">Req'd: 1.10 이상 ⚠</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 판정 */}
|
{/* 판정 */}
|
||||||
<div className="bg-[rgba(251,191,36,0.06)] border border-[rgba(251,191,36,0.2)] rounded-md p-2.5">
|
<div
|
||||||
<div className="text-[10px] font-bold text-color-caution font-korean mb-1">
|
className="rounded-md p-2.5"
|
||||||
|
style={{
|
||||||
|
background: 'color-mix(in srgb, var(--color-caution) 6%, transparent)',
|
||||||
|
border: '1px solid color-mix(in srgb, var(--color-caution) 20%, transparent)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="text-label-2 font-bold text-color-caution font-korean mb-1">
|
||||||
⚠ 종강도 판정: 주의 (CAUTION)
|
⚠ 종강도 판정: 주의 (CAUTION)
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[8px] text-fg-disabled font-korean leading-snug">
|
<div className="text-caption text-fg-disabled font-korean leading-snug">
|
||||||
· SF 최대: 허용치의 88% — <span className="text-color-caution">주의 구간</span>
|
· SF 최대: 허용치의 88% — <span className="text-color-caution">주의 구간</span>
|
||||||
<br />· BM 최대: 허용치의 92% — <span className="text-color-warning">경고 구간</span>
|
<br />· BM 최대: 허용치의 92% — <span className="text-color-warning">경고 구간</span>
|
||||||
<br />
|
<br />
|
||||||
@ -1315,9 +1449,9 @@ function BottomBar() {
|
|||||||
return (
|
return (
|
||||||
<div className="h-[145px] min-h-[145px] border-t border-stroke flex bg-bg-base flex-shrink-0">
|
<div className="h-[145px] min-h-[145px] border-t border-stroke flex bg-bg-base flex-shrink-0">
|
||||||
{/* 이벤트 로그 */}
|
{/* 이벤트 로그 */}
|
||||||
<div className="flex-1 flex flex-col overflow-hidden border-r border-stroke">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<div className="flex items-center justify-between px-3 py-1 border-b border-stroke flex-shrink-0">
|
<div className="flex items-center justify-between px-3 py-1 border-b border-stroke flex-shrink-0">
|
||||||
<span className="text-[9px] font-bold text-fg-disabled font-korean">
|
<span className="text-caption font-bold text-fg-disabled font-korean">
|
||||||
이벤트 로그 / 통신 기록 (EVENT LOG / COMMUNICATION TRANSCRIPT)
|
이벤트 로그 / 통신 기록 (EVENT LOG / COMMUNICATION TRANSCRIPT)
|
||||||
</span>
|
</span>
|
||||||
<div className="flex gap-0.5">
|
<div className="flex gap-0.5">
|
||||||
@ -1328,7 +1462,7 @@ function BottomBar() {
|
|||||||
].map((f, i) => (
|
].map((f, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
className="px-2 py-px rounded-sm text-[8px] font-bold cursor-pointer font-korean"
|
className="px-2 py-px rounded-sm text-caption font-bold cursor-pointer font-korean"
|
||||||
style={{
|
style={{
|
||||||
background: `color-mix(in srgb, ${f.color} 15%, transparent)`,
|
background: `color-mix(in srgb, ${f.color} 15%, transparent)`,
|
||||||
border: `1px solid color-mix(in srgb, ${f.color} 30%, transparent)`,
|
border: `1px solid color-mix(in srgb, ${f.color} 30%, transparent)`,
|
||||||
@ -1340,7 +1474,7 @@ function BottomBar() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-3 py-1 font-mono text-[9px] leading-[1.7] scrollbar-thin">
|
<div className="flex-1 overflow-y-auto px-3 py-1 font-mono text-caption leading-[1.7] scrollbar-thin">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
time: '10:35',
|
time: '10:35',
|
||||||
@ -1395,46 +1529,6 @@ function BottomBar() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 타임라인 시뮬레이션 */}
|
|
||||||
<div className="w-[330px] min-w-[330px] flex flex-col px-3.5 py-2 gap-1.5">
|
|
||||||
<div className="text-[9px] font-bold text-fg font-korean">
|
|
||||||
시간대별 시뮬레이션 컨트롤 (TIMELINE SIMULATION CONTROL)
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 text-[8px] text-fg-disabled font-mono">
|
|
||||||
<span>[-6h]</span>
|
|
||||||
<span className="flex-1 text-center font-bold text-fg">[CURRENT]</span>
|
|
||||||
<span>[+6H]</span>
|
|
||||||
<span>[+12H]</span>
|
|
||||||
<span>[+24H]</span>
|
|
||||||
</div>
|
|
||||||
<div className="relative h-1.5 bg-bg-surface-hover rounded-sm mx-1">
|
|
||||||
<div
|
|
||||||
className="absolute rounded-full border-2 border-bg-0 bg-color-accent"
|
|
||||||
style={{
|
|
||||||
left: '35%',
|
|
||||||
top: '-3px',
|
|
||||||
width: '12px',
|
|
||||||
height: '12px',
|
|
||||||
boxShadow: '0 0 8px rgba(6,182,212,.4)',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-center gap-2 mt-0.5">
|
|
||||||
<button className="w-7 h-7 bg-bg-card border border-stroke rounded-full text-fg-disabled text-[11px] flex items-center justify-center cursor-pointer hover:text-fg">
|
|
||||||
⏮
|
|
||||||
</button>
|
|
||||||
<button className="w-[34px] h-[34px] bg-[rgba(6,182,212,0.15)] border border-[rgba(6,182,212,0.3)] rounded-full text-[var(--color-accent)] text-[13px] flex items-center justify-center cursor-pointer hover:brightness-125">
|
|
||||||
▶
|
|
||||||
</button>
|
|
||||||
<button className="w-7 h-7 bg-bg-card border border-stroke rounded-full text-fg-disabled text-[11px] flex items-center justify-center cursor-pointer hover:text-fg">
|
|
||||||
⏭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-[8px] text-fg-disabled font-mono">
|
|
||||||
현재 시간: <b className="text-color-accent">10:45 KST</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1457,12 +1551,12 @@ function MetricCard({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
<div className="bg-bg-card border border-stroke rounded-md p-2">
|
||||||
<div className="text-[8px] text-fg-disabled font-korean">{label}</div>
|
<div className="text-caption text-fg-disabled font-korean">{label}</div>
|
||||||
<div className="text-xl font-bold font-mono" style={{ color }}>
|
<div className="text-title-2 font-bold font-mono" style={{ color }}>
|
||||||
{value}
|
{value}
|
||||||
<span className="text-[10px]"> {unit}</span>
|
<span className="text-label-2"> {unit}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-[8px] font-korean" style={{ color: subColor }}>
|
<div className="text-caption font-korean" style={{ color: subColor }}>
|
||||||
{sub}
|
{sub}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -1520,35 +1614,41 @@ function RescueListView() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col flex-1 overflow-hidden">
|
<div className="flex flex-col flex-1 overflow-hidden">
|
||||||
<div className="px-5 py-4 flex items-center justify-between border-b border-stroke">
|
<div className="px-5 py-4 flex items-center justify-between border-b border-stroke">
|
||||||
<span className="text-sm font-bold font-korean">긴급구난 사고 목록</span>
|
<span className="text-title-3 font-bold font-korean">긴급구난 사고 목록</span>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="선박명 / 사고번호 검색..."
|
placeholder="선박명 / 사고번호 검색..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-md text-fg-sub font-korean text-[11px] w-[200px] outline-none focus:border-[var(--color-accent)]"
|
className="px-3 py-1.5 bg-bg-base border border-stroke rounded-md text-fg-sub font-korean text-label-2 w-[200px] outline-none focus:border-[var(--color-accent)]"
|
||||||
/>
|
/>
|
||||||
<button className="px-3.5 py-1.5 bg-[rgba(6,182,212,0.12)] border border-[rgba(6,182,212,0.3)] rounded-md text-[var(--color-accent)] text-[11px] font-semibold cursor-pointer font-korean hover:bg-[rgba(6,182,212,0.2)]">
|
<button
|
||||||
|
className="rounded-sm text-label-2 font-semibold cursor-pointer text-color-accent px-3.5 py-1.5 font-korean"
|
||||||
|
style={{
|
||||||
|
border: '1px solid rgba(6,182,212,.3)',
|
||||||
|
background: 'rgba(6,182,212,.08)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
+ 신규 사고 등록
|
+ 신규 사고 등록
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 overflow-y-auto px-5 pb-4">
|
<div className="flex-1 overflow-y-auto px-5 pb-4">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-20 text-fg-disabled text-sm">로딩 중...</div>
|
<div className="text-center py-20 text-fg-disabled text-title-3">로딩 중...</div>
|
||||||
) : opsList.length === 0 ? (
|
) : opsList.length === 0 ? (
|
||||||
<div className="text-center py-20 text-fg-disabled text-sm">
|
<div className="text-center py-20 text-fg-disabled text-title-3">
|
||||||
구난 작전 데이터가 없습니다.
|
구난 작전 데이터가 없습니다.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full border-collapse text-[11px] mt-3">
|
<table className="w-full border-collapse text-label-2 mt-3">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="bg-bg-card border-b border-stroke">
|
<tr className="bg-bg-card border-b border-stroke">
|
||||||
{['상태', '사고번호', '선박명', '사고유형', '발생일시', '위치', '인명'].map((h) => (
|
{['상태', '사고번호', '선박명', '사고유형', '발생일시', '위치', '인명'].map((h) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
className="py-2 px-2.5 text-left font-korean font-semibold text-fg-disabled text-[10px]"
|
className="py-2 px-2.5 text-left font-korean font-semibold text-fg-disabled text-label-2"
|
||||||
>
|
>
|
||||||
{h}
|
{h}
|
||||||
</th>
|
</th>
|
||||||
@ -1565,7 +1665,7 @@ function RescueListView() {
|
|||||||
>
|
>
|
||||||
<td className="py-2 px-2.5">
|
<td className="py-2 px-2.5">
|
||||||
<span
|
<span
|
||||||
className="px-2 py-0.5 rounded-xl text-[9px] font-bold"
|
className="px-2 py-0.5 rounded-xl text-caption font-bold"
|
||||||
style={{
|
style={{
|
||||||
background: `color-mix(in srgb, ${status.color} 15%, transparent)`,
|
background: `color-mix(in srgb, ${status.color} 15%, transparent)`,
|
||||||
color: status.color,
|
color: status.color,
|
||||||
@ -1574,7 +1674,7 @@ function RescueListView() {
|
|||||||
{status.label}
|
{status.label}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2.5 font-mono text-[var(--color-accent)] font-semibold">
|
<td className="py-2 px-2.5 font-mono text-color-accent font-semibold">
|
||||||
{r.opsCd}
|
{r.opsCd}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2.5 font-korean text-fg font-semibold">{r.vesselNm}</td>
|
<td className="py-2 px-2.5 font-korean text-fg font-semibold">{r.vesselNm}</td>
|
||||||
@ -1582,7 +1682,7 @@ function RescueListView() {
|
|||||||
<td className="py-2 px-2.5 font-mono text-fg-disabled">
|
<td className="py-2 px-2.5 font-mono text-fg-disabled">
|
||||||
{r.regDtm ? new Date(r.regDtm).toLocaleString('ko-KR') : '—'}
|
{r.regDtm ? new Date(r.regDtm).toLocaleString('ko-KR') : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2.5 font-mono text-fg-disabled text-[10px]">
|
<td className="py-2 px-2.5 font-mono text-fg-disabled text-label-2">
|
||||||
{r.locDc ?? '—'}
|
{r.locDc ?? '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-2 px-2.5 font-mono">
|
<td className="py-2 px-2.5 font-mono">
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export default {
|
|||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
accent: 'var(--color-accent)',
|
accent: 'var(--color-accent)',
|
||||||
|
'accent-muted': 'var(--color-accent-muted)',
|
||||||
info: 'var(--color-info)',
|
info: 'var(--color-info)',
|
||||||
tertiary: 'var(--color-tertiary)',
|
tertiary: 'var(--color-tertiary)',
|
||||||
danger: 'var(--color-danger)',
|
danger: 'var(--color-danger)',
|
||||||
@ -32,6 +33,10 @@ export default {
|
|||||||
DEFAULT: 'var(--color-boom)',
|
DEFAULT: 'var(--color-boom)',
|
||||||
hover: 'var(--color-boom-hover)',
|
hover: 'var(--color-boom-hover)',
|
||||||
},
|
},
|
||||||
|
navy: {
|
||||||
|
DEFAULT: 'var(--color-navy)',
|
||||||
|
hover: 'var(--color-navy-hover)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user