Merge pull request 'release: 2026-03-25.1 (5건 커밋)' (#198) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 1m55s
All checks were successful
Deploy KCG / deploy (push) Successful in 1m55s
This commit is contained in:
커밋
f1f965fcd4
@ -4,6 +4,15 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2026-03-25.1]
|
||||
|
||||
### 변경
|
||||
- 현장분석: AI 파이프라인 더미 애니메이션 → analysisMap 기반 ON/OFF 실상태 표시
|
||||
- 현장분석: BD-09 변환 STANDBY → bd09OffsetM 실측 탐지 수 표시
|
||||
- 보고서: 수역별 허가업종 하드코딩 → ZONE_ALLOWED 상수 동적 참조
|
||||
- 보고서: 건의사항 월/최대 어구 선단 실데이터 연동
|
||||
- 보고서 버튼: 상단 헤더 → 현장분석 내부 닫기 버튼 좌측으로 이동
|
||||
|
||||
## [2026-03-25]
|
||||
|
||||
### 추가
|
||||
@ -22,79 +31,31 @@
|
||||
- fishing_pattern.py: 마지막 조업 세그먼트 누락 버그 수정
|
||||
- 히스토리 모드 시 현재 강조 레이어 (deck.gl + MapLibre) 정상 숨김
|
||||
|
||||
## [2026-03-24.4]
|
||||
|
||||
### 추가
|
||||
- 경비함정 작전가이드 모달: 3탭 구성 (실시간탐지/대응절차/조치기준) + 임검침로 해상 루트 시각화
|
||||
- 중국어선 감시현황 보고서 자동 생성 모달
|
||||
- 중국어 경고문 TTS 음성 재생 (Google Translate TTS + Vite CORS 프록시)
|
||||
- KoreaMap 임검침로 점선 시각화 (buildSeaRoute 육지 우회 알고리즘)
|
||||
|
||||
### 변경
|
||||
- feature/korea-layers-enhancement 브랜치 기능 → develop 아키텍처(KoreaDashboard 분리 구조)에 이식
|
||||
- 어구 그룹 지도 클릭 시 좌측 패널 해당 섹션 자동 오픈 + 스크롤 연동
|
||||
|
||||
## [2026-03-24.3]
|
||||
|
||||
### 추가
|
||||
- 가상 선박 마커: 선단/어구 그룹 멤버를 ship-triangle 아이콘으로 표시 (COG 회전 + zoom interpolate)
|
||||
- 어구 겹침 해결: queryRenderedFeatures → 다중 선택 팝업 + 호버 하이라이트
|
||||
- AI 분석 통계 서버사이드 전환: dark/spoofing/risk/cluster/gear 집계를 Backend에서 계산
|
||||
|
||||
### 변경
|
||||
- cnFishingSuspects에 모선 MMSI 포함 (어구 패턴에서 모선명 추출 → 동일명 선박 추가)
|
||||
- AI 분석 패널: 클라이언트 사이드 stats 계산 로직 완전 제거 (14K+ 선박 순회 useMemo 삭제)
|
||||
- Backend /api/vessel-analysis 응답에 stats 필드 추가 (집계 통계 서버 제공)
|
||||
- GroupPolygonService에 어구 집계 SQL 추가 (gearGroups/gearCount)
|
||||
- FleetClusterLayer: 패널 아코디언 전환 (하나만 열림), 높이 제한 min(45vh, 400px)
|
||||
- vessel_store.py: COG bearing 계산 (마지막 2점 좌표 기반 atan2)
|
||||
|
||||
### 수정
|
||||
- 어구 줌인 최대 제한 (maxZoom: 12)
|
||||
|
||||
## [2026-03-24.2]
|
||||
|
||||
### 추가
|
||||
- 선단/어구그룹 폴리곤 서버사이드 생성: Shapely convex hull + buffer → PostGIS 저장
|
||||
- DB migration 009: group_polygon_snapshots 테이블 (5분 APPEND, 7일 보존)
|
||||
- Backend API: GET /api/vessel-analysis/groups (목록/상세/히스토리)
|
||||
- useGroupPolygons 훅: 5분 폴링 (fleet/gearInZone/gearOutZone)
|
||||
|
||||
### 변경
|
||||
- FleetClusterLayer: 클라이언트 convexHull/padPolygon 제거 → API GeoJSON 직접 렌더링
|
||||
- 프론트 어구그룹 탐지(regex+거리 클러스터링) Python 이관
|
||||
|
||||
### 수정
|
||||
- 불법어선 탭 복원 (임시 숨김 해제)
|
||||
|
||||
## [2026-03-24.1]
|
||||
|
||||
### 추가
|
||||
- 웹폰트 내장: @fontsource-variable Inter, Noto Sans KR, Fira Code 자체 호스팅
|
||||
- 폰트 상수 파일 (FONT_MONO, FONT_SANS) + 타입 선언
|
||||
|
||||
### 변경
|
||||
- 전체 font-family 통일: CSS 55곳 + deck.gl TextLayer 30곳 + 인라인 스타일 8곳
|
||||
- 이란 시설물 색상 사막 대비 고채도 팔레트 교체 (amber/orange/yellow → rose/sky/cyan/lime)
|
||||
- 이란 시설 라벨 fontWeight 600→700, alpha 200→255 (가독성 개선)
|
||||
- 접힘 패널 상하 패딩 균일화 (area-ship-header :last-child)
|
||||
|
||||
## [2026-03-24]
|
||||
|
||||
### 추가
|
||||
- LayerPanel 공통 트리 구조: LayerTreeNode 재귀 렌더러 (한국/이란 양쪽 적용)
|
||||
- 위험시설/해외시설 emoji→SVG IconLayer 전환 (12 SVG 함수, hazard/CN/JP 3개 IconLayer)
|
||||
- S&P Global 피격 선박 27척 데이터 (damagedShips.ts)
|
||||
- 이란 리플레이 실데이터 전환: Backend 시점 조회 API + Events CRUD
|
||||
- GeoEvent `sea_attack` 타입 + SEA ATK 배지 (피격 선박 이벤트 로그 통합)
|
||||
- 더미↔API 토글 UI (리플레이 배속 우측)
|
||||
- 대시보드 탭 localStorage 영속화
|
||||
- 지도 글꼴 크기 커스텀: 시설/선박/분석/지역 4그룹 슬라이더 (0.5~2.0x, LAYERS 하단)
|
||||
- 선단/어구그룹 폴리곤 서버사이드 생성: Shapely convex hull + buffer → PostGIS 저장 (DB migration 009, 5분 APPEND, 7일 보존)
|
||||
- Backend API: groups 목록/상세/히스토리 + vessel-analysis stats 필드 (집계 통계 서버 제공)
|
||||
- 가상 선박 마커: ship-triangle 아이콘 (COG 회전 + zoom interpolate) + 어구 겹침 다중 선택 팝업
|
||||
- AI 분석 통계 서버사이드 전환: dark/spoofing/risk/cluster/gear 집계를 Backend에서 계산
|
||||
- 경비함정 작전가이드 모달: 3탭 + 임검침로 해상 루트 시각화 + 중국어 TTS
|
||||
- 중국어선 감시현황 보고서 자동 생성 모달
|
||||
- 웹폰트 내장: @fontsource-variable Inter/Noto Sans KR/Fira Code + 폰트 상수
|
||||
- LayerPanel 공통 트리 구조: 재귀 렌더러 + 부모 캐스케이드 ON/OFF
|
||||
- 위험시설/해외시설 SVG IconLayer 전환 (12 SVG 함수)
|
||||
- 이란 리플레이 실데이터 전환: Events CRUD + 시점 조회 API + 피격 선박 27척
|
||||
- 지도 글꼴 크기 커스텀: 4그룹 슬라이더 (0.5~2.0x)
|
||||
- useGroupPolygons 훅 (5분 폴링) + useIranData dataSource 분기
|
||||
|
||||
### 변경
|
||||
- 부모 노드 토글→하위 전체 ON/OFF 캐스케이드 + 카운트 합산
|
||||
- useIranData dataSource 분기 (dummy=sampleData, api=Backend DB 3월1일~오늘)
|
||||
- fetchAircraftByRange, fetchOsintByRange, fetchEventsByRange 서비스 함수
|
||||
- FleetClusterLayer: 클라이언트 convexHull 제거 → API GeoJSON 렌더링 + 패널 아코디언 전환
|
||||
- AI 분석 패널: 클라이언트 stats 계산 제거 → 서버 제공 (14K+ 순회 useMemo 삭제)
|
||||
- 프론트 어구그룹 탐지 Python 이관 + 어구 클릭 시 좌측 패널 섹션 자동 전환
|
||||
- 전체 font-family 통일 (CSS 55곳 + deck.gl 30곳) + 이란 시설물 사막 대비 고채도 팔레트
|
||||
- feature/korea-layers-enhancement 브랜치 기능 → develop 아키텍처에 이식
|
||||
|
||||
### 수정
|
||||
- 불법어선 탭 복원 + 어구 줌인 최대 제한 (maxZoom: 12)
|
||||
|
||||
## [2026-03-23]
|
||||
|
||||
|
||||
@ -110,6 +110,7 @@ interface Props {
|
||||
ships: Ship[];
|
||||
vesselAnalysis?: UseVesselAnalysisResult;
|
||||
onClose: () => void;
|
||||
onShowReport?: () => void;
|
||||
}
|
||||
|
||||
const PIPE_STEPS = [
|
||||
@ -124,14 +125,14 @@ const PIPE_STEPS = [
|
||||
|
||||
const ALERT_ORDER: Record<string, number> = { CRITICAL: 0, WATCH: 1, MONITOR: 2, NORMAL: 3 };
|
||||
|
||||
export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
export function FieldAnalysisModal({ ships, vesselAnalysis, onClose, onShowReport }: Props) {
|
||||
const emptyMap = useMemo(() => new Map<string, VesselAnalysisDto>(), []);
|
||||
const analysisMap = vesselAnalysis?.analysisMap ?? emptyMap;
|
||||
const [activeFilter, setActiveFilter] = useState('ALL');
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedMmsi, setSelectedMmsi] = useState<string | null>(null);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
const [pipeStep, setPipeStep] = useState(0);
|
||||
// pipeStep 제거 — 파이프라인 상태는 analysisMap 존재 여부로 판단
|
||||
const [tick, setTick] = useState(0);
|
||||
|
||||
// 중국 어선만 필터
|
||||
@ -189,9 +190,13 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
// 통계 — Python 분석 결과 기반
|
||||
const stats = useMemo(() => {
|
||||
let gpsAnomaly = 0;
|
||||
let bd09Detected = 0;
|
||||
for (const v of processed) {
|
||||
const dto = analysisMap.get(v.ship.mmsi);
|
||||
if (dto && dto.algorithms.gpsSpoofing.spoofingScore > 0.5) gpsAnomaly++;
|
||||
if (dto) {
|
||||
if (dto.algorithms.gpsSpoofing.spoofingScore > 0.5) gpsAnomaly++;
|
||||
if (dto.algorithms.gpsSpoofing.bd09OffsetM > 100) bd09Detected++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
total: processed.length,
|
||||
@ -199,6 +204,7 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
fishing: processed.filter(v => v.state === 'FISHING').length,
|
||||
aisLoss: processed.filter(v => v.state === 'AIS_LOSS' || v.state === 'UNKNOWN').length,
|
||||
gpsAnomaly,
|
||||
bd09Detected,
|
||||
clusters: new Set(processed.filter(v => v.cluster !== '—').map(v => v.cluster)).size,
|
||||
trawl: processed.filter(v => v.vtype === 'TRAWL').length,
|
||||
purse: processed.filter(v => v.vtype === 'PURSE').length,
|
||||
@ -231,12 +237,6 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// AI 파이프라인 애니메이션
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setPipeStep(s => s + 1), 1200);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
// 시계 tick
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setTick(s => s + 1), 1000);
|
||||
@ -349,6 +349,19 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
LIVE
|
||||
</span>
|
||||
<span style={{ color: C.ink2, fontSize: 10 }}>{new Date().toLocaleTimeString('ko-KR')}</span>
|
||||
{onShowReport && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onShowReport}
|
||||
style={{
|
||||
background: 'rgba(99,179,237,0.1)', border: '1px solid rgba(99,179,237,0.4)',
|
||||
color: '#63b3ed', padding: '4px 14px', cursor: 'pointer',
|
||||
fontSize: 11, borderRadius: 2, fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
📋 보고서
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@ -430,38 +443,46 @@ export function FieldAnalysisModal({ ships, vesselAnalysis, onClose }: Props) {
|
||||
|
||||
<div style={{ fontSize: 9, letterSpacing: 2, color: C.cyan, margin: '12px 0 8px', paddingBottom: 6, borderBottom: `1px solid ${C.border}` }}>
|
||||
AI 파이프라인 상태
|
||||
<span style={{ float: 'right', color: C.green, fontSize: 8 }}>●</span>
|
||||
<span style={{ float: 'right', color: analysisMap.size > 0 ? C.green : C.red, fontSize: 8 }}>●</span>
|
||||
</div>
|
||||
|
||||
{PIPE_STEPS.map((step, idx) => {
|
||||
const isRunning = idx === pipeStep % PIPE_STEPS.length;
|
||||
{PIPE_STEPS.map((step) => {
|
||||
const connected = analysisMap.size > 0;
|
||||
return (
|
||||
<div key={step.num} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 5 }}>
|
||||
<span style={{ fontSize: 9, color: C.ink3, width: 20 }}>{step.num}</span>
|
||||
<span style={{ fontSize: 10, color: C.ink, flex: 1 }}>{step.name}</span>
|
||||
<span style={{
|
||||
fontSize: 8, padding: '1px 6px', borderRadius: 2,
|
||||
background: isRunning ? 'rgba(0,230,118,0.15)' : 'rgba(0,230,118,0.06)',
|
||||
border: `1px solid ${isRunning ? C.green : C.border}`,
|
||||
color: isRunning ? C.green : C.ink3,
|
||||
fontWeight: isRunning ? 700 : 400,
|
||||
background: connected ? 'rgba(0,230,118,0.1)' : 'rgba(255,82,82,0.1)',
|
||||
border: `1px solid ${connected ? C.green : C.red}`,
|
||||
color: connected ? C.green : C.red,
|
||||
fontWeight: 400,
|
||||
}}>
|
||||
{isRunning ? 'PROC' : 'OK'}
|
||||
{connected ? 'ON' : 'OFF'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{[
|
||||
{ num: 'GPS', name: 'BD-09 변환', status: 'STANDBY', color: C.ink3 },
|
||||
{ num: 'NRD', name: '레이더 교차검증', status: '미연동', color: C.ink3 },
|
||||
{
|
||||
num: 'GPS', name: 'BD-09 변환',
|
||||
status: stats.bd09Detected > 0 ? `${stats.bd09Detected}척 탐지` : 'CLEAR',
|
||||
color: stats.bd09Detected > 0 ? C.amber : C.green,
|
||||
active: stats.bd09Detected > 0,
|
||||
},
|
||||
{ num: 'NRD', name: '레이더 교차검증', status: '미연동', color: C.ink3, active: false },
|
||||
].map(step => (
|
||||
<div key={step.num} style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 5 }}>
|
||||
<span style={{ fontSize: 9, color: C.ink3, width: 20 }}>{step.num}</span>
|
||||
<span style={{ fontSize: 10, color: C.ink, flex: 1 }}>{step.name}</span>
|
||||
<span style={{
|
||||
fontSize: 8, padding: '1px 6px', borderRadius: 2,
|
||||
background: 'rgba(24,255,255,0.08)', border: `1px solid ${C.border}`, color: step.color,
|
||||
background: step.active ? 'rgba(255,215,64,0.12)' : 'rgba(24,255,255,0.08)',
|
||||
border: `1px solid ${step.active ? C.amber : C.border}`,
|
||||
color: step.color,
|
||||
fontWeight: step.active ? 700 : 400,
|
||||
}}>
|
||||
{step.status}
|
||||
</span>
|
||||
|
||||
@ -171,6 +171,13 @@ export const KoreaDashboard = ({
|
||||
const vesselAnalysis = useVesselAnalysis(true);
|
||||
const groupPolygons = useGroupPolygons(true);
|
||||
|
||||
const largestGearGroup = useMemo(() => {
|
||||
const gears = groupPolygons.allGroups.filter(g => g.groupType !== 'FLEET');
|
||||
if (gears.length === 0) return undefined;
|
||||
const max = gears.reduce((a, b) => a.memberCount > b.memberCount ? a : b);
|
||||
return { name: max.groupLabel, count: max.memberCount };
|
||||
}, [groupPolygons.allGroups]);
|
||||
|
||||
const koreaFiltersResult = useKoreaFilters(
|
||||
koreaData.ships,
|
||||
koreaData.visibleShips,
|
||||
@ -300,10 +307,6 @@ export const KoreaDashboard = ({
|
||||
onClick={() => setShowFieldAnalysis(v => !v)} title="현장분석">
|
||||
<span className="text-[11px]">📊</span>현장분석
|
||||
</button>
|
||||
<button type="button" className={`mode-btn ${showReport ? 'active' : ''}`}
|
||||
onClick={() => setShowReport(v => !v)} title="감시현황 보고서">
|
||||
<span className="text-[11px]">📋</span>보고서
|
||||
</button>
|
||||
<button type="button" className={`mode-btn ${showOpsGuide ? 'active' : ''}`}
|
||||
onClick={() => setShowOpsGuide(v => !v)} title="경비함정 작전 가이드">
|
||||
<span className="text-[11px]">⚓</span>작전가이드
|
||||
@ -327,10 +330,11 @@ export const KoreaDashboard = ({
|
||||
ships={koreaData.ships}
|
||||
vesselAnalysis={vesselAnalysis}
|
||||
onClose={() => setShowFieldAnalysis(false)}
|
||||
onShowReport={() => setShowReport(v => !v)}
|
||||
/>
|
||||
)}
|
||||
{showReport && (
|
||||
<ReportModal ships={koreaData.ships} onClose={() => setShowReport(false)} />
|
||||
<ReportModal ships={koreaData.ships} onClose={() => setShowReport(false)} largestGearGroup={largestGearGroup} />
|
||||
)}
|
||||
{showOpsGuide && (
|
||||
<OpsGuideModal
|
||||
|
||||
@ -1,12 +1,42 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import type { Ship } from '../../types';
|
||||
import { getMarineTrafficCategory } from '../../utils/marineTraffic';
|
||||
import { aggregateFishingStats, GEAR_LABELS, classifyFishingZone } from '../../utils/fishingAnalysis';
|
||||
import { aggregateFishingStats, GEAR_LABELS, classifyFishingZone, ZONE_ALLOWED } from '../../utils/fishingAnalysis';
|
||||
import type { FishingGearType } from '../../utils/fishingAnalysis';
|
||||
|
||||
interface Props {
|
||||
ships: Ship[];
|
||||
onClose: () => void;
|
||||
largestGearGroup?: { name: string; count: number };
|
||||
}
|
||||
|
||||
const ALL_GEAR_TYPES = ['PT', 'OT', 'GN', 'PS', 'FC'];
|
||||
|
||||
const ZONE_LABELS: Record<string, string> = {
|
||||
ZONE_I: '수역 I (동해)',
|
||||
ZONE_II: '수역 II (남해)',
|
||||
ZONE_III: '수역 III (서남해)',
|
||||
ZONE_IV: '수역 IV (서해)',
|
||||
};
|
||||
|
||||
const ZONE_EXTRA_NOTES: Record<string, string> = {
|
||||
ZONE_III: '이어도 해역',
|
||||
};
|
||||
|
||||
function zoneAllowedText(zone: string): string {
|
||||
const allowed = ZONE_ALLOWED[zone];
|
||||
if (!allowed || allowed.length === 0) return '-';
|
||||
if (allowed.length >= ALL_GEAR_TYPES.length) return '전 업종';
|
||||
return allowed.join(', ') + (allowed.length <= 2 ? '만' : '');
|
||||
}
|
||||
|
||||
function zoneViolationText(zone: string): string {
|
||||
const allowed = ZONE_ALLOWED[zone];
|
||||
if (!allowed) return '-';
|
||||
const violations = ALL_GEAR_TYPES.filter(t => !allowed.includes(t));
|
||||
if (violations.length === 0) return ZONE_EXTRA_NOTES[zone] || '-';
|
||||
const extra = ZONE_EXTRA_NOTES[zone] ? ` (${ZONE_EXTRA_NOTES[zone]})` : '';
|
||||
return `${violations.join('/')} 발견 시 위반${extra}`;
|
||||
}
|
||||
|
||||
function now() {
|
||||
@ -14,7 +44,7 @@ function now() {
|
||||
return `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export function ReportModal({ ships, onClose }: Props) {
|
||||
export function ReportModal({ ships, onClose, largestGearGroup }: Props) {
|
||||
const reportRef = useRef<HTMLDivElement>(null);
|
||||
const timestamp = useMemo(() => now(), []);
|
||||
|
||||
@ -186,13 +216,17 @@ export function ReportModal({ ships, onClose }: Props) {
|
||||
<h2 style={h2Style}>4. 특정어업수역별 분포</h2>
|
||||
<table style={tableStyle}>
|
||||
<thead><tr style={{ background: '#1e293b' }}>
|
||||
<th style={thStyle}>수역</th><th style={thStyle}>어선 수</th><th style={thStyle}>허가 업종 (3월)</th><th style={thStyle}>비고</th>
|
||||
<th style={thStyle}>수역</th><th style={thStyle}>어선 수</th><th style={thStyle}>허가 업종 ({new Date().getMonth() + 1}월)</th><th style={thStyle}>비고</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr><td style={tdStyle}>수역 I (동해)</td><td style={tdBold}>{stats.zoneStats.ZONE_I}</td><td style={tdDim}>PS, FC만</td><td style={tdDim}>PT/OT/GN 발견 시 위반</td></tr>
|
||||
<tr><td style={tdStyle}>수역 II (남해)</td><td style={tdBold}>{stats.zoneStats.ZONE_II}</td><td style={tdDim}>전 업종</td><td style={tdDim}>-</td></tr>
|
||||
<tr><td style={tdStyle}>수역 III (서남해)</td><td style={tdBold}>{stats.zoneStats.ZONE_III}</td><td style={tdDim}>전 업종</td><td style={tdDim}>이어도 해역</td></tr>
|
||||
<tr><td style={tdStyle}>수역 IV (서해)</td><td style={tdBold}>{stats.zoneStats.ZONE_IV}</td><td style={tdDim}>GN, PS, FC</td><td style={tdDim}>PT/OT 발견 시 위반</td></tr>
|
||||
{(['ZONE_I', 'ZONE_II', 'ZONE_III', 'ZONE_IV'] as const).map(zone => (
|
||||
<tr key={zone}>
|
||||
<td style={tdStyle}>{ZONE_LABELS[zone]}</td>
|
||||
<td style={tdBold}>{stats.zoneStats[zone]}</td>
|
||||
<td style={tdDim}>{zoneAllowedText(zone)}</td>
|
||||
<td style={tdDim}>{zoneViolationText(zone)}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr style={{ background: 'rgba(239,68,68,0.1)' }}><td style={tdStyle}>수역 외</td><td style={{ ...tdBold, color: '#ef4444' }}>{stats.zoneStats.OUTSIDE}</td><td style={tdDim}>-</td><td style={{ ...tdDim, color: '#ef4444' }}>비허가 구역</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -226,11 +260,15 @@ export function ReportModal({ ships, onClose }: Props) {
|
||||
{/* 7. 건의사항 */}
|
||||
<h2 style={h2Style}>7. 건의사항</h2>
|
||||
<div style={{ fontSize: 10, color: '#94a3b8', paddingLeft: 12 }}>
|
||||
<p>1. 현재 3월은 전 업종 조업 가능 기간으로, <strong style={{ color: '#f59e0b' }}>수역 이탈 및 본선-부속선 분리</strong> 중심 감시 권고</p>
|
||||
<p>1. 현재 {new Date().getMonth() + 1}월은 전 업종 조업 가능 기간으로, <strong style={{ color: '#f59e0b' }}>수역 이탈 및 본선-부속선 분리</strong> 중심 감시 권고</p>
|
||||
<p>2. 다크베셀 의심 {stats.darkSuspect.length}척에 대해 <strong style={{ color: '#ef4444' }}>SAR 위성 집중 탐색</strong> 요청</p>
|
||||
<p>3. 수역 외 어선 {stats.zoneStats.OUTSIDE}척에 대해 <strong style={{ color: '#ef4444' }}>즉시 현장 확인</strong> 필요</p>
|
||||
<p>4. 4/16 저인망 휴어기 진입 대비 <strong>감시 강화 계획 수립</strong> 권고</p>
|
||||
<p>5. 宁波海裕 위망 선단 16척 <strong>그룹 위치 상시 추적</strong> 유지</p>
|
||||
{largestGearGroup ? (
|
||||
<p>5. {largestGearGroup.name} 선단 {largestGearGroup.count}척 <strong>그룹 위치 상시 추적</strong> 유지</p>
|
||||
) : (
|
||||
<p>5. 대형 어구 선단 <strong>그룹 위치 상시 추적</strong> 유지</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@ -56,7 +56,7 @@ export interface FishingZoneInfo {
|
||||
}
|
||||
|
||||
/** 수역별 허가 업종 */
|
||||
const ZONE_ALLOWED: Record<string, string[]> = {
|
||||
export const ZONE_ALLOWED: Record<string, string[]> = {
|
||||
ZONE_I: ['PS', 'FC'],
|
||||
ZONE_II: ['PT', 'OT', 'GN', 'PS', 'FC'],
|
||||
ZONE_III: ['PT', 'OT', 'GN', 'PS', 'FC'],
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user