Merge remote-tracking branch 'origin/main_laptop_1' into sub_laptop_2
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/* =============================================================================
|
||||
* B09_Draft_Print_Cost.ts
|
||||
* 인쇄 미리보기(PLAN 302-4 · 초안) 「원가계산」 영역 — B09 18장을 실무 내역서 차례로 모음.
|
||||
* 차례 · 이름 · 방향 = 기번3 내역서 xlsx 시트(목차 · 안내 빼고 · page_setup.orientation)
|
||||
* 값 = GET …/estimation + 화면 캐시(`b09-estimation`) → `computeSheets` = B09 화면과 같은 값
|
||||
* 표 = 화면 표 짜기(`…_UI_Builders`) 그대로 · 링크 · 강조 · 고칠 칸 표시 · 단가연결 칸만 뺌
|
||||
* 공기 초안 탭(공기집계 · 공기산출) = 내역서 차례 밖 → 안 넣음
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { DataTableOptions } from "@ui/ui_template_data_table";
|
||||
import { readState } from "../../A00_Common/b_page_state";
|
||||
import type { Row } from "../B09_Estimation_Calc_Base";
|
||||
import { tableOptionsOf, tabTableOf } from "../B09_Estimation_UI_Builders";
|
||||
import { computeSheets } from "../B09_Estimation_UI_Shell";
|
||||
import type { TabContext, TabId } from "../B09_Estimation_UI_Tabs";
|
||||
import type { CollectSheets, PrintSheet } from "./B09_Draft_Print_Types";
|
||||
|
||||
type Orientation = NonNullable<PrintSheet["orientation"]>;
|
||||
|
||||
/** 기번3 내역서 시트 차례 — [탭, 시트 이름, 인쇄 방향] */
|
||||
export const COST_SHEETS: Array<[TabId, string, Orientation]> = [
|
||||
["cost", "공사원가계산서", "landscape"],
|
||||
["total", "총괄설계내역서", "landscape"],
|
||||
["bill", "설계내역서", "landscape"],
|
||||
["dg_list", "일위대가목록표", "landscape"],
|
||||
["dg", "일위대가표", "landscape"],
|
||||
["sg_list", "단가산출근거목록표", "landscape"],
|
||||
["sg", "단가산출근거", "landscape"],
|
||||
["basis", "환율및기초자료", "landscape"],
|
||||
["machine", "중기목록표", "landscape"],
|
||||
["machine_blocks", "중기사용료", "landscape"],
|
||||
["material", "재료비목록표", "landscape"],
|
||||
["labor", "노무비목록표", "landscape"],
|
||||
["expense", "경비목록표", "landscape"],
|
||||
["compare", "자재단가대비표", "landscape"],
|
||||
["sum_material", "재료비수량금액집계표", "landscape"],
|
||||
["sum_labor", "노무비수량금액집계표", "landscape"],
|
||||
["sum_expense", "경비수량금액집계표", "landscape"],
|
||||
["sum_machine", "중기시간금액집계표", "landscape"],
|
||||
];
|
||||
|
||||
/** 화면 조작 전용 칸(설계내역서 「단가연결 ▾」) */
|
||||
const UI_COLUMNS = new Set(["connect"]);
|
||||
/** 인쇄에 남길 줄 꼴(제목 줄 굵게) — 손 줄 · 고른 줄 표시는 뺌 */
|
||||
const KEEP_ROW = new Set(["is-title"]);
|
||||
|
||||
/** 화면 표 옵션 → 인쇄 표(값 · 머리 · 레벨 · 합계 · 병합 그대로 · 누름 · 고침 · 강조 뺌) */
|
||||
export function printTable(options: DataTableOptions, caption: string): DataTableOptions {
|
||||
const { columns, rows, total, frozen, empty, level, mergeSame, rowClass } = options;
|
||||
return {
|
||||
columns: columns.filter((c) => !UI_COLUMNS.has(c.key)),
|
||||
rows,
|
||||
total,
|
||||
frozen,
|
||||
empty,
|
||||
caption,
|
||||
mergeSame,
|
||||
level: level && { ...level, tree: false },
|
||||
rowClass:
|
||||
rowClass &&
|
||||
((i) =>
|
||||
rowClass(i)
|
||||
?.split(" ")
|
||||
.filter((c) => KEEP_ROW.has(c))
|
||||
.join(" ") || null),
|
||||
};
|
||||
}
|
||||
|
||||
/** 받은 자료 + 사용자 값 → 18장(차례 그대로 · 표가 없는 탭 = 빈 표) */
|
||||
export function costSheetsOf(payload: Row, user: Row): PrintSheet[] {
|
||||
const ctx: TabContext = {
|
||||
sheets: computeSheets(payload, user),
|
||||
payload,
|
||||
user,
|
||||
setUser: () => {},
|
||||
};
|
||||
return COST_SHEETS.map(([id, name, orientation]) => {
|
||||
const spec = tabTableOf(id, ctx);
|
||||
const table =
|
||||
!spec || typeof spec === "string"
|
||||
? { columns: [], rows: [], caption: name }
|
||||
: printTable(tableOptionsOf(spec, id), name);
|
||||
return { id, name, orientation, table };
|
||||
});
|
||||
}
|
||||
|
||||
export const collectCostSheets: CollectSheets = async (projectId) => {
|
||||
const response = await fetch(`${API_BASE_URL}/projects/${projectId}/estimation`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!response.ok) throw new Error(`원가 자료를 불러오지 못했습니다 — HTTP ${response.status}`);
|
||||
const payload = (await response.json()) as Row;
|
||||
const user = readState<Row>("b09-estimation", projectId) ?? payload.user ?? {};
|
||||
return costSheetsOf(payload, user);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/* =============================================================================
|
||||
* print_cost_entry.ts (PLAN 302-4 시험 진입)
|
||||
* 원가계산 인쇄 쪽(`B09_Draft_Print_Cost.costSheetsOf`)을 화면 표 짜기(`tabTableOf`)와 대조.
|
||||
* vite `--ssr` 로 번들해 node 로 실행 — 인자 = payload JSON 경로 · 출력 = 쪽마다 대조 JSON.
|
||||
* ========================================================================== */
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { costSheetsOf } from "../../../B09_Estimation/draft/B09_Draft_Print_Cost";
|
||||
import { tabTableOf } from "../../../B09_Estimation/B09_Estimation_UI_Builders";
|
||||
import { computeSheets } from "../../../B09_Estimation/B09_Estimation_UI_Shell";
|
||||
import type { TabId } from "../../../B09_Estimation/B09_Estimation_UI_Tabs";
|
||||
|
||||
const payload = JSON.parse(readFileSync(process.argv[2], "utf-8"));
|
||||
const user = payload.user ?? {};
|
||||
const ctx = { sheets: computeSheets(payload, user), payload, user, setUser: () => {} };
|
||||
|
||||
const out = costSheetsOf(payload, user).map((sheet) => {
|
||||
const spec = tabTableOf(sheet.id as TabId, ctx);
|
||||
const screen = !spec || typeof spec === "string" ? null : spec;
|
||||
const t = sheet.table;
|
||||
const n = t.rows.length;
|
||||
return {
|
||||
id: sheet.id,
|
||||
name: sheet.name,
|
||||
orientation: sheet.orientation,
|
||||
rows: n,
|
||||
screenRows: screen ? screen.rows.length : 0,
|
||||
sameRows: JSON.stringify(t.rows) === JSON.stringify(screen?.rows ?? []),
|
||||
total: t.total ?? null,
|
||||
screenTotal: screen?.total ?? null,
|
||||
columns: t.columns.map((c) => c.key),
|
||||
screenColumns: screen ? screen.columns.map((c) => c.key) : [],
|
||||
groups: t.columns.filter((c) => c.group?.length).length,
|
||||
level: t.level ? { key: t.level.key, tree: t.level.tree ?? false } : null,
|
||||
levels: t.level ? Array.from({ length: n }, (_, i) => t.level?.of(i) ?? null) : [],
|
||||
rowClass: t.rowClass ? Array.from({ length: n }, (_, i) => t.rowClass?.(i) ?? null) : [],
|
||||
mergeSame: t.mergeSame ?? [],
|
||||
caption: t.caption,
|
||||
interactive: ["editable", "onEdit", "onCellClick", "onRowClick", "cellClass", "focus"].filter(
|
||||
(k) => k in t,
|
||||
),
|
||||
};
|
||||
});
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"""PLAN 302-4 인쇄 미리보기 원가계산 — B09 18장 = 기번3 내역서 시트 차례 · 이름 · 방향 ·
|
||||
쪽 표 = 화면 표 짜기와 같은 줄 · 합계(링크 · 강조 · 고칠 칸 · 단가연결 칸만 뺌).
|
||||
|
||||
차례 = 실무 xlsx(openpyxl · 목차 · 안내 · 숨김 시트 빼고) · 견본 = 임시 폴더 프로젝트(145-2 한 바퀴 꼴)
|
||||
→ 서버 셈 payload → `print_cost_entry.ts` 를 vite `--ssr` 로 번들해 node 로 대조.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
|
||||
import openpyxl
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for path in (ROOT, ROOT / "resources" / "tester"):
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
|
||||
import test_145_2_b09_labor_calc as t145 # noqa: E402
|
||||
|
||||
from B09_Estimation import B09_Estimation_Prices as prices # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Rates import rate_candidates, rate_table # noqa: E402
|
||||
from B09_Estimation.B09_Estimation_Router import compute_twice # noqa: E402
|
||||
|
||||
COST = ROOT / "B09_Estimation" / "draft" / "B09_Draft_Print_Cost.ts"
|
||||
TABS = ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs.ts"
|
||||
ENTRY = ROOT / "resources" / "tester" / "print_cost" / "print_cost_entry.ts"
|
||||
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
|
||||
XLSX = (
|
||||
ROOT
|
||||
/ "resources/knowledge/original/실무문서/2024년 간선임도(기번3-울진.대흥)(공통)"
|
||||
/ "1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).xlsx"
|
||||
)
|
||||
#: 내역서 앞 시트(차례 밖)
|
||||
FRONT = ("〓 목 차 〓", "※※안내※※")
|
||||
|
||||
ENTRY_RE = re.compile(r'\["(\w+)", "([^"]+)", "(landscape|portrait)"\]')
|
||||
|
||||
|
||||
def _cost_sheets() -> list[tuple[str, str, str]]:
|
||||
src = COST.read_text("utf-8")
|
||||
body = src[src.index("COST_SHEETS") : src.index("];", src.index("COST_SHEETS"))]
|
||||
return ENTRY_RE.findall(body)
|
||||
|
||||
|
||||
def _practical() -> list[tuple[str, str]]:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore") # 인쇄 영역 정의 이름 경고
|
||||
wb = openpyxl.load_workbook(XLSX)
|
||||
return [
|
||||
(ws.title, ws.page_setup.orientation or "landscape")
|
||||
for ws in wb.worksheets
|
||||
if ws.sheet_state == "visible" and ws.title not in FRONT
|
||||
]
|
||||
|
||||
|
||||
def test_차례_이름_방향_기번3_내역서():
|
||||
ours = _cost_sheets()
|
||||
assert len(ours) == 18
|
||||
assert [(name, o) for _id, name, o in ours] == _practical()
|
||||
assert all(len(name) <= 31 for _id, name, _o in ours) # 엑셀 시트 이름 한도
|
||||
# 탭 이름 = 화면 탭 이름(B09 TABS 앞 18장 · 같은 차례)
|
||||
tabs = re.findall(r'^ \["(\w+)", "([^"]+)"\],$', TABS.read_text("utf-8"), re.MULTILINE)
|
||||
assert [(i, n) for i, n, _o in ours] == tabs[:18]
|
||||
assert not {"schedule_sum", "schedule_calc"} & {i for i, _n, _o in ours} # 공기 초안 뺌
|
||||
|
||||
|
||||
def _payload(root: Path) -> dict:
|
||||
"""145-2 한 바퀴 견본 — 관 둘(대가 · 산근 · 중기 · 사전조합) · 설계내역 한 줄 → 라우터 꼴 payload."""
|
||||
t145._snap(root, "pipe@100.0", "SS000901", t145.LINES, t145._values(t145.STONE))
|
||||
promoted = [
|
||||
{"로직": "GF000119", "승격": True, "수량": {"손": 1}},
|
||||
{"참조": {"층": "system", "주인": None, "키": "SC-T"}, "수량": {"손": 4}},
|
||||
]
|
||||
t145._snap(root, "pipe@400.0", "SS000902", promoted, t145._values(t145.STONE), t145.SC)
|
||||
inputs = prices.build_inputs(None, linked_resource_keys(root))
|
||||
inputs["daega"] = daega_inputs(root)
|
||||
inputs["hopyo"] = {}
|
||||
inputs["bill"] = {
|
||||
"items": [
|
||||
{"key": "st:revet", "section": "2.1", "name": "돌기슭막이", "spec": "", "unit": "m",
|
||||
"qty": 10, "ref": {"tab": "dg_list", "key": "revet"}, "note": "",
|
||||
"owners": {"pipe@100.0": 4, "pipe@400.0": 6}},
|
||||
]
|
||||
} # fmt: skip
|
||||
sheets, summary = compute_twice(root, inputs, {})
|
||||
return {
|
||||
"inputs": inputs,
|
||||
"user": {},
|
||||
"book": None,
|
||||
"stale": None,
|
||||
"sheets": sheets,
|
||||
"rate_candidates": rate_candidates(),
|
||||
"rate_table": rate_table(),
|
||||
"notes": [],
|
||||
"summary": summary,
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def pages(tmp_path_factory) -> list[dict]:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
work = tmp_path_factory.mktemp("p302_4")
|
||||
root = t145._project(work / "proj", [(100, "SS000901"), (400, "SS000902")])
|
||||
data = work / "payload.json"
|
||||
data.write_text(json.dumps(_payload(root), ensure_ascii=False, default=str), "utf-8")
|
||||
with tempfile.TemporaryDirectory(prefix="print_cost_test_") as outdir:
|
||||
entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/")
|
||||
build = subprocess.run(
|
||||
[
|
||||
"node",
|
||||
str(VITE),
|
||||
"build",
|
||||
"--configLoader",
|
||||
"runner",
|
||||
"--ssr",
|
||||
entry_rel,
|
||||
"--outDir",
|
||||
outdir,
|
||||
],
|
||||
cwd=str(ROOT),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=180,
|
||||
)
|
||||
assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}"
|
||||
run = subprocess.run(
|
||||
["node", str(Path(outdir) / "print_cost_entry.js"), str(data)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=60,
|
||||
)
|
||||
assert run.stdout.strip(), f"시험 진입이 아무 결과도 안 냄:\n{run.stderr}"
|
||||
return json.loads(run.stdout)
|
||||
|
||||
|
||||
def test_쪽_차례_이름_방향(pages):
|
||||
assert [(p["id"], p["name"], p["orientation"]) for p in pages] == _cost_sheets()
|
||||
assert all(p["caption"] == p["name"] for p in pages)
|
||||
|
||||
|
||||
def test_쪽_표_화면과_같은_줄_합계(pages):
|
||||
for p in pages:
|
||||
assert p["sameRows"] and p["rows"] == p["screenRows"], p["id"]
|
||||
assert p["total"] == p["screenTotal"], p["id"]
|
||||
assert p["columns"] == [k for k in p["screenColumns"] if k != "connect"], p["id"]
|
||||
assert p["interactive"] == [], p["id"] # 누름 · 고침 · 강조 뺌
|
||||
by = {p["id"]: p for p in pages}
|
||||
# 견본이 표를 채움 — 내역 · 대가 · 산근 · 중기 · 집계
|
||||
for tab in ("bill", "dg_list", "dg", "sg_list", "sg", "machine", "sum_labor", "cost", "total"):
|
||||
assert by[tab]["rows"] > 0, tab
|
||||
|
||||
|
||||
def test_머리_여러_줄_레벨_병합_꼴(pages):
|
||||
by = {p["id"]: p for p in pages}
|
||||
bill = by["bill"]
|
||||
assert bill["groups"] > 0 # 머리 두 줄(재료비 · 노무비 · 경비 묶음)
|
||||
assert "connect" not in bill["columns"] # 단가연결 메뉴 칸 뺌
|
||||
assert bill["level"] == {"key": "name", "tree": False} # 들여쓰기만 · 나무 아이콘 뺌
|
||||
assert max(v for v in bill["levels"] if v) >= 2
|
||||
assert {c for c in bill["rowClass"] if c} <= {"is-title"} # 손 줄 · 고른 줄 표시 뺌
|
||||
assert "is-title" in bill["rowClass"]
|
||||
assert by["total"]["mergeSame"] == ["part"] # 총괄 공종 칸 세로 병합
|
||||
Reference in New Issue
Block a user