refactor(b09): 탭 표 짜기 꺼냄 — B09_Estimation_UI_Builders.ts(BUILDERS · tabTableOf · tableOptionsOf) · Shell 이 그대로 씀 · 동작 그대로 (303-2 앞 조각)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-13 07:48:02 +09:00
co-authored by Claude Opus 5.5
parent d7ba07832c
commit bc92038071
7 changed files with 104 additions and 67 deletions
@@ -0,0 +1,85 @@
/* =============================================================================
* B09_Estimation_UI_Builders.ts
* B09 탭 표 짜기(PLAN 303-2) — 탭 → 표(`TabTable`) · 표 → 공용 표 옵션(DOM · 누름 없이).
*
* 화면(Shell) · 팝업 창(303-2) · 인쇄(302-4)가 같은 표 짜기를 씀 — 보는 값 = 찍는 값
* 초안 탭 = `DRAFT.builders`(draft 폴더가 넣음 · 없으면 빈 채로)
* ========================================================================== */
import type { DataTableOptions } from "@ui/ui_template_data_table";
import { buildDaegaBlocks, buildDaegaList } from "./B09_Estimation_UI_Tabs_Labor";
import {
DRAFT,
TABS,
type TabBuild,
type TabContext,
type TabId,
type TabTable,
} from "./B09_Estimation_UI_Tabs";
import {
buildBasis,
buildCompare,
buildExpense,
buildLabor,
buildMachine,
buildMachineBlocks,
buildMaterial,
} from "./B09_Estimation_UI_Tabs_Base";
import { buildBill } from "./B09_Estimation_UI_Tabs_Bill";
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
import {
buildSumExpense,
buildSumLabor,
buildSumMachine,
buildSumMaterial,
} from "./B09_Estimation_UI_Tabs_Summary";
import { buildCost, buildTotal } from "./B09_Estimation_UI_Tabs_Total";
import { buildSg, buildSgList } from "./B09_Estimation_UI_Tabs_Sangeun";
const BUILDERS: Partial<Record<TabId, TabBuild>> = {
dg_list: buildDaegaList,
dg: buildDaegaBlocks,
bill: buildBill,
basis: buildBasis,
machine: buildMachine,
machine_blocks: buildMachineBlocks,
material: buildMaterial,
labor: buildLabor,
expense: buildExpense,
compare: buildCompare,
cost: buildCost,
total: buildTotal,
sum_material: buildSumMaterial,
sum_labor: buildSumLabor,
sum_expense: buildSumExpense,
sum_machine: buildSumMachine,
sg_list: buildSgList,
sg: buildSg,
};
/** 탭 짜기 — 없는 탭 = null(「다음 단계에서 채움」) · 글 = 안내만 */
export function tabTableOf(tab: TabId, ctx: TabContext): TabTable | string | null {
const build = BUILDERS[tab] ?? DRAFT.builders[tab];
return build ? build(ctx) : null;
}
/** 표 → 공용 표 옵션(화면과 같은 값 · 꼴) — 누름 · 메뉴 · 풍선은 그리는 쪽 몫 */
export function tableOptionsOf(spec: TabTable, tab?: TabId): DataTableOptions {
return {
columns: spec.columns,
rows: spec.rows,
total: spec.total ?? null,
frozen: spec.frozen ?? 0,
empty: "",
caption: TABS.find(([id]) => id === tab)?.[1],
editable: spec.editable,
// 243-1 고칠 수 있는 칸 = 모든 표 더블클릭 · Enter · F2 (설계내역서 키 = 레벨 키 몫)
editOnDouble: spec.editOnDouble ?? true,
keyEdit: !(spec as BillTable).menu,
onEdit: spec.onEdit,
rowClass: spec.rowClass,
cellClass: spec.cellClass,
level: spec.level,
mergeSame: spec.mergeSame,
};
}
+4 -58
View File
@@ -32,36 +32,17 @@ import { computeLabor, daegaPrice } from "./B09_Estimation_Calc_Labor";
import { computeTotal } from "./B09_Estimation_Calc_Total";
import { createTabBars } from "./B09_Estimation_UI_TabBar";
import { createCheckPanel } from "./B09_Estimation_UI_Check";
import { buildDaegaBlocks, buildDaegaList } from "./B09_Estimation_UI_Tabs_Labor";
import { tableOptionsOf, tabTableOf } from "./B09_Estimation_UI_Builders";
import {
DRAFT,
rowIndex,
TABS,
type Link,
type TabBuild,
type TabContext,
type TabId,
} from "./B09_Estimation_UI_Tabs";
import {
buildBasis,
buildCompare,
buildExpense,
buildLabor,
buildMachine,
buildMachineBlocks,
buildMaterial,
} from "./B09_Estimation_UI_Tabs_Base";
import { buildBill } from "./B09_Estimation_UI_Tabs_Bill";
import { attachRowMenu, openMenuAt, type BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
import { jumpRange, jumpTo, levelBar, pickedClass } from "./B09_Estimation_UI_Tabs_Bill_Level";
import {
buildSumExpense,
buildSumLabor,
buildSumMachine,
buildSumMaterial,
} from "./B09_Estimation_UI_Tabs_Summary";
import { buildCost, buildTotal } from "./B09_Estimation_UI_Tabs_Total";
import { buildSg, buildSgList } from "./B09_Estimation_UI_Tabs_Sangeun";
import {
isGroupTab,
lineHint,
@@ -79,27 +60,6 @@ function L(key: keyof typeof ui_locales): string {
const LATER: Record<string, string> = {};
const BUILDERS: Partial<Record<TabId, TabBuild>> = {
dg_list: buildDaegaList,
dg: buildDaegaBlocks,
bill: buildBill,
basis: buildBasis,
machine: buildMachine,
machine_blocks: buildMachineBlocks,
material: buildMaterial,
labor: buildLabor,
expense: buildExpense,
compare: buildCompare,
cost: buildCost,
total: buildTotal,
sum_material: buildSumMaterial,
sum_labor: buildSumLabor,
sum_expense: buildSumExpense,
sum_machine: buildSumMachine,
sg_list: buildSgList,
sg: buildSg,
};
async function request<T>(path: string, init: RequestInit = {}): Promise<T> {
const response = await fetch(`${API_BASE_URL}${path}`, {
credentials: "include",
@@ -198,10 +158,9 @@ export function renderB09Estimation(root: HTMLElement): void {
bars.setTools([]); // 보기 단추 · 열 수는 묶음 탭만 다시 채움(202-1)
if (!projectId) return void panel.replaceChildren(note("프로젝트를 먼저 고르세요."));
if (!payload) return void panel.replaceChildren(note("원가 자료를 불러오는 중…"));
const build = BUILDERS[tab] ?? DRAFT.builders[tab];
if (!build) return void panel.replaceChildren(note(LATER[tab] ?? "다음 단계에서 채움"));
const ctx: TabContext = { sheets, payload, user, setUser };
const spec = build(ctx);
const spec = tabTableOf(tab, ctx);
if (!spec) return void panel.replaceChildren(note(LATER[tab] ?? "다음 단계에서 채움"));
if (typeof spec === "string") return void panel.replaceChildren(note(spec));
const goto = (link: Link | null): void => {
if (link) show(link.tab, link.key);
@@ -209,23 +168,10 @@ export function renderB09Estimation(root: HTMLElement): void {
// extra = 줄 class 곁값(181-2 표 보기 호표 구간)
const makeTable = (extra?: (index: number) => string | null): DataTable => {
const made = createDataTable({
columns: spec.columns,
rows: spec.rows,
total: spec.total ?? null,
frozen: spec.frozen ?? 0,
empty: "",
caption: TABS.find(([id]) => id === tab)?.[1],
editable: spec.editable,
// 243-1 고칠 수 있는 칸 = 모든 표 더블클릭 · Enter · F2 (설계내역서 키 = 레벨 키 몫)
editOnDouble: spec.editOnDouble ?? true,
keyEdit: !(spec as BillTable).menu,
onEdit: spec.onEdit,
...tableOptionsOf(spec, tab),
rowClass: extra
? (i) => [spec.rowClass?.(i), extra(i)].filter(Boolean).join(" ") || null
: spec.rowClass,
cellClass: spec.cellClass,
level: spec.level,
mergeSame: spec.mergeSame,
onCellClick: (i, key) => {
if (spec.onCell) spec.onCell(i, key);
if (spec.link && !spec.editable?.(i, key)) goto(spec.link(i, key));
+2 -1
View File
@@ -178,7 +178,8 @@ def test_작업량_Q는_소수_둘():
def test_화면_탭_둘_서버_산근_그대로():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "sg_list: buildSgList," in shell and "sg: buildSg," in shell
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
assert "sg_list: buildSgList," in builders and "sg: buildSg," in builders # 303-2 표 짜기
assert 'const SERVER_ONLY = ["sg", "sg_list", "sg_gaps", "sg_notes"] as const;' in shell
assert "gaps: [...base.gaps, ...(server.sg_gaps || [])" in shell # 뒤에 대가(145-2) gaps 이음
tabs = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Sangeun.ts").read_text("utf-8")
@@ -101,12 +101,14 @@ def test_배선_수기_적용_칸_출처_돌림_없앰():
assert 'editable: (_i, key) => key === "p:견적",' in compare
assert "nextPick" not in base and 'key === "applied_from"' not in compare
assert 'key === `p:${rows[i]?.applied_from}` ? "is-picked"' in compare
assert 'key === "applied" && rows[i] ? chooseApplied(rows[i]) : null' in compare # 257-3 메뉴 둘 공용
assert (
'key === "applied" && rows[i] ? chooseApplied(rows[i]) : null' in compare
) # 257-3 메뉴 둘 공용
assert "delete cell.applied" in compare
shell = _src("B09_Estimation/B09_Estimation_UI_Shell.ts")
assert (
"cellClass: spec.cellClass," in shell and "openMenuAt(box.left, box.bottom, items)" in shell
)
builders = _src("B09_Estimation/B09_Estimation_UI_Builders.ts") # 303-2 옵션 짜기
assert "cellClass: spec.cellClass," in builders and "...tableOptionsOf(spec, tab)," in shell
assert "openMenuAt(box.left, box.bottom, items)" in shell
edit = _src("B09_Estimation/B09_Estimation_UI_Tabs_Bill_Edit.ts")
assert "export function openMenuAt(" in edit and "const ctxMenu = sharedMenu();" in edit
css = _src("B09_Estimation/B09_Estimation_UI_Page.css")
@@ -120,7 +120,7 @@ APPLIED = [
("B09_Estimation/B09_Estimation_UI_Tabs_Total.ts", 'mergeSame: ["group", "part"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Total.ts", 'mergeSame: ["part"]'),
("B09_Estimation/B09_Estimation_UI_Tabs_Bill.ts", 'group: (i) => rows[i]?.kind !== "item"'),
("B09_Estimation/B09_Estimation_UI_Shell.ts", "mergeSame: spec.mergeSame"),
("B09_Estimation/B09_Estimation_UI_Builders.ts", "mergeSame: spec.mergeSame"),
]
+2 -1
View File
@@ -345,7 +345,8 @@ def test_고른_줄_상자_테두리_테마_토큰():
def test_Shell_배선():
shell = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Shell.ts").read_text("utf-8")
assert "editOnDouble: spec.editOnDouble ?? true," in shell # 243-1 기본 = 더블클릭
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
assert "editOnDouble: spec.editOnDouble ?? true," in builders # 243-1 기본 = 더블클릭
assert "levelBar(table.root, bill, (i) => table.edit(i))" in shell
for name in [
"ui_template/ui_template_data_table.ts",
@@ -114,8 +114,10 @@ def test_B09_표_만드는_길_한_곳_더블클릭_기본():
]
assert calls == ["B09_Estimation_UI_Shell.ts"]
shell = (ROOT / "B09_Estimation" / calls[0]).read_text("utf-8")
assert "editOnDouble: spec.editOnDouble ?? true," in shell
assert "keyEdit: !(spec as BillTable).menu," in shell # 설계내역서 Enter · F2 = 레벨 키 몫
assert "...tableOptionsOf(spec, tab)," in shell # 303-2 옵션 짜기 = Builders 한 곳
builders = (ROOT / "B09_Estimation" / "B09_Estimation_UI_Builders.ts").read_text("utf-8")
assert "editOnDouble: spec.editOnDouble ?? true," in builders
assert "keyEdit: !(spec as BillTable).menu," in builders # 설계내역서 Enter · F2 = 레벨 키 몫
def test_B08_고칠_칸_있는_표_모두_editOnDouble():