From 14c3f01f11d420077da4b114de193da309808264 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 9 Oct 2026 14:56:57 +0900 Subject: [PATCH] =?UTF-8?q?fix(B08):=20=EC=9E=84=EC=9D=98=20=C2=B7=20?= =?UTF-8?q?=EC=8B=A4=EB=AC=B4=20=EA=B0=92=20=EB=B9=88=EC=B9=B8=20=E2=80=94?= =?UTF-8?q?=20=EA=B4=80=EA=B2=BD=20=C2=B7=20=EB=A0=A8=20=EB=AF=B8=EC=9E=85?= =?UTF-8?q?=EB=A0=A5=20=C2=B7=201=EB=B3=B8=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B0=92=EB=A7=8C=20=C2=B7=20=EA=B1=B0=EC=B0=BD=20?= =?UTF-8?q?=EA=B3=84=EC=88=98=20=EB=B9=84=EC=9C=A8=20=EB=AF=B8=EC=A0=95=20?= =?UTF-8?q?=C2=B7=20=EC=88=98=EC=8B=AC=EB=B9=84=20=EB=B9=88=EC=B9=B8(143-2?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 집계표 — 관경 · 련 빈칸 = 「미입력」 열(or 1000 · or 1 지움) · 관경 빈칸 흙막이 빈 높이 미입력 · 1본 = 프로젝트 층 변수만(시스템 층 폴백 지움) - 토공집계표 — 성토면다짐 · 초류종자살포 · 지장목제거 = 빈칸 + 「비율 미정」 · 구조물 쪽 늘 빈 줄 8 뺌(나머지 7 유지) - 수리 박스 — 수심비 처음 값 빈칸 · 빈칸이면 수심 · 유출량 멈춤 · 칸 옆 알림 - 좌측 패널 1본 읽기 전 빈칸 · 시험 test_69 · 98 · 122 · 73_11 고침 + 새 test_143_2 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01STqEqztUthVFBmEP18wBSz --- .../B08_Quantity_Engine_Earthwork_Summary.py | 28 ++---- B08_Quantity/B08_Quantity_Engine_Hydro.py | 4 +- B08_Quantity/B08_Quantity_Hydro_Defaults.py | 5 +- B08_Quantity/B08_Quantity_Router_Summary.py | 88 ++++++++++++------- .../B08_Quantity_UI_Earthwork_Sheets.ts | 2 +- B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts | 6 +- B08_Quantity/B08_Quantity_UI_Options.ts | 3 +- M02_MasterTemplete/M02_Item_Snapshot.py | 5 +- resources/tester/test_122_b08_options.py | 9 +- .../tester/test_143_2_b08_blank_values.py | 80 +++++++++++++++++ .../tester/test_69_b08_earthwork_tabs.py | 13 +-- .../tester/test_73_11_b08_hydro_box_ford.py | 21 ++++- resources/tester/test_98_b08_pipe_summary.py | 2 +- 13 files changed, 189 insertions(+), 77 deletions(-) create mode 100644 resources/tester/test_143_2_b08_blank_values.py diff --git a/B08_Quantity/B08_Quantity_Engine_Earthwork_Summary.py b/B08_Quantity/B08_Quantity_Engine_Earthwork_Summary.py index 080ea669c..406f49d41 100644 --- a/B08_Quantity/B08_Quantity_Engine_Earthwork_Summary.py +++ b/B08_Quantity/B08_Quantity_Engine_Earthwork_Summary.py @@ -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), ] diff --git a/B08_Quantity/B08_Quantity_Engine_Hydro.py b/B08_Quantity/B08_Quantity_Engine_Hydro.py index fd70ed906..adf8ae043 100644 --- a/B08_Quantity/B08_Quantity_Engine_Hydro.py +++ b/B08_Quantity/B08_Quantity_Engine_Hydro.py @@ -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: diff --git a/B08_Quantity/B08_Quantity_Hydro_Defaults.py b/B08_Quantity/B08_Quantity_Hydro_Defaults.py index 311c4e4e0..c3065da53 100644 --- a/B08_Quantity/B08_Quantity_Hydro_Defaults.py +++ b/B08_Quantity/B08_Quantity_Hydro_Defaults.py @@ -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, diff --git a/B08_Quantity/B08_Quantity_Router_Summary.py b/B08_Quantity/B08_Quantity_Router_Summary.py index fe16b794a..4e840da35 100644 --- a/B08_Quantity/B08_Quantity_Router_Summary.py +++ b/B08_Quantity/B08_Quantity_Router_Summary.py @@ -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() diff --git a/B08_Quantity/B08_Quantity_UI_Earthwork_Sheets.ts b/B08_Quantity/B08_Quantity_UI_Earthwork_Sheets.ts index 5f09a2827..ba97716f5 100644 --- a/B08_Quantity/B08_Quantity_UI_Earthwork_Sheets.ts +++ b/B08_Quantity/B08_Quantity_UI_Earthwork_Sheets.ts @@ -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, diff --git a/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts b/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts index bc0e84a06..e47659aee 100644 --- a/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts +++ b/B08_Quantity/B08_Quantity_UI_Hydro_Sheet.ts @@ -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")), diff --git a/B08_Quantity/B08_Quantity_UI_Options.ts b/B08_Quantity/B08_Quantity_UI_Options.ts index 2016d91ce..599fe4035 100644 --- a/B08_Quantity/B08_Quantity_UI_Options.ts +++ b/B08_Quantity/B08_Quantity_UI_Options.ts @@ -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, }; diff --git a/M02_MasterTemplete/M02_Item_Snapshot.py b/M02_MasterTemplete/M02_Item_Snapshot.py index 3e750d7cc..a85122e7d 100644 --- a/M02_MasterTemplete/M02_Item_Snapshot.py +++ b/M02_MasterTemplete/M02_Item_Snapshot.py @@ -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) diff --git a/resources/tester/test_122_b08_options.py b/resources/tester/test_122_b08_options.py index 7b38b50f6..ddb457020 100644 --- a/resources/tester/test_122_b08_options.py +++ b/resources/tester/test_122_b08_options.py @@ -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_비율_읽기_자름_경암만_있을_때(): diff --git a/resources/tester/test_143_2_b08_blank_values.py b/resources/tester/test_143_2_b08_blank_values.py new file mode 100644 index 000000000..31fcd2f31 --- /dev/null +++ b/resources/tester/test_143_2_b08_blank_values.py @@ -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"] diff --git a/resources/tester/test_69_b08_earthwork_tabs.py b/resources/tester/test_69_b08_earthwork_tabs.py index f29e0890a..63ea0b737 100644 --- a/resources/tester/test_69_b08_earthwork_tabs.py +++ b/resources/tester/test_69_b08_earthwork_tabs.py @@ -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_토적표_머리_글자는_사용자_견본대로(): diff --git a/resources/tester/test_73_11_b08_hydro_box_ford.py b/resources/tester/test_73_11_b08_hydro_box_ford.py index 1d5f6a96a..6593ffd1d 100644 --- a/resources/tester/test_73_11_b08_hydro_box_ford.py +++ b/resources/tester/test_73_11_b08_hydro_box_ford.py @@ -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") diff --git a/resources/tester/test_98_b08_pipe_summary.py b/resources/tester/test_98_b08_pipe_summary.py index c65fb5991..f851b9b4a 100644 --- a/resources/tester/test_98_b08_pipe_summary.py +++ b/resources/tester/test_98_b08_pipe_summary.py @@ -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")