From 2124bc85b92cd175151b562bdf7154802c103079 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 11:06:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05=20=EB=B0=B0=EC=88=98=EC=9C=A0=EC=97=AD)?= =?UTF-8?q?:=20342-3=20=EC=B2=AB=20=EC=97=B4=EA=B8=B0=20=EC=84=B8=EB=B6=80?= =?UTF-8?q?=20=EC=9C=A0=EC=97=AD=20=EC=9A=94=EC=B2=AD=2011=EB=B2=88=20?= =?UTF-8?q?=E2=86=92=201=EB=B2=88=20=E2=80=94=20=EA=B0=99=EC=9D=80=20?= =?UTF-8?q?=ED=8B=B1=20analyze=20=EB=A5=BC=20=ED=95=9C=20=EB=B2=88?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte --- B05_Profile/B05_Profile_UI_Drainage_Panel.ts | 7 ++-- B05_Profile/B05_Profile_UI_Drainage_Pipes.ts | 10 +++++ B05_Profile/B05_Profile_UI_Structures_Snap.ts | 2 +- resources/tester/helper_342_3_batched.cjs | 42 +++++++++++++++++++ .../tester/test_342_3_b05_detail_once.py | 22 ++++++++++ 5 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 resources/tester/helper_342_3_batched.cjs create mode 100644 resources/tester/test_342_3_b05_detail_once.py diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index 75a2d3ab5..5acb2fd98 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -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 => run(() => resetDetailPipePoints(projectId as string)); - /** 세부유역을 다시 나눈다. 격자 해석은 하지 않으므로 즉시 끝난다. + /** 세부유역을 다시 나눈다(격자 해석 없음) — 같은 틱의 여러 부름은 한 번(342-3 · 옛 소수 관 옮김). * 시설 종류·구간·옵션은 계산기에서 사라지므로 요청에 되붙인다. */ - const analyze = (): Promise => - 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()); diff --git a/B05_Profile/B05_Profile_UI_Drainage_Pipes.ts b/B05_Profile/B05_Profile_UI_Drainage_Pipes.ts index 12a99a0d6..34d56b1f3 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Pipes.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Pipes.ts @@ -60,6 +60,16 @@ export interface PipeEditor { /** `onChange`는 화면 갱신용, `onCommit`은 배치가 실제로 바뀌었을 때(추가·삭제·이동 완료). * 세부유역을 다시 나눠야 하는 시점이 정확히 `onCommit`이다(2026-08-01 사용자 지시). * `onMove`는 끌기를 마친 관의 옛 · 새 측점 — `onCommit` 앞에 불러 시설 속성이 관을 따라감. */ +/** 같은 틱에 여러 번 불러도 한 번만 돈다 — 옛 소수 관을 한꺼번에 옮겨도 세부유역 요청은 한 번(342-3). */ +export function batched(work: () => Promise): () => Promise { + let queued: Promise | null = null; + return () => + (queued ??= Promise.resolve().then(() => { + queued = null; + return work(); + })); +} + export function createPipeEditor( onChange: () => void, onCommit: () => void = () => {}, diff --git a/B05_Profile/B05_Profile_UI_Structures_Snap.ts b/B05_Profile/B05_Profile_UI_Structures_Snap.ts index d42ace5a7..29ac14a58 100644 --- a/B05_Profile/B05_Profile_UI_Structures_Snap.ts +++ b/B05_Profile/B05_Profile_UI_Structures_Snap.ts @@ -5,7 +5,7 @@ * * 맞춤 규칙은 공용 `common_util_structure_station`(파이썬 짝) 한 벌. 구조물은 한 번에, 관은 하나씩 * 폼 수정과 같은 길(`onPipeUpdate`)로 옮긴다 — 계획고 편집 따라가기(213-2) · 시설 속성 이어받기가 - * 손으로 옮길 때와 같다. 정본은 [저장]에서 나간다(알림 한 줄). + * 손으로 옮길 때와 같다 · 세부유역 요청은 한 번(배수유역 판 `batched` · 342-3). 정본은 [저장]에서 나간다(알림 한 줄). * ========================================================================== */ import { showToast } from "@ui/ui_template_elements"; diff --git a/resources/tester/helper_342_3_batched.cjs b/resources/tester/helper_342_3_batched.cjs new file mode 100644 index 000000000..1190cdc09 --- /dev/null +++ b/resources/tester/helper_342_3_batched.cjs @@ -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 })); +})(); diff --git a/resources/tester/test_342_3_b05_detail_once.py b/resources/tester/test_342_3_b05_detail_once.py new file mode 100644 index 000000000..1558caec7 --- /dev/null +++ b/resources/tester/test_342_3_b05_detail_once.py @@ -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}