From cd2e8df39aecb1ad4a9f64371f2e54c8a1e560dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 21 Sep 2026 23:34:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(M01):=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EC=8B=9C=ED=97=98=20=EC=A4=84=20=EB=AA=A8=EB=8B=AC?= =?UTF-8?q?=20=E2=80=94=20=ED=95=9C=20=EC=A4=84=20=ED=92=80=EC=9D=B4?= =?UTF-8?q?=EC=99=80=20=EC=95=8C=EC=95=BD=20=EB=8B=A8=EC=B6=94(=EB=88=84?= =?UTF-8?q?=EB=A5=B8=20=EC=9E=90=EB=A3=8C=20=ED=95=98=EB=82=98=EB=A7=8C=20?= =?UTF-8?q?=ED=8E=BC=EC=B9=A8=C2=B7=EC=A0=91=ED=9E=98)=20(PLAN=205-3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01PAdA5ThqmtVSsbzjusk1cJ --- .../M01_MasterData_UI_LogicLab_Modal.ts | 81 ++++++++++++------- .../M01_MasterData_UI_LogicLab_Pill.css | 35 ++++++++ .../M01_MasterData_UI_LogicLab_Pill.ts | 44 ++++++++++ .../M01_MasterData_UI_LogicLab_Text.ts | 5 ++ 4 files changed, 138 insertions(+), 27 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css create mode 100644 M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts index f6cd210a..0fbfa52f 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Modal.ts @@ -9,13 +9,16 @@ 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 { pillBar, type Pill } from "./M01_MasterData_UI_LogicLab_Pill"; import { tl } from "./M01_MasterData_UI_LogicLab_Text"; +import "./M01_MasterData_UI_LogicLab_Pill.css"; export interface ModalTarget { title: string; @@ -87,6 +90,9 @@ function tableView( }); } +const groupOf = (ref: string): string => + ref.startsWith("LB") ? "인력" : ref.startsWith("M") ? "재료" : ref.startsWith("EQ") ? "기계" : ""; + function priceView(ref: string, brief: ElementBrief): HTMLElement { const cols = Object.entries(brief.값칸 ?? {}); return el("div", { @@ -134,38 +140,59 @@ export function openMaterialModal(target: ModalTarget): void { backdrop.addEventListener("click", (ev) => ev.target === backdrop && close()); backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && close()); - const parts: HTMLElement[] = [ - el("p", { className: "m01-logic__muted", text: tl("Modal_Qty") }), - el("pre", { className: "m01lab__expr", text: target.expr }), - ]; const finds = relatedFinds(target.expr, target.middles); - const price = target.element ? target.prices[target.element] : undefined; - if (target.element?.startsWith("로직(")) { - parts.push(el("p", { text: `${tl("Modal_Logic")} · ${target.element}` })); - } else if (price) { - parts.push( - el("p", { className: "m01-logic__muted", text: tl("Modal_Price") }), - priceView(target.element as string, price), - ); + 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", { text: target.element }), + }); } - const tables = el("div", { className: "m01lab__tables" }); - parts.push(tables); - if (!finds.length && !price && !target.element?.startsWith("로직(")) { - parts.push(el("p", { className: "m01-logic__muted", text: tl("Modal_NoData") })); + 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); document.body.append(backdrop); dialog.tabIndex = -1; dialog.focus(); - - void Promise.all(finds.map(async (f) => ({ f, table: await loadTable(f.find.table) }))).then( - (got) => - tables.replaceChildren( - ...got.map(({ f, table }) => - table - ? tableView(f, table, target.values) - : el("p", { className: "m01-logic__bad", text: `${f.find.table} ?` }), - ), - ), - ); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css new file mode 100644 index 00000000..91d90601 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.css @@ -0,0 +1,35 @@ +/* M01 로직 개선 시험 — 줄 모달의 알약 단추 */ +.m01lab__pills { + display: flex; + flex-direction: column; + gap: var(--spacing-8); + min-width: 0; +} + +.m01lab__pill-row { + display: flex; + flex-wrap: wrap; + gap: var(--spacing-8); +} + +.m01lab__pill { + padding: var(--spacing-4) var(--spacing-12); + border: 1px solid var(--color-border); + border-radius: 999px; + background: var(--color-bg); + color: var(--color-text); + font: inherit; + cursor: pointer; + max-width: 100%; + overflow-wrap: anywhere; +} + +.m01lab__pill.is-open { + background: var(--color-accent); + border-color: var(--color-accent); + color: var(--color-bg); +} + +.m01lab__pill-stage { + min-width: 0; +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts new file mode 100644 index 00000000..7282dd4c --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Pill.ts @@ -0,0 +1,44 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_Pill.ts + * 줄 모달의 알약 단추 — 쓰인 자료를 알약으로만 늘어놓고, 누른 것 하나만 펼침(다시 누르면 접힘) + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; + +export interface Pill { + /** 알약 글 — 보기 「표 QF000421」 · 「인력 석공」 */ + label: string; + /** 펼칠 때 그림 — 표처럼 읽어 와야 하면 Promise */ + view: () => HTMLElement | Promise; +} + +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 => { + 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); + }); + return el("div", { + className: "m01lab__pills", + children: [el("div", { className: "m01lab__pill-row", children: buttons }), stage], + }); +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index c73c5063..c4992680 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -45,6 +45,11 @@ const TEXT = { ], Click_Hint: ["줄을 누르면 그 줄이 쓰는 자료가 뜸", "Click a line to see the data it uses"], Modal_Close: ["닫기", "Close"], + Modal_Sum: ["한 줄 풀이", "In one line"], + Pill_Hint: ["쓰인 자료 — 누르면 펼침", "Data used — tap to open"], + Modal_UnitPrice: ["단가", "Unit price"], + Pill_Table: ["표", "Table"], + Pill_Logic: ["로직", "Logic"], Modal_Qty: ["수량 식", "Quantity formula"], Modal_Price: ["단가 자료", "Price source"], Modal_Value: ["값", "Value"],