「표가 났는데 안 보이면 낸 것이 아님」. 꺼져 있던 탭 둘을 켜고 표를 붙임. - 새 파일 B09_Estimation_UI_BaseData.ts — 화면 조립부가 이미 1,200줄을 넘어 분리. 상태를 안 들고 그리기만 함. - 기초자료 탭: 노무비(118)·재료비(1)·경비(613) 목록표. 중기 탭: 중기목록표(5). - ⚠ 빈 표를 그냥 두지 않음 — 재료비목록표가 한 줄뿐인 사유를 표에 띄움. 「다 채운 것」으로 읽히면 안 됨. - ⚠ 계산 과정을 감추지 않음(8-13) — 조종원 환산이 실무·교본과 다르다는 사실과 그 영향(기계 든 공종 +19~24%)을 중기 탭에 적음. 잡재료가 연료에 접혀 있다는 것도. - 경비목록표는 취득가(천원), 시간당 사용료는 중기 탭이라는 안내를 달았음. 실측(공용 브라우저): 기초자료 표 3장 732줄 · 중기 5줄, 사유 문구 넷 다 뜸. ⚠ 백엔드 재시작이 있어야 새 조회가 붙음(재시작 전 404 → 후 200). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
195 lines
6.8 KiB
TypeScript
195 lines
6.8 KiB
TypeScript
/* =============================================================================
|
||
* B09_Estimation_UI_BaseData.ts
|
||
* 기초자료 탭 · 중기 탭 — 목록표 넷을 그린다 (사용자 확정 12번 「내야 할 표 16개 전체」).
|
||
*
|
||
* 기초자료 탭 : 노무비목록표 · 재료비목록표 · 경비목록표
|
||
* 중기 탭 : 중기목록표 (합계 + 노무·재료·경비 3분할)
|
||
*
|
||
* 서식은 지어내지 않았다 — 실무 내역서(영월 기번6 · 봉화 기번41)의 같은 이름 시트를
|
||
* 그대로 옮겼다. 칸 이름·차례가 그 시트와 같다.
|
||
*
|
||
* 화면 조립부(`B09_Estimation_UI_Page.ts`)가 이미 700줄을 크게 넘어 여기로 뺐다.
|
||
* 표를 그리는 일만 하고 **상태를 들지 않는다** — 부르는 쪽이 자료를 넘긴다.
|
||
* ========================================================================== */
|
||
|
||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||
import { API_BASE_URL } from "@config/config_frontend";
|
||
|
||
function L(key: keyof typeof ui_locales): string {
|
||
return ui_locales[key][currentLanguageIndex];
|
||
}
|
||
|
||
/** 목록표 한 줄 — 실무 시트 칸 그대로. */
|
||
export interface BaseDataRow {
|
||
code: string;
|
||
name: string;
|
||
spec: string;
|
||
unit: string;
|
||
unit_price_krw: string | null;
|
||
note: string;
|
||
}
|
||
|
||
/** 중기목록표 한 줄 — 합계와 3분할을 함께 보인다. */
|
||
export interface MachineRow {
|
||
code: string;
|
||
name: string;
|
||
spec: string;
|
||
unit: string;
|
||
total_krw: string | null;
|
||
labor_krw: string | null;
|
||
material_krw: string | null;
|
||
expense_krw: string | null;
|
||
note: string;
|
||
}
|
||
|
||
export interface BaseDataDto {
|
||
status: string;
|
||
labor: BaseDataRow[];
|
||
material: BaseDataRow[];
|
||
expense: BaseDataRow[];
|
||
machine: MachineRow[];
|
||
}
|
||
|
||
export async function fetchBaseData(projectId: string): Promise<BaseDataDto> {
|
||
const response = await fetch(
|
||
`${API_BASE_URL}/projects/${encodeURIComponent(projectId)}/estimation/base-data`,
|
||
{ credentials: "include" },
|
||
);
|
||
if (!response.ok) throw new Error(`base-data ${response.status}`);
|
||
return (await response.json()) as BaseDataDto;
|
||
}
|
||
|
||
function money(value: string | null): string {
|
||
if (value === null || value === "") return "";
|
||
const parsed = Number(value);
|
||
return Number.isFinite(parsed) ? parsed.toLocaleString("ko-KR") : value;
|
||
}
|
||
|
||
function head(text: string): HTMLElement {
|
||
const el = document.createElement("div");
|
||
el.className = "b09-hint";
|
||
el.style.fontWeight = "600";
|
||
el.textContent = text;
|
||
return el;
|
||
}
|
||
|
||
function note(text: string): HTMLElement {
|
||
const el = document.createElement("div");
|
||
el.className = "b09-hint";
|
||
el.textContent = text;
|
||
return el;
|
||
}
|
||
|
||
function table(headers: string[], rows: string[][], leftCols: number[]): HTMLElement {
|
||
const el = document.createElement("table");
|
||
el.className = "b09-sheet";
|
||
const thead = document.createElement("thead");
|
||
const headRow = document.createElement("tr");
|
||
headers.forEach((text, index) => {
|
||
const th = document.createElement("th");
|
||
th.textContent = text;
|
||
if (leftCols.includes(index)) th.className = "b09-left";
|
||
headRow.append(th);
|
||
});
|
||
thead.append(headRow);
|
||
const tbody = document.createElement("tbody");
|
||
for (const cells of rows) {
|
||
const tr = document.createElement("tr");
|
||
cells.forEach((text, index) => {
|
||
const td = document.createElement("td");
|
||
td.textContent = text;
|
||
if (leftCols.includes(index)) td.className = "b09-left";
|
||
tr.append(td);
|
||
});
|
||
tbody.append(tr);
|
||
}
|
||
el.append(thead, tbody);
|
||
return el;
|
||
}
|
||
|
||
/** 목록표 한 장 — 코드·명칭·규격·단위·단가·비고 (실무 시트와 같은 칸). */
|
||
function catalogTable(rows: BaseDataRow[]): HTMLElement {
|
||
return table(
|
||
["코드번호", "명 칭", "규 격", "단위", "단 가", "비 고"],
|
||
rows.map((row) => [
|
||
row.code,
|
||
row.name,
|
||
row.spec,
|
||
row.unit,
|
||
money(row.unit_price_krw),
|
||
row.note,
|
||
]),
|
||
[0, 1, 2, 5],
|
||
);
|
||
}
|
||
|
||
/**
|
||
* 기초자료 탭 — 목록표 셋.
|
||
*
|
||
* ⚠ 표가 비거나 한 줄뿐일 때 **그냥 두지 않는다** — 「다 채운 것」으로 읽히기 때문이다.
|
||
* 재료비목록표가 지금 그 자리다(사급 자재 카탈로그가 아직 안 섰다).
|
||
*/
|
||
export function drawBaseDataTab(body: HTMLElement, data: BaseDataDto): void {
|
||
const groups: Array<[string, BaseDataRow[], string]> = [
|
||
["노무비목록표", data.labor, ""],
|
||
[
|
||
"재료비목록표",
|
||
data.material,
|
||
data.material.length <= 1
|
||
? "⚠ 사급 자재 카탈로그가 아직 서지 않아 줄이 거의 없습니다 — 자재값 출처(업체 견적·물가지)를 붙이면 채워집니다."
|
||
: "",
|
||
],
|
||
["경비목록표", data.expense, "기계 취득가격입니다(천원) — 시간당 사용료는 「중기」 탭입니다."],
|
||
];
|
||
for (const [title, rows, hint] of groups) {
|
||
body.append(head(`${title} (${rows.length})`));
|
||
if (hint) body.append(note(hint));
|
||
if (rows.length === 0) {
|
||
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
|
||
continue;
|
||
}
|
||
body.append(catalogTable(rows));
|
||
}
|
||
}
|
||
|
||
/** 중기 탭 — 중기목록표. 합계와 3분할을 함께 보인다(실무 시트와 같은 칸). */
|
||
export function drawMachineTab(body: HTMLElement, data: BaseDataDto): void {
|
||
body.append(head(`중기목록표 (${data.machine.length})`));
|
||
if (data.machine.length === 0) {
|
||
body.append(note("아직 선 줄이 없습니다 — 값을 0 으로 채우지 않습니다."));
|
||
return;
|
||
}
|
||
body.append(
|
||
table(
|
||
["코드번호", "명 칭", "규 격", "단위", "합 계", "노 무 비", "재 료 비", "경 비", "비 고"],
|
||
data.machine.map((row) => [
|
||
row.code,
|
||
row.name,
|
||
row.spec,
|
||
row.unit,
|
||
money(row.total_krw),
|
||
money(row.labor_krw),
|
||
money(row.material_krw),
|
||
money(row.expense_krw),
|
||
row.note,
|
||
]),
|
||
[0, 1, 2, 8],
|
||
),
|
||
);
|
||
// ⚠ 계산 과정을 감추지 않는다(PLAN 8-13). 조종원 환산이 실무와 다른 것을 여기서 밝힌다.
|
||
body.append(
|
||
note(
|
||
"조종원 노임은 「노임 ÷ 8시간」으로 셉니다. 실무·임도교본은 여기에 " +
|
||
"「× 16/12 × 25/20」(약 1.67배)을 더 곱합니다 — 그 계수의 규정 원문(기재부 " +
|
||
"예정가격 작성기준)을 아직 확인하지 못해 적용하지 않았습니다. 확정되면 " +
|
||
"기계가 든 공종 단가가 약 19~24% 오릅니다.",
|
||
),
|
||
);
|
||
body.append(note("잡재료(주연료의 %)는 연료 소요량에 포함되어 있습니다 — 따로 세지 않습니다."));
|
||
}
|
||
|
||
/** 두 탭이 함께 쓰는 「아직 못 불러왔습니다」 문구. */
|
||
export function drawBaseDataError(body: HTMLElement): void {
|
||
body.append(note(L("B09_Estimation_Tab_Pending")));
|
||
}
|