Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1

This commit is contained in:
2026-10-22 21:14:57 +09:00
7 changed files with 164 additions and 10 deletions
@@ -25,6 +25,18 @@ export function fordHeightM(designFlowM3s: number | null, widthM: number): numbe
return section ? Math.ceil(section.depthM * 100 - 1e-9) / 100 : null;
}
/** 배수유역도가 낡은 동안(요구 328-3) 설계유량 = 옛 유역 측점 값이라 「낡음」으로 가림 · 높이도 안 채움.
* 판정은 배수유역 판 하나(`B05_Profile_UI_Drainage_Live` 가 부름) · 바뀌면 열린 폼을 다시 그림. */
let drainageStale = false;
// ponytail: 폼마다 하나씩 쌓임(페이지 다시 열 때만 · 떨어진 폼 다시 그림은 헛일) — 많아지면 폼 닫을 때 뺌
const staleRepaints = new Set<() => void>();
export const FORD_FLOW_STALE_HINT = "설계유량 낡음 — 배수유역도 [업데이트] 뒤 셈";
export function setDrainageStale(next: boolean): void {
if (drainageStale === next) return;
drainageStale = next;
staleRepaints.forEach((repaint) => repaint());
}
/**
* 월류 폭 ↔ 높이 연동 — 세월교 · 물넘이 포장 한 벌(52-4 · 65-4 · 65-5 · 287-3).
* · 폭을 바꾸면 높이 = 필요 수심 · 높이를 비우거나 그 아래로 넣으면 필요 수심으로.
@@ -41,7 +53,8 @@ export function overflowSync(
let pending = false;
const values = () => form().state()[box]?.[0] ?? {};
const width = () => Number(values().width ?? "");
const need = (): number | null => fordHeightM(flow(), width());
const need = (): number | null => (drainageStale ? null : fordHeightM(flow(), width()));
staleRepaints.add(() => form()?.refresh());
return {
arm: (fresh: boolean): void => {
pending = fresh;
@@ -67,7 +80,7 @@ export function overflowSync(
if (c.id !== box) return [];
const hint = document.createElement("p");
hint.className = "b05-structure__flow-hint";
hint.textContent = fordHeightHint(flow(), width());
hint.textContent = drainageStale ? FORD_FLOW_STALE_HINT : fordHeightHint(flow(), width());
return [hint];
},
};
@@ -20,6 +20,7 @@
import { API_BASE_URL } from "@config/config_frontend";
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import { createProgressCircle } from "@ui/ui_template_progress";
import { setDrainageStale } from "../A00_Common/design/design_structure_ford_calc";
import type { StageFn } from "../A00_Common/design/logic/design_pipeline_types";
import {
computeDetailBasins,
@@ -124,6 +125,7 @@ export function createDrainageLive(deps: DrainageLiveDeps): DrainageLive {
const paint = (): void => {
deps.root.classList.toggle("is-stale", stale);
setDrainageStale(stale); // 관 카드 설계유량 가림(요구 328-3)
update.classList.toggle("is-pending", stale);
update.disabled = inflight !== null;
update.textContent = inflight ? "계산 중…" : UPDATE_LABEL;
+3 -2
View File
@@ -330,8 +330,9 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
id: pipe.id,
})),
);
// 시설 확장 정보(종류·구간·옵션)는 응답이 정본 — 통째로 다시 채운다(기다리던 월류 높이를 먼저 얹음).
const fordFilled = fillFordHeights(response.pipe_points, response.basins, fordWaits());
// 시설 확장 정보는 응답이 정본 — 통째로 다시 채운다(기다리던 월류 높이 먼저 · 낡은 답이면 뒤로 · 328-3).
const fordFilled =
!live.stale && fillFordHeights(response.pipe_points, response.basins, fordWaits());
facilityStore.replaceFromResponse(response.pipe_points);
if (fordFilled) rememberPipes();
if (saved?.response === response) saved.request = JSON.stringify(pipeRequest());
@@ -118,6 +118,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
? inside.reduce((best, at) => (Math.abs(at - anchor) < Math.abs(best - anchor) ? at : best))
: anchor;
};
/** 담당 유역 설계유량 — 배수유역도 지금 답(관 보고 `shared.lastPipes`)이 먼저 · 아직 없으면 저장 유역
* ([업데이트] 뒤 B06 폼도 새 값 · 낡은 동안 가림은 `design_structure_ford_calc` · 요구 328-3). */
const flowAt = (
chainageM: number,
basins: ReadonlyArray<{ chainage_m: number; design_flow_m3s?: number | null }> = [],
): number | null => {
const near = (entry: { chainage_m: number }) =>
Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M;
const live = deps.shared.lastPipes;
return (live ? live.find(near) : basins.find(near))?.design_flow_m3s ?? null;
};
/** 종단 알약 레인에 올릴 목록 — 구조물 정본 + 관 정본(가상 구조물). 알약 · 측점선 · 관 id 장부 = 페이지
* 시설 줄 한 벌(462 · 숨은 B05 평면 마커도 따라감) · `carryEdits` = 사용자 고침(불러옴은 false). */
const pushMarks = (carryEdits = true): void => {
@@ -353,10 +364,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
// 담당 유역의 설계유량 — 물넘이·세월교 개략 단면(필요 수심 = 월류 높이
// 자동값)이 이 값에서 나온다. B05와 같은 규칙으로 유역을 측점으로 짝짓는다
// (2026-08-30 사용자: 월류 폭을 넣어도 높이가 자동 계산되지 않았다).
design_flow_m3s:
pipeResponse.basins.find(
(basin) => Math.abs(basin.chainage_m - pipe.chainage_m) < PIPE_MATCH_M,
)?.design_flow_m3s ?? null,
design_flow_m3s: flowAt(pipe.chainage_m, pipeResponse.basins),
}));
// 옛 소수 관 자리 = 정수 m 초안(요구 167 · B05 `snapOldStations` 와 같은 셈) — B06 을 바로 열어도
// 카드 · 측점이 B05 를 거친 것과 같음 · 정본은 [저장]에서
@@ -549,9 +557,16 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
entry.options = { ...entry.options, ford_height_m: height };
return true;
});
if (!filled.length) return;
// 설계유량도 새 답으로(요구 328-3)
const flowed = pipeFacilities.filter((entry) => {
const flow = flowAt(entry.chainage_m);
if (!deps.shared.lastPipes || flow === (entry.design_flow_m3s ?? null)) return false;
entry.design_flow_m3s = flow;
return true;
});
if (!filled.length && !flowed.length) return;
section.setPipeFacilities(pipeFacilities);
const open = filled.find((entry) => entry.chainage_m === currentChainageM);
const open = [...filled, ...flowed].find((entry) => entry.chainage_m === currentChainageM);
if (open) section.selectPipeByChainage(open.chainage_m);
},
releasePipe: () => {
@@ -191,6 +191,7 @@ export function createSectionProfile(
p.facility,
p.options?.pipe_diameter_mm ?? null,
p.options?.ford_height_m ?? null,
p.design_flow_m3s ?? null, // [업데이트] 뒤 설계유량(요구 328-3)
]),
);
const changed = pipeSignature !== null && signature !== pipeSignature;
@@ -0,0 +1,62 @@
/* 514-2 낡은 유량 가림 헬퍼 — 진짜 `design_structure_ford_calc.ts` 를 Node 로 돌려 JSON 한 벌로(요구 328-3). */
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();
const load = (file) => {
if (cache.has(file)) return cache.get(file);
const js = ts.transpileModule(fs.readFileSync(file, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const box = { exports: {} };
cache.set(file, box.exports);
const near = (m) => {
if (m.startsWith("@config/")) return load(path.join(ROOT, "config", `${m.slice(8)}.ts`));
if (m.startsWith(".")) return load(path.join(path.dirname(file), `${m}.ts`));
return new Proxy({}, { get: () => () => {} });
};
new Function("exports", "module", "require", js)(box.exports, box, near);
return box.exports;
};
const calc = load(path.join(ROOT, "A00_Common", "design", "design_structure_ford_calc.ts"));
globalThis.document = { createElement: () => ({ textContent: "", className: "" }) };
const state = { ford: [{ width: "10", height: "" }] };
let refreshed = 0;
const form = {
state: () => state,
setValue: (c, i, v) => (state[c][0][i] = v),
refresh: () => (refreshed += 1),
};
const sync = calc.overflowSync(
() => form,
"ford",
() => 2.54,
() => undefined,
);
const hint = () => sync.fields({ id: "ford" })[0].textContent;
const edit = (item, value) => {
state.ford[0][item] = value;
sync.onChange({ id: "ford" }, { id: item });
return state.ford[0].height;
};
const fresh = { hint: hint(), height: edit("width", "10") };
calc.setDrainageStale(true);
state.ford[0].height = "";
const stale = { refreshed, hint: hint(), height: edit("width", "8") };
sync.arm(true);
sync.fill();
stale.afterFill = state.ford[0].height;
let before = refreshed;
calc.setDrainageStale(true); // 같은 값 = 다시 안 그림
stale.repaintSame = refreshed - before;
before = refreshed;
calc.setDrainageStale(false);
const back = { repaint: refreshed - before, hint: hint() };
sync.fill();
back.afterFill = state.ford[0].height;
process.stdout.write(JSON.stringify({ fresh, stale, back }));
+60
View File
@@ -0,0 +1,60 @@
"""514-2 배수유역도 낡은 동안 관 카드 설계유량 「낡음」 가림(요구 328-3).
판정 한 곳 = 배수유역 판 `live.stale` → `setDrainageStale` · 세월교 · 물넘이 칸 안내 · 높이 자동값이 그것을 봄.
"""
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
HELPER = Path(__file__).with_name("helper_514_2_stale_flow.cjs")
STALE_HINT = "설계유량 낡음 — 배수유역도 [업데이트] 뒤 셈"
def _read(*parts: str) -> str:
return (ROOT.joinpath(*parts)).read_text(encoding="utf-8")
@pytest.fixture(scope="module")
def run() -> dict:
out = subprocess.run(["node", str(HELPER)], capture_output=True, check=True, cwd=ROOT).stdout
return json.loads(out.decode("utf-8"))
def test_안_낡으면_값(run):
assert run["fresh"]["hint"].startswith("필요 수심 ")
assert run["fresh"]["height"] != ""
def test_낡으면_가림_높이_안_채움(run):
stale = run["stale"]
assert stale["refreshed"] == 1 # 열린 폼 다시 그림
assert stale["hint"] == STALE_HINT
assert stale["height"] == "" and stale["afterFill"] == ""
assert stale["repaintSame"] == 0
def test_업데이트_뒤_값(run):
back = run["back"]
assert back["repaint"] == 1
assert back["hint"].startswith("필요 수심 ")
assert back["afterFill"] != "" # 기다리던 새 시설 높이 = 그때 채움
def test_판정은_배수유역_판_한_곳():
live = _read("B05_Profile", "B05_Profile_UI_Drainage_Live.ts")
assert "setDrainageStale(stale);" in live
panel = _read("B05_Profile", "B05_Profile_UI_Drainage_Panel.ts")
assert "!live.stale && fillFordHeights(" in " ".join(panel.split())
def test_B06_폼도_업데이트_뒤_새_유량():
b06 = " ".join(_read("B06_Section", "B06_Section_UI_Page_Structures_Panel.ts").split())
assert "design_flow_m3s: flowAt(pipe.chainage_m, pipeResponse.basins)," in b06
assert "const live = deps.shared.lastPipes;" in b06
assert "entry.design_flow_m3s = flow;" in b06 # adoptFordHeights 가 새 답 유량을 받음
view = _read("B06_Section", "B06_Section_UI_Section_View_Profile.ts")
assert "p.design_flow_m3s ?? null," in view # 유량만 바뀌어도 pipesChanged