- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
import React from 'react';
|
|
import { Activity, MousePointer, CircleDot, GitCommit, Magnet, ArrowRight, Trash2, Table, Layers, Scan, Sliders } from 'lucide-react';
|
|
import { type EditMode, type SimulationResult } from './types';
|
|
import { type InpNode } from './inpModel';
|
|
import { type BottomPanelTab } from './BottomPanel';
|
|
|
|
export interface ToolRailProps {
|
|
appMode: 'explore' | 'edit' | 'result';
|
|
setAppMode: React.Dispatch<React.SetStateAction<'explore' | 'edit' | 'result'>>;
|
|
editMode: EditMode;
|
|
setEditMode: React.Dispatch<React.SetStateAction<EditMode>>;
|
|
setNodeCreationType: React.Dispatch<React.SetStateAction<InpNode['type']>>;
|
|
isNodeTypeMenuOpen: boolean;
|
|
setIsNodeTypeMenuOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
boxSelectionEnabled: boolean;
|
|
setBoxSelectionEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
|
snapEnabled: boolean;
|
|
setSnapEnabled: React.Dispatch<React.SetStateAction<boolean>>;
|
|
showFlowArrows: boolean;
|
|
setShowFlowArrows: React.Dispatch<React.SetStateAction<boolean>>;
|
|
isLayerPanelOpen: boolean;
|
|
setIsLayerPanelOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
layerPanelWidth: number;
|
|
isStyleSettingsOpen: boolean;
|
|
setIsStyleSettingsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
bottomPanelTab: BottomPanelTab | null;
|
|
toggleBottomPanel: (tab: BottomPanelTab) => void;
|
|
selectionCounts: { nodes: number; links: number; gisFeatures: number; total: number };
|
|
handleDeleteSelected: () => void;
|
|
wasmResult: SimulationResult | null;
|
|
}
|
|
|
|
/** Left 44px tool rail: selection/edit tools, snapping, layer/table/style toggles (DESIGN.md § 3.1). */
|
|
export const ToolRail: React.FC<ToolRailProps> = ({
|
|
appMode,
|
|
setAppMode,
|
|
editMode,
|
|
setEditMode,
|
|
setNodeCreationType,
|
|
isNodeTypeMenuOpen,
|
|
setIsNodeTypeMenuOpen,
|
|
boxSelectionEnabled,
|
|
setBoxSelectionEnabled,
|
|
snapEnabled,
|
|
setSnapEnabled,
|
|
showFlowArrows,
|
|
setShowFlowArrows,
|
|
isLayerPanelOpen,
|
|
setIsLayerPanelOpen,
|
|
layerPanelWidth,
|
|
isStyleSettingsOpen,
|
|
setIsStyleSettingsOpen,
|
|
bottomPanelTab,
|
|
toggleBottomPanel,
|
|
selectionCounts,
|
|
handleDeleteSelected,
|
|
wasmResult,
|
|
}) => (
|
|
<aside className="tool-rail" title="도구 레일 (단축키: 1 탐색 / 2 편집 / 3 결과)">
|
|
<button
|
|
onClick={() => {
|
|
setIsLayerPanelOpen((v) => {
|
|
const next = !v;
|
|
localStorage.setItem('gispipe_layer_panel_open', String(next));
|
|
return next;
|
|
});
|
|
}}
|
|
className={`tool-btn ${isLayerPanelOpen ? 'active' : ''}`}
|
|
title="레이어 패널 토글 (단축키 [)"
|
|
>
|
|
<Layers size={16} />
|
|
</button>
|
|
|
|
<div className="tool-divider" />
|
|
|
|
<button
|
|
onClick={() => {
|
|
setEditMode('select');
|
|
setIsNodeTypeMenuOpen(false);
|
|
setBoxSelectionEnabled(false);
|
|
}}
|
|
className={`tool-btn ${editMode === 'select' && !boxSelectionEnabled ? 'active' : ''}`}
|
|
title="선택 / 이동 (단축키 V)"
|
|
>
|
|
<MousePointer size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setEditMode('select');
|
|
setIsNodeTypeMenuOpen(false);
|
|
setBoxSelectionEnabled((enabled) => !enabled);
|
|
}}
|
|
className={`tool-btn ${boxSelectionEnabled ? 'active' : ''}`}
|
|
title="박스 선택: 좌→우 완전 포함, 우→좌 교차 선택"
|
|
>
|
|
<Scan size={16} />
|
|
</button>
|
|
<div className="tool-menu-anchor">
|
|
<button
|
|
onClick={() => setIsNodeTypeMenuOpen((open) => !open)}
|
|
className={`tool-btn ${appMode === 'edit' && editMode === 'add-node' ? 'active' : ''}`}
|
|
title="지도에 시설 추가"
|
|
aria-haspopup="menu"
|
|
aria-expanded={isNodeTypeMenuOpen}
|
|
>
|
|
<CircleDot size={16} />
|
|
</button>
|
|
{isNodeTypeMenuOpen && (
|
|
<div
|
|
className="tool-flyout"
|
|
role="menu"
|
|
aria-label="추가할 시설 선택"
|
|
style={{ left: isLayerPanelOpen ? 40 + layerPanelWidth : 40 }}
|
|
>
|
|
<div className="tool-flyout-title">추가할 시설</div>
|
|
{([
|
|
['JUNCTION', '절점', 'Junction'],
|
|
['RESERVOIR', '저수지', 'Reservoir'],
|
|
['TANK', '탱크', 'Tank'],
|
|
] as const).map(([type, label, sublabel]) => (
|
|
<button
|
|
key={type}
|
|
type="button"
|
|
role="menuitem"
|
|
className="tool-flyout-item"
|
|
onClick={() => {
|
|
setNodeCreationType(type);
|
|
setIsNodeTypeMenuOpen(false);
|
|
if (appMode !== 'edit') setAppMode('edit');
|
|
setEditMode('add-node');
|
|
setBoxSelectionEnabled(false);
|
|
}}
|
|
>
|
|
<CircleDot size={14} aria-hidden="true" />
|
|
<span>{label}</span>
|
|
<small>{sublabel}</small>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
if (appMode !== 'edit') setAppMode('edit');
|
|
setEditMode('add-pipe');
|
|
setIsNodeTypeMenuOpen(false);
|
|
setBoxSelectionEnabled(false);
|
|
}}
|
|
className={`tool-btn ${appMode === 'edit' && editMode === 'add-pipe' ? 'active' : ''}`}
|
|
title="관로 연결 (단축키 P)"
|
|
>
|
|
<GitCommit size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setSnapEnabled((v) => !v)}
|
|
className={`tool-btn ${snapEnabled ? 'active' : ''}`}
|
|
title={`절점 자동 스냅 (${snapEnabled ? '켜짐' : '꺼짐'})`}
|
|
>
|
|
<Magnet size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setShowFlowArrows((v) => !v)}
|
|
className={`tool-btn ${showFlowArrows ? 'active' : ''}`}
|
|
title={`유향 화살표 (${showFlowArrows ? '켜짐' : '꺼짐'})`}
|
|
>
|
|
<ArrowRight size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => setIsStyleSettingsOpen((v) => !v)}
|
|
className={`tool-btn ${isStyleSettingsOpen ? 'active' : ''}`}
|
|
title="지도 스타일 설정 (절점/관로 크기·투명도, 배경지도 투명도)"
|
|
>
|
|
<Sliders size={16} />
|
|
</button>
|
|
<button
|
|
onClick={() => toggleBottomPanel('table')}
|
|
className={`tool-btn ${bottomPanelTab === 'table' ? 'active' : ''}`}
|
|
title="요소 테이블 열기/닫기 (Ctrl+T)"
|
|
>
|
|
<Table size={16} />
|
|
</button>
|
|
{wasmResult?.timeSteps && wasmResult.timeSteps.length > 1 && (
|
|
<button
|
|
onClick={() => toggleBottomPanel('eps')}
|
|
className={`tool-btn ${bottomPanelTab === 'eps' ? 'active' : ''}`}
|
|
title="EPS 시간 컨트롤러 열기/닫기"
|
|
>
|
|
<Activity size={16} />
|
|
</button>
|
|
)}
|
|
|
|
|
|
<div className="tool-divider" />
|
|
|
|
|
|
{selectionCounts.total > 0 && (
|
|
<button
|
|
onClick={handleDeleteSelected}
|
|
className="tool-btn"
|
|
style={{ color: 'var(--crit)' }}
|
|
title={`선택 요소 ${selectionCounts.total}개 삭제 (Delete 키)`}
|
|
>
|
|
<Trash2 size={16} />
|
|
</button>
|
|
)}
|
|
</aside>
|
|
);
|