From 8362bc5b6c106757ec0ae235e253c54c15a08c83 Mon Sep 17 00:00:00 2001 From: htlee Date: Sat, 4 Apr 2026 00:48:48 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=96=B4=EA=B5=AC=20=EB=AA=A8=EC=84=A0?= =?UTF-8?q?=20=EC=B6=94=EB=A1=A0=20UI=20=ED=86=B5=ED=95=A9=20=E2=80=94=20F?= =?UTF-8?q?leetClusterLayer=20+=20=EB=A6=AC=ED=94=8C=EB=A0=88=EC=9D=B4=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=9D=B4=EC=8B=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParentReviewPanel 마운트 + 관련 상태 관리를 FleetClusterLayer에 통합. 리플레이 컨트롤러, 어구 그룹 섹션, 일치율 패널 등 11개 컴포넌트 codex Lab 환경에서 검증된 버전으로 교체. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/korea/CorrelationPanel.tsx | 108 +- .../components/korea/FleetClusterLayer.tsx | 1046 +++++++++++++++-- .../components/korea/FleetGearListPanel.tsx | 28 +- .../src/components/korea/GearGroupSection.tsx | 82 +- .../korea/HistoryReplayController.tsx | 115 +- frontend/src/components/korea/KoreaMap.tsx | 29 + .../src/components/korea/fleetClusterTypes.ts | 3 + .../korea/useFleetClusterGeoJson.ts | 55 +- .../src/hooks/useFleetClusterDeckLayers.ts | 65 +- frontend/src/hooks/useGearReplayLayers.ts | 448 ++++--- frontend/src/stores/gearReplayStore.ts | 88 +- 11 files changed, 1729 insertions(+), 338 deletions(-) diff --git a/frontend/src/components/korea/CorrelationPanel.tsx b/frontend/src/components/korea/CorrelationPanel.tsx index 413ac59..62b81ab 100644 --- a/frontend/src/components/korea/CorrelationPanel.tsx +++ b/frontend/src/components/korea/CorrelationPanel.tsx @@ -6,6 +6,8 @@ import type { UseGroupPolygonsResult } from '../../hooks/useGroupPolygons'; import { FONT_MONO } from '../../styles/fonts'; import { MODEL_ORDER, MODEL_COLORS, MODEL_DESC } from './fleetClusterConstants'; import { useGearReplayStore } from '../../stores/gearReplayStore'; +import { useTranslation } from 'react-i18next'; +import { useReplayCenterPanelLayout } from './useReplayCenterPanelLayout'; interface CorrelationPanelProps { selectedGearGroup: string; @@ -17,6 +19,8 @@ interface CorrelationPanelProps { enabledVessels: Set; correlationLoading: boolean; hoveredTarget: { mmsi: string; model: string } | null; + hasRightReviewPanel?: boolean; + reviewDriven?: boolean; onEnabledModelsChange: (updater: (prev: Set) => Set) => void; onEnabledVesselsChange: (updater: (prev: Set) => Set) => void; onHoveredTargetChange: (target: { mmsi: string; model: string } | null) => void; @@ -35,11 +39,19 @@ const CorrelationPanel = ({ enabledVessels, correlationLoading, hoveredTarget, + hasRightReviewPanel = false, + reviewDriven = false, onEnabledModelsChange, onEnabledVesselsChange, onHoveredTargetChange, }: CorrelationPanelProps) => { + const { t } = useTranslation(); const historyActive = useGearReplayStore(s => s.historyFrames.length > 0); + const layout = useReplayCenterPanelLayout({ + minWidth: 252, + maxWidth: 966, + hasRightReviewPanel, + }); // Local tooltip state const [hoveredModelTip, setHoveredModelTip] = useState(null); @@ -193,16 +205,30 @@ const CorrelationPanel = ({ key={`${modelName}-${c.targetMmsi}`} style={{ fontSize: 9, marginBottom: 1, display: 'flex', alignItems: 'center', gap: 3, - padding: '1px 2px', borderRadius: 2, cursor: 'pointer', + padding: '1px 2px', borderRadius: 2, cursor: reviewDriven ? 'default' : 'pointer', background: isHovered ? `${color}22` : 'transparent', - opacity: isEnabled ? 1 : 0.5, + opacity: reviewDriven ? 1 : isEnabled ? 1 : 0.5, }} - onClick={() => toggleVessel(c.targetMmsi)} - onMouseEnter={() => onHoveredTargetChange({ mmsi: c.targetMmsi, model: modelName })} - onMouseLeave={() => onHoveredTargetChange(null)} + onClick={reviewDriven ? undefined : () => toggleVessel(c.targetMmsi)} + onMouseEnter={reviewDriven ? undefined : () => onHoveredTargetChange({ mmsi: c.targetMmsi, model: modelName })} + onMouseLeave={reviewDriven ? undefined : () => onHoveredTargetChange(null)} > - + {reviewDriven ? ( + + ) : ( + + )} {isVessel ? '⛴' : '◆'} @@ -219,6 +245,15 @@ const CorrelationPanel = ({ ); }; + const visibleModelNames = useMemo(() => { + if (reviewDriven) { + return availableModels + .filter(model => (correlationByModel.get(model.name) ?? []).length > 0) + .map(model => model.name); + } + return availableModels.filter(model => enabledModels.has(model.name)).map(model => model.name); + }, [availableModels, correlationByModel, enabledModels, reviewDriven]); + // Member row renderer (identity model — no score, independent hover) const renderMemberRow = (m: { mmsi: string; name: string }, icon: string, iconColor: string, keyPrefix = 'id') => { const isHovered = hoveredTarget?.mmsi === m.mmsi && hoveredTarget?.model === 'identity'; @@ -251,10 +286,8 @@ const CorrelationPanel = ({
{selectedGearGroup} {memberCount}개
+
+ {reviewDriven + ? t('parentInference.reference.reviewDriven') + : t('parentInference.reference.shipOnly')} +
폴리곤 오버레이