- 자재 = 다짐 후 부피 ÷ 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
160 lines
5.7 KiB
TypeScript
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")));
|
|
}
|