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

This commit is contained in:
2026-09-14 16:36:39 +09:00
8 changed files with 60 additions and 31 deletions
@@ -31,7 +31,7 @@ GROUND_SPLIT_GROUPS = ("흙깎기", "측구터파기", "구조물터파기", "
#: 면고르기 갈래 — 품셈 9-19-1 원문 표 두 벌 그대로(L5414 절토면 토질 6 · L5430 성토면 시공·토질).
#: B09 표 읽기(`_ResourceAxis_JudgedTable`)가 같은 이름으로 갈래를 세움 — 짝 시험이 마스터와 대조.
#: ⚠ 제안값 없음(판정 Ⓒ) · 성토면 · 기계」 는 굴착기 0.6㎥ 형식 둘이라 B09 가 안 세움(Ⓓ 사유).
#: ⚠ 토질·시공은 제안값 없음(판정 Ⓒ) · 성토면 기계는 굴착기 0.6㎥ 형식 둘로 갈래(브레인 ②).
FACE_DRESSING_CUT_CLASSES = (
"절토면 · 모래ㆍ사질토ㆍ점토ㆍ점질토",
"절토면 · 연질토ㆍ불순자갈",
@@ -43,7 +43,14 @@ FACE_DRESSING_CUT_CLASSES = (
FACE_DRESSING_FILL_CLASSES = (
"성토면 · 인력 · 점토 또는 점질토",
"성토면 · 인력 · 모래 또는 사질토",
"성토면 · 기계",
"성토면 · 기계 · 무한궤도",
"성토면 · 기계 · 타이어",
)
#: 성토면 제안 — 칸 곁 회색 제안일 뿐 스스로 고르지 않음(「기본값을 몰래 확정으로 안 바꿈」).
FACE_DRESSING_FILL_SUGGESTED = (
"성토면 · 기계 · 무한궤도",
"영월 실무 설계내역 「성토사면고르기 06M3 B/H」(백호 = 무한궤도 굴착기) · 임도는 산지라"
" 타이어식이 잘 안 들어감 — 타이어식도 고를 수 있음(2026-09-14 브레인 ②)",
)
#: ⭐ 2026-09-14 브레인 판정 Ⓐ — 밑수 = 초류종자살포(파종) 면적. 반영률 칸을 따로 두면 같은 값을
#: 두 곳에서 관리하게 됨. 실무도 그렇게 움직임(거창 파종 성토 50%·절토 100% · 영월 성토 50%).
@@ -33,6 +33,7 @@ from B06_Section.B06_Section_Repository import (
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import (
FACE_DRESSING_CUT_CLASSES,
FACE_DRESSING_FILL_CLASSES,
FACE_DRESSING_FILL_SUGGESTED,
SummaryInput,
)
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import build_table as build_summary_table
@@ -166,6 +167,11 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
table["face_dressing_choices"] = {
"cut": list(FACE_DRESSING_CUT_CLASSES),
"fill": list(FACE_DRESSING_FILL_CLASSES),
# 성토면 제안(회색 · [제안값 넣기]) — 비워 두면 여전히 「안 정함」(브레인 ②).
"fill_suggested": {
"value": FACE_DRESSING_FILL_SUGGESTED[0],
"basis": FACE_DRESSING_FILL_SUGGESTED[1],
},
}
# 준비공·사방공 — 못 서는 줄도 사유와 함께 남긴다(빈 표는 「빠뜨림」과 구별이 안 됨).
structures = await _route_structures(project_id)
+2 -2
View File
@@ -51,8 +51,8 @@ KNOWN_GAPS: dict[str, tuple[str, str]] = {
" — 표에 착암공 수량이 없어 줄을 안 세움(보통인부는 표 그대로). "
"ⓘ 절토면 풍화암: [주]② 「소형브레이커를 사용할 시는 연암 고르기 품을 **준용할 수 있다**」"
" — 선택지라 따로 갈래를 안 만듦(브레이커를 쓰면 연암 갈래를 고를 것). "
"ⓘ 성토면 · 기계: 굴착기 0.6㎥ 무한궤도·타이어 둘이라 **규격 미정** — 이 갈래는 안 섬"
"(굴착기 규격 칸과 함께 풀 자리 · 2026-09-14 판정 Ⓓ).",
"ⓘ 성토면 · 기계: 원문이 굴착기 0.6㎥ 형식을 안 적어 무한궤도·타이어 두 갈래로 세움"
"(제안 무한궤도 — 영월 실무 「06M3 B/H」 · 2026-09-14 브레인 ②).",
),
"FP-12-25": (
"운반거리 미정",
@@ -42,6 +42,9 @@ JUDGED_TABLES: dict[str, dict[str, Any]] = {
"prefix": "성토면",
# 원문 L5430 표 — 「인력시공」 칸이 두 줄 병합이라 둘째 줄(모래 또는 사질토)이 앞당겨 옴.
"merged_rows": (1,),
# 기계시공 「굴착기 | 0.6㎥」 — 원문이 형식을 안 적음. 카탈로그 0.6 은 무한궤도·타이어
# 둘이라 형식마다 갈래 · 고르기는 B08 칸(제안 무한궤도 — 영월 「06M3 B/H」 · 브레인 ②).
"form_split": {"굴착기": ("무한궤도", "타이어")},
"why": "원문 L5430 「2. 성토면 고르기 (단위: 10㎡당)」 — 시공 칸 병합으로 둘째 줄 앞당김",
},
}
@@ -160,12 +163,16 @@ def _merged_first(code, table, judged, rows, catalog, basis, unit) -> list | str
variant = " · ".join(
[judged["prefix"], short, *([soil] if methods.count(method) > 1 else [])]
)
entry = _entry(catalog, name, code, (size,) if size else ())
if entry is None:
# 그 갈래만 안 섬(자원이 하나뿐) — 사유는 못 맞춘 줄로(Ⓑ 굴착기 0.6㎥ 형식 둘).
label = f"{name} {size}".strip()
reason = unmatched_reason(catalog, name)
staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), label, reason))
continue
staged.append(_row(code, table, entry, amount / basis, unit, index, variant))
side = (size,) if size else ()
forms = judged.get("form_split", {}).get(name)
for form in forms or (None,):
entry = _entry(catalog, f"{name}({form})" if form else name, code, side)
if entry is None:
# 그 갈래만 안 섬(자원이 하나뿐) — 사유는 못 맞춘 줄로.
label = f"{name} {size}".strip()
reason = unmatched_reason(catalog, name)
staged.append(UnmatchedRow(code, str(table.get("pum_table_id", "")), label, reason))
continue
named = f"{variant} · {form}" if form else variant
staged.append(_row(code, table, entry, amount / basis, unit, index, named))
return staged
@@ -1,7 +1,7 @@
{
"schema_version": "1.0",
"dataset_id": "data_work_item_master_manifest",
"generated_at": "2026-09-14T14:28:19+09:00",
"generated_at": "2026-09-14T16:09:37+09:00",
"built_by": "B08_Quantity/B08_Quantity_Build_WorkItemMaster.py",
"source": {
"dataset_id": "pum_forest",
@@ -12,8 +12,8 @@
"files": [
{
"file": "work_item_master_2026-01-01.json",
"sha256": "f3e4367188b209bcb064f99825e55ecf5b9c7caafb6d6a37c7d6b179cd7fb91c",
"size_bytes": 838069
"sha256": "a942f7ad412441ba0c7ad3f8149fd143b10502dae5d20053e5d98029ab31c6b4",
"size_bytes": 838243
},
{
"file": "form_undetermined_2026-01-01.json",
@@ -3,7 +3,7 @@
"dataset_id": "work_item_master_forest",
"effective_date": "2026-01-01",
"pum_edition": "2026-01-01",
"generated_at": "2026-09-14T14:28:19+09:00",
"generated_at": "2026-09-14T16:09:37+09:00",
"dataset_version": {
"dataset_id": "pum_forest",
"effective_date": "2026-01-01",
@@ -28434,7 +28434,9 @@
"capacity_formula_here": false,
"variant_key": [
"성토면 · 인력 · 점토 또는 점질토",
"성토면 · 인력 · 모래 또는 사질토"
"성토면 · 인력 · 모래 또는 사질토",
"성토면 · 기계 · 무한궤도",
"성토면 · 기계 · 타이어"
],
"condition_note": [
"시 공",
@@ -28480,7 +28482,9 @@
"절토면 · 연암",
"절토면 · 보통암ㆍ경암",
"성토면 · 인력 · 점토 또는 점질토",
"성토면 · 인력 · 모래 또는 사질토"
"성토면 · 인력 · 모래 또는 사질토",
"성토면 · 기계 · 무한궤도",
"성토면 · 기계 · 타이어"
]
},
{
+12 -9
View File
@@ -174,7 +174,7 @@ def test_토질_시공_칸이_비면_금액_없이_입력_사유() -> None:
def test_고르는_갈래는_원문_표_두_벌과_마스터_갈래_키가_같음() -> None:
"""B08 칸의 선택지와 B09 표 읽기가 세운 갈래 — 두 벌이 갈리면 빨강(성토면·기계는 Ⓓ 로 안 섬)."""
"""B08 칸의 선택지와 B09 표 읽기가 세운 갈래 — 두 벌이 갈리면 빨강."""
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
from B09_Estimation.B09_Estimation_UnitPrice import normalize_variant_key
@@ -185,17 +185,17 @@ def test_고르는_갈래는_원문_표_두_벌과_마스터_갈래_키가_같
choices = {
normalize_variant_key(k) for k in FACE_DRESSING_CUT_CLASSES + FACE_DRESSING_FILL_CLASSES
}
assert keys == choices - {normalize_variant_key("성토면 · 기계")}
assert keys == choices
# ── 내역 — 갈래가 서면 금액 · 기계는 사유 ─────────────────────────────
def test_내역은_갈래가_서면_금액이_서고_성토면_기계는_규격_미정_사유() -> None:
def test_내역은_갈래가_서면_금액이_서고_성토면_기계는_무한궤도_제안() -> None:
"""9-19-2(연암 · 리핑) · 9-19-1(성토면 인력 점토 500㎡ × 3,269원) 금액이 섬.
ⓘ 9-19-1 은 첫 커밋(ee9368ec) 때 공기압축기 3.5 손료 미확보(공종 단위 「일부만」)에 막혀 있었고
운전경비 표 괄호 딸림값 파서 고침으로 풀림(같은 날 · `test_b09_operating_paren_spec`).
성토면·기계는 [주]·규격 미정 사유가 후보 목록 곁에 붙음(Ⓓ)."""
성토면·기계는 굴착기 형식 두 갈래 · 제안 무한궤도(영월 실무 「06M3 B/H」 · 브레인 ②)."""
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
handoff = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class=FILL_CLAY)
@@ -204,8 +204,11 @@ def test_내역은_갈래가_서면_금액이_서고_성토면_기계는_규격_
assert ripping.amount_krw and int(ripping.amount_krw) > 0, ripping.note
fill = next(r for r in rows if r.code == "FP-09-19-01" and r.spec == FILL_CLAY)
assert fill.amount_krw == 500 * 3269, fill.note
machine = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class="성토면 · 기계")
row = next(
r for r in build_bill(machine).rows if r.code == "FP-09-19-01" and r.spec == "성토면"
)
assert row.amount_krw is None and "규격 미정" in row.note, row.note
track = "성토면 · 기계 · 무한궤도"
machine = _handoff(face_dressing_cut_class=CUT_SAND, face_dressing_fill_class=track)
row = next(r for r in build_bill(machine).rows if r.code == "FP-09-19-01" and r.spec == track)
assert row.amount_krw and int(row.amount_krw) > 0, row.note
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import FACE_DRESSING_FILL_SUGGESTED
value, basis = FACE_DRESSING_FILL_SUGGESTED
assert value == track and "06M3 B/H" in basis and "타이어" in basis
+6 -4
View File
@@ -106,15 +106,17 @@ def test_토사면_고르기_절토면은_토질_여섯_갈래로_규격까지_
assert rock == {("1002", 0.046), ("5205-0035", 0.125), ("5210-0010", 0.245)}, rock
def test_토사면_고르기_성토면_인력_두_갈래_기계는_규격_미정으로_막힘() -> None:
def test_토사면_고르기_성토면_인력_두_갈래_기계는_형식_두_갈래() -> None:
"""Ⓒ 둘째 줄이 병합 칸 탓에 한 칸 밀려 옴 — 사람 판정표로만 읽음(일반 보정 금지 · 목재틀흙막이
503만원 전례). 굴착기 0.6㎥ 는 무한궤도·타이어 둘이라고름그 갈래는 안 섬."""
503만원 전례). 굴착기 0.6㎥ 는 원문이 형식을적음처음(Ⓑ)엔 근거가 없어 안 세웠고,
2026-09-14 브레인 ② 로 무한궤도·타이어 두 갈래를 세움(고르기는 B08 칸 · 제안 무한궤도)."""
build = cached_build()
assert _per_m2(FILL + "인력·점토또는점질토") == {"1002": 0.019}
assert _per_m2(FILL + "인력·모래또는사질토") == {"1002": 0.017}
assert not [code for code in build.book.titles if code.startswith(FILL + "기계")]
assert _per_m2(FILL + "기계·무한궤도") == {"X-0201-0060": 0.009}
assert _per_m2(FILL + "기계·타이어") == {"X-0211-0060": 0.009}
left = build.unattached.get("FP-09-19-01", [])
assert any("굴착기" in label for label in left), left
assert not any("굴착기" in label for label in left), left
def test_범위_별칭은_칸이_규격을_적었으면_덮지_않음() -> None: