Files
Aislo/ui_template/ui_template_hopyo_grid.ts
T
eomsangdonandClaude Opus 5.5 0377334f71 feat(일위대가 격자): 엑셀 꼴 공용 격자 부품 — M02 일위대가 카드 · 상세 · B09 일위대가표 같은 부품 (PLAN 704-3)
- 새 공용 부품 ui_template_hopyo_grid(.ts · .css) — 호표 머리 줄(이름 · 규격 · 단위 · 1 단위 합계) · 구성 줄(품명 · 규격 · 수량 · 단위 · 합계 · 노무비 · 재료비 · 경비 단가 / 금액 · 비고 · 구분 꼬리표) · 합계 줄 빈 줄 붉게 · 부품 모음 견본 칸
- M02 Labor_Grid — 문서 + 서버 calc 답 그대로(단가 = 기준 ÷ 배 · 금액 = 반영 · 합계 = 반영합 · 수량 없음 줄 = 빈 줄) · Labor_Detail 맨 위 격자(카드 · 상세 같이) · 읽기만 카드 = 격자만
- B09 일위대가표(호표보기 · 박스보기 바탕 줄) = 같은 부품 · 줄 값 · 이동 · 줄 꼴 그대로 + 구분 칸 · 머리 1 단위 합계
- 시험: test_704_3 새로 · B09 표 시험 다섯(155 · 157-4 · 201-1 · 202-1 · 304)은 격자 파일을 이어 붙이게 고침(줄 글이 공용 부품으로 옮김) · hopyo_screen 상세 줄 수 693 → 700

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

224 lines
7.5 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.
/* =============================================================================
* ui_template_hopyo_grid.ts
* 일위대가 엑셀 꼴 격자(PLAN 704-3) — 호표마다 머리 줄 + 구성 줄 + 합계 줄 · 공용 데이터표(`ui_template_data_table`) 위.
* M02 일위대가 카드 · 상세 · B09 일위대가표(호표보기)가 같은 열 · 같은 줄 꼴을 씀.
*
* 머리 줄 = 「제 N 호표 이름」(번호 없으면 이름만) · 규격 · 단위 · 1 단위 합계(합계 금액 칸) · 비고 = 호표 꼴
* 품셈 인용 줄(있으면) = 「ㄴ 조항」 · 비고 「품셈 인용」
* 구성 줄 = 「ㄴ 품명」 · 규격 · 품/수량 · 단위 · 합계 · 노무비 · 재료비 · 경비(단가 / 금액 쌍) · 비고 · 구분(줄 꼬리표)
* 합계 줄 = 비목 금액 합 · 빈 줄(금액 모름 · 수량 없음) 있으면 비고 「빈 줄 n」 붉게 — 합계는 아는 줄만
* 줄 꼬리표 = 「품셈」(우리 로직이 품을 셈) · 「고정」 갈래는 705 몫(자리만 · 같은 칸)
* 값은 부르는 쪽이 준 것 그대로(서버 계산 값) — 여기서 셈하지 않음
*
* const g = createHopyoGrid([block]); host.append(g.root); g.set([block2]);
* B09 처럼 탭 표가 따로 그리면 `hopyoGrid(blocks)` 의 열 · 줄 · 제목 줄만 씀
* ========================================================================== */
import {
createDataTable,
type DataColumn,
type DataRow,
type DataTableOptions,
} from "./ui_template_data_table";
import "./ui_template_hopyo_grid.css";
export interface HopyoMoney {
sum?: number | null;
labor?: number | null;
material?: number | null;
expense?: number | null;
}
export interface HopyoLine {
name: string;
spec?: string | null;
/** 규격 원문(풍선 · 부르는 쪽 몫) */
spec_raw?: unknown;
/** 품 · 수량(1 단위 당) — null = 수량 없음 */
qty: number | null;
unit?: string | null;
/** 1 단위(위 단위) 당 단가 */
prices: HopyoMoney;
/** 단가 × 품/수량 — sum null = 금액 모름(빈 줄) */
amounts: HopyoMoney;
note?: string | null;
/** 줄 꼬리표 — 「품셈」 · 「고정」(705) · 빈 값 = 없음 */
tag?: string | null;
}
export interface HopyoBlock {
/** 줄 키 앞머리(머리 = key · 줄 = key|n · 합계 = key|합계) */
key: string;
no?: string | number | null;
name: string;
spec?: string | null;
spec_raw?: unknown;
unit?: string | null;
/** 호표 꼴(머리 줄 비고) */
kind?: string | null;
/** 품셈 인용(조항) */
cite?: string | null;
total: HopyoMoney;
lines: HopyoLine[];
/** 빈 줄 수 · 이름 — 없으면 금액 모르는 줄에서 셈 */
blank?: number;
blank_names?: string[];
}
/** 줄 꼬리표 낱말 */
export const HOPYO_TAG = { pum: "품셈", fix: "고정" } as const;
/** 하위 줄 앞 — 공용 표가 앞 빈칸을 접지 않음(들여쓰기 = level) */
export const HOPYO_CHILD = "ㄴ ";
const HOPYO_COSTS = [
["sum", "합계"],
["labor", "노무비"],
["material", "재료비"],
["expense", "경비"],
] as const;
const hopyoText = (key: string, label: string, width?: number): DataColumn => ({
key,
label,
width,
});
const hopyoMoney = (key: string, label: string, group: string): DataColumn => ({
key,
label,
group: [group],
kind: "number",
decimals: 1,
width: 96,
});
/** 열 — 명칭 · 규격 · 수량 · 단위 · 비목 넷(단가 / 금액) · 비고 · 구분 */
export const HOPYO_COLUMNS: DataColumn[] = [
hopyoText("name", "명칭", 240),
hopyoText("spec", "규격", 160),
{ key: "qty", label: "수량", kind: "number", decimals: 4, trim: true, width: 76 },
hopyoText("unit", "단위", 56),
...HOPYO_COSTS.flatMap(([key, label]) => [
hopyoMoney(`${key}_price`, "단가", label),
hopyoMoney(key, "금액", label),
]),
hopyoText("note", "비고", 120),
hopyoText("tag", "구분", 56),
];
export interface HopyoGrid {
columns: DataColumn[];
rows: DataRow[];
/** 머리 · 합계 줄 번호(굵게 · 레벨 1) */
titles: Set<number>;
/** 줄 번호 → [호표 차례, 구성 줄 차례] · 구성 줄만 */
at: Map<number, [number, number]>;
}
const hopyoBlank = (b: HopyoBlock): { n: number; names: string[] } => {
if (b.blank !== undefined) return { n: b.blank, names: b.blank_names ?? [] };
const names = b.lines.filter((l) => l.amounts.sum == null).map((l) => l.name);
return { n: names.length, names };
};
/** 호표 → 표 줄(값 그대로) */
export function hopyoGrid(blocks: HopyoBlock[]): HopyoGrid {
const rows: DataRow[] = [];
const titles = new Set<number>();
const at = new Map<number, [number, number]>();
blocks.forEach((b, bi) => {
titles.add(rows.length);
rows.push({
key: b.key,
name: b.no != null && b.no !== "" ? `제 ${b.no} 호표 ${b.name}` : b.name,
no: b.no,
title: b.name,
spec: b.spec ?? "",
spec_raw: b.spec_raw,
unit: b.unit ?? "",
sum: b.total.sum ?? null, // 1 단위 합계
note: b.kind ?? "",
});
if (b.cite)
rows.push({ key: `${b.key}|cite`, name: `${HOPYO_CHILD}${b.cite}`, note: "품셈 인용" });
b.lines.forEach((ln, n) => {
at.set(rows.length, [bi, n]);
const row: DataRow = {
key: `${b.key}|${n}`,
name: `${HOPYO_CHILD}${ln.name}`,
spec: ln.spec ?? "",
spec_raw: ln.spec_raw,
qty: ln.qty,
unit: ln.unit ?? "",
note: ln.note ?? "",
tag: ln.tag ?? "",
};
for (const [key] of HOPYO_COSTS) {
row[`${key}_price`] = ln.prices[key] ?? null;
row[key] = ln.amounts[key] ?? null;
}
rows.push(row);
});
const blank = hopyoBlank(b);
titles.add(rows.length);
rows.push({
key: `${b.key}|합계`,
name: "합 계",
sum: b.total.sum ?? null,
labor: b.total.labor ?? null,
material: b.total.material ?? null,
expense: b.total.expense ?? null,
note: blank.n ? `빈 줄 ${blank.n}` : "",
blank: blank.n,
blank_names: blank.names,
});
});
return { columns: HOPYO_COLUMNS, rows, titles, at };
}
/** 칸 꼴 — 꼬리표 · 빈 줄 알림 */
export function hopyoCellClass(rows: DataRow[], i: number, key: string): string | null {
const r = rows[i];
if (key === "tag" && r?.tag)
return `ui-hopyo-grid__tag${r.tag === HOPYO_TAG.fix ? " is-fix" : " is-pum"}`;
if (key === "note" && Number(r?.blank) > 0) return "ui-hopyo-grid__blank";
return null;
}
/** 공용 표 옵션(격자 꼴 그대로) — `extra` 로 누름 · 풍선 자리를 더함 */
export function hopyoTableOptions(
blocks: HopyoBlock[],
extra: Partial<DataTableOptions> = {},
): DataTableOptions {
const g = hopyoGrid(blocks);
return {
columns: g.columns,
rows: g.rows,
frozen: 1,
empty: "",
rowClass: (i) => (g.titles.has(i) ? "is-title" : null),
cellClass: (i, key) => hopyoCellClass(g.rows, i, key),
level: { key: "name", of: (i) => (g.titles.has(i) ? 1 : 2) },
...extra,
};
}
export interface HopyoGridHandle {
root: HTMLElement;
set: (blocks: HopyoBlock[]) => void;
}
/** 격자 하나(읽기 전용) — 빈 호표 목록 = 빈 표 */
export function createHopyoGrid(
blocks: HopyoBlock[],
extra: Partial<DataTableOptions> = {},
): HopyoGridHandle {
const root = document.createElement("div");
root.className = "ui-hopyo-grid";
const set = (next: HopyoBlock[]): void => {
root.replaceChildren(createDataTable(hopyoTableOptions(next, extra)).root);
};
set(blocks);
return { root, set };
}