feat(B06): 관보호공 유입 · 유출 · 유입 집수정 높이 기본 = 관경 + 0.5 · 그림이 칸 값 읽음 · 옛 1.5(관경 > 1.0) 읽을 때 옮김 · 관경 바꾸면 따라감(139-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
2026-10-09 14:53:37 +09:00
co-authored by Claude Opus 5.5
parent c0cf0ab90a
commit e3cb11185c
15 changed files with 287 additions and 59 deletions
@@ -39,6 +39,7 @@ import {
} from "../../B05_Profile/B05_Profile_UI_Drainage_Facility_Fields";
import { buildSummaryRow } from "../../B05_Profile/B05_Profile_UI_Structures_Summary_Row";
import type { GroupItems, SummaryItem } from "../../common_util/common_util_summary_item";
import { followPipeHeights } from "../../common_util/common_util_culvert_sets";
/** 잇기 값 자리 — 패널 그룹 값(`group_items`)을 그대로 씀. */
export interface SpecLinks {
@@ -169,6 +170,8 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
raw: string,
view: Instance,
): void => {
if (spec?.typeId === "pipe" && c.id === "pipe" && item.id === "diameter")
followPipeHeights(state, raw, values[item.id] ?? "");
// 그림 값을 보이던 위 칸 아래를 고침 = 그 위 값을 고른 것 — 다른 갈래 값은 비움(54-13)
const parent = item.by?.item;
if (parent && view[parent] !== values[parent]) {
@@ -342,6 +345,7 @@ export function createSpecForm(links: SpecLinks | null, hooks: SpecFormHooks = {
names = nextNames;
groupNames = nextGroups;
state = readState(next, options);
if (next.typeId === "pipe") followPipeHeights(state, state.pipe?.[0]?.diameter ?? "");
recent.clear();
paint();
},
@@ -25,6 +25,7 @@ from fastapi.responses import JSONResponse
from B04_PreProcess.B04_PreProcess_Engine_Watershed_Export import drainage_dir
from B05_Profile.B05_Profile_Structures_Schema import structure_type_map
from B06_Section.B06_Section_Engine_Culvert import pipe_wall_min_height_m
from common_util.common_util_drainage_context import DrainageContext, load_drainage_context
from common_util.common_util_drainage_detail import DrainageDetail, build_detail
from common_util.common_util_drainage_pipes import (
@@ -89,6 +90,17 @@ def _build(
return detail, carried
# 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1)
PIPE_HEIGHT_KEYS = ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m")
def _float(value: Any) -> float:
try:
return float(value)
except (TypeError, ValueError):
return 0.0
def _apply_new_defaults(
points: list[PipePoint], flows: dict[float, float | None] | None = None
) -> None:
@@ -111,6 +123,13 @@ def _apply_new_defaults(
# 집수정 모양도 실제 값으로(54-39 · 54-36 기본 L형 — 화면 짝 `DEFAULT_BASIN_SHAPE`)
if options.get("inlet_type") == "집수정":
options.setdefault("inlet_structure", "L")
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · 화면 짝 `PIPE_HEIGHT_KEYS`)
keys = {field.key for field in definition.options}
diameter_m = _float(options.get("pipe_diameter_mm")) / 1000
if diameter_m > 0:
for key in PIPE_HEIGHT_KEYS:
if key in keys:
options.setdefault(key, pipe_wall_min_height_m(diameter_m))
if point.facility == "ford_bridge" and "ford_height_m" not in options:
flow = (flows or {}).get(round(point.chainage_m, 2))
height = ford_height_m(flow, float(options.get("ford_width_m") or 0))
@@ -9,6 +9,7 @@
import { chainageToStation, stationToChainage } from "./B05_Profile_Util_Station";
import { DEFAULT_BASIN_SHAPE } from "../B06_Section/B06_Section_UI_Cross_Culvert_Basin";
import { fillPipeHeights } from "../common_util/common_util_culvert_sets";
export function field(labelText: string, input: HTMLElement): HTMLLabelElement {
const wrapper = document.createElement("label");
@@ -110,6 +111,11 @@ export function optionDefaults(
);
// 배관 집수정 모양도 실제 값으로(54-39 · 54-36 기본 · 자동 배치 짝 `_apply_new_defaults`)
if (values.inlet_type === "집수정") values.inlet_structure ??= DEFAULT_BASIN_SHAPE;
// 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1)
fillPipeHeights(
values,
options.map((option) => option.key),
);
return values;
}
+29 -12
View File
@@ -71,26 +71,42 @@ CHUTE_THICKNESS_CM = 30.0
# 1:0.3~0.5의 급한 쪽. 지식DB 근거가 있는 값이라 교차 참조 표시가 붙지 않는다.
REVET_FACE_SLOPE = 0.3
# ── 관 벽 최소 높이 — ⚠ TS 짝: `B06_Section_UI_Cross_Culvert_Const.ts`
# (`REVET_FREEBOARD_M`·`REVET_HEIGHT_STEP_M`·`REVET_EMBED_DEPTH_M`·`pipeWallMinPureHeight`).
# ── 관 벽 기본 높이 — ⚠ TS 짝: `B06_Section_UI_Cross_Culvert_Const.ts`
# (`REVET_HEIGHT_STEP_M`·`REVET_EMBED_DEPTH_M`·`revetTargetHeight`·`pipeWallMinPureHeight`).
# 횡단도가 관 벽을 그리는 그 높이 — 구조물 표·구조물도 그림도 이 값을 씀(2026-09-14 브레인 판정
# 「그림 둘이 같은 벽을 다르게 그리면 결함 · 근거 있는 관경 기준으로 맞춤」). 거울 시험
# `test_b06_pipe_wall_height_mirror`.
REVET_FREEBOARD_M = 0.5 # 관 상단 ~ 벽 상단 여유고(잠정 · 토피 0.5 와 같은 값)
REVET_HEIGHT_STEP_M = 0.1 # 높이 눈금
REVET_EMBED_DEPTH_M = 0.5 # 근입 깊이
# 옛 정의 기본 높이(10-6 표 · 관경 무관) — 관경 > 1.0 관에 이 값이면 관 윗면보다 낮은 벽이라
# 읽을 때 관경 기준 값으로 봄(139-1).
OLD_PIPE_WALL_DEFAULT_M = 1.5
def pipe_wall_min_height_m(diameter_m: float) -> float:
"""관 벽 **순수 높이**(근입 포함) 최소값 — 관경 + 여유고를 0.1m 로 올린 뒤 근입을 더함.
"""관 벽 **순수 높이**(근입 포함) 기본값 = 관경(0.1m 로 올림) + 근입 0.5 — 벽 윗면 = 관 윗면(139-1).
⚠ 「최소」다 — 실제 설계값은 더 클 수 있어 이 값으로 선 수량은 미확정으로 둠(금액 밖).
관 · 유입 집수정 높이 칸의 새 기본값이자 빈 칸 값.
"""
raw = float(diameter_m) + REVET_FREEBOARD_M
raw = float(diameter_m)
steps = math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9)
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3)
def pipe_wall_height_m(stored: float | None, diameter_m: float) -> float | None:
"""관 벽 · 유입 집수정 높이 칸 읽기 — 옛 기본 1.5 이고 관경 > 1.0 이면 관경 기준 값(139-1).
빈 칸 = None(부르는 쪽이 빈 칸 규칙). ⚠ TS 짝 `common_util_culvert_sets.pipeWallHeightM`.
"""
if (
stored is not None
and abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9
and float(diameter_m) > 1.0 + 1e-9
):
return pipe_wall_min_height_m(diameter_m)
return stored
# (2026-08-20 사용자 확정) 배관은 **수평도 가능** — 그림에서 경사를 강제로 만들지
# 않는다. config `DRAINAGE_PIPE_SLOPE_DEG`(10도)는 유효직경 수리계산 전용으로 남는다.
@@ -188,16 +204,17 @@ def _side_spec(
None,
)
# 집수정 높이(좌측 폼 · 비면 정의 기본값) — 횡단도 집수정 벽 높이.
spec["basin_height_m"] = _number(options.get("inlet_basin_height_m"), None)
spec["basin_height_m"] = pipe_wall_height_m(
_number(options.get("inlet_basin_height_m"), None), diameter_m
)
# 집수정 모양(54-39) — 빈 값 = 안 고름
spec["basin_form"] = str(options.get("inlet_structure") or "") or None
return spec
# 높이를 안 적었으면 **횡단도가 그리는 관경 기준 최소 높이**(2026-09-14 · 옛 기본 2.5 걷음).
height = _number(
options.get(f"{side}_revet_height_m"),
pipe_wall_min_height_m(diameter_m),
)
# 높이를 안 적었으면 **관경 기준 높이**(관경 + 0.5 · 139-1) · 옛 1.5 도 관경 > 1.0 이면 그 값.
height = pipe_wall_height_m(_number(options.get(f"{side}_revet_height_m"), None), diameter_m)
if height is None:
height = pipe_wall_min_height_m(diameter_m)
length = _number(
options.get(f"{side}_revet_length_m"),
None,
@@ -24,23 +24,22 @@ export const REVET_LEAN_RATIO = 0.3;
export const REVET_TRAP_TOP_M = REVET_THICKNESS_M / 2;
/**
* 기슭막이 여유고(m) — 관 상단에서 벽 상단까지의 간격. 유입·유출 **공통**이다.
* 집수정 상부 여유(m) — 관 상단에서 집수정 벽 상단까지의 간격. 관 벽(유입·유출)은
* 여유 없이 벽 윗면 = 관 윗면(139-1 · `revetTargetHeight`).
*
* ⚠ 확정값 아님(2026-08-21 사용자 지정). 지식DB에 임도 배수관 기슭막이 여유고
* 규정이 없다(`통수단면.md` §여유고 — "임도·KDS 원문에 수치 없음"). 관 상단 최소
* 토피 0.5m와 같은 값을 잠정 채택했고, 추후 **사용자 입력값**으로 교체 예정이다.
*
* 이 값 때문에 벽 높이는 좌·우가 같은 규칙으로 정해진다 — 관이 경사져 벽 밑(invert)
* 이 서로 달라도 관 상단 위 간격은 항상 같다.
*/
export const REVET_FREEBOARD_M = 0.5;
/** 기슭막이 높이 눈금(m) — 계산 높이를 이 단위로 올린다(2026-08-21 사용자 확정). */
export const REVET_HEIGHT_STEP_M = 0.1;
/** 관경에 여유고를 더해 눈금(0.1m)으로 올린 기슭막이 높이(m). */
/** 관 벽 계산용 높이(m · invert~상단) — 관경을 눈금(0.1m)으로 올림 → 벽 윗면 = 관 윗면(139-1).
* ⚠ 파이썬 짝 `pipe_wall_min_height_m` · 세트 짝 `pipeWallMinHeightM`(거울 시험). */
export function revetTargetHeight(diameterM: number): number {
const raw = diameterM + REVET_FREEBOARD_M;
const raw = diameterM;
return Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M;
}
@@ -139,7 +138,7 @@ export function revetWallSpec(
// 관경 기준 높이는 기본값만 — 하한은 그림이 서는 값뿐(54-51 · 사용자가 고름).
const preferred = hiddenPipe
? (spec.revet_height_m ?? autoHeightM ?? REVET_EMBED_DEPTH_M)
: pipeWallMinPureHeight(diameterM);
: (spec.revet_height_m ?? pipeWallMinPureHeight(diameterM)); // 칸 값 → 그림(139-1)
const pureHeight = Math.max(adjust.h ?? preferred, WALL_MIN_PURE_HEIGHT_M);
return {
form,
@@ -154,10 +153,9 @@ export function revetWallSpec(
export const PIPE_CONNECT_GRADE = Math.tan((3 * Math.PI) / 180);
/**
* 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이(2026-08-23
* 사용자 확정: 배관 기준 아래 0.5·위 0.5 여유). 값은 근입 깊이와 같은 0.5라
* 형상은 종전과 동일하고, **순수 높이 하한 = 관경 + 위 0.5 + 아래 0.5**가 된다.
* 종전 고정 하한·기본 2.0m(관경 무관)는 폐기.
* 배관 기슭막이 관 하부 여유(m) — 관 invert 아래 벽 바닥까지의 깊이 = 근입 0.5.
* **순수 높이 기본 = 관경 + 0.5**(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 ·
* 10-6 표 Ø1000 = 1.5 · 139-1). 칸을 비우면 이 값.
*/
export function pipeWallMinPureHeight(diameterM: number): number {
return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;
+12 -2
View File
@@ -36,7 +36,7 @@ from uuid import UUID
from fastapi import APIRouter
from B06_Section.B06_Section_Engine_Culvert import pipe_wall_min_height_m
from B06_Section.B06_Section_Engine_Culvert import pipe_wall_height_m, pipe_wall_min_height_m
from B06_Section.B06_Section_Repository import (
get_confirmed_route_context,
get_cross_section_designs,
@@ -558,7 +558,17 @@ def pipe_summary(
sides = [side for side in ("inlet", "outlet") if options.get(f"{side}_type") == SOIL]
sources = [(side, links.get(LINKS[side])) for side in sides]
_revets(sheet, at, options, links, sources, pipe_wall_min_height_m(diameter / 1000), names)
# 옛 기본 1.5 · 관경 > 1.0 = 관경 기준 값(139-1 · 그림과 한 값)
moved = {
f"{side}_revet_height_m": pipe_wall_height_m(
_num(options.get(f"{side}_revet_height_m")), diameter / 1000
)
for side in sides
}
wall_options = {**options, **moved}
_revets(
sheet, at, wall_options, links, sources, pipe_wall_min_height_m(diameter / 1000), names
)
_chutes(sheet, at, options, links, names)
out = sheet.result()
+65 -7
View File
@@ -46,19 +46,75 @@ export const FORD_DEFAULT_WIDTH_M = 10.0;
export const FORD_PAVEMENT_DEFAULT_WIDTH_M = 10.0;
export const FORD_PAVEMENT_DEFAULT_SLOPE_DEG = 3.0;
export const BOX_COVER_M = 0.5;
/** 관 벽 최소 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조).
* 높이를 안 적은 관 벽은 이 값으로 섬(2026-09-14 브레인 판정 · 옛 등록부 기본 2.5 걷음). */
export const REVET_FREEBOARD_M = 0.5;
/** 관 벽 기본 높이 — 횡단도 `B06_Section_UI_Cross_Culvert_Const.ts` 와 같은 값(거울 시험이 셋을 대조).
* 높이를 안 적은 관 벽은 이 값으로 섬(139-1 · 관경 + 0.5 = 벽 윗면 = 관 윗면). */
export const REVET_HEIGHT_STEP_M = 0.1;
export const REVET_EMBED_DEPTH_M = 0.5;
export const OLD_PIPE_WALL_DEFAULT_M = 1.5;
/** 관 벽 순수 높이(근입 포함) 최소값 — 파이썬 `pipe_wall_min_height_m`. */
/** 관 벽 순수 높이(근입 포함) 기본값 — 파이썬 `pipe_wall_min_height_m`. */
export function pipeWallMinHeightM(diameterM: number): number {
const raw = diameterM + REVET_FREEBOARD_M;
const raw = diameterM;
const steps = Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9);
return round(steps * REVET_HEIGHT_STEP_M + REVET_EMBED_DEPTH_M, 3);
}
/** 관 벽 · 유입 집수정 높이 칸 읽기 — 옛 기본 1.5 이고 관경 > 1.0 이면 관경 기준 값(139-1) ·
* 빈 칸 = null. 파이썬 `pipe_wall_height_m`. */
export function pipeWallHeightM(stored: number | null, diameterM: number): number | null {
if (
stored !== null &&
Math.abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9 &&
diameterM > 1.0 + 1e-9
)
return pipeWallMinHeightM(diameterM);
return stored;
}
/** 관 보호공 · 유입 집수정 높이 칸 — 새 관 기본 = 관경 + 0.5(139-1 · 서버 짝 `PIPE_HEIGHT_KEYS`). */
export const PIPE_HEIGHT_KEYS = [
"inlet_revet_height_m",
"outlet_revet_height_m",
"inlet_basin_height_m",
];
/** 새 관 옵션 기본값에 높이 칸을 채움 — 그 칸이 정의에 있고 비었을 때만(B05 `optionDefaults`). */
export function fillPipeHeights(
values: Record<string, string | number>,
keys: ReadonlyArray<string>,
): void {
const diameterM = Number(values.pipe_diameter_mm) / 1000;
if (!(diameterM > 0)) return;
for (const key of PIPE_HEIGHT_KEYS)
if (keys.includes(key)) values[key] ??= pipeWallMinHeightM(diameterM);
}
/** 배관 관 보호공 높이 칸(유입 · 유출 · 유입 집수정 = 항목 `height`) — 관경 + 0.5 규칙(139-1).
* `previousMm` 없음 = 읽을 때(옛 1.5 · 관경 > 1.0 옮김) · 있음 = 관경을 바꿈(옛 관경 규칙 값 · 옛 1.5
* 칸만 새 규칙 값으로 · 손 값 · 빈 칸은 그대로). */
export function followPipeHeights(
state: Record<string, Array<Record<string, string>> | undefined>,
diameterMm: string,
previousMm?: string,
): void {
const d = Number(diameterMm) / 1000;
if (!(d > 0)) return;
const prev = previousMm ? pipeWallMinHeightM(Number(previousMm) / 1000) : null;
for (const id of ["inlet", "outlet"]) {
const values = state[id]?.[0];
const raw = values?.height ?? "";
if (!values || raw === "") continue;
const now = Number(raw);
const moved =
previousMm === undefined
? pipeWallHeightM(now, d)
: [prev, OLD_PIPE_WALL_DEFAULT_M].some((v) => v !== null && Math.abs(now - v) < 1e-9)
? pipeWallMinHeightM(d)
: now;
if (moved !== null && moved !== now) values.height = String(moved);
}
}
/** 시설 종류 — 파이썬 `common_util_drainage_pipes` 의 상수와 같은 문자열. */
const FACILITY_PIPE = "pipe";
const FACILITY_BOX = "box_culvert";
@@ -107,12 +163,14 @@ function sideSpec(
);
spec.basin_before_m = num(options.inlet_basin_before_m, null);
spec.basin_after_m = num(options.inlet_basin_after_m, null);
spec.basin_height_m = num(options.inlet_basin_height_m, null);
spec.basin_height_m = pipeWallHeightM(num(options.inlet_basin_height_m, null), diameterM);
spec.basin_form = text(options.inlet_structure); // 집수정 모양(54-39) — 빈 값 = 안 고름
return spec;
}
const height = num(options[`${side}_revet_height_m`], pipeWallMinHeightM(diameterM));
const height =
pipeWallHeightM(num(options[`${side}_revet_height_m`], null), diameterM) ??
pipeWallMinHeightM(diameterM);
const length = num(options[`${side}_revet_length_m`], null);
const form = text(options[`${side}_revet_form`]);
const before = num(options[`${side}_revet_before_m`], null);
@@ -153,10 +153,10 @@
"label": "집수정 높이",
"input": "number",
"unit": "m",
"default": 1.5,
"default": null,
"required": false,
"phase": "detail",
"default_basis": "사용자 확정 2026-10-06 배관 기본값 표(docs/raw/사용자요구/2026-10-05_수정요구.md)"
"default_basis": "새 관 = 관경 + 0.5 m(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 · Ø1000 = 1.5 = 10-6 배관 기본값 표) — 화면 · 서버가 관경으로 채움 · 관경 바꾸면 따라감 · 옛 1.5 이고 관경 > 1.0 이면 읽을 때 관경 + 0.5(139-1)"
},
{
"key": "inlet_revet_form",
@@ -205,11 +205,11 @@
"label": "유입 흙막이 높이",
"input": "number",
"unit": "m",
"default": 1.5,
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)",
"default_basis": "사용자 확정 2026-10-06 배관 기본값 표(docs/raw/사용자요구/2026-10-05_수정요구.md)"
"empty_means": "비우면 관경 기준 높이(관경을 0.1m 로 올림 + 근입 0.5 · 벽 윗면 = 관 윗면)로 그리고 셈 — 횡단도·구조물도·표가 같은 값(139-1)",
"default_basis": "새 관 = 관경 + 0.5 m(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 · Ø1000 = 1.5 = 10-6 배관 기본값 표) — 화면 · 서버가 관경으로 채움 · 관경 바꾸면 따라감 · 옛 1.5 이고 관경 > 1.0 이면 읽을 때 관경 + 0.5(139-1)"
},
{
"key": "outlet_type",
@@ -269,11 +269,11 @@
"label": "유출 흙막이 높이",
"input": "number",
"unit": "m",
"default": 1.5,
"default": null,
"required": false,
"phase": "detail",
"empty_means": "비우면 관경 기준 최소 높이(관경 + 여유 0.5 를 0.1m 로 올림 + 근입 0.5)로 그리고 셈 — 횡단도·구조물도·표가 같은 값 · 최소라 수량은 미확정(금액 밖) · 2026-09-14 브레인 판정(옛 기본 2.5 걷음)",
"default_basis": "사용자 확정 2026-10-06 배관 기본값 표(docs/raw/사용자요구/2026-10-05_수정요구.md)"
"empty_means": "비우면 관경 기준 높이(관경을 0.1m 로 올림 + 근입 0.5 · 벽 윗면 = 관 윗면)로 그리고 셈 — 횡단도·구조물도·표가 같은 값(139-1)",
"default_basis": "새 관 = 관경 + 0.5 m(땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면 · Ø1000 = 1.5 = 10-6 배관 기본값 표) — 화면 · 서버가 관경으로 채움 · 관경 바꾸면 따라감 · 옛 1.5 이고 관경 > 1.0 이면 읽을 때 관경 + 0.5(139-1)"
},
{
"key": "revet_foundation",
@@ -0,0 +1,71 @@
"""139-1 관 보호공 높이 = 관경 + 0.5 — 화면 쪽(새 관 기본값 · 폼 읽기 옮김 · 관경 따라감).
- B05 `optionDefaults` → `fillPipeHeights`: 새 관 높이 칸 셋(유입 · 유출 흙막이 · 유입 집수정) = 관경 + 0.5.
- 폼 `followPipeHeights`(둘 다 `common_util_culvert_sets.ts`): 읽을 때 옛 1.5 · 관경 > 1.0 → 관경 + 0.5 · 관경을 바꾸면 옛 규칙 값 ·
옛 1.5 칸만 따라감(손 값 · 빈 칸 그대로).
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
TSC = ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
_RUNNER = """
import { writeFileSync } from "node:fs";
import { fillPipeHeights, followPipeHeights } from "./common_util_culvert_sets.js";
const keys = ["pipe_diameter_mm", "inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m"];
const fresh = { pipe_diameter_mm: 1200 }; fillPipeHeights(fresh, keys);
const noPipe = {}; fillPipeHeights(noPipe, keys);
const st = (d, i, o) => ({ pipe: [{ diameter: d }], inlet: [{ height: i }], outlet: [{ height: o }] });
const read = st("1200", "1.5", "1.6"); followPipeHeights(read, "1200");
const small = st("800", "1.5", ""); followPipeHeights(small, "800");
const change = st("1200", "1.7", "2.2"); followPipeHeights(change, "1500", "1200");
const old = st("1000", "1.5", ""); followPipeHeights(old, "800", "1000");
writeFileSync(process.argv[2], JSON.stringify({ fresh, noPipe, read, small, change, old }));
"""
@pytest.mark.skipif(not TSC.is_file(), reason="프론트엔드 의존성 미설치")
def test_화면_높이_규칙(tmp_path: Path) -> None:
out = tmp_path / "js"
subprocess.run( # noqa: S603 — 고정 실행 파일
[
"node",
str(TSC),
str(ROOT / "common_util" / "common_util_culvert_sets.ts"),
"--outDir",
str(out),
"--module",
"esnext",
"--target",
"es2022",
"--skipLibCheck",
"--ignoreConfig",
],
cwd=str(ROOT),
capture_output=True,
)
(out / "runner.mjs").write_text(_RUNNER, encoding="utf-8")
result = tmp_path / "out.json"
run = subprocess.run( # noqa: S603
["node", str(out / "runner.mjs"), str(result)], capture_output=True, text=True
)
assert run.returncode == 0, run.stderr[-2000:]
got = json.loads(result.read_text(encoding="utf-8"))
for key in ("inlet_revet_height_m", "outlet_revet_height_m", "inlet_basin_height_m"):
assert got["fresh"][key] == pytest.approx(1.7), key # Φ1200 → 1.7
assert "inlet_revet_height_m" not in got["noPipe"] # 관경 없음 = 안 채움
assert got["read"]["inlet"][0]["height"] == "1.7" # 옛 1.5 · Φ1200 → 1.7
assert got["read"]["outlet"][0]["height"] == "1.6" # 손 값 그대로
assert got["small"]["inlet"][0]["height"] == "1.5" # Φ800 의 1.5 는 그대로
assert got["change"]["inlet"][0]["height"] == "2" # 옛 규칙 값(1.7) → 새 규칙(2.0)
assert got["change"]["outlet"][0]["height"] == "2.2" # 손 값 그대로
assert got["old"]["inlet"][0]["height"] == "1.3" # 옛 1.5 → Φ800 규칙
assert got["old"]["outlet"][0]["height"] == "" # 빈 칸 그대로
+4 -4
View File
@@ -141,10 +141,10 @@ def test_흙막이_형태_높이마다_길이(root):
# 집수정 유입은 흙막이로 안 셈 · 유출만
assert _cell(out, 1567, "pipe:revet|돌쌓기(찰)|2.0") is None
assert _cell(out, 1567, "pipe:revet|돌쌓기(메)|2.0") == 10
# 빈 높이 = 횡단도 관경 최소(Φ800 → 1.8) · 정의 기본 1.5 아님 · 빈 형태 = 미입력
assert _cell(out, 1790, "pipe:revet|-|1.8") == 10
head = next(c for c in out["columns"] if c["id"] == "pipe:revet|-|1.8")
assert head["group"][1:] == ["미입력", "H=1.8"]
# 빈 높이 = 횡단도 관경 기준(Φ800 → 1.3 · 139-1) · 빈 형태 = 미입력
assert _cell(out, 1790, "pipe:revet|-|1.3") == 10
head = next(c for c in out["columns"] if c["id"] == "pipe:revet|-|1.3")
assert head["group"][1:] == ["미입력", "H=1.3"]
def test_돌붙임은_형식_면적이_다_있을_때만(root):
@@ -34,7 +34,9 @@ def test_새_관만_기본값_빈_키에만():
fresh = PipePoint(chainage_m=10.0, source="stream", options={"pipe_diameter_mm": 800})
basins._apply_new_defaults([fresh])
# 추천(먼저 들어간 값)이 이김 · 집수정 모양 L 도 실제 값(54-39)
expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L"}
# 관 보호공 · 유입 집수정 높이 = 관경 + 0.5(139-1 · Φ800 → 1.3)
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.3)
expected = {**_defaults("pipe"), "pipe_diameter_mm": 800, "inlet_structure": "L", **heights}
assert fresh.options == expected
assert fresh.options["pipe_kind"] == "파형강관" and fresh.options["inlet_type"] == "집수정"
box = PipePoint(chainage_m=20.0, source="stream", facility="box_culvert")
@@ -64,7 +66,8 @@ def test_원본에_있던_관은_빈칸_그대로(monkeypatch):
by = {p.chainage_m: p for p in points}
assert by[100.0].options is None # 옛 빈칸 그대로
# 새로 보충된 관 = 기본값 + 집수정 모양 L(54-39)
assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L"}
heights = dict.fromkeys(basins.PIPE_HEIGHT_KEYS, 1.5) # 기본 관경 1000 → 1.5(139-1)
assert by[300.0].options == {**_defaults("pipe"), "inlet_structure": "L", **heights}
def test_월류_폭_기본값은_등록부_엔진과_같음():
@@ -466,11 +466,13 @@ def test_pipe_accessory_options_defined():
"outlet_pitching_area_m2",
):
assert gone not in options, f"pipe.{gone}는 보호공으로 합쳐졌다"
# 기본 치수 — 길이 10m · 높이 1.5(10-6 배관 기본값 표 · 54-36 · 근거 default_basis).
# 기본 치수 — 길이 10m · 높이 = 관경 + 0.5(정의 고정값 없음 · 화면 · 서버가 관경으로 채움 · 139-1).
for side in ("inlet", "outlet"):
assert options[f"{side}_revet_length_m"].default == 10
assert options[f"{side}_revet_height_m"].default == 1.5
assert options[f"{side}_revet_height_m"].default is None
assert "관경 + 0.5" in options[f"{side}_revet_height_m"].default_basis
assert "관경 기준" in options[f"{side}_revet_height_m"].empty_means
assert options["inlet_basin_height_m"].default is None
# 유무·길이·관경·유입 구조 = b05, 형태·치수 제원 = detail.
for key in ("inlet_type", "inlet_revet_length_m", "outlet_revet_length_m"):
assert options[key].phase == "b05", f"pipe.{key}는 b05 phase"
+26 -3
View File
@@ -43,14 +43,14 @@ def test_empty_options_have_no_members():
def test_saved_values_drive_members():
"""고른 값만 부재 — 빈 높이 = 관경 기준 최소(Ø1000 → 2.0) · 빈 형태 = 없음."""
"""고른 값만 부재 — 빈 높이 = 관경 기준(Ø1000 → 1.5 · 139-1) · 빈 형태 = 없음."""
spec = _culvert_set({"inlet_type": "집수정", "inlet_structure": "L", "outlet_type": "기슭막이"})
assert spec["inlet"]["basin_form"] == "L"
assert spec["inlet"]["basin_height_m"] is None
part = spec["outlet"]
assert part["structure"] == "기슭막이"
assert part["revet_form"] is None
assert part["revet_height_m"] == 2.0
assert part["revet_height_m"] == 1.5
assert part["face_slope"] == REVET_FACE_SLOPE
# 옛 보호공(apron) 칸은 걷음 — 유출 보호는 도수로/돌붙임 띠(90-2)
assert "apron_length_m" not in part
@@ -63,7 +63,7 @@ def test_basin_inlet_skips_revet_and_apron():
assert "revet_height_m" not in spec["inlet"]
# 유출측은 그대로 세트를 갖춘다.
assert spec["outlet"]["structure"] == "기슭막이"
assert spec["outlet"]["revet_height_m"] == 2.0 # 빈 높이 = 관경 기준 최소
assert spec["outlet"]["revet_height_m"] == 1.5 # 빈 높이 = 관경 기준(139-1)
def test_string_diameter_and_custom_height():
@@ -75,6 +75,29 @@ def test_string_diameter_and_custom_height():
assert spec["outlet"]["revet_height_m"] == 1.5
def test_old_default_moves_for_large_pipe():
"""옛 기본 1.5 · 관경 > 1.0 = 관경 + 0.5 로 읽음(139-1) · 관경 ≤ 1.0 · 다른 값은 그대로."""
big = _culvert_set(
{
"pipe_diameter_mm": 1200,
"inlet_type": "집수정",
"inlet_basin_height_m": 1.5,
"outlet_type": "기슭막이",
"outlet_revet_height_m": 1.5,
}
)
assert big["outlet"]["revet_height_m"] == 1.7
assert big["inlet"]["basin_height_m"] == 1.7
small = _culvert_set(
{"pipe_diameter_mm": 800, "outlet_type": "기슭막이", "outlet_revet_height_m": 1.5}
)
assert small["outlet"]["revet_height_m"] == 1.5
hand = _culvert_set(
{"pipe_diameter_mm": 1500, "outlet_type": "기슭막이", "outlet_revet_height_m": 1.6}
)
assert hand["outlet"]["revet_height_m"] == 1.6
def test_chute_thickness_cm_to_m():
"""도수로/돌붙임(90-2) — 구조를 고르면 `chute` · 두께 cm → m · 빈 두께 30 cm · 안 고르면 없음."""
assert "chute" not in _culvert_set({"chute_add_1_thickness": 40})
+4 -2
View File
@@ -23,13 +23,14 @@ import { buildOutletExtras } from "./B06_Section_UI_Cross_Culvert_Extra.js";
const spec = { revet_form: "찰쌓기", revet_height_m: 1.5 };
const pipe = (h) => revetWallSpec(spec, { h, m: null }, false, 0.8).pureHeight;
const own = (h) => revetWallSpec(spec, { h, m: null }, true, 0).pureHeight;
const blank = revetWallSpec({ revet_form: "찰쌓기" }, { h: null, m: null }, false, 0.8).pureHeight;
const extras = buildOutletExtras({
start: { offset: 10, elevation: 95 }, startBottomElevation: 94.5, outward: 1,
groundAt: () => 80, limitOffset: 40,
adjusts: [{ x: 0, d: 0, h: 0.3, m: null }],
});
writeFileSync(process.argv[2], JSON.stringify({
pipeLow: pipe(0.7), pipeAuto: pipe(null), pipeZero: pipe(0),
pipeLow: pipe(0.7), pipeAuto: pipe(null), pipeBlank: blank, pipeZero: pipe(0),
ownLow: own(0.3), ownNeg: own(-1),
extraCount: extras.walls.length,
extraPure: extras.walls.map((w) => w.topBack.elevation - w.bottomBack.elevation),
@@ -74,7 +75,8 @@ def test_높이는_입력_그대로_0이하만_멈춤(tmp_path: Path) -> None:
got = json.loads(result.read_text(encoding="utf-8"))
assert got["pipeLow"] == pytest.approx(0.7), "관 흙막이가 관경 기준 최소로 되돌아감"
assert got["pipeAuto"] == pytest.approx(1.8), "빈 칸 기본값(관경 + 1.0) 틀림"
assert got["pipeAuto"] == pytest.approx(1.5), "그림이 칸 값(1.5)을 안 읽음(139-1)"
assert got["pipeBlank"] == pytest.approx(1.3), "빈 칸 기본값(관경 + 0.5 · 139-1) 틀림"
assert got["pipeZero"] == pytest.approx(0.1), "0 이하가 그림 하한에서 안 멈춤"
assert got["ownLow"] == pytest.approx(0.3), "흙막이 0.3 이 근입 기준 최소로 되돌아감"
assert got["ownNeg"] == pytest.approx(0.1), "음수 높이가 그림 하한에서 안 멈춤"
@@ -26,18 +26,33 @@ def _ts_const(source: str, name: str) -> float:
def test_상수_셋이_TS_와_같다() -> None:
for name in ("REVET_FREEBOARD_M", "REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"):
for name in ("REVET_HEIGHT_STEP_M", "REVET_EMBED_DEPTH_M"):
assert getattr(culvert, name) == _ts_const(TS, name) == _ts_const(SETS, name), name
assert culvert.OLD_PIPE_WALL_DEFAULT_M == _ts_const(SETS, "OLD_PIPE_WALL_DEFAULT_M")
def test_TS_식_모양이_그대로다() -> None:
"""식을 바꾸면 여기서 잡힘 — 파이썬 식도 함께 고칠 것."""
assert "const raw = diameterM + REVET_FREEBOARD_M;" in TS
assert "const raw = diameterM;" in TS and "const raw = diameterM;" in SETS
assert "Math.ceil(raw / REVET_HEIGHT_STEP_M - 1e-9) * REVET_HEIGHT_STEP_M" in TS
assert "return revetTargetHeight(diameterM) + REVET_EMBED_DEPTH_M;" in TS
# 그림이 칸 값을 읽음(139-1) — 배관 기본 = 칸 → 관경 기준
assert "(spec.revet_height_m ?? pipeWallMinPureHeight(diameterM))" in TS
# 옛 1.5 옮김 — TS 세트 짝 식
assert "Math.abs(stored - OLD_PIPE_WALL_DEFAULT_M) < 1e-9 &&" in SETS
assert "diameterM > 1.0 + 1e-9" in SETS
def test_관경별_최소_높이() -> None:
assert culvert.pipe_wall_min_height_m(1.0) == 2.0
assert culvert.pipe_wall_min_height_m(0.8) == 1.8
assert culvert.pipe_wall_min_height_m(1.2) == 2.2
def test_관경별_기본_높이() -> None:
"""관경 + 0.5 = 땅속 근입 0.5 포함 벽 전체 높이 · 벽 윗면 = 관 윗면(139-1 · 10-6 표 Ø1000 = 1.5)."""
assert culvert.pipe_wall_min_height_m(1.0) == 1.5
assert culvert.pipe_wall_min_height_m(0.8) == 1.3
assert culvert.pipe_wall_min_height_m(1.2) == 1.7
assert culvert.pipe_wall_min_height_m(1.5) == 2.0
def test_옛_기본_옮김() -> None:
assert culvert.pipe_wall_height_m(1.5, 1.2) == 1.7
assert culvert.pipe_wall_height_m(1.5, 1.0) == 1.5
assert culvert.pipe_wall_height_m(1.6, 1.5) == 1.6
assert culvert.pipe_wall_height_m(None, 1.2) is None