Files
Aislo/B08_Quantity/B08_Quantity_UI_Earthwork.ts
T
eomsangdonandClaude Opus 5.5 aefcb23dd2 feat(b08): 3탭 토적표 — 옛 평균단면적법 엔진 복사 · 서버 읽기 전용 길 · 측점별 표 + 합계 줄(PLAN 61-3)
- 엔진 = old_code B08_Quantity_Engine_EarthworkTable.py 복사(B08_Quantity_Engine_Earthwork.py) · 계산식 그대로
- 고친 곳 = 입력부(암 종류 없으면 암 지반 유형) · 줄 cut_rock_kind · 합계 리핑암 · 발파암 칸
- 길 = GET /quantity/earthwork — B06 횡단 설계 저장 단면적(확정 노선 · 없으면 최신) · 환산계수 = 유토곡선과 같은 mixed_conversion_factors · 아무것도 안 씀
- 화면 = 측점 · 거리 · 절토(토사 · 리핑암 · 발파암) · 측구터파기(토사 · 암) 각 단면적 · 입적 · 보정량 · 보정량계 · 성토 · 유용토 · 차인토량 · 누가토량 · 합계 줄
- 옛 시험 test_b08_earthwork_table.py 는 가져오는 경로만 새 엔진으로 · 새 시험 test_61_3_b08_earthwork_kinds.py

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoSr2sqvYtBf1dtQdus88s
2026-10-06 18:26:17 +09:00

215 lines
7.7 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_Earthwork.ts
* 3탭 토적표(PLAN 61-3) — 줄 = 측점 · 열 = 옛 토적 엔진 출력 칸 그대로 · 맨 아래 합계 줄.
*
* GET …/quantity/earthwork (서버 단독 · 읽기만) — 평균단면적법 · 보정량 = 입적 × 다짐 환산계수
* 절토 암은 그 측점이 센 갈래(`cut_rock_kind`)대로 리핑암 · 발파암 열에 놓음.
* 표 모양은 1탭 집계표 것(`b08-summary__*`)을 다시 씀.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { stationLabel } from "../common_util/common_util_station";
import { stationInterval } from "./B08_Quantity_UI_Summary";
interface EarthworkRow {
chainage_m: number;
distance_m: number;
cut_soil_area_m2: number;
cut_soil_volume_m3: number;
cut_soil_adjusted_m3: number;
cut_rock_area_m2: number;
cut_rock_volume_m3: number;
cut_rock_adjusted_m3: number;
cut_rock_kind: string;
ditch_soil_area_m2: number;
ditch_soil_volume_m3: number;
ditch_soil_adjusted_m3: number;
ditch_rock_area_m2: number;
ditch_rock_volume_m3: number;
ditch_rock_adjusted_m3: number;
adjusted_total_m3: number;
fill_area_m2: number;
fill_volume_m3: number;
diverted_m3: number;
balance_m3: number;
cumulative_m3: number;
notes: string[];
}
interface EarthworkTable {
rows: EarthworkRow[];
totals: Record<string, number>;
conversion_factors: Record<string, { compacted: number }>;
station_count: number;
route_id: number;
confirmed: boolean;
}
const ROCKS = ["ripping_rock", "blasting_rock"] as const;
const GROUPS: Array<[string, number]> = [
["절토 토사", 3],
["절토 리핑암", 3],
["절토 발파암", 3],
["측구터파기 토사", 3],
["측구터파기 암", 3],
];
const TRIPLE = ["단면적", "입적", "보정량"];
const num = (value: number | undefined | null): string =>
value == null || !Number.isFinite(value) ? "" : value.toFixed(2);
function cell(tag: "th" | "td", text: string, span?: { row?: number; col?: number }) {
const node = document.createElement(tag);
node.textContent = text;
if (span?.row) node.rowSpan = span.row;
if (span?.col) node.colSpan = span.col;
return node;
}
function note(text: string): HTMLElement {
const p = document.createElement("p");
p.className = "ui-general-block__empty";
p.textContent = text;
return p;
}
function head(): HTMLTableSectionElement {
const top = document.createElement("tr");
top.append(cell("th", "측점", { row: 2 }), cell("th", "거리", { row: 2 }));
for (const [label, span] of GROUPS) top.append(cell("th", label, { col: span }));
top.append(
cell("th", "보정량계", { row: 2 }),
cell("th", "성토", { col: 2 }),
cell("th", "유용토", { row: 2 }),
cell("th", "차인토량", { row: 2 }),
cell("th", "누가토량", { row: 2 }),
);
top.firstElementChild?.classList.add("b08-summary__station");
const sub = document.createElement("tr");
for (const _ of GROUPS) sub.append(...TRIPLE.map((t) => cell("th", t)));
sub.append(cell("th", "단면적"), cell("th", "입적"));
const thead = document.createElement("thead");
thead.append(top, sub);
return thead;
}
/** 절토 암 세 칸 — 그 측점 갈래 열에만 · 다른 갈래 열은 빈 칸 */
function rockCells(row: EarthworkRow, kind: string): string[] {
return row.cut_rock_kind === kind
? [num(row.cut_rock_area_m2), num(row.cut_rock_volume_m3), num(row.cut_rock_adjusted_m3)]
: ["", "", ""];
}
function bodyRow(row: EarthworkRow, interval: number): HTMLTableRowElement {
const tr = document.createElement("tr");
tr.dataset.b08Earthwork = String(row.chainage_m);
const station = cell(
"th",
stationLabel(row.chainage_m, interval) + (row.notes.length ? " *" : ""),
);
station.className = "b08-summary__station";
if (row.notes.length) station.title = row.notes.join("\n");
const values = [
num(row.distance_m),
num(row.cut_soil_area_m2),
num(row.cut_soil_volume_m3),
num(row.cut_soil_adjusted_m3),
...ROCKS.flatMap((kind) => rockCells(row, kind)),
num(row.ditch_soil_area_m2),
num(row.ditch_soil_volume_m3),
num(row.ditch_soil_adjusted_m3),
num(row.ditch_rock_area_m2),
num(row.ditch_rock_volume_m3),
num(row.ditch_rock_adjusted_m3),
num(row.adjusted_total_m3),
num(row.fill_area_m2),
num(row.fill_volume_m3),
num(row.diverted_m3),
num(row.balance_m3),
num(row.cumulative_m3),
];
tr.append(station, ...values.map((v) => cell("td", v)));
return tr;
}
/** 합계 줄 — 단면적은 합이 뜻이 없어 비움(옛 엔진 `totals` 와 같음) · 누가토량 = 마지막 줄 */
function totalRow(table: EarthworkTable): HTMLTableRowElement {
const t = table.totals;
const last = table.rows[table.rows.length - 1];
const values = [
num(t.distance_m),
"",
num(t.cut_soil_volume_m3),
num(t.cut_soil_adjusted_m3),
...ROCKS.flatMap((kind) => [
"",
num(t[`cut_${kind}_volume_m3`]),
num(t[`cut_${kind}_adjusted_m3`]),
]),
"",
num(t.ditch_soil_volume_m3),
num(t.ditch_soil_adjusted_m3),
"",
num(t.ditch_rock_volume_m3),
num(t.ditch_rock_adjusted_m3),
num(t.adjusted_total_m3),
"",
num(t.fill_volume_m3),
num(t.diverted_m3),
num(t.balance_m3),
num(last?.cumulative_m3),
];
const tr = document.createElement("tr");
tr.className = "b08-earthwork__total";
tr.dataset.b08EarthworkTotal = "";
const label = cell("th", "합계");
label.className = "b08-summary__station";
tr.append(label, ...values.map((v) => cell("td", v)));
return tr;
}
export async function renderEarthwork(host: HTMLElement, projectId: string): Promise<void> {
host.replaceChildren(note("토적표를 만드는 중…"));
let table: EarthworkTable;
let interval = 20;
try {
const [response, gap] = await Promise.all([
fetch(`${API_BASE_URL}/projects/${projectId}/quantity/earthwork`, { credentials: "include" }),
stationInterval(projectId).catch(() => 20),
]);
const body = (await response.json().catch(() => null)) as
(EarthworkTable & { detail?: unknown }) | null;
if (!response.ok || !body) {
const detail = body?.detail;
throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`);
}
table = body;
interval = gap;
} catch (error) {
host.replaceChildren(note(`토적표를 불러오지 못했습니다 — ${String(error)}`));
return;
}
if (!table.rows.length) {
host.replaceChildren(note("측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요."));
return;
}
const c = (kind: string): string => num(table.conversion_factors[kind]?.compacted);
const info = note(
`평균단면적법 · 노선 ${table.route_id}${table.confirmed ? "(확정)" : "(확정 전 최신 노선)"} · ` +
`측점 ${table.station_count} · 다짐 환산계수 토사 ${c("soil")} · 리핑암 ${c("ripping_rock")} · ` +
`발파암 ${c("blasting_rock")} · 단위 ㎡ · ㎥ · * = 사유 있음(마우스를 올림)`,
);
const tbody = document.createElement("tbody");
tbody.append(...table.rows.map((row) => bodyRow(row, interval)));
const tfoot = document.createElement("tfoot");
tfoot.append(totalRow(table));
const el = document.createElement("table");
el.className = "ui-general-block__table b08-summary__table b08-earthwork__table";
el.dataset.b08Earthwork = "table";
el.append(head(), tbody, tfoot);
const wrap = document.createElement("div");
wrap.className = "ui-general-block__table-wrap b08-summary__wrap";
wrap.append(el);
host.replaceChildren(info, wrap);
}