fix(B08): 임의 · 실무 값 빈칸 — 관경 · 련 미입력 · 1본 프로젝트 값만 · 거창 계수 비율 미정 · 수심비 빈칸(143-2)

- 집계표 — 관경 · 련 빈칸 = 「미입력」 열(or 1000 · or 1 지움) · 관경 빈칸 흙막이 빈 높이 미입력 · 1본 = 프로젝트 층 변수만(시스템 층 폴백 지움)
- 토공집계표 — 성토면다짐 · 초류종자살포 · 지장목제거 = 빈칸 + 「비율 미정」 · 구조물 쪽 늘 빈 줄 8 뺌(나머지 7 유지)
- 수리 박스 — 수심비 처음 값 빈칸 · 빈칸이면 수심 · 유출량 멈춤 · 칸 옆 알림
- 좌측 패널 1본 읽기 전 빈칸 · 시험 test_69 · 98 · 122 · 73_11 고침 + 새 test_143_2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz
This commit is contained in:
2026-10-09 14:56:57 +09:00
co-authored by Claude Opus 5.5
parent b40a129cb5
commit 14c3f01f11
13 changed files with 189 additions and 77 deletions
@@ -12,7 +12,9 @@ Q · L 출처 = 저장된 유토곡선 배분(`mass_haul.haul_plan` — B07 토
(`free_haul` = 무대 · `dozer` = 불도우저 · `dump_truck` = 덤프트럭).
암 갈래 — 「연암」 · 「경암」 줄 = 암 전체 × 비율(좌측 패널 · 기본 90 : 10 · PLAN 122).
풍화암 · 보통암 · 구조물 쪽 줄(su2 시트)은 값이 없어 비움(None).
풍화암 · 보통암 · 표토제거 · 법면보호공 · 운반성토 줄은 값이 없어 비움(None) · 구조물 쪽 줄(su2 시트
구조물터파기 · 토사다짐 · 되메우기 · 잔토)은 뺌 · 거창 계수(성토면다짐 0.8 · 초류종자 성토 0.5 ·
지장목 0.8)는 근거가 없어 값 빈칸 + 「비율 미정」(PLAN 143-2).
짝 — `rock_split` · `split_rock_columns` ↔ `B08_Quantity_Options_Calc.ts` `rockSplit` ·
`splitRockColumns` · `splitRockSummary`(거울 시험 `test_122_b08_options_mirror`).
@@ -99,8 +101,8 @@ def summary_rows(
r0 = excel_round
free_q, dump_q, dozer_q = haul["free_haul"]["q"], haul["dump_truck"]["q"], haul["dozer"]["q"]
diverted = t["diverted_m3"]
tree = r0(t["tree_removal_fill_area_m2"] * 0.5 + t["tree_removal_cut_area_m2"], 2)
m3 = "㎥"
unsure = "비율 미정" # 거창 계수 자리 — 값을 확인한 뒤 다시 잇기(143-2)
return [
_row("흙깍기", "토사", "기계(굴삭기)", m3, r0(t["cut_soil_volume_m3"])), # 토적표!E137
_row("흙깍기", "풍화암", "굴삭기 + 브레카", m3, None),
@@ -121,26 +123,10 @@ def summary_rows(
_row("토사운반", "", "19 TON B/D", m3, dozer_q), # 평균운반거리 불도우저 ΣQ
_row("잔토처리", "", "백호우", m3, r0(t["balance_m3"])), # 토적표!T137
_row("층따기", "", "백호우", m3, t["benching_area_m2"]), # 토적표!W137
_row("성토면다짐", "", "", "㎡", r0(t["grading_fill_area_m2"] * 0.8), "성토면80%반영"),
_row("성토면다짐", "", "", "㎡", None, unsure), # 거창 = 성토면 × 0.8
_row("표토제거", "", "", "㎡", None),
_row(
"초류종자살포",
"",
"씨드스프레이",
"㎡",
r0(t["grading_fill_area_m2"] * 0.5 + t["grading_cut_area_m2"]),
"성토면50%반영+절토면",
),
_row("초류종자살포", "", "씨드스프레이", "㎡", None, unsure), # 거창 = 성토 × 0.5 + 절토
_row("법면보호공", "", "", "㎡", None),
# 토적표!AI138 = ROUND(성토 × 0.5 + 절토, 2) → × 0.8
_row("지장목제거", "", "", "㎡", r0(tree * 0.8), "80%반영"),
_row("구조물터파기", "토사", "기계(굴삭기)", m3, None, "구조물 수량"),
_row("구조물터파기", "풍화암", "굴삭기 + 브레카", m3, None),
_row("구조물터파기", "연암", "굴삭기 + 브레카", m3, None, "구조물 수량"),
_row("구조물터파기", "보통암", "굴삭기 + 브레카", m3, None),
_row("구조물터파기", "경암", "발파 + 굴삭기", m3, None),
_row("토사다짐", "", "", m3, None, "구조물 수량"),
_row("되메우기", "", "", m3, None, "구조물 수량"),
_row("잔토", "", "", m3, None, "현장내 성토(유용토)"),
_row("지장목제거", "", "", "㎡", None, unsure), # 거창 = (성토 × 0.5 + 절토) × 0.8
_row("운반성토", "", "BH+DT+BH", m3, None),
]
+2 -2
View File
@@ -102,8 +102,8 @@ def compute(inputs: dict[str, Any]) -> dict[str, Any]:
props = section_props(inputs["section"], diameter_mm / 1000.0) if diameter_mm else {}
elif sheet == "box":
width, height = _num(inputs, "box_width_m"), _num(inputs, "box_height_m")
ratio = _num(inputs, "depth_ratio") or SHEET_DEFAULTS["box"]["depth_ratio"]
if width and height:
ratio = _num(inputs, "depth_ratio") # 빈칸 = 수심 · 단면 · 유출량 멈춤(143-2)
if width and height and ratio:
depth = ratio * height
props = rect_props(width, depth)
else:
+3 -2
View File
@@ -56,10 +56,11 @@ SHEETS = {
"ford_pavement": ("ford", "물넘이포장"),
}
#: 장별 처음 값 — 박스 수심비 0.8H · n 0.013(울진 산출서 · 조사 69-7) · 내공 · 경사는 빈칸
#: 장별 처음 값 — 박스 n 0.013(KDS 콘크리트관) · 수심비 · 내공 · 경사는 빈칸(수심비 0.8 = 울진 산출서 —
#: 근거 없어 안 채움 · 빈칸이면 단면 계산 멈춤 · 143-2)
#: 세월교 · 물넘이 월류 폭 10 m(사용자 10-7) · n · 경사 = B05 세월교 높이 계산과 같은 값
SHEET_DEFAULTS: dict[str, dict[str, Any]] = {
"box": {"n": 0.013, "depth_ratio": 0.8, "box_width_m": None, "box_height_m": None},
"box": {"n": 0.013, "depth_ratio": None, "box_width_m": None, "box_height_m": None},
"ford": {
"n": FORD_MANNING_N,
"overflow_width_m": 10.0,
+54 -34
View File
@@ -8,7 +8,8 @@
· 길이 = 전 + 후 · 빈 높이 = 횡단도가 그리는 관경 최소 높이
돌붙임 › 찰붙임 · 메붙임 ㎡ 도수로/돌붙임 붙임 형식 · 면적(빈칸이면 안 셈 · 98-3)
관 공 › Φ{관경}m/m › 관매설 m B06 관 길이(1 m 올림 그대로)
› 커플링 ea 올림(L ÷ 1본) − 1 · 1본 = 프로젝트 층 M02 구조물집계표 변수
› 커플링 ea 올림(L ÷ 1본) − 1 · 1본 = 프로젝트 층 M02 구조물집계표 변수만
관경 · 련 빈칸 = 「미입력」 열(임의 값으로 안 채움 · 143-2)
› 관보호공 모양 실제로 선 집수정 모양마다 1 개소(98-2 `inlet_basin`)
BOX암거 › {가로×세로} › 본체 m B06 새 키 `box_length_m`(두 끝 offset 차 · 120)
› 날개벽 › 유입 · 유출 개소
@@ -86,6 +87,23 @@ def _num(value: Any) -> float | None:
return None
def _count(options: dict[str, Any], key: str) -> int | None:
"""관경 · 련 수 — 빈칸이면 None(「미입력」 열 · 임의 값으로 안 채움 · 143-2)."""
value = _num(options.get(key))
return int(value) if value else None
def _pipe_head(names: dict[str, str], diameter: int | None) -> list[str]:
"""「관 공」 머리 — 관경 빈칸이면 「미입력」."""
if diameter is None:
return ["관 공", fill.EMPTY_TEXT]
return ["관 공", f"Φ{names.get(f'pipe.diameter={diameter}', diameter)}m/m"]
def _key(value: int | None) -> str:
return fill.EMPTY_LABEL if value is None else str(value)
def _combo(item: Any) -> str | None:
"""집계 항목 → 조합 id(화면 `comboId` 와 같은 꼴) · 없으면 None."""
if not isinstance(item, dict) or not item.get("key"):
@@ -94,13 +112,10 @@ def _combo(item: Any) -> str | None:
def one_pipe_length(project_root: str | Path) -> float | None:
"""1본 길이(m) — 프로젝트 층 구조물집계표 변수 · 없으면 시스템 층 · 빈칸이면 None."""
for folder in (layers.project_dir(project_root), layers.system_dir()):
found = layers.read_template(folder, "table", TABLE_NAME)
if found is not None:
value = _num((found["문서"].get("변수") or {}).get(ONE_LENGTH_VAR))
return value if value and value > 0 else None
return None
"""1본 길이(m) — 프로젝트 층 구조물집계표 변수만(시스템 층으로 안 채움 · 143-2) · 빈칸 None."""
found = layers.read_template(layers.project_dir(project_root), "table", TABLE_NAME)
value = _num((found["문서"].get("변수") or {}).get(ONE_LENGTH_VAR)) if found else None
return value if value and value > 0 else None
def couplings(length_m: float | None, one_m: float | None) -> int | None:
@@ -344,10 +359,11 @@ def _ford(
combos: dict[str, str | None],
names: dict[str, str],
) -> None:
"""세월교 — Φ관경×n련마다 월류 폭 m · 본체 개소."""
diameter = int(_num(base["options"].get("pipe_diameter_mm")) or 1000)
count = int(_num(base["options"].get("pipe_count")) or 1)
spec = f"Φ{names.get(f'ford.diameter={diameter}', diameter)}×{count}련"
"""세월교 — Φ관경×n련마다 월류 폭 m · 본체 개소(관경 · 련 빈칸 = 「미입력」)."""
diameter = _count(base["options"], "pipe_diameter_mm")
count = _count(base["options"], "pipe_count")
size = fill.EMPTY_TEXT if diameter is None else names.get(f"ford.diameter={diameter}", diameter)
spec = f"Φ{size}×{fill.EMPTY_TEXT if count is None else count}련"
width_name = names.get("ford.width", "월류 폭").split(" (")[0]
cells = (
("width", width_name, "m", _num(base["options"].get("ford_width_m"))),
@@ -356,8 +372,8 @@ def _ford(
for index, (key, label, unit, value) in enumerate(cells):
sheet.add(
at,
f"ford|{diameter}|{count}|{key}",
(4, 0, diameter, count, index),
f"ford|{_key(diameter)}|{_key(count)}|{key}",
(4, 0, diameter or math.inf, count or math.inf, index),
[names["종류"], spec, label],
unit,
value,
@@ -380,31 +396,32 @@ def _ford_pipes(
`pipes` 에는 련마다 한 줄(좌측 패널 1본 길이로 커플링을 다시 셀 때 련 수만큼 셈) ·
조합 = 시설 본체 + 본체 잇기(`body` · 143-1).
"""
diameter = int(_num(base["options"].get("pipe_diameter_mm")) or 1000)
count = int(_num(base["options"].get("pipe_count")) or 1)
head = ["관 공", f"Φ{names.get(f'pipe.diameter={diameter}', diameter)}m/m"]
diameter = _count(base["options"], "pipe_diameter_mm")
count = _count(base["options"], "pipe_count")
head = _pipe_head(names, diameter)
combos = [_combo(base.get("summary_item")), body]
each = couplings(length, one_m)
sheet.add(
at,
f"len|{diameter}",
(2, diameter, 0),
f"len|{_key(diameter)}",
(2, diameter or math.inf, 0),
[*head, "관매설"],
"m",
length * count if length else None,
length * count if length and count else None,
combos,
)
sheet.add(
at,
f"cp|{diameter}",
(2, diameter, 1),
f"cp|{_key(diameter)}",
(2, diameter or math.inf, 1),
[*head, "커플링"],
"ea",
None if each is None else each * count,
None if each is None or count is None else each * count,
combos,
)
if any(combos):
pipes += [{"chainage_m": at, "column": f"pipe:cp|{diameter}", "length_m": length}] * count
column = f"pipe:cp|{_key(diameter)}"
pipes += [{"chainage_m": at, "column": column, "length_m": length}] * (count or 0)
def _pavement(
@@ -543,15 +560,15 @@ def pipe_summary(
no_ford += counted and not ford_length
_ford_pipes(sheet, at, base, ford_length, one_m, names, pipes, body)
continue
diameter = int(_num(base["options"].get("pipe_diameter_mm")) or 1000)
pipe_head = ["관 공", f"Φ{names.get(f'pipe.diameter={diameter}', diameter)}m/m"]
diameter = _count(base["options"], "pipe_diameter_mm")
pipe_head = _pipe_head(names, diameter)
pipe_combos = [_combo(base.get("summary_item")), links.get(LINKS["pipe"])]
length = base.get("관연장")
no_length += any(pipe_combos) and not length
sheet.add(
at,
f"len|{diameter}",
(2, diameter, 0),
f"len|{_key(diameter)}",
(2, diameter or math.inf, 0),
[*pipe_head, "관매설"],
"m",
length,
@@ -559,22 +576,23 @@ def pipe_summary(
)
sheet.add(
at,
f"cp|{diameter}",
(2, diameter, 1),
f"cp|{_key(diameter)}",
(2, diameter or math.inf, 1),
[*pipe_head, "커플링"],
"ea",
couplings(length, one_m),
pipe_combos,
)
if any(pipe_combos):
pipes.append({"chainage_m": at, "column": f"pipe:cp|{diameter}", "length_m": length})
column = f"pipe:cp|{_key(diameter)}"
pipes.append({"chainage_m": at, "column": column, "length_m": length})
shape = fill._nearest(basins, at)
no_basin += bool(links.get(LINKS["inlet"])) and shape is None
if shape:
sheet.add(
at,
f"basin|{diameter}|{shape}",
(2, diameter, 2 + _order(SHAPES, shape), shape),
f"basin|{_key(diameter)}|{shape}",
(2, diameter or math.inf, 2 + _order(SHAPES, shape), shape),
[*pipe_head, "관보호공"],
names.get(f"inlet.form={shape}", shape),
1.0,
@@ -583,7 +601,9 @@ 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)
# 관경 빈칸 = 빈 높이도 「미입력」(관경 최소 높이를 모름)
wall = pipe_wall_min_height_m(diameter / 1000) if diameter else None
_revets(sheet, at, options, links, sources, wall, names)
_chutes(sheet, at, options, links, names)
out = sheet.result()
@@ -28,7 +28,7 @@ const SUMMARY_COLUMNS: DataColumn[] = [
export function renderEarthworkSummary(host: HTMLElement, table: EarthworkTable): void {
const info = note(
"토적표 합계 · 평균운반거리 · 종방향 무대운반거리를 실무 토공집계표 식대로 묶음 · " +
"연암 · 경암 = 암 × 좌측 패널 비율 · 구조물 쪽 줄은 구조물 수량에서(- = 아직 값 없음)",
"연암 · 경암 = 암 × 좌측 패널 비율 · 「비율 미정」 = 반영 비율 확인 뒤 채움(- = 아직 값 없음)",
);
const grid = createDataTable({
columns: SUMMARY_COLUMNS,
+5 -1
View File
@@ -430,7 +430,11 @@ export function buildHydroSheet(
dims.append(
rowF("내공 폭 B (m)", null, numberInput("box_width_m", "입력")),
rowF("내공 높이 H (m)", null, numberInput("box_height_m", "입력")),
rowF("수심비 h / H", hint(`기본 ${sheetDefaults.depth_ratio}`), numberInput("depth_ratio")),
rowF(
"수심비 h / H",
hint("빈칸이면 계산 멈춤 — 값 넣기"),
numberInput("depth_ratio", "입력"),
),
rowF("수심 h (m)", formula("h = 수심비 × H"), outCell("depth")),
rowF("단면적", formula("A = B × h"), outCell("section_area")),
rowF("윤변", formula("P = B + 2h"), outCell("perimeter")),
+2 -1
View File
@@ -142,8 +142,9 @@ export function createOptionsPanel(projectId: string | null, onSaved: () => void
rockRow.className = "b08-options__row";
rockRow.append(soft.root, hard.root);
// 1본 = 프로젝트 값만(읽기 전 빈칸 · 143-2)
let saved: { one: number | null; soft: number; surcharge: Cells } = {
one: 8,
one: null,
soft: 90,
surcharge: PANEL_SURCHARGE,
};
+3 -2
View File
@@ -311,8 +311,9 @@ LINKED_PARTS = (("도면", "도면.json"), ("면적", "면적.json"), ("일위
def linked_snapshots(project_root: str | Path) -> dict[str, dict[str, Any]]:
"""연결된 사본 — 지정 ∩ 같은 항목 사본 `{구조물 id: {항목, 폴더, 부분, 경로}}`(PLAN 143-1).
구조물 id = `assignments` 키(관 지점 `pipe@측점` · 옵션 그룹 `{id}__{그룹}`) · 미지정 · 사본 없음 ·
다른 항목 사본은 안 담음(M02 `assign_items` 규칙) · 원본 층과 대조 안 함(빠른 읽기 · 쓰기 없음) ·
구조물 id = `assignments` 키(관 지점 `pipe@측점` · 옵션 그룹 `{id}__{그룹}`) ·
미지정 · 사본 없음 · 다른 항목 사본은 안 담음(M02 `assign_items` 규칙) ·
원본 층과 대조 안 함(빠른 읽기 · 쓰기 없음) ·
`경로` = manifest 원본 경로(가져온 때 M02 구분 · 이름) · B08 · B09 공용.
"""
base = structures_dir(project_root)
+5 -4
View File
@@ -40,9 +40,9 @@ def root(tmp_path, monkeypatch) -> Path:
return project
def test_기본값은_8_m_와_90_10_파일에_안_씀(root):
def test_1본_프로젝트_값_없으면_빈칸_90_10_파일에_안_씀(root):
assert options.read_options(root) == {
"one_pipe_length_m": 8,
"one_pipe_length_m": None, # 143-2 — 시스템 층 8 m 로 안 채움
"rock_ratios_pct": {"연암": 90.0, "경암": 10.0},
"material_surcharge_pct": {"sand": 12, "gravel": 5, "cement": 3}, # 124
}
@@ -51,7 +51,7 @@ def test_기본값은_8_m_와_90_10_파일에_안_씀(root):
def test_1본_6_m_저장_뒤_커플링을_다시_셈(root):
assert summary.couplings(22, summary.one_pipe_length(root)) == 2 # 올림(22/8) − 1
assert summary.couplings(22, summary.one_pipe_length(root)) is None # 프로젝트 값 없음
out = options.save_options(root, options.OptionsSave(one_pipe_length_m=6))
assert out["one_pipe_length_m"] == 6 == summary.one_pipe_length(root)
assert summary.couplings(22, 6) == 3 and summary.couplings(16, 6) == 2
@@ -63,6 +63,7 @@ def test_1본_6_m_저장_뒤_커플링을_다시_셈(root):
# 다시 저장 = 같은 사본 판 위에 씀
options.save_options(root, options.OptionsSave(one_pipe_length_m=8))
assert summary.one_pipe_length(root) == 8
assert summary.couplings(22, summary.one_pipe_length(root)) == 2 # 올림(22/8) − 1
def test_비율_70_30_저장_뒤_남고_다른_칸은_그대로(root):
@@ -79,7 +80,7 @@ def test_비율_70_30_저장_뒤_남고_다른_칸은_그대로(root):
assert stored["rock_methods"] == {"연암": "ripping"}
assert options.read_options(root)["rock_ratios_pct"]["연암"] == 70.0
# 1본 길이는 안 보냈으니 그대로
assert options.read_options(root)["one_pipe_length_m"] == 8
assert options.read_options(root)["one_pipe_length_m"] is None
def test_비율_읽기_자름_경암만_있을_때():
@@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
"""B08 구조물 집계표 — 관경 · 련 빈칸 = 「미입력」(임의 1000 · 1 로 안 채움 · PLAN 143-2).
견본: 100 관 관경 빈칸 L16 · 유출 흙막이 높이 빈칸 / 200 세월교 Φ1000 련 빈칸 · 관 길이 9.
"""
from __future__ import annotations
import json
from pathlib import Path
from B08_Quantity import B08_Quantity_Router_Summary as summary
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
ITEM = {"layer": "system", "owner": None, "key": "SS000001", "name": "견본"}
POINTS = [
{
"chainage_m": 100.0,
"options": {
"pipe_diameter_mm": "",
"outlet_type": "기슭막이",
"outlet_revet_form": "돌쌓기(메)",
"outlet_revet_before_m": 5,
"outlet_revet_after_m": 5,
},
"summary_item": ITEM,
"group_items": {"g9lq9uk": ITEM},
},
{
"chainage_m": 200.0,
"facility": "ford_bridge",
"options": {"pipe_diameter_mm": 1000, "pipe_count": "", "ford_width_m": 12},
"summary_item": ITEM,
},
]
DESIGNS = [
{"chainage_m": 100.0, "design": {"pipe_length_m": 16}},
{"chainage_m": 200.0, "design": {"ford_pipe_length_m": 9}},
]
def _run(tmp_path: Path) -> dict:
root = tmp_path / "proj"
edits = root / "B04_PreProcess/drainage/edits"
edits.mkdir(parents=True)
points = [{**p, "source": "user"} for p in POINTS]
(edits / "pipe_points.json").write_text(
json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8"
)
names = definition.read()["종류"]["pipe"]["이름"]
linked = {owner: {} for owner in snapshots.assignments(root)}
return summary.pipe_summary(root, DESIGNS, names, 6.0, linked=linked)
def _cell(out: dict, at: float, cid: str):
row = next(r for r in out["rows"] if abs(r["chainage_m"] - at) < 0.01)
return row["values"].get(cid)
def test_관경_빈칸은_미입력_열(tmp_path):
out = _run(tmp_path)
cols = {c["id"]: c for c in out["columns"]}
assert cols["pipe:len|-"]["group"] == ["관 공", "미입력", "관매설"]
assert _cell(out, 100, "pipe:len|-") == 16
assert _cell(out, 100, "pipe:cp|-") == 2 # 1본 6 m — 관경과 무관
assert not any(c.startswith("pipe:len|1000") and _cell(out, 100, c) for c in cols)
# 흙막이 빈 높이 = 관경 최소 높이를 모름 → 「미입력」
assert cols["pipe:revet|돌쌓기(메)|-"]["group"][2] == "미입력"
assert _cell(out, 100, "pipe:revet|돌쌓기(메)|-") == 10
def test_세월교_련_빈칸은_미입력_관_몫_빈칸(tmp_path):
out = _run(tmp_path)
cols = {c["id"]: c for c in out["columns"]}
assert cols["pipe:ford|1000|-|width"]["group"][1] == "Φ1000×미입력련"
assert _cell(out, 200, "pipe:ford|1000|-|width") == 12
assert _cell(out, 200, "pipe:len|1000") is None # 련 수를 모르면 관 몫도 빈칸
assert _cell(out, 200, "pipe:cp|1000") is None
assert not [p for p in out["pipes"] if p["column"] == "pipe:cp|1000"]
@@ -195,12 +195,15 @@ def test_토공집계표_식() -> None:
assert value[("토사운반", "", "19 TON B/D")] == 20
assert value[("유용토 소계", "", "")] == pytest.approx(t["diverted_m3"] + 82)
assert value[("잔토처리", "", "백호우")] == -98
assert value[("성토면다짐", "", "")] == excel_round(101.7 * 0.8)
assert value[("초류종자살포", "", "씨드스프레이")] == excel_round(101.7 * 0.5 + 15.3)
tree = excel_round(133.2 * 0.5 + 82.8, 2)
assert value[("지장목제거", "", "")] == excel_round(tree * 0.8)
# 143-2 — 거창 계수(0.8 · 0.5) 자리는 빈칸 + 「비율 미정」 · 구조물 쪽 줄 8 뺌 · 나머지 7 빈칸
notes = {r["group"]: r["note"] for r in rows}
for group in ("성토면다짐", "초류종자살포", "지장목제거"):
assert next(r["value"] for r in rows if r["group"] == group) is None
assert notes[group] == "비율 미정"
assert value[("표토제거", "", "")] is None
assert len(rows) == 33 # 시트 C5:C37
assert not {"구조물터파기", "토사다짐", "되메우기", "잔토"} & {r["group"] for r in rows}
assert sum(r["value"] is None and r["note"] != "비율 미정" for r in rows) == 7
assert len(rows) == 25 # 시트 C5:C37 33 − 구조물 쪽 8
def test_토적표_머리_글자는_사용자_견본대로():
@@ -56,15 +56,27 @@ def test_장은_시설이_정함() -> None:
def test_박스_울진_산출서_단면() -> None:
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] == 0.8
# 143-2 — 수심비 처음 값 빈칸(울진 0.8 은 손으로 넣은 값으로만)
r = compute(
_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, depth_ratio=0.8)
)
assert SHEET_DEFAULTS["box"]["n"] == 0.013 and SHEET_DEFAULTS["box"]["depth_ratio"] is None
assert r["n"] == 0.013
assert r["depth"] == pytest.approx(1.2)
assert r["section_area"] == pytest.approx(3.6)
assert r["perimeter"] == pytest.approx(5.4)
assert r["radius"] == pytest.approx(3.6 / 5.4)
# 실무 29.236 = 1/n 75 · R 0.67 반올림 — 같은 n 이면 R 반올림 차(0.3 %)만 남음
r75 = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50, n=1 / 75))
r75 = compute(
_inputs(
"box",
box_width_m=3.0,
box_height_m=1.5,
slope_denominator=50,
n=1 / 75,
depth_ratio=0.8,
)
)
assert r75["capacity"] == pytest.approx(29.236, rel=0.004)
assert 75 * 0.67 ** (2 / 3) * math.sqrt(1 / 50) * 3.6 == pytest.approx(29.236, abs=0.001)
assert r["capacity"] > r75["capacity"] # n 고치면 따라감
@@ -79,6 +91,9 @@ def test_박스_내공_경사_빈칸이면_대시() -> None:
r = compute(_inputs("box"))
assert r["design_flow"] is not None
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
# 수심비 빈칸 = 내공 · 경사가 있어도 계산 멈춤(143-2)
r = compute(_inputs("box", box_width_m=3.0, box_height_m=1.5, slope_denominator=50))
assert r["depth"] is None and r["capacity"] is None and r["ok"] is None
@pytest.fixture(scope="module")
+1 -1
View File
@@ -215,7 +215,7 @@ def test_1본_길이는_프로젝트_층_변수(root, tmp_path, monkeypatch):
json.dumps({"변수": {"파형강관_1본_m": 8}}), encoding="utf-8"
)
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
assert summary.one_pipe_length(root) == 8
assert summary.one_pipe_length(root) is None # 143-2 — 시스템 층으로 안 채움
mine = layers.template_path(layers.project_dir(root), "table", "구조물집계표")
mine.parent.mkdir(parents=True)
mine.write_text(json.dumps({"변수": {"파형강관_1본_m": 6}}), encoding="utf-8")