From caaedfa5e236f3dbed23a4fa6be07699efb883a7 Mon Sep 17 00:00:00 2001 From: htlee Date: Wed, 25 Mar 2026 09:33:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=96=B4=EA=B5=AC/=EC=96=B4=EB=A7=9D?= =?UTF-8?q?=20=EB=A7=88=EB=A6=84=EB=AA=A8=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC=20+=20=EB=A6=AC=ED=94=8C=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=20=EB=AA=A8=EC=84=A0=20=EC=83=89=EC=83=81=20=EA=B5=AC=EB=B6=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gear-diamond SDF 이미지 등록 (ShipLayer.tsx) - 라이브/가상/히스토리 전 레이어에서 어구 패턴 → 마름모, 회전 없음 - 모선/선단 선박은 삼각형 유지 (isGear 속성 기반 분기) - 어구 아이콘 크기 80% 축소 (baseSize 0.14→0.11, 히스토리 0.7→0.55) - 리플레이 시 모선 아이콘/라벨 노란색(#fbbf24) 구분 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/korea/FleetClusterLayer.tsx | 22 +++++++++------- frontend/src/components/korea/KoreaMap.tsx | 7 +++--- frontend/src/components/layers/ShipLayer.tsx | 25 ++++++++++++++++--- 3 files changed, 39 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/korea/FleetClusterLayer.tsx b/frontend/src/components/korea/FleetClusterLayer.tsx index 7a0a47f..ffe7413 100644 --- a/frontend/src/components/korea/FleetClusterLayer.tsx +++ b/frontend/src/components/korea/FleetClusterLayer.tsx @@ -444,7 +444,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS const lon = realShip?.lng ?? m.lon; features.push({ type: 'Feature', - properties: { mmsi: m.mmsi, name: m.name, groupKey, groupType, role: m.role, isParent: m.isParent ? 1 : 0, color, cog: heading, baseSize: m.isParent ? 0.18 : 0.14 }, + properties: { mmsi: m.mmsi, name: m.name, groupKey, groupType, role: m.role, isParent: m.isParent ? 1 : 0, isGear: (groupType !== 'FLEET' && !m.isParent) ? 1 : 0, color, cog: heading, baseSize: (groupType !== 'FLEET' && !m.isParent) ? 0.11 : m.isParent ? 0.18 : 0.14 }, geometry: { type: 'Point', coordinates: [lon, lat] }, }); }; @@ -542,7 +542,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS type: 'FeatureCollection', features: snap.members.map(m => ({ type: 'Feature' as const, - properties: { mmsi: m.mmsi, name: m.name, cog: m.cog ?? 0, role: m.role, stale: isStale ? 1 : 0 }, + properties: { mmsi: m.mmsi, name: m.name, cog: m.cog ?? 0, role: m.role, isGear: m.role === 'GEAR' ? 1 : 0, stale: isStale ? 1 : 0 }, geometry: { type: 'Point' as const, coordinates: [m.lon, m.lat] }, })), }; @@ -748,7 +748,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS id="group-member-icon" type="symbol" layout={{ - 'icon-image': 'ship-triangle', + 'icon-image': ['case', ['==', ['get', 'isGear'], 1], 'gear-diamond', 'ship-triangle'], 'icon-size': ['interpolate', ['linear'], ['zoom'], 4, ['*', ['get', 'baseSize'], 0.9], 6, ['*', ['get', 'baseSize'], 1.2], @@ -758,7 +758,7 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS 13, ['*', ['get', 'baseSize'], 4.0], 14, ['*', ['get', 'baseSize'], 4.8], ], - 'icon-rotate': ['get', 'cog'], + 'icon-rotate': ['case', ['==', ['get', 'isGear'], 1], 0, ['get', 'cog']], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true, @@ -927,13 +927,17 @@ export function FleetClusterLayer({ ships, analysisMap: analysisMapProp, onShipS {historyData && ( diff --git a/frontend/src/components/korea/KoreaMap.tsx b/frontend/src/components/korea/KoreaMap.tsx index 924f289..a9ade65 100644 --- a/frontend/src/components/korea/KoreaMap.tsx +++ b/frontend/src/components/korea/KoreaMap.tsx @@ -582,9 +582,10 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF if (!dto) continue; const level = dto.algorithms.riskScore.level; const color = level === 'CRITICAL' ? '#ef4444' : level === 'HIGH' ? '#f97316' : level === 'MEDIUM' ? '#eab308' : '#22c55e'; + const isGear = /^.+?_\d+_\d+_?$/.test(s.name || '') ? 1 : 0; features.push({ type: 'Feature', - properties: { mmsi: s.mmsi, name: s.name || s.mmsi, cog: s.heading ?? 0, color, baseSize: 0.16 }, + properties: { mmsi: s.mmsi, name: s.name || s.mmsi, cog: s.heading ?? 0, color, baseSize: 0.16, isGear }, geometry: { type: 'Point', coordinates: [s.lng, s.lat] }, }); } @@ -769,7 +770,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF id="analysis-ship-icon" type="symbol" layout={{ - 'icon-image': 'ship-triangle', + 'icon-image': ['case', ['==', ['get', 'isGear'], 1], 'gear-diamond', 'ship-triangle'], 'icon-size': ['interpolate', ['linear'], ['zoom'], 4, ['*', ['get', 'baseSize'], 1.0], 6, ['*', ['get', 'baseSize'], 1.3], @@ -779,7 +780,7 @@ export function KoreaMap({ ships, allShips, aircraft, satellites, layers, osintF 13, ['*', ['get', 'baseSize'], 4.2], 14, ['*', ['get', 'baseSize'], 5.0], ], - 'icon-rotate': ['get', 'cog'], + 'icon-rotate': ['case', ['==', ['get', 'isGear'], 1], 0, ['get', 'cog']], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true, diff --git a/frontend/src/components/layers/ShipLayer.tsx b/frontend/src/components/layers/ShipLayer.tsx index 49221d8..a02ca98 100644 --- a/frontend/src/components/layers/ShipLayer.tsx +++ b/frontend/src/components/layers/ShipLayer.tsx @@ -270,6 +270,24 @@ function ensureTriangleImage(map: maplibregl.Map) { ctx.fill(); const imgData = ctx.getImageData(0, 0, s, s); map.addImage('ship-triangle', { width: s, height: s, data: new Uint8Array(imgData.data.buffer) }, { sdf: true }); + + // 어구/어망 마름모 아이콘 + if (!map.hasImage('gear-diamond')) { + const dc = document.createElement('canvas'); + dc.width = s; + dc.height = s; + const dx = dc.getContext('2d')!; + dx.beginPath(); + dx.moveTo(s / 2, 4); // top + dx.lineTo(s - 4, s / 2); // right + dx.lineTo(s / 2, s - 4); // bottom + dx.lineTo(4, s / 2); // left + dx.closePath(); + dx.fillStyle = '#ffffff'; + dx.fill(); + const dd = dx.getImageData(0, 0, s, s); + map.addImage('gear-diamond', { width: s, height: s, data: new Uint8Array(dd.data.buffer) }, { sdf: true }); + } } // ── Main layer (WebGL symbol rendering — triangles) ── @@ -311,10 +329,11 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM mmsi: ship.mmsi, name: ship.name, color: getShipHex(ship), - size: SIZE_MAP[ship.category] ?? 0.12, + size: (/^.+?_\d+_\d+_?$/.test(ship.name || '') ? 0.8 : 1) * (SIZE_MAP[ship.category] ?? 0.12), isMil: isMilitary(ship.category) ? 1 : 0, isKorean: ship.flag === 'KR' ? 1 : 0, isCheonghae: ship.mmsi === '440001981' ? 1 : 0, + isGear: /^.+?_\d+_\d+_?$/.test(ship.name || '') ? 1 : 0, heading: ship.heading, mtCategory: getMarineTrafficCategory(ship.typecode, ship.category), natGroup: getNationalityGroup(ship.flag), @@ -507,7 +526,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM type="symbol" filter={shipVisibilityFilter} layout={{ - 'icon-image': 'ship-triangle', + 'icon-image': ['case', ['==', ['get', 'isGear'], 1], 'gear-diamond', 'ship-triangle'], 'icon-size': ['interpolate', ['linear'], ['zoom'], 4, ['*', ['get', 'size'], 0.8], 6, ['*', ['get', 'size'], 1.0], @@ -517,7 +536,7 @@ export function ShipLayer({ ships, militaryOnly, koreanOnly, hoveredMmsi, focusM 13, ['*', ['get', 'size'], 3.5], 14, ['*', ['get', 'size'], 4.2], ], - 'icon-rotate': ['get', 'heading'], + 'icon-rotate': ['case', ['==', ['get', 'isGear'], 1], 0, ['get', 'heading']], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true,