From eee9e798188d7ee8efbe1bb75568914cfb4271d3 Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 20 Apr 2026 06:31:11 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(ui):=20=EB=AA=A8=EB=8B=88=ED=84=B0?= =?UTF-8?q?=EB=A7=81/=EB=94=94=EC=9E=90=EC=9D=B8=EC=8B=9C=EC=8A=A4?= =?UTF-8?q?=ED=85=9C=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=ED=95=B4=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### SystemStatusPanel TypeError - 증상: /monitoring 에서 Uncaught TypeError: Cannot read properties of undefined (reading 'toLocaleString') - 원인: stats 객체는 존재하나 total 필드가 undefined 인 경우 (백엔드 응답이 기대 shape 와 다를 때) 크래시 - 수정: stats?.total != null ? ... / stats.critical ?? 0 식 null-safe 전환 (total/clusterCount/gearGroups/critical/high/medium/low 전부) ### CatalogBadges 렌더링 오류 - 증상: /design-system.html 에서 (1) Each child in a list should have a unique "key" prop (2) Objects are not valid as a React child (found: object with keys {ko, en}) - 원인: PERFORMANCE_STATUS_META 의 meta 는 {intent, hex, label: {ko, en}} 형식. code 필드 없고 label 이 객체. - Object.values() + 로 undefined key 중복 - getKoLabel 이 meta.label (객체) 그대로 반환해 Badge children 에 객체 주입 다른 카탈로그는 fallback: {ko, en} 패턴이라 문제 없음 (performanceStatus 만 label 객체) - 수정: - Object.entries() 로 순회해 Record key 를 안정적 식별자로 사용 - AnyMeta.label 타입을 string | {ko,en} 확장 - getKoLabel/getEnLabel 우선순위: fallback.ko → label.ko → label(문자열) → code → key - PERFORMANCE_STATUS_META 자체는 변경 안 함 (admin 페이지들이 label.ko/label.en 직접 참조 중) ### 검증 - npx tsc --noEmit 통과 - pre-commit tsc+ESLint 통과 --- .../design-system/sections/CatalogSection.tsx | 35 +++++++++++++------ .../features/monitoring/SystemStatusPanel.tsx | 14 ++++---- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/frontend/src/design-system/sections/CatalogSection.tsx b/frontend/src/design-system/sections/CatalogSection.tsx index e18a475..0ae5c99 100644 --- a/frontend/src/design-system/sections/CatalogSection.tsx +++ b/frontend/src/design-system/sections/CatalogSection.tsx @@ -11,19 +11,30 @@ import { CATALOG_REGISTRY, type CatalogEntry } from '@shared/constants/catalogRe */ interface AnyMeta { - code: string; + /** 일부 카탈로그는 code 없이 Record key 만 사용 (예: PERFORMANCE_STATUS_META) */ + code?: string; intent?: BadgeIntent; fallback?: { ko: string; en: string }; classes?: string | { bg?: string; text?: string; border?: string }; - label?: string; + /** 문자열 라벨 또는 { ko, en } 객체 라벨 양쪽 지원 */ + label?: string | { ko: string; en: string }; } -function getKoLabel(meta: AnyMeta): string { - return meta.fallback?.ko ?? meta.label ?? meta.code; +function getKoLabel(meta: AnyMeta, fallbackKey: string): string { + if (meta.fallback?.ko) return meta.fallback.ko; + if (meta.label && typeof meta.label === 'object' && 'ko' in meta.label) { + return meta.label.ko; + } + if (typeof meta.label === 'string') return meta.label; + return meta.code ?? fallbackKey; } function getEnLabel(meta: AnyMeta): string | undefined { - return meta.fallback?.en; + if (meta.fallback?.en) return meta.fallback.en; + if (meta.label && typeof meta.label === 'object' && 'en' in meta.label) { + return meta.label.en; + } + return undefined; } function getFallbackClasses(meta: AnyMeta): string | undefined { @@ -55,17 +66,19 @@ function renderBadge(meta: AnyMeta, label: string): ReactNode { } function CatalogBadges({ entry }: { entry: CatalogEntry }) { - const items = Object.values(entry.items) as AnyMeta[]; + // Record key 를 안정적 식별자로 사용 (일부 카탈로그는 meta.code 없음) + const items = Object.entries(entry.items) as [string, AnyMeta][]; return (
- {items.map((meta) => { - const koLabel = getKoLabel(meta); + {items.map(([key, meta]) => { + const displayCode = meta.code ?? key; + const koLabel = getKoLabel(meta, key); const enLabel = getEnLabel(meta); - const trkId = `${entry.showcaseId}-${meta.code}`; + const trkId = `${entry.showcaseId}-${displayCode}`; return ( - + - {meta.code} + {displayCode}
{renderBadge(meta, koLabel)}
diff --git a/frontend/src/features/monitoring/SystemStatusPanel.tsx b/frontend/src/features/monitoring/SystemStatusPanel.tsx index b90222e..3c4ab09 100644 --- a/frontend/src/features/monitoring/SystemStatusPanel.tsx +++ b/frontend/src/features/monitoring/SystemStatusPanel.tsx @@ -101,9 +101,9 @@ export function SystemStatusPanel() { status={stats ? 'CONNECTED' : 'DISCONNECTED'} statusIntent={stats ? 'success' : 'critical'} details={[ - ['선박 분석', stats ? `${stats.total.toLocaleString()}건` : '-'], - ['클러스터', stats ? `${stats.clusterCount}` : '-'], - ['어구 그룹', stats ? `${stats.gearGroups}` : '-'], + ['선박 분석', stats?.total != null ? `${stats.total.toLocaleString()}건` : '-'], + ['클러스터', stats?.clusterCount != null ? `${stats.clusterCount}` : '-'], + ['어구 그룹', stats?.gearGroups != null ? `${stats.gearGroups}` : '-'], ]} /> @@ -124,10 +124,10 @@ export function SystemStatusPanel() { {/* 위험도 분포 */} {stats && (
- - - - + + + +
)} From d971624090c62c6e6ea05d430fc1c631a53fd55c Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 20 Apr 2026 06:31:33 +0900 Subject: [PATCH 2/2] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/RELEASE-NOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 31744bb..7577d03 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -4,6 +4,9 @@ ## [Unreleased] +### 수정 +- **모니터링/디자인시스템 런타임 에러 해소** — `/monitoring` 의 `SystemStatusPanel` 에서 `stats.total.toLocaleString()` 호출이 백엔드 응답 shape 이슈로 `stats.total` 이 undefined 일 때 Uncaught TypeError 로 크래시하던 문제 null-safe 로 해소(`stats?.total != null`). `/design-system.html` 의 `CatalogBadges` 가 `PERFORMANCE_STATUS_META` 의 `label: {ko, en}` 객체를 그대로 Badge children 으로 주입해 "Objects are not valid as a React child" 를 던지고 `code` 필드 부재로 key 중복 경고가 함께 뜨던 문제 해소 — `Object.entries` 순회 + `AnyMeta.label` 을 `string | {ko,en}` 로 확장 + getKoLabel/getEnLabel 에 label 객체 케이스 추가 + ### 추가 - **환적 의심 전용 탐지 페이지 신설 (Phase 0-3)** — `/transshipment` 경로에 READ 전용 대시보드 추가. prediction `algorithms/transshipment.py` 의 5단계 필터 파이프라인 결과(is_transship_suspect=true)를 전체 목록·집계·상세 수준으로 조회. KPI 5장(Total + Transship tier CRITICAL/HIGH/MEDIUM + 종합 위험 CRITICAL) + DataTable 8컬럼 + 필터(hours/level/mmsi) + features JSON 상세. 기존 `/api/analysis/transship` + `getTransshipSuspects` 재사용해 backend 변경 없음. V033 마이그레이션으로 `detection:transshipment` 권한 트리 + 전 역할 READ 부여. (docs/prediction-analysis.md P1 UI 미노출 탐지 해소 — 2/2) - **불법 조업 이벤트 전용 페이지 신설 (Phase 0-2)** — `/illegal-fishing` 경로에 READ 전용 대시보드 추가. event_generator 가 생산하는 `GEAR_ILLEGAL`(G-01/G-05/G-06) + `EEZ_INTRUSION`(영해·접속수역) + `ZONE_DEPARTURE`(특정수역 진입) 3 카테고리를 한 화면에서 통합 조회. 심각도 KPI 5장 + 카테고리별 3장 + DataTable(7컬럼) + 필터(category/level/mmsi) + JSON features 상세 패널 + EventList 네비게이션. 기존 `/api/events` 를 category 다중 병렬 조회로 래핑하여 backend 변경 없이 구현. V032 마이그레이션으로 `detection:illegal-fishing` 권한 트리 + 전 역할 READ 부여 (운영자 처리 액션은 EventList 경유)