- 틀 B09_Estimation_UI_Shell: 탭 줄과 등록만 · 탭마다 파일 하나(계약 _Shell_Types) - 설계내역서: 실무 열(합계/노무/재료/경비 단가·금액) · 머리글 접기·레벨 고르개 · 줄 누르면 제 N 호표 · 단산 N 단추 · 미확정 빨간 테두리 - 일위대가·단가산출근거: 목록표(내역에 처음 쓰인 차례) + 본표 · 줄 누르면 하위 호표·산근·중기로 · 자취 눌러 되돌아감 · Q 식 글자 그대로 - 옛 탭 여섯(원가계산서·중기·관급사급·기초자료·설계서 구성·산출기초)은 옛 코드 그대로 이어 붙임 — 새 탭 파일이 서면 등록 한 줄씩 바꿈 - 본표 합계 줄 = 호표 성분 소계 원 미만 절사 값 · 비율 줄 금액도 0.1원 절사 표시 - 사전 ui_template_locale_b3 새 벌(b2 700줄 넘음) - 검증: ORCA 검증 프로젝트 — 본체 122,848,989 · 지장목제거 865·15,460,837 · 제 9 호표 합계 1,708 = 산근 8호표 합계 1,708 · 옛 탭 여섯 다 뜸 · 접기 53→51·레벨1 11줄 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
176 lines
7.8 KiB
TypeScript
176 lines
7.8 KiB
TypeScript
/* =============================================================================
|
|
* B09_Estimation_UI_Sheet.ts
|
|
* B09 실무 서식 표 공용 — 내역서·일위대가표·단가산출근거가 같은 모양으로 섬 (PLAN 12장)
|
|
*
|
|
* - 표 모양은 STmate 출력 시트 그대로: 명칭·규격·수량·단위 · 합계/노무비/재료비/경비 (단가·금액) · 비고.
|
|
* - ⚠ 여기서 곱하거나 더하지 않음 — 서버가 실은 칸을 찍기만. 숫자 꼴(천 단위 쉼표)만 바꿈.
|
|
* - 가로 넘침은 표 칸 안에서만(`min-width:0` · `overflow-x:auto`) — 옛 화면 2026-09-08 실측 교훈.
|
|
* ========================================================================== */
|
|
|
|
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
|
|
|
export function L(key: keyof typeof ui_locales): string {
|
|
return ui_locales[key][currentLanguageIndex];
|
|
}
|
|
|
|
/** 금액 글 → 「1,234,567」 · 소수부가 있으면 한 자리(일위대가 금액란 0.1원). 숫자가 아니면 그대로. */
|
|
export function won(value: string | null | undefined): string {
|
|
if (value === null || value === undefined || value === "") return "";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return String(value);
|
|
const fraction = Math.abs(number % 1) > 1e-9;
|
|
return number.toLocaleString("ko-KR", {
|
|
minimumFractionDigits: fraction ? 1 : 0,
|
|
maximumFractionDigits: fraction ? 1 : 0,
|
|
});
|
|
}
|
|
|
|
/** 수량 — 서버가 준 표시 자리(`digits`). 내역 줄인데 자리를 모르면 옛 화면처럼 둘째 자리,
|
|
* 호표 안 줄(`digits` 안 줌)은 품이 작아(0.0115 hr) 넷째 자리까지. */
|
|
export function quantity(value: string | null | undefined, digits?: number | null): string {
|
|
if (value === null || value === undefined || value === "") return "";
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return String(value);
|
|
if (digits === undefined) {
|
|
return number.toLocaleString("ko-KR", { maximumFractionDigits: 4 });
|
|
}
|
|
if (digits === null) {
|
|
return number.toLocaleString("ko-KR", { maximumFractionDigits: 2 });
|
|
}
|
|
return number.toLocaleString("ko-KR", {
|
|
minimumFractionDigits: digits,
|
|
maximumFractionDigits: digits,
|
|
});
|
|
}
|
|
|
|
export function el<K extends keyof HTMLElementTagNameMap>(
|
|
tag: K,
|
|
className = "",
|
|
text = "",
|
|
): HTMLElementTagNameMap[K] {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** 실무 시트 머리 두 줄 — 앞 칸(명칭 따위)은 두 줄을 차지, 성분 넷은 「단가·금액」 두 칸. */
|
|
export function sheetHead(
|
|
front: string[],
|
|
back: string[] = [L("B09_Sheet_Col_Note")],
|
|
): HTMLElement {
|
|
const thead = el("thead");
|
|
const top = el("tr");
|
|
const bottom = el("tr");
|
|
for (const label of front) {
|
|
const th = el("th", "", label);
|
|
th.rowSpan = 2;
|
|
top.append(th);
|
|
}
|
|
for (const key of [
|
|
"B09_Sheet_Col_Total",
|
|
"B09_Sheet_Col_Labor",
|
|
"B09_Sheet_Col_Material",
|
|
"B09_Sheet_Col_Expense",
|
|
] as const) {
|
|
const th = el("th", "", L(key));
|
|
th.colSpan = 2;
|
|
top.append(th);
|
|
bottom.append(
|
|
el("th", "", L("B09_Sheet_Col_UnitPrice")),
|
|
el("th", "", L("B09_Sheet_Col_Amount")),
|
|
);
|
|
}
|
|
for (const label of back) {
|
|
const th = el("th", "", label);
|
|
th.rowSpan = 2;
|
|
top.append(th);
|
|
}
|
|
thead.append(top, bottom);
|
|
return thead;
|
|
}
|
|
|
|
/** 숫자 칸 하나. */
|
|
export function numberCell(text: string): HTMLTableCellElement {
|
|
return el("td", "b09s-num", text);
|
|
}
|
|
|
|
/** 성분 넷 × (단가·금액) 여덟 칸 — 합계·노무·재료·경비 차례(실무 시트). */
|
|
export function moneyCells(
|
|
unit: [string, string, string, string] | null,
|
|
amount: [string, string, string, string] | null,
|
|
): HTMLTableCellElement[] {
|
|
const cells: HTMLTableCellElement[] = [];
|
|
for (let i = 0; i < 4; i += 1) {
|
|
cells.push(numberCell(unit ? won(unit[i]) : ""), numberCell(amount ? won(amount[i]) : ""));
|
|
}
|
|
return cells;
|
|
}
|
|
|
|
/** 표 한 장 — 가로로 넘치면 이 칸 안에서만 밀림. */
|
|
export function sheetTable(head: HTMLElement): { wrap: HTMLElement; tbody: HTMLElement } {
|
|
const wrap = el("div", "b09s-wrap");
|
|
const table = el("table", "b09s-table");
|
|
const tbody = el("tbody");
|
|
table.append(head, tbody);
|
|
wrap.append(table);
|
|
return { wrap, tbody };
|
|
}
|
|
|
|
/** 누르면 들어가는 글 — 「제 3 호표」·「단산 2」. */
|
|
export function linkButton(text: string, onClick: () => void): HTMLButtonElement {
|
|
const button = el("button", "b09s-link", text);
|
|
button.type = "button";
|
|
button.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
onClick();
|
|
});
|
|
return button;
|
|
}
|
|
|
|
/** 수동 단가 표시 — 어느 화면에서든 같게(빨간 테두리 + 「미확정 N건」). */
|
|
export function unconfirmedBadge(count: number): HTMLElement {
|
|
return el("span", "b09s-badge", `${L("B09_Sheet_Unconfirmed")} ${count}${L("B09_Sheet_Count")}`);
|
|
}
|
|
|
|
export function hint(text: string, warn = false): HTMLElement {
|
|
return el("div", warn ? "b09s-hint b09s-hint--warn" : "b09s-hint", text);
|
|
}
|
|
|
|
export function injectSheetStyles(): void {
|
|
if (document.getElementById("b09-sheet-styles")) return;
|
|
const style = document.createElement("style");
|
|
style.id = "b09-sheet-styles";
|
|
style.textContent = `
|
|
.b09s-page { display:flex; flex-direction:column; gap:8px; min-width:0; min-height:0; flex:1; }
|
|
.b09s-tabs { display:flex; flex-wrap:wrap; gap:4px; border-bottom:1px solid var(--ui-border, #d0d4dc); padding-bottom:4px; }
|
|
.b09s-tab { border:1px solid var(--ui-border, #d0d4dc); background:var(--ui-surface, #fff); padding:4px 10px; border-radius:6px 6px 0 0; cursor:pointer; font-size:13px; }
|
|
.b09s-tab.is-active { background:var(--ui-accent, #2f6fed); color:#fff; border-color:var(--ui-accent, #2f6fed); }
|
|
.b09s-body { display:flex; flex-direction:column; gap:8px; min-width:0; min-height:0; flex:1; overflow:auto; }
|
|
.b09s-bar { display:flex; flex-wrap:wrap; align-items:center; gap:8px; font-size:13px; }
|
|
.b09s-wrap { overflow-x:auto; max-width:100%; }
|
|
.b09s-table { border-collapse:collapse; font-size:12px; white-space:nowrap; }
|
|
.b09s-table th, .b09s-table td { border:1px solid var(--ui-border, #d0d4dc); padding:2px 6px; }
|
|
.b09s-table th { background:var(--ui-surface-muted, #f1f3f7); font-weight:600; text-align:center; }
|
|
.b09s-num { text-align:right; font-variant-numeric:tabular-nums; }
|
|
.b09s-table tr.is-group td { font-weight:600; background:var(--ui-surface-muted, #f7f8fb); }
|
|
.b09s-table tr.is-sum td { font-weight:700; background:var(--ui-surface-muted, #eef1f6); }
|
|
.b09s-table tr.is-clickable { cursor:pointer; }
|
|
.b09s-table tr.is-clickable:hover td { background:rgba(47,111,237,0.08); }
|
|
.b09s-table tr.is-selected td { background:rgba(47,111,237,0.16); }
|
|
.b09s-table tr.is-manual td { box-shadow:inset 0 0 0 1px #d93025; }
|
|
.b09s-table td.b09s-note { white-space:normal; min-width:160px; max-width:420px; }
|
|
.b09s-toggle { border:none; background:none; cursor:pointer; padding:0 4px 0 0; font-size:11px; }
|
|
.b09s-link { border:none; background:none; color:var(--ui-accent, #2f6fed); cursor:pointer; padding:0 4px 0 0; text-decoration:underline; font-size:12px; }
|
|
.b09s-badge { display:inline-block; border:1px solid #d93025; color:#d93025; border-radius:10px; padding:0 6px; font-size:11px; margin-left:4px; }
|
|
.b09s-hint { font-size:12px; color:var(--ui-text-muted, #5f6673); }
|
|
.b09s-hint--warn { color:#b3261e; }
|
|
.b09s-trail { display:flex; flex-wrap:wrap; gap:4px; align-items:center; font-size:13px; }
|
|
.b09s-split { display:flex; flex-direction:column; gap:12px; min-width:0; }
|
|
.b09s-title { font-weight:700; font-size:14px; }
|
|
.b09s-formula { white-space:pre-wrap; font-size:12px; color:var(--ui-text, #1f2430); }
|
|
.b09s-legacy .b09-tabs { display:none; }
|
|
`;
|
|
document.head.append(style);
|
|
}
|