fix(구조물표): 322-3 고침 — B06 을 바로 열어도 옛 소수 관 자리 = 정수 m 초안(B05 snapOldStations 와 같은 셈 · 요구 167) · 계획고 편집 · 고름 따라감 · 알림 한 줄 같은 글 · 투영 이월 끝 = 선 길이(관이 전부 0 m 로 가던 흠) · design_view_cross 685줄 안(322-3 넘침)

- B06 구조물 목록 load: 관 초안 없고 저장분에 소수 자리 = `snapPlan` 으로 옮긴 초안(persistPipes) → 카드 · 측점 계획이 B05 를 먼저 연 것과 같음 · 정본은 [저장]에서(B08 사본 이름도 PUT 길)
- 서버 관 읽기는 그대로(파일 = 측점) · `project_pipe_points` 끝 = 선 길이
- 시험: test_322_3_flow 견본(85.05 … 982.64) → 85 · 173 · 258 · 352 · 440 · 534 · 620 · 720 · 804 · 900 · 983 · 정수 저장분 = 새 미리 뜬 카드 0 · 소수 저장분 = 정수 자리로 · test_322_3_saved_snap

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-14 17:02:22 +09:00
co-authored by Claude Opus 5.5
parent 0da2fa0767
commit 3d8c161efa
7 changed files with 131 additions and 22 deletions
+3 -15
View File
@@ -17,12 +17,7 @@ import { leaveForDashboard } from "../b_missing_data_guard";
import { readByKey, stateKey, writeByKey } from "../b_page_state";
import { showToast } from "@ui/ui_template_elements";
import { mountMissingStationNotice } from "../../B06_Section/B06_Section_UI_Missing_Stations";
import type { PlanPipe } from "../../common_util/common_util_structure_station";
import {
onStaleStructureTypes,
type StructureInstance,
type StructureType,
} from "../../B05_Profile/B05_Profile_Api_Structures";
import { onStaleStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
import {
fetchSectionContext,
getSections,
@@ -136,13 +131,7 @@ export async function mountCrossView(
);
// 「구조물 배치」 — 종단 보기와 같은 폼 한 벌에 횡단 보기 동작을 건다(계획서 24-3b).
let structureMarksSink:
| ((
structures: StructureInstance[],
types: StructureType[],
pipes: ReadonlyArray<PlanPipe>,
) => void)
| null = null;
let structureMarksSink: Parameters<typeof createB06StructuresPanel>[0]["onMarks"] | null = null;
/** 관 목록 초안을 고치면 한 벌 배수유역도가 그 초안으로 다시 선다(잇단 조작은 한 번). */
let pipeReloadTimer = 0;
let wallAdjustTimer = 0;
@@ -344,8 +333,7 @@ export async function mountCrossView(
shared,
);
// 좌측 목록이 넘겨 준 구조물을 종단 알약 레인으로 보낸다(표시 통일).
structureMarksSink = (structures, types, pipes) =>
sectionView.setStructureMarks(structures, types, pipes);
structureMarksSink = sectionView.setStructureMarks;
// 계획선 편집(▲/▼)은 종단도 한 벌이 맡는다(계획서 24-2c) — 같은 초안 가게 · 같은 막기 규칙.
// 종단 그래프 우클릭 — 종단 보기와 같은 메뉴로 넣고 뺀다(2026-09-12 사용자). 어느 길로
+11 -6
View File
@@ -125,11 +125,16 @@ export function snapOldStations(ctx: SnapContext): number {
}
plan.pipes.forEach(({ from, to, attributes }) => ctx.movePipe(from, to, attributes));
}
if (count || plan.skipped)
showToast(
(count ? `옛 소수 측점 ${count}건을 정수 m 로 옮겼습니다 — [저장]하면 남습니다.` : "") +
(plan.skipped ? ` 맞출 자리에 다른 관이 있는 관 ${plan.skipped}건은 그대로 둡니다.` : ""),
plan.skipped ? "info" : "success",
);
snapNotice(count, plan.skipped);
return count;
}
/** 옛 소수 맞춤 알림 한 줄 — B05 종단 보기 · B06 구조물 목록(관)이 같은 글. */
export function snapNotice(count: number, skipped: number): void {
if (count || skipped)
showToast(
(count ? `옛 소수 측점 ${count}건을 정수 m 로 옮겼습니다 — [저장]하면 남습니다.` : "") +
(skipped ? ` 맞출 자리에 다른 관이 있는 관 ${skipped}건은 그대로 둡니다.` : ""),
skipped ? "info" : "success",
);
}
@@ -54,6 +54,7 @@ import {
import { showToast } from "@ui/ui_template_elements";
import { snapStructureStation } from "../common_util/common_util_structure_station";
import { plantsStation } from "../B05_Profile/B05_Profile_UI_Profile_Structures";
import { snapNotice, snapPlan } from "../B05_Profile/B05_Profile_UI_Structures_Snap";
import {
adjustDockRoot,
refreshAdjustSlots,
@@ -427,6 +428,9 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
(basin) => Math.abs(basin.chainage_m - pipe.chainage_m) < PIPE_MATCH_M,
)?.design_flow_m3s ?? null,
}));
// 옛 소수 관 자리 = 정수 m 초안(요구 167 · B05 `snapOldStations` 와 같은 셈) — B06 을 바로 열어도
// 카드 · 측점이 B05 를 거친 것과 같음 · 정본은 [저장]에서
if (!pendingPipes) snapOldPipes(projectId);
section.setPipeFacilities(pipeFacilities);
// 종단 알약 레인 — 구조물 정본 + 계곡 통과 시설(관 정본)을 합쳐 B05 와 같은 표기로.
pushMarks();
@@ -500,6 +504,24 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
deps.onPipesWritten?.();
}
/** 옛 소수 관 자리 → 정수 m(확인 창 없이) — 계획고 편집 · 고름도 새 자리로(`movePipeTo` 와 같은 따라가기). */
function snapOldPipes(projectId: string): void {
const plan = snapPlan([], pipeFacilities);
plan.pipes.forEach(({ from, to, attributes }) => {
const hit = pipeFacilities.find((pipe) => pipe.chainage_m === from);
if (!hit) return;
deps.shared.profile?.resetStationEdit(from, to);
followMovedPipe(projectId, from, to);
Object.assign(hit, { chainage_m: to, start_m: attributes.start_m, end_m: attributes.end_m });
});
if (plan.pipes.length) {
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
persistPipes();
deps.onStructuresChanged?.(); // 새 자리 횡단을 미리 떠 카드로
}
snapNotice(plan.pipes.length, plan.skipped);
}
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
function movePipeTo(fromChainageM: number, toChainageM: number): void {
if (!deps.projectId) {
+2 -1
View File
@@ -221,7 +221,8 @@ def project_pipe_points(points: list[PipePoint], vertices: list[RouteVertex]) ->
if line.length <= 0:
return points
# 새 노선 위 자리도 정수 m · 일반 측점 1 m 안이면 그 측점(234-1) — 시설 속성은 관과 함께 옮겨 감
stations = route_general_stations(vertices[-1].chainage_m)
# 끝 = 선 길이(B05 계획선 꼭짓점은 누가거리 0 으로 와서 끝이 0 → 관이 전부 0 m 로 가던 흠)
stations = route_general_stations(line.length)
for point in points:
if point.x is None or point.y is None:
continue
+43
View File
@@ -68,6 +68,7 @@ function load(target) {
const box = { exports: {} };
cache.set(full, box.exports);
const need = (spec) => {
if (spec.startsWith("@ui/")) return { showToast: () => {} };
const alias = spec.startsWith("@util/") ? path.join(ROOT, "common_util", spec.slice(6)) : null;
const base = alias ?? path.join(path.dirname(full), spec);
const hit = [`${base}.ts`, base.replace(/\.js$/, ".ts"), base].find(
@@ -189,6 +190,48 @@ async function run(setup, detail) {
sel.selectDesign("P", { chainageM: 101, structureId: "w1" });
sel.followMovedPipe("P", 101, 120);
out.follow = [moved, sel.readSelection("P")];
// 옛 소수 관(검증_sub1 견본) — B06 을 바로 열어도 B05 와 같은 정수 m 초안(`snapPlan` · 요구 167)
const snap = load(file("B05_Profile", "B05_Profile_UI_Structures_Snap.ts"));
const DEC = [
85.05, 173.09, 258.12, 352.14, 440.24, 533.98, 619.71, 720.44, 804.18, 899.91, 982.64,
];
const ford = new Set([173.09, 352.14]);
const decPipes = DEC.map((at) => ({
chainage_m: at,
facility: ford.has(at) ? "ford_bridge" : "pipe",
options: { pipe_diameter_mm: "800" },
}));
const plan = snap.snapPlan([], decPipes);
out.snapTo = [plan.pipes.map((move) => move.to), plan.skipped];
const snapped = decPipes.map((pipe) => ({
...pipe,
chainage_m: plan.pipes.find((move) => move.from === pipe.chainage_m)?.to ?? pipe.chainage_m,
}));
const gridTo = (end) =>
Array.from({ length: end / 20 + 1 }, (_v, i) => ({
chainage_m: i * 20,
kind: "regular",
station_id: `g${i * 20}`,
}));
const titleAt = (at) => (ford.has(at) ? "세월교" : "파형강관 D800");
const own = [85, 173, 258, 352, 534, 804, 983];
const onGrid = [440, 620, 720, 900];
// 서버가 정수 자리로 심은 저장분(격자 자리는 구조물 측점 하나 · grid) — 새 미리 뜬 카드 0
const savedInt = [
...gridTo(1000).filter((s) => !onGrid.includes(s.chainage_m)),
...onGrid.map((at) => irregular(at, "파형강관 D800", { grid: true })),
...own.map((at) => irregular(at, at === 173 || at === 352 ? "세월교" : "파형강관 D800")),
];
out.snapSavedInt = await run(
{ savedPipes: decPipes, draftPipes: snapped, draftStructures: [] },
{ cross_sections: savedInt },
);
// 소수 자리로 심긴 저장분(검증_sub1 지금) = B05 를 먼저 연 것과 같음 — 소수 카드는 빼 두고 정수 자리 미리 뜸
const savedDec = [...gridTo(1000), ...DEC.map((at) => irregular(at, titleAt(at)))];
out.snapSavedDec = await run(
{ savedPipes: decPipes, draftPipes: snapped },
{ cross_sections: savedDec },
);
process.stdout.write(JSON.stringify(out));
})().catch((error) => {
process.stderr.write(String(error?.stack ?? error));
+25
View File
@@ -86,3 +86,28 @@ def test_remove_and_reopen(flows: dict) -> None:
[101, "옹벽", False, []],
[113, "옹벽", False, []],
]
# ── 옛 소수 관(검증_sub1 견본) — B06 을 바로 열어도 B05 와 같은 정수 m 초안(요구 167 · `snapPlan`) ──
SNAPPED = [85, 173, 258, 352, 440, 534, 620, 720, 804, 900, 983]
def test_old_decimal_pipes_snap_like_b05(flows: dict) -> None:
assert flows["snapTo"] == [SNAPPED, 0]
def test_saved_integer_stations_keep_cards(flows: dict) -> None:
# 서버가 정수 자리로 심은 저장분 = 계획과 같음 — 저장 카드 그대로 · 새 미리 뜬 카드 0
rows = flows["snapSavedInt"]
assert [row[0] for row in _marked(rows)] == SNAPPED
assert not any(row[2] for row in rows)
assert len(rows) == 58
def test_saved_decimal_stations_follow_plan(flows: dict) -> None:
# 소수 자리 저장분 = B05 를 먼저 연 것과 같음 — 소수 카드는 빼 두고 정수 자리(격자 자리는 사본)
rows = flows["snapSavedDec"]
assert [row[0] for row in _marked(rows)] == SNAPPED
assert all(row[2] for row in _marked(rows))
assert all(float(row[0]).is_integer() for row in rows)
assert len(rows) == 58
+25
View File
@@ -0,0 +1,25 @@
"""PLAN 322-3 고침 — 노선이 바뀌어 관을 좌표로 옮길 때(투영 이월) 끝 = 선 길이.
B05 계획선 꼭짓점은 누가거리 0 으로 와서 끝이 0 으로 잡혀 관이 전부 0 m 로 가던 흠.
"""
from __future__ import annotations
import json
from B05_Profile.B05_Profile_Engine_Sections import _load_pipe_points
LINE = [[0.0, 0.0, 100.0], [300.0, 0.0, 100.0]]
def test_projected_pipes_snap_to_whole_metres_not_zero(tmp_path) -> None:
folder = tmp_path / "B04_PreProcess" / "drainage" / "edits"
folder.mkdir(parents=True)
points = [
{"chainage_m": 85.05, "source": "user", "x": 90.05, "y": 0.3},
{"chainage_m": 173.09, "source": "user", "x": 178.4, "y": 0.2},
]
(folder / "pipe_points.json").write_text(
json.dumps({"route_signature": "옛노선", "points": points}), encoding="utf-8"
)
assert [pipe.chainage_m for pipe in _load_pipe_points(tmp_path, LINE)] == [90.0, 178.0]