feat(B09): 산출기초 — 줄에 달린 근거를 한 장으로 접음

별표2 (5)(가) 열셋째. 근거는 줄마다 이미 있었고 묶는 자리만 없었음.
원가계산에 「산출기초」 탭으로 세움. 넷으로 접음 —
① 어느 판으로 계산했나(데이터 기준일·지문) ② 무엇을 골랐나(고른 값만)
③ 공종마다 무엇을 근거로 했나(줄 문구 그대로) ④ 못 채운 자리(0 으로 안 때운 자리).

- 판 목록은 장부(_manifest.json)를 그대로 읽음 — 목록을 따로 적으면 한쪽만 고쳐짐.
- ⚠ 여기서 값을 다시 계산하지 않음 — 금액 칸이 아예 없음(두 벌 방지, 시험으로 못 박음).

⇒ 「설계서 구성」의 산출기초가 반쪽 → 있음. 법이 정한 13 중 9 가 서고,
  우리가 더 낼 것은 공사설명서 하나(서식·설계하중 표기는 사용자에게 받아야 함).

곁들여: 자재총괄의 「미분류」를 「미정(발주기관 결정)」으로 고침 — 발주기관이 정할
자리인데 우리가 못 만든 것처럼 읽히던 문구(V-14). 값·판정은 그대로.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-10 00:06:22 +09:00
co-authored by Claude Opus 5
parent 3934bd8d07
commit 89effeca07
11 changed files with 419 additions and 15 deletions
@@ -357,6 +357,91 @@ export function drawMachineExpense(body: HTMLElement, data: MachineExpenseDto):
for (const sheet of data.sheets) body.append(machineExpenseSheet(sheet));
}
/* =============================================================================
* 산출기초 — 줄에 달린 근거를 한 장으로 모은 장(별표2 (5)(가) 열셋째).
* ⚠ 여기서 값을 다시 계산하지 않는다 — 모으기만 한다.
* ========================================================================== */
export interface BasisSheetDto {
status: string;
note: string;
summary: string;
dataset_versions: Array<{
dataset_id: string;
file: string;
effective_date: string;
sha256: string;
}>;
chosen_conditions: Array<{ item: string; value: string }>;
work_items: Array<{ code: string; name: string; unit: string; notes: string[] }>;
gaps: Array<{ kind: string; code: string; reason: string }>;
}
export async function fetchBasisSheet(projectId: string): Promise<BasisSheetDto> {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/basis-sheet`,
{ credentials: "include" },
);
if (!response.ok) throw new Error(`basis-sheet ${response.status}`);
return (await response.json()) as BasisSheetDto;
}
export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
body.append(head("산출기초"));
body.append(note(data.summary));
body.append(note(data.note));
body.append(head(`① 어느 판으로 계산했나 (${data.dataset_versions.length})`));
body.append(
table(
["자료", "파일", "기준일", "지문(앞 12)"],
data.dataset_versions.map((row) => [
row.dataset_id,
row.file,
row.effective_date,
row.sha256 || "—",
]),
[0, 1, 2, 3],
),
);
body.append(head(`② 무엇을 골랐나 (${data.chosen_conditions.length})`));
if (data.chosen_conditions.length === 0) {
body.append(note("고른 값이 없습니다 — 전부 확정 기본값으로 돌고 있습니다."));
} else {
body.append(
table(
["항 목", "고른 값"],
data.chosen_conditions.map((row) => [row.item, row.value]),
[0, 1],
),
);
}
body.append(head(`③ 공종마다 무엇을 근거로 했나 (${data.work_items.length})`));
body.append(
table(
["코드", "공 종", "단위", "근 거"],
data.work_items.map((row) => [row.code, row.name, row.unit, row.notes.join(" · ")]),
[0, 1, 2, 3],
),
);
body.append(head(`④ 못 채운 자리 (${data.gaps.length})`));
if (data.gaps.length === 0) {
body.append(note("못 채운 자리가 없습니다."));
} else {
body.append(
table(
["갈 래", "코드", "사 유"],
data.gaps.map((row) => [row.kind, row.code, row.reason]),
[0, 1, 2],
),
);
body.append(note("⚠ 여기 있는 것은 0 으로 때우지 않고 남겨 둔 자리입니다."));
}
}
/** 두 탭이 함께 쓰는 「아직 못 불러왔습니다」 문구. */
export function drawBaseDataError(body: HTMLElement): void {
body.append(note(L("B09_Estimation_Tab_Pending")));