feat(737): 단가산출 상세 옵션 6 열 격자 · 합계 한 줄 바닥 고정 · 텍스트 수식 비목 상자 · 두 보기 같은 제목

- 옵션 칸 = 상자 폭 기준 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 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 18:25:09 +09:00
co-authored by Claude Sonnet 5.5
parent 0ae48d0a89
commit 10984109b4
7 changed files with 249 additions and 68 deletions
+32 -19
View File
@@ -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<Entry>): string[] => [
),
];
function boxCard(b: HoBox<Entry>, ctx: HoContext, calculated: boolean): HTMLElement {
/** `common` = 어느 박스 줄도 안 쓰는 입력 — 737-3 모든 박스 맨 위 옵션 칸에 같이(한 값 · 어디서 바꿔도 모두 따라감) */
function boxCard(
b: HoBox<Entry>,
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<Entry>, 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<Entry>, 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) : "—" }),
],
}),
],
}),
];
}