feat(b08): 산출 조건 면고르기 칸 넷 — 절토면 토질 · 성토면 시공·토질 고르기(서버 선택지 그대로 · 제안값 없음) + 면적 덮어쓰기 둘(비우면 파종 면적) · 메인 ee9368ec 짝
배정 프로젝트: 절토면 토질을 고르고 [저장] → 내역 3-8-2 사유 「입력이 필요합니다」 → 「단가가 일부만 — 공기압축기(이동식) 3.5」 · 「안 정함」으로 되돌려 저장(네 칸 빈 값) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
This commit is contained in:
@@ -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 는 규정이라 값만 보인다. */
|
||||
|
||||
@@ -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} 을 화면에 다시 적음"
|
||||
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user