feat(M02): 일위대가 보기 갈래 한 줄 표(compact · 카드용) — 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계 · 금액 = 반영 · 없으면 기준 흐리게 · 자재 줄 같은 표 아래(40-1 ①)
- createLaborView(onRow, { compact: true }) · Labor_Rows compactTable · baseQtyText(결과단위 기준 수량) · qtyShort(손 5 ㎥ · B6 = 12.5 · —)
- 400px 이하면 결과 단위 칸 접음 · 300 · 320 · 360 · 420 · 560 에서 가로 넘침 없음
- 시험: test_m02_labor_values 에 수량산출 글 줄
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgChuAcyBqUkhHMTXomQC7
This commit is contained in:
@@ -211,3 +211,56 @@
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
/* 한 줄 표(카드용 · 40-1) — 금액 = 수량 반영 값 · 반영이 없으면 기준 값 흐리게 */
|
||||
.m02-labline {
|
||||
width: 100%;
|
||||
font-size: var(--text-caption);
|
||||
}
|
||||
|
||||
.m02-labline th,
|
||||
.m02-labline td {
|
||||
padding: var(--spacing-4) var(--spacing-6, 6px);
|
||||
}
|
||||
|
||||
.m02-labline th:nth-child(n + 4),
|
||||
.m02-labline .m02-labrow__num {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.m02-labline .m02-labrow__name {
|
||||
display: table-cell;
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.m02-labline .m02-labrow__qty,
|
||||
.m02-labline .m02-labrow__unit {
|
||||
color: var(--color-text-secondary);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.m02-labline .is-base {
|
||||
color: var(--color-text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.m02-labline .m02-labrow__sum td {
|
||||
font-weight: 600;
|
||||
border-top: 2px solid var(--color-border);
|
||||
}
|
||||
|
||||
/* 아주 좁으면(카드 3열 · 400px 이하) 결과 단위 칸을 접음 — 수량산출 글에 단위가 같이 보임 */
|
||||
@container m02-labview (max-width: 400px) {
|
||||
.m02-labline th:nth-child(3),
|
||||
.m02-labline .m02-labrow__unit {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.m02-labline th,
|
||||
.m02-labline td {
|
||||
padding: var(--spacing-4) 3px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,28 @@ export function qtyText(
|
||||
return `${tc("Labor_Qty")} ${cell} = ${v}`;
|
||||
}
|
||||
|
||||
/** 결과단위 기준 수량 — 분모 앞 수 · 분모(「10 ㎡」 · 「1 ㎥」) · 분모 없으면 빈 글 */
|
||||
export function baseQtyText(
|
||||
line: { 배?: number | null; 분모?: string | null } | undefined,
|
||||
): string {
|
||||
if (!line?.분모) return "";
|
||||
return `${num(line.배 || 1)} ${line.분모}`;
|
||||
}
|
||||
|
||||
/** 지금 수량 짧은 글 — 「손 5 ㎥」 · 「B6 = 12.5」 · 「B6 = 빈 칸」 · 없으면 「—」 */
|
||||
export function qtyShort(
|
||||
line: { 수량?: number | null; 수량출처?: string; 알약?: string | null } | undefined,
|
||||
unit: string,
|
||||
cells: ReadonlyMap<string, string> = new Map(),
|
||||
): string {
|
||||
const at = line?.알약 ? cells.get(line.알약) : undefined;
|
||||
if (!line || line.수량 === null || line.수량 === undefined)
|
||||
return at ? `${at} = ${tc("In_QtyEmpty")}` : "—";
|
||||
if (line.수량출처 === "손")
|
||||
return `${tc("Labor_Hand")} ${num(line.수량)}${unit ? ` ${unit}` : ""}`;
|
||||
return `${at ?? tc("In_QtyCell")} = ${num(line.수량)}`;
|
||||
}
|
||||
|
||||
/** 금액 칸 하나 — 위 기준(작게) · 아래 반영 · `label` = 좁은 칸에서 칸 이름(머리 줄 대신) */
|
||||
function moneyCell(
|
||||
base: number | null | undefined,
|
||||
@@ -307,3 +329,114 @@ export function matTable(data: MatTableData, on?: Omit<RowHandlers, "move">): HT
|
||||
? table(heads, trs, [])
|
||||
: el("p", { className: "m02-labrow__muted", text: tc("Labor_NoMat") });
|
||||
}
|
||||
|
||||
export interface CompactData {
|
||||
rows: LaborRow[];
|
||||
mats: LaborMat[];
|
||||
logics: Map<string, ItemLogic>;
|
||||
calc: LaborCalc | null;
|
||||
fail?: string | null;
|
||||
cells?: ReadonlyMap<string, string>;
|
||||
}
|
||||
|
||||
/** 한 줄 표(카드용 · 40-1) — 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계 · 금액 = 수량 반영 값 ·
|
||||
* 반영이 없으면 기준 값 흐리게(`is-base`) · 자재 줄은 같은 표 아래(재료비 · 합계만) · 맨 아래 합계 줄 */
|
||||
export function compactTable(data: CompactData): HTMLElement {
|
||||
const wait = !data.calc && !data.fail;
|
||||
const cell = (v: number | null | undefined, base: boolean, label: string): HTMLElement =>
|
||||
el("td", {
|
||||
className: `m02-labrow__num${base ? " is-base" : ""}`,
|
||||
text: wait ? "…" : v === null || v === undefined ? "—" : formatMoney(v),
|
||||
attrs: { "data-label": label, ...(base ? { title: tc("Labor_Base") } : {}) },
|
||||
});
|
||||
const heads = [
|
||||
tc("Col_Name"),
|
||||
tc("Labor_QtyCol"),
|
||||
tc("Col_Unit"),
|
||||
...COSTS.map((c) => tc(HEAD[c])),
|
||||
];
|
||||
const lines = data.rows.map((row) => {
|
||||
const line = data.calc?.줄.find((l) => l.로직 === row.로직);
|
||||
const money = line?.반영 ?? line?.기준;
|
||||
const base = !line?.반영 && !!line?.기준;
|
||||
const logic = data.logics.get(row.로직);
|
||||
return el("tr", {
|
||||
className: "m02-labrow__row",
|
||||
attrs: { "data-labor-logic": row.로직 },
|
||||
children: [
|
||||
el("td", {
|
||||
className: "m02-labrow__name",
|
||||
text: logic?.이름 ?? row.로직,
|
||||
attrs: { title: `${row.로직}${line?.까닭 ? ` · ${line.까닭}` : ""}` },
|
||||
}),
|
||||
el("td", {
|
||||
className: "m02-labrow__qty",
|
||||
text: qtyShort(line, line?.분모 ?? "", data.cells),
|
||||
attrs: { "data-label": tc("Labor_QtyCol") },
|
||||
}),
|
||||
el("td", { className: "m02-labrow__unit", text: logic?.결과단위 ?? "" }),
|
||||
...COSTS.map((c) => cell(money?.[c], base, tc(HEAD[c]))),
|
||||
],
|
||||
});
|
||||
});
|
||||
const mats = data.mats.map((mat) => {
|
||||
const line = data.calc?.자재.find((m) => m.코드 === mat.코드);
|
||||
const has = line?.반영 !== null && line?.반영 !== undefined;
|
||||
const v = has ? line?.반영 : line?.기준단가;
|
||||
return el("tr", {
|
||||
className: "m02-labrow__row is-mat",
|
||||
attrs: { "data-labor-mat": mat.코드 },
|
||||
children: [
|
||||
el("td", {
|
||||
className: "m02-labrow__name",
|
||||
text: mat.이름 ?? mat.코드,
|
||||
attrs: { title: [mat.코드, mat.규격].filter(Boolean).join(" · ") },
|
||||
}),
|
||||
el("td", {
|
||||
className: "m02-labrow__qty",
|
||||
text: qtyShort(line, mat.단위 ?? "", data.cells),
|
||||
attrs: { "data-label": tc("Labor_QtyCol") },
|
||||
}),
|
||||
el("td", { className: "m02-labrow__unit", text: mat.단위 ?? "" }),
|
||||
el("td", { className: "m02-labrow__num", attrs: { "data-label": tc(HEAD.노무비) } }),
|
||||
cell(v, !has && v !== null && v !== undefined, tc(HEAD.재료비)),
|
||||
el("td", { className: "m02-labrow__num", attrs: { "data-label": tc(HEAD.경비) } }),
|
||||
cell(v, !has && v !== null && v !== undefined, tc(HEAD.계)),
|
||||
],
|
||||
});
|
||||
});
|
||||
const applied = data.calc?.반영합;
|
||||
const anyApplied = !!applied && (applied.계 ?? 0) !== 0;
|
||||
const total = anyApplied ? applied : data.calc?.기준합;
|
||||
const foot =
|
||||
data.rows.length + data.mats.length
|
||||
? [
|
||||
el("tr", {
|
||||
className: "m02-labrow__sum",
|
||||
children: [
|
||||
el("td", { className: "m02-labrow__name", text: tc("Prev_Sum") }),
|
||||
el("td", { className: "m02-labrow__qty", text: "" }),
|
||||
el("td", { className: "m02-labrow__unit", text: "" }),
|
||||
...COSTS.map((c) => cell(total?.[c], !anyApplied && !!total, tc(HEAD[c]))),
|
||||
],
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
return data.rows.length + data.mats.length
|
||||
? el("div", {
|
||||
className: "ui-general-block__table-wrap m02-labcompact",
|
||||
children: [
|
||||
el("table", {
|
||||
className: "ui-general-block__table m02-labline",
|
||||
children: [
|
||||
el("thead", {
|
||||
children: [el("tr", { children: heads.map((t) => el("th", { text: t })) })],
|
||||
}),
|
||||
el("tbody", { children: [...lines, ...mats] }),
|
||||
...(foot.length ? [el("tfoot", { children: foot })] : []),
|
||||
],
|
||||
}),
|
||||
],
|
||||
})
|
||||
: el("p", { className: "m02-labrow__muted", text: tc("NoLogic") });
|
||||
}
|
||||
|
||||
@@ -56,6 +56,8 @@ const TEXT = {
|
||||
Labor_NoMat: ["담은 자재 없음", "No materials yet"],
|
||||
Labor_AddMat: ["자재 추가", "Add material"],
|
||||
Labor_Qty: ["수량", "Quantity"],
|
||||
Labor_QtyCol: ["수량산출", "Quantity"],
|
||||
Labor_Hand: ["손", "Manual"],
|
||||
Labor_Base: ["기준", "Base"],
|
||||
Labor_Applied: ["반영", "Applied"],
|
||||
Labor_BasePrice: ["기준 단가", "Base price"],
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* · `setQty({알약 키: 칸 값}, {알약 키: 칸 주소}?)` = 면적 칸 값이 바뀌면 서버 없이 짝 셈(`Labor_Calc.settle`)으로
|
||||
* 바로 곱함 · 칸 주소를 주면 저장 전 연결(setLink · 알약 끌기)도 「수량 B6 = 20」 으로 보임(안 주면 저장본 연결)
|
||||
* (그 열 면적 알약 값 전부를 줌 — 빠진 키는 「수량 없음」)
|
||||
* · `compact` = 한 줄 표(구조물 조합 일위대가 카드 · 40-1)
|
||||
* · `onRow` = 줄 누름(알약 키 · 이름 · 단위 — 칸 지정 모달 등) · 없으면 누름 없음
|
||||
* ========================================================================== */
|
||||
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
type LaborDoc,
|
||||
} from "./M02_MasterTemplete_Labor_Api";
|
||||
import { settle } from "./M02_MasterTemplete_Labor_Calc";
|
||||
import { logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { compactTable, logicTable, matTable } from "./M02_MasterTemplete_Labor_Rows";
|
||||
import { tc } from "./M02_MasterTemplete_Labor_Text";
|
||||
import "./M02_MasterTemplete_Labor.css";
|
||||
|
||||
@@ -46,7 +47,15 @@ export interface LaborViewHandle {
|
||||
destroy: () => void;
|
||||
}
|
||||
|
||||
export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewHandle {
|
||||
/** `compact` = 한 줄 표(카드용 · 항목 · 수량산출 · 단위 · 금액 넷 · 자재 줄 같은 표 아래 · 줄 누름 없음) */
|
||||
export interface LaborViewOptions {
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
export function createLaborView(
|
||||
onRow?: (row: LaborViewRow) => void,
|
||||
opts: LaborViewOptions = {},
|
||||
): LaborViewHandle {
|
||||
const root = el("div", { className: "m02-labview" });
|
||||
const logics = new Map<string, ItemLogic>();
|
||||
let doc: LaborDoc | null = null;
|
||||
@@ -84,6 +93,10 @@ export function createLaborView(onRow?: (row: LaborViewRow) => void): LaborViewH
|
||||
unit: mat.단위 ?? "",
|
||||
})?.();
|
||||
};
|
||||
if (opts.compact)
|
||||
return void root.replaceChildren(
|
||||
compactTable({ rows: d.담은로직, mats: d.자재, logics, calc, fail, cells }),
|
||||
);
|
||||
root.replaceChildren(
|
||||
el("h4", { className: "m02-labview__title", text: tc("Logics_Title") }),
|
||||
logicTable(
|
||||
|
||||
@@ -127,6 +127,13 @@ function load(f) {
|
||||
const m = load(path.join(root, "M02_MasterTemplete", "M02_MasterTemplete_Labor_Rows.ts"));
|
||||
const cells = new Map([["돌쌓기|㎡", "B6"]]);
|
||||
console.log(JSON.stringify([
|
||||
m.baseQtyText({ 배: 10, 분모: "㎡" }),
|
||||
m.baseQtyText({ 배: 1, 분모: "㎥" }),
|
||||
m.baseQtyText({ 배: 1, 분모: null }),
|
||||
m.qtyShort({ 수량: 5, 수량출처: "손", 알약: "돌쌓기|㎡" }, "㎥", cells),
|
||||
m.qtyShort({ 수량: 12.5, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡", cells),
|
||||
m.qtyShort({ 수량: null, 수량출처: "없음", 알약: "돌쌓기|㎡" }, "㎡", cells),
|
||||
m.qtyShort({ 수량: null, 수량출처: "없음", 알약: "없는|㎡" }, "㎡", cells),
|
||||
m.qtyText({ 수량: 20, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡", cells),
|
||||
m.qtyText({ 수량: 20, 수량출처: "칸", 알약: "돌쌓기|㎡" }, "㎡"),
|
||||
m.qtyText({ 수량: 3, 수량출처: "손", 알약: "돌쌓기|㎡" }, "㎡", cells),
|
||||
@@ -145,7 +152,10 @@ def test_수량_글은_이은_칸_주소를_보임() -> None:
|
||||
[node, "-e", ROWS_SCRIPT, str(ROOT)], capture_output=True, text=True, encoding="utf-8"
|
||||
)
|
||||
assert out.returncode == 0, out.stderr
|
||||
assert json.loads(out.stdout) == [
|
||||
got = json.loads(out.stdout)
|
||||
# 수량산출 열(40-1) — 위 결과단위 기준 수량 · 아래 지금 수량 짧은 글
|
||||
assert got[:7] == ["10 ㎡", "1 ㎥", "", "손 5 ㎥", "B6 = 12.5", "B6 = 빈 칸", "—"]
|
||||
assert got[7:] == [
|
||||
"수량 B6 = 20 ㎡",
|
||||
"수량 면적산출 칸 = 20 ㎡",
|
||||
"수량 손 입력 3 ㎡",
|
||||
|
||||
Reference in New Issue
Block a user