diff --git a/CLAUDE.md b/CLAUDE.md index ffe7de2..6f311df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,6 +202,35 @@ deploy/ # systemd + nginx 배포 설정 | **DB** | kcgdb | 211.208.115.83:5432/kcgdb (유저: kcg_app, pw: Kcg2026monitor) | | **DB** | snpdb | 211.208.115.83:5432/snpdb (유저: snp, pw: snp#8932, 읽기 전용) | +## 디버그 도구 가이드 + +### 원칙 +- 디버그/개발 전용 기능은 `import.meta.env.DEV` 가드로 감싸서 **프로덕션 빌드에서 코드 자체가 제거**되도록 구현 +- Vite production 빌드 시 `import.meta.env.DEV = false` → dead code elimination → 번들 미포함 +- 무거운 DB 조회, 통계 계산 등도 DEV 가드 안이면 프로덕션에 영향 없음 + +### 파일 구조 +- 디버그 컴포넌트: `frontend/src/components/{도메인}/debug/` 디렉토리에 분리 +- 메인 컴포넌트에서는 import + DEV 가드로만 연결: +```tsx +import { DebugTool } from './debug/DebugTool'; +const debug = import.meta.env.DEV ? useDebugHook() : null; +// JSX: +{debug && } +``` + +### 기존 디버그 도구 +| 도구 | 위치 | 기능 | +|------|------|------| +| CoordDebugTool | `korea/debug/CoordDebugTool.tsx` | Ctrl+Click 좌표 표시 (DD/DMS, 다중 포인트) | + +### 디버그 도구 분류 기준 +다음에 해당하면 디버그 도구로 분류하고, 불확실하면 사용자에게 확인: +- 개발/검증 목적의 좌표/데이터 표시 도구 +- 프로덕션 사용자에게 불필요한 진단 정보 +- 임시 데이터 시각화, 성능 프로파일링 +- 특정 조건에서만 활성화되는 테스트 기능 + ## 팀 규칙 - 코드 스타일: `.claude/rules/code-style.md` diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index a9ade65..e28d316 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -201,6 +201,10 @@ const FILTER_I18N_KEY: Record = { cnFishing: 'filters.cnFishingMonitor', }; +// [DEBUG] 좌표 디버그 도구 — 프로덕션 빌드에서 tree-shaking 제거 +import { useCoordDebug } from './debug/useCoordDebug'; +import { CoordDebugOverlay } from './debug/CoordDebugTool'; + export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, groupPolygons, hiddenShipCategories, hiddenNationalities, externalFlyTo, onExternalFlyToDone, opsRoute }: Props) { const { t } = useTranslation(); const mapRef = useRef(null); @@ -212,6 +216,8 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF const [selectedFleetData, setSelectedFleetData] = useState(null); const { fontScale } = useFontScale(); const [zoomLevel, setZoomLevel] = useState(KOREA_MAP_ZOOM); + // [DEBUG] 좌표 디버그 — DEV에서만 활성화, 프로덕션 빌드에서 tree-shaking 제거 + const coordDebug = useCoordDebug(!import.meta.env.DEV); const zoomRef = useRef(KOREA_MAP_ZOOM); const handleZoom = useCallback((e: { viewState: { zoom: number } }) => { const z = Math.floor(e.viewState.zoom); @@ -606,9 +612,13 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF style={{ width: '100%', height: '100%' }} mapStyle={MAP_STYLE} onZoom={handleZoom} + onClick={coordDebug ? coordDebug.handleMapClick : undefined} > + {/* [DEBUG] Ctrl+Click 좌표 표시 — 프로덕션에서 제거 */} + {coordDebug && } + = 0 ? 'N' : 'S') : (dd >= 0 ? 'E' : 'W'); + const abs = Math.abs(dd); + const d = Math.floor(abs); + const mFull = (abs - d) * 60; + const m = Math.floor(mFull); + const s = ((mFull - m) * 60).toFixed(2); + return `${d}°${String(m).padStart(2, '0')}′${String(s).padStart(5, '0')}″${dir}`; +} + +export function CoordDebugOverlay({ points, onRemove }: { points: CoordPoint[]; onRemove: (id: number) => void }) { + return ( + <> + {points.map(cp => ( +
+ +
+ + onRemove(cp.id)} + closeButton={true} + closeOnClick={false} + anchor="bottom" + offset={[0, -10]} + style={{ zIndex: 50 }} + > +
+
+ WGS84 (EPSG:4326) +
+
DD
+
{cp.lat.toFixed(6)}°N
+
{cp.lng.toFixed(6)}°E
+
DMS
+
{toDMS(cp.lat, 'lat')}
+
{toDMS(cp.lng, 'lng')}
+
+
+
+ ))} + + ); +} diff --git a/frontend/src/components/korea/debug/useCoordDebug.ts b/frontend/src/components/korea/debug/useCoordDebug.ts new file mode 100644 index 0000000..c8fae87 --- /dev/null +++ b/frontend/src/components/korea/debug/useCoordDebug.ts @@ -0,0 +1,30 @@ +/** + * [DEBUG] Ctrl+Click 좌표 디버그 훅 + * - disabled=true 시 noop (프로덕션 빌드에서 dead code 제거) + */ +import { useState, useCallback } from 'react'; +import type { MapLayerMouseEvent } from 'react-map-gl/maplibre'; + +export interface CoordPoint { + lat: number; + lng: number; + id: number; +} + +export function useCoordDebug(disabled = false) { + const [points, setPoints] = useState([]); + + const handleMapClick = useCallback((e: MapLayerMouseEvent) => { + if (disabled) return; + if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { + e.originalEvent.preventDefault(); + setPoints(prev => [...prev, { lat: e.lngLat.lat, lng: e.lngLat.lng, id: Date.now() }]); + } + }, [disabled]); + + const removePoint = useCallback((id: number) => { + setPoints(prev => prev.filter(p => p.id !== id)); + }, []); + + return { points, handleMapClick, removePoint, disabled }; +}