From 0fef925456e438e474a7f2f1584a8b6142ac31a2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 17:15:25 +0900 Subject: [PATCH 1/9] =?UTF-8?q?fix(394-1):=20=EC=98=9B=20=EC=8B=9C?= =?UTF-8?q?=ED=97=98=20=EC=8B=A4=ED=8C=A8=20=EC=85=8B=20=E2=80=94=20?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0=20=EC=95=8C=EC=95=84=EC=B1=84=EA=B8=B0=20win?= =?UTF-8?q?dow=20=EC=97=86=EB=8A=94=20=EA=B3=B3=20=EA=B1=B4=EB=84=88?= =?UTF-8?q?=EB=9C=80=20=C2=B7=20254=5F1=20=EC=98=AE=EA=B8=B4=20=ED=91=9C?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EC=97=90=EC=84=9C=20=EC=B0=BE=EC=9D=8C=20?= =?UTF-8?q?=C2=B7=2052=5F6=20=EB=A7=88=EC=8A=A4=ED=84=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=EA=B0=92=EC=97=90=20=EC=95=88=20=EA=B8=B0=EB=8C=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- A00_Common/device_context.ts | 10 ++++++---- resources/tester/test_254_1_b08_milestone_bases.py | 3 ++- resources/tester/test_52_6_cross_underdrain.py | 9 ++------- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/A00_Common/device_context.ts b/A00_Common/device_context.ts index 4ea4e6d42..cf07adf04 100644 --- a/A00_Common/device_context.ts +++ b/A00_Common/device_context.ts @@ -51,12 +51,14 @@ export function readTouchMode(): TouchMode { } } -const coarseQuery = window.matchMedia("(pointer: coarse)"); +// window 없는 곳(서버 Node 묶음) = 건너뜀 +const hasWindow = typeof window !== "undefined"; +const coarseQuery = hasWindow ? window.matchMedia("(pointer: coarse)") : null; let current: DeviceContext | null = null; let lastInput = ""; function read(): DeviceContext { - const pointer = coarseQuery.matches ? "coarse" : "fine"; + const pointer = coarseQuery?.matches ? "coarse" : "fine"; const touchMode = readTouchMode(); const touch = touchMode === "on" || (touchMode === "auto" && pointer === "coarse"); return { @@ -105,7 +107,7 @@ function init(): void { for (const [name, px] of Object.entries(SIZE_BREAKS)) html.style.setProperty(`--bp-${name}`, `${px}px`); apply(); - coarseQuery.addEventListener("change", apply); + coarseQuery?.addEventListener("change", apply); window.addEventListener("resize", apply); window.visualViewport?.addEventListener("resize", apply); screen.orientation?.addEventListener("change", apply); @@ -120,4 +122,4 @@ function init(): void { ); } -init(); +if (hasWindow) init(); diff --git a/resources/tester/test_254_1_b08_milestone_bases.py b/resources/tester/test_254_1_b08_milestone_bases.py index 5cccd62e7..2eb6d6c66 100644 --- a/resources/tester/test_254_1_b08_milestone_bases.py +++ b/resources/tester/test_254_1_b08_milestone_bases.py @@ -205,7 +205,8 @@ def test_화면_기준점_여럿_등록_삭제(): state = _read(B08 + "UI_Milestone_State.ts") assert "migrateMilestone(" in state and "bases: []," in state page = _read(B08 + "UI_Milestone.ts") - assert "data.bases.length > 0" in page and 'to: "site",' in page + assert 'to: "site",' in page + assert "data.bases.length > 0" in _read(B08 + "Milestone_Table.ts") # 302-3 표로 옮김 for name in ("Milestone_Calc.ts", "Milestone_Layout.ts", "UI_Milestone_Diagram.ts", "UI_Milestone_Modal.ts", "UI_Milestone_State.ts", "UI_Milestone_LegModal.ts", "UI_Milestone_Route.ts", "UI_Milestone.ts"): # fmt: skip diff --git a/resources/tester/test_52_6_cross_underdrain.py b/resources/tester/test_52_6_cross_underdrain.py index b8146e009..6e5a15584 100644 --- a/resources/tester/test_52_6_cross_underdrain.py +++ b/resources/tester/test_52_6_cross_underdrain.py @@ -36,13 +36,8 @@ def test_컨테이너_맹암거_높이_경사_손_입력(): ("culvert_slope_pct", "암거 경사", "%"), ): o = opts[key] - assert (o.label, o.input, o.unit, o.default, o.required) == ( - label, - "number", - unit, - None, - False, - ) + # 기본값 = 마스터 사용자 편집 몫 — 실제 값에 기대지 않음 + assert (o.label, o.input, o.unit, o.required) == (label, "number", unit, False) assert o.empty_means # 손 입력 · 기본값 근거 없음 — 비워 둘 수 있음(B05 단계 필수 금지) groups = load_definition()["그룹"][KEY] assert groups == [ From a108c3ae112c131ebdf96ca829ff3e45e4a230d2 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 17 Oct 2026 17:17:51 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat(B05=20=ED=8F=89=EB=A9=B4=20=EB=8F=84?= =?UTF-8?q?=EA=B5=AC=EC=A4=84):=20=EA=B5=AC=EB=B6=84=EC=84=A0=20=C2=B7=20?= =?UTF-8?q?=EC=8A=A4=EB=83=85=20=EA=B0=84=EA=B2=A9=20=EA=B3=A0=EB=A5=B4?= =?UTF-8?q?=EA=B8=B0=20=C2=B7=20Ctrl+=ED=9C=A0=205=C2=B0=20=ED=9A=8C?= =?UTF-8?q?=EC=A0=84=20=C2=B7=20[=EA=B8=B0=EC=9A=B8=EA=B8=B0]=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EA=BA=BC=EC=A7=90=20(390-1=20=C2=B7=202=20=C2=B7?= =?UTF-8?q?=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [측점 스냅] · 간격 | 분석 지형 구분선(분석 지형 숨으면 같이 숨김) · [기울기] 기본 끔 - 스냅 간격 0.1 · 1 · 10 m · 측점 No.(기본) — 프로젝트별 화면 기억 · 스냅 끔이면 흐림 · MC 를 간격 배수에 · 허용 오차 간격 따라(0.1 m = 0.005 m) - Ctrl+휠 한 칸 5° 회전(단추 15° 그대로 · 손가락 · 트랙패드 벌림 = 확대 · 추천 지도 안 돎) · 도움말 한 줄 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- A00_Common/b_page_state.ts | 2 + B05_Profile/B05_Profile_UI_RouteEdit.ts | 16 +++---- .../B05_Profile_UI_RouteEdit_Chrome.ts | 11 +++++ B05_Profile/B05_Profile_UI_RouteEdit_Input.ts | 7 ++- B05_Profile/B05_Profile_UI_RouteEdit_Marks.ts | 4 +- .../B05_Profile_UI_RouteEdit_Rotate.ts | 17 ++++++-- .../B05_Profile_UI_RouteEdit_SnapStep.ts | 40 +++++++++++++++++ .../B05_Profile_UI_Style_RouteEdit.css | 9 ++++ .../logic/B05_Profile_RouteEdit_StationFit.ts | 12 ++++-- resources/tester/helper_366_3_station_fit.cjs | 15 +++++-- .../tester/helper_b05_routeedit_view.cjs | 43 +++++++++++++++++++ resources/tester/test_366_3_station_fit.py | 15 +++++++ .../tester/test_381_2_routeedit_marks.py | 6 +-- .../tester/test_b05_routeedit_initial.py | 14 ++++-- resources/tester/test_b05_routeedit_view.py | 10 +++++ 15 files changed, 191 insertions(+), 30 deletions(-) create mode 100644 B05_Profile/B05_Profile_UI_RouteEdit_SnapStep.ts diff --git a/A00_Common/b_page_state.ts b/A00_Common/b_page_state.ts index 60ee6b9dd..8250c2cce 100644 --- a/A00_Common/b_page_state.ts +++ b/A00_Common/b_page_state.ts @@ -75,6 +75,8 @@ export const STATE_REGISTRY = { "routeedit-locks": { bucket: "pref", scope: "project" }, /** B05 계획노선 편집 창 — 분석 지형(las · sheet). 확정 지표면은 안 바꿈 */ "routeedit-terrain": { bucket: "pref", scope: "project" }, + /** B05 계획노선 편집 창 — 측점 스냅 간격(0.1 · 1 · 10 · station) */ + "routeedit-snap-step": { bucket: "pref", scope: "project" }, "masshaul-open": { bucket: "pref", scope: "global", diff --git a/B05_Profile/B05_Profile_UI_RouteEdit.ts b/B05_Profile/B05_Profile_UI_RouteEdit.ts index 540c6e57a..e74c3bf2e 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit.ts @@ -91,6 +91,7 @@ import { import type { DesignPageHost } from "../A00_Common/design/design_page"; import * as draftLink from "./B05_Profile_UI_RouteEdit_Draft"; import { bindRouteShift } from "./B05_Profile_UI_RouteEdit_Shift"; +import { bindStationSnap, paintStationSnap } from "./B05_Profile_UI_RouteEdit_SnapStep"; import * as fit from "./logic/B05_Profile_RouteEdit_StationFit"; import "./B05_Profile_UI_Style_RouteEdit.css"; @@ -173,11 +174,7 @@ export async function mountRouteEditor( curveSpans(line, curveInfo), ); }; - const setStationSnap = (on: boolean): void => { - stationSnap = on; - stationSnapButton.classList.toggle("is-active", on); - stationSnapButton.setAttribute("aria-pressed", String(on)); - }; + const setStationSnap = (on: boolean) => paintStationSnap(stationSnapButton, (stationSnap = on)); /** 되돌리기 사진첩 — 노선을 읽은 뒤에 선다(그전에는 되돌릴 것이 없다). */ let history: RouteEditHistory | null = null; let meta: VWorldMeta | null = null; @@ -264,8 +261,8 @@ export async function mountRouteEditor( const slope = createSlopeLabels({ projectId, isClosed: () => closed, onChange: () => draw() }); /** 1m 간격 등고선 켬끔 — 기본 꺼짐(계획서 18 ②). */ const showMinorContours = bindMinorContourToggle(minorContourButton, () => draw()); - stationSnapButton.addEventListener("click", () => { - setStationSnap(!stationSnap); + const snapStepM = bindStationSnap(chrome, projectId, stationIntervalM, (toggle) => { + if (toggle) setStationSnap(!stationSnap); if (stationSnap && history) applyEdit("곡선 가운데(MC)를 일반측점에 맞췄습니다."); // 전체 · 한 걸음 draw(); }); @@ -416,7 +413,7 @@ export async function mountRouteEditor( violationCount: nodeInfo.filter((node) => node.violations.length).length, minRadiusM, fresh: nodeInfo.length === 0, - offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, stationIntervalM) : 0, + offStation: stationSnap ? fit.offStationCount(plannedLine, curveInfo, snapStepM()) : 0, }); // ── 곡선 라벨 — 고른 꺾임점 옆에 뜨는 조작 패널. 배선은 `_CurveBar` 몫 ── @@ -450,7 +447,7 @@ export async function mountRouteEditor( /** 한 번의 편집을 마무리한다 — 다시 그리고, 라벨·상태줄을 맞추고, 되돌리기에 쌓는다. * 측점 스냅 켬이면 고친 자리 앞 곡선부터 MC 를 일반측점에(366-3 · 되살림 `fitting=false` 은 안 맞춤). */ function applyEdit(message: string, record = true, fitting = true): void { - const limits = { minRadiusM, limitRadiusM, limitArcM, intervalM: stationIntervalM }; + const limits = { minRadiusM, limitRadiusM, limitArcM, intervalM: snapStepM() }; const from = fit.fitStart(history?.current?.(), snapshotNow()); if (stationSnap && fitting) planned = fit.fitCurveStations(snapshotNow(), limits, from); markEdited(); @@ -574,6 +571,7 @@ export async function mountRouteEditor( getMeta: () => meta, // 돌린 지도에서는 손이 민 방향과 그림이 움직일 방향이 다르다 — 거꾸로 돌려 넘긴다. unrotateDelta: rotation.unrotateDelta, + rotateWheel: rotation.wheel, // Ctrl+휠 = 5° 돌리기(390-3) draw, }); diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts index 6c1273f5e..faa7659c5 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Chrome.ts @@ -110,6 +110,16 @@ export function createRouteEditChrome( + +