SPipe 데모 v2 (2026-10-30) 서버 배포본
- SPipe_server_2026-10-30.zip 내용을 풀어서 등록 (압축 파일은 올리지 않음) - README에 데모 v2 서버 업데이트 순서 추가, 저장소에 없는 문서 목차 정리 - 패치내역을 docs/로 이동, _worklog.md 추가 - 임시 파일(backend/temp.*)과 API 키가 든 파일은 제외
This commit is contained in:
commit
21ebb16fb7
181 files changed
+57207
No files matched your search
@@ -0,0 +1,207 @@
|
||||
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>
|
||||
);
|
||||
Reference in new issue
Block a user