Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts
T
eomsangdonandClaude Sonnet 5.5 88dd941d73 fix(B08·B09): 표 아래 안내 글 제작 · 셈 설명 지움 · 늘 한 줄 · 페이지 스크롤 0(236-1)
- B09 표 탭 안내 글 = 쓸모 있는 한 줄(lineHint) · 묶음 탭과 같은 줄임표
- B08 표 아래 안내 · 경고 줄 = footNote 한 줄 · 서버 알림은 한 줄로 이어 붙임
- 새 시험 test_236_1 · 창 높이별 창 끝까지 셈 헬퍼

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jonfdxn1Xit9bdkNJpE4G
2026-10-10 19:09:56 +09:00

117 lines
5.4 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tabs_Bill.ts
* B09 설계내역서 탭(PLAN 145-3 · 184-3) — 실무 엑셀 꼴 칸(공종 · 명칭 · 규격 · 수량 · 단위 · 합계 · 노무 · 재료 · 경비 · 비고).
*
* 값 = `B09_Estimation_Calc_Bill`(조작 중 브라우저 · [저장] 때 서버 정본)
* 초기값 = 기본 로직이 B08 수량으로 짠 줄 · 모든 줄 고침 · 더함 · 지움 · 옮김 자유(184 · `…_UI_Tabs_Bill_Edit`)
* 고칠 칸 = 항목 줄 명칭 · 규격 · 단위 · 수량 · 묶음 줄 이름 · 조달수수료 율(%) — 자동 줄은 덮개
* 고친 칸 = 칸 강조(셈 줄 `auto_{칸}` 있음) · 풍선 = 자동 값
* 줄 누름 = 단가를 낸 탭 줄(자재 → 재료비목록표 · 구조물 → 일위대가목록표)
* 레벨 = 줄 `level`(셈이 냄) → 공용 표 level 옵션(공종 · 명칭 칸 들여쓰기 · 201-1) · 다루기 = `…_Bill_Level`
* 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴
* ========================================================================== */
import type { DataRow } from "@ui/ui_template_data_table";
import type { Row } from "./B09_Estimation_Calc_Base";
import { money, parseNumber, qty, text, type TabBuild, type TabId } from "./B09_Estimation_UI_Tabs";
import {
billMenu,
dropMove,
editCell,
linkable,
openLinkMenu,
type BillTable,
} from "./B09_Estimation_UI_Tabs_Bill_Edit";
const TEXT_KEYS = ["name", "spec", "unit"];
export const buildBill: TabBuild = (ctx): BillTable => {
const { sheets, user, setUser } = ctx;
const bill: Row = sheets.bill || { rows: [], totals: null };
// 손질 · 메뉴 = 셈 줄 그대로(raw) · 표 = 번호 붙인 이름(no = 레벨 1 · 2 묶음 차례)
const raw: DataRow[] = bill.rows;
const rows: DataRow[] = raw.map((r: Row) => ({
...r,
name: r.no ? `${r.no} ${r.name}` : r.name,
connect: linkable(r) ? `${!r.auto && r.note ? r.note : r.ref ? "연결됨" : "연결"} ▾` : "",
}));
const direct: Row = bill.totals?.direct || {};
const pair = (key: string, label: string) => [
money(`price_${key}`, "단가", [label]),
money(key, "금액", [label]),
];
/** 고친 칸 — 셈 줄에 자동 값(auto_칸)이 있음 · 연결 칸 = auto_ref */
const edited = (r: DataRow | undefined, key: string): boolean =>
!!r && `auto_${key === "connect" ? "ref" : key}` in r;
return {
columns: [
text("name", "공종 · 명칭", 220),
text("spec", "규격", 160),
qty("qty", "수량", 3),
text("unit", "단위", 56),
...pair("sum", "합계"),
...pair("labor", "노무비"),
...pair("material", "재료비"),
...pair("expense", "경비"),
text("connect", "연결", 96),
text("note", "비고", 200),
],
rows,
total: { name: "순공사비 계(관급 제외)", ...direct },
frozen: 1,
hint:
"줄을 우클릭하면 추가 · 지움 · 옮김 · 「연결 ▾」로 일위대가 · 단가산출 연결" +
(direct.blank
? ` · 빈 줄 ${direct.blank} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`
: ""),
editable: (i, key) => {
const r = rows[i];
if (!r) return false;
if (r.key === "fee") return key === "qty";
if (r.kind !== "item") return key === "name";
return key === "qty" || TEXT_KEYS.includes(key);
},
onEdit: (i, key, value) => {
const r = rows[i];
if (!r) return;
// 번호 = 셈이 붙임 — 고친 이름 앞 번호는 뗌
const head = `${r.no} `;
if (key === "name" && r.no && value.startsWith(head)) value = value.slice(head.length);
if (key !== "qty") return editCell(ctx, raw, i, key, value.trim());
const v = parseNumber(value);
if (v === undefined) return;
if (r.key !== "fee") return editCell(ctx, raw, i, key, v);
setUser({ ...user, estimate: { ...(user.estimate || {}), procurement_fee_pct: v } });
},
cellClass: (i, key) => (edited(rows[i], key) ? "is-edited" : null),
tip: (i, key) => {
const r = rows[i];
if (!r || !edited(r, key)) return null;
const auto = key === "connect" ? (r.auto_ref as Row | null)?.key : r[`auto_${key}`];
return `자동 값 ${auto ?? "빈칸"} — 우클릭 [자동값으로 되돌리기]`;
},
rowClass: (i) => {
const r = rows[i];
if (!r) return null;
const title = r.kind === "title" ? "is-title" : r.kind === "group" ? "is-group" : "";
return [title, r.auto ? "" : "is-hand"].filter((c) => c).join(" ") || null;
},
// 228-1 레벨 = 셈 나무 깊이(들여 · 내어쓰기 · 끌기 = 다시 셈 · 다시 그림) · 글꼴 = 레벨 · 묶음(빈 공종도)
level: {
key: "name",
of: (i) => rows[i]?.level as number | undefined,
group: (i) => rows[i]?.kind !== "item",
},
menu: (i) => billMenu(ctx, raw, i),
drop: (i, parent, before) => dropMove(ctx, raw, i, parent, before),
onCell: (i, key) => {
if (key === "connect") openLinkMenu(ctx, raw, i);
},
link: (i, key) => {
if (key === "connect") return null; // 「연결 ▾」 = 메뉴(이동 안 함)
const ref = rows[i]?.ref as Row | null | undefined;
return ref?.key ? { tab: ref.tab as TabId, key: String(ref.key) } : null;
},
};
};