Merge remote-tracking branch 'origin/dev' into main_laptop_1

This commit is contained in:
2026-09-15 02:48:55 +09:00
17 changed files with 517 additions and 40 deletions
@@ -76,6 +76,12 @@
"type_id": "ford_pavement",
"reuse_count": null,
"note": "물넘이포장은 거푸집이 서지 않는 구조(면 포장) — 대상 아님"
},
{
"type_id": "box_culvert",
"reuse_count": 6,
"matched_example": "관의 기초",
"note": "BOX암거 합판은 **버림 옆면뿐**(본체는 유로폼 — 12-38 유형 「박스」 보통) — 원문 6회 줄 「관의 기초 등 극히 간단한 구조」 · 실무 울진 2×2 「합판6회」 와 같음(2026-09-15)"
}
],
"reuse_ratio_pct": {
@@ -99,12 +105,11 @@
}
},
"shoring": {
"note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 지금 서는 구조물(옹벽·집수정)은 벽체 거푸집만이라 대상이 아니다.",
"note": "강관동바리(품셈 12-20)는 **슬래브를 떠받칠 때** 필요하다. 옹벽·집수정은 벽체 거푸집만이라 대상이 아니고, BOX암거는 전개식이 동바리 줄을 낸다(2026-09-15).",
"targets_pending": [
"box_culvert",
"ford_bridge"
],
"why": "그 둘은 원단위·치수가 미확보라 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보"
"why": "세월교는 수량 근거가 없어 슬래브 면적 자체가 안 나온다 — 동바리도 함께 미확보"
},
"euroform_type": {
"note": "유로폼 설치·해체 유형(간단/보통/복잡) — **품셈 12-38-3 [주]④ 원문**이 시설 예시로 갈라 둔다. 거푸집 사용횟수(1-7-1)·철근 갈래(12-3 [주]①)와 같은 자리로, 사람이 고르는 값이 아니다.",
@@ -560,15 +560,10 @@
"not_found": {
"note": "규격은 우리 모델에 있으나 **관측 원단위가 어디에도 없는** 것. 지어내지 않는다.",
"items": [
{
"type_id": "box_culvert",
"why": "울진 2공구에 BOX암거가 실재하나 원단위 라이브러리에 탭이 없음. 게다가 structures.json 의 BOX 제원은 body_width_m·body_height_m 와 날개벽뿐이라 **벽·저판·상판 두께가 없어 전개식도 못 세움**.",
"needs": "표준 단면(벽·저판·상판 두께) 확보 — 사용자 확정 대기"
},
{
"type_id": "ford_bridge",
"why": "세월교 본체(날개벽 포함) 원단위 없음. 관 부분은 pipe 로 따로 섬.",
"needs": "표준도 물량 또는 실무 관측"
"why": "세월교 수량 근거 없음 — 법령·임도기술교본은 설치 조건만(그림 3-18 은 배치 모식도) · 실무 원본(울진 4공구·오솔길 전수)에 세월교 시트·도면 없음. 관 부분은 pipe 로 따로 섬(2026-09-15 브레인 판정 ⑥ — 사유로 닫음).",
"needs": "세월교 표준도 물량 또는 실무 관측 — 나오면 다시 엶"
},
{
"type_id": "retaining_wall",
@@ -435,6 +435,62 @@
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김",
"placing_note": "타설 코드는 프로젝트 설정의 타설 방식으로 갈림(기본 레디믹스트). 철근구조물 판정은 원단위의 D13·D16 에서 자동으로 나옴.",
"parts_note": "조각마다 **어느 성분에서 나오는지**(`from_components`)를 적는다. 이름을 바꾸면 물량이 조용히 0 이 되므로 원단위 성분 이름과 정확히 같아야 하고, 못 찾으면 드러낸다. `unit_from` 이 있으면 그 단위에서 목표 단위로 환산한다 — **철근은 ㎏ → ton (÷1000)**. ⚠ 단위를 안 맞추면 1000배 틀린다."
},
{
"type_id": "box_culvert",
"parts": [
{
"code": "FP-12-01-01",
"name": "콘크리트 타설",
"unit": "㎥",
"from_components": ["콘크리트", "버림콘크리트"],
"kind_suffix": "concrete_placing"
},
{
"code": "FP-12-38",
"name": "유로폼",
"unit": "㎡",
"from_components": ["유로폼"],
"kind_suffix": "euroform_type"
},
{
"code": "FP-12-04",
"name": "합판거푸집",
"unit": "㎡",
"from_components": ["합판거푸집"],
"kind_suffix": "formwork_reuse"
},
{
"code": "FP-12-03",
"name": "철근 현장가공 및 조립",
"unit": "ton",
"from_components": ["이형철근 D13", "이형철근 D16"],
"unit_from": "kg",
"kind_suffix": "rebar_complexity",
"why": "철근은 크기별 식이 없어 울진 기번3 구조도 2×2 관측값만 섬 — 다른 크기는 이 조각이 비어 묶음이 막힘(2026-09-15 브레인 판정 ②)"
},
{
"code": "FP-12-20",
"name": "강관동바리",
"unit": "㎥",
"from_components": ["동바리"]
},
{
"code": "FP-12-19",
"name": "강관비계",
"unit": "㎡",
"from_components": ["비계"]
},
{
"code": "FP-12-25",
"name": "기초잡석",
"unit": "㎥",
"from_components": ["기초잡석"],
"why": "BOX 근거 둘(예제·울진)에는 잡석 줄이 없으나 **버림이 선 구조물엔 기초잡석이 함께 섬**(사용자 확정 3차 ② · 옹벽과 같은 규칙) — 원단위에 선 줄을 묶음에서 빼면 수량만 보이고 금액이 빠짐"
}
],
"why": "품셈 12장에 「BOX암거」 공종이 없음 — 산림과임업기술(임도) 5장 수량산출 예제가 구체콘크리트·기초·거푸집·동바리·철근으로 가르고, 실무 울진 기번3 「암거수량집계표(2×2)」가 레미콘·버림·유로폼·합판·철근·동바리·비계로 셈(2026-09-15 브레인 12장 D).",
"needs": "일위대가 조립은 B09 몫 — B08 은 물량과 묶음만 넘김"
}
],
"unit_conversion": {
+6 -8
View File
@@ -19,31 +19,29 @@ if str(ROOT) not in sys.path:
from B06_Section.B06_Section_Engine_Culvert import ( # noqa: E402
BOX_COVER_M,
FORD_SLAB_THICKNESS_M,
FORD_WALL_THICKNESS_M,
_box_set,
attach_culvert_sets,
)
def test_box_set_defaults() -> None:
"""저장 옵션이 없으면 레지스트리 기본 2.0×2.0에 세월교 두께를 얹는다."""
"""옵션이 없으면 등록부 기본 2.0×2.0 · 두께도 등록부 제안값(2026-09-15 · 세월교 승계 걷음)."""
spec = _box_set(None)
assert spec["type"] == "box"
assert spec["inner_width_m"] == 2.0
assert spec["inner_height_m"] == 2.0
assert spec["wall_thickness_m"] == FORD_WALL_THICKNESS_M
assert spec["slab_thickness_m"] == FORD_SLAB_THICKNESS_M
assert spec["top_thickness_m"] == FORD_SLAB_THICKNESS_M
assert spec["wall_thickness_m"] == 0.3
assert spec["slab_thickness_m"] == 0.25
assert spec["top_thickness_m"] == 0.25
assert spec["cover_m"] == BOX_COVER_M
# 도로 방향 길이 = 내공 폭 + 측벽 두 장.
assert spec["span_m"] == 2.0 + 2 * FORD_WALL_THICKNESS_M
assert spec["span_m"] == 2.0 + 2 * 0.3
def test_box_wing_extends_slab() -> None:
"""저판 편측 연장 = 날개벽 길이 × cos(각도). 세월교와 같은 산식이다."""
spec = _box_set({"body_width_m": 3, "body_height_m": 3, "wing_in_length_m": 2})
assert spec["span_m"] == 3 + 2 * FORD_WALL_THICKNESS_M
assert spec["span_m"] == 3 + 2 * 0.3
assert spec["wing_in"]["slab_extend_m"] == round(2 * math.cos(math.radians(45)), 3)
@@ -0,0 +1,61 @@
"""BOX암거 두께·연장 칸 — 등록부가 정본, B06 그림이 그 칸을 읽음 (2026-09-15 브레인 12장 D ①③).
① 제안값 = 산림과임업기술(임도) 5장 수량산출 예제 — 벽 0.3 · 판 0.25 · 헌치 0.2 · 버림 0.1
근거에 KDS 44 90 00 4.5.2 「부재 최소두께 300㎜」(도로 기준)를 나란히 — 판 0.25 는 못 미침
② 종전 B06 상수(벽 0.2 · 판 0.3 — 세월교 값 승계)는 근거 없는 값이라 걷음
③ 연장 칸 — 기본값 없음
"""
from __future__ import annotations
import json
from pathlib import Path
from B06_Section.B06_Section_Engine_Culvert import _box_set
ROOT = Path(__file__).resolve().parents[2]
def _box_options() -> dict[str, dict]:
registry = json.loads(
(ROOT / "B05_Profile" / "B05_Profile_Structure_Types.json").read_text(encoding="utf-8")
)
types = registry if isinstance(registry, list) else registry.get("types") or []
box = next(t for t in types if t["type_id"] == "box_culvert")
return {option["key"]: option for option in box["options"]}
def test_등록부에_두께_헌치_버림_연장_칸이_선다() -> None:
options = _box_options()
expected = {
"wall_thickness_m": 0.3,
"slab_thickness_m": 0.25,
"haunch_m": 0.2,
"blinding_thickness_m": 0.1,
}
for key, value in expected.items():
assert options[key]["default"] == value, key
assert "산림과임업기술" in options[key]["default_basis"], key
# 판 0.25 는 KDS 도로 최소 300㎜ 에 못 미침 — 나란히 적어 설계자가 고르게.
assert "KDS" in options["slab_thickness_m"]["default_basis"]
assert "300" in options["slab_thickness_m"]["default_basis"]
assert "KDS" in options["wall_thickness_m"]["default_basis"]
# 연장은 기본값 없음.
assert options["length_m"]["default"] is None
def test_B06_세트가_등록부_칸을_읽는다() -> None:
spec = _box_set(None)
assert (spec["wall_thickness_m"], spec["slab_thickness_m"], spec["top_thickness_m"]) == (
0.3,
0.25,
0.25,
)
assert spec["span_m"] == 2.0 + 2 * 0.3
stored = _box_set({"wall_thickness_m": 0.35, "slab_thickness_m": 0.3})
assert (stored["wall_thickness_m"], stored["slab_thickness_m"], stored["top_thickness_m"]) == (
0.35,
0.3,
0.3,
)
assert stored["span_m"] == 2.0 + 2 * 0.35
@@ -0,0 +1,102 @@
"""BOX암거 전개식 — m당 × 연장 (2026-09-15 브레인 12장 D 판정 ①~⑤).
근거 둘(구조 같음)
산림과임업기술(임도) 5장 「구조물도에 의한 수량산출(예)」 — 구체 = (외곽 − 유수구 + 헌치) ·
기초 폭×두께 · 거푸집 외벽·유수구·받침판·귀면 · 동바리 유수구 단면
실무 울진 기번3 구조도 「암거수량집계표(2×2)」 m당 — 레미콘 2.84 · 버림 0.28 · 유로폼 11.132 ·
합판 0.2 · 철근 0.336t(H13 0.179 · H16 0.157) · 동바리 3.92 · 비계 벽체 5.2 + 시종점 13.52
⚠ 철근은 크기별 식이 없어 **2×2 관측값만** · 날개벽·토공은 사유.
"""
from __future__ import annotations
import pytest
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff
from B08_Quantity.B08_Quantity_Engine_Pipe import facility_structures
from B08_Quantity.B08_Quantity_Engine_UnitQuantity import build_table, expand
ULJIN_2X2 = {
"body_width_m": 2.0,
"body_height_m": 2.0,
"wall_thickness_m": 0.3,
"slab_thickness_m": 0.3,
"haunch_m": 0.2,
"blinding_thickness_m": 0.1,
"length_m": 10.0,
}
def _amounts(options: dict) -> tuple[dict, list[str], object]:
result = expand({"type_id": "box_culvert", "options": options}, {"box_culvert": "BOX암거"})
return (
{(c.name, c.spec): c.amount for c in result.components},
result.notes,
result,
)
def test_울진_2x2_m당_값을_연장에_곱한다() -> None:
got, notes, result = _amounts(ULJIN_2X2)
assert got[("콘크리트", "")] == pytest.approx(2.84 * 10)
assert got[("버림콘크리트", "")] == pytest.approx(0.28 * 10)
assert got[("유로폼", "")] == pytest.approx(
11.1314 * 10, abs=0.01
) # 원문 11.132(귀면 0.566 반올림)
assert got[("합판거푸집", "")] == pytest.approx(0.2 * 10)
assert got[("이형철근", "D13")] == pytest.approx(179 * 10)
assert got[("이형철근", "D16")] == pytest.approx(157 * 10)
assert got[("동바리", "")] == pytest.approx(3.92 * 10)
# 비계 — 벽체 m당 5.2 × 연장 + 시종점 두 면 13.52 는 한 번(원문은 m당에 넣었음 · 사유).
assert got[("비계", "")] == pytest.approx(5.2 * 10 + 13.52)
assert (result.billing_unit, result.billing_quantity) == ("m", 10.0)
text = " ".join(notes)
assert "관측" in text and "날개벽" in text
def test_예제_두께_기본값으로_구체가_줄어든다() -> None:
got, _notes, _r = _amounts({**ULJIN_2X2, "slab_thickness_m": 0.25})
# (2.6 × 2.5 − 2 × 2 + 0.2² ÷ 2 × 4) = 2.58㎥/m
assert got[("콘크리트", "")] == pytest.approx(2.58 * 10)
def test_2x2_가_아니면_철근은_사유() -> None:
got, notes, _r = _amounts({**ULJIN_2X2, "body_width_m": 3.0})
assert not [key for key in got if key[0] == "이형철근"]
assert any("크기별 식 없음" in note and "2×2" in note for note in notes)
assert got[("콘크리트", "")] > 0 # 나머지는 섬
def test_연장이_없으면_안_선다() -> None:
got, notes, _r = _amounts({k: v for k, v in ULJIN_2X2.items() if k != "length_m"})
assert got == {}
assert any("연장" in note for note in notes)
def test_치수를_안_적으면_기본값으로_서되_미확정() -> None:
rows = facility_structures(
[{"chainage_m": 50.0, "facility": "box_culvert", "options": {"length_m": 8.0}}]
)
box = next(row for row in rows if row["type_id"] == "box_culvert")
assert box["unconfirmed"] and "측벽 두께" in box["unconfirmed"]
assert box["options"]["wall_thickness_m"] == 0.3 and box["options"]["slab_thickness_m"] == 0.25
stored = facility_structures(
[{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}]
)
assert not next(r for r in stored if r["type_id"] == "box_culvert").get("unconfirmed")
def test_인계_묶음이_조각을_세운다() -> None:
table = build_table(
facility_structures(
[{"chainage_m": 50.0, "facility": "box_culvert", "options": ULJIN_2X2}]
),
{"box_culvert": "BOX암거"},
)
row = next(
r for r in build_handoff(unit_quantity_table=table)["work_items"] if "BOX" in r["name"]
)
parts = {part["code"].split("#")[0]: part for part in row["composite_parts"] or []}
assert {"FP-12-01-01", "FP-12-04", "FP-12-38", "FP-12-03", "FP-12-20", "FP-12-19"} <= set(parts)
assert parts["FP-12-03"]["quantity"] == pytest.approx((179 + 157) * 10 / 1000)
assert parts["FP-12-20"]["quantity"] == pytest.approx(3.92 * 10)
@@ -116,7 +116,8 @@ def test_독립_기슭막이_높이는_설계자_입력_없으면_미확정과_
def test_산출식_없는_세월교는_사유로_서고_터파기_빠짐_줄을_안_만든다() -> None:
table = _table([{"facility": "ford_bridge", "chainage_m": 30.0, "options": {}}])
assert "산출식이 아직 없습니다" in table["structures"][0]["notes"][0]
# 2026-09-15 브레인 판정 ⑥ — 「아직 없음」이 아니라 「근거 없음」으로 닫음.
assert "세월교 수량 근거 없음" in table["structures"][0]["notes"][0]
rows = build_handoff(unit_quantity_table=table)["work_items"]
assert not any("터파기가 안 선 구조물" in str(row.get("spec_detail")) for row in rows)
+15 -1
View File
@@ -114,8 +114,22 @@ def test_동바리는_대상이_없으면_없다고_말할것() -> None:
status = shoring_status()
assert status["applicable"] is False
assert status["reason"]
assert set(status["pending_types"]) == {"box_culvert", "ford_bridge"}
# BOX암거는 2026-09-15 전개식이 서서 대상에서 빠짐 — 세월교만 남음(근거 없음 사유).
assert set(status["pending_types"]) == {"ford_bridge"}
def test_표에도_동바리_상태가_실림() -> None:
assert build_table([옹벽()])["shoring"]["applicable"] is False
box = {
"type_id": "box_culvert",
"options": {
"body_width_m": 2.0,
"body_height_m": 2.0,
"wall_thickness_m": 0.3,
"slab_thickness_m": 0.3,
"haunch_m": 0.2,
"blinding_thickness_m": 0.1,
"length_m": 5.0,
},
}
assert build_table([box])["shoring"]["applicable"] is True
+8 -5
View File
@@ -169,13 +169,16 @@ def test_숫자와_글자_규격을_같이_본다() -> None:
assert table.find("retaining_wall", {"form": "반중력식", "height_m": 1.9999}) is None
def test_BOX암거는_미확보로_남음() -> None:
"""⚠ 두께가 저장돼 있지 않아 식도 못 세우고 관측값도 없다 — 지어내면 콘크리트·거푸집·
철근으로 번져 나간다. 사용자 확정 대기."""
def test_BOX암거는_관측표가_아니라_전개식으로_섬() -> None:
"""2026-09-15 — 두께 칸이 등록부에 서고 전개식(`_UnitQuantity_Box`)이 생겨 관측표 몫이 아님.
관측값은 여전히 없음(울진 2×2 철근만 전개식 안에 관측값으로 듦) · 세월교는 사유로 닫힘.
"""
table = load_observed_table()
assert table.find("box_culvert", {"body_width_m": 3.0, "body_height_m": 1.2}) is None
reasons = [item["type_id"] for item in table.not_found["items"]]
assert "box_culvert" in reasons
reasons = {item["type_id"]: item for item in table.not_found["items"]}
assert "box_culvert" not in reasons
assert "세월교 수량 근거 없음" in reasons["ford_bridge"]["why"]
def test_파일이_없으면_전부_미확보() -> None: