feat(B09): 각종 중기경비계산서 — 기종마다 한 장

별표2 (5)(가) 아홉째. 중기목록표가 「얼마」라면 이 장은 「왜 그 값인가」임.
원가계산 → 중기 탭의 목록표 아래에 붙음.

- 기종마다 취득가·내용시간·손료계수(상각·정비·관리) → 시간당 손료 · 주연료와 유가 ·
  조종원 일당과 시간당 환산 · 시간당 사용료 3분할을 차례로 보임.
- 내역에 실제로 선 기종만 냄(카탈로그 613 을 다 뿌리지 않음).
- 부착 장비(브레이커·콤팩터·집게)는 손료만 드는 것이 정상이라 결함으로 안 셈
  — 제 엔진이 없어 연료·조종원이 본체에 듦(건설품셈 제8장 [주]⑤).
- 수송비는 이 장에 안 붙임 — 「회당」으로 서는 별개 공종이라 자리만 가리킴.
- 계산서 합계가 일위대가가 실제로 쓰는 값과 같은지 시험으로 못 박음(두 벌 방지).

⇒ 「설계서 구성」의 중기경비계산서가 반쪽 → 있음으로 바뀜(법이 정한 13 중 8 이 섬).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-09 23:45:54 +09:00
co-authored by Claude Opus 5
parent 6cf7de9e82
commit 23f9656e91
7 changed files with 406 additions and 7 deletions
@@ -240,6 +240,112 @@ export function drawDesignDocTab(body: HTMLElement, data: DesignDocDto): void {
for (const line of data.notes) body.append(note(line));
}
/* =============================================================================
* 각종 중기경비계산서 — 기종마다 한 장(별표2 (5)(가) 아홉째).
* ⚠ 목록표가 「얼마」라면 이 장은 **왜 그 값인가**다. 계산 과정을 감추지 않는다.
* ========================================================================== */
export interface MachineExpenseDto {
status: string;
summary: string;
notes: string[];
sheets: Array<{
machine_code: string;
name: string;
spec: string;
price_thousand_krw: string | null;
economic_life_hours: number | null;
annual_standard_hours: number | null;
depreciation_coefficient: number | null;
maintenance_coefficient: number | null;
management_coefficient: number | null;
loss_coefficient: number | null;
loss_krw_per_hour: string | null;
fuel_liters_per_hour: string | null;
fuel_price_per_liter: string | null;
fuel_scope: string;
misc_material_percent: string | null;
operator_code: string;
operator_daily_wage: string | null;
operator_krw_per_hour: string | null;
material_krw: string | null;
labor_krw: string | null;
expense_krw: string | null;
total_krw: string | null;
attachment: boolean;
attachment_note: string;
gaps: string[];
}>;
}
export async function fetchMachineExpense(projectId: string): Promise<MachineExpenseDto> {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/machine-expense`,
{ credentials: "include" },
);
if (!response.ok) throw new Error(`machine-expense ${response.status}`);
return (await response.json()) as MachineExpenseDto;
}
/** 기종 한 장 — 손료·운전경비·시간당 사용료를 차례로. */
function machineExpenseSheet(sheet: MachineExpenseDto["sheets"][number]): HTMLElement {
const box = document.createElement("div");
box.className = "b09-panel__group";
const title = document.createElement("p");
title.className = "b09-panel__legend";
title.textContent = `${sheet.machine_code} ${sheet.name} ${sheet.spec}`.trim();
box.append(title);
const coefficient = (value: number | null) => (value === null ? "—" : String(value));
box.append(
table(
["구 분", "내 용", "값"],
[
["① 손료", "취득가격(천원)", money(sheet.price_thousand_krw)],
[
"",
"내용시간 / 연간표준가동시간",
`${coefficient(sheet.economic_life_hours)} / ${coefficient(sheet.annual_standard_hours)}`,
],
[
"",
"상각비·정비비·관리비 계수 (10⁻⁷)",
`${coefficient(sheet.depreciation_coefficient)} + ${coefficient(sheet.maintenance_coefficient)} + ${coefficient(sheet.management_coefficient)} = ${coefficient(sheet.loss_coefficient)}`,
],
["", "시간당 손료(원)", money(sheet.loss_krw_per_hour)],
[
"② 운전경비",
`주연료(L/hr) × 유가(${sheet.fuel_scope})`,
`${sheet.fuel_liters_per_hour ?? "—"} × ${money(sheet.fuel_price_per_liter)}`,
],
["", "잡재료(주연료의 %)", sheet.misc_material_percent ?? "—"],
[
"",
`조종원(${sheet.operator_code || "—"}) 일당 → 시간당`,
`${money(sheet.operator_daily_wage)} → ${money(sheet.operator_krw_per_hour)}`,
],
[
"③ 시간당 사용료",
"재료비 / 노무비 / 경비",
`${money(sheet.material_krw)} / ${money(sheet.labor_krw)} / ${money(sheet.expense_krw)}`,
],
["", "합 계", money(sheet.total_krw)],
],
[0, 1],
),
);
if (sheet.attachment_note) box.append(note(sheet.attachment_note));
for (const gap of sheet.gaps) box.append(note(`⚠ ${gap}`));
return box;
}
export function drawMachineExpense(body: HTMLElement, data: MachineExpenseDto): void {
body.append(head(`각종 중기경비계산서 (${data.sheets.length})`));
body.append(note(data.summary));
for (const line of data.notes) body.append(note(line));
for (const sheet of data.sheets) body.append(machineExpenseSheet(sheet));
}
/** 두 탭이 함께 쓰는 「아직 못 불러왔습니다」 문구. */
export function drawBaseDataError(body: HTMLElement): void {
body.append(note(L("B09_Estimation_Tab_Pending")));