Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts
T
eomsangdonandClaude Opus 5.5 90273507c5 fix(규격 표 화면): 열 합계 한 자리 · 규격 줄 칸 폭 · B08 · B09 금액 = 열 수량 × 단가 시험 (PLAN 704-4)
- 격자 합계 줄 규격 칸 = 금액 칸처럼 소수 한 자리
- 규격 이름 · 열 이름 칸 6em — 좁은 카드에서 한 줄
- 임시 storage 시험 — 사본 셈 반영합 = 층 문서 그 열 셈(B08) · 대가 줄 금액 = 열 수량 × 단가(B09)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:41:13 +09:00

192 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* M02_MasterTemplete_Labor_Spec.ts
* 일위대가 규격 표(PLAN 704-4) — 704-3 격자에 규격 열(H=1.5 | 2.0 …)을 폄 · 고칠 때 격자 위 규격 줄.
* 문서 `규격` = {이름, 열, 기본, 수량: {줄 id: {열: 수}}}(짝 `M02_Labor_Spec.py`) · 화면 캐시만(저장은 [저장])
* 규격 줄 = 규격 이름 · 열마다 이름 칸 · [기본] · [×] · [+ 열](새 열 수량 = 지금 기본 열 값 그대로)
* 격자 수량 칸(열마다) 고침 = 그 줄 · 그 열 수량(빈 글 = 공통 수량을 따름)
* 열마다 수량 · 1 단위 합계 = 짝 셈 `settle(…, 열)`(서버 안 부름) · 금액 칸 = 기본 열
* 쓰는 사본이 있는 열 지움 · 이름 바꿈은 [저장] 때 서버가 막음(409 까닭 그대로)
* ========================================================================== */
import { createButton, createInputField, el } from "@ui/ui_template_elements";
import type { DataTableOptions } from "@ui/ui_template_data_table";
import { hopyoGrid, hopyoSpecKey, type HopyoBlock } from "@ui/ui_template_hopyo_grid";
import { createToolbar } from "@ui/ui_template_toolbar";
import { createText } from "@ui/ui_template_text";
import type { LaborCalc, LaborDoc } from "./M02_MasterTemplete_Labor_Api";
import { givenOf, lineId, settle, specColumn } from "./M02_MasterTemplete_Labor_Calc";
/** 고칠 때 — 규격이 바뀌면 `true`(수량만 · 기준 그대로 · 짝 셈으로 바로) */
export type SpecEdit = (qtyOnly: boolean) => void;
const TEXT = {
none: "규격 열 없음 — [+ 열] = H 같은 규격마다 줄 수량만 다른 열",
add: "+ 열",
name: "규격 이름",
base: "기본",
remove: "열 지움",
first: "1",
next: "새 열",
};
/** 줄 id 차례 — 격자 줄 차례(로직 줄 → 자재 줄)와 같음 */
const lineIds = (doc: LaborDoc): string[] => [
...doc.담은로직.map((r) => lineId(r) ?? ""),
...doc.자재.map((m) => m.코드),
];
/** 열마다 셈 → 블록 `specs` · 줄 `qtys`(규격 없음 = 그대로) */
export function withSpecs(block: HopyoBlock, doc: LaborDoc, calc: LaborCalc | null): HopyoBlock {
const spec = doc.규격;
if (!spec?.열.length) return block;
const per = calc ? spec.열.map((c) => settle(calc, doc, givenOf(calc), c)) : [];
const ids = lineIds(doc);
const logics = doc.담은로직.length;
const qtyOf = (p: LaborCalc, n: number): number | null =>
(n < logics
? p.줄.find((l) => lineId(l) === ids[n])?.수량
: p.자재.find((m) => m.코드 === ids[n])?.수량) ?? null;
return {
...block,
specs: {
name: spec.이름,
cols: spec.열,
pick: specColumn(doc),
totals: per.map((p) => p.반영합?.계 ?? null),
},
lines: block.lines.map((ln, n) => ({ ...ln, qtys: per.map((p) => qtyOf(p, n)) })),
};
}
/** 격자 수량 칸 고침(열마다) — 공용 표 옵션 덧 */
export function specCells(
block: HopyoBlock,
doc: LaborDoc,
edit: SpecEdit,
): Partial<DataTableOptions> {
const spec = doc.규격;
if (!spec?.열.length) return {};
const g = hopyoGrid([block]);
const ids = lineIds(doc);
const col = (key: string): string | undefined => spec.열.find((_, n) => hopyoSpecKey(n) === key);
return {
editable: (i, key) => g.at.has(i) && col(key) !== undefined,
onEdit: (i, key, text) => {
const id = ids[g.at.get(i)?.[1] ?? -1];
const name = col(key);
if (id === undefined || name === undefined) return;
const v = Number(text.replace(/,/g, "").trim());
const row = { ...(spec.수량[id] ?? {}) };
if (text.trim() === "" || !Number.isFinite(v)) delete row[name];
else row[name] = v;
if (Object.keys(row).length) spec.수량[id] = row;
else delete spec.수량[id];
edit(true);
},
};
}
/** 새 열 이름 — 겹치면 뒤에 번호 */
const fresh = (cols: string[], want: string): string => {
let out = want;
for (let n = 2; cols.includes(out); n++) out = `${want} ${n}`;
return out;
};
/** [+ 열] — 규격이 없으면 지금 수량 열(1) + 새 열 · 새 열 수량 = 기본 열 손 값 그대로 */
function addColumn(doc: LaborDoc, calc: LaborCalc | null): void {
const spec = (doc.규격 ??= { 이름: "H", 열: [TEXT.first], 기본: TEXT.first, 수량: {} });
const col = fresh(spec.열, TEXT.next);
const base = specColumn(doc) ?? spec.열[0];
spec.열.push(col);
const ids = lineIds(doc);
const logics = doc.담은로직.length;
ids.forEach((id, n) => {
const own = spec.수량[id]?.[base];
const line =
n < logics ? calc?.줄.find((l) => lineId(l) === id) : calc?.자재.find((m) => m.코드 === id);
const v = own ?? (line?.수량출처 === "손" ? line.수량 : null);
if (v != null) spec.수량[id] = { ...(spec.수량[id] ?? {}), [col]: v };
});
}
function renameColumn(doc: LaborDoc, from: string, to: string): boolean {
const spec = doc.규격;
const name = to.trim();
if (!spec || !name || name.length > 20 || name === from || spec.열.includes(name)) return false;
spec.열 = spec.열.map((c) => (c === from ? name : c));
if (spec.기본 === from) spec.기본 = name;
for (const row of Object.values(spec.수량))
if (from in row) {
row[name] = row[from];
delete row[from];
}
return true;
}
function removeColumn(doc: LaborDoc, col: string): void {
const spec = doc.규격;
if (!spec) return;
spec.열 = spec.열.filter((c) => c !== col);
for (const [id, row] of Object.entries(spec.수량)) {
delete row[col];
if (!Object.keys(row).length) delete spec.수량[id];
}
if (!spec.열.length)
doc.규격 = null; // 마지막 열 = 규격 없음(옛 문서 꼴)
else if (spec.기본 === col) spec.기본 = spec.열[0];
}
/** 격자 위 규격 줄(고칠 때만) */
export function specBar(doc: LaborDoc, calc: LaborCalc | null, edit: SpecEdit): HTMLElement {
const add = createButton({
label: TEXT.add,
variant: "ghost",
onClick: () => {
addColumn(doc, calc);
edit(true);
},
});
const spec = doc.규격;
if (!spec?.열.length)
return createToolbar({ items: [createText(TEXT.none, "muted"), add], label: TEXT.name });
const name = createInputField({ value: spec.이름, compact: true, placeholder: TEXT.name });
name.input.title = TEXT.name;
name.root.style.width = "6em"; // 짧은 글 칸 — 열 이름 칸도 같은 폭(카드 한 줄에)
name.input.addEventListener("change", () => {
const v = name.input.value.trim();
if (v && v.length <= 20) spec.이름 = v;
edit(true);
});
const cols = spec.열.flatMap((col) => {
const box = createInputField({ value: col, compact: true });
box.input.setAttribute("data-spec-col", col);
box.root.style.width = "6em";
box.input.addEventListener("change", () => {
renameColumn(doc, col, box.input.value);
edit(true);
});
const base = createButton({
label: TEXT.base,
variant: col === spec.기본 ? "filled" : "ghost",
size: "sm",
onClick: () => {
spec.기본 = col;
edit(true);
},
});
const drop = createButton({
label: "×",
variant: "ghost",
size: "sm",
onClick: () => {
removeColumn(doc, col);
edit(true);
},
});
drop.title = TEXT.remove;
return [el("span", { children: [box.root] }), base, drop, "divider" as const];
});
return createToolbar({ items: [name.root, "divider", ...cols, add], label: TEXT.name });
}