Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts
T
eomsangdonandClaude Opus 5.5 8d50aa0582 feat(B09): 설계내역서 입력 칸 테두리 되돌림 · 우클릭 「항목 추가」 하나 · 연결 이름 셋 · 「줄 삭제」 · 하위 메뉴 옆 판 (246-1 · 246-2)
- 공용 표 입력 칸 = 241 앞 모양(accent 5% 채움 · td is-can-edit 표시 지움 · 더블클릭 입력 그대로)
- 우클릭 더함 넷 → 「항목 추가」 — 항목 = 바로 아래 같은 부모 · 폴더 = 첫 하위 · 1단 = ← 내어쓰기(직접비 · 관급자재대 앞 · 자재 줄 막음)
- 연결 이름 = 구조물 조합 · 일위대가 사전조합 · 단가산출 · 들여오기 알림 글 쉬운 말 · 「줄 지우기」 → 「줄 삭제」 · 안내 글
- 공용 우클릭 메뉴 children = 「글 ▸」 한 줄 + 오른쪽 옆 판(올림 · 누름 · → 열기 · ← 닫기 · 화면 끝 = 왼쪽)
- 시험 기대값 고침(241_1 · 243_1 · 166_2 · 182 · 184_3 · 204_2) · 새 시험 246_1 · 246_2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
2026-10-10 21:28:09 +09:00

125 lines
6.0 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`
* 「연결 ▾」 칸 = 항목 줄(자재 · 조달수수료 빼고) · 누름 = 우클릭 [연결] 과 같은 메뉴
* 접히지 않는 나무(235-1) = 폴더(묶음 · 자식 있는 줄) = 이름만 · 수량 · 단가 빈칸 · 금액 = 자손 합 수식(읽기 전용) ·
* 파일(항목) = 이름 · 규격 · 단위 · 수량 + 연결 · 안내선 · 아이콘 = 공용 표 level.tree · 고칠 칸 = 더블클릭 입력
* ========================================================================== */
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"];
/** 폴더 줄 수식 칸 — 자손 합(셈) */
const SUM_KEYS = ["sum", "labor", "material", "expense"];
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} — 계는 아는 금액만 · 원가계산서는 빈 줄이 없을 때만 받음`
: ""),
editOnDouble: true,
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 && r.kind !== "item" && SUM_KEYS.includes(key)) return "수식 = 아래 항목 합(읽기 전용)";
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;
// 240 폴더 줄 = 처음 · 레벨 고쳐 생긴 것 모두 같은 줄 꼴(글자 크기 = 레벨 · 공용 is-lvN)
const title = r.kind === "item" ? "" : "is-title";
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",
tree: true,
},
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;
},
};
};