Files
Aislo/B09_Estimation/B09_Estimation_UI_Sheet.ts
T
eomsangdonandClaude Opus 5 964375d2a7 refactor(b09): 옛 화면 파일 셋 지움 — UI_Page(1,561줄) · UI_BaseData(1,161줄) · UI_Tab_Legacy
- 옛 탭 여섯은 모두 새 틀 탭 파일로 옮겨짐(설계서 구성 · 산출기초 · 관급·사급 · 중기경비계산서 · 기초자료, 원가계산서는 랩탑_메인)
- 화면 확인(지운 뒤 13 탭 전부): 설계내역서 53줄 · 본체 122,848,989 · 일위대가 18 · 산근 10 · 중기 표 19·줄 171 ·
  관급·사급 13 · 기초자료 표 6·줄 737 · 설계서 구성 13 · 산출기초 표 3·줄 192 — 옮기기 전과 같음 · tsc 통과

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 02:56:27 +09:00

207 lines
9.3 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 };
}
/** 머리 한 줄짜리 표(목록표·집계표) — 실무 시트 칸 이름 그대로 넘김. */
export function plainTable(headers: string[]): { wrap: HTMLElement; tbody: HTMLElement } {
const thead = el("thead");
const tr = el("tr");
for (const label of headers) tr.append(el("th", "", label));
thead.append(tr);
return sheetTable(thead);
}
/** 한 탭 안의 갈래 고르개(재료비·노무비…) — 고른 갈래를 돌려줌. */
export function segmented(
items: Array<[string, string]>,
active: string,
onPick: (key: string) => void,
): HTMLElement {
const bar = el("div", "b09s-bar");
for (const [key, label] of items) {
const button = el("button", `b09s-tab${key === active ? " is-active" : ""}`, label);
button.type = "button";
button.addEventListener("click", () => onPick(key));
bar.append(button);
}
return bar;
}
/** 누르면 들어가는 글 — 「제 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-head { font-weight:600; margin-top:6px; }
.b09s-info td { text-align:right; font-variant-numeric:tabular-nums; }
.b09s-info td.b09s-left, .b09s-info th.b09s-left { text-align:left; white-space:normal; }
.b09s-group { display:flex; flex-direction:column; gap:4px; border-top:1px solid var(--ui-border, #d0d4dc); padding-top:6px; }
.b09s-inline { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.b09s-table td.b09s-adopted { font-weight:700; color:var(--ui-accent, #2f6fed); background:rgba(47,111,237,0.08); }
.b09s-min { display:inline-block; margin-left:4px; font-size:10px; color:#1e8e3e; border:1px solid #1e8e3e; border-radius:8px; padding:0 4px; }
`;
document.head.append(style);
}