diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 4eb22ae7..d7ad8825 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -34,6 +34,7 @@ import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterDa import type { SideHandle } from "./M01_MasterData_UI_Side"; import { tx } from "./M01_MasterData_UI_Logic_Text"; import { buildLabDetail, isOwnLogic } from "./M01_MasterData_UI_LogicLab_Detail"; +import { setLogicOpener } from "./M01_MasterData_UI_LogicLab_Modal"; import { openCopyScreen } from "./M01_MasterData_UI_LogicLab_Copy"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New"; @@ -441,5 +442,6 @@ export async function mountM01LogicLab( } finally { hideLoadingOverlay(); } + setLogicOpener((key) => void openKeyOn(key)); return { openKey: openKeyOn }; } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 0a4ae57b..58f3b4d7 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -26,6 +26,7 @@ import { openMaterialModal, openNoteModal, openRefModal, + openLogic, openTableModal, type PickCond, } from "./M01_MasterData_UI_LogicLab_Modal"; @@ -47,7 +48,7 @@ function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] { openTableModal({ title: piece.글, table: ref.키, row: ref.행, col: ref.열 }); else if (piece.kind === "요소") openRefModal({ title: piece.글, ref: ref.키, prices: ctx.prices }); - else openNoteModal(piece.글, `${tl("Modal_Logic")} · ${ref.키}`); + else openLogic(ref.키); }; return { pill: { label: piece.글, onClick: open }, ...dim }; }); @@ -93,7 +94,10 @@ interface Entry { amount: number | null; /** 덧줄이면 true */ extra: boolean; - open: () => void; + open: (self: Entry) => void; + /** 서버 조각으로 그린 단가 자리 · 수량 풀이 — 줄 모달이 씀(붙기 전엔 없음) */ + unitFrag?: Fragment[]; + piecesFrag?: Fragment[]; } /** 재료 고르기 줄 — `요소` 가 글 대신 {구분·상세구분·규격·대표} 객체 */ @@ -194,7 +198,15 @@ const spread = (e: Entry, line?: CalcLine): Entry[] => { ...e, group: OF_COST[k] ?? "other", amount: v, - ...(parts.length > 1 ? { share: k } : {}), + ...(parts.length > 1 + ? { + share: k, + // 몫 줄 단가 = 단가 × (몫 금액 ÷ 줄 금액) — 곱하기 수량이 몫 금액과 맞게 + ...(line && line.금액 && line.단가 != null + ? { price: formatMoney((line.단가 * v) / line.금액) } + : {}), + } + : {}), })); }; @@ -217,8 +229,10 @@ function entries(ctx: DetailContext): Entry[] { price: price === undefined || price === null ? "" : formatMoney(price), amount: line ? line.금액 : null, extra: false, - open: () => + open: (self) => openMaterialModal({ + unitFrag: self.unitFrag, + qtyFrag: self.piecesFrag, title: item.이름 ?? (cond ? condLabel(cond) : item.요소), expr: item.수량, ...(cond ? { cond, unit: item.단위 } : { element: item.요소 }), @@ -242,8 +256,10 @@ function entries(ctx: DetailContext): Entry[] { price: "", amount: line ? line.금액 : null, extra: true, - open: () => + open: (self) => openMaterialModal({ + unitFrag: self.unitFrag, + qtyFrag: self.piecesFrag, title: extra.이름, expr: extra.식, prices: ctx.prices, @@ -267,7 +283,9 @@ function attachPieces(all: Entry[], ctx: DetailContext): void { if (lines.length !== mine.length) continue; mine.forEach((e, i) => { const pieces = lines[i].조각; - if (pieces?.length) e.qtyFrag = toFragments(pieces, ctx); + if (pieces?.length) e.qtyFrag = e.piecesFrag = toFragments(pieces, ctx); + const unit = lines[i].요소조각; + if (unit?.length) e.unitFrag = toFragments(unit, ctx); }); } } @@ -300,8 +318,8 @@ function lineRow(e: Entry): HTMLElement { cellOf(e.amount === null ? "" : formatMoney(e.amount), "m01-logic__money"), ], }); - tr.addEventListener("click", e.open); - tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open()); + tr.addEventListener("click", () => e.open(e)); + tr.addEventListener("keydown", (ev) => ev.key === "Enter" && e.open(e)); return tr; } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index f392be09..9e6375bb 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -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; + /** 서버 `요소조각` — 단가 자리(요소 · 로직 부르기)를 조각으로 · 없으면 로직 키만 알약으로 */ + 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, + }), ], }), ); diff --git a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts index 1626f95d..97565902 100644 --- a/M01_MasterData/M01_MasterData_UI_Logic_Api.ts +++ b/M01_MasterData/M01_MasterData_UI_Logic_Api.ts @@ -113,6 +113,8 @@ export interface TextLine { 이름글?: string; /** 수량 식을 쪼갠 조각 — 알약(요소·표·로직) · 글자(값·기호·글) */ 조각?: TextPiece[]; + /** 단가 자리(요소 · 로직 부르기)를 쪼갠 조각 — 줄 모달 윗줄 */ + 요소조각?: TextPiece[]; /** 줄 번호(답 전체에서 하나씩) — 두 칸 줄 맞춤 · 밝히기 */ 짝?: number; 금액: number;