From a0bd269d7ffe4f4d84e54c305e6219e3469df365 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 15:13:23 +0900 Subject: [PATCH] =?UTF-8?q?feat(710-2):=20M01=20=EB=8B=A8=EA=B0=80?= =?UTF-8?q?=EC=82=B0=EC=B6=9C=20=EC=83=81=EC=84=B8=20=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EA=B3=A0=EB=A5=B4=EA=B8=B0=20=E2=80=94=20=EB=A8=B8=EB=A6=AC?= =?UTF-8?q?=EC=A4=84=20=EB=B6=84=ED=95=A0=20=EA=B3=A0=EB=A5=B4=EA=B8=B0=20?= =?UTF-8?q?=C2=B7=20=EA=B3=A0=EB=A5=B8=20=EB=8D=A9=EC=9D=B4=EB=A7=8C=20?= =?UTF-8?q?=C2=B7=20=EA=B8=B0=EB=B3=B8=EC=A0=95=EB=B3=B4=20=EC=A0=91?= =?UTF-8?q?=ED=9E=98=20=ED=95=9C=20=EC=A4=84=20=C2=B7=20=EC=A7=80=EA=B8=88?= =?UTF-8?q?=20=EC=9E=85=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M01_MasterData_UI_LogicLab_View.ts — 보기 넷(단가산출 상세 기본 · 인력·재료·경비 준비 중 · 시험 계산 · 텍스트 수식) · [신규 로직] 왼쪽 세로 나눔선 - 덩이 이름표 data-block + 그릇 data-view CSS 숨김 — 시험 계산은 숨어도 그려 두어 자동 계산 돎 · 로직 바꿔도 보기 유지 - 흐름 로직(FlowView) 두 덩이도 같은 이름표 - 기본정보 = 맨 위 접힌 한 줄(키 · 원문번호 · 이름 · 결과단위) · 막힘 까닭은 접어도 보임 - 단가산출 상세 머리 「지금 입력」 한 줄(읽기만) Co-Authored-By: Claude Opus 5.5 --- M01_MasterData/M01_MasterData_UI_LogicLab.ts | 5 +- .../M01_MasterData_UI_LogicLab_Detail.ts | 93 ++++++++++++------- .../M01_MasterData_UI_LogicLab_FlowView.ts | 11 ++- .../M01_MasterData_UI_LogicLab_Style.css | 25 +++++ .../M01_MasterData_UI_LogicLab_Text.ts | 8 ++ .../M01_MasterData_UI_LogicLab_View.ts | 50 ++++++++++ 6 files changed, 153 insertions(+), 39 deletions(-) create mode 100644 M01_MasterData/M01_MasterData_UI_LogicLab_View.ts diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index f8cd1b47b..877fb2cf8 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -52,6 +52,7 @@ import { } from "./M01_MasterData_UI_LogicLab_Flow"; import { askBasicInfo } from "./M01_MasterData_UI_LogicLab_Basic"; import { buildLogicSide } from "./M01_MasterData_UI_LogicLab_Side"; +import { createViewPicker } from "./M01_MasterData_UI_LogicLab_View"; import { buildHeadBar } from "./M01_MasterData_UI_Head"; import { fetchSessionUser } from "../A06_Login/A06_Login_Api_Fetch"; import type { CfLogic } from "../M02_MasterTemplete/DataFlow/M02_DF_Model"; @@ -130,6 +131,7 @@ export async function mountM01LogicLab( const editor = el("div", { className: "m01-logic__editor" }); // 로직 상세 — 왼쪽 지금 내용 · 오른쪽 접는 패널 둘(흐름도 · 원문 · 하나만 열림 · 폭 공유) const panes = buildLogicSide(editor); + const views = createViewPicker(editor); // 보기 고르기 — 머리줄 [신규 로직] 왼쪽 const errors = el("div", { className: "ui-banner ui-banner--danger m01-logic__reasons", attrs: { hidden: "" }, @@ -236,7 +238,7 @@ export async function mountM01LogicLab( list.root.hidden = !listing || !loaded; waiting.hidden = loaded; panes.root.hidden = !opened; - back.hidden = listing; + back.hidden = views.root.hidden = listing; if (!opened) return; if (!opened.row) { editor.replaceChildren(createEmpty({ text: `● ${tx("List_Deleted")}`, small: true })); @@ -582,6 +584,7 @@ export async function mountM01LogicLab( title: tx("Title"), status: [pending], buttons: [ + views.root, createButton({ label: tf("New"), variant: "filled", onClick: () => void onNew() }), copyButton, createButton({ label: tx("Bar_Delete"), variant: "danger", onClick: () => void onDelete() }), diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 023fc34d0..dac172596 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -37,6 +37,8 @@ import { tl, type LabTextKey } from "./M01_MasterData_UI_LogicLab_Text"; import "./M01_MasterData_UI_LogicLab_Style.css"; import "./M01_MasterData_UI_LogicLab_Pill.css"; import { createBadge } from "@ui/ui_template_badge"; +import { createEmpty } from "@ui/ui_template_empty"; +import { block, nowInputs } from "./M01_MasterData_UI_LogicLab_View"; /** 서버 조각(`/text` 줄의 `조각`) → 수량 칸 조각 — 요소·표·로직은 알약(글 = 조각 글) · 나머지는 글자 */ function toFragments(pieces: TextPiece[], ctx: DetailContext): Fragment[] { @@ -142,7 +144,7 @@ const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement => ], }); -function infoBox(ctx: DetailContext): HTMLElement { +function infoBox(ctx: DetailContext): HTMLElement[] { const row = ctx.row; const own = isOwnLogic(row); const field = (label: string, key: "이름" | "원문번호" | "결과단위" | "출처"): HTMLElement => { @@ -189,32 +191,46 @@ function infoBox(ctx: DetailContext): HTMLElement { }), ] : []; - return section(tl("Info_Title"), [ - el("p", { - className: "ui-text ui-text--muted", - text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"), - }), - ...reasons, - el("div", { - className: "m01lab__info", - children: [ - cell(tx("Head_File"), ctx.file), - cell(tx("Head_Key"), row.키), - field(tx("Head_Number"), "원문번호"), - field(tx("Head_Name"), "이름"), - field(tx("Head_Unit"), "결과단위"), - field(tx("Head_Source"), "출처"), - cell(tx("Head_Owner"), row.소유 ?? ""), - el("label", { - className: "m01lab__cell m01lab__cell--wide", - children: [ - el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }), - note, - ], - }), - ], - }), - ]); + const grid = el("div", { + className: "m01lab__info", + children: [ + cell(tx("Head_File"), ctx.file), + cell(tx("Head_Key"), row.키), + field(tx("Head_Number"), "원문번호"), + field(tx("Head_Name"), "이름"), + field(tx("Head_Unit"), "결과단위"), + field(tx("Head_Source"), "출처"), + cell(tx("Head_Owner"), row.소유 ?? ""), + el("label", { + className: "m01lab__cell m01lab__cell--wide", + children: [ + el("span", { className: "ui-text ui-text--muted", text: tx("Head_Note") }), + note, + ], + }), + ], + }); + // 늘 맨 위 접힌 한 줄 — 펼침은 이 로직 안에서 기억(다시 그려도) · 막힘 까닭은 접어도 보임 + const head = [row.키, row.원문번호, row.이름, row.결과단위].filter(Boolean).join(" · "); + const fold = el("details", { + className: "m01lab__box m01lab__infofold", + attrs: ctx.ownHost.dataset.info ? { open: "" } : {}, + children: [ + el("summary", { + children: [ + el("strong", { text: tl("Info_Title") }), + el("span", { className: "ui-text ui-text--muted", text: head }), + ], + }), + el("p", { + className: "ui-text ui-text--muted", + text: tl(own ? "Info_Own" : ctx.admin ? "Info_Base" : "Info_ReadOnly"), + }), + grid, + ], + }); + fold.addEventListener("toggle", () => (ctx.ownHost.dataset.info = fold.open ? "1" : "")); + return [fold, ...reasons]; } /** 수량 칸 = 계산 답의 고른 수량(% 덧줄은 율) · 계산 전 「—」 */ @@ -453,10 +469,13 @@ function ownEditor(ctx: DetailContext): HTMLElement { function hoBox(ctx: DetailContext): HTMLElement { const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); if (!money) { - return section(tl("Ho_Title"), [ - el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }), - el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), - ]); + return block( + section(tl("Ho_Title"), [ + el("p", { className: "ui-text ui-text--muted", text: tl("Not_Money") }), + el("pre", { className: "m01lab__expr", text: ctx.row.결과?.식 ?? "" }), + ]), + "ho", + ); } const all = entries(ctx); const calculated = ctx.lines !== null; @@ -479,7 +498,7 @@ function hoBox(ctx: DetailContext): HTMLElement { fold.addEventListener("toggle", () => (ctx.ownHost.dataset.fold = fold.open ? "1" : "")); const blocks = isOwnLogic(ctx.row) ? [ownEditor(ctx), fold, totalLine] : [...tables, totalLine]; const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; - return section(tl("Ho_Title"), blocks, hint); + return block(section(tl("Ho_Title"), [nowInputs(ctx.values), ...blocks], hint), "ho"); } /** 상세 화면 넷을 `host` 에 쌓음 */ @@ -492,10 +511,14 @@ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { active instanceof HTMLInputElement || active instanceof HTMLTextAreaElement ? { node: active, from: active.selectionStart, to: active.selectionEnd } : null; + // 보기 고르기(`_View.ts`) — 덩이마다 이름표 · 고른 덩이만 보임 · 시험 계산은 숨어도 그려 둠 host.replaceChildren( - infoBox(ctx), - section(tl("Calc_Title"), [ctx.calcHost]), - ...(ctx.flowHost ? [ctx.flowHost] : [hoBox(ctx), section(tl("Formula_Title"), [formula])]), + ...infoBox(ctx), + block(section(tl("Calc_Title"), [ctx.calcHost]), "calc"), + block(section(tl("View_Est"), [createEmpty({ text: tl("View_Soon"), small: true })]), "est"), + ...(ctx.flowHost + ? [block(nowInputs(ctx.values), "ho"), ctx.flowHost] + : [hoBox(ctx), block(section(tl("Formula_Title"), [formula]), "formula")]), ); const own = kept?.node.dataset.own; // 기본정보 칸은 새로 만들어지므로 이름표로 다시 찾음 const back = own ? host.querySelector(`[data-own="${own}"]`) : kept?.node; diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts index f4eb8eb86..16520184e 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_FlowView.ts @@ -159,9 +159,11 @@ export function buildFlowView( try { ev = opts.result ?? evalLogic(lg, values, { logics: opts.logics }); } catch (e) { - host.replaceChildren( - section(tf("Detail"), [el("p", { className: "ui-text ui-text--muted", text: String(e) })]), - ); + const failed = section(tf("Detail"), [ + el("p", { className: "ui-text ui-text--muted", text: String(e) }), + ]); + failed.dataset.block = "ho"; + host.replaceChildren(failed); return; } const totalRound = totalRoundOf(lg, totalRoundNow()); @@ -196,5 +198,8 @@ export function buildFlowView( tf("Formula"), bodies.length ? bodies : [el("p", { className: "ui-text ui-text--muted", text: tf("NoFunc") })], ); + // 보기 고르기 이름표(`LogicLab_View.ts`) — 고른 덩이만 보임 + detail.dataset.block = "ho"; + text.dataset.block = "formula"; host.replaceChildren(detail, text); } diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index e9779d7e2..0a8e482bc 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -190,3 +190,28 @@ .m01lab__pickrow.is-picked { font-weight: 600; } + +/* 보기 고르기(710-2 · `_View.ts`) — 그릇 data-view 와 다른 이름표 덩이는 숨김(시험 계산은 숨어도 돎) */ +.m01-logic__editor[data-view="ho"] [data-block]:not([data-block="ho"]), +.m01-logic__editor[data-view="est"] [data-block]:not([data-block="est"]), +.m01-logic__editor[data-view="calc"] [data-block]:not([data-block="calc"]), +.m01-logic__editor[data-view="formula"] [data-block]:not([data-block="formula"]) { + display: none; +} + +/* 기본정보 — 늘 맨 위 접힌 한 줄 */ +.m01lab__infofold > summary { + display: flex; + align-items: baseline; + gap: var(--spacing-8); + cursor: pointer; +} + +.m01lab__infofold[open] > summary { + margin-bottom: var(--spacing-8); +} + +/* 상세 머리 「지금 입력」 한 줄 */ +.m01lab__now { + margin: 0; +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 5baa2b5d1..1b977355b 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -28,6 +28,14 @@ const TEXT = { Formula_Title: ["텍스트 수식", "Text formula"], Formula_Slot: ["시험 계산을 누르면 식이 글로 나옴", "Run the trial calc to see the formula"], Calc_Title: ["시험 계산", "Trial calculation"], + View_Est: ["인력·재료·경비", "Labor·material·expense"], + View_Label: ["보기", "View"], + View_Soon: [ + "준비 중 — 인력 · 재료 · 경비 묶음 보기", + "Coming soon — labor · material · expense view", + ], + Now_Inputs: ["지금 입력", "Current inputs"], + Now_None: ["없음", "none"], G_labor: ["인력", "Labor"], G_material: ["재료", "Material"], G_cost: ["경비", "Expense"], diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts new file mode 100644 index 000000000..577eb6d07 --- /dev/null +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_View.ts @@ -0,0 +1,50 @@ +/* ============================================================================= + * M01_MasterData_UI_LogicLab_View.ts + * 「단가산출 로직」 상세 보기 고르기 — 머리줄 [신규 로직] 왼쪽 「분할 고르기 | 세로 나눔선」 + * 보기 넷 = 단가산출 상세(기본) · 인력·재료·경비 · 시험 계산 · 텍스트 수식 — 고른 덩이만 보임 + * 덩이 = `data-block` 이름표 · 숨김 = 그릇 `data-view` + CSS(`LogicLab_Style.css`) — 다시 그려도 고름 유지 + * 시험 계산은 숨겨도 그려 둠(자동 계산이 숨은 칸에서도 돎) · 기본정보는 이름표 없음(늘 맨 위) + * 기억 = 이 화면 안에서만(로직을 바꿔도 유지 · 새로 들어오면 단가산출 상세) + * ========================================================================== */ + +import { el } from "@ui/ui_template_elements"; +import { createSegment } from "@ui/ui_template_segment"; +import { createToolbar } from "@ui/ui_template_toolbar"; +import { tl } from "./M01_MasterData_UI_LogicLab_Text"; + +export type LabView = "ho" | "est" | "calc" | "formula"; + +/** 덩이에 보기 이름표를 붙여 돌려줌 */ +export const block = (node: T, view: LabView): T => { + node.dataset.block = view; + return node; +}; + +/** 보기 고르기 — `host`(상세 그릇)에 `data-view` 를 둠 · `root` 는 머리줄에 놓음 */ +export function createViewPicker(host: HTMLElement): { root: HTMLElement } { + host.dataset.view = "ho"; + const segment = createSegment({ + options: [ + { value: "ho", label: tl("Ho_Title") }, + { value: "est", label: tl("View_Est") }, + { value: "calc", label: tl("Calc_Title") }, + { value: "formula", label: tl("Formula_Title") }, + ], + value: "ho", + size: 32, + ariaLabel: tl("View_Label"), + onChange: (v) => (host.dataset.view = v), + }); + return { root: createToolbar({ items: [segment.root, "divider"], label: tl("View_Label") }) }; +} + +/** 상세 머리 「지금 입력」 한 줄(읽기만) — 숨은 시험 계산 칸에 든 값 */ +export function nowInputs(values: Record): HTMLElement { + const filled = Object.entries(values).filter(([, v]) => String(v ?? "").trim() !== ""); + const text = filled.length ? filled.map(([k, v]) => `${k} ${v}`).join(" · ") : tl("Now_None"); + return el("p", { + className: "ui-text ui-text--muted m01lab__now", + attrs: { "data-now": "" }, + text: `${tl("Now_Inputs")} · ${text}`, + }); +}