Files
SPipe/src/MapContextMenu.tsx
T
bjkim1 21ebb16fb7 SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음)
- README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리
- 패치내역을 docs/로 이동, _worklog.md 추가
- 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
2026-10-30 11:00:46 +09:00

112 lines
4.0 KiB
TypeScript

import React from 'react';
import { ContextMenu } from './ContextMenu';
import { type MapContextMenuEvent, type EditMode, type SimulationResult, type SelectionTarget } from './types';
import { type NetworkModel, type InpNode } from './inpModel';
import { type CRS } from './crs';
import { type MapFocusRequest } from './Map';
export interface MapContextMenuProps {
contextMenu: MapContextMenuEvent;
setContextMenu: React.Dispatch<React.SetStateAction<MapContextMenuEvent | null>>;
appMode: 'explore' | 'edit' | 'result';
setAppMode: React.Dispatch<React.SetStateAction<'explore' | 'edit' | 'result'>>;
setEditMode: React.Dispatch<React.SetStateAction<EditMode>>;
networkModel: NetworkModel | null;
wasmResult: SimulationResult | null;
crs: CRS;
setSelectedTarget: React.Dispatch<React.SetStateAction<SelectionTarget>>;
setActiveTab: React.Dispatch<React.SetStateAction<import('./types').RightTab | null>>;
requestMapFocus: (request: Omit<MapFocusRequest, 'id'>) => void;
showToast: (msg: string) => void;
handleAddNodeAtCoords: (x: number, y: number, nodeType?: InpNode['type']) => void;
handleCopyText: (text: string, label: string) => void;
handleZoomToLayer: (layerId: string) => void;
handleDeleteSelected: () => void;
handleRecalculateLength: (pipeId: string) => void;
handleReversePipe: (pipeId: string) => void;
handleSplitPipe: (pipeId: string) => void;
}
/** Right-click menu on the map (node/link/empty-space actions). */
export const MapContextMenu: React.FC<MapContextMenuProps> = ({
contextMenu,
setContextMenu,
appMode,
setAppMode,
setEditMode,
networkModel,
wasmResult,
crs,
setSelectedTarget,
setActiveTab,
requestMapFocus,
showToast,
handleAddNodeAtCoords,
handleCopyText,
handleZoomToLayer,
handleDeleteSelected,
handleRecalculateLength,
handleReversePipe,
handleSplitPipe,
}) => (
<ContextMenu
crs={crs}
event={contextMenu}
onClose={() => setContextMenu(null)}
onCopyText={handleCopyText}
onAddNodeAtCoords={(x, y) => {
handleAddNodeAtCoords(x, y, 'JUNCTION');
setContextMenu(null);
}}
onFitBounds={() => {
showToast('전체 화면을 표시합니다.');
}}
onPanTo={(lngLat) => {
requestMapFocus({ lngLat, zoom: 17 });
showToast(`좌표 (${lngLat[0].toFixed(4)}, ${lngLat[1].toFixed(4)})로 이동했습니다.`);
}}
onInspectTarget={(target) => {
if (target.type === 'node' || target.type === 'link') {
setSelectedTarget(target);
}
setActiveTab('inspector');
}}
onViewResults={(target) => {
setSelectedTarget(target);
setActiveTab('results');
}}
onStartPipeFromNode={(nodeId) => {
if (appMode !== 'edit') setAppMode('edit');
setEditMode('add-pipe');
setSelectedTarget({ type: 'node', id: nodeId });
}}
onSplitPipe={handleSplitPipe}
onSelectConnectedLinks={(nodeId) => {
if (!networkModel) return;
const connectedPipes = Object.values(networkModel.pipes).filter(
(p) => p.node1 === nodeId || p.node2 === nodeId
);
if (connectedPipes.length > 0) {
setSelectedTarget({ type: 'link', id: connectedPipes[0].id });
setActiveTab('inspector');
showToast(`연결 관로 [${connectedPipes[0].id}] 선택됨 (총 ${connectedPipes.length}개 관로)`);
}
}}
onSelectConnectedNodes={(pipeId) => {
if (!networkModel) return;
const pipe = networkModel.pipes[pipeId];
if (pipe) {
setSelectedTarget({ type: 'node', id: pipe.node1 });
setActiveTab('inspector');
showToast(`시작 절점 [${pipe.node1}] 선택됨 (끝 절점: ${pipe.node2})`);
}
}}
onCalculatePipeLength={handleRecalculateLength}
onReversePipe={handleReversePipe}
onDeleteNode={handleDeleteSelected}
onDeletePipe={handleDeleteSelected}
onZoomToLayer={handleZoomToLayer}
canRunResults={!!wasmResult}
/>
);