Merge remote-tracking branch 'origin/main_desktop_1' into sub_desktop_1

This commit is contained in:
2026-09-08 19:16:38 +09:00
8 changed files with 251 additions and 10 deletions
@@ -183,6 +183,11 @@ def build_handoff(
result["basis_unit_warnings"] = verify_unit_matches_basis( result["basis_unit_warnings"] = verify_unit_matches_basis(
work_items, extra=table.declared_units() work_items, extra=table.declared_units()
) )
# ⚠ 채집석 공제 — **양수 ㎥ 로 넘기기만** 한다. 빼는 자리는 유토곡선의 사토뿐이다
# (2026-09-09 세 창 확정 · 부호를 넘기면 두 번 뒤집힌다).
result["collected_stone_deduction_m3"] = float(
(unit_quantity_table or {}).get("collected_stone_deduction_m3") or 0.0
)
result["placing_notes"] = placing_notes result["placing_notes"] = placing_notes
return result return result
+103 -2
View File
@@ -246,16 +246,117 @@ def erosion_rows(
] ]
#: 부대시설·가설공사 — **법이 요구하는데 우리가 안 내던 다섯 줄**(2026-09-09 사용자 확정 ⑬).
#: `key` 는 설정의 `ancillary_counts` 칸 이름, `code` 는 품셈 공종(없으면 `None`).
#: ⚠ 다섯 중 **품셈에 공종이 있는 것은 가설창고 하나뿐**이다(마스터 전수 확인).
#: 나머지 넷은 **금액이 못 선다** — 줄은 세우되 「공종 자체가 품셈에 없음」이라고 적는다.
#: 「아직 안 만든 것」과 갈라 적어야 다음에 할 일이 달라진다.
ANCILLARY_ITEMS: tuple[dict[str, Any], ...] = (
{
"key": "national_point_sign",
"item": "국가지점번호판",
"unit": "개소",
"code": None,
"legal": True,
"why": (
"⚠ 법정 의무 — 임도규정 제26조제5항 「국가지점번호판을 제작하여 500미터 마다 "
"설치·관리하되, 필요시 거리를 조정할 수 있으며」. ⚠ 기점 포함·종점 잔여·갈림길 "
"중복을 원문이 정하지 않아 **연장÷500 을 산식으로 쓰지 않는다** — 개소를 넣으면 섬"
),
},
{
"key": "guide_sign",
"item": "임도 안내판",
"unit": "개소",
"code": None,
"legal": True,
"why": (
"⚠ 법정 의무 — 임도규정 제26조제6항 「임도의 시점 및 종점에 안내판…을 설치하여야 "
"한다」. 노선을 이어 가면 최초 시점·최종 종점에 둘 수 있어 **개소는 설계 판단**임"
),
},
{
"key": "gate",
"item": "차단기",
"unit": "개소",
"code": None,
"legal": False,
"why": "임도기술교본 11장(실무 참고) — 법령·행정규칙에는 없음. 설치 개소는 설계 판단",
},
{
"key": "site_container",
"item": "가설창고(컨테이너)",
"unit": "개소",
"code": "FP-11-01",
"legal": False,
"why": "품셈 11-1 콘테이너형 가설건축물 — ⚠ 개소는 현장 조건이라 설계가 정함",
},
{
"key": "flood_supplies",
"item": "수방대책 자재",
"unit": "",
"code": None,
"legal": False,
"why": (
"임도기술교본 11장 — 비닐·말뚝·마대·삽 등을 현장 입구에 비치. "
"품목·수량이 원문에 없어 **한 벌(식)로 받는다**"
),
},
)
#: 품셈에 그 이름의 공종이 아예 없는 줄에 적는 사유. 「아직 안 만든 것」과 갈라 쓴다.
REASON_NO_WORK_ITEM = "품셈에 그 이름의 공종이 없음 — 금액은 별도 단가로만 설 수 있음"
def ancillary_rows(counts: dict[str, Any] | None = None) -> list[dict[str, Any]]:
"""부대시설·가설공사 줄. **개소를 안 넣어도 줄은 선다** — 빠진 것이 보이게.
⚠ 개소를 **지어내지 않는다**(확정 ⑬). 설정 `ancillary_counts` 에 넣은 값만 쓴다.
"""
given = {str(key): value for key, value in (counts or {}).items()}
rows: list[dict[str, Any]] = []
for spec in ANCILLARY_ITEMS:
raw = given.get(spec["key"])
try:
amount = float(raw) if raw is not None and str(raw).strip() != "" else None
except (TypeError, ValueError):
amount = None
reasons = [spec["why"]]
if spec["code"] is None:
reasons.append(REASON_NO_WORK_ITEM)
if amount is None:
reasons.append("개소가 아직 입력되지 않았습니다 — 넣으면 물량이 섭니다")
status = STATUS_PENDING
else:
status = STATUS_READY if spec["code"] else STATUS_PENDING
rows.append(
{
"group": "부대시설",
"item": spec["item"],
"unit": spec["unit"],
"amount": amount,
"status": status,
"work_item_code": spec["code"],
"legal_required": bool(spec["legal"]),
"reason": " · ".join(reasons),
}
)
return rows
def build_table( def build_table(
slope_totals: dict[str, float] | None = None, slope_totals: dict[str, float] | None = None,
structures: Iterable[dict[str, Any]] = (), structures: Iterable[dict[str, Any]] = (),
slope_rows: Iterable[dict[str, Any]] = (), slope_rows: Iterable[dict[str, Any]] = (),
topsoil_thickness_m: float | None = None, topsoil_thickness_m: float | None = None,
names: dict[str, str] | None = None, names: dict[str, str] | None = None,
ancillary_counts: dict[str, Any] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**""" """화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
rows = preparation_rows(slope_totals, slope_rows, topsoil_thickness_m) + erosion_rows( rows = (
structures, names preparation_rows(slope_totals, slope_rows, topsoil_thickness_m)
+ erosion_rows(structures, names)
+ ancillary_rows(ancillary_counts)
) )
return { return {
"columns": ["구분", "공종", "단위", "수량", "상태", "사유"], "columns": ["구분", "공종", "단위", "수량", "상태", "사유"],
@@ -86,8 +86,11 @@ def _length_of(slope: StationSlope, series: str, face: str) -> float:
법면보호공은 면고르기를 참조한다 — 같은 사면길이를 쓴다. 끊고 싶으면 이 함수만 고친다. 법면보호공은 면고르기를 참조한다 — 같은 사면길이를 쓴다. 끊고 싶으면 이 함수만 고친다.
층따기는 성토면만 대상이다. 층따기는 성토면만 대상이다.
""" """
if series == "bench_cut" and face != "fill": if series == "bench_cut":
return 0.0 # ⚠ 층따기는 **원지반 표면**을 깎는 일이라 밑수가 성토 비탈면이 아니다
# (교본 6장 4절). B06 설계가 측점마다 내는 값을 그대로 쓴다.
# 없으면 0 — 성토 사면길이로 대신 채우면 **다른 면을 세게 된다**(2026-09-09 정정).
return slope.bench_cut_length_m if face == "fill" else 0.0
return slope.fill_length_m if face == "fill" else slope.cut_length_m return slope.fill_length_m if face == "fill" else slope.cut_length_m
@@ -58,6 +58,12 @@ class StationSlope:
chainage_m: float chainage_m: float
cut_length_m: float = 0.0 cut_length_m: float = 0.0
fill_length_m: float = 0.0 fill_length_m: float = 0.0
#: 층따기 밑수 — **원지반 표면**의 경사길이(m). B06 설계가 측점마다 낸다
#: (`design.bench_cut_length_m`, 2026-09-09 랩탑 메인).
#: ⚠ **성토 비탈면 길이와 다른 면이다** — 층따기는 성토부 **아래 원지반**을 계단으로
#: 깎는 일이라(교본 6장 4절) 비탈면이 아니라 지표면을 따라간다. 앞서 성토 사면길이를
#: 밑수로 쓰고 있었는데 **면이 달랐다.**
bench_cut_length_m: float = 0.0
berm_width_m: float = 0.0 berm_width_m: float = 0.0
# 성토고(m) — 성토 사면 조각들의 **수직 낙차 합**. 노면 끝에서 원지반까지 내려간 높이다. # 성토고(m) — 성토 사면 조각들의 **수직 낙차 합**. 노면 끝에서 원지반까지 내려간 높이다.
# ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은 # ⚠ 좌우가 다르면 **큰 쪽**을 쓴다. 「중심점 성토고 5m 이상」(품셈 11-3 [주]①) 판정은
@@ -216,6 +222,8 @@ def station_slope(chainage_m: float, design: dict[str, Any]) -> StationSlope:
chainage_m=float(chainage_m), chainage_m=float(chainage_m),
cut_length_m=sum(s.length_m for s in segments if s.role == "cut"), cut_length_m=sum(s.length_m for s in segments if s.role == "cut"),
fill_length_m=sum(s.length_m for s in segments if s.role == "fill"), fill_length_m=sum(s.length_m for s in segments if s.role == "fill"),
# ⚠ 없는 측점은 0 이다 — 성토 사면길이로 **대신 채우지 않는다**(면이 다름).
bench_cut_length_m=_num(design.get("bench_cut_length_m")) or 0.0,
fill_height_m=max(fill_by_side.values(), default=0.0), fill_height_m=max(fill_by_side.values(), default=0.0),
berm_width_m=_num(berm.get("width_m")) or 0.0, berm_width_m=_num(berm.get("width_m")) or 0.0,
segments=tuple(segments), segments=tuple(segments),
@@ -157,6 +157,39 @@ STONE_MASONRY = {
# `earthwork` = 토공 대분류로 합산(울진 토적집계 D12~D14 실증) # `earthwork` = 토공 대분류로 합산(울진 토적집계 D12~D14 실증)
# `material` = 자재총괄로 감(할증은 거기서 한 번만) # `material` = 자재총괄로 감(할증은 거기서 한 번만)
# `unit_price` = 일위대가 재료비 구성으로 감(B09 가 배합을 분해) # `unit_price` = 일위대가 재료비 구성으로 감(B09 가 배합을 분해)
#: 버림 콘크리트 — **빠뜨리고 있던 줄**이다(2026-09-09 사용자 확정 ⑭).
#: 두께 근거: KDS 44 90 00 도로암거구조설계기준 「기초시공시 기초지반 다짐을 시행하고
#: 구조물 시공이 원활하도록 **100 mm 두께의 버림콘크리트**를 타설하도록 한다」.
#: 폭 근거: KCS 34 50 05 「버림 콘크리트의 두께는 설계도서에 따르며, **폭은 잡석다짐의
#: 폭과 동일**하게 한다」.
#: ⚠ 우리에게 **잡석다짐 폭이 아직 없다**(기초잡석은 품셈에 공종만 있고 두께·폭이 없음).
#: 그래서 **터파기 폭(평균두께 + 여유 0.2m)을 잠정으로** 쓰고 근거 문구에 그 사실을 적는다.
#: 잡석다짐 폭이 정해지면 이 한 줄만 바꾸면 된다.
#: 돌을 **사 오나 캐나** — 기본은 「캔다」(2026-09-09 사용자 확정 ②).
#: 법이 그쪽을 권한다 — 별표2 「석축 등에 필요한 야면석 등은 **가급적 현장에서
#: 채취·사용**하도록 운반거리를 조사한다」. 실무 견적 다섯 권에도 야면석 **구입 단가가
#: 0건**이고 울진 일위대가는 「구입/채집」 두 벌을 갖고 있다.
#: ⚠ 구조물마다 바꿀 수 있다 — 저장 제원에 「구입」이라 적힌 구조물만 공제에서 빠진다.
STONE_SUPPLY_KEYS = ("stone_supply", "stone_source")
STONE_SUPPLY_PURCHASED = {"구입", "구입품", "사서", "purchase", "purchased", "buy"}
#: ⚠⚠ **채집석 공제는 사토에서 한 번만 뺀다** (2026-09-09 세 창 확정 · 랩탑 메인의 통로에도
#: 같은 문장이 박혀 있다 — 두 곳이 같은 말이라야 나중에 누가 봐도 안 갈린다).
#:
#: 채집석 공제는 사토에서 한 번만 뺀다.
#: B08 은 소요량(collected_stone_deduction_m3, ㎥ 양수)을 내기만 하고 공제하지 않으며,
#: 빼는 자리는 유토곡선의 사토뿐이다 —
#: 실어 내는 몫(spoil_m3 natural_spoil_m3)에서 먼저 빼고 모자라면 자연방토에서 뺀다.
#:
#: ⚠ **부호를 넘기지 않는다.** 실무 시트가 `−274.66` 으로 적혀 있어 그대로 넘기면 두 번
#: 뒤집힌다. 우리는 **양수**로 주고, 빼는 것은 받는 쪽이 한다.
COLLECTED_STONE_KEY = "collected_stone_deduction_m3"
BLINDING_THICKNESS_M = 0.10
#: 버림을 뺄 수 있는 칸 — 「기본은 넣고, 빼고 싶으면 뺀다」(사용자 확정 ⑭).
#: 저장 제원에 이 칸이 없으면 **넣는 쪽**이 기본이다.
BLINDING_OPTION_KEYS = ("blinding_concrete", "base_blinding")
DESTINATION = { DESTINATION = {
"터파기": "earthwork", "터파기": "earthwork",
"되메우기": "earthwork", "되메우기": "earthwork",
@@ -169,9 +202,13 @@ DESTINATION = {
"막자갈": "material", "막자갈": "material",
"콘크리트": "unit_price", "콘크리트": "unit_price",
"채움콘크리트": "unit_price", "채움콘크리트": "unit_price",
"버림콘크리트": "unit_price",
"모르터": "unit_price", "모르터": "unit_price",
"거푸집": "unit_price", "거푸집": "unit_price",
"물구멍관": "material", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약) "물구멍관": "material",
# ⚠ 자재도 토공도 아닌 자리 — **유토곡선이 사토에서 뺄 밑수**다. 자재총괄은
# `material` 만 모으므로 여기 섞이지 않는다.
"채집석": "haul_deduction", # ⚠ 자재 카탈로그가 이름으로 찾는다 — 공백 없는 한 낱말(B09 규약)
} }
# ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다. # ⚠ 배합 성분 — 산출물에 나타나면 안 된다(㉢). B09 일위대가가 배합표로 분해한다.
@@ -221,6 +258,48 @@ class StructureQuantity:
billing_quantity: float = 0.0 billing_quantity: float = 0.0
def is_collected_stone(options: dict[str, Any]) -> bool:
"""이 구조물의 돌을 **캐서 쓰는가**. 정한 적이 없으면 「캔다」(확정 ②)."""
for key in STONE_SUPPLY_KEYS:
raw = options.get(key)
if raw is None:
continue
if str(raw).strip().lower() in STONE_SUPPLY_PURCHASED:
return False
return True
def wants_blinding(options: dict[str, Any]) -> bool:
"""버림 콘크리트를 넣을지. **정한 적이 없으면 넣는다**(사용자 확정 ⑭).
「안 넣음」·「제외」·`false` 로 적혀 있을 때만 뺀다 — 빈 칸을 「빼기」로 읽으면
저장해 둔 적 없는 프로젝트에서 줄이 통째로 사라진다.
"""
for key in BLINDING_OPTION_KEYS:
raw = options.get(key)
if raw is None:
continue
text = str(raw).strip().lower()
if text in {"false", "0", "no", "제외", "안 넣음", "안넣음", "빼기"}:
return False
return True
def _blinding_component(base_width_m: float, length_m: float, options: dict[str, Any]):
"""버림 콘크리트 한 줄. 넣지 않기로 했으면 `None`."""
if not wants_blinding(options) or base_width_m <= 0 or length_m <= 0:
return None
return Component(
"버림콘크리트",
"",
base_width_m * length_m * BLINDING_THICKNESS_M,
DESTINATION["버림콘크리트"],
f"기초 폭 {base_width_m:.2f}m × 연장 × 두께 {BLINDING_THICKNESS_M:.2f}m"
" · 두께는 KDS 44 90 00(100㎜) · ⚠ 폭은 잡석다짐 폭(KCS 34 50 05)이라야 하나"
" 그 값이 아직 없어 **터파기 폭을 잠정**으로 씀",
)
#: ⚠ **저장 제원의 실제 칸 이름**은 `back_len_cm` 이다(레지스트리 확인). #: ⚠ **저장 제원의 실제 칸 이름**은 `back_len_cm` 이다(레지스트리 확인).
#: 앞서 `stone_back_length_cm` 을 읽고 있어 **저장값이 영영 안 닿고 늘 기본 45㎝ 로 돌았다** #: 앞서 `stone_back_length_cm` 을 읽고 있어 **저장값이 영영 안 닿고 늘 기본 45㎝ 로 돌았다**
#: — 뒷길이를 75 로 골라도 45 계수가 붙던 자리다. 값이 나오므로 아무 시험도 안 잡았다. #: — 뒷길이를 75 로 골라도 45 계수가 붙던 자리다. 값이 나오므로 아무 시험도 안 잡았다.
@@ -325,6 +404,10 @@ def boulder_masonry(
# 낸다. 근거 문구에 그 사실을 적어 되짚을 수 있게 한다. # 낸다. 근거 문구에 그 사실을 적어 되짚을 수 있게 한다.
upper_cm = float(str(diameter).split("~")[-1]) upper_cm = float(str(diameter).split("~")[-1])
thickness = upper_cm / 100.0 thickness = upper_cm / 100.0
# 버림 콘크리트 — 돌쌓기와 같은 자리(사용자 확정 ⑭). 빼려면 저장 제원에서 「안 넣음」.
blinding = _blinding_component(thickness + constants["excavation_extra_m"], length_m, options)
if blinding is not None:
components.append(blinding)
excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m
backfill = height_m * constants["backfill_thickness_m"] * length_m backfill = height_m * constants["backfill_thickness_m"] * length_m
components.extend( components.extend(
@@ -470,7 +553,8 @@ def stone_masonry(
# 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌). # 막자갈 = 입적 − (면적 × 뒷길이 × 뒤채움몫 + 고임돌).
# 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3). # 뒤채움 몫은 **돌 종류로 갈린다** — 깬돌·잡석 1/2 · 야면석 1/3 (교본 7-3).
wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 — 막자갈에서 안 뺌 wedge = masonry_area * _num(table["wedge_stone_m3_per_m2"]) # None 이면 0 — 막자갈에서 안 뺌
rubble = volume - (masonry_area * (back_cm / 100.0) * body_ratio + wedge) stone_body = masonry_area * (back_cm / 100.0) * body_ratio # 면석 몸통 체적
rubble = volume - (stone_body + wedge)
if rubble > 0: if rubble > 0:
components.append( components.append(
Component( Component(
@@ -527,6 +611,28 @@ def stone_masonry(
) )
) )
# 채집석 — **캐서 쓰는 구조물**의 돌 체적. 사토에서 뺄 밑수이고 **여기서 빼지 않는다.**
if is_collected_stone(options):
collected = max(stone_body + wedge + max(rubble, 0.0), 0.0)
if collected > 0:
components.append(
Component(
"채집석",
"",
collected,
DESTINATION["채집석"],
f"면석 몸통 {stone_body:.3f} + 고임돌 {wedge:.3f} + 막자갈"
f" {max(rubble, 0.0):.3f} ㎥ · 현장 채집분 ·"
" ⚠ 여기서 빼지 않음 — 사토에서 한 번만 뺌",
)
)
# 버림 콘크리트 — 기초 바닥에 까는 얇은 층. 빼려면 저장 제원에서 「안 넣음」으로 둔다.
base_width = thickness + constants["excavation_extra_m"]
blinding = _blinding_component(base_width, length_m, options)
if blinding is not None:
components.append(blinding)
# 터파기·되메우기·잔토 — 토공으로 합산되는 값이다(내역 줄이 아니다). # 터파기·되메우기·잔토 — 토공으로 합산되는 값이다(내역 줄이 아니다).
excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m excavation = height_m * (thickness + constants["excavation_extra_m"]) * length_m
backfill = height_m * constants["backfill_thickness_m"] * length_m backfill = height_m * constants["backfill_thickness_m"] * length_m
@@ -781,5 +887,15 @@ def build_table(
"surcharge_applied": False, "surcharge_applied": False,
"mix_components_found": violations, "mix_components_found": violations,
"structure_count": len(quantities), "structure_count": len(quantities),
# ⚠ 사토에서 뺄 밑수 — **양수**로 낸다. 빼는 것은 유토곡선(랩탑 메인) 몫이다.
COLLECTED_STONE_KEY: round(
sum(
component.amount
for item in quantities
for component in item.components
if component.name == "채집석"
),
3,
),
"amount_spread": spread_by_unit(totals.values(), value_key="amount"), "amount_spread": spread_by_unit(totals.values(), value_key="amount"),
} }
@@ -131,6 +131,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
slope.get("rows") or [], slope.get("rows") or [],
settings.get("topsoil_thickness_m"), settings.get("topsoil_thickness_m"),
{type_id: definition.name for type_id, definition in structure_type_map().items()}, {type_id: definition.name for type_id, definition in structure_type_map().items()},
# 부대시설 개소 — 산식으로 만들지 않고 **설계자가 넣은 값**만 쓴다(확정 ⑬).
settings.get("ancillary_counts") or {},
) )
method, method_is_default = concrete_placing_method(settings) method, method_is_default = concrete_placing_method(settings)
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다. # ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
@@ -102,6 +102,13 @@ def default_settings() -> dict[str, Any]:
# ⚠ 기본은 `None` — 「안 정함」과 「일부러 레디믹스트를 고른 것」을 갈라야 # ⚠ 기본은 `None` — 「안 정함」과 「일부러 레디믹스트를 고른 것」을 갈라야
# 화면이 「기본값 적용 중」을 정직하게 띄운다. 값을 미리 넣으면 그 구별이 사라진다. # 화면이 「기본값 적용 중」을 정직하게 띄운다. 값을 미리 넣으면 그 구별이 사라진다.
"concrete_placing_method": None, "concrete_placing_method": None,
# 부대시설 개소 — `{항목키: 개소}` (2026-09-09 사용자 확정 ⑬).
# ⚠ **산식으로 만들지 않는다.** 국가지점번호판은 임도규정 제26조제5항이
# 「500미터 마다 설치·관리하되 **필요시 거리를 조정**」이라 하고, 기점 포함·
# 종점 잔여·갈림길 중복을 원문이 안 정한다(지식DB 부대시설 [구현]).
# ⇒ `ceil(연장÷500)` 을 확정 산식으로 쓰지 않고 **개소를 받는다.**
# 비워 두면 물량을 안 낸다(0 으로 때우지 않음).
"ancillary_counts": {},
"dataset_versions": {}, "dataset_versions": {},
}, },
"estimation": { "estimation": {
@@ -75,10 +75,9 @@
"group": "층따기", "group": "층따기",
"work_item_code": "FP-09-18", "work_item_code": "FP-09-18",
"master_name": "층따기", "master_name": "층따기",
"basis_unit": "", "basis_unit": "",
"basis_source": "품셈 9-18 [주]Q1 = 3600×q×K×f×E/㎝ = ㎥/시간」 — 절 머리에 「(단위: …)」가 없고 **공식으로만** 단위가 밝혀지는 자리라 마스터 `basis_unit` 이 비어 있다(2026-09-08 B09 확인).", "basis_source": "⭐ 2026-09-09 사용자 확정 ⑦ — 층따기 수량 단위는 **㎡**(실무 관행). 밑수는 **원지반 표면의 경사길이**(B06 `design.bench_cut_length_m`)를 평균단면적법으로 면적화한 값이다. ⚠ 품셈 9-18 [주]는 `Q1 = 3600×q×K×f×E/㎝ = ㎥/시간` 이라 **공식은 체적 기준**이다 — 단의 높이·폭이 설계도서 값이라 체적을 지어낼 수 없어 면적으로 간다. ⇒ **받는 쪽(B09)이 ㎡ 단가를 세워야 한다**(㎥ 단가를 그대로 곱하면 금액이 틀림).",
"mismatch_reason": "층따기는 품셈이 **체적(㎥)**으로 세는데 우리 집계는 **성토 비탈면적(㎡)** 입니다 — 층따기 단의 높이·폭이 있어야 체적이 나옵니다(교본: 「층따기 높이·폭은 설계도서에 명시」). 그 값이 정해지면 물량이 섭니다.", "unit_note": "앞서 2026-09-08 에는 이 어긋남을 「막힘」으로 두어 금액이 안 섰다(410만원 자리). 사용자 확정으로 ㎡ 가 정본이 되었으므로 막지 않는다."
"mismatch_kind": "input_missing"
}, },
{ {
"group": "면고르기", "group": "면고르기",