feat(B09): 인계 계약 시험 + ㉦ 큰돌쌓기 포함자재 가드 + 수량 2자리 표시

인계 계약 시험 (같은 병 재발 방지)
- **B08 이 보내는 칸을 내가 실제로 읽는지** 시험으로 마주 걺. 새 칸이 오면 깨짐.
  안 읽는 칸은 까닭을 함께 적게 함(측점·spec_detail 등)
- 값이 자료형까지 실제로 닿는지도 짝으로 봄 — 이름만 맞고 값이 안 실리면 같은 병

㉦ 가드 신설 (품셈 13-6 [주]① 「고임돌 및 채움 콘크리트 등은 품에 포함」)
- 큰돌쌓기 줄이 서 있는데 **그 구조물이 낳은** 고임돌·채움콘크리트가 자재로도 서면 멈춤
- ⚠ 13-6 한정 — 돌쌓기 13-4 의 고임돌은 정상이라 안 걸림. 짝 시험 2건
- 내 일위대가는 그 둘을 안 세우고 있음을 확인(품셈 표에 없음)

수량 표시
- 내역서 수량을 **소수 2자리**로 보임. 계산은 전정밀 그대로
- 「표시는 2자리, 계산은 전정밀 — 표시값끼리 곱하면 끝자리가 다릅니다」를 표 아래 상시 표시
  (실무 서식 자릿수는 기준 문서에 없어 2자리는 잠정)

검증: pytest 195 통과(신규 5), tsc 0건, 화면 실측 — 15,726.93 · 92.45 로 뜸

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-08 04:38:03 +09:00
co-authored by Claude Opus 5
parent 56e0351389
commit fecda89e5f
4 changed files with 78 additions and 2 deletions
+27 -2
View File
@@ -673,6 +673,23 @@ interface BillDto {
};
}
/**
* 수량 표시 — 소수 **2자리**. 계산은 전정밀 그대로다.
*
* ⚠ 표시값끼리 곱하면 금액이 몇 원 어긋난다(90.51 × 5,288.6 ≠ 화면 금액). 그것이
* 정상임을 표 아래 문구로 밝힌다 — 밝히지 않으면 「1원 틀린다」는 지적으로 돌아온다.
* 실무 서식이 수량을 몇 자리로 쓰는지는 기준 문서에 없어(미결) 2자리는 잠정이다.
*/
function formatQuantity(value: string | null): string {
if (value === null || value === "") return "";
const parsed = Number(value);
if (!Number.isFinite(parsed)) return value;
return parsed.toLocaleString("ko-KR", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
async function fetchBill(projectId: string): Promise<BillDto> {
const response = await fetch(
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/bill`,
@@ -806,7 +823,7 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
indent + row.name,
row.spec,
row.unit,
row.quantity ?? "",
formatQuantity(row.quantity),
row.unit_price_krw ?? "",
row.amount_krw ?? "",
row.note,
@@ -827,6 +844,12 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
total.textContent = `${L("B09_Estimation_Boq_Total")}: ${bill.summary.body_total_krw}`;
body.append(total);
// 표시 자릿수와 계산 자릿수가 다르다는 것을 숨기지 않는다.
const precision = document.createElement("div");
precision.className = "b09-hint";
precision.textContent = L("B09_Estimation_Boq_Precision");
body.append(precision);
// ⚠ 자재비가 빠진 채 선 합계임을 숨기지 않는다.
const shortfall = document.createElement("div");
shortfall.className = "b09-hint";
@@ -838,7 +861,9 @@ export async function renderB09Estimation(root: HTMLElement): Promise<void> {
note.className = "b09-hint";
note.textContent =
`${L("B09_Estimation_Boq_Excluded")}: ` +
bill.excluded.map((row) => `${row.name} ${row.quantity ?? ""}${row.unit}`).join(", ");
bill.excluded
.map((row) => `${row.name} ${formatQuantity(row.quantity)}${row.unit}`)
.join(", ");
body.append(note);
}