fix(B08): 제근 사유가 틀린 말이었음 — 본수가 아니라 임목축적으로 갈림
- 「입목 본수를 들고 있지 않음」이 원문과 달랐음. 품셈 9-21 [주]① 은 **소림(30~60㎥/㏊)·중림(60~90)·밀림(90 이상)** 으로 가름 — 본수가 아니라 **축적** - ⚠⚠ 그리고 **9-21 표에 밑수 단위 열이 없음**(9-20 은 「10주당」이라 적혀 있음). 등급을 넣어도 「무엇당 값」인지 모르면 못 셈 — 원문 확인이 필요한 자리로 사유에 적음 - 임목축적 등급 설정·화면 칸 신설(`stand_volume_class`) + 근거 한 줄 (산림조사부·영림계획에서 옮겨 적는 값이라 프로그램이 만들 수 없음) - 대상 면적(벌개제근 연동, 지장목제거와 같은 자리)을 참고값으로 실음 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -99,6 +99,7 @@ def preparation_rows(
|
||||
slope_rows: Iterable[dict[str, Any]] = (),
|
||||
topsoil_thickness_m: float | None = None,
|
||||
topsoil_haul_distance_m: float | None = None,
|
||||
stand_volume_class: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""준비공 줄 — 값이 서는 것과 안 서는 것을 **한 목록에** 낸다."""
|
||||
slope = slope_totals or {}
|
||||
@@ -125,15 +126,7 @@ def preparation_rows(
|
||||
topsoil,
|
||||
# ⚠ 법이 요구하는 **운반·적치** — 제거 물량이 곧 밑수다(별표2).
|
||||
_topsoil_haul_row(topsoil, topsoil_haul_distance_m),
|
||||
{
|
||||
"group": "준비공",
|
||||
"item": "제근·뿌리다듬기",
|
||||
"unit": "개",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": "단위가 「개」(그루 수)인데 입목 본수를 들고 있지 않음 (품셈 9-20~21)",
|
||||
"work_item_code": "FP-09-21",
|
||||
},
|
||||
_root_removal_row(slope, stand_volume_class),
|
||||
_batter_frame_row(list(slope_rows)),
|
||||
_level_frame_row(list(slope_rows)),
|
||||
]
|
||||
@@ -185,6 +178,48 @@ TOPSOIL_HAUL_LAW = (
|
||||
)
|
||||
|
||||
|
||||
#: 제근 — ⚠ **본수가 아니라 임목축적으로 갈린다**(2026-09-09 원문 확인).
|
||||
#: 품셈 9-21 [주]① 「소림 : 임목축적이 30㎥/㏊ 이상 60 미만 · 중림 : 60 이상 90 미만 ·
|
||||
#: 밀림 : 90 이상」. ⇒ 물어야 할 값은 **본/ha 밀도가 아니라 축적 등급**이다.
|
||||
#: ⚠⚠ **그런데 원문 표에 밑수 단위 열이 없다** — 9-20(뿌리다듬기·적재)은 「10주당」이라
|
||||
#: 적혀 있는데 9-21 표에는 그 표기가 없다. 등급을 넣어도 **무엇당 값인지 모르면 못 센다.**
|
||||
#: 지어내지 않고 그 사실을 사유로 낸다(원문 확인이 필요한 자리).
|
||||
STAND_VOLUME_CLASSES = ("소림", "중림", "밀림")
|
||||
ROOT_REMOVAL_BASIS_UNKNOWN = (
|
||||
"⚠ 품셈 9-21 표에 **밑수 단위가 없음** — 9-20 은 「10주당」이라 적혀 있으나 9-21 에는"
|
||||
" 그 표기가 없어 「무엇당 값」인지 원문으로 확인해야 함. 등급을 넣어도 그 전에는 못 셈"
|
||||
)
|
||||
ROOT_REMOVAL_CLASS_MISSING = (
|
||||
"임목축적 등급이 아직 입력되지 않았습니다 — 품셈 9-21 [주]① 이 소림(30~60㎥/㏊)·"
|
||||
"중림(60~90)·밀림(90 이상)으로 가름. ⚠ 본수가 아니라 **축적**이고, 산림조사부·영림계획에서"
|
||||
" 옮겨 적는 값이라 프로그램이 만들 수 없음"
|
||||
)
|
||||
|
||||
|
||||
def _root_removal_row(slope: dict[str, float], stand_volume_class: str | None) -> dict[str, Any]:
|
||||
"""제근·뿌리다듬기 — 등급과 밑수 단위가 다 서야 값이 난다.
|
||||
|
||||
⚠ 대상 면적은 지장목제거와 같은 자리(벌개제근 연동)라 이미 서 있다 — 참고로 보인다.
|
||||
"""
|
||||
area = float(slope.get("tree_removal_fill", 0.0)) + float(slope.get("tree_removal_cut", 0.0))
|
||||
picked = str(stand_volume_class or "").strip()
|
||||
reasons = [ROOT_REMOVAL_BASIS_UNKNOWN]
|
||||
if picked in STAND_VOLUME_CLASSES:
|
||||
reasons.append(f"등급은 「{picked}」로 정해져 있음")
|
||||
else:
|
||||
reasons.append(ROOT_REMOVAL_CLASS_MISSING)
|
||||
return {
|
||||
"group": "준비공",
|
||||
"item": "제근·뿌리다듬기",
|
||||
"unit": "개",
|
||||
"amount": None,
|
||||
"status": STATUS_PENDING,
|
||||
"reason": " · ".join(reasons) + f" (대상 면적 {area:,.1f}㎡ — 벌개제근 연동)",
|
||||
"reference_amount": area,
|
||||
"work_item_code": "FP-09-21",
|
||||
}
|
||||
|
||||
|
||||
def _topsoil_haul_row(topsoil: dict[str, Any], distance_m: float | None) -> dict[str, Any]:
|
||||
"""표토 운반 — 물량은 제거 물량 그대로, 거리는 설계 입력.
|
||||
|
||||
@@ -417,10 +452,17 @@ def build_table(
|
||||
names: dict[str, str] | None = None,
|
||||
ancillary_counts: dict[str, Any] | None = None,
|
||||
topsoil_haul_distance_m: float | None = None,
|
||||
stand_volume_class: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""화면·인계가 그대로 쓰는 모양. **못 서는 줄도 목록에 남는다.**"""
|
||||
rows = (
|
||||
preparation_rows(slope_totals, slope_rows, topsoil_thickness_m, topsoil_haul_distance_m)
|
||||
preparation_rows(
|
||||
slope_totals,
|
||||
slope_rows,
|
||||
topsoil_thickness_m,
|
||||
topsoil_haul_distance_m,
|
||||
stand_volume_class,
|
||||
)
|
||||
+ erosion_rows(structures, names)
|
||||
+ ancillary_rows(ancillary_counts)
|
||||
)
|
||||
|
||||
@@ -140,6 +140,8 @@ async def get_earthwork_table(project_id: UUID, route_id: int) -> JSONResponse:
|
||||
settings.get("ancillary_counts") or {},
|
||||
# 표토 운반거리 — 별표2 가 요구하는 운반·적치의 밑수(거리는 현장값).
|
||||
settings.get("topsoil_haul_distance_m"),
|
||||
# 임목축적 등급 — 품셈 9-21 제근이 소·중·밀로 갈리는 축(본수가 아니다).
|
||||
settings.get("stand_volume_class"),
|
||||
)
|
||||
method, method_is_default = concrete_placing_method(settings)
|
||||
# ⚠ 금액에 바로 걸리는 값이라 「기본값으로 돌고 있음」을 응답에 실어 화면이 띄우게 한다.
|
||||
@@ -320,6 +322,8 @@ class QuantitySettingsBody(BaseModel):
|
||||
structure_trench_water: str | None = None
|
||||
# 표토 운반거리(m) — 별표2 가 요구하는 운반·적치의 밑수. 비면 그 줄이 막힌다.
|
||||
topsoil_haul_distance_m: float | None = None
|
||||
# 임목축적 등급 — "소림"·"중림"·"밀림"(품셈 9-21 [주]①). `""` 는 「안 정함」이다.
|
||||
stand_volume_class: str | None = None
|
||||
|
||||
|
||||
#: `None` 이 「안 정함」을 뜻하는 칸 — 저장에서 **버리지 않고 그대로 덮어쓴다**.
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface QuantitySettings {
|
||||
topsoil_haul_distance_m?: number | null;
|
||||
/** 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13). */
|
||||
ancillary_counts?: Record<string, number | null>;
|
||||
/** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */
|
||||
stand_volume_class?: string | null;
|
||||
}
|
||||
|
||||
export interface EarthworkTable {
|
||||
|
||||
@@ -92,6 +92,7 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
|
||||
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,
|
||||
stand_volume_class: draft.stand_volume_class,
|
||||
// 개소는 **통째로** 보낸다 — 지운 항목까지 그대로 가야 되돌릴 길이 있다.
|
||||
ancillary_counts: draft.ancillary_counts,
|
||||
}),
|
||||
@@ -265,6 +266,8 @@ interface DraftSettings {
|
||||
topsoil_haul_distance_m: number | null;
|
||||
// 부대시설 개소 — `{항목키: 개소}`. ⚠ 산식으로 만들지 않는다(확정 13).
|
||||
ancillary_counts: Record<string, number | null>;
|
||||
// 임목축적 등급 — "소림"·"중림"·"밀림". ⚠ 본수가 아니라 축적이다(품셈 9-21 [주]①).
|
||||
stand_volume_class: string;
|
||||
// 자재별 관급/사급 — 표 안에서 줄마다 고른 값.
|
||||
material_supply: Record<string, SupplyChoice>;
|
||||
dirty: boolean;
|
||||
@@ -514,6 +517,23 @@ function buildQuantitySidePanel(
|
||||
),
|
||||
);
|
||||
panel.append(hintRow(L("B08_Quantity_Side_TopsoilHaul_Hint")));
|
||||
panel.append(
|
||||
selectField(
|
||||
L("B08_Quantity_Side_StandVolume_Label"),
|
||||
draft.stand_volume_class,
|
||||
[
|
||||
{ value: "", label: L("B08_Quantity_StandVolume_Unset") },
|
||||
{ value: "소림", label: L("B08_Quantity_StandVolume_Low") },
|
||||
{ value: "중림", label: L("B08_Quantity_StandVolume_Mid") },
|
||||
{ value: "밀림", label: L("B08_Quantity_StandVolume_High") },
|
||||
],
|
||||
(value) => {
|
||||
draft.stand_volume_class = value;
|
||||
draft.dirty = true;
|
||||
},
|
||||
),
|
||||
);
|
||||
panel.append(hintRow(L("B08_Quantity_Side_StandVolume_Hint")));
|
||||
|
||||
// ── 부대시설 개소 — ⚠ **산식으로 만들지 않는다**(확정 13). 넣어야 줄이 선다 ──
|
||||
panel.append(field(L("B08_Quantity_Side_Ancillary"), ""));
|
||||
@@ -788,6 +808,7 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
|
||||
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,
|
||||
stand_volume_class: (stored.stand_volume_class as string) ?? "",
|
||||
ancillary_counts: {
|
||||
...((stored.ancillary_counts ?? {}) as Record<string, number | null>),
|
||||
},
|
||||
|
||||
@@ -131,6 +131,10 @@ def default_settings() -> dict[str, Any]:
|
||||
# 높은 장소로 **운반하고 쌓아두어야** 한다」. 제거만 세면 법이 요구하는 운반이 빠진다.
|
||||
# 거리는 현장값이라 품셈이 정하지 않는다 — 비면 운반 줄이 막힌 채로 선다.
|
||||
"topsoil_haul_distance_m": None,
|
||||
# 임목축적 등급 — 품셈 9-21 제근이 **소·중·밀** 셋으로 갈리는 축.
|
||||
# [주]① 「소림 30~60㎥/㏊ · 중림 60~90 · 밀림 90 이상」 — 본수가 아니라 **축적**이다.
|
||||
# ⚠ 산림조사부·영림계획에서 옮겨 적는 값이라 프로그램이 만들 수 없다. 비면 막는다.
|
||||
"stand_volume_class": None,
|
||||
"dataset_versions": {},
|
||||
},
|
||||
"estimation": {
|
||||
|
||||
@@ -674,6 +674,15 @@ export const ui_locales_b2 = {
|
||||
"⚠ 「육상」은 통상값이고 사용자 확정이 아닙니다(확정 3차 ④) — 품셈 9-13 의 18구분이 이 값으로 갈립니다",
|
||||
"⚠ “Dry” is a customary default, not a user decision — it selects one of the 18 sub-items",
|
||||
],
|
||||
B08_Quantity_Side_StandVolume_Label: ["임목축적 등급", "Stand volume class"],
|
||||
B08_Quantity_StandVolume_Unset: ["안 정함(줄이 막힘)", "Not set (row blocked)"],
|
||||
B08_Quantity_StandVolume_Low: ["소림(30~60㎥/㏊)", "Low (30–60 ㎥/ha)"],
|
||||
B08_Quantity_StandVolume_Mid: ["중림(60~90㎥/㏊)", "Mid (60–90 ㎥/ha)"],
|
||||
B08_Quantity_StandVolume_High: ["밀림(90㎥/㏊ 이상)", "High (90+ ㎥/ha)"],
|
||||
B08_Quantity_Side_StandVolume_Hint: [
|
||||
"제근(품셈 9-21)이 이 등급으로 갈립니다 — ⚠ 본수가 아니라 축적이고, 산림조사부·영림계획에서 옮겨 적는 값입니다",
|
||||
"Root removal (9-21) splits by this class — stand volume, not tree count; copied from the forest survey",
|
||||
],
|
||||
B08_Quantity_Side_Ancillary: ["부대시설 개소", "Ancillary Facilities"],
|
||||
B08_Quantity_Side_Ancillary_Hint: [
|
||||
"개소를 넣어야 줄이 섭니다 — 원문이 배치 간격을 정하지 않아 연장÷500 같은 산식을 쓰지 않습니다(확정 13)",
|
||||
|
||||
Reference in New Issue
Block a user