Files
Aislo/B08_Quantity/B08_Quantity_UI_StructureSheet_UnitPrice.ts
T
eomsangdonandClaude Opus 5 3c47120f2a feat(b08): 구조물도 하단 일위대가 표 틀 — 양식 줄 조합 × B09 단가표 미리보기
- 양식에 unit_price.rows(수량은 원단위 줄 from_row · 코드는 work_item_code+variant_from 또는 ref_code)
- 서버가 B09 단가표(PriceBook.resolve, 읽기만)로 줄마다 재료·노무·경비 · 금액란 0.1원 버림 · 계금 1원 버림
- 못 푼 줄은 막힘 + 까닭(0 아님), 원단위 줄이 안 선 장은 안 섬 · 단위 다름·하위 구조물(B-AX-ST)은 막힘
- 창구 /structure-sheets/unit-price 를 장 조회와 따로 둠(단가표 첫 조립 14초)
- 찰쌓기: 돌쌓기 B-FP-13-04-05#갈래 · 기초잡석 B-FP-12-25 · 모르터는 공종 코드 미정
- 시험 4개 추가 · 전체 1539 통과

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

146 lines
4.9 KiB
TypeScript

/* =============================================================================
* B08_Quantity_UI_StructureSheet_UnitPrice.ts
* 구조물도 장 아래 **일위대가 표**(미리보기) — PLAN 3장 하단 ①②.
*
* ⚠ 값을 셈하지 않음 — 서버(`…/structure-sheets/unit-price`)가 B09 단가표로 낸 금액을 적기만.
* ⚠ 장 조회와 따로 받음 — 단가표 첫 조립이 십여 초라 표가 늦게 차도 위 수량표는 먼저 보임.
* ⚠ 막힌 줄은 0 이 아니라 까닭을 적고, 하나라도 있으면 합계 앞에 「미완」.
* ========================================================================== */
import { API_BASE_URL } from "@config/config_frontend";
import { el, num } from "./B08_Quantity_UI_StructureSheet_Formula";
interface UnitPriceRow {
seq: number;
name: string;
spec: string;
ref_code: string;
unit: string;
quantity: number | null;
skipped: boolean;
reason: string;
material?: number;
labor?: number;
expense?: number;
total?: number;
}
interface UnitPriceTable {
code: string;
name: string;
unit: string;
rows: UnitPriceRow[];
material: number;
labor: number;
expense: number;
total: number;
blocked: number;
complete: boolean;
}
/** 금액 칸 — 0.1원 자리까지(금액란 규칙). 못 푼 줄은 빈칸. */
function won(value: number | undefined): string {
return value === undefined ? "" : num(value, 1);
}
/** 장 아래 일위대가 칸 — 받는 동안 안내를 두고, 오면 표로 갈음. */
export function unitPriceSection(projectId: string, sheetKey: string): HTMLElement {
const wrap = el("div", "b08-grid");
wrap.append(
el("p", "b08-grid__caption", "일위대가(미리보기) 불러오는 중… 단가표 첫 조립은 십여 초"),
);
void (async () => {
try {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/quantity/structure-sheets/unit-price?sheet_key=${encodeURIComponent(sheetKey)}`,
{ credentials: "include" },
);
const payload = (await response.json().catch(() => ({}))) as {
unit_price?: UnitPriceTable | null;
message?: string;
};
if (!response.ok) throw new Error(payload.message ?? `HTTP ${response.status}`);
const table = payload.unit_price;
if (!table) {
wrap.replaceChildren(el("p", "b08-grid__caption", "이 양식에는 일위대가 줄이 아직 없음"));
return;
}
wrap.replaceChildren(...render(table));
} catch (error) {
wrap.replaceChildren(
el(
"p",
"b08-grid__caption b08-grid__caption--warn",
`일위대가를 불러오지 못함 — ${error instanceof Error ? error.message : ""}`,
),
);
}
})();
return wrap;
}
function render(table: UnitPriceTable): HTMLElement[] {
const total = table.complete
? `${num(table.total, 0)}원`
: `미완 — 막힌 줄 ${table.blocked} · 선 줄만 ${num(table.total, 0)}원`;
const head = el(
"p",
"b08-sheet__head",
`일위대가 ${table.code} · ${table.unit}${total} (미리보기 — 내역 금액은 원가계산이 셈)`,
);
const scroller = el("div", "b08-grid__scroll");
const grid = el("table", "b08-grid__table b08-grid__table--summary");
const headRow = document.createElement("tr");
for (const label of [
"공종·자원",
"코드",
"수량",
"단위",
"재료비",
"노무비",
"경비",
"합계",
"비고",
]) {
headRow.append(el("th", "", label));
}
const thead = document.createElement("thead");
thead.append(headRow);
const tbody = document.createElement("tbody");
for (const row of table.rows) {
const tr = document.createElement("tr");
const note = row.skipped ? `안 섬 — ${row.reason}` : row.reason ? `⚠ ${row.reason}` : "";
// 단가표 이름에 갈래가 이미 들었거나 규격 칸이 코드 자체면 겹쳐 적지 않음.
const spec =
row.spec && !row.name.includes(row.spec) && !row.ref_code.includes(row.spec) ? row.spec : "";
tr.append(
el("td", "", spec ? `${row.name} (${spec})` : row.name),
el("td", "", row.ref_code),
el("td", "", row.quantity === null ? "" : num(row.quantity, 3)),
el("td", "", row.unit),
el("td", "", won(row.material)),
el("td", "", won(row.labor)),
el("td", "", won(row.expense)),
el("td", "", won(row.total)),
el("td", "", note),
);
tbody.append(tr);
}
const foot = document.createElement("tr");
foot.append(
el("td", "", "계"),
el("td", "", ""),
el("td", "", ""),
el("td", "", ""),
el("td", "", won(table.material)),
el("td", "", won(table.labor)),
el("td", "", won(table.expense)),
el("td", "", table.complete ? num(table.total, 0) : `미완 ${num(table.total, 0)}`),
el("td", "", ""),
);
tbody.append(foot);
grid.append(thead, tbody);
scroller.append(grid);
return [head, scroller];
}