feat(B08): 표토 운반·적치 줄 신설 + 부대시설·표토거리 화면 칸
표토 운반(법이 요구하는데 제거만 세고 있던 자리) - 시행규칙 별표2 「표토는 전량 제거한 후 … 최고 홍수위보다 높은 장소로 운반하고 쌓아두어야 한다」 — 제거(9-15)만 세면 운반이 빠짐 - 물량은 제거 물량 그대로(다시 안 셈), 거리는 설계 입력 `topsoil_haul_distance_m` - 거리가 없으면 막고 사유 · 제거가 안 서면 운반도 안 섬(밑수가 그 줄) · 법 문구를 사유에 화면 칸(사용자 지시 「대신 페이지에 남길 것」) - 부대시설 개소 다섯(국가지점번호판·안내판·차단기·가설창고·수방자재) — 서버가 받고 있었는데 넣을 칸이 없던 자리. 산식으로 만들지 않는다는 사유를 칸 밑에 적음 - 표토 운반거리 칸 + 별표2 근거 한 줄 - tmp/tests/test_b08_topsoil_haul.py 신설(5건) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -98,12 +98,14 @@ def preparation_rows(
|
||||
slope_totals: dict[str, float] | None = None,
|
||||
slope_rows: Iterable[dict[str, Any]] = (),
|
||||
topsoil_thickness_m: float | None = None,
|
||||
topsoil_haul_distance_m: float | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""준비공 줄 — 값이 서는 것과 안 서는 것을 **한 목록에** 낸다."""
|
||||
slope = slope_totals or {}
|
||||
tree_area = float(slope.get("tree_removal_fill", 0.0)) + float(
|
||||
slope.get("tree_removal_cut", 0.0)
|
||||
)
|
||||
topsoil = _topsoil_row(slope, topsoil_thickness_m)
|
||||
return [
|
||||
{
|
||||
"group": "준비공",
|
||||
@@ -120,7 +122,9 @@ def preparation_rows(
|
||||
),
|
||||
"work_item_code": None,
|
||||
},
|
||||
_topsoil_row(slope, topsoil_thickness_m),
|
||||
topsoil,
|
||||
# ⚠ 법이 요구하는 **운반·적치** — 제거 물량이 곧 밑수다(별표2).
|
||||
_topsoil_haul_row(topsoil, topsoil_haul_distance_m),
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "제근·뿌리다듬기",
|
||||
@@ -169,6 +173,67 @@ def _topsoil_row(slope: dict[str, float], thickness_m: float | None) -> dict[str
|
||||
}
|
||||
|
||||
|
||||
#: 표토 운반·적치 — ⚠ **법이 요구하는데 우리가 제거만 세고 있던 자리**(2026-09-09).
|
||||
#: 시행규칙 별표2 Ⅰ.2.차.(6)·Ⅰ.3.카.(6):
|
||||
#: 「노면·절토대상지에 있는 입목…과 그 뿌리, **표토는 전량 제거한 후** 강우 시 유실되거나
|
||||
#: 경관에 저해되지 않도록 **최고 홍수위보다 높은 장소로 운반하고 쌓아두어야 한다**」
|
||||
#: ⇒ 제거(9-15)만 세면 **운반이 빠진다.** 물량은 제거 물량 그대로이고 **거리가 설계 입력**이다.
|
||||
#: ⚠ 거리를 지어내지 않는다 — 비면 막고 사유를 낸다.
|
||||
TOPSOIL_HAUL_LAW = (
|
||||
"⚠ 법정 의무 — 시행규칙 별표2 「표토는 전량 제거한 후 … 최고 홍수위보다 높은 장소로"
|
||||
" 운반하고 쌓아두어야 한다」. 제거만 세면 운반이 빠짐"
|
||||
)
|
||||
|
||||
|
||||
def _topsoil_haul_row(topsoil: dict[str, Any], distance_m: float | None) -> dict[str, Any]:
|
||||
"""표토 운반 — 물량은 제거 물량 그대로, 거리는 설계 입력.
|
||||
|
||||
⚠ 제거 줄이 안 서면(두께 미입력) 운반도 안 선다 — 밑수가 그 줄이기 때문이다.
|
||||
"""
|
||||
amount = topsoil.get("amount")
|
||||
distance = None
|
||||
if distance_m is not None:
|
||||
try:
|
||||
distance = float(distance_m)
|
||||
except (TypeError, ValueError):
|
||||
distance = None
|
||||
if amount is None:
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": f"{TOPSOIL_HAUL_LAW} · 제거 물량이 아직 안 서서 운반도 못 셈(두께 먼저)",
|
||||
"work_item_code": None,
|
||||
}
|
||||
if distance is None or distance <= 0:
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": (
|
||||
f"{TOPSOIL_HAUL_LAW} · 운반거리가 아직 입력되지 않았습니다 — 「최고 홍수위보다"
|
||||
f" 높은 장소」는 현장에서 정하는 자리라 품셈이 거리를 주지 않습니다"
|
||||
f" (운반할 물량 {float(amount):,.2f}㎥)"
|
||||
),
|
||||
"reference_amount": float(amount),
|
||||
"work_item_code": None,
|
||||
}
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "표토 운반·적치",
|
||||
"unit": "㎥",
|
||||
"amount": float(amount),
|
||||
"status": STATUS_READY,
|
||||
"reason": f"{TOPSOIL_HAUL_LAW} · 제거 물량 그대로 · 운반거리 {distance:g}m(설계 입력)",
|
||||
"work_item_code": "FP-10-12",
|
||||
"haul_distance_m": distance,
|
||||
}
|
||||
|
||||
|
||||
def _batter_frame_row(slope_rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""비탈 규준틀 한 줄. **개소는 원문 기준으로 서고 재료는 미확보**다."""
|
||||
count, notes = batter_frame_count(slope_rows)
|
||||
@@ -351,10 +416,11 @@ def build_table(
|
||||
topsoil_thickness_m: float | None = None,
|
||||
names: dict[str, str] | None = None,
|
||||
ancillary_counts: dict[str, Any] | None = None,
|
||||
topsoil_haul_distance_m: float | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
|
||||
rows = (
|
||||
preparation_rows(slope_totals, slope_rows, topsoil_thickness_m)
|
||||
preparation_rows(slope_totals, slope_rows, topsoil_thickness_m, topsoil_haul_distance_m)
|
||||
+ erosion_rows(structures, names)
|
||||
+ ancillary_rows(ancillary_counts)
|
||||
)
|
||||
|
||||
@@ -138,6 +138,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
{type_id: definition.name for type_id, definition in structure_type_map().items()},
|
||||
# 부대시설 개소 — 산식으로 만들지 않고 **설계자가 넣은 값**만 쓴다(확정 ⑬).
|
||||
settings.get("ancillary_counts") or {},
|
||||
# 표토 운반거리 — 별표2 가 요구하는 운반·적치의 밑수(거리는 현장값).
|
||||
settings.get("topsoil_haul_distance_m"),
|
||||
)
|
||||
method, method_is_default = concrete_placing_method(settings)
|
||||
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
|
||||
@@ -316,6 +318,8 @@ class QuantitySettingsBody(BaseModel):
|
||||
rubble_base_thickness_m: float | None = None
|
||||
# 구조물터파기 용수 유무 — "육상"·"용수". ⚠ 기본 육상은 **통상값**이지 사용자 확정이 아니다.
|
||||
structure_trench_water: str | None = None
|
||||
# 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다.
|
||||
topsoil_haul_distance_m: float | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
@@ -325,6 +329,7 @@ NULLABLE_SETTING_KEYS = (
|
||||
"bench_cut_depth_m",
|
||||
"spoil_site_distance_m",
|
||||
"rubble_base_thickness_m",
|
||||
"topsoil_haul_distance_m",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -75,6 +75,10 @@ export interface QuantitySettings {
|
||||
spoil_site_distance_m?: number | null;
|
||||
/** 구조물터파기 용수 — `"육상"`·`"용수"`. ⚠ 「육상」은 통상값이지 사용자 확정이 아니다. */
|
||||
structure_trench_water?: string | null;
|
||||
/** 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다. */
|
||||
topsoil_haul_distance_m?: number | null;
|
||||
/** 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13). */
|
||||
ancillary_counts?: Record<string, number | null>;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
|
||||
@@ -91,6 +91,9 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
||||
rubble_base_thickness_m: draft.rubble_base_thickness_m,
|
||||
spoil_site_distance_m: draft.spoil_site_distance_m,
|
||||
structure_trench_water: draft.structure_trench_water,
|
||||
topsoil_haul_distance_m: draft.topsoil_haul_distance_m,
|
||||
// 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다.
|
||||
ancillary_counts: draft.ancillary_counts,
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -110,6 +113,15 @@ function field(label: string, value: string): HTMLElement {
|
||||
return row;
|
||||
}
|
||||
|
||||
/** 부대시설 항목 키 — 서버 `ANCILLARY_ITEMS` 와 **같은 차례·같은 낱말**이라야 한다. */
|
||||
const ANCILLARY_KEYS = [
|
||||
"national_point_sign",
|
||||
"guide_sign",
|
||||
"gate",
|
||||
"site_container",
|
||||
"flood_supplies",
|
||||
] as const;
|
||||
|
||||
/** 칸 밑에 붙는 **근거 한 줄** — 왜 그 값인지 화면에서 보이게 한다(2026-09-09 사용자 지시). */
|
||||
function hintRow(text: string): HTMLElement {
|
||||
const row = document.createElement("p");
|
||||
@@ -249,6 +261,10 @@ interface DraftSettings {
|
||||
spoil_site_distance_m: number | null;
|
||||
// 구조물터파기 용수 — "육상"·"용수". ⚠ 「육상」은 **통상값**이지 사용자 확정이 아니다.
|
||||
structure_trench_water: string;
|
||||
// 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. `null` 은 「안 정함」.
|
||||
topsoil_haul_distance_m: number | null;
|
||||
// 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13).
|
||||
ancillary_counts: Record<string, number | null>;
|
||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||
material_supply: Record<string, SupplyChoice>;
|
||||
dirty: boolean;
|
||||
@@ -486,6 +502,35 @@ function buildQuantitySidePanel(
|
||||
),
|
||||
);
|
||||
panel.append(hintRow(L("B08_Quantity_Side_Water_Hint")));
|
||||
panel.append(
|
||||
optionalNumberField(
|
||||
L("B08_Quantity_Side_TopsoilHaul_Label"),
|
||||
draft.topsoil_haul_distance_m,
|
||||
"10",
|
||||
(value) => {
|
||||
draft.topsoil_haul_distance_m = value;
|
||||
draft.dirty = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
panel.append(hintRow(L("B08_Quantity_Side_TopsoilHaul_Hint")));
|
||||
|
||||
// ── 부대시설 개소 — ⚠ **산식으로 만들지 않는다**(확정 13). 넣어야 줄이 선다 ──
|
||||
panel.append(field(L("B08_Quantity_Side_Ancillary"), ""));
|
||||
for (const key of ANCILLARY_KEYS) {
|
||||
panel.append(
|
||||
optionalNumberField(
|
||||
L(`B08_Quantity_Ancillary_${key}` as keyof typeof ui_locales),
|
||||
draft.ancillary_counts[key] ?? null,
|
||||
"1",
|
||||
(value) => {
|
||||
draft.ancillary_counts[key] = value;
|
||||
draft.dirty = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
panel.append(hintRow(L("B08_Quantity_Side_Ancillary_Hint")));
|
||||
|
||||
// ── 콘크리트 타설 방식 — ⚠ **금액에 바로 걸리는 값**이라 잠정임을 조용히 두지 않는다 ──
|
||||
panel.append(field(L("B08_Quantity_Side_Placing"), ""));
|
||||
@@ -742,6 +787,10 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
||||
rubble_base_thickness_m: (stored.rubble_base_thickness_m as number | null) ?? null,
|
||||
spoil_site_distance_m: (stored.spoil_site_distance_m as number | null) ?? null,
|
||||
structure_trench_water: (stored.structure_trench_water as string) ?? "",
|
||||
topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null,
|
||||
ancillary_counts: {
|
||||
...((stored.ancillary_counts ?? {}) as Record<string, number | null>),
|
||||
},
|
||||
material_supply: { ...((stored.material_supply ?? {}) as Record<string, SupplyChoice>) },
|
||||
dirty: false,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user