fix(b05): 도수로/돌붙임 면적 = 늘 손 입력 — 길이 · 월류 폭으로 면적 자동 셈(syncChuteArea · chuteAreaM2) 걷음 · 물넘이 · 세월교 폼 onChange 원래 흐름 상자로 · 길이 = 그림 전용 (301-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TteK8mfYZetk5P3bEZkyVZ
This commit is contained in:
2026-10-13 07:32:34 +09:00
co-authored by Claude Opus 5.5
parent 9a035bc188
commit d7ba07832c
6 changed files with 47 additions and 79 deletions
@@ -15,10 +15,8 @@
* (같은 풀이 · n · S 를 받음) · 거울 시험 test_73_11_b08_hydro_box_ford.
* ========================================================================== */
import { FORD_PAVEMENT_DEFAULT_WIDTH_M } from "@config/config_frontend";
import { fordSection } from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
import type { SpecForm } from "./design_structure_form";
import { CHUTE_ITEMS } from "./design_structure_guard";
import type { SpecContainer, SpecItem } from "./design_structure_spec";
/** 월류 폭(m) → 필요 월류 높이(m) · 0.01 m 올림. 유량 · 폭이 없으면 null. */
@@ -81,39 +79,3 @@ export function fordHeightHint(designFlowM3s: number | null, widthM: number): st
if (need !== null) return `필요 수심 ${need.toFixed(2)} 이상`;
return designFlowM3s ? "월류 폭을 넣으면 셈" : "설계유량 없음 — 수기 입력";
}
/** 도수로 · 돌붙임 면적(㎡ · 287-1) = 폭 × 길이 · 0.01 반올림. */
export function chuteAreaM2(widthM: number, lengthM: number): number {
return Math.round(widthM * lengthM * 100) / 100;
}
/**
* 물넘이 포장 · 세월교 도수로/돌붙임 길이 ↔ 면적(287-1 · 294-4) — 길이를 넣으면 면적 = 폭 × 길이(폭 = 띠가
* 도로 방향으로 걸치는 월류 폭 · 비면 물넘이 기본 폭 · 배관 · 박스는 폭 정의 없어 자동 안 함) · 길이를 비우면 면적 = 칸 기본값 · 월류 폭을 바꾸면 길이가 있을 때만
* 다시 셈. 길이는 그림(사면 따라 그 길이) · 면적은 수량(B08 돌붙임)이 씀.
*/
export function syncChuteArea(
form: Pick<SpecForm, "state" | "setValue">,
c: SpecContainer,
item: SpecItem,
/** 도수로 상자 · 월류 폭 상자 — 물넘이 `chute` · `slope` · 세월교 `chute_add` · `ford`(294-4). */
chuteBox = "chute",
widthBox = "slope",
): void {
const byLength = c.id === chuteBox && item.id === "length_m";
if (!byLength && !(c.id === widthBox && item.id === "width")) return;
const chute = form.state()[chuteBox]?.[0];
if (!chute) return;
const length = Number(chute.length_m ?? "");
if (!(chute.length_m ?? "").trim() || !(length > 0)) {
if (byLength)
form.setValue(chuteBox, "area", CHUTE_ITEMS.find((i) => i.id === "area")?.default ?? "");
return;
}
const width = Number(form.state()[widthBox]?.[0]?.width ?? "");
form.setValue(
chuteBox,
"area",
String(chuteAreaM2(width > 0 ? width : FORD_PAVEMENT_DEFAULT_WIDTH_M, length)),
);
}
@@ -64,7 +64,7 @@ import {
boxOptionsIn,
} from "../A00_Common/design/design_structure_box";
import { FORD_SPEC, withLegacyGuards } from "../A00_Common/design/design_structure_ford";
import { overflowSync, syncChuteArea } from "../A00_Common/design/design_structure_ford_calc";
import { overflowSync } from "../A00_Common/design/design_structure_ford_calc";
import {
pipeDrawn,
pipeExtraCount,
@@ -384,10 +384,7 @@ export function createFacilityOptionsForm(
);
// 유출 흙막이 단 수 = 흙막이 상자 수(관보호공과 같은 방향 · 「+ 흙막이」 없음 · 90-1)
const fordForm: SpecForm = createSpecForm(specLinks, {
onChange: (c, item) => (
syncChuteArea(fordForm, c, item, "chute_add", "ford"),
fordFlowBox.onChange(c, item)
),
onChange: fordFlowBox.onChange,
fields: (c, values) => [...guardFields(c, values), ...fordFlowBox.fields(c)],
count: guardSoilCount(
() => chainage,
@@ -477,7 +474,7 @@ export function createFacilityOptionsForm(
emit,
);
const paveForm: SpecForm = createSpecForm(specLinks, {
onChange: (c, item) => (syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)),
onChange: paveFlowBox.onChange,
fields: paveFlowBox.fields,
});
+29 -16
View File
@@ -26,16 +26,6 @@ const load = (file, stubs = {}) => {
const D = (f) => path.join(ROOT, "A00_Common", "design", f);
const calc = load(D("design_structure_ford_calc.ts"));
/** 폼 흉내 — 상태 · setValue 만. */
const formOf = (slope, chute) => {
const state = { slope: [{ ...slope }], chute: [{ ...chute }] };
return { state: () => state, setValue: (c, i, v) => (state[c][0][i] = v) };
};
const change = (form, c, i) => {
calc.syncChuteArea(form, { id: c }, { id: i });
return form.state().chute[0].area;
};
// 287-2 · 287-4 그림 — DOM 흉내(속성 · 자식만)
class Node {
constructor(tag) {
@@ -209,6 +199,32 @@ const overflowRun = (flowValue) => {
};
};
// 301-1 폼 훅 그대로(B05 물넘이 paveForm · 세월교 fordForm onChange = overflowSync onChange)
const areaRun = (widthBox, chuteBox) => {
const state = { [widthBox]: [{ width: "6" }], [chuteBox]: [{ length_m: "", area: "17" }] };
const form = {
state: () => state,
setValue: (c, i, v) => (state[c][0][i] = v),
refresh: () => undefined,
};
const sync = calc.overflowSync(
() => form,
widthBox,
() => 14.5776,
() => undefined,
);
const edit = (box, item, value) => {
state[box][0][item] = value;
sync.onChange({ id: box }, { id: item });
return state[chuteBox][0].area;
};
return [
edit(chuteBox, "length_m", "4"),
edit(widthBox, "width", "8"),
edit(chuteBox, "length_m", ""),
];
};
const out = {
overflowFlow: overflowRun(14.5776),
overflowNoFlow: overflowRun(null),
@@ -235,11 +251,8 @@ const out = {
paveSlopeItems: load(D("design_structure_ford_pavement.ts"))
.FORD_PAVEMENT_SPEC.containers.find((c) => c.id === "slope")
.items.map((i) => i.id),
// 287-1 길이 넣음 = 월류 폭 × 길이 · 비움 = 기본값 · 폭 바꿈 = 길이 있을 때만 다시 셈 · 폭 비면 기본 폭
areaByLength: change(formOf({ width: "6" }, { length_m: "4", area: "30" }), "chute", "length_m"),
areaCleared: change(formOf({ width: "6" }, { length_m: "", area: "24" }), "chute", "length_m"),
areaByWidth: change(formOf({ width: "8" }, { length_m: "2.5", area: "15" }), "slope", "width"),
areaWidthNoLength: change(formOf({ width: "8" }, { length_m: "", area: "17" }), "slope", "width"),
areaNoWidth: change(formOf({ width: "" }, { length_m: "3", area: "30" }), "chute", "length_m"),
// 301-1 면적 = 늘 손 입력 — 길이 · 월류 폭을 바꾸거나 길이를 비워도 면적 그대로(물넘이 · 세월교 폼 훅)
areaKeptPave: areaRun("slope", "chute"),
areaKeptFord: areaRun("ford", "chute_add"),
};
process.stdout.write(JSON.stringify(out));
@@ -64,10 +64,6 @@ const pts = (lines) =>
lines.map((l) => l.points.map((p) => [+p.offset.toFixed(4), +p.elevation.toFixed(4)]));
const dipGround = hatch.groundLine(input.dipSamples);
const fillGround = hatch.groundLine(input.fillSamples);
const calc = load(path.join(ROOT, "A00_Common", "design", "design_structure_ford_calc.ts"));
const formOf = (state) => ({ state: () => state, setValue: (c, i, v) => (state[c][0][i] = v) });
const fordState = { ford: [{ width: "8" }], chute_add: [{ length_m: "2.5", area: "30" }] };
calc.syncChuteArea(formOf(fordState), { id: "chute_add" }, { id: "length_m" }, "chute_add", "ford");
const guard = load(path.join(ROOT, "A00_Common", "design", "design_structure_guard.ts"));
const fordPave = load(path.join(ROOT, "A00_Common", "design", "design_structure_ford_pavement.ts"));
@@ -85,7 +81,6 @@ const out = {
fillLine: pave.outletFillLine(input.fillDesign, fillGround),
fillLineOld: pave.outletFillLine(input.fillDesign),
fillChuteLength: pave.fordChuteLine(input.fillDesign, { length_m: 1 }, fillGround),
fordArea: fordState.chute_add[0].area,
chuteItems: guard.CHUTE_ITEMS.map((i) => i.id),
fordPaveChuteSame:
fordPave.FORD_PAVEMENT_SPEC.containers.find((c) => c.id === "chute").items ===
@@ -1,8 +1,8 @@
"""287 물넘이포장 후속(요구 183-2 · 4 · 6 · 7 · 8 · 9).
못박는 것
- 287-1 도수로/돌붙임 길이 ↔ 면적 — 길이 넣음 = 월류 폭 × 길이 · 비움 = 칸 기본값 · 월류 폭을 바꾸면
길이가 있을 때만 다시 셈 · 폭이 비면 물넘이 기본 폭.
- 301-1 도수로/돌붙임 면적 = 늘 손 입력(요구 192-2 · 287-1 자동 셈 걷음) — 길이 · 월류 폭을 바꾸거나
길이를 비워도 면적 그대로 · 길이 = 그림 전용(294-4).
- 287-3 월류 폭 → 높이 자동 = 세월교 한 벌(`fordHeightM` · 폼 `overflow(…, "slope")`) · 유량 없으면 손 입력.
- 287-5 물넘이포장 형상 누름 = 다른 시설과 같은 고름 창구(`toggleRevet("pipe")` → 폼 · 알약 · 측점선) ·
판정면은 보라 손잡이 아래(요구 185 ①) · 끌기(3 px 넘게 움직임) = 고름 아님(②) · 노면공 포장층
@@ -51,17 +51,19 @@ def ts() -> dict:
return json.loads(proc.stdout)
def test_도수로_길이_면적(ts):
assert ts["areaByLength"] == "24" # 6 m × 4 m
assert ts["areaCleared"] == "30" # 길이 비움 = 기본값
assert ts["areaByWidth"] == "20" # 폭 8 m × 2.5 m
assert ts["areaWidthNoLength"] == "17" # 길이 없으면 폭을 바꿔도 그대로
assert ts["areaNoWidth"] == "30" # 폭 비움 = 기본 폭 10 m × 3 m
def test_도수로_면적은_손_입력_그대로(ts):
# 길이 4 넣음 · 월류 폭 8 · 길이 비움 — 면적은 손 입력 17 그대로
assert ts["areaKeptPave"] == ["17", "17", "17"]
assert ts["areaKeptFord"] == ["17", "17", "17"]
def test_물넘이_폼이_길이_면적을_잇는다():
def test_폼이_길이_면적을_잇지_않는다():
form = _read("B05_Profile", "B05_Profile_UI_Drainage_Facility.ts")
assert "syncChuteArea(paveForm, c, item), paveFlowBox.onChange(c, item)" in form
assert "syncChuteArea" not in form
assert "onChange: fordFlowBox.onChange," in form
assert "onChange: paveFlowBox.onChange," in form
calc = _read("A00_Common", "design", "design_structure_ford_calc.ts")
assert "syncChuteArea" not in calc and "chuteAreaM2" not in calc
def _pts(text: str) -> list[float]:
@@ -7,8 +7,8 @@
연장」) · 그림 = 콘크리트(포장층)가 늘인 끝까지.
- 294-4 도수로/돌붙임 띠 = 낮은 쪽 끝단(늘인 끝 · 아니면 유출 쪽 노견 끝)에서 성토 사면 따라 · 길이 비움 =
원지반 닿으면 끝 · 양측 절토 물넘이 = 띠 없음 · 길이 = 그 길이(사면보다 길면 원지반 따라) · 다른 구조물
(배관 · 박스 · 세월교) 같은 규칙(`chuteRun` · 원지반 `layout.ground`) + 같은 「길이 (m)」 칸 · 면적 = 세월교만
월류 폭 × 길이(배관 · 박스 = 폭 정의 없어 자동 안 함) · 길이는 집계 · B08 수량에 안 실림.
(배관 · 박스 · 세월교) 같은 규칙(`chuteRun` · 원지반 `layout.ground`) + 같은 「길이 (m)」 칸 · 면적 = 늘
손 입력(301-1 · 길이 · 폭으로 안 셈) · 길이는 집계 · B08 수량에 안 실림.
진짜 TS 는 `helper_294_3_ford_extend_chute.cjs` 가 Node 로 돌림. 화면은 사용자 확인.
"""
@@ -155,7 +155,6 @@ def test_성토_쪽_띠는_원지반_닿으면_끝(ts):
def test_다른_구조물_같은_규칙과_길이_칸(ts):
assert ts["chuteItems"][-1] == "length_m"
assert ts["fordPaveChuteSame"] is True
assert ts["fordArea"] == "20" # 세월교 월류 폭 8 × 길이 2.5
hatch = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Wall_Hatch.ts").read_text(
encoding="utf-8"
)
@@ -168,7 +167,7 @@ def test_다른_구조물_같은_규칙과_길이_칸(ts):
form = (ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility.ts").read_text(
encoding="utf-8"
)
assert form.count("syncChuteArea(") == 2 # 물넘이 · 세월교만(배관 · 박스 = 폭 정의 없음)
assert "syncChuteArea" not in form # 면적 = 손 입력(301-1)
def test_길이는_집계_수량에_안_실림():