Files
SPipe/src/ToolRail.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

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>
);