feat(B09): 칸 단위 근거 호버·등급색 배선 + 줄 사유 덮어쓰기 버그 수정

① 버그 — BillRow.note 한 칸에 덮어쓰던 13곳·이어 붙이던 7곳을 조각 배열 notes 로
   옮김. 한 줄에 사유가 둘이면 하나가 조용히 사라지던 것이 멈춤. 조각마다 닿는 열
   키를 함께 실어 호버가 그 칸에만 띄우게 함. 화면 비고 칸은 종전과 같은 꼴 유지.
② 사전 — B09_Estimation_Provenance.py 신설(기존 B09 파일이 700줄 초과). 8장 57열.
   이미 있던 formula_text·source_label·base_label 은 그대로 쓰고 없는 것만 새로 적음.
③ 배선 — 라우터 5개 응답에 _with_provenance(), 표 여섯에 markProvenanceCell +
   attachProvenance. 개발환경이 아니면 응답에 칸 자체가 안 생김(로직 보안).
④ 등급색 토글을 탭 줄 끝에. 사전이 안 오면 단추도 안 세움.

⚠ 원가계산서 금액은 같은 열에서 중간줄과 마지막줄의 성격이 갈리는데 등급이 열에
   하나뿐이라 calc 로 두고 rule 에 어느 줄이 final 인지 적음(PLAN 8-36 ① 에 남김).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp
This commit is contained in:
2026-09-12 15:50:55 +09:00
co-authored by Claude Opus 5
parent 3e93360285
commit d1a2508053
6 changed files with 766 additions and 73 deletions
+41 -3
View File
@@ -13,6 +13,12 @@
* ========================================================================== */
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
import {
attachProvenance,
markProvenanceCell,
type ProvenancePayload,
type ProvenanceSheet,
} from "@ui/ui_template_provenance";
import { API_BASE_URL } from "@config/config_frontend";
function L(key: keyof typeof ui_locales): string {
@@ -48,6 +54,8 @@ export interface BaseDataDto {
material: BaseDataRow[];
expense: BaseDataRow[];
machine: MachineRow[];
/** 근거 사전 — **개발환경에서만** 온다. 없으면 호버·등급색이 통째로 안 붙는다. */
provenance?: ProvenancePayload;
}
export async function fetchBaseData(projectId: string): Promise<BaseDataDto> {
@@ -80,7 +88,19 @@ function note(text: string): HTMLElement {
return el;
}
function table(headers: string[], rows: string[][], leftCols: number[]): HTMLElement {
/**
* 표 한 장.
*
* `keys`·`sheet` 를 함께 주면 칸마다 근거 호버가 붙는다 — **사전이 없으면 아무 일도
* 안 한다**(빈 카드를 띄우면 「설명이 있다」는 거짓만 남는다). 안 주는 표는 종전 그대로다.
*/
function table(
headers: string[],
rows: string[][],
leftCols: number[],
keys?: string[],
sheet?: ProvenanceSheet,
): HTMLElement {
const el = document.createElement("table");
el.className = "b09-sheet";
const thead = document.createElement("thead");
@@ -99,16 +119,20 @@ function table(headers: string[], rows: string[][], leftCols: number[]): HTMLEle
const td = document.createElement("td");
td.textContent = text;
if (leftCols.includes(index)) td.className = "b09-left";
const key = keys?.[index];
const column = key ? sheet?.columns[key] : undefined;
if (key && column) markProvenanceCell(td, key, column.tier);
tr.append(td);
});
tbody.append(tr);
}
el.append(thead, tbody);
attachProvenance(el, sheet);
return el;
}
/** 목록표 한 장 — 코드·명칭·규격·단위·단가·비고 (실무 시트와 같은 칸). */
function catalogTable(rows: BaseDataRow[]): HTMLElement {
function catalogTable(rows: BaseDataRow[], sheet?: ProvenanceSheet): HTMLElement {
return table(
["코드번호", "명 칭", "규 격", "단위", "단 가", "비 고"],
rows.map((row) => [
@@ -120,6 +144,8 @@ function catalogTable(rows: BaseDataRow[]): HTMLElement {
row.note,
]),
[0, 1, 2, 5],
["code", "name", "spec", "unit", "unit_price_krw", "note"],
sheet,
);
}
@@ -148,7 +174,7 @@ export function drawBaseDataTab(body: HTMLElement, data: BaseDataDto): void {
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
continue;
}
body.append(catalogTable(rows));
body.append(catalogTable(rows, data.provenance?.sheets?.catalog));
}
}
@@ -174,6 +200,18 @@ export function drawMachineTab(body: HTMLElement, data: BaseDataDto): void {
row.note,
]),
[0, 1, 2, 8],
[
"code",
"name",
"spec",
"unit",
"total_krw",
"labor_krw",
"material_krw",
"expense_krw",
"note",
],
data.provenance?.sheets?.machine,
),
);
// ⚠ 계산 과정을 감추지 않는다(PLAN 8-13). 조종원 환산이 실무와 다른 것을 여기서 밝힌다.