From 92d952d8acd04f89dc7d7a764e0b116700252226 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 28 Oct 2026 17:24:07 +0900 Subject: [PATCH] =?UTF-8?q?fix(B04=20=C2=B7=20B06):=20=EC=9C=A0=EC=9E=85?= =?UTF-8?q?=20=EC=BA=90=EC=8B=9C=20=EC=83=81=ED=95=9C=20=EB=84=B7=20=C2=B7?= =?UTF-8?q?=20=ED=9D=99=EB=A7=89=EC=9D=B4=20=EB=B3=B4=EB=82=B8=20=EA=B0=92?= =?UTF-8?q?=20=EC=97=B4=EC=87=A0=EC=97=90=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=C2=B7=20=EB=85=B8=EC=84=A0=20(PLAN=20572-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - B04 유입 배열 캐시 = 최근 프로젝트 넷만(_DETAIL_CACHE 꼴) · 서버 켠 뒤 본 프로젝트마다 쌓이던 것 - B06 soilSent 열쇠 = {프로젝트}:{노선}:측점:역할 · 다른 프로젝트 · 노선 같은 측점에 같은 값 안 먹던 흠 - 시험 test_572_2_inflow_cache 새로 · test_54_10 다른 프로젝트 같은 값 · helper_387_4b scope 칸 Co-Authored-By: Claude Opus 5.5 --- A00_Common/design/design_view_cross.ts | 1 + .../B04_PreProcess_Router_Inflow.py | 12 ++++++- .../B06_Section_UI_Page_Pipe_Options.ts | 6 ++-- .../tester/helper_387_4b_pipe_options.cjs | 5 ++- resources/tester/helper_54_10_soil_tiers.cjs | 13 ++++++++ resources/tester/test_54_10_soil_tiers.py | 8 +++++ resources/tester/test_572_2_inflow_cache.py | 31 +++++++++++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 resources/tester/test_572_2_inflow_cache.py diff --git a/A00_Common/design/design_view_cross.ts b/A00_Common/design/design_view_cross.ts index a90c8a790..2180a86df 100644 --- a/A00_Common/design/design_view_cross.ts +++ b/A00_Common/design/design_view_cross.ts @@ -354,6 +354,7 @@ export async function mountCrossView( // 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리). const pipeOptionsContext: PipeOptionsContext = { detail: () => sectionDetail, + scope: () => `${projectId}:${currentRouteId}`, ford: stationControls.ford, box: stationControls.box, revetOffset: stationControls.revetOffset, diff --git a/B04_PreProcess/B04_PreProcess_Router_Inflow.py b/B04_PreProcess/B04_PreProcess_Router_Inflow.py index 98ba38f6c..1acd00c9e 100644 --- a/B04_PreProcess/B04_PreProcess_Router_Inflow.py +++ b/B04_PreProcess/B04_PreProcess_Router_Inflow.py @@ -34,7 +34,9 @@ router = APIRouter(prefix="/api/projects", tags=["B04 Surface Inflow"]) # 읽어 둔 배열을 프로세스 메모리에 들고 있는다. 마커를 누를 때마다 수십 MB npz를 다시 푸는 # 것을 막는다. 파일이 새로 쓰이면(재분석) mtime이 달라져 자동으로 버려진다. +# 프로젝트마다 한 벌(≈ 11 MB) — 최근 쓴 것 넷만 남김. _CACHE: dict[str, tuple[float, dict[str, Any]]] = {} +_CACHE_SIZE = 4 # 유입 셀 외곽선은 조각이 많을 수 있다. 화면에서 읽을 수 있는 수준까지만 보낸다. _MAX_RINGS = 40 @@ -43,6 +45,13 @@ def _routing_path(stored_path: str) -> Path: return drainage_dir(stored_path) / f"{STAGES['road_routing']}_road_routing.npz" +def _remember(key: str, value: tuple[float, dict[str, Any]]) -> None: + _CACHE.pop(key, None) + _CACHE[key] = value + while len(_CACHE) > _CACHE_SIZE: + _CACHE.pop(next(iter(_CACHE))) + + def _load_routing(stored_path: str) -> dict[str, Any] | None: """`03_road_routing.npz`를 읽어 필요한 배열만 남긴다(mtime 기준 캐시).""" path = _routing_path(stored_path) @@ -51,6 +60,7 @@ def _load_routing(stored_path: str) -> dict[str, Any] | None: stamp = path.stat().st_mtime cached = _CACHE.get(stored_path) if cached is not None and cached[0] == stamp: + _remember(stored_path, cached) return cached[1] with np.load(path, allow_pickle=False) as data: loaded = { @@ -65,7 +75,7 @@ def _load_routing(stored_path: str) -> dict[str, Any] | None: "road_chainage": data["road_chainage"], "path_length": data["path_length"], } - _CACHE[stored_path] = (stamp, loaded) + _remember(stored_path, (stamp, loaded)) logger.info("배수유역: 도로 귀속 배열을 읽었습니다 (%s).", path.name) return loaded diff --git a/B06_Section/B06_Section_UI_Page_Pipe_Options.ts b/B06_Section/B06_Section_UI_Page_Pipe_Options.ts index 722041cf7..a09b9df04 100644 --- a/B06_Section/B06_Section_UI_Page_Pipe_Options.ts +++ b/B06_Section/B06_Section_UI_Page_Pipe_Options.ts @@ -24,6 +24,8 @@ type StationControls = ReturnType; /** 폼 → 캐시 반영에 필요한 페이지 창구만 모은 것. */ export interface PipeOptionsContext { detail: () => SectionDetailResponse | null; + /** 보낸 값 기억의 앞머리 = `{프로젝트}:{노선}` — 다른 프로젝트 · 노선의 같은 측점 값과 섞이지 않게(572-2). */ + scope: () => string; ford: StationControls["ford"]; box: StationControls["box"]; revetOffset: StationControls["revetOffset"]; @@ -213,7 +215,7 @@ function syncBodyGuard( return !!guard; } -/** 흙막이 컨테이너가 마지막으로 보낸 높이 · 형태(`측점:extra{n}`) — 기하가 다시 그릴 때마다 키를 지형에 +/** 흙막이 컨테이너가 마지막으로 보낸 높이 · 형태(`{프로젝트}:{노선}:측점:extra{n}`) — 기하가 다시 그릴 때마다 키를 지형에 * 맞춰 고쳐 되받으므로(syncApplied) 되받은 값과 견주면 안 바꾼 단도 다시 보냄(54-10). */ const soilSent = new Map(); @@ -239,7 +241,7 @@ function applySoilTiers( const clipped: Record = {}; for (let n = 1; n <= (ctx.soilTiers?.(owner.chainage_m) ?? 0); n++) { const role = `extra${n - 1}` as const; - const key = `${owner.chainage_m.toFixed(2)}:${role}`; + const key = `${ctx.scope()}:${owner.chainage_m.toFixed(2)}:${role}`; const form = patch[`soil_add_${n}_form`]; const want = { h: value(n, "height") ?? blank, diff --git a/resources/tester/helper_387_4b_pipe_options.cjs b/resources/tester/helper_387_4b_pipe_options.cjs index 51d877fbf..1985dcd97 100644 --- a/resources/tester/helper_387_4b_pipe_options.cjs +++ b/resources/tester/helper_387_4b_pipe_options.cjs @@ -43,7 +43,9 @@ function memoryStorage() { globalThis.window = { sessionStorage: memoryStorage(), localStorage: memoryStorage() }; const B06 = (f) => path.join(ROOT, "B06_Section", `${f}.ts`); -const { structureAreaRows, computeStoredLayouts } = loadTs(B06("logic/B06_Section_Structure_Layouts")); +const { structureAreaRows, computeStoredLayouts } = loadTs( + B06("logic/B06_Section_Structure_Layouts"), +); const { layPipeOptions } = loadTs(B06("B06_Section_Section_Adjusts")); const { applyPipeOptionsToCache } = loadTs(B06("B06_Section_UI_Page_Pipe_Options")); const { ZERO_ADJUST } = loadTs(B06("logic/B06_Section_Cross_Culvert_Types")); @@ -68,6 +70,7 @@ function b06(sections, options, extra = 0) { const round1 = (v) => Math.round(v * 10) / 10; const ctx = { detail: () => ({ cross_sections: sections }), + scope: () => "p:1", ford: {}, box: {}, revetOffset: { diff --git a/resources/tester/helper_54_10_soil_tiers.cjs b/resources/tester/helper_54_10_soil_tiers.cjs index 22f7f2c14..93dc7c77c 100644 --- a/resources/tester/helper_54_10_soil_tiers.cjs +++ b/resources/tester/helper_54_10_soil_tiers.cjs @@ -121,8 +121,10 @@ const section = { chainage_m: 100, culvert: { diameter_m: 1, hidden_pipe: false, inlet: {}, outlet: { revet_form: "돌쌓기(찰)" } }, }; +let scope = "X:1"; const ctx = { detail: () => ({ cross_sections: [section] }), + scope: () => scope, ford: {}, box: {}, revetOffset: { @@ -164,6 +166,17 @@ applyPipeOptionsToCache(ctx, 100, { soil_add_2_height: 2.5, }); out.same = { updates: updates.splice(0), tiers: tiers.splice(0) }; +// 다른 프로젝트 · 노선의 같은 측점에 같은 값 — 보낸 값 기억이 섞이지 않아 보냄(572-2) +scope = "Y:1"; +for (const role of Object.keys(adjusts)) delete adjusts[role]; +applyPipeOptionsToCache(ctx, 100, { + soil_add_1_form: "콘크리트", + soil_add_1_height: 1.8, + soil_add_2_form: "돌쌓기(메)", + soil_add_2_height: 2.5, +}); +out.otherScope = updates.splice(0); +scope = "X:1"; // 그림이 안 서는 높이(0 이하)만 하한 0.1 에서 멈춰 폼에 되돌림 · 토스트 없음(54-51) applyPipeOptionsToCache(ctx, 100, { soil_add_1_form: "콘크리트", diff --git a/resources/tester/test_54_10_soil_tiers.py b/resources/tester/test_54_10_soil_tiers.py index ec4310e11..298d2f215 100644 --- a/resources/tester/test_54_10_soil_tiers.py +++ b/resources/tester/test_54_10_soil_tiers.py @@ -65,6 +65,14 @@ def test_같은_값은_다시_그리지_않음(out) -> None: assert out["same"]["tiers"] == [] +def test_다른_프로젝트_노선_같은_값은_보냄(out) -> None: + """572-2 — 보낸 값 기억 열쇠에 프로젝트 · 노선 · 옛 열쇠(측점만)면 Y 에서 건너뜀.""" + assert extras(out["otherScope"]) == [ + {"role": "extra0", "h": 1.8, "m": "콘크리트"}, + {"role": "extra1", "h": 2.5, "m": "돌쌓기(메)"}, + ] + + def test_비운_칸은_자동_구간값은_그대로(out) -> None: assert extras(out["cleared"]["updates"]) == [ {"role": "extra0", "h": None, "m": None}, diff --git a/resources/tester/test_572_2_inflow_cache.py b/resources/tester/test_572_2_inflow_cache.py new file mode 100644 index 000000000..c198aa13a --- /dev/null +++ b/resources/tester/test_572_2_inflow_cache.py @@ -0,0 +1,31 @@ +"""572-2 B04 유입 캐시 상한 — 프로젝트 넷만 남김 · 다시 쓴 것은 뒤로.""" + +import numpy as np + +from B04_PreProcess import B04_PreProcess_Router_Inflow as inflow + + +def _write(path): + np.savez( + path, + x_min=0.0, + y_max=0.0, + cell_m=1.0, + n_rows=1, + n_cols=1, + road_slot=np.zeros(1), + road_chainage=np.zeros(1), + path_length=np.zeros(1), + ) + + +def test_cache_keeps_four_recent(tmp_path, monkeypatch): + monkeypatch.setattr(inflow, "_routing_path", lambda stored: tmp_path / f"{stored}.npz") + inflow._CACHE.clear() + for name in "abcde": + _write(tmp_path / f"{name}.npz") + for name in "abcd": + inflow._load_routing(name) + inflow._load_routing("a") # 다시 씀 → 맨 뒤 + inflow._load_routing("e") + assert list(inflow._CACHE) == ["c", "d", "a", "e"]