Files
Aislo/B08_Quantity/B08_Quantity_UI_Side_Gravel.ts
T
eomsangdonandClaude Opus 5 4186a851a4 feat(b05·b08): 혼합석 부설 — 법령 별표2 조건 자동(종단 8% 초과 토사 · 연약·습윤 칸) · 「비포장 전 구간」 고르개(교본 3-2) · 11-4 ㎥ + 사전터파기 토공 축 · 자재총괄 혼합석(12장 D 포장 둘 · 브레인 ⓑ①~⑤)
- 자재 = 다짐 후 부피 ÷ C × L · 두께 제안 0.10(교본) · C 0.85 · L 1.25 제안(소광 관측 · 원문 표에 혼합석 줄 없음 · 역 C 와 방향 반대 사유)
- 종단 경사는 계획선에서 B05 포장 제안과 같은 한 벌(local_grade_pct 로 뺌 — 종단 파일 측점 경사엔 비정규 측점이 없었음)
- 해당 측점이 없으면 0 원 줄 안 세움 · 다짐 줄은 공종 없어 사유

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-15 04:55:31 +09:00

160 lines
5.7 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Side_Gravel.ts
* 산출 조건 「혼합석 부설」 구획 — 범위 고르개 · 연약·습윤 구간 · 두께·C·L (2026-09-15 브레인 포장 둘 ⓑ).
*
* 페이지 본문(`B08_Quantity_UI_Page.ts`)이 700줄을 넘어 새 칸을 이 파일로 뺌(CLAUDE.md 4장).
* ⚠ 화면은 셈하지 않음 — 면적·부피는 서버 `gravel` 을 보이기만 · 제안값도 서버 `gravel.suggested`.
* ⚠ 빈 칸(`null`)은 「제안값으로」 — 0 과 다름. 구간 글자를 못 읽으면 저장값을 안 바꾸고 알림.
* ========================================================================== */
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 GravelRange {
from_m: number;
to_m: number;
}
export interface GravelDraft {
gravel_all_unpaved: boolean;
gravel_soft_wet_ranges: GravelRange[];
gravel_thickness_m: number | null;
gravel_conversion_c: number | null;
gravel_conversion_l: number | null;
}
/** 서버 `gravel_surfacing` 결과 중 화면이 보이는 몫. */
export interface GravelSummary {
area_m2: number;
loose_m3: number;
excavation_soil_m3: number;
suggested?: { thickness_m: number; c: number; l: number };
}
export function gravelDraftFrom(stored: Record<string, unknown>): GravelDraft {
const ranges = Array.isArray(stored.gravel_soft_wet_ranges)
? (stored.gravel_soft_wet_ranges as GravelRange[])
: [];
const num = (value: unknown) => (typeof value === "number" ? value : null);
return {
gravel_all_unpaved: Boolean(stored.gravel_all_unpaved),
gravel_soft_wet_ranges: ranges.map((r) => ({ from_m: r.from_m, to_m: r.to_m })),
gravel_thickness_m: num(stored.gravel_thickness_m),
gravel_conversion_c: num(stored.gravel_conversion_c),
gravel_conversion_l: num(stored.gravel_conversion_l),
};
}
/** [저장] 몸통 — 구간은 **통째로**(지운 구간까지 가야 되돌릴 길이 있음) · 숫자 `null` 도 보냄. */
export function gravelPayload(draft: GravelDraft): GravelDraft {
return gravelDraftFrom(draft as unknown as Record<string, unknown>);
}
/** 「120~160, 480~520」 → 구간 목록. 한 조각이라도 못 읽으면 `null`(빈 글은 빈 목록). */
export function parseGravelRanges(text: string): GravelRange[] | null {
const parts = text
.split(",")
.map((part) => part.trim())
.filter(Boolean);
const ranges: GravelRange[] = [];
for (const part of parts) {
const match = /^(\d+(?:\.\d+)?)\s*[~\-]\s*(\d+(?:\.\d+)?)$/.exec(part);
if (!match) return null;
const a = Number(match[1]);
const b = Number(match[2]);
if (a === b) return null;
ranges.push({ from_m: Math.min(a, b), to_m: Math.max(a, b) });
}
return ranges;
}
export function appendGravelFields(
panel: HTMLElement,
draft: GravelDraft & { dirty: boolean },
summary: GravelSummary | undefined,
h: SideFieldHelpers,
): void {
panel.append(h.field(L("B08_Quantity_Side_Gravel"), ""));
panel.append(
h.selectField(
L("B08_Quantity_Gravel_Scope"),
draft.gravel_all_unpaved ? "all" : "",
[
{ value: "", label: L("B08_Quantity_Gravel_Scope_Legal") },
{ value: "all", label: L("B08_Quantity_Gravel_Scope_All") },
],
(value) => {
draft.gravel_all_unpaved = value === "all";
draft.dirty = true;
},
),
);
const row = document.createElement("label");
row.className = "b08-quantity__field";
const name = document.createElement("span");
name.textContent = L("B08_Quantity_Gravel_SoftWet");
const input = document.createElement("input");
input.type = "text";
input.className = "b08-quantity__input";
input.placeholder = L("B08_Quantity_Gravel_SoftWet_Placeholder");
input.value = draft.gravel_soft_wet_ranges.map((r) => `${r.from_m}~${r.to_m}`).join(", ");
const warn = h.hintRow("");
input.addEventListener("input", () => {
const parsed = parseGravelRanges(input.value);
warn.textContent = parsed ? "" : L("B08_Quantity_Gravel_SoftWet_Invalid");
if (!parsed) return;
draft.gravel_soft_wet_ranges = parsed;
draft.dirty = true;
});
row.append(name, input);
panel.append(row, warn);
const suggested = summary?.suggested;
const number = (
label: keyof typeof ui_locales,
value: number | null,
hint: number | undefined,
set: (v: number | null) => void,
) => {
const field = h.optionalNumberField(L(label), value, "0.01", (typed) => {
set(typed);
draft.dirty = true;
});
const box = field.querySelector("input");
if (box && hint !== undefined)
box.placeholder = `${L("B08_Quantity_Gravel_Suggested")} ${hint}`;
return field;
};
panel.append(
number(
"B08_Quantity_Gravel_Thickness",
draft.gravel_thickness_m,
suggested?.thickness_m,
(v) => {
draft.gravel_thickness_m = v;
},
),
number("B08_Quantity_Gravel_C", draft.gravel_conversion_c, suggested?.c, (v) => {
draft.gravel_conversion_c = v;
}),
number("B08_Quantity_Gravel_L", draft.gravel_conversion_l, suggested?.l, (v) => {
draft.gravel_conversion_l = v;
}),
);
if (summary) {
const fmt = (value: number) => value.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
panel.append(
h.hintRow(
`${L("B08_Quantity_Gravel_Now")} — ${fmt(summary.area_m2)}㎡ · ${fmt(summary.loose_m3)}㎥` +
` · ${L("B08_Quantity_Gravel_Excavation")} ${fmt(summary.excavation_soil_m3)}㎥`,
),
);
}
panel.append(h.hintRow(L("B08_Quantity_Side_Gravel_Hint")));
}