refactor(B05): 배관 추가·삭제 우클릭 메뉴를 종단 그래프로 이동

구조물 조작이 테이블과 그래프로 나뉘어 있었다. 이동은 그래프 측점선, 추가·삭제는
테이블이라 어디서 무엇을 하는지 알기 어려웠다. 메뉴를 그래프 칸으로 옮겨 셋을 한
자리에 모은다. 테이블은 값만 보여 준다.

- mountStructureMenu의 부착 대상을 테이블 → 그래프 칸(.b05-profile__chart)으로 교체
- ProfileTableOptions.structureLines 제거
- CSS 블록명 b05-profile-table__context-menu → b05-profile-chart__context-menu

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:57:40 +09:00
co-authored by Claude Opus 5
parent c141ad4ef8
commit 6d13b3b781
4 changed files with 24 additions and 36 deletions
@@ -22,6 +22,7 @@ import { LONG_PAD } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Sectio
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import { createPanelResizer } from "@ui/ui_template_resizer";
import { createDrainagePanel } from "./B05_wf2_Route_UI_Drainage_Panel";
import { mountStructureMenu } from "./B05_wf2_Route_UI_Profile_Structures";
import {
hasLegacyAlignment,
normalizedLongitudinal,
@@ -457,13 +458,6 @@ export function createRouteProfilePanel(
// 값 열 계획고 직접 입력 → 규칙 측점과 동일한 station_offset 파이프라인.
onAdjustStation: (chainage, delta) =>
base && applyEdits(adjustStation(base, store.edits(), chainage, delta)),
// 구조물 우클릭 메뉴 — 배관 추가 / 구조물 삭제. 이동은 그래프 측점선에서 한다.
structureLines: {
chainageAt: chainageInverter(longitudinal, width, layout.originOffset),
maxChainageM: maxChainageOf(longitudinal),
onRemove: (station) => callbacks?.onStructureRemove?.(station),
onAddPipe: (chainage) => callbacks?.onPipeAdd?.(chainage),
},
})
: null;
@@ -511,6 +505,15 @@ export function createRouteProfilePanel(
},
),
);
// 구조물 우클릭 메뉴 — 측점선 위면 삭제, 빈 자리면 배관 추가. 이동도 여기(측점선 끌기)서 한다.
mountStructureMenu(chartWrap, {
stations: irregularStations,
x,
chainageAt: chainageInverter(longitudinal, width, layout.originOffset),
maxChainageM: maxChainageOf(longitudinal),
onRemove: (station) => callbacks?.onStructureRemove?.(station),
onAddPipe: (chainage) => callbacks?.onPipeAdd?.(chainage),
});
// 가로 스크롤에도 고정되는 sticky Y축 오버레이(SVG와 같은 눈금·불투명 배경으로 값 누출 차단).
if (yAxis) chartWrap.append(buildStickyYAxis(yAxis, chartHeight));
if (alignment) {
@@ -1,9 +1,8 @@
/* =============================================================================
* 종단 테이블 구조물 배치 라인 (B05)
*
* 테이블 위 우클릭 메뉴만 담당한다. 세로선은 **그리지 않는다** — 구조물 위치는 그래프의 측점선이
* 이미 보여 주므로 테이블에까지 얹으면 같은 것을 두 번 그리는 셈이다(2026-08-01 사용자 지시).
* 위치를 옮기는 것도 그래프 측점선에서 한다.
* 종단 **그래프** 위 우클릭 메뉴. 구조물 조작은 전부 그래프에서 한다 — 측점선을 끌어 옮기고,
* 우클릭으로 배관을 넣거나 지운다(2026-08-01 사용자 지시). 테이블은 값만 보여 준다.
*
* 가까운 구조물이 있으면 삭제, 없으면 그 자리에 배관 추가를 띄운다. 배관은 배수유역도의 관 지점이
* 투영된 것이라(`origin: "pipe"`), 지우면 관 지점 정본(`pipe_points.json`)이 바뀌고 세부유역도
@@ -32,23 +31,23 @@ export interface StructureLineOptions {
}
/**
* 테이블 요소에 우클릭 메뉴를 붙인다. 테이블은 매 그리기마다 새로 만들어지므로 이 함수도
* 그래프 칸에 우클릭 메뉴를 붙인다. 그래프는 매 그리기마다 새로 만들어지므로 이 함수도
* 그때마다 다시 부른다 — 상태를 밖에 남기지 않는다.
*/
export function mountStructureMenu(table: HTMLElement, options: StructureLineOptions): void {
const menu = createMapContextMenu("b05-profile-table");
export function mountStructureMenu(host: HTMLElement, options: StructureLineOptions): void {
const menu = createMapContextMenu("b05-profile-chart");
function clamp(chainageM: number): number {
return Math.min(Math.max(chainageM, 0), options.maxChainageM);
}
// 우클릭 — 가까운 구조물이 있으면 삭제, 없으면 그 자리에 배관을 넣는다.
table.addEventListener("contextmenu", (event) => {
host.addEventListener("contextmenu", (event) => {
if (menu.contains(event.target)) {
event.preventDefault();
return;
}
const rect = table.getBoundingClientRect();
const rect = host.getBoundingClientRect();
const localX = event.clientX - rect.left;
const chainage = clamp(options.chainageAt(localX));
// 선을 그리지 않으므로 화면 거리로 가장 가까운 구조물을 찾는다.
@@ -66,9 +65,9 @@ export function mountStructureMenu(table: HTMLElement, options: StructureLineOpt
: ["배관 추가", () => options.onAddPipe(Number(chainage.toFixed(2)))],
]);
});
table.addEventListener("pointerdown", (event) => {
host.addEventListener("pointerdown", (event) => {
if (!menu.contains(event.target)) menu.close();
});
table.append(menu.element);
host.append(menu.element);
}
@@ -19,10 +19,6 @@ import type {
} from "./B05_wf2_Route_UI_Profile_Alignment";
import { stationLabel } from "../B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_Common";
import { irregularStationId, type IrregularStation } from "./B05_wf2_Route_UI_IrregularStations";
import {
mountStructureMenu,
type StructureLineOptions,
} from "./B05_wf2_Route_UI_Profile_Structures";
export interface ProfileTableOptions {
alignment: ProfileAlignment;
@@ -46,8 +42,6 @@ export interface ProfileTableOptions {
onCurveRadiusChange: (curve: AlignmentCurve, radiusM: number | null) => void;
/** 임의 chainage의 계획고를 delta만큼 조정(비정규 측점 값 열 직접 입력용). */
onAdjustStation?: (chainageM: number, deltaM: number) => void;
/** 구조물 우클릭 메뉴(배관 추가·구조물 삭제). 없으면 메뉴를 붙이지 않는다. */
structureLines?: Omit<StructureLineOptions, "stations" | "x">;
}
interface StationRowSpec {
@@ -541,14 +535,6 @@ export function createProfileTable(options: ProfileTableOptions): HTMLElement {
),
);
}
// 구조물 우클릭 메뉴 — 세로선은 그래프가 그리므로 여기서는 메뉴만 붙인다.
if (options.structureLines) {
mountStructureMenu(table, {
...options.structureLines,
stations: options.irregularStations ?? [],
x,
});
}
return table;
}
+5 -5
View File
@@ -685,9 +685,9 @@
/* ─── 선택된 비정규 측점 값 열 오버레이 (규칙 열과 같은 12행) ──────────────
세로 점선·구조물 태그 없이, 선택 시에만 값 열을 테이블 위에 겹쳐 보여준다. */
/* ── 구조물 우클릭 메뉴 (배관 추가 · 구조물 삭제) ────────────────────────── */
/* ── 구조물 우클릭 메뉴 (그래프 위 — 배관 추가 · 구조물 삭제) ─────────────── */
.b05-profile-table__context-menu {
.b05-profile-chart__context-menu {
position: absolute;
z-index: 7;
display: flex;
@@ -700,11 +700,11 @@
box-shadow: 0 4px 16px rgb(0 0 0 / 25%);
}
.b05-profile-table__context-menu[hidden] {
.b05-profile-chart__context-menu[hidden] {
display: none;
}
.b05-profile-table__context-menu-item {
.b05-profile-chart__context-menu-item {
padding: var(--spacing-4) var(--spacing-8);
border: 0;
border-radius: var(--radius-cards);
@@ -715,7 +715,7 @@
cursor: pointer;
}
.b05-profile-table__context-menu-item:hover {
.b05-profile-chart__context-menu-item:hover {
background: var(--color-surface-sunken);
}