diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.py b/B09_Estimation/B09_Estimation_Calc_Bill.py index 2e71e0ea0..6c7379b47 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.py +++ b/B09_Estimation/B09_Estimation_Calc_Bill.py @@ -490,11 +490,10 @@ def compute_bill( gaps.extend(row.pop("_gaps", [])) row.pop("_home", None) row["no"] = "" - if row["kind"] == "title": - empty = f" 빈 줄 {row['blank']}" if row["blank"] else "" - row["note"] = ("(제외금액)" if row["key"] == "sec:G" else "") + empty + if row["kind"] == "title": # 257-2 비고 「빈 줄 n」 없음(blank 수는 줄 값 그대로) + row["note"] = "(제외금액)" if row["key"] == "sec:G" else "" elif row["kind"] == "group": - row["note"] = f"빈 줄 {row['blank']}" if row["blank"] else "" + row["note"] = "" if row["kind"] != "item" and row["cost"] == "direct" and row["level"] <= 2: up = row["parent"] # 번호 = 레벨 1 · 2 묶음 차례(1 · 1.1 · 관급 없음) seq[up] = seq.get(up, 0) + 1 diff --git a/B09_Estimation/B09_Estimation_Calc_Bill.ts b/B09_Estimation/B09_Estimation_Calc_Bill.ts index 215c7dee6..50ad3e3fd 100644 --- a/B09_Estimation/B09_Estimation_Calc_Bill.ts +++ b/B09_Estimation/B09_Estimation_Calc_Bill.ts @@ -458,10 +458,9 @@ export function computeBill( delete row._gaps; delete row._home; row.no = ""; - if (row.kind === "title") - row.note = - (row.key === "sec:G" ? "(제외금액)" : "") + (row.blank ? ` 빈 줄 ${row.blank}` : ""); - else if (row.kind === "group") row.note = row.blank ? `빈 줄 ${row.blank}` : ""; + // 257-2 비고 「빈 줄 n」 없음(blank 수는 줄 값 그대로) + if (row.kind === "title") row.note = row.key === "sec:G" ? "(제외금액)" : ""; + else if (row.kind === "group") row.note = ""; if (row.kind !== "item" && row.cost === "direct" && row.level <= 2) { // 번호 = 레벨 1 · 2 묶음 차례(1 · 1.1 · 관급 없음) const up = row.parent; diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index 445f1c234..1f7c9faeb 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -62,7 +62,13 @@ import { } 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, pickClass, renderGroupPanel } from "./B09_Estimation_UI_Tabs_Groups"; +import { + isGroupTab, + lineHint, + pickClass, + renderGroupPanel, + unpickOnRelease, +} from "./B09_Estimation_UI_Tabs_Groups"; // 252 초안 탭(공기 등) — draft 폴더가 있으면 등록 · 지우면 빈 목록(오류 없음) import.meta.glob("./draft/*_UI.ts", { eager: true }); @@ -274,13 +280,17 @@ export function renderB09Estimation(root: HTMLElement): void { const bill = (spec as BillTable).menu ? (spec as BillTable) : null; const at = focus === undefined ? -1 : rowIndex(spec.rows, focus); if (bill) jumpTo(bill.rows, at); + // 257-2 설계내역서 밖 탭도 이동해 온 줄 = 같은 빨강 상자(그 줄 하나 · 줄 누름 · Esc 로 풂) + const range: [number, number] | null = !bill && at >= 0 ? [at, at] : null; + const rangeClass = range ? (i: number) => pickClass(range, i) : undefined; const table = makeTable( - bill ? (i) => pickedClass(bill.rows[i]) ?? pickClass(jumpRange(), i) : undefined, + bill ? (i) => pickedClass(bill.rows[i]) ?? pickClass(jumpRange(), i) : rangeClass, ); + if (range) unpickOnRelease(table.root); const level = bill ? levelBar(table.root, bill, (i) => table.edit(i)) : null; if (level) bars.setTools(level.tools); panel.replaceChildren(table.root, ...(level ? [level.guide] : []), ...(hint ? [hint] : [])); - if (at >= 0) requestAnimationFrame(() => table.focusRow(at, !bill)); // 설계내역서 = 스크롤만(표시 = 빨강) + if (at >= 0) requestAnimationFrame(() => table.focusRow(at, false)); // 스크롤만(표시 = 빨강) }; const apply = (next: Row, fromServer: boolean): void => { diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts index 8618460f7..568979122 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Groups.ts @@ -203,6 +203,17 @@ function listenKeys(): void { }); } +/** 257-2 이동해 온 줄 빨강(묶음 아닌 표 · 설계내역서 빼고) — 줄 누름 · 방향키 · Esc 로 풂(다시 그리지 않음) */ +export function unpickOnRelease(root: HTMLElement): void { + const clear = (): void => { + for (const tr of root.querySelectorAll("tr.is-pick")) + tr.classList.remove("is-pick", "is-pick-top", "is-pick-end"); + }; + root.addEventListener("click", clear); + live = { root, unpick: clear }; + listenKeys(); +} + /** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */ const chosen = new Map(); diff --git a/B09_Estimation/B09_Estimation_UI_Tabs_Total.ts b/B09_Estimation/B09_Estimation_UI_Tabs_Total.ts index c95485580..1ea78b13f 100644 --- a/B09_Estimation/B09_Estimation_UI_Tabs_Total.ts +++ b/B09_Estimation/B09_Estimation_UI_Tabs_Total.ts @@ -223,8 +223,9 @@ export const buildTotal: TabBuild = (ctx): TabTable => { `순자재대 ${won(supply.net ?? null)} + 조달수수료 ${won(supply.fee ?? null)} → 천원 올림`, { hand: hand("supply") }, ), - row("grand", "바.", "총공사비", sub.grand ?? null), ]; + // 257-2 총공사비 = 표 아래 고정 줄(템플릿 total · 몸 줄 아님 · 줄 이동 누름 없음) + const grand = row("grand", "바.", "총공사비", sub.grand ?? null); const safetyRow = rows.find((r) => r.key === "safety"); if (safetyRow && safety.a !== null && safety.a !== undefined) safetyRow.note = `A(관급 ÷ 1.1 포함) ${won(safety.a)} · B(× 1.2) ${won(safety.b)} → 작은 쪽`; @@ -245,6 +246,7 @@ export const buildTotal: TabBuild = (ctx): TabTable => { text("candidate", "요율 출처", 260), ], rows, + total: grand, frozen: 2, rowClass: (i) => levelClass(rows[i]), level: nameLevel(rows), @@ -288,8 +290,7 @@ const COST_ROWS: Array<[string, string, string, string, string?]> = [ ["vat", "부가가치세", "", "", "vat"], ["contract", "도급공사비", "", "", "contract"], ["supply", "관급자재대", "", "", "supply"], - ["grand", "총공사비", "", "", "grand"], -]; +]; // 총공사비(grand) = 표 아래 고정 줄(257-2 · buildCost total) export const buildCost: TabBuild = (ctx): TabTable => { const t: Row = ctx.sheets.total || {}; @@ -314,6 +315,14 @@ export const buildCost: TabBuild = (ctx): TabTable => { level: !part ? 1 : name === "소계" ? 2 : 3, })); return { + // 257-2 총공사비 = 표 아래 고정 줄(템플릿 total · 줄 이동 누름 없음) + total: { + key: "grand", + group: "총공사비", + amount: cost.grand ?? null, + share: share.grand ?? null, + note: formula.grand ?? "", + }, columns: [ text("group", "구분", 96), text("part", "비목", 72), diff --git a/resources/tester/test_145_3_b09_bill.py b/resources/tester/test_145_3_b09_bill.py index 87a11b144..ec6ef4aad 100644 --- a/resources/tester/test_145_3_b09_bill.py +++ b/resources/tester/test_145_3_b09_bill.py @@ -124,7 +124,7 @@ def test_제목_합_빈_줄과_원가계산서_이음(): assert keys[:3] == ["sec:1", "sec:1.1", "sec:1.2"] and keys[-1] == "sec:G" assert rows["sec:1.1"]["sum"] == 361494 + 1119272 and rows["sec:1.1"]["blank"] == 0 assert rows["sec:1"]["sum"] == 361494 + 1119272 + 11003825 # 빈 줄은 0 이 아니라 빈 줄 수 - assert rows["sec:1"]["blank"] == 1 and "빈 줄 1" in rows["sec:1"]["note"] + assert rows["sec:1"]["blank"] == 1 and "빈 줄" not in rows["sec:1"]["note"] # 257-2 비고 빈 줄 없음 # 145-4 이음 = {material, labor, expense, supply_net} · 빈 줄 있으면 None assert sheets["bill"]["sums"] == { "material": None, diff --git a/resources/tester/test_145_4_b09_total_calc.py b/resources/tester/test_145_4_b09_total_calc.py index ad95a5763..42b6e9ac2 100644 --- a/resources/tester/test_145_4_b09_total_calc.py +++ b/resources/tester/test_145_4_b09_total_calc.py @@ -191,7 +191,7 @@ t = buildTotal(ctx()); t.onEdit(at(t.rows, "care"), "rate", ""); out.careGone = "care" in user.estimate.rates; t = buildTotal(ctx()); -out.grandBlank = t.rows[at(t.rows, "grand")].amount; +out.grandBlank = t.total.amount; // 257-2 총공사비 = 표 아래 고정 줄 t.onEdit(at(t.rows, "safety"), "hand", "1,000"); out.base = user.estimate.safety_base; const c = buildCost(ctx()); diff --git a/resources/tester/test_182_b09_bill_level.py b/resources/tester/test_182_b09_bill_level.py index 122959fd6..6b9129080 100644 --- a/resources/tester/test_182_b09_bill_level.py +++ b/resources/tester/test_182_b09_bill_level.py @@ -121,7 +121,7 @@ def test_묶음_금액_자손_합_공종_합_그대로(): rows = _rows(sheets) g2, g1 = rows["hand:g2"], rows["hand:g1"] assert (g2["sum"], g2["labor"], g2["material"], g2["expense"]) == (2460, 2000, 400, 60) - assert (g2["blank"], g2["note"]) == (1, "빈 줄 1") + assert (g2["blank"], g2["note"]) == (1, "") # 257-2 비고 빈 줄 없음 assert (g1["sum"], g1["labor"], g1["blank"]) == (12300 + 2460, 12000, 1) # a + b(+ c 빈 줄) assert rows["hand:p"]["sum"] == 0 and rows["hand:p"]["note"] == "" # 공종 합 · 원가계산서 sums · 계 = 묶음 없을 때와 같음(묶음 줄 안 셈) diff --git a/resources/tester/test_185_1_b09_total_level.py b/resources/tester/test_185_1_b09_total_level.py index 896400301..5ba5c8e07 100644 --- a/resources/tester/test_185_1_b09_total_level.py +++ b/resources/tester/test_185_1_b09_total_level.py @@ -21,7 +21,8 @@ BODY = f""" const user = {{ estimate: {{}} }}; const ctx = {{ sheets: {{ total: computeTotal({json.dumps(SUMS)}, user.estimate) }}, payload: {{}}, user, setUser: () => {{}} }}; const pack = (t) => t.rows.map((r, i) => [r.key, r.level, t.rowClass(i), t.level.key, t.level.of(i), t.cellClass ?? null]); -console.log(JSON.stringify({{ total: pack(buildTotal(ctx)), cost: pack(buildCost(ctx)) }})); +const T = buildTotal(ctx), C = buildCost(ctx); +console.log(JSON.stringify({{ total: pack(T), cost: pack(C), fixed: {{ total: T.total, cost: C.total }} }})); """ @@ -41,8 +42,9 @@ def _run() -> dict: def test_총괄_레벨(): rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["total"]} - top = ["direct", "net_cost", "da", "supply_price", "contract", "grand"] + top = ["direct", "net_cost", "da", "supply_price", "contract"] assert all(rows[k] == (1, "is-title") for k in top) + assert "grand" not in rows # 257-2 총공사비 = 표 아래 고정 줄(total) for k in ["in_price", "in_env", "indirect_labor", "machine", "retirement", "overhead", "vat", "supply"]: # fmt: skip assert rows[k] == (2, None), k @@ -51,8 +53,9 @@ def test_총괄_레벨(): def test_원가계산서_레벨(): rows = {k: (lv, cls) for k, lv, cls, *_ in _run()["cost"]} - for k in ["overhead", "profit", "supply_price", "vat", "contract", "supply", "grand"]: + for k in ["overhead", "profit", "supply_price", "vat", "contract", "supply"]: assert rows[k] == (1, "is-title"), k + assert "grand" not in rows # 257-2 총공사비 = 표 아래 고정 줄(total) for k in ["material", "labor", "expense_sum"]: assert rows[k] == (2, None), k for k in ["direct_material", "scrap", "direct_labor", "indirect_labor", "expense", "safety"]: @@ -74,3 +77,10 @@ def test_CSS_옛_레벨_규칙_없음_공용_표로(): r"td\.ui-data-table__lv \{\s*padding-left: calc\(var\(--spacing-8\) \+ var\(--dt-lv, 0\) \* var\(--dt-indent\)\);", common, ) + + +def test_총공사비_아래_고정_줄_총괄_원가계산서(): + fixed = _run()["fixed"] + assert fixed["total"]["key"] == "grand" and fixed["total"]["name"] == "총공사비" + assert fixed["cost"]["key"] == "grand" and fixed["cost"]["group"] == "총공사비" + assert fixed["cost"]["amount"] == fixed["total"]["amount"] # 원가계산서 = 총괄 거울 diff --git a/resources/tester/test_242_1_b09_bill_jump_range.py b/resources/tester/test_242_1_b09_bill_jump_range.py index cf65f27a4..5f9b71b27 100644 --- a/resources/tester/test_242_1_b09_bill_jump_range.py +++ b/resources/tester/test_242_1_b09_bill_jump_range.py @@ -122,7 +122,7 @@ def test_Shell_배선_공용_표_스크롤만(): shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") assert "if (bill) jumpTo(bill.rows, at);" in shell assert "pickedClass(bill.rows[i]) ?? pickClass(jumpRange(), i)" in shell - assert "table.focusRow(at, !bill)" in shell + assert "table.focusRow(at, false)" in shell # 257-2 모든 탭 빨강 = 스크롤만 common = (ROOT / "ui_template" / "ui_template_data_table.ts").read_text("utf-8") assert "focusRow: (index: number, mark?: boolean) => void;" in common assert "state.focus = mark ? index : null;" in common diff --git a/resources/tester/test_257_2_b09_pick_blank_grand.py b/resources/tester/test_257_2_b09_pick_blank_grand.py new file mode 100644 index 000000000..ef5fdd736 --- /dev/null +++ b/resources/tester/test_257_2_b09_pick_blank_grand.py @@ -0,0 +1,64 @@ +"""257-2 B09 이동해 온 줄 = 모든 탭 빨강 상자 · 비고 「빈 줄 N」 없음 · 총공사비 = 표 아래 고정 줄. + +- 쉘 = 설계내역서 밖 · 묶음 아닌 탭에도 줄 범위 [at, at] 빨강(pickClass) · 줄 누름 · Esc · 방향키로 풂(unpickOnRelease) +- 비고 = 제목 줄 「(제외금액)」만 · 묶음 줄 빈칸(Py · TS 짝 · blank 수는 줄 값 그대로) +- 총괄 · 원가계산서 총공사비(grand) = `TabTable.total`(몸 줄에서 빠짐 · 줄 이동 누름 없음 — 185-1 시험이 값 확인) +""" + +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from resources.tester.helper_b09_group_cards import COMMON, GROUPS, RAW, TABS, run # noqa: E402 + +B09 = ROOT / "B09_Estimation" + + +def test_쉘_모든_탭_빨강_배선(): + shell = (B09 / "B09_Estimation_UI_Shell.ts").read_text("utf-8") + assert "!bill && at >= 0 ? [at, at] : null" in shell + assert "pickClass(range, i)" in shell and "unpickOnRelease(table.root)" in shell + assert "table.focusRow(at, false)" in shell + + +def test_풂_줄_누름_Esc_빨강만_지움(): + out = run( + [COMMON, TABS, GROUPS], + """ +const root = document.createElement("div"); +const rows = [0, 1, 2].map((i) => { + const tr = document.createElement("tr"); + tr.dataset.i = String(i); + if (i === 1) tr.classList.add("is-pick", "is-pick-top", "is-pick-end"); + root.append(tr); + return tr; +}); +unpickOnRelease(root); +const before = rows.map((t) => t.className); +key("Escape"); +const afterEsc = rows.map((t) => t.className); +rows[1].classList.add("is-pick"); +root.click(); +const afterClick = rows.map((t) => t.className); +console.log(JSON.stringify({ before, afterEsc, afterClick })); +""", + RAW + + """ +E.prototype.querySelectorAll = function () { return this.all("TR").filter((t) => t.cls.has("is-pick")); }; +""", + ) + assert out["before"] == ["", "is-pick is-pick-top is-pick-end", ""] + assert out["afterEsc"] == ["", "", ""] + assert out["afterClick"] == ["", "", ""] + + +def test_비고_빈_줄_없음_Py_TS_짝(): + py = (B09 / "B09_Estimation_Calc_Bill.py").read_text("utf-8") + ts = (B09 / "B09_Estimation_Calc_Bill.ts").read_text("utf-8") + assert "빈 줄 {" not in py and not re.search(r"빈 줄 \$\{", ts) + assert 'row["note"] = "(제외금액)" if row["key"] == "sec:G" else ""' in py + assert 'row.note = row.key === "sec:G" ? "(제외금액)" : ""' in ts