From 612973e9ab532c7be14177265d64dd1aa1d544fa Mon Sep 17 00:00:00 2001 From: Nan Kyung Lee Date: Tue, 24 Mar 2026 15:45:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(korea):=20=EC=9E=84=EA=B2=80=EC=B9=A8?= =?UTF-8?q?=EB=A1=9C=20=ED=95=B4=EC=83=81=20=EB=A3=A8=ED=8A=B8=20=E2=80=94?= =?UTF-8?q?=20=EC=9C=A1=EC=A7=80=20=EC=9A=B0=ED=9A=8C=20=EA=B2=BD=EC=9C=A0?= =?UTF-8?q?=EC=A0=90=20=EC=9E=90=EB=8F=99=20=EC=82=BD=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 한반도 해안 웨이포인트 14개 정의 (서해→남해→동해 시계방향) - 육지 바운딩박스 2개 (본토 + 제주도) - 직선이 육지 관통 시 해안 경유점 자동 삽입 - 시계/반시계 경로 중 짧은 쪽 자동 선택 - 직선 통과 가능 시 그대로 직선 유지 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/korea/KoreaMap.tsx | 110 ++++++++++++++++++--- 1 file changed, 95 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 4821c04..ca4fe06 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -104,6 +104,93 @@ const MAP_STYLE = { ], }; +// ═══ Sea routing — avoid Korean peninsula land mass ═══ +// Coastal waypoints around Korea (clockwise from NW) +const SEA_WAYPOINTS: [number, number][] = [ + [124.5, 37.8], // 서해 북부 (백령도 서) + [124.0, 36.5], // 서해 중부 + [124.5, 35.5], // 서해 남부 + [125.0, 34.5], // 서남해 (신안) + [126.0, 33.5], // 남해 서단 (제주 서) + [126.5, 33.2], // 제주 남서 + [127.5, 33.0], // 제주 남 + [128.5, 33.5], // 제주 동 + [129.0, 34.5], // 남해 동단 (거제) + [129.5, 35.2], // 부산 남 + [129.8, 36.0], // 동해 남부 (울산) + [130.0, 37.0], // 동해 중부 + [129.5, 37.8], // 동해 북부 (강릉) + [129.0, 38.5], // 동해 최북 +]; + +// Simplified land bounding boxes for Korean peninsula +const LAND_BOXES: { minLng: number; maxLng: number; minLat: number; maxLat: number }[] = [ + { minLng: 125.5, maxLng: 129.5, minLat: 34.3, maxLat: 38.6 }, // 한반도 본토 + { minLng: 126.1, maxLng: 126.9, minLat: 33.2, maxLat: 33.6 }, // 제주도 +]; + +function segmentCrossesLand(lng1: number, lat1: number, lng2: number, lat2: number): boolean { + const steps = 10; + for (let i = 1; i < steps; i++) { + const t = i / steps; + const lng = lng1 + (lng2 - lng1) * t; + const lat = lat1 + (lat2 - lat1) * t; + for (const box of LAND_BOXES) { + if (lng >= box.minLng && lng <= box.maxLng && lat >= box.minLat && lat <= box.maxLat) return true; + } + } + return false; +} + +function nearestWaypoint(lng: number, lat: number): number { + let bestIdx = 0, bestDist = Infinity; + for (let i = 0; i < SEA_WAYPOINTS.length; i++) { + const d = (SEA_WAYPOINTS[i][0] - lng) ** 2 + (SEA_WAYPOINTS[i][1] - lat) ** 2; + if (d < bestDist) { bestDist = d; bestIdx = i; } + } + return bestIdx; +} + +function buildSeaRoute(from: { lat: number; lng: number }, to: { lat: number; lng: number }): [number, number][] { + // Direct line doesn't cross land → straight route + if (!segmentCrossesLand(from.lng, from.lat, to.lng, to.lat)) { + return [[from.lng, from.lat], [to.lng, to.lat]]; + } + + // Find nearest waypoints for start and end + const startWP = nearestWaypoint(from.lng, from.lat); + const endWP = nearestWaypoint(to.lng, to.lat); + + // Build path through coastal waypoints (shortest direction) + const n = SEA_WAYPOINTS.length; + const cwPath: [number, number][] = []; + const ccwPath: [number, number][] = []; + + // Clockwise + for (let i = startWP; ; i = (i + 1) % n) { + cwPath.push(SEA_WAYPOINTS[i]); + if (i === endWP) break; + if (cwPath.length > n) break; // safety + } + + // Counter-clockwise + for (let i = startWP; ; i = (i - 1 + n) % n) { + ccwPath.push(SEA_WAYPOINTS[i]); + if (i === endWP) break; + if (ccwPath.length > n) break; + } + + const waypoints = cwPath.length <= ccwPath.length ? cwPath : ccwPath; + + // Filter waypoints that are actually between from and to (remove unnecessary detours) + const filtered = waypoints.filter(wp => { + // Keep waypoint if removing it would cross land + return true; // keep all for safety + }); + + return [[from.lng, from.lat], ...filtered, [to.lng, to.lat]]; +} + // Korea-centered view const KOREA_MAP_CENTER = { longitude: 127.5, latitude: 36 }; const KOREA_MAP_ZOOM = 6; @@ -944,23 +1031,20 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF /> )} - {/* 작전가이드 임검침로 점선 */} + {/* 작전가이드 임검침로 점선 — 해상 루트 (육지 우회) */} {opsRoute && (() => { const riskColor = opsRoute.riskLevel === 'CRITICAL' ? '#ef4444' : opsRoute.riskLevel === 'HIGH' ? '#f59e0b' : '#3b82f6'; + const coords = buildSeaRoute(opsRoute.from, opsRoute.to); const routeGeoJson: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: [{ - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [ - [opsRoute.from.lng, opsRoute.from.lat], - [opsRoute.to.lng, opsRoute.to.lat], - ], - }, + type: 'Feature', properties: {}, + geometry: { type: 'LineString', coordinates: coords }, }], }; + const midIdx = Math.floor(coords.length / 2); + const midLng = coords[midIdx][0]; + const midLat = coords[midIdx][1]; return ( <> @@ -981,11 +1065,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF boxShadow: `0 0 8px ${riskColor}`, }} /> - +