- 운반 단가 창 몸 두 칸 비율 — 왼쪽(옵션 값) 약 0.8 배 - 단가산출근거 [박스보기] 머리 줄 = 운반 창 ③ 칸과 같은 꼴(이름 · 값 오른쪽 굵게 · 식 작게) · 출처 = 칸 맨 위 - 줄 나눔 · 그리기 한 벌 — Haul_Steps headSteps 내보내 같이 씀 · 공용 묶음 카드 = 선택 몸 위 칸(top) - 호표 줄 · 총계 · 표 보기 · 다른 탭 카드 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
170 lines
6.3 KiB
TypeScript
170 lines
6.3 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 {
|
|
blockLevel,
|
|
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;
|
|
|
|
/** 157-3 규격 풍선 = 원문번호 글(「이름 규격」 으로 바꾼 칸만) · 일위대가표도 씀(157-4) */
|
|
export const raw = (shown: unknown, codes: unknown): string | null =>
|
|
codes && codes !== shown ? `원문번호 ${String(codes)}` : null;
|
|
|
|
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),
|
|
tip: (i, key) => (key === "spec" ? raw(rows[i]?.spec, rows[i]?.spec_raw) : 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,
|
|
note_raw: b.spec_raw,
|
|
name: b.name,
|
|
unit: b.unit,
|
|
source: b.source ?? "", // 278-2 박스보기 머리 줄 나눔(출처 줄)
|
|
},
|
|
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, // 같은 공종 줄 = 세로 병합(201-1)
|
|
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:
|
|
"줄을 누르면 단가를 낸 탭으로 이동 · 자재 적용 단가를 고치면 [저장] 뒤 반영" + notes(sheets),
|
|
link: (i: number) => links[i] ?? null,
|
|
rowClass: (i: number) => (titles.has(i) ? "is-title" : null),
|
|
level: blockLevel("text", (i) => titles.has(i)),
|
|
mergeSame: ["kind"],
|
|
tip: (i: number, key: string) =>
|
|
key === "note" ? raw(rows[i]?.note, rows[i]?.note_raw) : null,
|
|
};
|
|
};
|