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

This commit is contained in:
2026-09-14 15:55:45 +09:00
5 changed files with 168 additions and 0 deletions
@@ -100,6 +100,11 @@ export interface QuantitySettings {
ancillary_counts?: Record<string, number | null>;
/** 임목축적 등급 — `"소림"`·`"중림"`·`"밀림"`(품셈 9-21 [주]①). 본수가 아니라 축적이다. */
stand_volume_class?: string | null;
/** 면고르기 갈래(서버 선택지 안) · 면적 덮어쓰기(㎡, 없음 = 파종 면적). */
face_dressing_cut_class?: string | null;
face_dressing_fill_class?: string | null;
face_dressing_fill_area_m2?: number | null;
face_dressing_cut_area_m2?: number | null;
/** 표토제거 대상 — 없음이면 기본(노면 + 절토) · `"road_only"` 는 노면만. */
topsoil_target?: string | null;
/** 규준틀 개소당 재료 — `{자재명: 수량}`. 비우면 제안값(실무 관측)이 선다. */
@@ -155,6 +160,8 @@ export interface EarthworkTable {
settings?: QuantitySettings;
/** 갈래별 토량환산계수 선택 상태 — 산출 조건 패널이 그린다. */
conversion_factor_choices?: Record<string, ConversionFactorChoice>;
/** 면고르기 갈래 선택지 — 품셈 9-19-1 원문 표 두 벌(서버 한 곳). 화면은 그대로 보임. */
face_dressing_choices?: { cut: string[]; fill: string[] };
/** 품셈 암종별 범위(안내용). 정의처가 서버라 내려받아 보인다. */
conversion_factor_pumsem_ranges?: PumsemRange[];
/** 도쟈 한계거리 — 지금 값·기본값·근거(서버가 정본). 종무대 20 m 는 규정이라 값만 보인다. */
+23
View File
@@ -41,6 +41,7 @@ import {
import { renderStructureSheets } from "./B08_Quantity_UI_StructureSheet";
import { renderStructureSummary } from "./B08_Quantity_UI_StructureSummary";
import { appendTreeWasteFields } from "./B08_Quantity_UI_Side_TreeWaste";
import { appendFaceDressingFields } from "./B08_Quantity_UI_Side_FaceDressing";
import { appendBenchCutFields } from "./B08_Quantity_UI_Side_BenchCut";
/** locale 헬퍼 */
@@ -108,6 +109,11 @@ async function saveQuantitySettings(projectId: string, draft: DraftSettings): Pr
// `""` 는 기본(노면 + 절토)으로 되돌림 — 서버가 None 으로 둔다.
topsoil_target: draft.topsoil_target,
stand_volume_class: draft.stand_volume_class,
// 면고르기 — 갈래 `""`·면적 `null` 도 그대로(「안 정함」·「파종 면적 그대로」로 되돌리는 길).
face_dressing_cut_class: draft.face_dressing_cut_class,
face_dressing_fill_class: draft.face_dressing_fill_class,
face_dressing_fill_area_m2: draft.face_dressing_fill_area_m2,
face_dressing_cut_area_m2: draft.face_dressing_cut_area_m2,
frame_material: draft.frame_material,
wood_chipping_enabled: draft.wood_chipping_enabled,
wood_chipping_volume_m3: draft.wood_chipping_volume_m3,
@@ -301,6 +307,11 @@ interface DraftSettings {
ancillary_counts: Record<string, number | null>;
// 임목축적 등급 — "소림"·"중림"·"밀림". ⚠ 본수가 아니라 축적이다(품셈 9-21 [주]①).
stand_volume_class: string;
// 면고르기 — 갈래 둘(서버 선택지) · 면적 덮어쓰기 둘(`null` = 파종 면적). 칸은 `_Side_FaceDressing`.
face_dressing_cut_class: string;
face_dressing_fill_class: string;
face_dressing_fill_area_m2: number | null;
face_dressing_cut_area_m2: number | null;
// 규준틀 개소당 재료 — 비우면 **제안값(실무 관측)**이 선다. 값이 아니라 「고칠 수 있음」이 요점.
frame_material: Record<string, number | null>;
// 임목파쇄 — **기본 꺼짐**(확정 5차 5번). 켜야 줄이 선다. 근주이식은 칸 자체가 없다.
@@ -510,6 +521,14 @@ function buildQuantitySidePanel(
}
}
// ── 면고르기 — 밑수가 파종 면적이라 반영률 바로 밑. 선택지는 서버 목록 그대로(칸은 따로 뺀 파일).
appendFaceDressingFields(panel, draft, table?.face_dressing_choices, {
field,
optionalNumberField,
selectField,
hintRow,
});
// ── 표토 두께 — 표토 운반 부피(제거 ㎡ × T)에 씀 · 제거 줄은 ㎡ 라 안 곱함(2026-09-13 「실무대로」) ──
// ⚠ 2026-09-08 ㉘ 자기 감사: 서버·엔진은 이 값을 받고 있었는데 **화면에 넣을 칸이 없었다.**
// 「죽은 칸」(넣어도 안 쓰임)의 반대 짝이다 — 쓰이는데 넣을 데가 없던 자리.
@@ -1002,6 +1021,10 @@ export async function renderB08Quantity(root: HTMLElement): Promise<void> {
topsoil_haul_distance_m: (stored.topsoil_haul_distance_m as number | null) ?? null,
topsoil_target: (stored.topsoil_target as string) ?? "",
stand_volume_class: (stored.stand_volume_class as string) ?? "",
face_dressing_cut_class: (stored.face_dressing_cut_class as string) ?? "",
face_dressing_fill_class: (stored.face_dressing_fill_class as string) ?? "",
face_dressing_fill_area_m2: (stored.face_dressing_fill_area_m2 as number | null) ?? null,
face_dressing_cut_area_m2: (stored.face_dressing_cut_area_m2 as number | null) ?? null,
frame_material: { ...((stored.frame_material ?? {}) as Record<string, number | null>) },
wood_chipping_enabled: Boolean(stored.wood_chipping_enabled),
wood_chipping_volume_m3: (stored.wood_chipping_volume_m3 as number | null) ?? null,
@@ -0,0 +1,95 @@
/* =============================================================================
* B08_Quantity_UI_Side_FaceDressing.ts
* 산출 조건 「면고르기」 구획 — 절토면 토질 · 성토면 시공·토질 고르기 둘 + 면적 덮어쓰기 둘
* (2026-09-14 브레인 판정 Ⓐ~Ⓓ · 서버 ee9368ec).
*
* 페이지 본문(`B08_Quantity_UI_Page.ts`)이 700줄을 넘어 새 칸을 이 파일로 뺌(CLAUDE.md 4장).
* ⚠ 선택지는 **서버가 내려준 목록 그대로**(`face_dressing_choices`) — 화면에 다시 적지 않음(두 벌 금지).
* ⚠ 제안값 없음(판정 Ⓒ) — 첫 보기가 「안 정함」이고 비면 내역 줄이 입력 사유로 섬.
* ⚠ 면적은 화면이 셈하지 않음 — 비우면 서버가 파종 면적(초류종자살포 × 반영률)을 씀.
* ========================================================================== */
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import type { SideFieldHelpers } from "./B08_Quantity_UI_Side_TreeWaste";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
export interface FaceDressingDraft {
/** `""` 는 「안 정함」 — 서버가 선택지 밖 값도 `""` 로 되돌림. */
face_dressing_cut_class: string;
face_dressing_fill_class: string;
/** `null` 은 「파종 면적 그대로」. */
face_dressing_fill_area_m2: number | null;
face_dressing_cut_area_m2: number | null;
dirty: boolean;
}
export interface FaceDressingChoices {
cut: string[];
fill: string[];
}
export function appendFaceDressingFields(
panel: HTMLElement,
draft: FaceDressingDraft,
choices: FaceDressingChoices | undefined,
h: SideFieldHelpers,
): void {
if (!choices) return;
const pick = (
label: keyof typeof ui_locales,
value: string,
list: string[],
set: (value: string) => void,
): HTMLElement =>
h.selectField(
L(label),
value,
[
{ value: "", label: L("B08_Quantity_FaceDressing_Unset") },
...list.map((name) => ({ value: name, label: name })),
],
(picked) => {
set(picked);
draft.dirty = true;
},
);
const area = (
label: keyof typeof ui_locales,
value: number | null,
set: (v: number | null) => void,
) => {
const row = h.optionalNumberField(L(label), value, "0.01", (typed) => {
set(typed);
draft.dirty = true;
});
const input = row.querySelector("input");
if (input) input.placeholder = L("B08_Quantity_FaceDressing_Area_Placeholder");
return row;
};
panel.append(h.field(L("B08_Quantity_Side_FaceDressing"), ""));
panel.append(
pick("B08_Quantity_FaceDressing_Cut", draft.face_dressing_cut_class, choices.cut, (v) => {
draft.face_dressing_cut_class = v;
}),
);
panel.append(
pick("B08_Quantity_FaceDressing_Fill", draft.face_dressing_fill_class, choices.fill, (v) => {
draft.face_dressing_fill_class = v;
}),
);
panel.append(
area("B08_Quantity_FaceDressing_FillArea", draft.face_dressing_fill_area_m2, (v) => {
draft.face_dressing_fill_area_m2 = v;
}),
);
panel.append(
area("B08_Quantity_FaceDressing_CutArea", draft.face_dressing_cut_area_m2, (v) => {
draft.face_dressing_cut_area_m2 = v;
}),
);
panel.append(h.hintRow(L("B08_Quantity_Side_FaceDressing_Hint")));
}
@@ -0,0 +1,32 @@
"""면고르기 산출 조건 칸 넷 — 화면이 서버 선택지를 그대로 쓰고 [저장]에 네 키를 싣는지(2026-09-14 메인 부탁).
서버(ee9368ec)는 `face_dressing_cut_class`·`face_dressing_fill_class`·`face_dressing_fill_area_m2`·
`face_dressing_cut_area_m2` 를 받고 선택지는 `earthwork-table` 의 `face_dressing_choices` 로 내려줌.
⚠ 선택지를 화면에 다시 적으면 두 벌이 됨 — TS 에 갈래 이름이 없어야 함.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
PAGE = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.ts").read_text(encoding="utf-8")
SIDE = ROOT / "B08_Quantity" / "B08_Quantity_UI_Side_FaceDressing.ts"
KEYS = (
"face_dressing_cut_class",
"face_dressing_fill_class",
"face_dressing_fill_area_m2",
"face_dressing_cut_area_m2",
)
def test_저장에_네_키를_싣고_불러온다() -> None:
for key in KEYS:
assert f"{key}: draft.{key}" in PAGE, f"[저장]에 {key} 없음"
assert f"{key}: (stored.{key}" in PAGE, f"불러오기에 {key} 없음"
def test_선택지는_서버_목록_그대로() -> None:
side = SIDE.read_text(encoding="utf-8")
assert "appendFaceDressingFields(" in PAGE and "face_dressing_choices" in PAGE
assert "choices.cut" in side and "choices.fill" in side
for name in ("모래ㆍ사질토", "성토면 · 인력", "성토면 · 기계"):
assert name not in side and name not in PAGE, f"갈래 이름 {name} 을 화면에 다시 적음"
+11
View File
@@ -746,6 +746,17 @@ export const ui_locales_b2 = {
"⚠ 임목폐기물은 분리발주 대상 아님(환경부 질의회신 — 임목폐기물처리 실정보고 p4 · 건설폐기물 업무처리지침: 건설폐기물 아닌 사업장 일반폐기물) — 기본 꺼짐으로 원가계산서 경비에 듭니다. 이 칸은 다른 건설폐기물(위탁처리 100톤 이상 분리발주 대상)이 생길 때 쓰는 칸 — 켜면 총원가 밖에 서고 총공사비에만 더합니다. 5톤 이상은 배출자 신고 대상(폐기물관리법 시행령 제2조8호)",
"Tree waste is not subject to separate ordering (Ministry of Environment reply; classified as business general waste) — off by default, inside the expense category. Use this switch for other construction waste (100 t or more). When on, it sits outside total cost. 5 t or more requires a discharge report",
],
B08_Quantity_Side_FaceDressing: ["면고르기", "Face dressing"],
B08_Quantity_FaceDressing_Cut: ["절토면 토질", "Cut face soil"],
B08_Quantity_FaceDressing_Fill: ["성토면 시공·토질", "Fill face method·soil"],
B08_Quantity_FaceDressing_Unset: ["안 정함(금액 안 섬)", "Not set (no amount)"],
B08_Quantity_FaceDressing_FillArea: ["성토면 면적(㎡)", "Fill face area (㎡)"],
B08_Quantity_FaceDressing_CutArea: ["절토면 면적(㎡)", "Cut face area (㎡)"],
B08_Quantity_FaceDressing_Area_Placeholder: ["파종 면적 그대로", "Same as seeding area"],
B08_Quantity_Side_FaceDressing_Hint: [
"품셈 9-19-1 원문 표 두 벌(절토면 토질 6 · 성토면 시공·토질) — 제안값 없음, 안 고르면 내역 줄이 입력 사유로 섭니다. 면적은 비우면 초류종자살포 면적(× 반영률) 그대로 · 넣으면 그 값으로 덮어씀",
"Standard estimate 9-19-1 tables (cut face soil · fill face method/soil) — no suggested value; unset leaves the bill row with an input reason. Area left blank uses the seeding area (× ratio); a value overrides it",
],
B08_Quantity_Side_SubgradeCompaction: ["노체다짐", "Subgrade Compaction"],
B08_Quantity_SubgradeCompaction_Label: ["노체다짐을 셀 것인가", "Count subgrade compaction"],
B08_Quantity_Side_SubgradeCompaction_Hint: [