feat(711-1): M01 시험 계산 — 옵션 · 자재 격자 둘 + 수평 구분선 · 기본 4 열 · 자재 이름 누르면 고르기 모달

- 옵션 고르기 · 자재 고르기 사이 709-1 수평 구분선 부품(createDivider)
- 기본 4 열 · 긴 이름 칸 두 칸
- [품목 고르기] 단추 걷음 — 잡힌 품목 이름 링크를 누르면 모달 · 고르면 다시 셈

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-01 15:16:23 +09:00
co-authored by Claude Opus 5.5
parent a0bd269d7f
commit 4953848768
3 changed files with 44 additions and 18 deletions
+11 -2
View File
@@ -28,6 +28,7 @@ import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { safeReason, tx } from "./M01_MasterData_UI_Logic_Text";
import { createBadge } from "@ui/ui_template_badge";
import { createDivider } from "@ui/ui_template_divider";
/** 고르기 값이 마스터 키(자재·기계·직종)면 이름으로 바꿔 보임 — 값 칸은 키 그대로 보냄 */
const KEY_RE = /^[A-Z]{2}\d{6}$/;
@@ -67,6 +68,9 @@ export interface CalcContext {
onText?: (answer: TextAnswer) => void;
}
const grid = (children: HTMLElement[]): HTMLElement =>
el("div", { className: "m01-logic__head", children });
const SUMS = ["노무비", "재료비", "경비", "계"];
/** 로직마다 자동값을 한 번만 받음(다시 그려도 안 부름) */
@@ -122,7 +126,7 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
control = box;
}
const label = spec.단위 ? `${name} (${spec.단위})` : name;
// 긴 이름 줄만 한 칸 다 씀 — 고르기 줄은 짧으면 그대로 2열에 맞춰 나란히
// 긴 이름 줄만 두 칸 차지 — 짧으면 4 열에 맞춰 나란히
const wide = label.length > 10;
return el("label", {
className: `m01-logic__field${wide ? " m01-logic__field--wide" : ""}`,
@@ -196,8 +200,13 @@ export function buildCalc(host: HTMLElement, ctx: CalcContext): void {
runButton.setAttribute("data-calc-run", "");
host.replaceChildren(
el("h3", { text: tx("Calc_Title") }),
// 옵션 고르기 · 자재 고르기 두 격자(기본 4 열) — 사이 수평 구분선(709-1 부품)
...(fields.length || materials.length
? [el("div", { className: "m01-logic__head", children: [...fields, ...materials] })] // 요소 표가 쓰는 auto-fill 그리드 재사용 — 2열
? [
...(fields.length ? [grid(fields)] : []),
...(fields.length && materials.length ? [createDivider()] : []),
...(materials.length ? [grid(materials)] : []),
]
: [el("p", { className: "ui-text ui-text--muted", text: tx("Calc_NoInputs") })]),
runButton,
out,
@@ -1,6 +1,6 @@
/* =============================================================================
* M01_MasterData_UI_Logic_CalcPick.ts
* 시험 계산의 재료 · 기계 고르기 — 재료 줄마다 「잡힌 품목 + 품목 고르기」 · 기계 입력은 단추 하나
* 시험 계산의 재료 · 기계 고르기 — 재료 칸마다 잡힌 품목 이름(누르면 고르기) · 기계 입력은 단추 하나
* 누르면 마스터 표 모달(`PickTable`) · 고른 재료는 `고름`(호표 줄 차례 → 키)에 실어 다시 계산 · 저장 안 함
* ========================================================================== */
@@ -49,10 +49,11 @@ const isMachineInput = (spec: LogicInput): boolean =>
const pickButton = (label: string, onClick: () => void): HTMLButtonElement =>
createButton({ label, variant: "ghost", onClick });
const line = (label: string, current: HTMLElement, button: HTMLElement): HTMLElement =>
/** 자재 칸 — 이름 + 잡힌 품목(누르면 고르기 모달) · 긴 이름은 두 칸 */
const line = (label: string, current: HTMLElement): HTMLElement =>
el("div", {
className: "m01-logic__field m01-logic__field--wide",
children: [el("span", { text: label }), el("div", { children: [current, button] })],
className: `m01-logic__field${label.length > 10 ? " m01-logic__field--wide" : ""}`,
children: [el("span", { text: label }), current],
});
/** 재료 줄마다 한 칸 — 지금 잡힌 품목 + 품목 고르기 */
@@ -80,12 +81,16 @@ export function materialRows(
const seeds = [cond?.["검색어"], cond?.이름, cond?.상세구분, cond?.구분, label]
.map(headWord)
.filter((w, k, all) => w && all.indexOf(w) === k);
const current = el("span", {
className: "ui-text ui-text--muted",
text: `${nowName || tx("Mat_NoPrice")} `,
attrs: { "data-mat-idx": String(i), "data-ref": brief?.ref ?? "" },
// 잡힌 품목 이름 = 링크 단추 — 누르면 고르기 모달([품목 고르기] 단추 걷음)
const current = createButton({
label: nowName || tx("Mat_NoPrice"),
variant: "link",
onClick: () => pick(),
});
const button = pickButton(tx("Pick_Item"), () =>
current.dataset.matIdx = String(i);
current.dataset.ref = brief?.ref ?? "";
current.title = tx("Pick_Item");
const pick = (): void =>
openPickTable({
kind: "재료",
title: `${tx("Pick_MatTitle")} · ${label}`,
@@ -94,12 +99,11 @@ export function materialRows(
isCurrent: (r) => String(r["키"]) === (mine.get(i)?.ref ?? current.dataset.ref),
onPick: (r) => {
mine.set(i, { ref: String(r["키"]), label: nameOf(r) });
current.textContent = `${nameOf(r)} `;
current.textContent = nameOf(r);
again();
},
}),
);
return line(`${label}${item.규격 ? ` ${item.규격}` : ""}`, current, button);
});
return line(`${label}${item.규격 ? ` ${item.규격}` : ""}`, current);
});
}
@@ -115,7 +119,7 @@ export function showItems(
const item = lines[i]?.품목;
if (!item || mine.has(i)) return;
span.dataset.ref = item.키;
span.textContent = `${`${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키} `;
span.textContent = `${item.이름 ?? ""} ${item.규격 ?? ""}`.trim() || item.키;
});
}
@@ -73,11 +73,24 @@
gap: var(--spacing-8);
}
/* 시험 계산 입력만 2열 고정 — 요소 표 쪽(auto-fill)은 그대로 둠
/* 시험 계산 입력만 4열 고정(옵션 · 자재 격자 둘) — 요소 표 쪽(auto-fill)은 그대로 둠 · 긴 이름 칸은 두 칸
실제 그릇은 LogicLab 이 씌우는 `.m01lab__calc`(`.m01-logic__calc` 는 옛 테스트 컨테이너 몫이라 안 걸림) */
.m01lab__calc .m01-logic__head,
.m01-logic__calc .m01-logic__head {
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.m01lab__calc .m01-logic__field--wide {
grid-column: span 2;
}
/* 자재 칸 잡힌 품목 링크 — 칸 폭 안에서 왼쪽 맞춤 · 넘치면 말줄임 */
.m01lab__calc .m01-logic__field > .ui-btn--link {
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.m01-logic__field {