fix(design): 종단 · 유토곡선 · 횡단 그리드 빈 곳 누름도 고름 안 풂 (233-4)
- 빈 곳 누름 해제 듣개 뗌 — 3D 와 같은 한 규칙 - 풀기는 Esc · 같은 것 다시 누름 한 길 · 도구 중 빈 곳 누름은 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
@@ -135,8 +135,6 @@ export interface RouteMassHaulDrawParams {
|
||||
widthPx: number;
|
||||
selectedStationId: string | null;
|
||||
onSelectStation: (stationId: string) => void;
|
||||
/** 측점선이 아닌 빈 곳 클릭 — 선택 해제(2026-08-04 사용자 지시). */
|
||||
onClearSelection?: () => void;
|
||||
/**
|
||||
* 저장된 횡단이 지금 계획선과 어긋나 재계산이 예약된 상태. 그 사이의 면적은 **옛
|
||||
* 계획고로 만든 값**이라 그리면 사용자가 옛 그림을 보게 된다(2026-09-03 사용자 확정:
|
||||
@@ -250,13 +248,6 @@ export function createRouteMassHaulPanel(
|
||||
pendingChip.hidden = true;
|
||||
overlay.append(resizer.root, bar, scroll, legendLayer, pendingChip);
|
||||
|
||||
// 측점선이 아닌 빈 곳 클릭 = 선택 해제 — 종단 그래프와 같은 규칙(2026-08-04 사용자 지시).
|
||||
let clearSelection: (() => void) | undefined;
|
||||
scroll.addEventListener("click", (event) => {
|
||||
if ((event.target as HTMLElement).closest(".b06-chart__station")) return;
|
||||
clearSelection?.();
|
||||
});
|
||||
|
||||
// 종단 그래프 영역과 같은 문법 — 세로 휠을 가로 이동으로 돌린다(2026-08-04 사용자 지시).
|
||||
// scrollLeft 동기화 덕에 위 종단 그래프도 함께 움직인다. Shift+휠은 브라우저 기본 그대로.
|
||||
attachWheelHorizontalScroll(scroll);
|
||||
@@ -368,7 +359,6 @@ export function createRouteMassHaulPanel(
|
||||
|
||||
function draw(params: RouteMassHaulDrawParams): void {
|
||||
lastParams = params;
|
||||
clearSelection = params.onClearSelection;
|
||||
overlay.hidden = !open;
|
||||
syncHandlePosition();
|
||||
// 접혀 있어도 상단줄에는 값이 서야 한다 — 이때는 **횡단 기준만** 적분한다.
|
||||
|
||||
@@ -199,8 +199,8 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
const chartWrap = document.createElement("div");
|
||||
chartWrap.className = "b05-profile__chart";
|
||||
chartWrap.style.height = `${chartHeight}px`;
|
||||
// 측점선이 아닌 빈 곳을 누르면 선택 해제(2026-08-04 사용자 지시).
|
||||
// 측점 마커·편집 버튼 클릭은 각자 처리하므로 여기까지 안 온다(closest·stopPropagation).
|
||||
// 빈 곳 누름은 고름을 안 풂 — 풀기는 Esc · 같은 것 다시 누름 한 길(3D 와 같은 규칙 · 233-4).
|
||||
// 도구([직선화] · [쉬프트] · [자유지정]) 중 빈 곳 누름은 그 도구 몫 그대로.
|
||||
chartWrap.addEventListener("click", (event) => {
|
||||
// [직선화]·[쉬프트] 모드에서는 그래프 클릭이 도구 선택으로 간다 — 측점선을 눌렀으면
|
||||
// 그 측점, 빈 곳이면 그 x의 누가거리로 직선 구간을 고른다(2026-09-02).
|
||||
@@ -219,15 +219,6 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
if (ctx.handleToolPick(Number.isFinite(chainage) ? chainage : null)) return;
|
||||
}
|
||||
}
|
||||
if ((event.target as HTMLElement).closest(".b06-chart__station")) return;
|
||||
if (ctx.selectedStationId() !== null) ctx.selectStation(null);
|
||||
// 구조물 알약 선택도 함께 푼다 — 빈 공간 클릭 시 사이드 폼(구조물군·종류)까지
|
||||
// 리셋되는 해제 신호가 여기서만 나갈 수 있다(2026-08-18 사용자 보고).
|
||||
if (ctx.selectedStructureId() !== null) {
|
||||
ctx.setSelectedStructureId(null);
|
||||
callbacks?.onStructureSelect?.(null);
|
||||
ctx.redraw();
|
||||
}
|
||||
});
|
||||
const designProfiles = alignment
|
||||
? [toDesignProfile(alignment, longitudinal.design_profiles?.[0])]
|
||||
@@ -454,9 +445,6 @@ export function renderProfile(ctx: ProfileRenderContext): void {
|
||||
widthPx: width,
|
||||
selectedStationId,
|
||||
onSelectStation: ctx.selectStation,
|
||||
onClearSelection: () => {
|
||||
if (ctx.selectedStationId() !== null) ctx.selectStation(null);
|
||||
},
|
||||
};
|
||||
massHaul.draw(massHaulParams);
|
||||
// 유토곡선도 **같은 창**을 본다(2026-09-04 사용자 확정) — 스크롤할 때마다 곡선만 다시
|
||||
|
||||
@@ -418,10 +418,7 @@ export function createSectionView(
|
||||
// 적혀 있어 무엇을 보는지는 이미 분명하다.
|
||||
const grid = document.createElement("div");
|
||||
grid.className = "b06-section__grid";
|
||||
// 카드 사이 빈 자리를 누르면 선택 해제 — 카드 안 클릭은 카드가 먼저 처리한다.
|
||||
grid.addEventListener("click", (event) => {
|
||||
if (!(event.target as HTMLElement).closest(".b06-cross-card")) clearSelection();
|
||||
});
|
||||
// 카드 사이 · 밖 빈 자리 누름은 고름을 안 풂 — 풀기는 Esc · 같은 카드 다시 누름(233-4)
|
||||
// 노면공 형상 누름 = 목록 고름(범위 카드 전부 · 형상 강조) — 카드 고름보다 먼저 가로챔(189-1)
|
||||
grid.addEventListener(
|
||||
"click",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/* 233-4 빈 곳 누름 = 어디서도 안 풂 — 진짜 소스의 누름 처리 본문을 떼어(TS 그 자리 트랜스파일) 돌림.
|
||||
* ① 종단 그래프 빈 곳 누름(`_Profile_Render` chartWrap click) ② 횡단 그리드 빈 곳 누름(`_Section_View` grid click)
|
||||
* ③ 같은 것 다시 누름 = 풂(종단 측점선 `design_profile` selectStation · 횡단 카드 `_Section_View` selectStation)
|
||||
* ④ Esc = 풂(`design_pick_escape` + `design_selection`). 표준출력 JSON 한 벌 · 짝 = test_233_4_empty_click.py.
|
||||
*/
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const ROOT = path.join(__dirname, "..", "..");
|
||||
const ts = require(path.join(ROOT, "config", "node_modules", "typescript"));
|
||||
|
||||
const read = (...parts) => fs.readFileSync(path.join(ROOT, ...parts), "utf8").replace(/\r/g, "");
|
||||
const run = (code, names, values) => {
|
||||
const js = ts.transpileModule(code, {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
return new Function(...names, js)(...values);
|
||||
};
|
||||
/** `start` 줄부터 같은 들여쓰기로 닫는 `end` 줄까지 떼어 냄. */
|
||||
const block = (src, start, end) => {
|
||||
const from = src.indexOf(start);
|
||||
if (from < 0) throw new Error(`본문 없음: ${start}`);
|
||||
const to = src.indexOf(end, from);
|
||||
return src.slice(from, to + end.length);
|
||||
};
|
||||
|
||||
const out = {};
|
||||
const target = (inStation = false, inCard = false) => ({
|
||||
closest: (sel) =>
|
||||
(sel.includes("b06-chart__station") && inStation) || (sel.includes("b06-cross-card") && inCard)
|
||||
? {}
|
||||
: null,
|
||||
});
|
||||
|
||||
/* ① 종단 그래프 빈 곳 누름 */
|
||||
const render = read("B05_Profile", "B05_Profile_UI_Profile_Render.ts");
|
||||
const chartClick = block(render, ` chartWrap.addEventListener("click", (event) => {`, "\n });");
|
||||
const calls = [];
|
||||
let tool = false;
|
||||
const ctx = {
|
||||
toolActive: () => tool,
|
||||
handleToolPick: (at) => (calls.push(`tool:${at}`), true),
|
||||
selectedStationId: () => "s120",
|
||||
selectedStructureId: () => "wall",
|
||||
selectStation: (id) => calls.push(`station:${id}`),
|
||||
setSelectedStructureId: (id) => calls.push(`structure:${id}`),
|
||||
redraw: () => calls.push("redraw"),
|
||||
};
|
||||
const callbacks = { onStructureSelect: (id) => calls.push(`onStructureSelect:${id}`) };
|
||||
let handler = null;
|
||||
const chartWrap = {
|
||||
addEventListener: (_type, fn) => (handler = fn),
|
||||
getBoundingClientRect: () => ({ left: 0 }),
|
||||
scrollLeft: 0,
|
||||
};
|
||||
run(
|
||||
chartClick,
|
||||
["ctx", "callbacks", "chartWrap", "chainageInverter", "longitudinal", "width", "layout"],
|
||||
[ctx, callbacks, chartWrap, () => (x) => x, null, 100, { originOffset: 0 }],
|
||||
);
|
||||
handler({ target: target(), clientX: 42 });
|
||||
out.profileEmpty = calls.splice(0);
|
||||
tool = true; // 도구 중 빈 곳 누름은 도구 몫 그대로
|
||||
handler({ target: target(), clientX: 42 });
|
||||
out.profileEmptyTool = calls.splice(0);
|
||||
|
||||
/* ② 횡단 그리드 빈 곳 누름 — 그리드에 걸린 누름 듣개를 모두 돌림 */
|
||||
const view = read("B06_Section", "B06_Section_UI_Section_View.ts");
|
||||
const gridStart = view.indexOf(' grid.className = "b06-section__grid";');
|
||||
const gridCode = view.slice(gridStart, view.indexOf(" // 열 수 = 사용자 값", gridStart)); // 그리드 누름 듣개 전부
|
||||
const gridCalls = [];
|
||||
const grid = { listeners: [], addEventListener: (_t, fn) => grid.listeners.push(fn) };
|
||||
run(
|
||||
gridCode.replace(' grid.className = "b06-section__grid";', ""),
|
||||
["grid", "clearSelection", "pavementAt", "spanMarks", "isPavement", "pavementSelectListener"],
|
||||
[grid, () => gridCalls.push("clearSelection"), () => null, [], () => false, null],
|
||||
);
|
||||
for (const fn of grid.listeners)
|
||||
fn({ target: target(), stopPropagation() {}, stopImmediatePropagation() {} });
|
||||
out.gridListeners = grid.listeners.length;
|
||||
out.gridEmpty = gridCalls.splice(0);
|
||||
|
||||
/* ③ 같은 것 다시 누름 = 풂 */
|
||||
const dpSrc = read("A00_Common", "design", "design_profile.ts");
|
||||
const dpSelect = block(
|
||||
dpSrc,
|
||||
" function selectStation(stationId: string | null): void {",
|
||||
"\n }",
|
||||
);
|
||||
const profileState = run(
|
||||
`let selectedStationId = null; let selectedStructureId = null;
|
||||
${dpSelect}
|
||||
return { selectStation, line: () => selectedStationId };`,
|
||||
["structureIdAtStation", "draw", "onSelectStation"],
|
||||
[() => null, () => undefined, () => undefined],
|
||||
);
|
||||
profileState.selectStation("s120");
|
||||
out.profileFirst = profileState.line();
|
||||
profileState.selectStation("s120");
|
||||
out.profileAgain = profileState.line();
|
||||
|
||||
const svSelect = block(
|
||||
view,
|
||||
" const selectStation = (stationId: string, scroll: boolean): void => {",
|
||||
"\n };",
|
||||
);
|
||||
const cardState = run(
|
||||
`let selectedStationId = null; let activeAreaKey = null;
|
||||
${svSelect}
|
||||
return { selectStation, card: () => selectedStationId };`,
|
||||
["applySelection", "revealCard"],
|
||||
[() => undefined, () => undefined],
|
||||
);
|
||||
cardState.selectStation("s120", true);
|
||||
out.cardFirst = cardState.card();
|
||||
cardState.selectStation("s120", true);
|
||||
out.cardAgain = cardState.card();
|
||||
|
||||
/* ④ Esc = 풂 */
|
||||
const store = new Map();
|
||||
const loadTs = (file, stubs) => {
|
||||
const box = { exports: {} };
|
||||
const js = ts.transpileModule(read(...file.split("/")), {
|
||||
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||
}).outputText;
|
||||
new Function("exports", "module", "require", js)(box.exports, box, (r) => stubs[r]);
|
||||
return box.exports;
|
||||
};
|
||||
const DSel = loadTs("A00_Common/design/design_selection.ts", {
|
||||
"../b_page_state": {
|
||||
readState: (k, p) => store.get(`${k}:${p}`) ?? null,
|
||||
writeState: (k, v, p) => store.set(`${k}:${p}`, v),
|
||||
},
|
||||
});
|
||||
let onKey = null;
|
||||
globalThis.window = { addEventListener: (_t, fn) => (onKey = fn), removeEventListener() {} };
|
||||
globalThis.document = { querySelector: () => null };
|
||||
const ESC = loadTs("A00_Common/design/design_pick_escape.ts", { "./design_selection": DSel });
|
||||
ESC.bindEscapeRelease("p", { visible: () => true, onDispose() {} });
|
||||
DSel.selectDesign("p", { chainageM: 120, structureId: null }, { from: "b06" });
|
||||
out.beforeEsc = DSel.readSelection("p");
|
||||
onKey({ key: "Escape", defaultPrevented: false, target: { closest: () => null } });
|
||||
out.afterEsc = DSel.readSelection("p");
|
||||
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""233-4 빈 곳 누름 = 어디서도 안 풂 — 3D(232-1)와 같은 한 규칙.
|
||||
|
||||
까닭: 종단 그래프 · 유토곡선 빈 곳 누름(08-04)과 횡단 그리드(카드 사이 · 밖) 빈 곳 누름(08-02)은 고름을 풀었고
|
||||
3D 빈 곳 누름(232-1)만 안 풀어 화면마다 「해제」 뜻이 달랐음(조사 AF 5절 5).
|
||||
고침: 빈 곳 누름 듣개를 뗌 · 풀기는 Esc(`design_pick_escape`) 또는 같은 것 다시 누름 한 길 ·
|
||||
도구([직선화] · [쉬프트] · [자유지정]) 중 빈 곳 누름은 도구 몫 그대로.
|
||||
진짜 소스의 누름 처리 본문을 `helper_233_4_empty_click.cjs` 가 떼어 Node 로 돌린다.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.path.isdir(os.path.join(ROOT, "config", "node_modules")),
|
||||
reason="프론트엔드 의존성 미설치",
|
||||
)
|
||||
|
||||
|
||||
def _read(*parts: str) -> str:
|
||||
with open(os.path.join(ROOT, *parts), encoding="utf-8") as handle:
|
||||
return handle.read()
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def out() -> dict:
|
||||
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||
["node", os.path.join(HERE, "helper_233_4_empty_click.cjs")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=60,
|
||||
)
|
||||
assert proc.returncode == 0, proc.stderr
|
||||
return json.loads(proc.stdout)
|
||||
|
||||
|
||||
def test_종단_빈_곳_누름은_고름_유지(out) -> None:
|
||||
assert out["profileEmpty"] == [] # 옛 코드 = 측점 · 알약 · 폼 풂
|
||||
|
||||
|
||||
def test_종단_도구_중_빈_곳_누름은_도구_몫_그대로(out) -> None:
|
||||
assert out["profileEmptyTool"] == ["tool:42"]
|
||||
|
||||
|
||||
def test_횡단_그리드_빈_곳_누름은_고름_유지(out) -> None:
|
||||
assert out["gridEmpty"] == [] # 옛 코드 = clearSelection
|
||||
assert out["gridListeners"] == 1 # 노면공 형상 누름만 남음(189-1)
|
||||
|
||||
|
||||
def test_같은_것_다시_누름은_풂(out) -> None:
|
||||
assert (out["profileFirst"], out["profileAgain"]) == ("s120", None)
|
||||
assert (out["cardFirst"], out["cardAgain"]) == ("s120", None)
|
||||
|
||||
|
||||
def test_Esc는_풂(out) -> None:
|
||||
assert out["beforeEsc"] == {"chainageM": 120, "structureId": None}
|
||||
assert out["afterEsc"] is None
|
||||
|
||||
|
||||
def test_유토곡선_빈_곳_누름도_안_풂() -> None:
|
||||
mass = _read("B05_Profile", "B05_Profile_UI_Profile_MassHaul.ts")
|
||||
render = _read("B05_Profile", "B05_Profile_UI_Profile_Render.ts")
|
||||
assert "onClearSelection" not in mass and "onClearSelection" not in render
|
||||
Reference in New Issue
Block a user