Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Sangeun.ts
T
eomsangdonandClaude Opus 5.5 5dd72cfa38 feat(b09): 산근 텍스트 수식 칸 · 일위대가화 글 · % 덧줄 표기(155-5)
- 산근 블록마다 서버가 글 지음(sub4 render_sangeun_text · B09 굳힌 단가) — head · 줄마다 text · foot(계)
- 단가산출근거 탭 = 엑셀 꼴(공종 · 산출내역 · 합계 · 노무비 · 재료비 · 경비 · 비고) · 제목 · 총계 굵게
- 중기 = 노 · 재 · 경 세 줄(서버 글 줄 수와 같게)
- % 덧줄 = 규격 「노무비의 %」 · 수량 = 율 · 단위 %(sub3 pct_extras)
- 산근목록 안내 글 = 단가산출 줄 · 사전조합 안 줄 · 일위대가화 줄의 하위 로직(「단가승격」 글 지움)
- 시험: 145-1 화면 글 대조 새 갈래 · test_155_b09_sources · test_155_b09_tabs 덧붙임

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
2026-10-09 19:26:40 +09:00

150 lines
5.9 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tabs_Sangeun.ts
* B09 단가산출근거 · 단가산출근거목록표 탭(PLAN 145-1) — 값 = 서버 `B09_Estimation_Sangeun`
* (서버 단독 · 이 탭엔 조작 없음 · [저장] · [단가 갱신] 때 서버가 다시 셈).
*
* 목록표 = 산근 호표 한 줄(합계 · 노무비 · 재료비 · 경비 · 1 단위) · 줄 누름 = 그 블록
* 풀이표(155-5 · 엑셀 단가산출근거 꼴) = 공종 · 산출내역(서버가 지은 텍스트 수식 한 칸) · 합계 ·
* 노무비 · 재료비 · 경비 — 머리(제 n 호표 · 출처 · 입력 · 중간 Q) → 줄(중기 = 노 · 재 · 경
* 세 줄) → 총계(계) · 줄 누름 = 단가를 낸 탭 · 줄(노무비목록표 · 중기목록표 · 자재단가대비표 ·
* 하위 산근)
* ========================================================================== */
import type { DataRow } from "@ui/ui_template_data_table";
import type { Row } from "./B09_Estimation_Calc_Base";
import {
money,
qty,
text,
type Link,
type TabBuild,
type TabId,
type TabTable,
} from "./B09_Estimation_UI_Tabs";
const PART_NAME: Record<string, string> = {
labor: "노무비",
material: "재료비",
expense: "경 비",
};
const PARTS = ["labor", "material", "expense"] as const;
const notes = (sheets: Row): string =>
((sheets.sg_notes as string[]) || []).map((n) => ` · ${n}`).join("");
export const buildSgList: TabBuild = ({ sheets }): TabTable => {
const rows: DataRow[] = ((sheets.sg_list as Row[]) || []).map((r) => ({
...r,
note: `쓰는 구조물 ${(r.uses || []).length}`,
}));
return {
columns: [
qty("no", "호표", 0),
text("name", "명칭", 220),
text("spec", "규격(입력값)", 200),
text("unit", "단위", 56),
money("sum", "합계"),
money("labor", "노무비"),
money("material", "재료비"),
money("expense", "경비"),
text("note", "비고", 110),
],
rows,
frozen: 2,
hint:
"산근 = 연결된 구조물 조합 사본의 단가산출 로직 줄 · 사전조합 안 줄 · 일위대가화 줄의 하위 로직(노무 · 자재 · 중기 줄은 각 목록표) · 같은 로직 + 같은 입력값 = 한 호표 · 합계 = 블록 총계 · 줄 누름 = 그 블록" +
notes(sheets),
link: (i) => (rows[i] ? { tab: "sg", key: String(rows[i].key) } : null),
};
};
const LINE_TAB: Record<string, TabId> = {
labor: "labor",
machine: "machine",
compare: "compare",
sg: "sg",
};
/** 공종 칸(엑셀 A 열) — 줄 꼴 이름 · 하위 산근 = 「산근 n호표」 */
const KIND_NAME: Record<string, string> = { labor: "노무", machine: "중기", material: "자재" };
export const buildSg: TabBuild = ({ sheets }): TabTable | string => {
const rows: DataRow[] = [];
const links: Array<Link | null> = [];
const titles = new Set<number>();
const push = (row: DataRow, link: Link | null = null, title = false): void => {
if (title) titles.add(rows.length);
rows.push(row);
links.push(link);
};
const blocks = (sheets.sg as Row[]) || [];
const noOf: Record<string, number> = Object.fromEntries(blocks.map((b) => [b.key, b.no]));
for (const b of blocks) {
const head: string[] = b.head?.length ? b.head : [`${b.name} / ${b.unit}`];
push({ key: b.key, kind: `제 ${b.no} 호표`, text: head[0], note: b.spec }, null, true);
head.slice(1).forEach((line, n) => push({ key: `${b.key}|머리${n}`, text: ` ${line}` }));
(b.lines as Row[]).forEach((ln, n) => {
const link = ln.ref?.key ? { tab: LINE_TAB[ln.ref.tab] ?? "compare", key: ln.ref.key } : null;
const kind =
ln.kind === "sangeun" && noOf[ln.ref?.key]
? `산근 ${noOf[ln.ref.key]}호표`
: (KIND_NAME[ln.kind] ?? "");
// 중기 = 노 · 재 · 경 세 줄(엑셀 꼴 · 서버 글 줄과 같은 수) · 그 밖 = 한 줄
const parts = ln.kind === "machine" && !ln.why ? PARTS : [null];
const texts: string[] = ln.text || [];
parts.forEach((part, k) => {
const at = (p: string): number | null => (part === null || part === p ? ln[p] : 0);
const fallback = `${ln.name}${part ? ` ${PART_NAME[part]}` : ""} · ${ln.qty ?? ""} ${ln.unit ?? ""}`;
push(
{
key: `${b.key}|${n}${part ? `|${part}` : ""}`,
kind: k === 0 ? kind : "",
text: ` ${texts[k] ?? fallback}`,
sum: PARTS.some((p) => at(p) === null)
? null
: PARTS.reduce((s, p) => s + (at(p) as number), 0),
labor: at("labor"),
material: at("material"),
expense: at("expense"),
note: ln.why ?? "",
},
link,
);
});
});
push(
{
key: `${b.key}|총계`,
kind: "총 계",
text: b.foot || "",
sum: b.sum,
labor: b.labor,
material: b.material,
expense: b.expense,
note: b.why ?? "",
},
null,
true,
);
}
if (!rows.length) return ((sheets.sg_notes as string[]) || []).join(" · ") || "산근 없음";
return {
columns: [
text("kind", "공종", 90),
text("text", "산출내역", 520),
money("sum", "합계", undefined, 1),
money("labor", "노무비", undefined, 1),
money("material", "재료비", undefined, 1),
money("expense", "경비", undefined, 1),
text("note", "비고 · 까닭", 180),
],
rows,
frozen: 1,
hint:
"산출내역 = 값이 든 식(B09 굳힌 단가 * 수량 = 금액 · 작업량 Q = ROUND 2) · 줄 ROUNDDOWN 1 · 총계 비목마다 ROUNDDOWN 0 · 자재 적용 단가를 고치면 [저장] 뒤 반영 · 줄 누름 = 단가를 낸 탭" +
notes(sheets),
link: (i: number) => links[i] ?? null,
rowClass: (i: number) => (titles.has(i) ? "is-title" : null),
};
};