Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts
T
eomsangdonandClaude Opus 5.5 56e2c0b515 feat(규격 표 화면): 격자 규격 열 · M02 규격 표 편집 · 고르기 창 열 단추 (PLAN 704-4)
- 공용 격자 — 첫 호표 specs 면 수량 칸을 열마다(머리 「H 열」 · 기본 표시) · 합계 줄 그 칸 = 열마다 1 단위 합계
- M02 일위대가 카드 · 상세 — 격자 위 규격 줄(이름 · 열 이름 · 기본 · 열 지움 · [+ 열]) · 격자 수량 칸 열마다 고침(짝 셈 바로)
- 새 파일 M02_MasterTemplete_Labor_Spec.ts — 상세(700 줄)는 한 줄만 바꿈
- 고르기 창 — 규격 열 둘 이상이면 열 단추(열마다 합계) · [적용] = spec 붙은 참조 · 이름 뒤 「(H=열)」 · 열 하나 = 단추 없음
- 시험 704-3 격자 시험 두 줄 고침(laborGrid 넷째 인자 · Labor_Spec 이어 붙임) · 704-4 화면 자료 시험 둘

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

190 lines
7.3 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.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.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 });
}