feat(m01): 단가산출 상세 수량 칸 알약화 — 표 찾기만 알약 · 나머지는 글자
수량 칸을 「알약 + 사칙연산 글자」로 그림 — 찾기(...) 표 호출만 알약(조각 계약 `ref/_분석_알약_상세줄.md` ㉮ 표대로) · 나머지(숫자·연산기호·중간값·설계입력 이름)는 글자. 서버 조각 API 는 아직이라 화면에서 임시로 쪼갬(Logic_Note.ts 찾기() 규칙과 같음) — 서버가 붙으면 걷어냄. 단가·금액 칸은 안 건드림. 알약을 누르면 그 표만 바로 모달로(모달 안 알약 목록은 없앰) · 줄을 누르면 그 줄의 단가·다른 로직을 바로 보임(알약 없이 한 가지만). 인력·재료·경비 세 표를 table-layout: fixed + 같은 칸 폭으로 맞춤 · 알약은 칸 폭 기준 줄바꿈. 원 단위 소수점 0 — 돈 자리(단가·금액·소계·계)만 정수 · 수량·비율은 그대로 · 조합 미리 보기(Combo_Preview)도 같이 고침. 검증: typecheck 통과 · ORCA 로 13-4-1·12-4·12-17-1 열어 확인(알약 클릭·행 클릭·칸 폭 동일 729.57px·정수 표시 확인, data-sum 은 원래 값 그대로 유지). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1MKKZKpUHTPKb513FneU8
This commit is contained in:
@@ -17,7 +17,8 @@ const LABEL: Record<(typeof COSTS)[number], ComboTextKey> = {
|
||||
};
|
||||
|
||||
const num = (v: string): number | string => (v.trim() !== "" && !isNaN(Number(v)) ? Number(v) : v);
|
||||
const fmt = (n: number): string => n.toLocaleString("ko-KR", { maximumFractionDigits: 4 });
|
||||
/** 원 단위 소수점 0 — 조합 미리 보기는 모두 돈 자리(노무비·재료비·경비·계) */
|
||||
const fmt = (n: number): string => n.toLocaleString("ko-KR", { maximumFractionDigits: 0 });
|
||||
|
||||
/** 로직 입력 정의 → 처음 값(고르기 첫째 · 범위 아래끝) */
|
||||
const initial = (i: LogicInput): string => String(i.고르기?.[0] ?? i.범위?.[0] ?? "");
|
||||
|
||||
@@ -19,9 +19,53 @@ import { buildEditor, formatNumber, qtyKind } from "./M01_MasterData_UI_Logic_Ed
|
||||
import { openPicker } from "./M01_MasterData_UI_Logic_Pick";
|
||||
import { tx } from "./M01_MasterData_UI_Logic_Text";
|
||||
import { buildFormula } from "./M01_MasterData_UI_LogicLab_Formula";
|
||||
import { openMaterialModal } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { openMaterialModal, openTableModal } from "./M01_MasterData_UI_LogicLab_Modal";
|
||||
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Style.css";
|
||||
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
||||
|
||||
/** 원 단위 소수점 0 — 돈 자리(단가·금액·소계·계)만. 수량·비율은 `formatNumber` 그대로 둠. */
|
||||
const formatMoney = (value: unknown): string =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 })
|
||||
: formatNumber(value);
|
||||
|
||||
/** 조각 계약(`ref/_분석_알약_상세줄.md` ㉮ 표) — 수량 식 안 찾기(...) 표 호출만 알약,
|
||||
* 그 밖(숫자·연산기호·중간값·설계입력 이름)은 글자. 서버가 조각 목록을 아직 안 주므로
|
||||
* 여기서 임시로 쪼갬(`Logic_Note.ts` 찾기() 정규식과 같은 규칙) — 서버가 붙으면 이 파싱을 걷어냄. */
|
||||
const FIND_RE = /찾기\(\s*([A-Za-z0-9_]+)\s*,([^)]*)\)\s*\.\s*([A-Za-z0-9_가-힣]+)/g;
|
||||
|
||||
function splitFindConds(raw: string): [string, string][] {
|
||||
return raw
|
||||
.split(",")
|
||||
.map((p) => p.trim())
|
||||
.filter(Boolean)
|
||||
.map((p): [string, string] => {
|
||||
const i = p.indexOf("=");
|
||||
return i < 0 ? [p, p] : [p.slice(0, i).trim(), p.slice(i + 1).trim()];
|
||||
});
|
||||
}
|
||||
|
||||
function qtyFragments(expr: string, values: Record<string, string>): Fragment[] {
|
||||
const out: Fragment[] = [];
|
||||
let last = 0;
|
||||
for (const m of expr.matchAll(FIND_RE)) {
|
||||
const at = m.index ?? 0;
|
||||
if (at > last) out.push({ text: expr.slice(last, at) });
|
||||
const table = m[1];
|
||||
const label = `${tl("Pill_Table")} ${table}`;
|
||||
out.push({
|
||||
pill: {
|
||||
label,
|
||||
onClick: () => openTableModal({ title: label, table, conds: splitFindConds(m[2]), values }),
|
||||
},
|
||||
});
|
||||
last = at + m[0].length;
|
||||
}
|
||||
if (last < expr.length) out.push({ text: expr.slice(last) });
|
||||
return out.length ? out : [{ text: expr }];
|
||||
}
|
||||
|
||||
export interface DetailContext {
|
||||
row: LogicRow;
|
||||
@@ -56,6 +100,7 @@ interface Entry {
|
||||
unit: string;
|
||||
qty: string;
|
||||
qtyTag: string;
|
||||
qtyFrag: Fragment[];
|
||||
price: string;
|
||||
amount: number | null;
|
||||
/** 덧줄이면 true */
|
||||
@@ -168,7 +213,8 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
unit: item.단위 ?? "",
|
||||
qty: item.수량,
|
||||
qtyTag: qtyKind(item.수량) === tx("Ho_QtyTable") ? tl("Tag_Table") : qtyKind(item.수량),
|
||||
price: price === undefined || price === null ? "" : formatNumber(price),
|
||||
qtyFrag: qtyFragments(item.수량, ctx.values),
|
||||
price: price === undefined || price === null ? "" : formatMoney(price),
|
||||
amount: line ? line.금액 : null,
|
||||
extra: false,
|
||||
open: () =>
|
||||
@@ -192,6 +238,7 @@ function entries(ctx: DetailContext): Entry[] {
|
||||
unit: "",
|
||||
qty: extra.식,
|
||||
qtyTag: tl("Extra"),
|
||||
qtyFrag: qtyFragments(extra.식, ctx.values),
|
||||
price: "",
|
||||
amount: line ? line.금액 : null,
|
||||
extra: true,
|
||||
@@ -224,13 +271,14 @@ function lineRow(e: Entry): HTMLElement {
|
||||
}),
|
||||
cellOf(e.unit),
|
||||
el("td", {
|
||||
className: "m01lab__qty-cell",
|
||||
children: [
|
||||
el("span", { className: "m01-logic__tag", text: e.qtyTag }),
|
||||
el("span", { className: "m01lab__qty", text: ` ${e.qty}` }),
|
||||
fragmentRow(e.qtyFrag),
|
||||
],
|
||||
}),
|
||||
cellOf(e.price, "m01-logic__money"),
|
||||
cellOf(e.amount === null ? "" : formatNumber(e.amount), "m01-logic__money"),
|
||||
cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"),
|
||||
],
|
||||
});
|
||||
tr.addEventListener("click", e.open);
|
||||
@@ -254,7 +302,7 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme
|
||||
attrs: { "data-group": group, "data-subtotal": calculated ? String(sub) : "" },
|
||||
children: [
|
||||
el("td", { attrs: { colspan: "4" }, text: `${groupName(group)} ${tl("Subtotal")}` }),
|
||||
cellOf(calculated ? formatNumber(sub) : "—", "m01-logic__money"),
|
||||
cellOf(calculated ? formatMoney(sub) : "—", "m01-logic__money"),
|
||||
],
|
||||
});
|
||||
const headers = [
|
||||
@@ -326,7 +374,7 @@ function hoBox(ctx: DetailContext): HTMLElement {
|
||||
const totalLine = el("p", {
|
||||
className: "m01lab__total",
|
||||
attrs: { "data-sum": calculated ? String(total) : "" },
|
||||
text: `${tl("Total")} ${calculated ? formatNumber(total) : "—"}`,
|
||||
text: `${tl("Total")} ${calculated ? formatMoney(total) : "—"}`,
|
||||
});
|
||||
// 자체 로직 — 고치는 자리는 편집기 하나 · 읽기용 묶음 표는 접어 둠(펼치면 소계가 보임)
|
||||
const fold = el("details", {
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Modal.ts
|
||||
* 호표 줄 자료 모달 — 그 줄이 쓰는 자료를 보임
|
||||
* 표(소요량·계수)는 표 그대로 + 지금 넣은 값으로 걸린 줄 강조 · 단일 값(단가)은 값과 출처
|
||||
* 자료 모달 둘 —
|
||||
* ① 줄 모달(`openMaterialModal`) — 줄을 누르면 그 줄의 단가(재료·인력·기계 또는 다른 로직)를
|
||||
* 바로 보임(알약 목록 없이) · 그 아래 한 줄 풀이 + 원문 수량 식
|
||||
* ② 표 모달(`openTableModal`) — 수량 칸 안 알약(찾기(...) 표 참조)을 누르면 그 표만 보임
|
||||
* 표는 표 그대로 + 지금 넣은 값으로 걸린 줄 강조 · 단일 값(단가)은 값과 출처
|
||||
* 표 읽기 = `/elements` → `/table`(`Logic_Note.loadTable`) · 단가 = 로직 화면이 이미 받은 `prices`
|
||||
* ========================================================================== */
|
||||
|
||||
import { createButton, el } from "@ui/ui_template_elements";
|
||||
import type { ElementBrief, NamedFormula } from "./M01_MasterData_UI_Logic_Api";
|
||||
import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
|
||||
import {
|
||||
explain,
|
||||
loadTable,
|
||||
matchRow,
|
||||
relatedFinds,
|
||||
type RelatedFind,
|
||||
type TableRow,
|
||||
} from "./M01_MasterData_UI_Logic_Note";
|
||||
import { explain, loadTable, matchRow, type TableRow } from "./M01_MasterData_UI_Logic_Note";
|
||||
import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side";
|
||||
import { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill";
|
||||
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
|
||||
import "./M01_MasterData_UI_LogicLab_Pill.css";
|
||||
|
||||
/** 원 단위 소수점 0 — 돈 자리만(수량·비율은 `formatNumber` 그대로) */
|
||||
const formatMoney = (value: unknown): string =>
|
||||
typeof value === "number"
|
||||
? value.toLocaleString("ko-KR", { maximumFractionDigits: 0 })
|
||||
: formatNumber(value);
|
||||
|
||||
export interface ModalTarget {
|
||||
title: string;
|
||||
@@ -33,19 +33,25 @@ export interface ModalTarget {
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
/** 수량 칸 표 알약 하나가 여는 자료 — 조각 계약의 「표 찾기」 자리 */
|
||||
export interface TableTarget {
|
||||
title: string;
|
||||
table: string;
|
||||
conds: [string, string][];
|
||||
values: Record<string, string>;
|
||||
}
|
||||
|
||||
function tableView(
|
||||
find: RelatedFind,
|
||||
table: TableRow,
|
||||
conds: [string, string][],
|
||||
values: Record<string, string>,
|
||||
): HTMLElement {
|
||||
// 표 줄의 빈 칸은 「무엇이든」 — 서버 찾기()와 같게 그 칸은 조건에서 뺌
|
||||
// 표 줄의 빈 칸은 「무엇이든」 — 서버 찾기()와 같게 그 칸은 조건에서 뺌(줄마다 따로 봄)
|
||||
const hit =
|
||||
(table.줄 ?? []).find((line) =>
|
||||
matchRow(
|
||||
{ ...table, 줄: [line] },
|
||||
find.find.conds.filter(
|
||||
([col]) => line[col] !== undefined && line[col] !== null && line[col] !== "",
|
||||
),
|
||||
conds.filter(([col]) => line[col] !== undefined && line[col] !== null && line[col] !== ""),
|
||||
{ values, middle: {} },
|
||||
),
|
||||
) ?? null;
|
||||
@@ -71,7 +77,7 @@ function tableView(
|
||||
}),
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${find.source} → ${find.find.col} · ${hit ? tl("Modal_Hit") : tl("Modal_NoHit")}`,
|
||||
text: hit ? tl("Modal_Hit") : tl("Modal_NoHit"),
|
||||
}),
|
||||
goButton({ kind: "table", key: table.키 }),
|
||||
el("div", {
|
||||
@@ -117,7 +123,7 @@ function priceView(ref: string, brief: ElementBrief): HTMLElement {
|
||||
? [el("dt", { text: tl("Modal_Spec") }), el("dd", { text: brief.규격 })]
|
||||
: []),
|
||||
el("dt", { text: tl("Modal_Value") }),
|
||||
el("dd", { className: "m01-logic__money", text: formatNumber(brief.값) }),
|
||||
el("dd", { className: "m01-logic__money", text: formatMoney(brief.값) }),
|
||||
...(brief.file
|
||||
? [el("dt", { text: tl("Modal_Source") }), el("dd", { text: brief.file })]
|
||||
: []),
|
||||
@@ -129,18 +135,18 @@ function priceView(ref: string, brief: ElementBrief): HTMLElement {
|
||||
});
|
||||
}
|
||||
|
||||
/** 모달을 엶 */
|
||||
export function openMaterialModal(target: ModalTarget): void {
|
||||
/** 모달 겉틀(배경·닫기·포커스) — 몸은 `mount` 이 채움 */
|
||||
function openDialog(title: string, mount: (body: HTMLElement) => void): void {
|
||||
const body = el("div", { className: "m01lab__modal-body" });
|
||||
const close = (): void => backdrop.remove();
|
||||
const dialog = el("div", {
|
||||
className: "m01-logic__pick m01lab__modal",
|
||||
attrs: { role: "dialog", "aria-label": target.title },
|
||||
attrs: { role: "dialog", "aria-label": title },
|
||||
children: [
|
||||
el("div", {
|
||||
className: "m01lab__modal-top",
|
||||
children: [
|
||||
el("h3", { text: target.title }),
|
||||
el("h3", { text: title }),
|
||||
createButton({ label: tl("Modal_Close"), variant: "ghost", onClick: close }),
|
||||
],
|
||||
}),
|
||||
@@ -150,64 +156,62 @@ export function openMaterialModal(target: ModalTarget): void {
|
||||
const backdrop = el("div", { className: "m01-logic__backdrop", children: [dialog] });
|
||||
backdrop.addEventListener("click", (ev) => ev.target === backdrop && close());
|
||||
backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close());
|
||||
|
||||
const finds = relatedFinds(target.expr, target.middles);
|
||||
const base = target.element?.split(".")[0] ?? "";
|
||||
const price = target.element ? (target.prices[target.element] ?? target.prices[base]) : undefined;
|
||||
const logicRef = target.element?.startsWith("로직(") ? target.element : "";
|
||||
const pills: Pill[] = [];
|
||||
if (price && target.element) {
|
||||
pills.push({
|
||||
label: `${groupOf(base)} ${price.이름 ?? base}`.trim(),
|
||||
view: () => priceView(target.element as string, price),
|
||||
});
|
||||
} else if (target.element && !logicRef) {
|
||||
pills.push({
|
||||
label: `${groupOf(base)} ${target.title}`.trim(),
|
||||
view: () =>
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${target.element} — ${tl(target.element?.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if (logicRef) {
|
||||
pills.push({
|
||||
label: `${tl("Pill_Logic")} ${logicRef.slice(3).split(",")[0].replace(/\)$/, "")}`,
|
||||
view: () => el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }),
|
||||
});
|
||||
}
|
||||
for (const f of finds) {
|
||||
pills.push({
|
||||
label: `${tl("Pill_Table")} ${f.find.table}`,
|
||||
view: async () => {
|
||||
const table = await loadTable(f.find.table);
|
||||
return table
|
||||
? tableView(f, table, target.values)
|
||||
: el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` });
|
||||
},
|
||||
});
|
||||
}
|
||||
const unit = price ? `${tl("Modal_UnitPrice")} ${formatNumber(price.값)} × ` : "";
|
||||
const parts: HTMLElement[] = [
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }),
|
||||
el("p", {
|
||||
className: "m01lab__sum",
|
||||
text: `${target.title} = ${unit}${explain(target.expr)}`,
|
||||
}),
|
||||
...(pills.length
|
||||
? [el("p", { className: "m01-logic__muted", text: tl("Pill_Hint") }), pillBar(pills)]
|
||||
: [el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") })]),
|
||||
el("details", {
|
||||
className: "m01lab__raw",
|
||||
children: [
|
||||
el("summary", { text: tl("Modal_Qty") }),
|
||||
el("pre", { className: "m01lab__expr", text: target.expr }),
|
||||
],
|
||||
}),
|
||||
];
|
||||
body.replaceChildren(...parts);
|
||||
mount(body);
|
||||
document.body.append(backdrop);
|
||||
dialog.tabIndex = -1;
|
||||
dialog.focus();
|
||||
}
|
||||
|
||||
/** 줄을 눌렀을 때 — 그 줄의 단가(재료·인력·기계 또는 다른 로직)를 바로 보임(알약 없이) */
|
||||
export function openMaterialModal(target: ModalTarget): void {
|
||||
openDialog(target.title, (body) => {
|
||||
const base = target.element?.split(".")[0] ?? "";
|
||||
const price = target.element
|
||||
? (target.prices[target.element] ?? target.prices[base])
|
||||
: undefined;
|
||||
const logicRef = target.element?.startsWith("로직(") ? target.element : "";
|
||||
const rateParts: HTMLElement[] = [];
|
||||
if (price && target.element) {
|
||||
rateParts.push(priceView(target.element, price));
|
||||
} else if (logicRef) {
|
||||
rateParts.push(el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }));
|
||||
} else if (target.element) {
|
||||
rateParts.push(
|
||||
el("p", {
|
||||
className: "m01-logic__muted",
|
||||
text: `${target.element} — ${tl(target.element.includes("{") ? "Pill_NoPick" : "Pill_NoValue")}`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const unit = price ? `${tl("Modal_UnitPrice")} ${formatMoney(price.값)} × ` : "";
|
||||
body.replaceChildren(
|
||||
...rateParts,
|
||||
el("p", { className: "m01-logic__muted", text: tl("Modal_Sum") }),
|
||||
el("p", {
|
||||
className: "m01lab__sum",
|
||||
text: `${target.title} = ${unit}${explain(target.expr)}`,
|
||||
}),
|
||||
el("details", {
|
||||
className: "m01lab__raw",
|
||||
children: [
|
||||
el("summary", { text: tl("Modal_Qty") }),
|
||||
el("pre", { className: "m01lab__expr", text: target.expr }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** 수량 칸 표 알약을 눌렀을 때 — 그 표만 보임(걸린 줄 강조) */
|
||||
export function openTableModal(target: TableTarget): void {
|
||||
openDialog(target.title, (body) => {
|
||||
body.replaceChildren(el("p", { className: "m01-logic__muted", text: "…" }));
|
||||
void loadTable(target.table).then((table) => {
|
||||
body.replaceChildren(
|
||||
table
|
||||
? tableView(table, target.conds, target.values)
|
||||
: el("p", { className: "m01-logic__bad", text: `${target.table} ?` }),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,44 +1,44 @@
|
||||
/* =============================================================================
|
||||
* M01_MasterData_UI_LogicLab_Pill.ts
|
||||
* 줄 모달의 알약 단추 — 쓰인 자료를 알약으로만 늘어놓고, 누른 것 하나만 펼침(다시 누르면 접힘)
|
||||
* 알약 단추 — 수량 칸 안 조각(표 찾기 등)을 누르면 곧장 그 자료 모달을 엶.
|
||||
* (이전에는 알약을 늘어놓고 아래 자리에서 펼쳤으나, 알약이 수량 칸 안으로 들어가며
|
||||
* 「누르면 모달」 로 바뀜 — 모달 안에 알약 목록을 또 두지 않음.)
|
||||
* ========================================================================== */
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
|
||||
export interface Pill {
|
||||
/** 알약 글 — 보기 「표 QF000421」 · 「인력 석공」 */
|
||||
/** 알약 글 — 보기 「표 QF000421」 */
|
||||
label: string;
|
||||
/** 펼칠 때 그림 — 표처럼 읽어 와야 하면 Promise */
|
||||
view: () => HTMLElement | Promise<HTMLElement>;
|
||||
/** 누르면 할 일 — 그 자료 모달을 엶 */
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export function pillBar(pills: Pill[]): HTMLElement {
|
||||
const stage = el("div", { className: "m01lab__pill-stage" });
|
||||
const buttons: HTMLButtonElement[] = [];
|
||||
let open = -1;
|
||||
let turn = 0;
|
||||
const show = async (i: number): Promise<void> => {
|
||||
const mine = ++turn;
|
||||
open = open === i ? -1 : i;
|
||||
buttons.forEach((b, k) => {
|
||||
b.setAttribute("aria-expanded", String(k === open));
|
||||
b.classList.toggle("is-open", k === open);
|
||||
});
|
||||
if (open < 0) return stage.replaceChildren();
|
||||
const view = await pills[open].view();
|
||||
if (mine === turn) stage.replaceChildren(view); // 빨리 눌러도 마지막 것만
|
||||
};
|
||||
pills.forEach((p, i) => {
|
||||
const b = el("button", {
|
||||
className: "m01lab__pill",
|
||||
text: p.label,
|
||||
attrs: { type: "button", "aria-expanded": "false", "data-pill": p.label },
|
||||
});
|
||||
b.addEventListener("click", () => void show(i));
|
||||
buttons.push(b);
|
||||
/** 알약 하나 — 글자 조각 사이에 끼워 쓰는 단추 */
|
||||
export function pillButton(p: Pill): HTMLButtonElement {
|
||||
const b = el("button", {
|
||||
className: "m01lab__pill",
|
||||
text: p.label,
|
||||
attrs: { type: "button", "data-pill": p.label },
|
||||
});
|
||||
return el("div", {
|
||||
className: "m01lab__pills",
|
||||
children: [el("div", { className: "m01lab__pill-row", children: buttons }), stage],
|
||||
b.addEventListener("click", (ev) => {
|
||||
ev.stopPropagation(); // 줄 전체 누르기(행 모달)와 안 겹치게
|
||||
p.onClick();
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
/** 수량 칸 조각 — 알약 아니면 그냥 글자(연산기호·숫자·중간값 이름) */
|
||||
export type Fragment = { pill: Pill } | { text: string };
|
||||
|
||||
/** 조각을 이어 그림 — 칸 폭을 넘으면 자동으로 줄바꿈(`flex-wrap`) */
|
||||
export function fragmentRow(fragments: Fragment[]): HTMLElement {
|
||||
return el("span", {
|
||||
className: "m01lab__qty-frags",
|
||||
children: fragments.map((f) =>
|
||||
"pill" in f
|
||||
? pillButton(f.pill)
|
||||
: el("span", { className: "m01lab__qty-text", text: f.text }),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,13 +39,27 @@
|
||||
margin: var(--spacing-8) 0 var(--spacing-4);
|
||||
}
|
||||
|
||||
/* 칸 폭 고정(`table-layout: fixed`) — 인력·재료·경비 세 표가 저마다 딴 `<table>` 이라
|
||||
내용 기준으로 폭이 갈리던 것을 막고 셋이 같은 폭으로 맞춰짐(일감35 이어 — 수량 칸 알약화). */
|
||||
.m01lab__ho {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.m01lab__ho th:nth-child(1) {
|
||||
width: 26%;
|
||||
}
|
||||
|
||||
.m01lab__ho th:nth-child(2) {
|
||||
width: 64px;
|
||||
width: 8%;
|
||||
}
|
||||
|
||||
.m01lab__ho th:nth-child(3) {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.m01lab__ho th:nth-child(4),
|
||||
.m01lab__ho th:nth-child(5) {
|
||||
width: 110px;
|
||||
width: 13%;
|
||||
}
|
||||
|
||||
.m01lab__line {
|
||||
@@ -61,7 +75,6 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.m01lab__qty,
|
||||
.m01lab__expr {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--text-body-sm);
|
||||
@@ -69,6 +82,25 @@
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* 수량 칸 — 알약 + 사칙연산 글자를 이어 붙이고, 칸 폭을 넘으면 그 칸 폭 기준으로 줄바꿈 */
|
||||
.m01lab__qty-cell {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.m01lab__qty-frags {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 2px 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.m01lab__qty-text {
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: var(--text-body-sm);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.m01lab__subtotal td {
|
||||
font-weight: 600;
|
||||
background: var(--color-paper);
|
||||
|
||||
Reference in New Issue
Block a user