feat(M02): 구조물 조합 일위대가 카드 = 상세 그대로 — 단가산출 로직 2행 표 + 자재목록(Labor_Detail 카드 갈래 · 순서 · 로직 추가 · 다이어그램 · 정보 칸 없음 · 빼기 · 자재 추가 그대로) · 카드 [저장] · Ctrl+S

43-4 — 줄 누름 = 넣을 값 모달 · 수량산출 칸 = 수량 모달(상세와 같은 함수) · 저장 차례 = 면적 먼저 → 일위대가 · 표 감싸개 안 가로 스크롤 · 40-2 한 줄 표(compactTable · Labor_View compact · m02-labline css) 지움 · 빼기만 있는 줄도 끝 칸 · 화면 시나리오 ui_checks/43-4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
This commit is contained in:
2026-10-05 00:33:55 +09:00
co-authored by Claude Opus 5.5
parent 31a753b7fd
commit ca4fcf0364
7 changed files with 195 additions and 262 deletions
@@ -170,7 +170,8 @@ export interface LogicTableData {
export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement {
const wait = !data.calc && !data.fail;
const { move, remove } = on ?? {};
const tools = !!(move && remove);
// 빼기만(구조물 조합 카드 · 43-4)이어도 끝 칸 — 순서 단추는 `move` 가 있을 때만
const tools = !!(move || remove);
const lineOf = (key: string): LaborCalcLine | undefined =>
data.calc?.줄.find((l) => l.로직 === key);
const trs = data.rows.map((row, i) => {
@@ -222,14 +223,15 @@ export function logicTable(data: LogicTableData, on?: RowHandlers): HTMLElement
qtyCell(line, data.cells, on?.qty ? () => on.qty?.(i) : null),
el("td", { className: "m02-labrow__unit", text: unit }),
...COSTS.map((c) => moneyCell(line?.기준?.[c], line?.반영?.[c], wait, tc(HEAD[c]))),
...(move && remove
...(tools
? [
el("td", {
className: "m02-labrow__move",
children: [
small(tc("Up"), () => move(i, -1)),
small(tc("Down"), () => move(i, 1)),
small(tc("Remove"), () => remove(i)),
...(move
? [small(tc("Up"), () => move(i, -1)), small(tc("Down"), () => move(i, 1))]
: []),
...(remove ? [small(tc("Remove"), () => remove(i))] : []),
],
}),
]
@@ -361,115 +363,3 @@ 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") } : {}) },
});
// 머리 글 = 사용자 글 그대로(40-1 · 항목 · 수량산출 · 단위 · 노무비 · 재료비 · 경비 · 합계)
const heads = [
tc("Labor_Item"),
tc("Labor_QtyCol"),
tc("Labor_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") });
}