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:
@@ -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", {
|
||||
|
||||
Reference in New Issue
Block a user