From 96e58106bb4869ba771abee7d51225412fb9ba20 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 21 Sep 2026 20:58:38 +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=E2=80=94=20=EC=9E=90=EC=B2=B4?= =?UTF-8?q?=20=EB=A1=9C=EC=A7=81(GX)=20=EC=83=81=EC=84=B8=EB=8F=84=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B8=EA=B3=BC=20=EA=B0=99=EC=9D=80=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=20=EB=84=B7=20=C2=B7=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=98=EB=A9=B4=20=ED=98=B8=ED=91=9C=20=EC=86=8C=EA=B3=84?= =?UTF-8?q?=EC=99=80=20=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=8B=9D?= =?UTF-8?q?=EC=9D=B4=20=EB=8B=A4=EC=8B=9C=20=EA=B3=84=EC=82=B0=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 기본정보 칸(번호·이름·단위·출처)을 자체 로직이면 고칠 수 있게 · 호표 묶음·소계 아래에 편집기(입력·호표·중간 값·덧줄·끝수)를 폄 - 고친 뒤 0.7 초 쉬면 시험 계산을 다시 눌러 소계 · 텍스트 수식을 새로 그림 · 편집기 칸은 다시 그려도 그대로 씀 - ORCA 본뜬 GX — 컨테이너 넷 정본과 같음 · 증가율 고침 203,281.2→223,609.32 소계·텍스트 수식 계 같이 바뀜 · 시험 자료·키 대장 되돌림 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_016WRFJmmhPi4exAzHgPZHMT --- M01_MasterData/M01_MasterData_UI_LogicLab.ts | 13 ++- .../M01_MasterData_UI_LogicLab_Detail.ts | 79 ++++++++++++------- .../M01_MasterData_UI_LogicLab_Style.css | 8 ++ .../M01_MasterData_UI_LogicLab_Text.ts | 8 ++ 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab.ts b/M01_MasterData/M01_MasterData_UI_LogicLab.ts index 77ecbd8b..852facb2 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab.ts @@ -33,7 +33,7 @@ import { buildCalc } from "./M01_MasterData_UI_Logic_Calc"; import { buildList, logicId, type ListItem, type ListMark } from "./M01_MasterData_UI_Logic_List"; import type { SideHandle } from "./M01_MasterData_UI_Side"; import { tx } from "./M01_MasterData_UI_Logic_Text"; -import { buildLabDetail } from "./M01_MasterData_UI_LogicLab_Detail"; +import { buildLabDetail, isOwnLogic } from "./M01_MasterData_UI_LogicLab_Detail"; import { buildNewLogic } from "./M01_MasterData_UI_LogicLab_New"; import { tn } from "./M01_MasterData_UI_LogicLab_New_Text"; import "./M01_MasterData_UI_Logic_Style.css"; @@ -56,10 +56,12 @@ interface Opened extends Draft { reasons: string[]; lines: CalcLine[] | null; text: TextAnswer | null; + ownHost: HTMLElement; values: Record; } const cacheKey = "m01_logic_drafts_lab"; +const ownHost = (): HTMLElement => el("div", { className: "m01lab__own" }); const clone = (v: T): T => JSON.parse(JSON.stringify(v)) as T; function loadDrafts(): Record { @@ -84,6 +86,7 @@ export async function mountM01LogicLab( let opened: Opened | null = null; let creating = false; // 「식으로 새로 만들기」 화면이 열려 있음 let calcInputs = ""; + let recalc: number | undefined; const editor = el("div", { className: "m01-logic__editor" }); const errors = el("div", { className: "m01-logic__reasons", attrs: { hidden: "" } }); const calc = el("div", { className: "m01lab__calc" }); @@ -134,6 +137,11 @@ export async function mountM01LogicLab( persist(); const inputs = JSON.stringify(opened.row.입력 ?? []); if (inputs !== calcInputs) drawCalc(); + if (isOwnLogic(opened.row)) { + // 자체 로직 — 고친 뒤 잠시 쉬면 다시 계산해 호표 소계와 텍스트 수식을 새로 그림 + window.clearTimeout(recalc); + recalc = window.setTimeout(() => calc.querySelector("button")?.click(), 700); + } }; const pick = (o: Opened, row: LogicRow | null): Draft => ({ @@ -191,6 +199,7 @@ export async function mountM01LogicLab( lines: current.lines, text: current.text, values: current.values, + ownHost: current.ownHost, onChange: touch, calcHost: calc, }); @@ -215,6 +224,7 @@ export async function mountM01LogicLab( reasons: [], lines: null, text: null, + ownHost: ownHost(), values: {}, }); return; @@ -233,6 +243,7 @@ export async function mountM01LogicLab( reasons: one.reasons, lines: null, text: null, + ownHost: ownHost(), values: {}, }); } catch (error) { diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts index 5d81ccc7..dc657656 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Detail.ts @@ -33,6 +33,8 @@ export interface DetailContext { /** 마지막 시험 계산의 읽는 식 줄(서버가 줌) */ text: TextAnswer | null; values: Record; + /** 자체 로직 편집기 자리 — 다시 그려도 같은 칸을 씀(고치던 값이 안 날아감) */ + ownHost: HTMLElement; onChange: () => void; /** 시험 계산 칸(`buildCalc` 가 채움) — 넷째 컨테이너 안에 놓음 */ calcHost: HTMLElement; @@ -80,6 +82,20 @@ const section = (title: string, body: HTMLElement[], hint = ""): HTMLElement => function infoBox(ctx: DetailContext): HTMLElement { const row = ctx.row; + const own = isOwnLogic(row); + const field = (label: string, key: "이름" | "원문번호" | "결과단위" | "출처"): HTMLElement => { + if (!own) return cell(label, row[key] ?? ""); + const box = el("input", { className: "m01-logic__input", attrs: { "data-own": key } }); + box.value = row[key] ?? ""; + box.addEventListener("input", () => { + row[key] = box.value; + ctx.onChange(); + }); + return el("label", { + className: "m01lab__cell", + children: [el("span", { className: "m01-logic__muted", text: label }), box], + }); + }; const cell = (label: string, value: string): HTMLElement => el("div", { className: "m01lab__cell", @@ -107,17 +123,20 @@ function infoBox(ctx: DetailContext): HTMLElement { ] : []; return section(tl("Info_Title"), [ - el("p", { className: "m01-logic__muted", text: tl("Info_ReadOnly") }), + el("p", { + className: "m01-logic__muted", + text: tl(own ? "Info_Own" : "Info_ReadOnly"), + }), ...reasons, el("div", { className: "m01lab__info", children: [ cell(tx("Head_File"), ctx.file), cell(tx("Head_Key"), row.키), - cell(tx("Head_Number"), row.원문번호), - cell(tx("Head_Name"), row.이름), - cell(tx("Head_Unit"), row.결과단위), - cell(tx("Head_Source"), 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", @@ -255,6 +274,31 @@ function groupTable(group: Group, mine: Entry[], calculated: boolean): HTMLEleme ]; } +/** 자체 로직(키 GX…) — 정본은 아님 */ +export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX"); + +/** 자체 로직은 정본 화면과 같은 편집기를 호표 아래에 폄(머리 칸은 기본정보에서 고치므로 숨김) */ +function ownEditor(ctx: DetailContext): HTMLElement { + if (!ctx.ownHost.childElementCount) + buildEditor(ctx.ownHost, { + row: ctx.row, + file: ctx.file, + isNew: false, + files: [], + reasons: [], + prices: ctx.prices, + lines: ctx.lines, + onChange: ctx.onChange, + onFile: () => undefined, + onPick: openPicker, + }); + return el("details", { + className: "m01lab__edit", + attrs: { open: "" }, + children: [el("summary", { text: tl("Edit_Title") }), ctx.ownHost], + }); +} + function hoBox(ctx: DetailContext): HTMLElement { const money = !("결과" in ctx.row) && (ctx.row.결과단위 ?? "").startsWith("원"); if (!money) { @@ -277,37 +321,18 @@ function hoBox(ctx: DetailContext): HTMLElement { text: `${tl("Total")} ${calculated ? formatNumber(total) : "—"}`, }), ); + if (isOwnLogic(ctx.row)) blocks.push(ownEditor(ctx)); const hint = calculated ? tl("Click_Hint") : `${tl("Click_Hint")} · ${tl("Calc_First")}`; return section(tl("Ho_Title"), blocks, hint); } -/** 자체 로직(키 GX…) — 정본은 아님 */ -export const isOwnLogic = (row: LogicRow): boolean => row.키.startsWith("GX"); - -/** 자체 로직은 고치는 화면(정본 로직 화면과 같은 편집기) 하나로 기본정보 · 호표를 대신함 */ -function ownBox(ctx: DetailContext): HTMLElement { - const host = el("div"); - buildEditor(host, { - row: ctx.row, - file: ctx.file, - isNew: false, - files: [], - reasons: ctx.reasons, - prices: ctx.prices, - lines: ctx.lines, - onChange: ctx.onChange, - onFile: () => undefined, - onPick: openPicker, - }); - return section(tl("Info_Title"), [host]); -} - /** 상세 화면 넷을 `host` 에 쌓음 */ export function buildLabDetail(host: HTMLElement, ctx: DetailContext): void { const formula = el("div", { className: "m01lab__formula" }); buildFormula(formula, { text: ctx.text }); host.replaceChildren( - ...(isOwnLogic(ctx.row) ? [ownBox(ctx)] : [infoBox(ctx), hoBox(ctx)]), + infoBox(ctx), + hoBox(ctx), section(tl("Formula_Title"), [formula]), section(tl("Calc_Title"), [ctx.calcHost]), ); diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css index e5bdfcda..7bd6fa71 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Style.css @@ -109,3 +109,11 @@ font-weight: 600; text-align: right; } + +.m01lab__own > :first-child { + display: none; +} + +.m01lab__edit { + margin-top: var(--spacing-12); +} diff --git a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts index 7c1ffbf3..dd725120 100644 --- a/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts +++ b/M01_MasterData/M01_MasterData_UI_LogicLab_Text.ts @@ -11,6 +11,14 @@ const TEXT = { "기본 로직은 품셈 원문 그대로라 여기서 고치지 못함 — 비고만 고칠 수 있음", "Base logic follows the source book; only the note can be edited", ], + Info_Own: [ + "자체 로직 — 칸을 고치면 잠시 뒤 호표 소계와 텍스트 수식이 다시 계산됨", + "Own logic — edits recalculate the subtotals and formula shortly", + ], + Edit_Title: [ + "고치기 (호표 · 중간 값 · 덧줄 · 끝수)", + "Edit (lines · middles · extras · rounding)", + ], Ho_Title: ["일위대가 호표", "Unit-cost table"], Formula_Title: ["텍스트 수식", "Text formula"], Formula_Slot: ["시험 계산을 누르면 식이 글로 나옴", "Run the trial calc to see the formula"],