feat(B09): 근거 사전 네 장 추가·칸 단위 최종 등급·사전 파일 셋으로 분리
- 자재단가대비표: 적용 칸을 calc+rule(기.가·유.물·견적), 값 없는 원천 칸은 blocked - 환율및기초자료: 공표·공시값 standard 와 사용자가 고른 유가 범위 input 을 가름 - 단가산출근거·산출기초: 모으는 표라 식을 비우고 source·등급만 채움 - 설계서 구성표·산출 조건 패널은 일부러 뺌(프로젝트 값이 아니거나 근거 한 줄과 중복) - 원가계산서 금액은 총원가·도급금액·총계 세 줄에만 칸 단위 final 을 심음 - 700줄을 넘어 Provenance / _Sources / _Common 셋으로 가름(공통 조각을 한쪽에 두면 불러들이기가 고리를 이룸) 사전 16장 92열, 등급 여덟이 모두 쓰임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GrXDD23Dvt2sR7q3X6oekp
This commit is contained in:
@@ -402,6 +402,7 @@ export function drawMachineExpense(body: HTMLElement, data: MachineExpenseDto):
|
||||
|
||||
export interface BasisSheetDto {
|
||||
status: string;
|
||||
provenance?: ProvenancePayload;
|
||||
note: string;
|
||||
summary: string;
|
||||
dataset_versions: Array<{
|
||||
@@ -440,6 +441,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
row.sha256 || "—",
|
||||
]),
|
||||
[0, 1, 2, 3],
|
||||
["dataset_id", "file", "effective_date", "sha256"],
|
||||
data.provenance?.sheets?.basis_versions,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -452,6 +455,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["항 목", "고른 값"],
|
||||
data.chosen_conditions.map((row) => [row.item, row.value]),
|
||||
[0, 1],
|
||||
["item", "value"],
|
||||
data.provenance?.sheets?.basis_chosen,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -462,6 +467,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["코드", "공 종", "단위", "근 거"],
|
||||
data.work_items.map((row) => [row.code, row.name, row.unit, row.notes.join(" · ")]),
|
||||
[0, 1, 2, 3],
|
||||
["code", "name", "unit", "notes"],
|
||||
data.provenance?.sheets?.basis_items,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -474,6 +481,8 @@ export function drawBasisSheet(body: HTMLElement, data: BasisSheetDto): void {
|
||||
["갈 래", "코드", "사 유"],
|
||||
data.gaps.map((row) => [row.kind, row.code, row.reason]),
|
||||
[0, 1, 2],
|
||||
["kind", "code", "reason"],
|
||||
data.provenance?.sheets?.basis_gaps,
|
||||
),
|
||||
);
|
||||
body.append(note("⚠ 여기 있는 것은 0 으로 때우지 않고 남겨 둔 자리입니다."));
|
||||
@@ -522,6 +531,7 @@ export interface FuelScope {
|
||||
|
||||
export interface PriceSourcesDto {
|
||||
status: string;
|
||||
provenance?: ProvenancePayload;
|
||||
material_comparison: {
|
||||
slot_names: string[];
|
||||
rows: MaterialComparisonRow[];
|
||||
@@ -568,7 +578,11 @@ export async function fetchPriceSources(projectId: string): Promise<PriceSources
|
||||
* 자재단가대비표 — 원천마다 **단가·페이지** 두 칸이라 머리글이 두 줄이다.
|
||||
* 채택한 원천 칸에 표시를 넣어 「어느 것을 썼나」가 한눈에 보이게 한다.
|
||||
*/
|
||||
function comparisonTable(slotNames: string[], rows: MaterialComparisonRow[]): HTMLElement {
|
||||
function comparisonTable(
|
||||
slotNames: string[],
|
||||
rows: MaterialComparisonRow[],
|
||||
sheet?: ProvenanceSheet,
|
||||
): HTMLElement {
|
||||
const el = document.createElement("table");
|
||||
el.className = "b09-sheet";
|
||||
|
||||
@@ -609,35 +623,45 @@ function comparisonTable(slotNames: string[], rows: MaterialComparisonRow[]): HT
|
||||
const tbody = document.createElement("tbody");
|
||||
for (const row of rows) {
|
||||
const tr = document.createElement("tr");
|
||||
const put = (text: string, left = false): void => {
|
||||
/** 칸 하나 — `key` 를 주면 근거 호버가 붙는다(사전에 없는 열은 아무 일도 안 한다). */
|
||||
const put2 = (td: HTMLElement, key: string, tier?: string): void => {
|
||||
const column = sheet?.columns[key];
|
||||
if (column) markProvenanceCell(td, key, tier ?? column.tier);
|
||||
};
|
||||
const put = (text: string, left = false, key?: string): void => {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = text;
|
||||
if (left) td.className = "b09-left";
|
||||
if (key) put2(td, key);
|
||||
tr.append(td);
|
||||
};
|
||||
put(row.code, true);
|
||||
put(row.name, true);
|
||||
put(row.spec, true);
|
||||
put(row.unit);
|
||||
put(row.code, true, "code");
|
||||
put(row.name, true, "name");
|
||||
put(row.spec, true, "spec");
|
||||
put(row.unit, false, "unit");
|
||||
for (const slot of row.slots) {
|
||||
const td = document.createElement("td");
|
||||
td.textContent = money(slot.price_krw);
|
||||
// 채택한 원천을 굵게 — 「어느 값을 썼나」를 표가 스스로 밝힌다.
|
||||
if (slot.adopted) td.style.fontWeight = "700";
|
||||
// ⚠ **빈 칸은 「0원」이 아니라 「그 판에 그 품목이 없다」** — 막힌 자리로 표시한다.
|
||||
put2(td, "slot_price", slot.price_krw === null ? "blocked" : undefined);
|
||||
tr.append(td);
|
||||
const page = document.createElement("td");
|
||||
page.textContent = slot.source_note;
|
||||
page.className = "b09-left";
|
||||
put2(page, "slot_page");
|
||||
tr.append(page);
|
||||
}
|
||||
if (!appliedIsLastSlot) {
|
||||
put(money(row.adopted_price_krw));
|
||||
put(money(row.adopted_price_krw), false, "adopted_price_krw");
|
||||
put(row.adopted_slot ? (row.slots[row.adopted_slot - 1]?.name ?? "") : "", true);
|
||||
}
|
||||
put(row.note, true);
|
||||
put(row.note, true, "note");
|
||||
tbody.append(tr);
|
||||
}
|
||||
el.append(thead, tbody);
|
||||
attachProvenance(el, sheet);
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -647,6 +671,8 @@ function baseReferenceSections(
|
||||
data: PriceSourcesDto["base_reference"],
|
||||
projectId: string,
|
||||
reload: () => void,
|
||||
laborSheet?: ProvenanceSheet,
|
||||
fuelSheet?: ProvenanceSheet,
|
||||
): void {
|
||||
body.append(head("환율및기초자료 — ① 환율"));
|
||||
body.append(note(data.exchange.note));
|
||||
@@ -666,6 +692,8 @@ function baseReferenceSections(
|
||||
row.formula,
|
||||
]),
|
||||
[0, 1, 4],
|
||||
["code", "name", "day_wage_krw", "hourly_krw", "formula"],
|
||||
laborSheet,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -693,6 +721,8 @@ function baseReferenceSections(
|
||||
],
|
||||
],
|
||||
[0, 2, 3, 4],
|
||||
["item", "price_krw", "scope", "effective_date", "dataset_id"],
|
||||
fuelSheet,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -765,11 +795,24 @@ export function drawPriceSourcesSections(
|
||||
if (comparison.rows.length === 0) {
|
||||
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
|
||||
} else {
|
||||
body.append(comparisonTable(comparison.slot_names, comparison.rows));
|
||||
body.append(
|
||||
comparisonTable(
|
||||
comparison.slot_names,
|
||||
comparison.rows,
|
||||
data.provenance?.sheets?.material_comparison,
|
||||
),
|
||||
);
|
||||
}
|
||||
for (const text of comparison.notes) body.append(note(text));
|
||||
|
||||
baseReferenceSections(body, data.base_reference, projectId, reload);
|
||||
baseReferenceSections(
|
||||
body,
|
||||
data.base_reference,
|
||||
projectId,
|
||||
reload,
|
||||
data.provenance?.sheets?.base_reference_labor,
|
||||
data.provenance?.sheets?.base_reference_fuel,
|
||||
);
|
||||
}
|
||||
|
||||
/** 두 표를 아직 못 받아왔을 때 — 화면을 비우지 않는다. */
|
||||
|
||||
Reference in New Issue
Block a user