From 596d4c7cf410837d87e51ba55fe3da75b84f1c1e Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 11:58:50 +0900 Subject: [PATCH 1/7] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20291=20=E2=80=94=20=EC=B0=8C=EA=BA=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=20=3D=20=EB=B8=8C=EB=A0=88=EC=9D=B8=EC=9D=B4?= =?UTF-8?q?=20=ED=81=B4=EB=A1=9C=EB=93=9C=20=EC=97=90=EC=9D=B4=EC=A0=84?= =?UTF-8?q?=ED=8A=B8=EB=A1=9C?= 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_01AbccG79TsSMUjnGqBq1MUE --- docs/raw/요구_1020/사용자요구_1020.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1020/사용자요구_1020.md b/docs/raw/요구_1020/사용자요구_1020.md index a1fdae589..17409874d 100644 --- a/docs/raw/요구_1020/사용자요구_1020.md +++ b/docs/raw/요구_1020/사용자요구_1020.md @@ -32,3 +32,7 @@ ## 요구 290 (10-20 · 사용자 · 289 물음 답) 1. 메인 화면에서 앞 측점 방향의 곡선을 변경하면 뒤에있는 구조물이나 일반, 곡선 측점은 자연스럽게 해당 로직에 맞춰 자동이동이됨. 구조물의 경우에는 최대한 평면도 상의 위치를 고수하기 위한 근접 위치로 옮겨갈거고 일반측점과 곡선측점은 거리값을 지키려고 할거야. 지금 그렇게 반영되어있을텐데 물어보는 이유는? + +## 요구 291 (10-20 · 사용자 · 492-3 찌꺼기 정리) + +1. 찌꺼기삭제는 너가 직접 워크트리 에이전트가 아닌 클로드 에이전트 기능으로 실행 From 7f204e3ded7d14b5684e3d0b6f361e22c73fca98 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 12:24:14 +0900 Subject: [PATCH 2/7] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20292=20=E2=80=94=20B05=20=C2=B7=20B06=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EC=B8=A1=EC=A0=90=20=ED=9A=A1=EB=8B=A8?= =?UTF-8?q?=EB=8F=84=20=ED=98=95=EC=83=81=20=EC=95=88=20=EB=B3=B4=EC=9E=84?= 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_01AbccG79TsSMUjnGqBq1MUE --- docs/raw/요구_1020/사용자요구_1020.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1020/사용자요구_1020.md b/docs/raw/요구_1020/사용자요구_1020.md index 17409874d..9dca54e68 100644 --- a/docs/raw/요구_1020/사용자요구_1020.md +++ b/docs/raw/요구_1020/사용자요구_1020.md @@ -36,3 +36,7 @@ ## 요구 291 (10-20 · 사용자 · 492-3 찌꺼기 정리) 1. 찌꺼기삭제는 너가 직접 워크트리 에이전트가 아닌 클로드 에이전트 기능으로 실행 + +## 요구 292 (10-20 · 사용자 · 추가 수정) + +1. 추가 수정 지금 b05 b06의 구조물 측점을 선택할때 나오는 횡단도의 구조물 형상 연결기능 확인. 횡단도 구조물 형상 안보임 From 9ed82887ff951a661113c9109003258c92fc7793 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 12:44:08 +0900 Subject: [PATCH 3/7] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20293=20=E2=80=94=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=A1=B0=ED=95=A9=20=3D=20=EC=88=98=EB=9F=89=20=C2=B7=20?= =?UTF-8?q?=EB=82=B4=EC=97=AD=20=C2=B7=20B07=20=EA=B5=AC=EC=A1=B0=EB=AC=BC?= =?UTF-8?q?=EB=8F=84=20=C2=B7=20=ED=98=95=EC=83=81=EC=9D=80=20=EB=B3=84?= =?UTF-8?q?=EA=B0=9C?= 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_01AbccG79TsSMUjnGqBq1MUE --- docs/raw/요구_1020/사용자요구_1020.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/raw/요구_1020/사용자요구_1020.md b/docs/raw/요구_1020/사용자요구_1020.md index 9dca54e68..1714660af 100644 --- a/docs/raw/요구_1020/사용자요구_1020.md +++ b/docs/raw/요구_1020/사용자요구_1020.md @@ -40,3 +40,8 @@ ## 요구 292 (10-20 · 사용자 · 추가 수정) 1. 추가 수정 지금 b05 b06의 구조물 측점을 선택할때 나오는 횡단도의 구조물 형상 연결기능 확인. 횡단도 구조물 형상 안보임 + +## 요구 293 (10-20 · 사용자 · 292 에 덧붙임 · 데이터 흐름) + +1. m02의 구조물조합을 연결하여 나중에 수량산출이나 내역서로 넘어가지만 형상 자체는 별개라는 말이야. +2. 측점에 연결된구조물 조합에서 도면 정보는 b07의 구조물도로 연결되어야하는거야. 이런 데이터 흐름은 벌써 이전 자료를 찾아보면 쉬워. From 7978579313fafd53699d41e6a54c5f6f243ed5e4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 12:54:12 +0900 Subject: [PATCH 4/7] =?UTF-8?q?fix(494-1=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20?= =?UTF-8?q?=EC=B8=A1=EC=A0=90=20=ED=9A=A1=EB=8B=A8=20=ED=98=95=EC=83=81):?= =?UTF-8?q?=20B06=20=ED=99=94=EB=A9=B4=20=EB=B0=96=20=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=3D=20=EC=89=AC=EB=8A=94=20=ED=8B=88=20=ED=95=9C=EB=8F=84=20?= =?UTF-8?q?=C2=B7=20B05=20=ED=9A=A1=EB=8B=A8=20=ED=8C=90=20=3D=20=EB=93=A4?= =?UTF-8?q?=EC=96=B4=EC=98=AC=20=EB=95=8C=20=EA=B3=A0=EB=A5=B8=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 원인 B06 = 구조물을 놓으면 새 측점 카드가 빈 설계로 먼저 서고 다시 셈 뒤 화면 밖 카드는 쉬는 틈 줄에서만 갈아 끼움 · 시간 제한이 없어 틈이 안 오는 창(ORCA 내장 · 가려진 창)에서는 설계선 · 옹벽 없는 카드가 그대로 남음(검증_sub1 옹벽 7+5 · 7+10 재현 · 다시 셈 값은 맞음) - 고침 = createIdleQueue 쉬는 틈 한도 200 ms(늦어도 그 안에 그림) - 원인 B05 = 「횡단」 판이 고름 바뀜만 들어 고른 구조물을 들고 들어오면 빈 판 · 다시 누르면 고름이 풀림 - 고침 = 판 만들 때 지금 고름을 한 번 따라감(판은 안 폄) - 시험 test_494_1_idle_queue(쉬는 틈 없는 창 흉내 · 들어올 때 고름 · 고치기 전 빨강) · helper_411_a 에 readSelection 흉내 · 들어옴 장면 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts | 19 +++++-- B06_Section/B06_Section_UI_Section_Common.ts | 10 +++- .../tester/helper_411_a_cross_structure.cjs | 18 +++++++ resources/tester/helper_494_idle_queue.cjs | 51 ++++++++++++++++++ resources/tester/test_494_1_idle_queue.py | 53 +++++++++++++++++++ 5 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 resources/tester/helper_494_idle_queue.cjs create mode 100644 resources/tester/test_494_1_idle_queue.py diff --git a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts index 177be2ab0..e53421e4c 100644 --- a/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts +++ b/B05_Profile/B05_Profile_UI_RouteEdit_Cross.ts @@ -30,6 +30,7 @@ import type { } from "../A00_Common/design/logic/design_pipeline_types"; import { nearestWithin, + readSelection, selectDesign, subscribeSelection, type DesignSelection, @@ -316,8 +317,13 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind stopPick(); }; - /** 그 측점 횡단을 위 판에 · 아래 판은 비움(전후 비교는 노선을 고쳤을 때만) · 접힌 판은 폄. */ - function showStation(key: string | null, chainageM: number, structure = false): void { + /** 그 측점 횡단을 위 판에 · 아래 판은 비움(전후 비교는 노선을 고쳤을 때만) · 접힌 판은 폄(`open`). */ + function showStation( + key: string | null, + chainageM: number, + structure = false, + open = true, + ): void { // 아래 판은 **노선을 고쳤을 때만** 채운다(2026-09-12 사용자 지시 ⑤). 예전에는 같은 // 측점을 두 번 누르면 위·아래에 **같은 그림 두 장**이 떠 전후 비교가 되지 않았다. previous.show(null, interval()); @@ -326,11 +332,11 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind shown = null; redraw(); const panels = sidePanelsOf.get(params.side); - if (panels && panels.current() === null) panels.open("cross", false); // 열되 기억 안 함(393-1) + if (open && panels && panels.current() === null) panels.open("cross", false); // 열되 기억 안 함(393-1) } /** 다른 보기(종단 · 3D · 목록 · 횡단 카드 · 평면 마커) 고름 → 그 측점 횡단(구조물 · 관도 · 411-A). */ - function follow(selection: DesignSelection): void { + function follow(selection: DesignSelection, open = true): void { attach(); const at = selection.chainageM; if (at === null) { @@ -343,12 +349,15 @@ export function createCrossPreview(params: CrossPreviewParams): CrossPreviewWind } const station = nearestWithin(stationsNow(), at, (entry) => entry.chainageM); const structure = !!selection.structureId || (!!station && !isGridStation(station)); - showStation(station?.key ?? null, station?.chainageM ?? at, structure); + showStation(station?.key ?? null, station?.chainageM ?? at, structure, open); } const stopPick = subscribeSelection(params.projectId, (selection, from) => { if (from !== PLAN_PICK_FROM) follow(selection); }); attach(); + // 들어올 때 이미 고른 것 — 판에 그 측점 횡단(494-1 · 안 그리면 고른 채 빈 판 · 다시 누르면 고름이 풀림) · 판은 안 폄 + const picked = readSelection(params.projectId); + if (picked) follow(picked, false); /** 그 측점 발자국 기록(열쇠 · 없으면 누가거리) — 발자국 단계 전이면 없음. */ const footprintAt = (key: string | null, chainageM: number): FootprintRecord | undefined => { diff --git a/B06_Section/B06_Section_UI_Section_Common.ts b/B06_Section/B06_Section_UI_Section_Common.ts index d85a2ee28..d476c03cf 100644 --- a/B06_Section/B06_Section_UI_Section_Common.ts +++ b/B06_Section/B06_Section_UI_Section_Common.ts @@ -127,16 +127,24 @@ export function staleKey( .join("|"); } +/** 화면 밖 카드 줄이 쉬는 틈을 기다리는 한도(ms). */ +const IDLE_TIMEOUT_MS = 200; + /** * 쉬는 틈 일거리 줄(346-2) — 화면 밖 횡단 카드를 몇 장씩 갈아 끼움(카드 하나 ≈ 8 ms · 0.54 s / 66). * 줄에 든 측점은 `run` 이 꺼내 그림 · 통째로 다시 그리면 `queue.clear()`. + * 쉬는 틈이 안 오는 창(ORCA 내장 · 가려진 창)도 늦어도 `IDLE_TIMEOUT_MS` 에 그림 — 안 그러면 새 구조물 측점 + * 카드가 빈 설계 그대로 남음(494-1). */ export function createIdleQueue(run: (chainageM: number) => void, batch = 6) { const queue = new Set(); let waiting = 0; const drain = (): void => { if (waiting || !queue.size) return; - const idle = window.requestIdleCallback ?? ((next: () => void) => window.setTimeout(next, 16)); + const idle = (next: () => void): number => + window.requestIdleCallback + ? window.requestIdleCallback(next, { timeout: IDLE_TIMEOUT_MS }) + : window.setTimeout(next, 16); waiting = idle(() => { waiting = 0; for (const chainageM of [...queue].slice(0, batch)) { diff --git a/resources/tester/helper_411_a_cross_structure.cjs b/resources/tester/helper_411_a_cross_structure.cjs index d5a6c4f98..224a624ce 100644 --- a/resources/tester/helper_411_a_cross_structure.cjs +++ b/resources/tester/helper_411_a_cross_structure.cjs @@ -36,6 +36,8 @@ global.ResizeObserver = class { }; let listener = null; +/** 들어올 때 이미 고른 것(`readSelection` · 494-1). */ +let stored = null; let stations = []; const route = { id: 1 }; const subscribers = []; @@ -57,6 +59,7 @@ const STUBS = { if (Math.abs(of(item) - at) < gap) [best, gap] = [item, Math.abs(of(item) - at)]; return best; }, + readSelection: () => stored, selectDesign() {}, subscribeSelection: (_projectId, fn) => ((listener = fn), () => {}), }, @@ -157,4 +160,19 @@ out.latchedMoved = shownNow(); // ⑦ 다른 보기가 풂(null · b05) = 판 비움(382-2 그대로) pick(null, null, "b05"); out.otherRelease = { ...shownNow(), watched: preview.watched() }; +// ⑧ 고른 구조물을 들고 들어옴(494-1) — 판을 만들자마자 그 측점 횡단(빈 판 아님) +stored = { chainageM: 45, structureId: "A" }; +stations = [...grid, station("structure:A", 45, "structure", "A")]; +const entrySide = fakeElement(); +const entry = cross.createCrossPreview({ + projectId: "p1", + side: entrySide, + request: () => ({ vertices: [], min_radius_m: 15, station_interval_m: 20 }), +}); +const entryPane = entrySide.children[0]; +out.entryPick = { + station: entryPane.querySelector(".b05-routeedit__cross-station").textContent, + foot: entryPane.querySelector(".b05-routeedit__cross-foot").textContent, + structure: entry.watched()?.structure ?? null, +}; console.log(JSON.stringify(out)); diff --git a/resources/tester/helper_494_idle_queue.cjs b/resources/tester/helper_494_idle_queue.cjs new file mode 100644 index 000000000..da3f79094 --- /dev/null +++ b/resources/tester/helper_494_idle_queue.cjs @@ -0,0 +1,51 @@ +/* PLAN 494-1 단위검증 헬퍼 — 화면 밖 횡단 카드 줄(`createIdleQueue`)이 쉬는 틈이 안 와도 그려지는가. + * `B06_Section_UI_Section_Common.ts` 에서 그 함수만 꺼내 트랜스파일해 Node 로 돌린다(다른 import 안 끎). + * 가짜 창 = 쉬는 틈이 영영 안 오는 브라우저(ORCA 내장 · 가려진 창) — 시간 제한을 준 부탁만 그 시간에 불림. + * 결과는 표준출력에 JSON 한 벌. + */ +const fs = require("fs"); +const path = require("path"); +const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript")); + +const file = path.join(__dirname, "..", "..", "B06_Section", "B06_Section_UI_Section_Common.ts"); +const src = fs.readFileSync(file, "utf-8").split("\r\n").join("\n"); +const body = src.match(/export function createIdleQueue[\s\S]*?\n}\n/); +if (!body) throw new Error("createIdleQueue 못 찾음"); +// 함수가 쓰는 파일 머리 상수(대문자 이름 한 줄) +const consts = [...src.matchAll(/^const [A-Z_]+ = [^;\n]+;$/gm)].map((m) => m[0]).join("\n"); +const js = ts.transpileModule(`${consts}\n${body[0]}`, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }, +}).outputText; + +let now = 0; +const timers = []; // { at, fn } +const window = { + requestIdleCallback: (fn, options) => { + // 쉬는 틈 없음 — 시간 제한이 있을 때만 그 시간에 부름(didTimeout) + if (options && Number.isFinite(options.timeout)) + timers.push({ + at: now + options.timeout, + fn: () => fn({ didTimeout: true, timeRemaining: () => 0 }), + }); + return timers.length + 1; + }, + setTimeout: (fn, ms) => (timers.push({ at: now + (ms || 0), fn }), timers.length + 1), +}; +const mod = { exports: {} }; +new Function("module", "exports", "window", js)(mod, mod.exports, window); +const { createIdleQueue } = mod.exports; + +const ran = []; +const q = createIdleQueue((m) => ran.push(m), 2); +[145, 150, 155, 160, 165].forEach((m) => q.queue.add(m)); +q.drain(); +// 2 초 흐름 — 그 사이 불릴 것 모두 부름 +for (;;) { + timers.sort((a, b) => a.at - b.at); + const next = timers[0]; + if (!next || next.at > 2000) break; + timers.shift(); + now = next.at; + next.fn(); +} +process.stdout.write(JSON.stringify({ ran, left: [...q.queue], elapsedMs: now })); diff --git a/resources/tester/test_494_1_idle_queue.py b/resources/tester/test_494_1_idle_queue.py new file mode 100644 index 000000000..d67a800cc --- /dev/null +++ b/resources/tester/test_494_1_idle_queue.py @@ -0,0 +1,53 @@ +"""PLAN 494-1 — B06 화면 밖 횡단 카드가 쉬는 틈이 안 와도 다시 그려지는가. + +구조물을 놓으면 새 측점 카드가 빈 설계로 먼저 서고, 다시 셈 뒤 화면 밖 카드는 쉬는 틈 줄 +(`createIdleQueue`)에서 갈아 끼움. 쉬는 틈이 안 오는 창(ORCA 내장 · 가려진 창)에서는 줄이 영영 안 +비어 설계선 · 구조물 형상이 없는 카드가 남았음. 짝: `B06_Section/B06_Section_UI_Section_Common.ts`. +실제 TS 를 `helper_494_idle_queue.cjs` 가 Node 로 돌린다. +""" + +from __future__ import annotations + +import json +import os +import subprocess + +HERE = os.path.dirname(__file__) + + +def _run() -> dict: + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_494_idle_queue.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=60, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_쉬는_틈이_없어도_줄의_카드를_모두_그린다() -> None: + out = _run() + assert out["ran"] == [145, 150, 155, 160, 165] + assert out["left"] == [] + + +def test_늦어도_1초_안에_다_그린다() -> None: + """카드 다섯 · 두 장씩 = 세 번 — 시간 제한 안에 끝남.""" + assert _run()["elapsedMs"] <= 1000 + + +def test_B05_평면_횡단판은_들어올_때_이미_고른_측점을_그린다() -> None: + """B05 「횡단」 판 — 고른 구조물을 들고 들어오면 빈 판이었음(다시 누르면 고름이 풀림). + `_RouteEdit_Cross` 진짜를 `helper_411_a_cross_structure.cjs` 가 돌림(⑧).""" + proc = subprocess.run( # noqa: S603 — 고정 실행 파일 + ["node", os.path.join(HERE, "helper_411_a_cross_structure.cjs")], + capture_output=True, + text=True, + encoding="utf-8", + timeout=120, + ) + assert proc.returncode == 0, proc.stderr + got = json.loads(proc.stdout)["entryPick"] + assert got == {"station": "45", "foot": "횡단 45", "structure": True} From bbcb632c857eaf990fe428ebd550ef6e8a0e3b0c Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 13:15:52 +0900 Subject: [PATCH 5/7] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20294=20=E2=80=94=20=EA=B2=80=EC=A6=9D=5F=EB=B8=8C?= =?UTF-8?q?=EB=A0=88=EC=9D=B8232=20=ED=9A=A1=EB=8B=A8=20=ED=98=95=EC=83=81?= =?UTF-8?q?=20=ED=99=95=EC=9D=B8?= 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_01AbccG79TsSMUjnGqBq1MUE --- docs/raw/요구_1020/사용자요구_1020.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1020/사용자요구_1020.md b/docs/raw/요구_1020/사용자요구_1020.md index 1714660af..a7cd6a5f4 100644 --- a/docs/raw/요구_1020/사용자요구_1020.md +++ b/docs/raw/요구_1020/사용자요구_1020.md @@ -45,3 +45,7 @@ 1. m02의 구조물조합을 연결하여 나중에 수량산출이나 내역서로 넘어가지만 형상 자체는 별개라는 말이야. 2. 측점에 연결된구조물 조합에서 도면 정보는 b07의 구조물도로 연결되어야하는거야. 이런 데이터 흐름은 벌써 이전 자료를 찾아보면 쉬워. + +## 요구 294 (10-20 · 사용자 · 292 이어서) + +1. 검증_브레인232 프로젝트 확인 From 52553a6c6a75fd61f4ae8e713535083cc384ea33 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 13:19:14 +0900 Subject: [PATCH 6/7] =?UTF-8?q?fix(494-2=20=EC=83=88=20=EA=B5=90=EC=B0=A8?= =?UTF-8?q?=20=EA=B4=80=20=EA=B8=B0=EB=B3=B8=EA=B0=92):=20=EB=85=B8?= =?UTF-8?q?=EC=84=A0=20=EB=B0=94=EB=80=9C=20=EA=B8=B8(=EC=86=90=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=EC=A0=80=EC=9E=A5=20=C2=B7=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20=EC=A0=81=EC=9A=A9=20=C2=B7=20=EC=B4=88=EA=B8=B0=20?= =?UTF-8?q?=EC=A2=85=EB=8B=A8)=20=EC=83=88=20=EC=84=B8=EB=A5=98=EC=84=A0?= =?UTF-8?q?=20=EA=B5=90=EC=B0=A8=20=EA=B4=80=20=3D=20B04=20=EC=83=88=20?= =?UTF-8?q?=EA=B4=80=20=EA=B8=B0=EB=B3=B8=EA=B0=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - followed_pipes 에서 새로 더한 관(짝 -1)만 _apply_new_defaults · 옛 빈 관 · 사용자 관 그대로 - 시험 test_494_2(옛 코드 빨강) · test_283 새 교차 관 옵션 기대 = 기본값 Co-Authored-By: Claude Opus 5.5 --- B05_Profile/B05_Profile_Route_Save.py | 3 + .../tester/test_283_route_change_reset.py | 15 ++- .../test_494_2_crossing_pipe_defaults.py | 95 +++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 resources/tester/test_494_2_crossing_pipe_defaults.py diff --git a/B05_Profile/B05_Profile_Route_Save.py b/B05_Profile/B05_Profile_Route_Save.py index 75d225105..8d71fba47 100644 --- a/B05_Profile/B05_Profile_Route_Save.py +++ b/B05_Profile/B05_Profile_Route_Save.py @@ -176,6 +176,7 @@ def followed_pipes( """관 따라감 + 새 세류선 교차점 기본 관(저장 안 함) → (짝, 교차 수, 새 노선 정점) — 체인 · 추천 초기 종단(476) 한 벌.""" from B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze import _base_pipes + from B04_PreProcess.B04_PreProcess_Router_Basins import _apply_new_defaults from common_util.common_util_drainage_pipes import fill_pipe_coordinates vertices = vertices_of([(p["x"], p["y"]) for p in points]) @@ -183,6 +184,8 @@ def followed_pipes( pairs = follow_pipes(old_pipes, vertices) total = float(vertices[-1].chainage_m) crossing = add_crossing_pipes(pairs, _base_pipes(vertices, streams), old_xy, total) + # 새 교차 관만 B04 와 같은 등록부 기본값(54-39 · 494-2) — 옛 관 빈칸은 미확정 그대로 + _apply_new_defaults([pipe for index, pipe in pairs if index < 0]) return pairs, crossing, vertices diff --git a/resources/tester/test_283_route_change_reset.py b/resources/tester/test_283_route_change_reset.py index 4d61925b6..bcd6884f2 100644 --- a/resources/tester/test_283_route_change_reset.py +++ b/resources/tester/test_283_route_change_reset.py @@ -12,7 +12,7 @@ ⑥ 추천 [적용] 실패 — 초기값 사본(`initial_snapshot`)까지 되돌림 ⑦ 떠 두기 중 뒤 미리 셈이 파일을 지움(`_derived` 원자 쓰기 · 정리) — 건너뜀(옛 = 500 FileNotFoundError) -492 코드 = ① ④(손 고침) ⑤ 실패 · 옛 떠 두기 = ⑦ 실패. +492 코드 = ① ④(손 고침) ⑤ 실패 · 옛 떠 두기 = ⑦ 실패 · 새 교차 관 = B04 새 관 기본값(494-2). 체인 · API 는 346-3 가짜 세계(`chain_world` · `api_world`)를 빌려 씀 — 파일은 임시 폴더만. @@ -43,8 +43,10 @@ import B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze as analyze # noqa import B05_Profile.B05_Profile_Route_Save as save # noqa: E402 import B05_Profile.B05_Profile_Router_Replan as replan # noqa: E402 import B06_Section.B06_Section_Repository as b06_repo # noqa: E402 +from B04_PreProcess.B04_PreProcess_Router_Basins import _apply_new_defaults # noqa: E402 from B05_Profile.B05_Profile_Plus_Stations import load_plus_stations, save_plus_stations # noqa: E402 from B05_Profile.B05_Profile_Structures_Repository import load_structures # noqa: E402 +from common_util.common_util_drainage_pipes import PipePoint # noqa: E402 from common_util.common_util_json import atomic_write_json # noqa: E402 FIXTURE = HERE / "fixtures" / "req283_sub8_route.json" @@ -70,6 +72,13 @@ WALL = { CROSSING = SimpleNamespace(chainage_m=158.31, x=150.0, y=30.0) +def _new_pipe_options() -> dict: + """새 교차 관 옵션 = B04 유역 길의 새 관 기본값(494-2 · 54-39).""" + pipe = PipePoint(chainage_m=0.0, source="stream") + _apply_new_defaults([pipe]) + return pipe.options + + def _side(xy, x: float, y: float) -> float: from shapely.geometry import LineString, Point @@ -137,7 +146,7 @@ def _assert_reset(world, result: dict, cross_seen: list) -> None: assert load_plus_stations(world.root) == [] points = json.loads(world.pipes_file.read_text("utf-8"))["points"] assert len(points) == 1, points # 옛 사용자 관 버림 · 새 교차 관만 - assert points[0]["source"] == "stream" and not points[0].get("options") + assert points[0]["source"] == "stream" and points[0]["options"] == _new_pipe_options() assert _side(base.NEW, points[0]["x"], points[0]["y"]) < 1e-6 assert "profile" not in world.order # 계획고 옮김 없음 = 1차 계획선 assert cross_seen == [[]] # 횡단 손 입력 안 넘김 @@ -257,7 +266,7 @@ def test_검증_sub8_추천_적용은_교차_하나(sub8, chain_world, cross_see points = json.loads(chain_world.pipes_file.read_text("utf-8"))["points"] assert [round(p["chainage_m"]) for p in points] == [388] assert _side(sub8["new_xy"], points[0]["x"], points[0]["y"]) < 0.01 - assert not points[0].get("options") + assert points[0]["options"] == _new_pipe_options() # 494-2 새 교차 관 = 기본값 assert result["route_reset"]["dropped"]["pipes"] == 1 diff --git a/resources/tester/test_494_2_crossing_pipe_defaults.py b/resources/tester/test_494_2_crossing_pipe_defaults.py new file mode 100644 index 000000000..fe4717358 --- /dev/null +++ b/resources/tester/test_494_2_crossing_pipe_defaults.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +"""494-2 노선 바뀜 길의 새 세류선 교차 관 = B04 와 같은 등록부 기본값(54-39). + +못박는 것 + - 손 고침 [저장] · 추천 [적용](체인 `_place_pipes`) · 추천 초기 종단(`reset_facilities`) 셋 다 + `followed_pipes` 를 지나 새 교차 관이 B04 `_apply_new_defaults` 와 같은 옵션을 받음. + - 이미 저장된 빈 관 · 사용자가 옵션을 고른 관은 그대로(10-05 「기존 빈칸 = 미확정」). +""" + +from __future__ import annotations + +import asyncio +import sys +from pathlib import Path +from types import SimpleNamespace + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +import B04_PreProcess.B04_PreProcess_Engine_Watershed_Analyze as analyze # noqa: E402 +import B04_PreProcess.B04_PreProcess_Router_Watershed as watershed # noqa: E402 +import B05_Profile.B05_Profile_Engine_InitialProfile as initial_profile # noqa: E402 +import B05_Profile.B05_Profile_Route_Save as save # noqa: E402 +import common_util.common_util_drainage_pipes as drainage_pipes # noqa: E402 +from B04_PreProcess.B04_PreProcess_Router_Basins import _apply_new_defaults # noqa: E402 +from common_util.common_util_drainage_pipes import PIPE_SOURCE_STREAM, PipePoint # noqa: E402 + +OLD = [(0.0, 0.0), (100.0, 0.0), (200.0, 0.0), (300.0, 0.0), (400.0, 0.0)] +NEW = [(0.0, 0.0), (100.0, 0.0), (200.0, 60.0), (300.0, 0.0), (400.0, 0.0)] +# 새 노선 북쪽 휨 위(옛 노선에서 먼) 세류선 교차 하나 +CROSSING = SimpleNamespace(chainage_m=150.4, x=150.0, y=30.0) + + +def _b04_defaults() -> dict: + """B04 유역 길이 새 관에 주는 옵션 — 비교 기준.""" + pipe = PipePoint(chainage_m=150.0, source=PIPE_SOURCE_STREAM) + _apply_new_defaults([pipe]) + return pipe.options + + +def _old_pipes() -> list[PipePoint]: + return [ + PipePoint(chainage_m=50.0, x=50.0, y=0.0), # 저장된 빈 관 — 미확정 그대로 + PipePoint(chainage_m=350.0, x=350.0, y=0.0, options={"pipe_kind": "흄관"}), # 사용자 고름 + ] + + +def _points(xy) -> list[dict[str, float]]: + return [{"x": x, "y": y} for x, y in xy] + + +def _fake_crossings(monkeypatch) -> None: + monkeypatch.setattr(analyze, "_base_pipes", lambda vertices, streams: [CROSSING]) + + +def _check(pipes: list[PipePoint], kept: bool) -> None: + new = [pipe for pipe in pipes if abs(pipe.chainage_m - 150.0) < 1] + assert len(new) == 1 and new[0].options == _b04_defaults() + assert new[0].options.get("pipe_diameter_mm") # 관경 빈칸 아님(횡단 관 그림 · 54-39) + if kept: # 누가거리 순 = 빈 관 · 새 관 · 사용자 관 + assert [pipe.options for pipe in pipes[::2]] == [None, {"pipe_kind": "흄관"}] + + +def test_손_고침_저장_followed_pipes(monkeypatch): + _fake_crossings(monkeypatch) + old = _old_pipes() + pairs, crossing, _ = save.followed_pipes(old, [list(p) for p in OLD], _points(NEW), []) + assert crossing == 1 + _check([pipe for _, pipe in pairs], kept=True) + assert old[0].options is None # 옛 목록 그대로 + + +def test_추천_적용_체인_관_저장(monkeypatch): + """체인 [적용](492 초기화) — 옛 관 버림 · 새 교차 관 = 기본값으로 정본 저장.""" + _fake_crossings(monkeypatch) + written: list = [] + + async def prepare(_project_id): + return {"streams": []} + + monkeypatch.setattr(watershed, "_prepare", prepare) + monkeypatch.setattr( + drainage_pipes, "save_pipe_points", lambda path, sig, pipes, verts: written.extend(pipes) + ) + pairs, counts = asyncio.run(save._place_pipes(None, "unused", [], [], _points(NEW))) + assert counts["crossing"] == 1 + _check(written, kept=False) + + +def test_추천_초기_종단_reset_facilities(monkeypatch): + _fake_crossings(monkeypatch) + pipes, structures, plus = initial_profile.reset_facilities(NEW, []) + assert structures == [] and plus == [] + _check(pipes, kept=False) From e6927fa2ef41a1c9e622e493ad61eebf185e9672 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Tue, 20 Oct 2026 13:29:23 +0900 Subject: [PATCH 7/7] =?UTF-8?q?chore(=EC=9A=94=EA=B5=AC):=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=20295=20=E2=80=94=20=EA=B8=B0=EC=A1=B4=20=EB=B9=88=20?= =?UTF-8?q?=EA=B4=80=20=3D=20=EC=BD=94=EB=93=9C=EB=A1=9C=20=EC=95=88=20?= =?UTF-8?q?=EC=B1=84=EC=9B=80=20=C2=B7=20=EB=85=B8=EC=84=A0=20=EC=B6=94?= =?UTF-8?q?=EC=B2=9C=20[=EC=A0=81=EC=9A=A9]=EC=9C=BC=EB=A1=9C=20=EC=83=88?= =?UTF-8?q?=EB=A1=9C?= 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_01AbccG79TsSMUjnGqBq1MUE --- docs/raw/요구_1020/사용자요구_1020.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/raw/요구_1020/사용자요구_1020.md b/docs/raw/요구_1020/사용자요구_1020.md index a7cd6a5f4..bfc4cfc8d 100644 --- a/docs/raw/요구_1020/사용자요구_1020.md +++ b/docs/raw/요구_1020/사용자요구_1020.md @@ -49,3 +49,7 @@ ## 요구 294 (10-20 · 사용자 · 292 이어서) 1. 검증_브레인232 프로젝트 확인 + +## 요구 295 (10-20 · 사용자 · 494-2 기존 빈 관 물음 답) + +1. 지금 진행중인 조사건들을 완료 후 빈채로 있는것들은 그냥 노선 추천에서 다른 항목으로 변경하여 저장하면 되는거 아니야,