From 8f342f70b76d7ea6c970bac2463e5001b7ae6052 Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 26 Mar 2026 10:17:04 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat(debug):=20Ctrl+Click=20=EC=A2=8C?= =?UTF-8?q?=ED=91=9C=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EB=8F=84=EA=B5=AC=20?= =?UTF-8?q?+=20DEV=20=EA=B0=80=EB=93=9C=20=EC=B2=B4=EA=B3=84=20=EA=B5=AC?= =?UTF-8?q?=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CoordDebugTool: Ctrl+Click 다중 좌표 표시 (DD/DMS, WGS84) - import.meta.env.DEV 가드로 프로덕션 빌드에서 코드 제거 - CLAUDE.md: 디버그 도구 가이드 섹션 추가 --- CLAUDE.md | 29 ++++++++++ frontend/src/components/korea/KoreaMap.tsx | 10 ++++ .../components/korea/debug/CoordDebugTool.tsx | 56 +++++++++++++++++++ .../components/korea/debug/useCoordDebug.ts | 30 ++++++++++ 4 files changed, 125 insertions(+) create mode 100644 frontend/src/components/korea/debug/CoordDebugTool.tsx create mode 100644 frontend/src/components/korea/debug/useCoordDebug.ts 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 }; +} -- 2.45.2 From 364a34ce102782e64a76e019d690950a8036aceb Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 26 Mar 2026 10:30:58 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor(debug):=20=EC=9E=90=EC=B2=B4=20?= =?UTF-8?q?=EC=99=84=EA=B2=B0=ED=98=95=20DevCoordDebug=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20=E2=80=94=20=ED=94=84=EB=A1=9C=EB=8D=95?= =?UTF-8?q?=EC=85=98=20=EB=B2=88=EB=93=A4=20=EC=99=84=EC=A0=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20=EB=B3=B4=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lazy + 동적 import로 DEV에서만 청크 로드 - mapRef 기반 이벤트 등록으로 KoreaMap 코드에 디버그 흔적 없음 - 이전 CoordDebugTool/useCoordDebug 삭제 --- frontend/src/components/korea/KoreaMap.tsx | 15 +++---- .../{CoordDebugTool.tsx => DevCoordDebug.tsx} | 44 ++++++++++++++++--- .../components/korea/debug/useCoordDebug.ts | 30 ------------- 3 files changed, 46 insertions(+), 43 deletions(-) rename frontend/src/components/korea/debug/{CoordDebugTool.tsx => DevCoordDebug.tsx} (58%) delete mode 100644 frontend/src/components/korea/debug/useCoordDebug.ts diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index e28d316..1df454a 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -201,9 +201,11 @@ const FILTER_I18N_KEY: Record = { cnFishing: 'filters.cnFishingMonitor', }; -// [DEBUG] 좌표 디버그 도구 — 프로덕션 빌드에서 tree-shaking 제거 -import { useCoordDebug } from './debug/useCoordDebug'; -import { CoordDebugOverlay } from './debug/CoordDebugTool'; +// [DEBUG] 좌표 디버그 — DEV에서만 동적 로드, 프로덕션 번들에서 완전 제거 +import { lazy, Suspense } from 'react'; +const DevCoordDebug = import.meta.env.DEV + ? lazy(() => import('./debug/DevCoordDebug')) + : null; 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(); @@ -216,8 +218,6 @@ 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); @@ -612,12 +612,11 @@ 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 && } + {/* [DEBUG] Ctrl+Click 좌표 표시 — 프로덕션 번들에서 완전 제거 */} + {DevCoordDebug && } = 0 ? 'N' : 'S') : (dd >= 0 ? 'E' : 'W'); @@ -15,7 +23,33 @@ function toDMS(dd: number, axis: 'lat' | 'lng'): string { return `${d}°${String(m).padStart(2, '0')}′${String(s).padStart(5, '0')}″${dir}`; } -export function CoordDebugOverlay({ points, onRemove }: { points: CoordPoint[]; onRemove: (id: number) => void }) { +interface Props { + mapRef: React.RefObject; +} + +/** + * 자체 완결형 디버그 오버레이. + * mapRef를 받아서 직접 click 이벤트를 등록/해제. + * KoreaMap의 기존 코드에 어떤 것도 섞이지 않음. + */ +export default function DevCoordDebug({ mapRef }: Props) { + const [points, setPoints] = useState([]); + + useEffect(() => { + const map = mapRef.current?.getMap(); + if (!map) return; + + const handler = (e: maplibregl.MapMouseEvent) => { + if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) { + e.originalEvent.preventDefault(); + setPoints(prev => [...prev, { lat: e.lngLat.lat, lng: e.lngLat.lng, id: Date.now() }]); + } + }; + + map.on('click', handler); + return () => { map.off('click', handler); }; + }, [mapRef]); + return ( <> {points.map(cp => ( @@ -30,7 +64,7 @@ export function CoordDebugOverlay({ points, onRemove }: { points: CoordPoint[]; onRemove(cp.id)} + onClose={() => setPoints(prev => prev.filter(p => p.id !== cp.id))} closeButton={true} closeOnClick={false} anchor="bottom" diff --git a/frontend/src/components/korea/debug/useCoordDebug.ts b/frontend/src/components/korea/debug/useCoordDebug.ts deleted file mode 100644 index c8fae87..0000000 --- a/frontend/src/components/korea/debug/useCoordDebug.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * [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 }; -} -- 2.45.2 From 7fe9e048bfdeb85d90b8810d438249b547efa4bd Mon Sep 17 00:00:00 2001 From: htlee Date: Thu, 26 Mar 2026 10:46:05 +0900 Subject: [PATCH 3/3] =?UTF-8?q?refactor(debug):=20DebugTools=20=ED=97=88?= =?UTF-8?q?=EB=B8=8C=20=ED=8C=A8=ED=84=B4=20=E2=80=94=20=EB=8B=A8=EC=9D=BC?= =?UTF-8?q?=20lazy=20import=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - debug/index.tsx: 모든 디버그 도구 조합 export - KoreaMap: lazy import 1줄 + JSX 1줄만 유지 - 디버그 도구 추가/제거 시 debug/index.tsx만 수정 --- frontend/src/components/korea/KoreaMap.tsx | 10 ++++---- frontend/src/components/korea/debug/index.tsx | 24 +++++++++++++++++++ 2 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 frontend/src/components/korea/debug/index.tsx diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 1df454a..9e2c0d3 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -201,10 +201,10 @@ const FILTER_I18N_KEY: Record = { cnFishing: 'filters.cnFishingMonitor', }; -// [DEBUG] 좌표 디버그 — DEV에서만 동적 로드, 프로덕션 번들에서 완전 제거 +// [DEBUG] 개발용 도구 — DEV에서만 동적 로드, 프로덕션 번들에서 완전 제거 import { lazy, Suspense } from 'react'; -const DevCoordDebug = import.meta.env.DEV - ? lazy(() => import('./debug/DevCoordDebug')) +const DebugTools = import.meta.env.DEV + ? lazy(() => import('./debug')) : null; export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintFeed, currentTime, koreaFilters, transshipSuspects, cableWatchSuspects, dokdoWatchSuspects, dokdoAlerts, vesselAnalysis, groupPolygons, hiddenShipCategories, hiddenNationalities, externalFlyTo, onExternalFlyToDone, opsRoute }: Props) { @@ -615,8 +615,8 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF > - {/* [DEBUG] Ctrl+Click 좌표 표시 — 프로덕션 번들에서 완전 제거 */} - {DevCoordDebug && } + {/* [DEBUG] 개발용 도구 — 프로덕션 번들에서 완전 제거 */} + {DebugTools && } ; +} + +export default function DebugTools({ mapRef }: Props) { + return ( + <> + + {/* 디버그 도구 추가 시 여기에 한 줄 추가 */} + {/* */} + {/* */} + + ); +} -- 2.45.2