From baeed9d18ecc2283d38af4ef6985f087387a6aba Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 2 Nov 2026 18:13:23 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(738-1):=20M02=20=EB=93=A4=EC=96=B4?= =?UTF-8?q?=EC=98=AC=20=EB=95=8C=20=EB=8B=A8=EA=B0=80=EC=82=B0=EC=B6=9C=20?= =?UTF-8?q?=C2=B7=20=EC=9D=BC=EC=9C=84=EB=8C=80=EA=B0=80=20=3D=20=EA=B7=B8?= =?UTF-8?q?=20=EC=BB=A8=ED=85=8C=EC=9D=B4=EB=84=88=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=B8=B5(=EA=B0=9C=EC=9D=B8)=20=EB=AA=A9=EB=A1=9D=ED=91=9C=20?= =?UTF-8?q?=C2=B7=20=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EA=B8=B8=20=EA=B7=B8=EB=8C=80=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 시험 test_738_m02_entry_personal 추가 · test_m02_precombo_scope 한 건(삭제 확인 글 찾기)은 이 일과 무관한 기존 실패 Co-Authored-By: Claude Sonnet 5.5 --- .../M02_MasterTemplete_UI_Page.ts | 10 +++++++++- .../tester/test_738_m02_entry_personal.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 resources/tester/test_738_m02_entry_personal.py diff --git a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts index 24f9d606c..99cafeb84 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_UI_Page.ts @@ -339,7 +339,15 @@ export async function renderM02MasterTemplate(root: HTMLElement): Promise Date: Mon, 2 Nov 2026 18:25:09 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat(737):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=83=81=EC=84=B8=20=EC=98=B5=EC=85=98=206=20?= =?UTF-8?q?=EC=97=B4=20=EA=B2=A9=EC=9E=90=20=C2=B7=20=ED=95=A9=EA=B3=84=20?= =?UTF-8?q?=ED=95=9C=20=EC=A4=84=20=EB=B0=94=EB=8B=A5=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=20=C2=B7=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=8B=9D=20?= =?UTF-8?q?=EB=B9=84=EB=AA=A9=20=EC=83=81=EC=9E=90=20=C2=B7=20=EB=91=90=20?= =?UTF-8?q?=EB=B3=B4=EA=B8=B0=20=EA=B0=99=EC=9D=80=20=EC=A0=9C=EB=AA=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 옵션 칸 = 상자 폭 기준 1 → 2 → 3 → 4 → 6 열(calc_sheet optionGrid · container query) · 공통 옵션 따로 상자 걷고 모든 상자 맨 위 옵션 칸에 같이 - 맨 아래 노무비 · 재료비 · 경비 · 계 = 한 줄 4 칸 · 화면(모달) 바닥 sticky · 메인 칸 overflow clip(굴림 칸이면 안 붙음) - 텍스트 수식 = 비목 구분 상자 · 상자 위 옵션 칸 · 줄마다 붙던 옵션 꼬리표 걷음 - 두 보기 상자 제목 「단가산출 상세」 · 「지금 입력」 꼬리표 걷음 - 신규 지킴 시험 test_737_option_grid_foot Co-Authored-By: Claude Sonnet 5.5 --- A00_Common/calc_sheet.css | 47 ++++++++ A00_Common/calc_sheet.ts | 11 ++ .../M01_MasterData_UI_LogicLab_Detail.ts | 9 +- .../M01_MasterData_UI_LogicLab_Formula.ts | 100 ++++++++++-------- .../M01_MasterData_UI_LogicLab_Ho.ts | 51 +++++---- .../M01_MasterData_UI_LogicLab_Style.css | 53 +++++++++- resources/tester/test_737_option_grid_foot.py | 46 ++++++++ 7 files changed, 249 insertions(+), 68 deletions(-) create mode 100644 resources/tester/test_737_option_grid_foot.py diff --git a/A00_Common/calc_sheet.css b/A00_Common/calc_sheet.css index 27e4cf53d..264a1c70b 100644 --- a/A00_Common/calc_sheet.css +++ b/A00_Common/calc_sheet.css @@ -17,3 +17,50 @@ p[data-sheet-part] { margin: 0; padding-left: var(--spacing-16); } + +/* 737-1 옵션 칸 격자 — 상자 폭 따라 1 → 2 → 3 → 4 → 6 열(300 · 450 · 600 · 900) · 칸마다 이름 위 · 고르기 아래 */ +.calc-sheet__opts { + container-type: inline-size; +} + +.calc-sheet__opts-grid { + display: grid; + grid-template-columns: minmax(0, 1fr); + gap: var(--spacing-8); +} + +@container (min-width: 300px) { + .calc-sheet__opts-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@container (min-width: 450px) { + .calc-sheet__opts-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@container (min-width: 600px) { + .calc-sheet__opts-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@container (min-width: 900px) { + .calc-sheet__opts-grid { + grid-template-columns: repeat(6, minmax(0, 1fr)); + } +} + +.calc-sheet__opts-grid > .ui-calc-row { + grid-template-columns: minmax(0, 1fr); + align-items: stretch; + gap: var(--spacing-4); +} + +.calc-sheet__opts-grid > .ui-calc-row > :is(.ui-calc-row__select, .ui-calc-row__in, .ui-field) { + grid-column: 1; + justify-self: stretch; + width: 100%; +} diff --git a/A00_Common/calc_sheet.ts b/A00_Common/calc_sheet.ts index 10e0fad4c..4197de7a9 100644 --- a/A00_Common/calc_sheet.ts +++ b/A00_Common/calc_sheet.ts @@ -182,6 +182,17 @@ export function varRow(spec: SheetInput, value: string, onInput: (v: string) => return row; } +/** 옵션 칸들 한 덩이(737-1) — 상자 폭 따라 6 → 4 → 3 → 2 → 1 열 격자(`calc_sheet.css`) · 없으면 빈 목록 */ +export const optionGrid = (rows: HTMLElement[]): HTMLElement[] => + rows.length + ? [ + el("div", { + className: "calc-sheet__opts", + children: [el("div", { className: "calc-sheet__opts-grid", children: rows })], + }), + ] + : []; + /** 장비 · 하위 한 줄 — 노 · 재 · 경 칸 + 합 · 누르면(Enter · Space) 몫 식 줄 펼침 */ function mergedRows(m: SheetMerged): HTMLElement[] { const share = (cost: string): number | null => diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index e6cc7b59e..344337994 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -210,7 +210,7 @@ function hoBox(ctx: DetailContext): HTMLElement { const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...body, totalLine]; const calculated = ctx.lines !== null; const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; - return block(section(tl("Ho_Title"), [nowInputs(ctx.values), ...blocks], hint), "ho"); + return block(section(tl("Ho_Title"), blocks, hint), "ho"); } /** 상세 화면 넷을 `host` 에 쌓음 */ @@ -224,6 +224,7 @@ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? { node: active, from: active.selectionStart, to: active.selectionEnd } : null; + // 737-3 단가산출 상세 · 텍스트 수식 두 보기 상자 제목 같음 · 「지금 입력」 꼬리표 없음 // 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임 · 시험 계산은 숨어도 그려 둠 host.replaceChildren( ...infoBox(ctx), @@ -237,10 +238,8 @@ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { ...(ctx.flowHost && !ctx.cover ? [block(nowInputs(ctx.values), "ho"), ctx.flowHost] : [ - ctx.cover - ? block(section(tl("Ho_Title"), [nowInputs(ctx.values), ctx.cover.ho]), "ho") - : hoBox(ctx), - block(section(tl("Formula_Title"), [formula]), "formula"), + ctx.cover ? block(section(tl("Ho_Title"), [ctx.cover.ho]), "ho") : hoBox(ctx), + block(section(tl("Ho_Title"), [formula]), "formula"), ]), ); const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음 diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts index 6aae21fd9..24af10577 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Formula.ts @@ -3,13 +3,15 @@ * 「텍스트 수식」 컨테이너 — 서버(`POST /text`)가 준 줄을 그대로 찍음(화면에서 다시 계산하지 않음) * 두 칸 — 왼쪽 = 변수 이름 식(`이름글`) · 오른쪽 = 값이 들어간 식(`글`) · 줄은 `짝` 번호로 같은 높이 * 한쪽에 올리거나 누르면 같은 줄이 밝아짐 · 비목 제목 · 소계 · 끝수 한 마디 · 까닭 있는 줄은 옅게 - * 714-3 — 줄이 쓰는 입력(서버 `쓰는입력` · 없으면 표 찾기 행 값 · 이름 조각)을 오른쪽 칸 아래 작은 단추로 · 누르면 그 자리에 고르기/글 칸 → + * 737-2 — 비목(노무비 · 재료비 · 경비)마다 구분 상자 · 맨 위 옵션 고르기(그 상자 줄이 쓰는 입력 · 서버 `쓰는입력` · 없으면 표 찾기 행 값 · 이름 조각 + 어느 줄도 안 쓰는 공통 입력 · 6 → 4 → 3 → 2 → 1 열) → * 바꾸면 `onInput`(시험 계산 입력 고침 + 다시 셈) · 정본은 안 바뀜(요구서 4-1 확정 8) * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; +import { createCard } from "@ui/ui_template_card"; import { createInputField, createSelectField } from "@ui/ui_template_input"; import type { LogicInput, TextAnswer, TextLine } from "./M01_MasterData_UI_Logic_Api"; +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"; @@ -118,43 +120,40 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { ); return; } - const heads = el("div", { - className: "m01lab-fx__head", - children: [ - el("strong", { text: tl("Formula_Left") }), - el("strong", { text: tl("Formula_Right") }), - ], - }); + const heads = (): HTMLElement => + el("div", { + className: "m01lab-fx__head", + children: [ + el("strong", { text: tl("Formula_Left") }), + el("strong", { text: tl("Formula_Right") }), + ], + }); const rows: HTMLElement[] = []; const specs = ctx.onInput ? (ctx.inputs ?? []) : []; - const vars = (line: TextLine): HTMLElement[] => { - const names = specs.map((x) => x.이름); - const seen = new Map(usedInputs(line, names)); - // 서버 「쓰는입력」(714-1 · 조건 · 재료 고르기 · 중간값까지) 이 있으면 그것 · 없으면 조각에서 - const used: [string, string][] = line.쓰는입력 - ? line.쓰는입력.filter((n) => names.includes(n)).map((n) => [n, seen.get(n) ?? ""]) - : [...seen]; - if (!used.length || !ctx.onInput) return []; + // 737-2 옵션 칸 = 비목 상자 맨 위 — 그 상자 줄이 쓰는 입력 + 어느 줄도 안 쓰는 입력(공통 · 737-3) + const names = specs.map((x) => x.이름); + const usedBy = (line: TextLine): string[] => + line.쓰는입력 + ? line.쓰는입력.filter((n) => names.includes(n)) + : usedInputs(line, names).map(([n]) => n); + const everyUsed = new Set(text.groups.flatMap((g) => g.줄.flatMap(usedBy))); + const common = names.filter((n) => !everyUsed.has(n)); + const options = (g: (typeof text.groups)[number]): HTMLElement[] => { const onInput = ctx.onInput; - return [ - el("div", { - className: "m01lab-fx__vars", - children: used.map(([name, was]) => - inputChip( - specs.find((x) => x.이름 === name)!, - ctx.values?.[name] || was, - (v) => onInput(name, v), - ), - ), - }), - ]; + if (!onInput) return []; + const mine = new Set([...g.줄.flatMap(usedBy), ...common]); + return optionGrid( + specs + .filter((x) => mine.has(x.이름)) + .map((x) => varRow(x, ctx.values?.[x.이름] ?? "", (v) => onInput(x.이름, v))), + ); }; const calculated = text.계산됨 !== false; // [계산] 뒤 응답은 표식이 안 붙음 — 미리보기만 false const light = (pair: string, on: boolean): void => rows.forEach((r) => r.dataset.pair === pair && r.classList.toggle("is-on", on)); - const blocks = text.groups.flatMap((g) => [ - el("h4", { className: "m01lab__group m01lab-fx__wide", text: g.비목 }), - ...g.줄.map((line, i) => { + // 737-2 비목마다 구분 상자 — 맨 위 옵션 칸 · 아래 두 칸 식 줄들 · 소계 + const boxes = text.groups.map((g) => { + const lines = g.줄.map((line, i) => { const pair = String(line.짝 ?? `${g.비목}${i}`); const row = el("div", { className: `m01lab-fx__row m01lab__text${line.까닭 ? " m01lab__text--off" : ""}`, @@ -166,7 +165,6 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { children: calculated ? [ el("div", { text: `${line.이름} : ${line.글}` }), - ...vars(line), ...(line.까닭 ? [ el("div", { @@ -189,20 +187,36 @@ export function buildFormula(host: HTMLElement, ctx: FormulaContext): void { }); rows.push(row); return row; - }), + }); + const card = createCard({ + title: g.비목, + pad: 12, + body: [ + ...options(g), + el("div", { + className: "m01lab-fx", + children: [ + heads(), + ...lines, + el("p", { + className: "m01lab__subtotal-text m01lab-fx__wide", + attrs: { "data-text-sub": String(g.소계) }, + text: `${g.비목} ${tl("Subtotal")} ${formatMoney(g.소계)}${g.끝수 ? ` (${g.끝수})` : ""}`, + }), + ], + }), + ], + }); + card.classList.add("m01lab__ho-box"); + card.dataset.fxBox = g.비목; + return card; + }); + host.replaceChildren( + ...boxes, el("p", { - className: "m01lab__subtotal-text m01lab-fx__wide", - attrs: { "data-text-sub": String(g.소계) }, - text: `${g.비목} ${tl("Subtotal")} ${formatMoney(g.소계)}${g.끝수 ? ` (${g.끝수})` : ""}`, - }), - ]); - blocks.unshift(heads); - blocks.push( - el("p", { - className: "m01lab__total m01lab-fx__wide", + className: "m01lab__total", attrs: { "data-text-sum": String(text.계) }, text: `${tl("Total")} ${formatMoney(text.계)}`, }), ); - host.replaceChildren(el("div", { className: "m01lab-fx", children: blocks })); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts index 0cc25a967..f76aa17f8 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Ho.ts @@ -3,9 +3,10 @@ * 공용 「단가산출 상세」(PLAN 722-1 · 요구서 내역16 2장 6 · 7) — M01 단가산출 로직 상세가 씀 · (724) M02 단가산출 · 일위대가 모달도. * 하위 박스 = 서버 `묶음`(인력 · 재료 · 경비(장비) · 하위 — `/text` 줄 덧칸 714-1) · 계산 전 = 호표 종류로 * · 장비 · 하위 로직 줄 = 박스 안 한 줄(노 · 재 · 경 몫 합) + 누르면 펼침(부른 로직 시간당 줄 · 몫 식 줄) - * 박스 제목 바로 아래 = 그 박스 줄들이 쓰는 입력 고르기(`calc_sheet` 변수 칸 꼴) · 어느 박스도 안 쓰는 입력 = 맨 위 「공통 옵션」 + * 박스 제목 바로 아래 = 그 박스 줄들이 쓰는 입력 고르기(`calc_sheet` 변수 칸 꼴 · 6 → 4 → 3 → 2 → 1 열 격자) + + * 어느 박스도 안 쓰는 입력(공통 옵션)도 모든 박스에 같이(737-3 · 한 값 · 어디서 바꿔도 모두 따라감) * 「계산식」 열 = 값 든 식(「268,908 × 0.12」 · 서버 `글`) + 표 · 요소 · 로직 알약 + 그 줄 입력 칩 · 변수 이름 식(`이름글`)은 마우스 올리면 - * 맨 아래 = 비목 소계 셋(노무비 · 재료비 · 경비 — 장비 노무는 여기서 인력과 더해짐) + 계 + * 맨 아래 = 한 줄 4 칸 — 비목 소계 셋(노무비 · 재료비 · 경비 — 장비 노무는 여기서 인력과 더해짐) + 계 · 화면(모달) 바닥에 붙음(737-1) * 줄 누름 = 그 줄 자료 모달(`_LogicLab_Modal.ts`) · 고침 고리(`edit`) 있으면 모달 안 「그 자리 고침」(`_LogicLab_Cover.ts`) * 값 = 서버 셈 그대로(박스 소계 · 비목 소계만 줄 금액 합) · 화면에서 다시 셈 안 함 * ========================================================================== */ @@ -17,7 +18,7 @@ import { el } from "@ui/ui_template_elements"; import { currentLanguageIndex } from "@ui/ui_template_locale"; import { emptyRow, tableRow, tableShell, type Column } from "@ui/ui_template_table"; import { createText } from "@ui/ui_template_text"; -import { sheetFormula, varRow, type SheetUnfold } from "../A00_Common/calc_sheet"; +import { optionGrid, sheetFormula, varRow, type SheetUnfold } from "../A00_Common/calc_sheet"; import type { CalcLine, ElementBrief, @@ -600,7 +601,13 @@ const usesOf = (b: HoBox): string[] => [ ), ]; -function boxCard(b: HoBox, ctx: HoContext, calculated: boolean): HTMLElement { +/** `common` = 어느 박스 줄도 안 쓰는 입력 — 737-3 모든 박스 맨 위 옵션 칸에 같이(한 값 · 어디서 바꿔도 모두 따라감) */ +function boxCard( + b: HoBox, + ctx: HoContext, + calculated: boolean, + common: string[], +): HTMLElement { const name = boxName(b.묶음); const lines = b.줄.flatMap((r) => ("몫" in r ? r.몫 : [r])); const basis = [...new Set(lines.map((e) => e.textLine?.기준단위).filter(Boolean))]; @@ -626,7 +633,7 @@ function boxCard(b: HoBox, ctx: HoContext, calculated: boolean): HTMLElem body: [ ...(basis.length ? [createText(`${th("Basis")} ${basis.join(" · ")}`, "muted")] : []), ...(notes.length ? [createText(notes.join(" · "), "hint")] : []), - ...optionRows(usesOf(b), ctx), + ...optionGrid(optionRows([...usesOf(b), ...common], ctx)), root, ], }); @@ -635,24 +642,21 @@ function boxCard(b: HoBox, ctx: HoContext, calculated: boolean): HTMLElem return card; } -/** 어느 박스 줄도 안 쓰는 입력 — 맨 위 한 카드 */ +/** 박스가 하나도 없을 때만 — 어느 박스 줄도 안 쓰는 입력 한 카드 */ function commonCard(rows: HTMLElement[]): HTMLElement { - const card = createCard({ title: th("Common"), pad: 12, body: rows }); + const card = createCard({ title: th("Common"), pad: 12, body: optionGrid(rows) }); card.classList.add("m01lab__ho-box"); card.dataset.hoBox = "common"; return card; } -/** 공용 단가산출 상세 몸 — 공통 옵션 · 박스들 · 비목 소계 셋 · 계 */ +/** 공용 단가산출 상세 몸 — 박스들(맨 위 옵션 칸 · 공통 옵션 포함) · 맨 아래 한 줄(비목 소계 셋 · 계) */ export function buildHo(ctx: HoContext): HTMLElement[] { const all = entries(ctx); const calculated = ctx.lines !== null; const boxes = boxesOf(all, ["인력", "재료", "경비"]); const used = new Set(boxes.flatMap(usesOf)); - const common = optionRows( - (ctx.row.입력 ?? []).map((s) => s.이름).filter((n) => !used.has(n)), - ctx, - ); + 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", { @@ -666,13 +670,22 @@ export function buildHo(ctx: HoContext): HTMLElement[] { }); const total = all.reduce((s, e) => s + (e.amount ?? 0), 0); return [ - ...(common.length ? [commonCard(common)] : []), - ...boxes.map((b) => boxCard(b, ctx, calculated)), - el("div", { className: "m01lab__sums", children: sums }), - el("p", { - className: "m01lab__total", - attrs: { "data-sum": calculated ? String(total) : "" }, - text: `${tl("Total")} ${calculated ? formatMoney(total) : "—"}`, + ...(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) : "—" }), + ], + }), + ], }), ]; } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index 947469e4c..ed1d313d3 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -241,10 +241,61 @@ gap: var(--spacing-8); } +/* 737-1 맨 아래 한 줄 4 칸(노무비 · 재료비 · 경비 · 계) — 화면(모달) 바닥에 붙음 · 칸마다 이름 위 · 값 아래 */ +.m01lab__foot { + position: sticky; + bottom: 0; + z-index: 2; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--spacing-8); + margin-top: var(--spacing-12); + padding: var(--spacing-8) var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + background: var(--color-surface-raised); +} + +/* 합계 줄이 화면 바닥에 붙으려면 메인 칸이 굴림 칸(overflow:auto)이 아니어야 함 — 메인은 높이가 안 묶여 안 굴렀음 */ +.ui-workflow-layout__main:has(.m01lab__foot) { + overflow: clip; +} + .m01lab__sums { + display: contents; +} + +.m01lab__foot .ui-calc-row { display: flex; flex-direction: column; - gap: var(--spacing-4); + align-items: stretch; + gap: 0; + min-height: 0; +} + +.m01lab__foot .ui-calc-row > :empty { + display: none; +} + +.m01lab__foot .ui-calc-row__out { + padding: 0; + text-align: left; +} + +.m01lab__foot .m01lab__total { + display: flex; + flex-direction: column; + margin: 0; + text-align: left; +} + +.m01lab__foot .m01lab__total > :first-child { + color: var(--color-text-secondary); + font-weight: var(--font-weight-regular, 400); +} + +.m01lab__foot .m01lab__total > :last-child { + font-variant-numeric: tabular-nums; } /* 722-2 줄 모달 「그 자리 고침」 */ diff --git a/resources/tester/test_737_option_grid_foot.py b/resources/tester/test_737_option_grid_foot.py new file mode 100644 index 000000000..66a2ba68e --- /dev/null +++ b/resources/tester/test_737_option_grid_foot.py @@ -0,0 +1,46 @@ +"""737 단가산출 상세 — 옵션 칸 격자 · 합계 한 줄 바닥 · 두 보기 같은 제목 · 비목 상자 지킴 시험(글 대조 · 서버 · 화면 없음).""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +M01 = ROOT / "M01_MasterData" +SHEET_CSS = (ROOT / "A00_Common" / "calc_sheet.css").read_text(encoding="utf-8") +STYLE = (M01 / "M01_MasterData_UI_LogicLab_Style.css").read_text(encoding="utf-8") +HO = (M01 / "M01_MasterData_UI_LogicLab_Ho.ts").read_text(encoding="utf-8") +DETAIL = (M01 / "M01_MasterData_UI_LogicLab_Detail.ts").read_text(encoding="utf-8") +FX = (M01 / "M01_MasterData_UI_LogicLab_Formula.ts").read_text(encoding="utf-8") + + +def test_옵션_격자_폭_따라_6_4_3_2_1(): + cols = re.findall( + r"@container \(min-width: (\d+)px\) \{\s*\.calc-sheet__opts-grid \{\s*grid-template-columns: repeat\((\d), ", + SHEET_CSS, + ) + assert cols == [("300", "2"), ("450", "3"), ("600", "4"), ("900", "6")] + assert "container-type: inline-size" in SHEET_CSS # 창 폭이 아니라 상자 폭 기준 + + +def test_합계_한_줄_바닥_고정(): + foot = STYLE[STYLE.index(".m01lab__foot {") :] + foot = foot[: foot.index("}")] + 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 + + +def test_공통_옵션은_모든_박스에_같이(): + assert "optionRows([...usesOf(b), ...common], ctx)" in HO + assert "commonCard(" in HO and "boxes.length ||" in HO # 박스 없을 때만 따로 카드 + + +def test_두_보기_같은_제목_지금_입력_꼬리표_없음(): + assert 'section(tl("Formula_Title")' not in DETAIL + assert 'section(tl("Ho_Title"), [formula])' in DETAIL + assert "nowInputs(ctx.values), ...blocks" not in DETAIL + + +def test_텍스트_수식은_비목_상자_옵션은_상자_위(): + assert "createCard({" in FX and "fxBox" in FX and "optionGrid(" in FX + assert "...vars(line)" not in FX # 줄마다 붙던 옵션 꼬리표 걷음