fix(B05 배수유역): 342-3 첫 열기 세부 유역 요청 11번 → 1번 — 같은 틱 analyze 를 한 번으로

- B05_Profile_UI_Drainage_Pipes batched(같은 틱 부름 = 한 요청) · 배수유역 판 analyze 에 씌움
- 옛 소수 관 정수 맞춤(snapOldStations)이 관마다 부르던 POST detail-basins 가 한 번 · 계획고 따라가기 · 속성 이어받기 길은 그대로
- 시험 test_342_3_b05_detail_once(11번 부름 = 1번 · 다음 조작은 다시)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-16 11:06:47 +09:00
co-authored by Claude Opus 5.5
parent b8ed082874
commit 2124bc85b9
5 changed files with 78 additions and 5 deletions
+3 -4
View File
@@ -29,7 +29,7 @@ import { type RoutePoint } from "./B05_Profile_Api_Fetch";
import type { FlowArrow } from "../B04_PreProcess/B04_PreProcess_UI_FlowArrows";
import { buildStrengthArray } from "../B04_PreProcess/B04_PreProcess_UI_FlowRamp";
import { resampleRoute } from "../B04_PreProcess/B04_PreProcess_UI_RouteSamples";
import { createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
import { batched, createPipeEditor } from "./B05_Profile_UI_Drainage_Pipes";
import { followMovedPipe, PIPE_MATCH_M } from "../A00_Common/design/design_pipe_ids";
import { snapStructureStation } from "../common_util/common_util_structure_station";
import { createFacilityStore } from "./B05_Profile_UI_Drainage_Facility";
@@ -430,10 +430,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
* 옛 관이 살아난다(2026-08-02 사용자 보고). */
const resetPipes = (): Promise<void> => run(() => resetDetailPipePoints(projectId as string));
/** 세부유역을 다시 나눈다. 격자 해석은 하지 않으므로 즉시 끝난다.
/** 세부유역을 다시 나눈다(격자 해석 없음) — 같은 틱의 여러 부름은 한 번(342-3 · 옛 소수 관 옮김).
* 시설 종류·구간·옵션은 계산기에서 사라지므로 요청에 되붙인다. */
const analyze = (): Promise<void> =>
run(() => computeDetailBasins(projectId as string, pipeRequest()));
const analyze = batched(() => run(() => computeDetailBasins(projectId as string, pipeRequest())));
analyzeButton.addEventListener("click", () => void analyze());
deleteButton.addEventListener("click", () => void pipeEditor.deleteSelected());
@@ -60,6 +60,16 @@ export interface PipeEditor {
/** `onChange`는 화면 갱신용, `onCommit`은 배치가 실제로 바뀌었을 때(추가·삭제·이동 완료).
* 세부유역을 다시 나눠야 하는 시점이 정확히 `onCommit`이다(2026-08-01 사용자 지시).
* `onMove`는 끌기를 마친 관의 옛 · 새 측점 — `onCommit` 앞에 불러 시설 속성이 관을 따라감. */
/** 같은 틱에 여러 번 불러도 한 번만 돈다 — 옛 소수 관을 한꺼번에 옮겨도 세부유역 요청은 한 번(342-3). */
export function batched(work: () => Promise<void>): () => Promise<void> {
let queued: Promise<void> | null = null;
return () =>
(queued ??= Promise.resolve().then(() => {
queued = null;
return work();
}));
}
export function createPipeEditor(
onChange: () => void,
onCommit: () => void = () => {},
@@ -5,7 +5,7 @@
*
* 맞춤 규칙은 공용 `common_util_structure_station`(파이썬 짝) 한 벌. 구조물은 한 번에, 관은 하나씩
* 폼 수정과 같은 길(`onPipeUpdate`)로 옮긴다 — 계획고 편집 따라가기(213-2) · 시설 속성 이어받기가
* 손으로 옮길 때와 같다. 정본은 [저장]에서 나간다(알림 한 줄).
* 손으로 옮길 때와 같다 · 세부유역 요청은 한 번(배수유역 판 `batched` · 342-3). 정본은 [저장]에서 나간다(알림 한 줄).
* ========================================================================== */
import { showToast } from "@ui/ui_template_elements";
+42
View File
@@ -0,0 +1,42 @@
/* 342-3 B05 첫 열기 세부 유역 요청 한 번 헬퍼 — `B05_Profile_UI_Drainage_Pipes.ts` 의 `batched` 를
* 그 자리에서 트랜스파일해 Node 로 돌리고 부른 수를 JSON 으로 낸다(틀 = helper_281_1).
*/
const fs = require("fs");
const path = require("path");
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
const ROOT = path.join(__dirname, "..", "..");
const cache = new Map();
function loadTs(file) {
const full = path.resolve(file);
if (cache.has(full)) return cache.get(full);
const js = ts.transpileModule(fs.readFileSync(full, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(full, box.exports);
const localRequire = (request) => {
if (request.endsWith(".json")) return require(path.join(path.dirname(full), request));
if (request.startsWith(".")) return loadTs(path.join(path.dirname(full), `${request}.ts`));
if (request.startsWith("@ui/")) return { themeColor: () => "" };
return require(request);
};
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
cache.set(full, box.exports);
return box.exports;
}
(async () => {
const { batched } = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_Drainage_Pipes.ts"));
let runs = 0;
const analyze = batched(async () => {
runs += 1;
});
const first = Array.from({ length: 11 }, () => analyze()); // 옛 소수 관 11개 옮김
const samePromise = first.every((p) => p === first[0]);
await Promise.all(first);
const afterBurst = runs;
await analyze(); // 다음 조작은 다시 돈다
process.stdout.write(JSON.stringify({ afterBurst, samePromise, afterNext: runs }));
})();
@@ -0,0 +1,22 @@
"""342-3 B05 첫 열기 — 옛 소수 관 11개를 정수 m 로 옮겨도 세부 유역 요청은 한 번.
배수유역 판 `analyze` = `batched`(같은 틱 부름을 한 번으로) · Node 헬퍼가 TS 를 그 자리에서 돌린다.
"""
import json
import os
import subprocess
HERE = os.path.dirname(os.path.abspath(__file__))
def test_burst_runs_once_then_again():
proc = subprocess.run(
["node", os.path.join(HERE, "helper_342_3_batched.cjs")],
capture_output=True,
text=True,
timeout=60,
)
assert proc.returncode == 0, proc.stderr
result = json.loads(proc.stdout)
assert result == {"afterBurst": 1, "samePromise": True, "afterNext": 2}