"""혼합석(쇄석) 부설 — **법령 조건 자동 · 연약·습윤 칸 · 「비포장 전 구간」 고르개** (2026-09-15). 근거 ① 법령 별표2 Ⅰ.2.바.(2) — 종단 8% 초과 사질·점토 구간 · 8% 이하 연약·습윤 구간에 쇄석·자갈 부설 (최소 요구 · 조건이 또렷함 ⇒ 기본) ② 임도기술교본 3-2 — 「콘크리트 포장 구간 이외에는 혼합석 부설 다짐 후 0.10m 내외」 (더 넓은 권장 ⇒ 고르개) ③ 임도기술교본 10-1 · 부록 7-2 — 포설 전 10㎝ 터파기 · 적용범위 「노면의 토사구간」 ④ 품셈 11-4 쇄석·혼합석 부설(㎥ · 백호 부설만) · 1-2-3 체적환산표(혼합석 줄 없음) ⚠ 면적은 측점 사이 평균(끝 둘 중 하나만 해당이면 반만) — 토적표 평균단면적법과 같은 결. """ from __future__ import annotations from typing import Any from B08_Quantity.B08_Quantity_Engine_UnitQuantity_Base import _num LEGAL_GRADE_PCT = 8.0 THICKNESS_SUGGESTED_M = 0.10 # 교본 3-2 「다짐 후 0.10m 내외」 PRE_EXCAVATION_M = 0.10 # 교본 10-1 「포설 전 10㎝ 터파기」 C_SUGGESTED, L_SUGGESTED = 0.85, 1.25 # 실무 소광 관측 NOTE_LEGAL = ( "법령 별표2 Ⅰ.2.바.(2) 조건으로 자동 — 종단 8% 초과 토사 측점 + 8% 이하 중 연약·습윤 칸 구간" " · 교본 3-2 는 「콘크리트 포장 구간 이외」 전부(산출 조건 「비포장 전 구간」으로 넓힘)" ) NOTE_ALL = ( "비포장 전 구간 — 임도기술교본 3-2 「콘크리트 포장 구간 이외에는 혼합석 부설」 · 법령 별표2" " Ⅰ.2.바.(2) 는 종단 8% 초과 사질·점토 / 8% 이하 연약·습윤 구간만 요구" ) NOTE_SOIL = ( "법령은 사질·점토를 가르는데 우리 프리셋은 토사 하나 — 토사 전부를 해당으로 봄(암은 비해당)" " · 토사 = B06 측점 토사 토글(구성비는 암 몫만 가름)" ) NOTE_GRADE = "종단 경사 = 종단 계획선에서 측점을 감싼 구간 경사 중 큰 값(B05 포장 제안과 같은 식)" NOTE_WIDTH = "폭 = 차도 표준 폭 + 확폭(노견 제외 · 실무 소광 「혼합석부설 B=3.0」)" NOTE_COMPACTION = ( "다짐 줄 없음 — 품셈 11-4 는 부설만이고 산림품셈에 쇄석 다짐 공종이 없음" " · 9-16-2 노체다짐은 「대규모 성토지 층다짐」 조건이라 안 이음" ) NOTE_EXCAVATION = ( "사전 터파기 — 교본 10-1 「포설 전 10㎝ 터파기」 · 부록 7-2 적용범위 「노면의 토사구간」이라" " 토사 측점만 · 산림품셈에 노면 사전 터파기 공종 없음 — 9-3-2 토사깍기(기계)로 이음" " · 파낸 흙의 운반·사토는 유토곡선 밖이라 안 셈" ) def _factor_note(c: float, l_factor: float, suggested: bool) -> str: head = ( f"C {c:g} · L {l_factor:g} — 제안값 실무 소광 관측" if suggested else f"C {c:g} · L {l_factor:g} — 산출 조건 칸" ) return ( f"{head} · 자재 = 다짐 후 부피 ÷ C × L · 원문 표에 혼합석 줄 없음(품셈 1-2-3 가까운 줄" " 역(礫) L 1.10~1.20 · C 1.05~1.10) · ⚠ 소광 C 0.85(다지면 줄어듦)와 원문 역 C(늘어남)는" " 방향이 반대 — 같은 C 라도 뜻이 다를 수 있음" ) def _qualifies( chainage: float, design: dict[str, Any], grades: dict[float, float], soft_wet: list[tuple[float, float]], all_unpaved: bool, ) -> bool | None: """이 측점에 까는가 — 경사를 모르면 `None`(판정 못 함).""" if design.get("paved"): return False if all_unpaved: return True grade = grades.get(round(chainage, 3)) if grade is None: return None if grade > LEGAL_GRADE_PCT: return design.get("ground_type") == "soil" return any(start <= chainage <= end for start, end in soft_wet) def gravel_surfacing( designs: list[dict[str, Any]] | None, grades: dict[float, float] | None, settings: dict[str, Any], ) -> dict[str, Any]: """노선 혼합석 부설 한 벌 — 면적·부피·사전 터파기·사유.""" grades = {round(float(k), 3): float(v) for k, v in (grades or {}).items()} all_unpaved = bool(settings.get("gravel_all_unpaved")) soft_wet = [ tuple(sorted((_num(r.get("from_m")), _num(r.get("to_m"))))) for r in settings.get("gravel_soft_wet_ranges") or [] if isinstance(r, dict) ] stations = sorted( ( (_num(item.get("chainage_m")), item["design"]) for item in designs or () if isinstance(item, dict) and isinstance(item.get("design"), dict) ), key=lambda row: row[0], ) weights: list[tuple[float, float, float]] = [] # (측점, 폭×해당, 폭×해당×토사) unknown = 0 for chainage, design in stations: hit = _qualifies(chainage, design, grades, soft_wet, all_unpaved) unknown += hit is None width = ( _num(design.get("carriageway_standard_width_m")) + _num(design.get("widening_left_m")) + _num(design.get("widening_right_m")) ) on = width if hit else 0.0 weights.append((chainage, on, on if design.get("ground_type") == "soil" else 0.0)) area = soil_area = 0.0 for (s0, w0, t0), (s1, w1, t1) in zip(weights, weights[1:]): area += (w0 + w1) / 2 * (s1 - s0) soil_area += (t0 + t1) / 2 * (s1 - s0) thickness = _num(settings.get("gravel_thickness_m")) or THICKNESS_SUGGESTED_M c = _num(settings.get("gravel_conversion_c")) or C_SUGGESTED l_factor = _num(settings.get("gravel_conversion_l")) or L_SUGGESTED suggested = not (settings.get("gravel_conversion_c") or settings.get("gravel_conversion_l")) notes = [NOTE_ALL if all_unpaved else NOTE_LEGAL, NOTE_SOIL, NOTE_GRADE, NOTE_WIDTH] notes.append( f"두께(다짐 후) {thickness:g}m — " + ( "산출 조건 칸" if settings.get("gravel_thickness_m") else "제안값 교본 3-2 「0.10m 내외」" ) ) notes += [_factor_note(c, l_factor, suggested), NOTE_COMPACTION, NOTE_EXCAVATION] if unknown: notes.append(f"종단 경사를 못 읽은 측점 {unknown}곳은 안 깜(판정 못 함)") compacted = area * thickness return { "all_unpaved": all_unpaved, "area_m2": area, "soil_area_m2": soil_area, "thickness_m": thickness, "compacted_m3": compacted, "loose_m3": compacted / c * l_factor, "excavation_soil_m3": soil_area * PRE_EXCAVATION_M, "notes": notes, # 칸이 비면 쓰는 제안값 — 화면이 회색으로 보임(값의 정의처는 여기 한 곳). "suggested": {"thickness_m": THICKNESS_SUGGESTED_M, "c": C_SUGGESTED, "l": L_SUGGESTED}, } def gravel_material_rows(result: dict[str, Any] | None) -> list[dict[str, Any]]: """혼합석 자재(흐트러진 부피) → 자재총괄 성분 한 줄 — 부피가 없으면 빈 목록.""" amount = _num((result or {}).get("loose_m3")) if amount <= 0: return [] return [ { "name": "혼합석", "spec": "", "unit": "㎥", "amount": amount, "destination": "material", "source": "혼합석 부설", } ]