feat(M01): 몫 줄 단가 = 단가 x 몫 비율 · 줄 모달 윗줄 요소조각 · 로직 알약이 그 로직을 엶 (PLAN 5장)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT
This commit is contained in:
2026-09-24 11:10:52 +09:00
co-authored by Claude Sonnet 5
parent cebd49980a
commit a523066de4
4 changed files with 69 additions and 13 deletions
@@ -19,8 +19,19 @@ import { formatNumber } from "./M01_MasterData_UI_Logic_Edit";
import { formatMoney } from "./M01_MasterData_UI_Logic_Money";
import { explain, loadTable, type TableRow } from "./M01_MasterData_UI_Logic_Note";
import { jumpToMaster, type MasterJump } from "./M01_MasterData_UI_Side";
import { fragmentRow, type Fragment } from "./M01_MasterData_UI_LogicLab_Pill";
import { tl } from "./M01_MasterData_UI_LogicLab_Text";
/** 로직 알약 → 그 로직 열기 — 로직 화면이 자기 열기를 등록 · 열려 있는 모달은 다 닫음 */
let logicOpener: (key: string) => void = () => {};
export const setLogicOpener = (fn: (key: string) => void): void => {
logicOpener = fn;
};
export function openLogic(key: string): void {
document.querySelectorAll(".m01-logic__backdrop").forEach((b) => b.remove());
logicOpener(key);
}
/** 재료 고르기 조건(호표 줄 `요소` 가 글 대신 객체일 때) — 서버 `master_material.PICK_KEYS` 와 같은 칸 */
export interface PickCond {
구분?: string;
@@ -51,6 +62,10 @@ export interface ModalTarget {
middles: NamedFormula[];
/** 시험 계산 칸에 넣은 값 — 걸린 줄을 맞히는 데 씀 */
values: Record<string, string>;
/** 서버 `요소조각` — 단가 자리(요소 · 로직 부르기)를 조각으로 · 없으면 로직 키만 알약으로 */
unitFrag?: Fragment[];
/** 서버 수량 조각 — 한 줄 풀이를 조각으로(알약 포함) · 없으면 식 풀이 글 */
qtyFrag?: Fragment[];
}
/** 수량 칸 표 알약 하나가 여는 자료 — 서버 조각 `참조`(키 · 행 · 열)가 곧 찾은 자리 */
@@ -289,18 +304,31 @@ export function openMaterialModal(target: ModalTarget): void {
? (target.prices[element] ?? target.prices[base])
: undefined;
const priceRef = cond ? pickedOf(cond, target.values, known?.ref ?? "") : element;
const logicRef = element.startsWith("로직(") ? element : "";
const logicKey = /^로직\((\w+)/.exec(element)?.[1] ?? "";
const sum = el("p", { className: "m01lab__sum" });
const setSum = (price: ElementBrief | null | undefined): void => {
const unit = price ? `${tl("Modal_UnitPrice")} ${formatMoney(price.값)} × ` : "";
sum.textContent = `${target.title} = ${unit}${explain(target.expr)}`;
if (target.qtyFrag) {
sum.replaceChildren(`${target.title} = ${unit}`, fragmentRow(target.qtyFrag));
} else sum.textContent = `${target.title} = ${unit}${explain(target.expr)}`;
};
setSum(known);
const rate = el("div", { className: "m01lab__modal-body" });
if (known && priceRef) {
rate.append(priceView(priceRef, known));
} else if (logicRef) {
rate.append(el("p", { text: `${tl("Modal_Logic")} · ${logicRef}` }));
} else if (logicKey) {
rate.append(
el("p", {
children: [
`${tl("Modal_Logic")} · `,
fragmentRow(
target.unitFrag ?? [
{ pill: { label: logicKey, onClick: () => openLogic(logicKey) } },
],
),
],
}),
);
} else if (cond && priceRef) {
rate.append(muted("…")); // 후보 목록이 오면 그 줄로 채움 — 100건 밖이면 키로 따로 찾아 옴
} else if (cond) {
@@ -335,7 +363,13 @@ export function openMaterialModal(target: ModalTarget): void {
className: "m01lab__raw",
children: [
el("summary", { text: tl("Modal_Qty") }),
el("pre", { className: "m01lab__expr", text: target.expr }),
el("pre", {
className: "m01lab__expr",
text: logicKey
? `${element}
${target.expr}`
: target.expr,
}),
],
}),
);