Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -35,6 +35,9 @@ export interface StructureOptionField {
|
||||
* ⚠ 이 칸의 select 는 **빈 보기를 둔다** — 첫 보기를 슬쩍 고르면 기준값과 다른 값이
|
||||
* 조용히 저장된다(2026-09-13: `fill_concrete_mpa` 가 180 으로 박혀 엔진 기준 210 과 갈렸다). */
|
||||
empty_means?: string | null;
|
||||
/** 이 값을 넘으면 칸이 경고색 + 툴팁(막지 않음) — 기준값과 까닭 한 줄. */
|
||||
warn_above?: number | null;
|
||||
warn_message?: string | null;
|
||||
}
|
||||
|
||||
/** 구조물 배치 폼을 어느 화면이 쓰는가 — B05 는 유무·종류·위치만, **B06/B07 은 상세
|
||||
|
||||
@@ -921,6 +921,16 @@
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "face_role",
|
||||
"label": "성토/절토",
|
||||
"input": "select",
|
||||
"choices": ["성토", "절토"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
@@ -990,7 +1000,9 @@
|
||||
"default": 2.5,
|
||||
"required": false,
|
||||
"phase": "b05",
|
||||
"empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)"
|
||||
"empty_means": "비워도 놓임 — 줄만 서고 미확정이라 금액에 안 들어감(높이를 적으면 섬 · 2026-09-14 브레인 판정 ①)",
|
||||
"warn_above": 2.0,
|
||||
"warn_message": "메쌓기 높이 기준을 넘음 — 임도기술교본 7-3:27 「메쌓기는 2.0m 이하」 · 품셈 13-4-4 [주]⑪(원문 L7192) 「높이 3m 이상이면 전부 또는 하부를 찰쌓기」 · 막지 않음(설계자 확인)"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
@@ -1095,6 +1107,16 @@
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "face_role",
|
||||
"label": "성토/절토",
|
||||
"input": "select",
|
||||
"choices": ["성토", "절토"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
@@ -1995,6 +2017,16 @@
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 품셈 표준경사로 자동 판정 — 값을 넣으면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "face_role",
|
||||
"label": "성토/절토",
|
||||
"input": "select",
|
||||
"choices": ["성토", "절토"],
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "detail",
|
||||
"empty_means": "비우면 측점 단면유형 + 설치 측으로 자동 판정(계산이지 기본값이 아님) — 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191)가 성토/절토로 갈림 · 고르면 그 값이 이김"
|
||||
},
|
||||
{
|
||||
"key": "foundation",
|
||||
"label": "기초",
|
||||
@@ -2009,9 +2041,10 @@
|
||||
"label": "높이",
|
||||
"input": "number",
|
||||
"unit": "m",
|
||||
"default": 2.5,
|
||||
"default": null,
|
||||
"required": false,
|
||||
"phase": "b05"
|
||||
"phase": "b05",
|
||||
"empty_means": "설계자 입력 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181 · 사방(계류) 교본 기준). 비우면 줄만 서고 미확정이라 금액에 안 들어감 · 옛 기본 2.5 는 근거 없어 걷음(2026-09-14 브레인 판정)"
|
||||
},
|
||||
{
|
||||
"key": "length_m",
|
||||
|
||||
@@ -60,6 +60,10 @@ class StructureOptionField(BaseModel):
|
||||
# 기본값이 **도메인 확정값이 아닐 때** 그 뜻을 적는다(예: 「다단 없음」·「안 더함」).
|
||||
# 법정·확정 수치면 비워 둔다 — 비어 있는 것이 「확정값」이라는 뜻이다.
|
||||
default_basis: str | None = None
|
||||
# 이 값을 **넘으면** 칸이 경고색 + 툴팁 — 막지 않음(2026-09-14 브레인 판정 ①·㉰ 「놓기를 막지 말 것」).
|
||||
# 넘는지 보는 기준값과 그 까닭 한 줄(원문 쪽·줄 번호).
|
||||
warn_above: float | None = None
|
||||
warn_message: str | None = None
|
||||
|
||||
|
||||
class StructureType(BaseModel):
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* ========================================================================== */
|
||||
|
||||
import { FORD_MANNING_N, FORD_SLOPE } from "@config/config_frontend";
|
||||
import revetmentSabang from "../resources/data_masonry/revetment_sabang_2026-01-01.json";
|
||||
|
||||
/* ── 유입구 구조 ──────────────────────────────────────────────────────────── */
|
||||
|
||||
@@ -252,16 +253,43 @@ export interface RevetSideGroup {
|
||||
slot: HTMLElement;
|
||||
}
|
||||
|
||||
/** 독립 기슭막이 치수 근거 줄 — `revetment_sabang_*.json` 한 벌(B08 사유와 같은 파일 · 쪽·줄 번호).
|
||||
* ⚠ 파일 판이 바뀌면 이 import 도 옮길 것 — 시험(`test_b05_revetment_sabang_basis`)이 최신 판을 대조. */
|
||||
function sabangBasisLines(): string[] {
|
||||
const { items, scope } = revetmentSabang;
|
||||
const span = (values: number[], unit = ""): string => `${values[0]}~${values[1]}${unit}`;
|
||||
return [
|
||||
`높이 — ${items.height.rule}(${items.height.source}) · ${items.height.input}`,
|
||||
`계획비탈 1:${span(items.face_slope.standard_ratio)}(${items.face_slope.source}) — 제안값은 품셈 표준경사`,
|
||||
`둑마루 두께 ${span(items.crown_thickness.standard_m, "m")}(${items.crown_thickness.source}) — 콘크리트 기준 · 돌쌓기는 뒷길이 + 0.30 식`,
|
||||
`뒷채움 조약돌 두께 — ${items.backfill_pebble.source}`,
|
||||
scope,
|
||||
];
|
||||
}
|
||||
|
||||
export function createRevetSideGroup(keys: RevetmentKeys, formDefault: string): RevetSideGroup {
|
||||
const box = group("기슭막이");
|
||||
const kind = document.createElement("select");
|
||||
kind.replaceChildren(new Option("기슭막이", "기슭막이"));
|
||||
const fields = createRevetmentFields(keys, { ...REVET_COMMON_DEFAULTS, form: formDefault });
|
||||
// 높이는 제안 없음 — 설계자 입력(계획홍수위 + 여유고 · 2026-09-14 브레인 판정). 옛 2.5 는 근거 없어 걷음.
|
||||
const fields = createRevetmentFields(keys, {
|
||||
...REVET_COMMON_DEFAULTS,
|
||||
form: formDefault,
|
||||
height: "",
|
||||
});
|
||||
const basis = document.createElement("p");
|
||||
basis.className = "b05-route__note";
|
||||
basis.replaceChildren(
|
||||
...sabangBasisLines().flatMap((line, index) =>
|
||||
index ? [document.createElement("br"), line] : [line],
|
||||
),
|
||||
);
|
||||
const slot = document.createElement("div");
|
||||
slot.className = "b05-structure__adjust-slot";
|
||||
box.body.append(
|
||||
grid(labeled("구조", kind), fields.formField),
|
||||
...fields.rows,
|
||||
basis,
|
||||
// 단 수는 여기 두지 않는다 — 조정창에서 옮겨 오는 [추가 기슭막이(단)] 행과
|
||||
// 같은 값이라 두 벌이면 헷갈린다(2026-08-30 사용자).
|
||||
slot,
|
||||
@@ -301,9 +329,11 @@ export function createRevetmentFields(
|
||||
...REVET_FORMS.map((value) => new Option(value, value)),
|
||||
);
|
||||
const length = keys.length ? numberInput("0.1", "0", `제안 ${defaults.length}`) : null;
|
||||
const height = numberInput("0.1", "0", `제안 ${suggestHeight()}`);
|
||||
// 제안이 없는 칸(독립 기슭막이 높이)은 「설계자 입력」 — 값을 지어내지 않음.
|
||||
const heightHint = (): string => (suggestHeight() ? `제안 ${suggestHeight()}` : "설계자 입력");
|
||||
const height = numberInput("0.1", "0", heightHint());
|
||||
// 관 벽 높이 제안은 관경을 따라감 — 칸을 볼 때마다 다시 셈.
|
||||
height.addEventListener("focus", () => (height.placeholder = `제안 ${suggestHeight()}`));
|
||||
height.addEventListener("focus", () => (height.placeholder = heightHint()));
|
||||
// 기준측점 전/후(2026-08-23 사용자) — 구조물 패널(D군)과 같은 연동:
|
||||
// 전 + 후 = 길이, 나중에 고친 쪽이 살아남고 반대쪽이 재배분된다.
|
||||
const before = keys.before && length ? numberInput("0.1", "0", `제안 ${defaults.before}`) : null;
|
||||
@@ -363,7 +393,7 @@ export function createRevetmentFields(
|
||||
form.value = text(keys.form);
|
||||
if (length && keys.length) length.value = text(keys.length);
|
||||
height.value = text(keys.height);
|
||||
height.placeholder = `제안 ${suggestHeight()}`;
|
||||
height.placeholder = heightHint();
|
||||
if (before && keys.before) before.value = text(keys.before);
|
||||
if (after && keys.after) after.value = text(keys.after);
|
||||
syncSplit("length");
|
||||
|
||||
@@ -40,6 +40,9 @@ interface OptionShape {
|
||||
required?: boolean;
|
||||
/** 비워 두면 어떻게 되나 — 칸에 마우스를 올리면 보임. */
|
||||
empty_means?: string | null;
|
||||
/** 이 값을 넘으면 칸이 경고색 + 툴팁(막지 않음). */
|
||||
warn_above?: number | null;
|
||||
warn_message?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,6 +74,17 @@ export function optionControl(
|
||||
element.value = value;
|
||||
element.placeholder = suggested ? `제안 ${suggested}` : option.required ? "필수 입력" : "";
|
||||
if (option.empty_means) element.title = option.empty_means;
|
||||
const limit = option.warn_above;
|
||||
if (typeof limit === "number") {
|
||||
// 기준을 넘으면 알리기만 — 막지 않음(2026-09-14 브레인 판정 ①·㉰).
|
||||
const warn = (): void => {
|
||||
const over = element.value.trim() !== "" && Number(element.value) > limit;
|
||||
element.classList.toggle("is-outside-standard", over);
|
||||
element.title = over ? (option.warn_message ?? "") : (option.empty_means ?? "");
|
||||
};
|
||||
element.addEventListener("input", warn);
|
||||
warn();
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,13 @@ function facilityNotes(section: CrossSection): Array<[string, string]> {
|
||||
`B05 시설 칸에 안 적어 기본값으로 그림 — ${items.join(" · ")} · 적으면 그 값으로 그림`,
|
||||
]);
|
||||
}
|
||||
const own = section.culvert?.hidden_pipe ? section.culvert : null;
|
||||
if (own && [own.inlet, own.outlet].some((side) => !side.revet_height_m)) {
|
||||
notes.push([
|
||||
"⚠ 기슭막이 높이 없음 — 근입만 그림",
|
||||
"독립 기슭막이 높이는 설계자 입력 — 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181) · 비우면 수량도 미확정 · B05 시설 칸에 적으면 그림",
|
||||
]);
|
||||
}
|
||||
if (section.ford_pavement && !section.ford_pavement.depth_m) {
|
||||
notes.push([
|
||||
"⚠ 월류 높이 없음 — 파임 안 그림",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
#: 배관으로 보는 `facility` 값. 그 밖(BOX암거·물넘이·세월교·독립 기슭막이)은 관이 아니다.
|
||||
@@ -214,9 +217,52 @@ REVET_ROLES = (
|
||||
NOTE_DEFAULT_WALL = (
|
||||
"⚠ 시설 지점에 {filled} 을 안 적어 기본값으로 섰음 — 미확정이라 금액에 안 들어감"
|
||||
)
|
||||
#: 꼬리 — 관 벽은 횡단도와 같은 높이 · 독립 기슭막이는 아직 갈림(관경이 없음).
|
||||
#: 꼬리 — 관 벽은 횡단도와 같은 높이.
|
||||
NOTE_WALL_HEIGHT_PIPE = " · 높이는 횡단도와 같은 관경 기준 최소 높이(실제 설계값은 더 클 수 있음)"
|
||||
NOTE_WALL_HEIGHT_OWN = " · ⚠ 횡단도는 저장 높이가 없으면 근입 0.5m 만 그림 — 표 높이와 다름"
|
||||
|
||||
#: 독립 기슭막이 치수 **한 벌**(`resources/data_masonry/revetment_sabang_*.json`) — B05 칸 밑 근거와
|
||||
#: 이 사유가 같은 파일을 읽음(2026-09-14 브레인 판정). 높이는 **설계자 입력** — 등록부 기본 2.5 걷음.
|
||||
REVET_SABANG_DIR = Path(__file__).resolve().parents[1] / "resources" / "data_masonry"
|
||||
REVET_SABANG_PREFIX = "revetment_sabang_"
|
||||
UNCONFIRMED_OWN_HEIGHT = "높이를 안 적음 — {rule}({source}) 설계자 입력 · 적으면 금액이 섬"
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def load_revetment_sabang() -> dict[str, Any]:
|
||||
"""독립 기슭막이 치수 한 벌. 없으면 빈 표 — 사유가 근거 없이 짧아질 뿐 값은 안 바뀜."""
|
||||
files = sorted(REVET_SABANG_DIR.glob(REVET_SABANG_PREFIX + "*.json"))
|
||||
return json.loads(files[-1].read_text(encoding="utf-8")) if files else {}
|
||||
|
||||
|
||||
def _span(values: list[float], unit: str = "") -> str:
|
||||
return f"{values[0]:g}~{values[1]:g}{unit}"
|
||||
|
||||
|
||||
def own_revetment_basis() -> str:
|
||||
"""독립 기슭막이 근거 한 줄 — 높이·계획비탈·둑마루·뒷채움, 쪽·줄 번호 그대로."""
|
||||
data = load_revetment_sabang()
|
||||
items = data.get("items") or {}
|
||||
if not items:
|
||||
return ""
|
||||
height, slope = items["height"], items["face_slope"]
|
||||
crown, pebble = items["crown_thickness"], items["backfill_pebble"]
|
||||
return (
|
||||
f"ⓘ 독립 기슭막이 치수 근거 — 높이 {height['rule']}({height['source']})"
|
||||
f" · 계획비탈 1:{_span(slope['standard_ratio'])}({slope['source']})"
|
||||
f" · 둑마루 두께 {_span(crown['standard_m'], 'm')}({crown['source']} · 콘크리트 기준)"
|
||||
f" · 뒷채움 조약돌 두께 {pebble['source']} · {data.get('scope', '')}"
|
||||
)
|
||||
|
||||
|
||||
def own_height_unconfirmed() -> str:
|
||||
"""높이를 안 적은 독립 기슭막이의 미확정 까닭."""
|
||||
height = (load_revetment_sabang().get("items") or {}).get("height") or {}
|
||||
return UNCONFIRMED_OWN_HEIGHT.format(
|
||||
rule=height.get("rule", "계획홍수위 + 0.5~0.7m"),
|
||||
source=height.get("source", "사방기술교본"),
|
||||
)
|
||||
|
||||
|
||||
#: 금액에서 뺄 까닭(2026-09-14 브레인 판정) — 줄·표·그림은 서되 내역 합에는 안 듦.
|
||||
UNCONFIRMED_WALL = "벽 치수({filled})가 시설 지점에 없어 기본값으로 섰음 — 적으면 금액이 섬"
|
||||
NOTE_SIDE_UNKNOWN = (
|
||||
@@ -330,8 +376,13 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
values, filled = _revet_values(options, role, wall_defaults, legacy, min_height)
|
||||
notes = list(inlet_notes) if role == "inlet" else []
|
||||
if filled:
|
||||
tail = NOTE_WALL_HEIGHT_OWN if legacy else NOTE_WALL_HEIGHT_PIPE
|
||||
tail = "" if legacy else NOTE_WALL_HEIGHT_PIPE
|
||||
notes.append(NOTE_DEFAULT_WALL.format(filled=" · ".join(filled)) + tail)
|
||||
if legacy:
|
||||
# 독립 기슭막이 — 치수 한 벌 근거(사방 교본 기준 · 쪽·줄 번호)를 늘 붙임.
|
||||
basis = own_revetment_basis()
|
||||
if basis:
|
||||
notes.append(basis)
|
||||
walls.append([role, own_label if legacy else pipe_label, values, notes, False, filled])
|
||||
side = str(options.get("side") or "") if legacy else ""
|
||||
if side in ("좌", "우"):
|
||||
@@ -345,13 +396,17 @@ def facility_structures(points: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
for role, label, values, notes, withheld, filled in walls:
|
||||
before, after = _num_or_zero(values["before_m"]), _num_or_zero(values["after_m"])
|
||||
row = child_row(base, "revetment", label, f"{role}_revet")
|
||||
reasons = [UNCONFIRMED_WALL.format(filled=" · ".join(filled))] if filled else []
|
||||
if legacy and _blank(values["height_m"]):
|
||||
# 높이는 설계자 입력(계획홍수위 + 여유고) — 없으면 줄만 서고 금액 밖.
|
||||
reasons.append(own_height_unconfirmed())
|
||||
row.update(
|
||||
start_m=chainage - before,
|
||||
end_m=chainage + after,
|
||||
options={**(kept if legacy else {}), **values, "foundation": foundation},
|
||||
notes=notes,
|
||||
withheld=withheld,
|
||||
unconfirmed=UNCONFIRMED_WALL.format(filled=" · ".join(filled)) if filled else "",
|
||||
unconfirmed=" · ".join(reasons),
|
||||
)
|
||||
rows.append(row)
|
||||
return rows
|
||||
|
||||
@@ -212,7 +212,7 @@ def build_standard_sheets(
|
||||
⚠ 계산을 다시 하지 않는다 — 들어온 전개를 제원 조합으로 묶고 단위당으로 나눌 뿐이다.
|
||||
"""
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import _section_mode_at
|
||||
from common_util.common_util_structure_face_role import structure_face_role
|
||||
from common_util.common_util_structure_face_role import structure_face_role_of
|
||||
|
||||
groups: dict[str, dict[str, Any]] = {}
|
||||
for structure in unit_table.get("structures") or []:
|
||||
@@ -220,7 +220,7 @@ def build_standard_sheets(
|
||||
# 근거를 못 받아 종전값으로 떨어져 **표와 갈린다**(2026-09-09 실측: 표 1:0.35 ·
|
||||
# 제목 1:0.3). 자리를 고르는 것도 B08 이 쓰는 그 함수를 쓴다.
|
||||
mode = _section_mode_at(structure, section_modes)
|
||||
face, face_reason = structure_face_role(mode, (structure.get("options") or {}).get("side"))
|
||||
face, face_reason = structure_face_role_of(mode, structure.get("options"))
|
||||
structure = {**structure, "face": face, "face_reason": face_reason}
|
||||
key = sheet_key(structure)
|
||||
sheet = groups.get(key)
|
||||
|
||||
@@ -227,7 +227,7 @@ def replace_with_templates(
|
||||
from B08_Quantity.B08_Quantity_Engine_Formula import evaluate_sheets
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureSheet import slope_of
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import Component, _section_mode_at
|
||||
from common_util.common_util_structure_face_role import structure_face_role
|
||||
from common_util.common_util_structure_face_role import structure_face_role_of
|
||||
|
||||
settings = {"rubble_base_thickness_m": rubble_base_thickness_m}
|
||||
targets: list[tuple[Any, dict[str, Any]]] = []
|
||||
@@ -239,9 +239,7 @@ def replace_with_templates(
|
||||
if (quantity.billing_unit or "m") not in _PER_LENGTH_UNITS:
|
||||
continue
|
||||
options = item.get("options") or {}
|
||||
face, face_reason = structure_face_role(
|
||||
_section_mode_at(item, section_modes), options.get("side")
|
||||
)
|
||||
face, face_reason = structure_face_role_of(_section_mode_at(item, section_modes), options)
|
||||
sheet = {
|
||||
"type_id": quantity.type_id,
|
||||
"height_m": quantity.height_m,
|
||||
|
||||
@@ -80,7 +80,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import ( # noqa: F
|
||||
weep_hole_spec,
|
||||
)
|
||||
from common_util.common_util_quantity_spread import spread_by_unit
|
||||
from common_util.common_util_structure_face_role import structure_face_role
|
||||
from common_util.common_util_structure_face_role import structure_face_role_of
|
||||
|
||||
# 구조물 종류 → 전개식. 없는 종류는 전개하지 않고 이름만 남긴다(지어내지 않는다).
|
||||
# ⚠ **관측 원단위표로 가는 종류** — 치수가 저장돼 있지 않아 전개식을 못 세우는 것들이다.
|
||||
@@ -319,7 +319,7 @@ def expand(
|
||||
)
|
||||
return result
|
||||
# 성토/절토 — **판정 한 벌**을 부른다(우리가 따로 짜지 않는다).
|
||||
face, face_reason = structure_face_role(section_mode, options.get("side"))
|
||||
face, face_reason = structure_face_role_of(section_mode, options)
|
||||
result.components, notes = expander(height, length, options, face, face_reason)
|
||||
result.notes.extend(notes)
|
||||
billing = EXPANDER_BILLING.get(type_id)
|
||||
|
||||
@@ -21,6 +21,7 @@ from B08_Quantity.B08_Quantity_Engine_UnitQuantity_StoneSpec import (
|
||||
STONE_PILE_BASIS,
|
||||
_back_length,
|
||||
back_length_default_note,
|
||||
dry_masonry_height_note,
|
||||
face_slope_ratio,
|
||||
fill_concrete_mpa,
|
||||
STONE_SUPPLY_DEFAULT_NOTE,
|
||||
@@ -270,6 +271,9 @@ def stone_masonry(
|
||||
default_note = back_length_default_note(options, back_cm, wet=wet, height_m=height_m)
|
||||
if default_note:
|
||||
notes.append(default_note)
|
||||
height_note = None if wet else dry_masonry_height_note(height_m)
|
||||
if height_note:
|
||||
notes.append(height_note)
|
||||
# ⚠ **돌 종류로 계수가 갈린다** (품셈 13-4-3 · 13-4-4 [주]① · 교본 7-3).
|
||||
# 안 고르면 종전 값(건설품셈 참고자료)으로 돌되 그 사실을 알린다 — 값을 못 낸다고
|
||||
# 멈추면 이미 저장된 프로젝트가 통째로 빈다.
|
||||
|
||||
@@ -444,6 +444,29 @@ def wants_blinding(options: dict[str, Any]) -> bool:
|
||||
BACK_LENGTH_KEYS = ("back_len_cm", "stone_back_length_cm")
|
||||
|
||||
|
||||
#: 메쌓기 높이 — **두 기준이 공존**(2026-09-14 브레인 판정 ㉰ · 지식DB 대조 정합):
|
||||
#: 임도기술교본 7-3:27 「돌흙막이 높이는 원칙적으로 찰쌓기는 3.0m 이하, 메쌓기는 2.0m 이하」 ↔
|
||||
#: 품셈 13-4-4 [주]⑪ 원문 L7192 「돌쌓기는 높이 3m 이상이면 전부 또는 하부를 찰쌓기로 한다」.
|
||||
#: 막지 않고 사유만(판정 ① 「놓기를 막지 말 것」과 한 결).
|
||||
DRY_MASONRY_GUIDE_M = 2.0
|
||||
DRY_MASONRY_LIMIT_M = 3.0
|
||||
|
||||
|
||||
def dry_masonry_height_note(height_m: float) -> str | None:
|
||||
"""메쌓기 높이가 기준을 넘으면 사유 한 줄 — 두 기준을 다 적음."""
|
||||
if height_m >= DRY_MASONRY_LIMIT_M:
|
||||
return (
|
||||
f"⚠ 메쌓기 높이 {height_m:g}m — 품셈 13-4-4 [주]⑪(원문 L7192) 「돌쌓기는 높이 3m 이상이면"
|
||||
" 전부 또는 하부를 찰쌓기로 한다」 · 임도기술교본 7-3:27 은 메쌓기 2.0m 이하 — 막지 않음(설계자 확인)"
|
||||
)
|
||||
if height_m > DRY_MASONRY_GUIDE_M:
|
||||
return (
|
||||
f"⚠ 메쌓기 높이 {height_m:g}m — 임도기술교본 7-3:27 「메쌓기는 2.0m 이하」를 넘음"
|
||||
" · 품셈 13-4-4 [주]⑪(원문 L7192)은 3m 이상부터 찰쌓기 — 두 기준이 다름 · 막지 않음"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
#: 뒷길이 표준표 — 품셈 13-4-4 [주]⑩. 안 고른 돌쌓기는 붙박이 45 가 아니라 이 표의 하한
|
||||
#: (2026-09-14 브레인 판정 ㉮ 「범위 안에 드는 규격 중 가장 작은 것」). 파일이 없으면 종전 45.
|
||||
BACK_LENGTH_PREFIX = "masonry_back_length_"
|
||||
|
||||
@@ -59,3 +59,18 @@ def structure_face_role(section_mode: str | None, side: str | None) -> tuple[str
|
||||
return None, f"{mode} · 자동(성토 쪽)인데 성토면이 없음 — 가를 근거 없음"
|
||||
return FILL, f"{mode} · 자동(성토 쪽) → 성토면"
|
||||
return None, f"설치 측을 알 수 없음({side!r}) — 성절토를 가를 근거 없음"
|
||||
|
||||
|
||||
#: 성토/절토를 **사용자가 고르는 칸** — 비우면 위 판정(계산이지 기본값이 아님 · 2026-09-14 브레인 판정 ㉲).
|
||||
FACE_ROLE_KEY = "face_role"
|
||||
|
||||
|
||||
def structure_face_role_of(
|
||||
section_mode: str | None, options: dict | None
|
||||
) -> tuple[str | None, str]:
|
||||
"""칸 `face_role`(성토·절토)을 골랐으면 그 값, 비었으면 단면유형 + 설치 측 판정."""
|
||||
values = options or {}
|
||||
picked = str(values.get(FACE_ROLE_KEY) or "").strip()
|
||||
if picked in (FILL, CUT):
|
||||
return picked, f"사용자 지정 {picked}면(성토/절토 칸)"
|
||||
return structure_face_role(section_mode, values.get("side"))
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"schema_version": "1.0",
|
||||
"dataset_id": "revetment_sabang",
|
||||
"effective_date": "2026-01-01",
|
||||
"note": "독립 기슭막이 치수 **한 벌** — 높이 · 계획비탈 · 둑마루 두께 · 뒷채움 조약돌 두께를 한 자리에 둠(2026-09-14 브레인 판정). B05 칸 밑 근거와 B08 사유가 이 파일 하나를 같이 읽음.",
|
||||
"scope": "⚠ 사방(계류) 교본 기준 — 임도 배수관 유입·유출부 적용은 확인 남음",
|
||||
"rank": "품셈(산림청고시 제2025-82호)은 행정규칙 · 사방기술교본은 참고자료 — 제안값은 품셈 표, 교본은 근거로 병기(브레인 판정 ㉯ · 운영지침 「프로그램 기본값 = 현행 법령·행정규칙」)",
|
||||
"items": {
|
||||
"height": {
|
||||
"label": "높이",
|
||||
"rule": "계획홍수위 + 0.5~0.7m",
|
||||
"freeboard_m": [0.5, 0.7],
|
||||
"input": "설계자 입력 · 비우면 미확정(금액에 안 들어감)",
|
||||
"source": "사방기술교본 2-나:141 · 3-가:181",
|
||||
"quote": "기슭막이 높이는 계획홍수위 이상으로 하여 홍수시에도 넘치지 않도록 시공한다, 일반적으로 계획홍수위 보다 0.5~0.7m 높게 하며, 계류의 만곡부에서는 凹안이 凸안에 비해 수위가 높아지므로 더 높게 시공한다"
|
||||
},
|
||||
"face_slope": {
|
||||
"label": "계획비탈",
|
||||
"standard_ratio": [0.3, 0.5],
|
||||
"extended_ratio": [1.0, 1.5],
|
||||
"source": "사방기술교본 2-나:129 · 3-가:151",
|
||||
"quote": "기슭막이의 계획비탈은 1:0.3∼0.5를 표준으로 하고, 사용 재료와 현장 연건에 따라 1:1.0∼1.5 정도로 계획할 수 있다",
|
||||
"applied": "제안값은 품셈 13-4-4 [주]⑪ 표준경사(원문 L7185~7191) — 이 범위는 근거로만 보임 · 표 값이 범위 밖이면 사유만(교본을 이기려 들지 않음)"
|
||||
},
|
||||
"crown_thickness": {
|
||||
"label": "둑마루 두께",
|
||||
"standard_m": [0.3, 0.5],
|
||||
"source": "사방기술교본 2-나:143 · 3-가:201 · 3-가:219(콘크리트기슭막이) · 2-나:135(콘크리트 0.3m)",
|
||||
"quote": "기슭막이 둑마루 두께는 0.3∼0.5m를 표준으로 하고, 둑마루 기울기는 원칙적으로 계획계상기울기와 일치하도록 시공한다",
|
||||
"applied": "⚠ 콘크리트 기슭막이 기준 — 돌쌓기 상부 두께는 식(뒷길이 + 0.30)으로 따로 셈 · 식을 안 바꾸고 근거로만(브레인 판정 ㉱)"
|
||||
},
|
||||
"backfill_pebble": {
|
||||
"label": "뒷채움 조약돌 두께",
|
||||
"source": "사방기술교본 2-나:133(「사방품셈 규정에 의하여」) → 품셈 13-4-4 [주]⑨ 원문 L7164~7171",
|
||||
"steps_m": [1.5, 3.0, 5.0, 7.0],
|
||||
"fill_cm": {
|
||||
"top": [20, 40],
|
||||
"bottom": [[30, 60], [45, 75], [60, 100], [80, 140]]
|
||||
},
|
||||
"cut_cm": [30, 40],
|
||||
"applied": "막자갈은 정본 뒷채움 폭(상 0.30 · 하 0.45)으로 셈 — 이 표는 근거로만 보임(`STONE_BACKFILL_WIDTH_M` 주석의 갈림)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""독립 기슭막이 치수 한 벌 — B05 칸 밑 근거와 B08 사유가 **같은 파일**을 읽음 (2026-09-14 브레인 판정).
|
||||
|
||||
한 자리 = `resources/data_masonry/revetment_sabang_*.json`. TS 는 판 이름을 import 로 박으므로 판이
|
||||
바뀌면 import 도 옮겨야 함 — 여기서 최신 판과 대조. 쪽·줄 번호가 사유에 그대로 실리는지도 봄.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_Pipe import ( # noqa: E402
|
||||
own_height_unconfirmed,
|
||||
own_revetment_basis,
|
||||
)
|
||||
|
||||
DATA_DIR = ROOT / "resources" / "data_masonry"
|
||||
FIELDS = (ROOT / "B05_Profile" / "B05_Profile_UI_Drainage_Facility_Fields.ts").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
CHROME = (ROOT / "B06_Section" / "B06_Section_UI_Cross_Card_Chrome.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_TS_가_최신_판을_읽는다() -> None:
|
||||
latest = sorted(DATA_DIR.glob("revetment_sabang_*.json"))[-1].name
|
||||
imported = re.search(r'from "\.\./resources/data_masonry/(revetment_sabang_[^"]+)"', FIELDS)
|
||||
assert imported and imported.group(1) == latest
|
||||
|
||||
|
||||
def test_TS_가_쓰는_칸이_파일에_있다() -> None:
|
||||
latest = sorted(DATA_DIR.glob("revetment_sabang_*.json"))[-1]
|
||||
data = json.loads(latest.read_text(encoding="utf-8"))
|
||||
items = data["items"]
|
||||
assert data["scope"] and "사방(계류) 교본 기준" in data["scope"]
|
||||
assert items["height"]["rule"] == "계획홍수위 + 0.5~0.7m"
|
||||
assert items["face_slope"]["standard_ratio"] == [0.3, 0.5]
|
||||
assert items["crown_thickness"]["standard_m"] == [0.3, 0.5]
|
||||
assert "L7164" in items["backfill_pebble"]["source"]
|
||||
|
||||
|
||||
def test_B08_사유가_같은_근거를_쪽_줄_번호로() -> None:
|
||||
basis = own_revetment_basis()
|
||||
for ref in ("2-나:141", "3-가:181", "2-나:129", "3-가:151", "2-나:143", "L7164~7171"):
|
||||
assert ref in basis, ref
|
||||
assert "2-나:141" in own_height_unconfirmed()
|
||||
|
||||
|
||||
def test_B05_높이_칸은_제안_없이_설계자_입력() -> None:
|
||||
assert 'height: "",' in FIELDS and '"설계자 입력"' in FIELDS
|
||||
assert "기슭막이 높이 없음 — 근입만 그림" in CHROME
|
||||
@@ -68,7 +68,8 @@ def test_client_format_gisungmagi_maps_to_revetment():
|
||||
# 기준측점 전/후 5m — 레지스트리 기본값.
|
||||
assert (item.start_m, item.end_m) == (115.0, 125.0)
|
||||
assert item.options["form"] == "돌쌓기(메)"
|
||||
assert item.options["height_m"] == 2.5 and item.options["length_m"] == 10
|
||||
# 높이는 설계자 입력 — 등록부 기본 2.5 걷음(2026-09-14 브레인 판정 · 사방기술교본 2-나:141)
|
||||
assert "height_m" not in item.options and item.options["length_m"] == 10
|
||||
|
||||
|
||||
def test_client_format_escape_route_width_field_wins():
|
||||
|
||||
@@ -485,7 +485,9 @@ def test_revetment_range_options_like_slope_group():
|
||||
pipe_options = {option.key: option for option in by_id["pipe"].options}
|
||||
|
||||
assert item.placement == "point" and item.managed_by == "pipe_points"
|
||||
expected = {"height_m": 2.5, "length_m": 10, "before_m": 5, "after_m": 5}
|
||||
# ⚠ 높이는 2026-09-14 부터 **설계자 입력**(기본 없음) — 계획홍수위 + 0.5~0.7m(사방기술교본
|
||||
# 2-나:141 · 3-가:181) · 옛 2.5 는 근거 없어 걷음(브레인 판정).
|
||||
expected = {"height_m": None, "length_m": 10, "before_m": 5, "after_m": 5}
|
||||
for key, default in expected.items():
|
||||
assert key in options, f"revetment.{key} 옵션 없음"
|
||||
assert options[key].phase == "b05" and not options[key].required, key
|
||||
|
||||
@@ -247,7 +247,8 @@ def test_migrate_converts_legacy_stations(client, tmp_path):
|
||||
assert pipe["facility"] == "revetment" and pipe["chainage_m"] == 120.0
|
||||
# 구간·제원은 레지스트리 기본값 승계 — 기준측점 전/후 5m.
|
||||
assert (pipe["start_m"], pipe["end_m"]) == (115.0, 125.0)
|
||||
assert pipe["options"]["form"] == "돌쌓기(메)" and pipe["options"]["height_m"] == 2.5
|
||||
# 높이는 설계자 입력 — 등록부 기본 2.5 걷음(2026-09-14 브레인 판정)
|
||||
assert pipe["options"]["form"] == "돌쌓기(메)" and "height_m" not in pipe["options"]
|
||||
|
||||
|
||||
def test_migrate_without_pipe_points_defers_revetment(client):
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""메쌓기 높이 사유(B2) · 성토/절토 칸(B3) — 2026-09-14 브레인 판정 ㉰·㉲.
|
||||
|
||||
㉰ 두 기준 공존 — 임도기술교본 7-3:27 「메쌓기 2.0m 이하」 ↔ 품셈 13-4-4 [주]⑪ 원문 L7192
|
||||
「3m 이상이면 전부 또는 하부를 찰쌓기」. 막지 않고 사유(B08 줄) · 칸 경고(B05, `warn_above`).
|
||||
㉲ `face_role` — 비우면 단면유형 + 설치 측 자동 판정(계산이지 기본값이 아님) · 고르면 그 값이 이김.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import expand, stone_masonry # noqa: E402
|
||||
from common_util.common_util_structure_face_role import structure_face_role_of # noqa: E402
|
||||
|
||||
REGISTRY = json.loads(
|
||||
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
|
||||
)
|
||||
FIELDS = (ROOT / "B05_Profile" / "B05_Profile_UI_Structures_Fields.ts").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _options(type_id: str) -> dict[str, dict]:
|
||||
item = next(t for t in REGISTRY["types"] if t["type_id"] == type_id)
|
||||
return {o["key"]: o for o in item["options"]}
|
||||
|
||||
|
||||
def _height_notes(height: float, wet: bool) -> list[str]:
|
||||
_rows, notes = stone_masonry(height, 10.0, {"back_len_cm": 45}, wet)
|
||||
return [n for n in notes if "메쌓기 높이" in n]
|
||||
|
||||
|
||||
def test_메쌓기_높이_두_기준_사유() -> None:
|
||||
assert _height_notes(2.0, False) == []
|
||||
over_guide = _height_notes(2.5, False)
|
||||
assert len(over_guide) == 1 and "7-3:27" in over_guide[0] and "L7192" in over_guide[0]
|
||||
over_limit = _height_notes(3.0, False)
|
||||
assert len(over_limit) == 1 and "3m 이상이면" in over_limit[0]
|
||||
assert _height_notes(3.5, True) == [] # 찰쌓기는 이 사유 대상 아님
|
||||
|
||||
|
||||
def test_등록부_메쌓기_높이_칸은_경고만() -> None:
|
||||
height = _options("masonry_dry")["height_m"]
|
||||
assert height["warn_above"] == 2.0 and "L7192" in height["warn_message"]
|
||||
assert "is-outside-standard" in FIELDS and "warn_above" in FIELDS
|
||||
|
||||
|
||||
def test_성토절토_칸이_이긴다() -> None:
|
||||
assert structure_face_role_of("both_fill", {"face_role": "절토"}) == (
|
||||
"절토",
|
||||
"사용자 지정 절토면(성토/절토 칸)",
|
||||
)
|
||||
# 비우면 지금 판정 그대로
|
||||
assert structure_face_role_of("both_fill", {})[0] == "성토"
|
||||
for type_id in ("masonry_wet", "masonry_dry", "revetment"):
|
||||
role = _options(type_id)["face_role"]
|
||||
assert role["choices"] == ["성토", "절토"] and role["default"] is None, type_id
|
||||
|
||||
|
||||
def test_전개가_칸_값으로_기울기를_고른다() -> None:
|
||||
structure = {
|
||||
"structure_id": "w",
|
||||
"type_id": "masonry_wet",
|
||||
"start_m": 0.0,
|
||||
"end_m": 10.0,
|
||||
"options": {"height_m": 2.5, "length_m": 10.0, "back_len_cm": 45, "face_role": "절토"},
|
||||
}
|
||||
result = expand(structure, None, None, "both_fill")
|
||||
wall = next(c for c in result.components if c.name == "돌쌓기")
|
||||
# 찰쌓기 · 절토 · 직고 ≤3m → 1:0.25 (성토였으면 1:0.30)
|
||||
assert "절토" in wall.basis and "1:0.25" in wall.basis
|
||||
@@ -95,7 +95,7 @@ def test_유입구가_기슭막이인데_집수정_형식이_남아_있으면_
|
||||
|
||||
|
||||
def test_독립_기슭막이_한쪽이면_두_칸이_같을_때만_세고_다르면_사유() -> None:
|
||||
same = {"facility": "revetment", "chainage_m": 50.0, "options": {"side": "좌"}}
|
||||
same = {"facility": "revetment", "chainage_m": 50.0, "options": {"side": "좌", "height_m": 2.0}}
|
||||
rows = _table([same])["structures"]
|
||||
assert [r["name"] for r in rows] == ["기슭막이 · 좌 벽"] and rows[0]["components"]
|
||||
differ = {**same, "options": {"side": "우", "inlet_revet_height_m": 3.0}}
|
||||
@@ -103,6 +103,17 @@ def test_독립_기슭막이_한쪽이면_두_칸이_같을_때만_세고_다르
|
||||
assert not row["components"] and "서버가 못 가림" in row["notes"][-1]
|
||||
|
||||
|
||||
def test_독립_기슭막이_높이는_설계자_입력_없으면_미확정과_사방_근거() -> None:
|
||||
"""2026-09-14 브레인 판정 — 높이 = 계획홍수위 + 0.5~0.7m(사방기술교본 2-나:141 · 3-가:181)."""
|
||||
point = {"facility": "revetment", "chainage_m": 50.0, "options": {"side": "좌"}}
|
||||
(row,) = _table([point])["structures"]
|
||||
assert "높이를 안 적음" in row["unconfirmed"] and "2-나:141" in row["unconfirmed"]
|
||||
assert any("사방(계류) 교본 기준" in note and "3-가:151" in note for note in row["notes"])
|
||||
with_height = {**point, "options": {"side": "좌", "height_m": 2.0, "form": "돌쌓기(메)"}}
|
||||
(row,) = _table([with_height])["structures"]
|
||||
assert "높이를 안 적음" not in row["unconfirmed"]
|
||||
|
||||
|
||||
def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None:
|
||||
table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}])
|
||||
assert "산출식이 아직 없습니다" in table["structures"][0]["notes"][0]
|
||||
|
||||
Reference in New Issue
Block a user