From b58b01a007f1c1ea4209660afc0c034614a9074d Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 22:20:09 +0900 Subject: [PATCH] =?UTF-8?q?feat(748-3):=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=8B=9D=20=EB=B0=94=EB=8B=A5=20=EB=85=B8=EB=AC=B4?= =?UTF-8?q?=EB=B9=84=20=C2=B7=20=EC=9E=AC=EB=A3=8C=EB=B9=84=20=C2=B7=20?= =?UTF-8?q?=EA=B2=BD=EB=B9=84=20=EA=B3=A0=EC=A0=95=20=EC=A4=84(=EA=B3=84?= =?UTF-8?q?=20=EC=97=86=EC=9D=8C)=20=C2=B7=20=EB=B9=84=EB=AA=A9=20?= =?UTF-8?q?=EC=83=81=EC=9E=90=20=EC=82=AC=EC=9D=B4=20=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?=3D=20=EB=B0=94=EB=8B=A5=20=EC=A4=84=20=EC=9C=84=20=EC=97=AC?= =?UTF-8?q?=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 `LogicLab_Foot` — 바닥 비목 소계 줄 한 부품(단가산출 상세 4 칸 · 텍스트 수식 3 칸 계 칸 없음) · 상자 소계는 그대로 - 비목 상자 사이 간격 = 바닥 줄 위 여유(공통 값 `--m01lab-box-gap` 12px · 두 보기) - 줄마다 붙던 옵션 값 칩 걷음은 748-2 커밋에 함께(위 옵션 칸이 대신) - 낡은 시험 고침: test_737 바닥 줄 부품 · 칸 수 · 상자 간격 대조 Co-Authored-By: Claude Sonnet 5.5 --- .../M01_MasterData_UI_LogicLab_Foot.ts | 59 +++++++++++++++++++ .../M01_MasterData_UI_LogicLab_Formula.ts | 14 ++--- .../M01_MasterData_UI_LogicLab_Ho.ts | 34 +++-------- .../M01_MasterData_UI_LogicLab_Style.css | 16 ++++- resources/tester/test_737_option_grid_foot.py | 5 +- 5 files changed, 92 insertions(+), 36 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_LogicLab_Foot.ts diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Foot.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Foot.ts new file mode 100644 index 000000000..75e6f8605 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Foot.ts @@ -0,0 +1,59 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Foot.ts + * 화면(모달) 바닥에 붙는 비목 소계 줄(737-1 · PLAN 748-3) — 단가산출 상세(노무비 · 재료비 · 경비 · 계 네 칸) · + * 텍스트 수식(노무비 · 재료비 · 경비 세 칸 · 계 칸 없음 — 중복 표기) 두 보기가 같이 씀. + * 값 = 서버 셈 그대로(여기서 다시 셈 안 함) · 계산 전 「—」. + * ========================================================================== */ + +import { createCalcFormulaRow } from "@ui/ui_template_calc_row"; +import { el } from "@ui/ui_template_elements"; +import { formatMoney } from "./M01_MasterData_UI_Logic_Money"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; + +export type FootKey = "labor" | "material" | "cost"; +const LABEL: Record = { labor: "노무비", material: "재료비", cost: "경비" }; +export const FOOT_KEYS = Object.keys(LABEL) as FootKey[]; +/** 서버 비목 이름 → 칸 */ +export const FOOT_OF: Record = { + 노무비: "labor", + 재료비: "material", + 경비: "cost", +}; + +/** 비목 소계 줄 — `total` 을 주면 맨 끝 「계」 칸까지 네 칸(주지 않으면 세 칸) */ +export function buildFoot( + sums: Record, + calculated: boolean, + total?: number, +): HTMLElement { + const cells = FOOT_KEYS.map((k) => { + const out = el("span", { + className: "ui-calc-row__out is-key", + text: calculated ? formatMoney(sums[k]) : "—", + }); + const row = createCalcFormulaRow(LABEL[k], null, out); + row.dataset.group = k; + row.dataset.subtotal = calculated ? String(sums[k]) : ""; + return row; + }); + const foot = el("div", { + className: "m01lab__foot", + children: [ + el("div", { className: "m01lab__sums", children: cells }), + ...(total === undefined + ? [] + : [ + el("p", { + className: "m01lab__total", + attrs: { "data-sum": calculated ? String(total) : "" }, + children: [ + el("span", { text: `${tl("Total")} ` }), + el("span", { text: calculated ? formatMoney(total) : "—" }), + ], + }), + ]), + ], + }); + if (total === undefined) foot.dataset.cols = "3"; + return foot; +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts index 99523db45..ae4bf7c60 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -15,6 +15,7 @@ import { optionGrid, varRow } from "../A00_Common/calc_sheet"; import { formatMoney } from "./M01_MasterData_UI_Logic_Money"; import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; +import { buildFoot, FOOT_KEYS, FOOT_OF, type FootKey } from "./M01_MasterData_UI_LogicLab_Foot"; import { nameTokens, tokenRow, valueTokens } from "./M01_MasterData_UI_LogicLab_FormulaTokens"; import "./M01_MasterData_UI_LogicLab_Formula.css"; @@ -223,12 +224,9 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { card.dataset.fxBox = g.비목; return card; }); - host.replaceChildren( - ...boxes, - el("p", { - className: "m01lab__total", - attrs: { "data-text-sum": String(text.계) }, - text: `${tl("Total")} ${formatMoney(text.계)}`, - }), - ); + // 748-3 맨 아래 비목 소계 고정 줄(노무비 · 재료비 · 경비 — 계 칸 없음 · 상자 소계는 그대로) + const sums = Object.fromEntries( + FOOT_KEYS.map((k) => [k, text.groups.find((g) => FOOT_OF[g.비목] === k)?.소계 ?? 0]), + ) as Record; + host.replaceChildren(...boxes, buildFoot(sums, calculated)); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts index 754d8e28c..9e2c32258 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts @@ -12,7 +12,6 @@ * ========================================================================== */ import { createBadge } from "@ui/ui_template_badge"; -import { createCalcFormulaRow } from "@ui/ui_template_calc_row"; import { createCard } from "@ui/ui_template_card"; import { el } from "@ui/ui_template_elements"; import { currentLanguageIndex } from "@ui/ui_template_locale"; @@ -44,6 +43,7 @@ import { openTableModal, type PickCond, } from "./M01_MasterData_UI_LogicLab_Modal"; +import { buildFoot, FOOT_KEYS, type FootKey } from "./M01_MasterData_UI_LogicLab_Foot"; import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; @@ -653,35 +653,17 @@ export function buildHo(ctx: HoContext): HTMLElement[] { const boxes = boxesOf(all, ["인력", "재료", "경비"]); const used = new Set(boxes.flatMap(usesOf)); const common = (ctx.row.입력 ?? []).map((s) => s.이름).filter((n) => !used.has(n)); - const sums = (["labor", "material", "cost"] as Group[]).map((g) => { - const v = all.filter((e) => e.group === g).reduce((s, e) => s + (e.amount ?? 0), 0); - const out = el("span", { - className: "ui-calc-row__out is-key", - text: calculated ? formatMoney(v) : "—", - }); - const row = createCalcFormulaRow(COST_OF[g], null, out); - row.dataset.group = g; - row.dataset.subtotal = calculated ? String(v) : ""; - return row; - }); + const sums = Object.fromEntries( + FOOT_KEYS.map((g) => [ + g, + all.filter((e) => e.group === g).reduce((s, e) => s + (e.amount ?? 0), 0), + ]), + ) as Record; const total = all.reduce((s, e) => s + (e.amount ?? 0), 0); return [ ...(boxes.length || !common.length ? [] : [commonCard(optionRows(common, ctx))]), ...boxes.map((b) => boxCard(b, ctx, calculated, common)), // 737-1 맨 아래 한 줄 4 칸(노무비 · 재료비 · 경비 · 계) — 화면(모달) 바닥에 붙음 - el("div", { - className: "m01lab__foot", - children: [ - el("div", { className: "m01lab__sums", children: sums }), - el("p", { - className: "m01lab__total", - attrs: { "data-sum": calculated ? String(total) : "" }, - children: [ - el("span", { text: `${tl("Total")} ` }), - el("span", { text: calculated ? formatMoney(total) : "—" }), - ], - }), - ], - }), + buildFoot(sums, calculated, total), ]; } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index 43d0a8ac4..e1902999b 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -256,7 +256,7 @@ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--spacing-8); - margin-top: var(--spacing-12); + margin-top: var(--m01lab-box-gap, var(--spacing-12)); padding: var(--spacing-8) var(--spacing-12); border: 1px solid var(--color-border); border-radius: var(--radius-lg); @@ -272,6 +272,20 @@ display: contents; } +/* 748-3 텍스트 수식 바닥 줄 = 세 칸(계 칸 없음) */ +.m01lab__foot[data-cols="3"] { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +/* 748-3 비목 상자 사이 간격 = 바닥 고정 줄 위 여유(같은 값 · 두 보기) */ +.m01-logic__editor { + --m01lab-box-gap: var(--spacing-12); +} + +.m01lab__ho-box + .m01lab__ho-box { + margin-top: var(--m01lab-box-gap, var(--spacing-12)); +} + .m01lab__foot .ui-calc-row { display: flex; flex-direction: column; diff --git a/resources/tester/test_737_option_grid_foot.py b/resources/tester/test_737_option_grid_foot.py index 66a2ba68e..e04d13368 100644 --- a/resources/tester/test_737_option_grid_foot.py +++ b/resources/tester/test_737_option_grid_foot.py @@ -27,7 +27,10 @@ def test_합계_한_줄_바닥_고정(): assert "position: sticky" in foot and "bottom: 0" in foot assert "repeat(4, minmax(0, 1fr))" in foot assert "overflow: clip" in STYLE # 메인 칸이 굴림 칸이면 바닥에 안 붙음 - assert 'className: "m01lab__foot"' in HO + foot_ts = (M01 / "M01_MasterData_UI_LogicLab_Foot.ts").read_text(encoding="utf-8") + assert 'className: "m01lab__foot"' in foot_ts # 748-3 두 보기가 같은 바닥 줄 부품 + assert "buildFoot(sums, calculated, total)" in HO and "buildFoot(sums, calculated)" in FX + assert 'data-cols="3"' in STYLE and ".m01lab__ho-box + .m01lab__ho-box" in STYLE def test_공통_옵션은_모든_박스에_같이():