feat(debug): Ctrl+Click 좌표 디버그 도구 + DEV 가드 체계 구축
- CoordDebugTool: Ctrl+Click 다중 좌표 표시 (DD/DMS, WGS84) - import.meta.env.DEV 가드로 프로덕션 빌드에서 코드 제거 - CLAUDE.md: 디버그 도구 가이드 섹션 추가
This commit is contained in:
부모
8e17febd1b
커밋
8f342f70b7
29
CLAUDE.md
29
CLAUDE.md
@ -202,6 +202,35 @@ deploy/ # systemd + nginx 배포 설정
|
|||||||
| **DB** | kcgdb | 211.208.115.83:5432/kcgdb (유저: kcg_app, pw: Kcg2026monitor) |
|
| **DB** | kcgdb | 211.208.115.83:5432/kcgdb (유저: kcg_app, pw: Kcg2026monitor) |
|
||||||
| **DB** | snpdb | 211.208.115.83:5432/snpdb (유저: snp, pw: snp#8932, 읽기 전용) |
|
| **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 && <DebugTool ... />}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 기존 디버그 도구
|
||||||
|
| 도구 | 위치 | 기능 |
|
||||||
|
|------|------|------|
|
||||||
|
| CoordDebugTool | `korea/debug/CoordDebugTool.tsx` | Ctrl+Click 좌표 표시 (DD/DMS, 다중 포인트) |
|
||||||
|
|
||||||
|
### 디버그 도구 분류 기준
|
||||||
|
다음에 해당하면 디버그 도구로 분류하고, 불확실하면 사용자에게 확인:
|
||||||
|
- 개발/검증 목적의 좌표/데이터 표시 도구
|
||||||
|
- 프로덕션 사용자에게 불필요한 진단 정보
|
||||||
|
- 임시 데이터 시각화, 성능 프로파일링
|
||||||
|
- 특정 조건에서만 활성화되는 테스트 기능
|
||||||
|
|
||||||
## 팀 규칙
|
## 팀 규칙
|
||||||
|
|
||||||
- 코드 스타일: `.claude/rules/code-style.md`
|
- 코드 스타일: `.claude/rules/code-style.md`
|
||||||
|
|||||||
@ -201,6 +201,10 @@ const FILTER_I18N_KEY: Record<string, string> = {
|
|||||||
cnFishing: 'filters.cnFishingMonitor',
|
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) {
|
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 { t } = useTranslation();
|
||||||
const mapRef = useRef<MapRef>(null);
|
const mapRef = useRef<MapRef>(null);
|
||||||
@ -212,6 +216,8 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
|
|||||||
const [selectedFleetData, setSelectedFleetData] = useState<SelectedFleetData | null>(null);
|
const [selectedFleetData, setSelectedFleetData] = useState<SelectedFleetData | null>(null);
|
||||||
const { fontScale } = useFontScale();
|
const { fontScale } = useFontScale();
|
||||||
const [zoomLevel, setZoomLevel] = useState(KOREA_MAP_ZOOM);
|
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 zoomRef = useRef(KOREA_MAP_ZOOM);
|
||||||
const handleZoom = useCallback((e: { viewState: { zoom: number } }) => {
|
const handleZoom = useCallback((e: { viewState: { zoom: number } }) => {
|
||||||
const z = Math.floor(e.viewState.zoom);
|
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%' }}
|
style={{ width: '100%', height: '100%' }}
|
||||||
mapStyle={MAP_STYLE}
|
mapStyle={MAP_STYLE}
|
||||||
onZoom={handleZoom}
|
onZoom={handleZoom}
|
||||||
|
onClick={coordDebug ? coordDebug.handleMapClick : undefined}
|
||||||
>
|
>
|
||||||
<NavigationControl position="top-right" />
|
<NavigationControl position="top-right" />
|
||||||
|
|
||||||
|
{/* [DEBUG] Ctrl+Click 좌표 표시 — 프로덕션에서 제거 */}
|
||||||
|
{coordDebug && <CoordDebugOverlay points={coordDebug.points} onRemove={coordDebug.removePoint} />}
|
||||||
|
|
||||||
<Source id="country-labels" type="geojson" data={countryLabelsGeoJSON()}>
|
<Source id="country-labels" type="geojson" data={countryLabelsGeoJSON()}>
|
||||||
<Layer
|
<Layer
|
||||||
id="country-label-lg"
|
id="country-label-lg"
|
||||||
|
|||||||
56
frontend/src/components/korea/debug/CoordDebugTool.tsx
Normal file
56
frontend/src/components/korea/debug/CoordDebugTool.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* [DEBUG TOOL] Ctrl+Click 좌표 표시 오버레이 컴포넌트
|
||||||
|
* - 프로덕션 빌드에서 자동 제거됨 (import.meta.env.DEV 가드)
|
||||||
|
*/
|
||||||
|
import { Marker, Popup } from 'react-map-gl/maplibre';
|
||||||
|
import type { CoordPoint } from './useCoordDebug';
|
||||||
|
|
||||||
|
function toDMS(dd: number, axis: 'lat' | 'lng'): string {
|
||||||
|
const dir = axis === 'lat' ? (dd >= 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 => (
|
||||||
|
<div key={cp.id}>
|
||||||
|
<Marker longitude={cp.lng} latitude={cp.lat}>
|
||||||
|
<div style={{
|
||||||
|
width: 12, height: 12, borderRadius: '50%',
|
||||||
|
background: '#f43f5e', border: '2px solid #fff',
|
||||||
|
boxShadow: '0 0 6px rgba(244,63,94,0.8)',
|
||||||
|
}} />
|
||||||
|
</Marker>
|
||||||
|
<Popup
|
||||||
|
longitude={cp.lng}
|
||||||
|
latitude={cp.lat}
|
||||||
|
onClose={() => onRemove(cp.id)}
|
||||||
|
closeButton={true}
|
||||||
|
closeOnClick={false}
|
||||||
|
anchor="bottom"
|
||||||
|
offset={[0, -10]}
|
||||||
|
style={{ zIndex: 50 }}
|
||||||
|
>
|
||||||
|
<div style={{ fontFamily: 'monospace', fontSize: 11, lineHeight: 1.8, padding: '2px 4px', color: '#fff' }}>
|
||||||
|
<div style={{ fontWeight: 700, marginBottom: 4, borderBottom: '1px solid rgba(255,255,255,0.3)', paddingBottom: 2, color: '#93c5fd' }}>
|
||||||
|
WGS84 (EPSG:4326)
|
||||||
|
</div>
|
||||||
|
<div><b>DD</b></div>
|
||||||
|
<div style={{ paddingLeft: 8 }}>{cp.lat.toFixed(6)}°N</div>
|
||||||
|
<div style={{ paddingLeft: 8 }}>{cp.lng.toFixed(6)}°E</div>
|
||||||
|
<div style={{ marginTop: 2 }}><b>DMS</b></div>
|
||||||
|
<div style={{ paddingLeft: 8 }}>{toDMS(cp.lat, 'lat')}</div>
|
||||||
|
<div style={{ paddingLeft: 8 }}>{toDMS(cp.lng, 'lng')}</div>
|
||||||
|
</div>
|
||||||
|
</Popup>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
frontend/src/components/korea/debug/useCoordDebug.ts
Normal file
30
frontend/src/components/korea/debug/useCoordDebug.ts
Normal file
@ -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<CoordPoint[]>([]);
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
불러오는 중...
Reference in New Issue
Block a user