fix(axis): 표 읽기 1차 — 뭉친 줄 표기 맞추기·비고 거름·「-」 칸 · 지장목제거를 품셈에 이음
- 뭉친 줄 해석기: 「굴 삭 기 (무한궤도)」를 굴착기(무한궤도)로 맞춰 큰돌쌓기 메쌓기(13-6-1)· 13-7-2 장비 몫이 붙음(형식을 적은 이름만 · 형식 없는 굴삭기는 판정 Ⓒ대로 규격 미정) - 비고·합계 줄을 자원으로 안 읽음(단목베기 4-2-2 가짜 막힘 제거) · 「-」 칸은 그 갈래에 품 없음 (산림복원용 흙막이 13-13-2 가 특별인부·굴삭기 빠진 채 조용히 서던 것을 사유와 함께 막음) - 지장목제거(판정 Ⓑ): 잡관목제거 → FP-04-02-02 #5m미만 · 뿌리뽑기 → FP-09-21 + 임목축적 등급 갈래 · 준비공 「제근·뿌리다듬기」는 같은 면적이라 참조로만(이중계상 막이) · 미결 목록에서 뺌 - 기계 이름 별칭(백호·굴삭기→굴착기)은 별칭표 대상이 아닌 카탈로그 이름 정규화로 주석 정리 - 일위대가 전후 대조(HEAD↔수정): 새로 선 공종 13-6-1·13-7-2 · 새로 막힌 공종 13-13-2(사유 있음) · 내역 금액: 단목베기·제근은 밑수 미확보로 계속 막혀 변화 없음 · 전체 시험 1513 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BW6Jdsh18WPtYUR6THZJqn
This commit is contained in:
@@ -125,6 +125,28 @@
|
||||
"basis_source": "산림사업 표준품셈(고시 2025-82) 12-25 기초잡석 「(단위: ㎥당)」.",
|
||||
"master_name": "기초잡석",
|
||||
"note": "품셈은 운반·부설·다짐 품만 주고 **두께·폭을 정하지 않는다.** 폭은 버림 폭과 같고(KCS 34 50 05) 두께는 사용자 확정 3차 ②(0.2m)다 — 화면에서 바꿀 수 있다."
|
||||
},
|
||||
{
|
||||
"group": "지장목제거",
|
||||
"item": "잡관목제거",
|
||||
"work_item_code": "FP-04-02-02",
|
||||
"master_name": "단목베기 > 1,000㎡당",
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "산림사업 표준품셈(고시 2025-82) 4-2-2 절 이름 「1,000㎡당」 — ⚠ 표 안·절 머리에 「(단위: …)」가 없어 마스터 밑수가 비어 있음(basis_missing F0087). B09 가 밑수 미확보로 막으므로 1,000배 틀린 금액은 안 섬.",
|
||||
"variant_axis": "tree_height",
|
||||
"variant_value": "5m 미만",
|
||||
"note": "지장목제거 = 벌목(4장) + 제근(9-20~21) 단계 합산(2026-09-13 브레인 판정 · 원가_출력변수_사전.md:55). 실무 영월 1.9.2 「잡관목제거 벌목(5m미만)」이 단목베기 1,000㎡당 5m 미만 갈래와 같은 이름·같은 단위(㎡)."
|
||||
},
|
||||
{
|
||||
"group": "지장목제거",
|
||||
"item": "뿌리뽑기",
|
||||
"work_item_code": "FP-09-21",
|
||||
"master_name": "토공 > 제근",
|
||||
"basis_unit": "㎡",
|
||||
"basis_source": "⚠ 교차 참조 — 산림품셈 9-21 표에 밑수 표기가 없어 건설공사 표준품셈 3-9-2 뿌리뽑기 「1,000㎡당」 면적 축을 빌려 씀(사용자 확정 5차 6번). 마스터 밑수는 비어 있음(basis_missing F0294).",
|
||||
"variant_axis": "stand_volume_class",
|
||||
"variant_from": "stand_volume_class",
|
||||
"note": "2026-09-13 브레인 판정 Ⓑ — 제근은 토공 줄(토공_수량.md:30 「뿌리다듬기·적재·제근 | 9-20~21 | 벌개제근 연동」)이라 여기서 셈. 준비공 「제근·뿌리다듬기」는 같은 면적이라 참조로만 보임(이중계상 막이). 품은 임목축적 등급(소림·중림·밀림, 9-21 [주]①)으로 갈려 등급을 갈래로 넘김."
|
||||
}
|
||||
],
|
||||
"haul": [
|
||||
@@ -235,15 +257,6 @@
|
||||
"pending_user": {
|
||||
"note": "이름이 비슷한 후보는 있으나 **어느 것인지 정할 근거가 없는** 자리. 임의로 고르지 않고 unmatched 로 낸다(CLAUDE.md 3장).",
|
||||
"items": [
|
||||
{
|
||||
"group": "지장목제거",
|
||||
"candidates": [
|
||||
"FP-04-01 수확베기",
|
||||
"FP-04-02 단목베기",
|
||||
"FP-04-03 위험목 베기"
|
||||
],
|
||||
"why": "품셈 4장은 벌목을 목적별로 가르는데 임도 지장목이 어느 쪽인지 원본이 말하지 않음"
|
||||
},
|
||||
{
|
||||
"group": "흙깎기/측구터파기 암",
|
||||
"candidates": [
|
||||
|
||||
@@ -94,7 +94,8 @@ def test_구조물은_한_줄로_서고_전개_성분은_안_옴() -> None:
|
||||
assert len(placing) == 1
|
||||
# 그 줄의 물량은 **버림 콘크리트 몫뿐**이어야 한다 — 채움이 섞이면 이중계상이다.
|
||||
blinding = next(
|
||||
c for c in build_unit_table([구조물()])["structures"][0]["components"]
|
||||
c
|
||||
for c in build_unit_table([구조물()])["structures"][0]["components"]
|
||||
if c["name"] == "버림콘크리트"
|
||||
)
|
||||
assert placing[0]["quantity"] == pytest.approx(blinding["amount"], abs=1e-6)
|
||||
@@ -249,7 +250,11 @@ def test_매핑_파일이_없으면_전부_드러남() -> None:
|
||||
|
||||
|
||||
def test_정할_근거가_없는_자리는_지어내지_않고_알림() -> None:
|
||||
"""지장목제거·암 갈래는 후보만 있고 고를 근거가 없다 — 임의 확정 금지(3장)."""
|
||||
"""암 갈래는 고를 근거가 없다 — 임의 확정 금지(3장).
|
||||
|
||||
⭐ 2026-09-13 판정으로 지장목제거는 작업 갈래(뿌리뽑기 9-21 · 잡관목제거 4-2-2)로 이어졌다 —
|
||||
**작업 갈래가 없는** 지장목제거 줄만 여전히 못 잇는다.
|
||||
"""
|
||||
handoff = build_handoff(summary_table=집계표(집계줄("지장목제거", amount=100.0)))
|
||||
assert handoff["work_items"][0]["work_item_code"] is None
|
||||
assert "지장목제거" in handoff["unmatched_work_items"]
|
||||
@@ -259,7 +264,7 @@ def test_정할_근거가_없는_자리는_지어내지_않고_알림() -> None:
|
||||
item.get("group") or item.get("type_id")
|
||||
for item in handoff["mapping_pending_user"]["items"]
|
||||
]
|
||||
assert "지장목제거" in keys
|
||||
assert "지장목제거" not in keys # 판정으로 닫힘(2026-09-13)
|
||||
# ⚠ 큰돌쌓기는 **닫혔다** — 랩탑이 `bond`(메쌓기/찰쌓기) 칸을 만들어 자동으로 갈린다.
|
||||
# 미결 목록에서 빠졌는지도 함께 본다(고쳐졌는데 목록만 남는 것 방지).
|
||||
assert "boulder_masonry" not in keys
|
||||
@@ -954,7 +959,9 @@ def test_검사가_실제로_잡는다_일부러_깨뜨려_봄():
|
||||
handoff = build_handoff(unit_quantity_table=build_unit_table([구조물()]))
|
||||
handoff["materials"].append({"material_name": "가짜자재", "work_item_code": "FP-99-99"})
|
||||
assert verify_no_code_on_materials(handoff) == ["가짜자재"]
|
||||
handoff["work_items"].append({"name": "코드없이내역에선줄", "in_bill": True, "work_item_code": None})
|
||||
handoff["work_items"].append(
|
||||
{"name": "코드없이내역에선줄", "in_bill": True, "work_item_code": None}
|
||||
)
|
||||
assert "코드없이내역에선줄" in verify_bill_flags(handoff)
|
||||
|
||||
|
||||
@@ -983,7 +990,12 @@ def test_타설_줄이_실제로_선다() -> None:
|
||||
_콘크리트구조물(
|
||||
[
|
||||
{"name": "콘크리트", "unit": "㎥", "amount": 13.5, "destination": "unit_price"},
|
||||
{"name": "이형철근 D13", "unit": "kg", "amount": 134.5, "destination": "material"},
|
||||
{
|
||||
"name": "이형철근 D13",
|
||||
"unit": "kg",
|
||||
"amount": 134.5,
|
||||
"destination": "material",
|
||||
},
|
||||
]
|
||||
)
|
||||
]
|
||||
@@ -999,7 +1011,13 @@ def test_타설_줄이_실제로_선다() -> None:
|
||||
|
||||
|
||||
def test_방식을_안_정하면_기본값으로_서되_알린다() -> None:
|
||||
unit = {"structures": [_콘크리트구조물([{"name": "콘크리트", "unit": "㎥", "amount": 2.0, "destination": "unit_price"}])]}
|
||||
unit = {
|
||||
"structures": [
|
||||
_콘크리트구조물(
|
||||
[{"name": "콘크리트", "unit": "㎥", "amount": 2.0, "destination": "unit_price"}]
|
||||
)
|
||||
]
|
||||
}
|
||||
handoff = build_handoff(unit_quantity_table=unit)
|
||||
타설 = next(r for r in handoff["work_items"] if r["name"] == "콘크리트 타설")
|
||||
assert 타설["work_item_code"] == "FP-12-01-01" # 기본값 = 레디믹스트
|
||||
@@ -1008,7 +1026,13 @@ def test_방식을_안_정하면_기본값으로_서되_알린다() -> None:
|
||||
|
||||
|
||||
def test_방식을_바꾸면_공종이_갈린다() -> None:
|
||||
unit = {"structures": [_콘크리트구조물([{"name": "콘크리트", "unit": "㎥", "amount": 2.0, "destination": "unit_price"}])]}
|
||||
unit = {
|
||||
"structures": [
|
||||
_콘크리트구조물(
|
||||
[{"name": "콘크리트", "unit": "㎥", "amount": 2.0, "destination": "unit_price"}]
|
||||
)
|
||||
]
|
||||
}
|
||||
코드 = {}
|
||||
for method in ("ready_mixed", "machine_mixed", "hand_mixed"):
|
||||
handoff = build_handoff(unit_quantity_table=unit, concrete_placing_method=method)
|
||||
@@ -1024,7 +1048,13 @@ def test_방식을_바꾸면_공종이_갈린다() -> None:
|
||||
|
||||
def test_콘크리트가_없으면_타설_줄도_없다() -> None:
|
||||
"""0 ㎥ 짜리 빈 줄을 만들지 않는다."""
|
||||
unit = {"structures": [_콘크리트구조물([{"name": "야면석", "unit": "ton", "amount": 3.0, "destination": "material"}])]}
|
||||
unit = {
|
||||
"structures": [
|
||||
_콘크리트구조물(
|
||||
[{"name": "야면석", "unit": "ton", "amount": 3.0, "destination": "material"}]
|
||||
)
|
||||
]
|
||||
}
|
||||
handoff = build_handoff(unit_quantity_table=unit, concrete_placing_method="ready_mixed")
|
||||
assert not [r for r in handoff["work_items"] if r["name"] == "콘크리트 타설"]
|
||||
|
||||
@@ -1053,7 +1083,9 @@ def test_개소_구조물은_연장으로_안_센다() -> None:
|
||||
|
||||
성분은 개소 기준으로 맞게 서는데 **줄의 축만** 어긋나 있어 아무 시험도 안 잡았다.
|
||||
"""
|
||||
handoff = build_handoff(unit_quantity_table=build_unit_table([_집수정(2.0)], {"pipe_inlet_basin": "집수정"}))
|
||||
handoff = build_handoff(
|
||||
unit_quantity_table=build_unit_table([_집수정(2.0)], {"pipe_inlet_basin": "집수정"})
|
||||
)
|
||||
줄 = next(r for r in handoff["work_items"] if r["name"] == "집수정")
|
||||
assert (줄["unit"], 줄["quantity"]) == ("개소", 1.0)
|
||||
|
||||
@@ -1063,7 +1095,9 @@ def test_개소_구조물은_연장이_길어도_한_개소() -> None:
|
||||
긴것 = build_unit_table([_집수정(5.0)], {"pipe_inlet_basin": "집수정"})
|
||||
for table in (한개, 긴것):
|
||||
줄 = next(
|
||||
r for r in build_handoff(unit_quantity_table=table)["work_items"] if r["name"] == "집수정"
|
||||
r
|
||||
for r in build_handoff(unit_quantity_table=table)["work_items"]
|
||||
if r["name"] == "집수정"
|
||||
)
|
||||
assert (줄["unit"], 줄["quantity"]) == ("개소", 1.0)
|
||||
|
||||
@@ -1080,7 +1114,9 @@ def test_m당_구조물은_그대로_연장으로_센다() -> None:
|
||||
}
|
||||
줄 = next(
|
||||
r
|
||||
for r in build_handoff(unit_quantity_table=build_unit_table([옹벽], {"retaining_wall": "옹벽"}))["work_items"]
|
||||
for r in build_handoff(
|
||||
unit_quantity_table=build_unit_table([옹벽], {"retaining_wall": "옹벽"})
|
||||
)["work_items"]
|
||||
if r["name"] == "옹벽"
|
||||
)
|
||||
assert (줄["unit"], 줄["quantity"]) == ("m", 20.0)
|
||||
@@ -1120,7 +1156,10 @@ def test_못_내는_줄도_사유와_함께_간다() -> None:
|
||||
# ⭐ 2026-09-09 확정 5차 6번으로 **밑수가 면적 축으로 정해져 값이 선다** — 그래서 이 줄은
|
||||
# 더 이상 「못 내는 줄」의 표본이 아니다. 「못 내는 줄도 사유와 함께 간다」는 계약 자체는
|
||||
# 아래 표토·부대시설 줄이 지킨다.
|
||||
assert 제근["in_bill"] is True and 제근["quantity"] > 0
|
||||
# ⭐ 2026-09-13 판정 Ⓑ — 셈은 토공집계 뿌리뽑기(FP-09-21)가 하고 이 줄은 **보이되 안 실린다**.
|
||||
assert 제근["in_bill"] is False and 제근["quantity"] > 0
|
||||
assert 제근["work_item_code"] is None and 제근["blocked_kind"] is None
|
||||
assert "토공집계" in 제근["in_bill_reason"]
|
||||
assert 제근["unit"] == "㎡" # 밑수가 면적 축으로 확정됨(확정 5차 6번)
|
||||
# ⚠ 사유가 **두 번 바뀐 자리**다 — ㉠ 「입목 본수가 없다」(틀린 말: 본수 축이 아니었다)
|
||||
# ㉡ 「밑수 단위가 원문에 없다」(사실이었으나 확정으로 닫힘) ㉢ 지금은 **값이 서고**
|
||||
@@ -1173,7 +1212,14 @@ def test_묶음이_없는_종류는_그대로_타설_줄이_선다() -> None:
|
||||
unit_quantity_table={
|
||||
"structures": [
|
||||
_콘크리트구조물(
|
||||
[{"name": "콘크리트", "unit": "㎥", "amount": 2.84, "destination": "unit_price"}]
|
||||
[
|
||||
{
|
||||
"name": "콘크리트",
|
||||
"unit": "㎥",
|
||||
"amount": 2.84,
|
||||
"destination": "unit_price",
|
||||
}
|
||||
]
|
||||
)
|
||||
]
|
||||
},
|
||||
@@ -1203,9 +1249,7 @@ def test_연장으로_서는_공종은_전개식이_없어도_안_막힌다() ->
|
||||
앞서 「성분이 없으면 전개식 없음」으로 단정해 B09 가 「우리가 만들 것」으로 빼
|
||||
**금액이 0** 이었다(실측: 맹암거 40m 이 0원 → 고친 뒤 1,019,685원).
|
||||
"""
|
||||
handoff = build_handoff(
|
||||
unit_quantity_table={"structures": [_B군("underdrain", "맹암거")]}
|
||||
)
|
||||
handoff = build_handoff(unit_quantity_table={"structures": [_B군("underdrain", "맹암거")]})
|
||||
줄 = next(r for r in handoff["work_items"] if r["name"] == "맹암거")
|
||||
assert 줄["work_item_code"] == "FP-12-10"
|
||||
assert 줄["blocked_kind"] is None, 줄["blocked_reason"]
|
||||
@@ -1283,8 +1327,16 @@ def test_겹치지_않으면_원합을_안_싣는다() -> None:
|
||||
|
||||
|
||||
def test_공종을_못_이은_B군은_사유와_함께_막힌다() -> None:
|
||||
표 = [{"type_id": "chute", "group": "B", "name": "도수로·산비탈수로",
|
||||
"count": 1, "length_m": 40.0, "raw_length_m": 40.0}]
|
||||
표 = [
|
||||
{
|
||||
"type_id": "chute",
|
||||
"group": "B",
|
||||
"name": "도수로·산비탈수로",
|
||||
"count": 1,
|
||||
"length_m": 40.0,
|
||||
"raw_length_m": 40.0,
|
||||
}
|
||||
]
|
||||
줄 = build_handoff(length_table=표)["work_items"][0]
|
||||
assert 줄["work_item_code"] is None
|
||||
assert 줄["in_bill"] is False
|
||||
|
||||
@@ -110,10 +110,10 @@ def test_셈이_맞을것() -> None:
|
||||
# 사용자가 무엇을 정할지 안다(원단위 미확보에서 「표에 있는 규격을 함께 알린」 그 방식).
|
||||
|
||||
|
||||
def test_지장목제거_줄에_공종_미확정_사유가_적힐것() -> None:
|
||||
def test_지장목제거_줄에_공종_자리가_적힐것() -> None:
|
||||
"""⭐ 2026-09-13 판정 — 잡관목제거는 단목베기(4-2-2), 뿌리뽑기는 제근(9-21)."""
|
||||
row = 줄(build_table(), "벌목·지장목제거")
|
||||
assert "공종 미확정" in row["reason"]
|
||||
assert "수확베기" in row["reason"] # 후보가 무엇인지도 함께 보인다
|
||||
assert "FP-04-02-02" in row["reason"] and "FP-09-21" in row["reason"]
|
||||
|
||||
|
||||
# ── 규준틀 개소 — 원문이 기준을 정해 둠 (2026-09-07 ㉒) ────────────
|
||||
|
||||
@@ -44,7 +44,11 @@ def 집계() -> dict:
|
||||
|
||||
|
||||
def 줄들() -> list[dict]:
|
||||
return [row for row in build_handoff(summary_table=집계())["work_items"] if row["name"] == "지장목제거"]
|
||||
return [
|
||||
row
|
||||
for row in build_handoff(summary_table=집계())["work_items"]
|
||||
if row["name"] == "지장목제거"
|
||||
]
|
||||
|
||||
|
||||
def test_두_줄이_선다() -> None:
|
||||
@@ -59,7 +63,9 @@ def test_같은_면적이_두_줄에_들어간다() -> None:
|
||||
|
||||
def test_이중계상이_아님이_사유에_적힌다() -> None:
|
||||
for row in 줄들():
|
||||
assert "이중계상 아님" in row["in_bill_reason"] or "이중계상 아님" in str(row["spec_detail"])
|
||||
assert "이중계상 아님" in row["in_bill_reason"] or "이중계상 아님" in str(
|
||||
row["spec_detail"]
|
||||
)
|
||||
|
||||
|
||||
def test_작업_갈래를_지반_갈래로_읽지_않는다() -> None:
|
||||
@@ -67,11 +73,21 @@ def test_작업_갈래를_지반_갈래로_읽지_않는다() -> None:
|
||||
for row in 줄들():
|
||||
assert row["ground_class"] is None
|
||||
unmatched = build_handoff(summary_table=집계())["unmatched_work_items"]
|
||||
assert "지장목제거" in unmatched
|
||||
assert not any("지장목제거(" in item for item in unmatched)
|
||||
assert not any("지장목제거" in item for item in unmatched) # 두 작업 갈래 모두 이어짐
|
||||
|
||||
|
||||
def test_잡관목제거는_품셈에_이름이_없음이_남는다() -> None:
|
||||
def test_잡관목제거는_단목베기_5m_미만으로_이어진다() -> None:
|
||||
"""⭐ 2026-09-13 판정 — 지장목제거는 품셈에 있다(벌목 4장 + 제근 9-20~21)."""
|
||||
잡관목 = next(row for row in 줄들() if row["spec"] == "잡관목제거")
|
||||
사유 = str(잡관목["spec_detail"]) + str(잡관목["in_bill_reason"])
|
||||
assert "품셈에 그 이름이 없어" in 사유
|
||||
assert 잡관목["work_item_code"] == "FP-04-02-02"
|
||||
assert 잡관목["variant_value"] == "5m 미만"
|
||||
뿌리뽑기 = next(row for row in 줄들() if row["spec"] == "뿌리뽑기")
|
||||
assert 뿌리뽑기["work_item_code"] == "FP-09-21"
|
||||
assert 뿌리뽑기["variant_axis"] == "stand_volume_class"
|
||||
|
||||
|
||||
def test_뿌리뽑기는_임목축적_등급을_갈래로_싣는다() -> None:
|
||||
"""⭐ 판정 Ⓑ — 품이 등급(소림·중림·밀림)으로 갈려 갈래를 잃으면 안 된다."""
|
||||
rows = build_handoff(summary_table=집계(), stand_volume_class="중림")["work_items"]
|
||||
뿌리뽑기 = next(r for r in rows if r["name"] == "지장목제거" and r["spec"] == "뿌리뽑기")
|
||||
assert 뿌리뽑기["variant_value"] == "중림"
|
||||
|
||||
@@ -54,4 +54,6 @@ def test_금액이_서는_줄은_수량째_본체에_선다() -> None:
|
||||
in_bill = [
|
||||
r["name"] for r in handoff["work_items"] if r["origin"] == "preparation" and r["in_bill"]
|
||||
]
|
||||
assert {"표토제거", "제근·뿌리다듬기", "뿌리 적재"} <= set(in_bill)
|
||||
assert {"표토제거", "뿌리 적재"} <= set(in_bill)
|
||||
# ⭐ 2026-09-13 판정 Ⓑ — 제근은 토공집계 뿌리뽑기가 셈. 준비공 줄은 참조(제외 목록)로 간다.
|
||||
assert "제근·뿌리다듬기" not in in_bill
|
||||
|
||||
Reference in New Issue
Block a user