Merge pull request 'feat(debug): DEV 전용 디버그 도구 체계 + 좌표 표시 도구' (#205) from feature/debug-coord-tool into develop

This commit is contained in:
htlee 2026-03-26 13:34:55 +09:00
커밋 32dd957f4b
4개의 변경된 파일152개의 추가작업 그리고 0개의 파일을 삭제

파일 보기

@ -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 && <DebugTool ... />}
```
### 기존 디버그 도구
| 도구 | 위치 | 기능 |
|------|------|------|
| CoordDebugTool | `korea/debug/CoordDebugTool.tsx` | Ctrl+Click 좌표 표시 (DD/DMS, 다중 포인트) |
### 디버그 도구 분류 기준
다음에 해당하면 디버그 도구로 분류하고, 불확실하면 사용자에게 확인:
- 개발/검증 목적의 좌표/데이터 표시 도구
- 프로덕션 사용자에게 불필요한 진단 정보
- 임시 데이터 시각화, 성능 프로파일링
- 특정 조건에서만 활성화되는 테스트 기능
## 팀 규칙
- 코드 스타일: `.claude/rules/code-style.md`

파일 보기

@ -201,6 +201,12 @@ const FILTER_I18N_KEY: Record<string, string> = {
cnFishing: 'filters.cnFishingMonitor',
};
// [DEBUG] 개발용 도구 — DEV에서만 동적 로드, 프로덕션 번들에서 완전 제거
import { lazy, Suspense } from 'react';
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) {
const { t } = useTranslation();
const mapRef = useRef<MapRef>(null);
@ -609,6 +615,9 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF
>
<NavigationControl position="top-right" />
{/* [DEBUG] 개발용 도구 — 프로덕션 번들에서 완전 제거 */}
{DebugTools && <Suspense><DebugTools mapRef={mapRef} /></Suspense>}
<Source id="country-labels" type="geojson" data={countryLabelsGeoJSON()}>
<Layer
id="country-label-lg"

파일 보기

@ -0,0 +1,90 @@
/**
* [DEBUG]
* KoreaMap에서 import하면 .
* import, , .
*/
import { useState, useEffect } from 'react';
import { Marker, Popup } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
interface CoordPoint {
lat: number;
lng: number;
id: number;
}
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}`;
}
interface Props {
mapRef: React.RefObject<MapRef | null>;
}
/**
* .
* mapRef를 click /.
* KoreaMap의 .
*/
export default function DevCoordDebug({ mapRef }: Props) {
const [points, setPoints] = useState<CoordPoint[]>([]);
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 => (
<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={() => setPoints(prev => prev.filter(p => p.id !== 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>
))}
</>
);
}

파일 보기

@ -0,0 +1,24 @@
/**
* [DEBUG] export
*
* / .
* KoreaMap에서는 lazy import.
* .
*/
import type { MapRef } from 'react-map-gl/maplibre';
import DevCoordDebug from './DevCoordDebug';
interface Props {
mapRef: React.RefObject<MapRef | null>;
}
export default function DebugTools({ mapRef }: Props) {
return (
<>
<DevCoordDebug mapRef={mapRef} />
{/* 디버그 도구 추가 시 여기에 한 줄 추가 */}
{/* <DevZoneOverlay mapRef={mapRef} /> */}
{/* <DevPerformance mapRef={mapRef} /> */}
</>
);
}