From 09a67ce91f4b7cd61650cefe465a706fa97e288a Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 5 Nov 2026 17:40:07 +0900 Subject: [PATCH] =?UTF-8?q?feat(792):=20=EB=8B=A8=EA=B0=80=EC=82=B0?= =?UTF-8?q?=EC=B6=9C=20=EC=8B=9C=ED=8A=B8=20=EC=8B=9D=20=EC=B9=B8=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=E2=80=94=20=EC=A4=84=20=EC=8B=9D=20?= =?UTF-8?q?=EA=B3=A0=EC=B9=A8=20=C2=B7=20=EC=B9=A0=20=EB=95=8C=EB=A7=88?= =?UTF-8?q?=EB=8B=A4=20=EA=B2=80=EC=82=AC=20=C2=B7=20=EB=B9=A8=EA=B0=84=20?= =?UTF-8?q?=EC=B9=B8=20=EA=B9=8C=EB=8B=AD=20=C2=B7=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 M02_MasterTemplete_Sangeun_SheetFormula.ts — 줄마다 [식 고침] → 이름 꼴 식 칸 · 칠 때마다 formula-check(옛 요청 끊기) · 틀리면 빨간 칸 + 까닭 · 바꿀 때 엔진 식을 식덮개로(같은 자리 상수 · 표값 · 품 덮개 걷음) · 빈 칸 · M01 식 = 풂 · 더한줄 수량식(율 · 로직 줄 뺌) - 변수 줄 — [+ 변수 줄] · 이름 · 식 · 값 · 빼기(Formula_Model addVar · editVar · dropVar) · 자리 뒤에 그림 - Sheet — 식 덮개 줄은 품 칸 안 둠 · 숫자 칸 대신 식 칸 · [전체 M01 원래로]에 식 덮개 셈 - Detail — 시트 옵션에 층(scope)만(줄 수 그대로) - test_790_1 — 식 칸 정적 단언 Co-Authored-By: Claude Opus 5.5 --- .../M02_MasterTemplete_Sangeun_Detail.ts | 2 +- .../M02_MasterTemplete_Sangeun_Sheet.css | 16 + .../M02_MasterTemplete_Sangeun_Sheet.ts | 51 +++- .../M02_MasterTemplete_Sangeun_SheetEdit.ts | 5 +- ...M02_MasterTemplete_Sangeun_SheetFormula.ts | 287 ++++++++++++++++++ .../logic/M02_Sangeun_Sheet_Model.ts | 6 + resources/tester/test_790_1_sangeun_sheet.py | 36 +++ 7 files changed, 394 insertions(+), 9 deletions(-) create mode 100644 M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetFormula.ts diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts index 1e5a92170..82a59a2e2 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Detail.ts @@ -105,7 +105,7 @@ export function mountSangeunDetail( const editor = el("div", { className: "m01-logic__editor" }); const sheet = createSangeunSheet(editor, { ...{ edit, opened: () => opened(), touch: () => touch(), onInput: (n, v) => onInput(n, v) }, - ...{ addLine: () => addButton.click(), addRate: () => rateButton.click() }, // 790-1 시트 첫 탭 + ...{ addLine: () => addButton.click(), addRate: () => rateButton.click(), scope }, // 790-1 · 792 식 검사 층 }); const calcHost = el("div", { className: "m01lab__calc" }); const ownHost = el("div"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.css b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.css index ef81928fd..cde103da8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.css +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.css @@ -124,3 +124,19 @@ color: var(--color-text-secondary); font-style: italic; } + +/* 792 식 칸 — 칸 + 검사 알림 한 줄 */ +.m02-sks__fx { + display: flex; + flex-direction: column; + gap: var(--spacing-4); + min-width: 0; +} + +.m02-sks__fx .ui-field { + width: 100%; +} + +.m02-sks__fx .ui-text:empty { + display: none; +} diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.ts index 2612038cf..be62c99fe 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_Sheet.ts @@ -7,6 +7,7 @@ * → SK 문서(덮개 · 더한줄 · 뺀줄 = `M02_Sangeun_Cover_Model`) → 상세 `touch` = 서버 셈 한 번(최신 응답만) * 상태줄 = M01 원래 · 지금 · 고치기 전(화면 캐시) + 차이 · 자동저장 없음([저장] = 상세 그대로) * 790-3 숫자 넓힘(식 속 상수 · 표값 · 글 줄 · 까닭 · [M01 원래로] · M01 바뀜) = `_Sangeun_SheetEdit` + * 792 식 칸(줄 식 고침 · 칠 때마다 검사 · 변수 줄) = `_Sangeun_SheetFormula` * 부품 = `ui_template_calc_row` 시트 줄 · 입력 · 고르기 · 단추 · 배지 · 글 — 날 단추 · 입력 없음 * ========================================================================== */ @@ -32,8 +33,15 @@ import { removedReasons, restoreAll, spotTail, - type EditCtx, } from "./M02_MasterTemplete_Sangeun_SheetEdit"; +import { + addVarButton, + formulaToggle, + rowFormula, + varRows, + type FormulaCtx, +} from "./M02_MasterTemplete_Sangeun_SheetFormula"; +import type { Layer } from "./M02_MasterTemplete_Api_Fetch"; import { coverKinds, dropAddKeys, type ConstDoc } from "./logic/M02_Sangeun_Const_Model"; import { dropLine, @@ -117,6 +125,8 @@ export interface SheetOptions { onInput: (name: string, value: string) => void; addLine: () => void; addRate: () => void; + /** 792 식 검사 길 층 */ + scope: { layer: Layer; owner: string | null }; } type View = "sheet" | "ho" | "est" | "calc" | "formula"; @@ -132,7 +142,9 @@ export function createSangeunSheet( let mode: "sheet" | "paper" = "sheet"; let last: SheetDraw | null = null; let before: number | null = null; // 고치기 전 = 연 뒤 · 저장 뒤 첫 셈 계(화면 캐시) - let ctx: EditCtx | null = null; // 790-3 숫자 넓힘 칸 — 그릴 때마다 새로 + let ctx: FormulaCtx | null = null; // 790-3 숫자 넓힘 · 792 식 칸 — 그릴 때마다 새로 + const open = new Set(); // 식 칸 연 자리 + const drafts = new Map(); // 치는 중 식 글 let focusNext = ""; // 새 글 줄 칸 — 다음 그림 뒤 초점 const show = (v: View): void => { host.hidden = v !== "sheet"; @@ -239,8 +251,15 @@ export function createSangeunSheet( if (!opt.edit) return []; const parts = Object.keys(r.parts); const part = PART_OF[parts[0] ?? ""]; + const fx = coverKinds(d.doc as ConstDoc, r.cid).includes("식"); // 식 덮개 = 품 칸 안 둠(792) const cells = [ - numCell(`${r.cid}|qty`, r.pct ? t("Rate") : t("Qty"), r.수량, (x) => setCell(d, r, "qty", x)), + ...(fx + ? [] + : [ + numCell(`${r.cid}|qty`, r.pct ? t("Rate") : t("Qty"), r.수량, (x) => + setCell(d, r, "qty", x), + ), + ]), ...(r.priceEdit && part && parts.length === 1 ? [numCell(`${r.cid}|price`, t("Price"), r.단가, (x) => setCell(d, r, part, x))] : []), @@ -254,6 +273,7 @@ export function createSangeunSheet( ? [createBadge(it.비목, it.비목 === "별도" ? "warning" : "neutral")] : []; if (!nums) for (const tag of it.표) kids.push(createBadge(`${t("Table")} ${tag}`, "info")); + if (ctx && fx(it)) kids.push(...formulaToggle(ctx, it.자리, ctx.spots?.자리[it.자리])); if (ctx) kids.push(...spotTail(ctx, it.자리, !!r && r.added !== null)); if (r && opt.edit) { const drop = createButton({ @@ -310,8 +330,13 @@ export function createSangeunSheet( return el("span", { className: `ui-calc-row__out${key ? " is-key" : ""}`, text: String(text) }); }; + /** 식 칸 되는 줄 — 율 · 로직 더한줄 · 둘째 기초 세부 줄 뺌 */ + const fx = (it: SheetItem): boolean => + !!it.자리 && !(it.row?.sub || (it.row?.added != null && it.row.pct)); + function rowOf(d: SheetDraw, it: SheetItem): HTMLElement[] { - const nums = ctx && it.자리 ? formulaEditor(ctx, it.자리) : null; + const own = ctx && fx(it) ? rowFormula(ctx, it.자리, ctx.spots?.자리[it.자리]) : null; + const nums = ctx && it.자리 && !own ? formulaEditor(ctx, it.자리) : null; const shown = it.식 || (it.kind === "계산" ? t("Hidden") : ""); const middle = it.kind === "옵션" @@ -321,6 +346,7 @@ export function createSangeunSheet( children: [ ...(it.row || !nums ? [createCalcFormula(shown)] : []), ...(nums ? [nums] : []), + ...(own ? [own] : []), ...(it.row ? editLine(d, it.row) : []), ...(ctx ? reasonCell(ctx, it.자리, it.changed) : []), ], @@ -331,7 +357,8 @@ export function createSangeunSheet( if (it.자리) row.dataset.sheetSpot = it.자리; if (it.changed) row.classList.add("is-changed"); const notes = ctx && it.자리 ? noteRows(ctx, (x) => x.자리 === it.자리) : []; - return [row, ...notes]; + const after = ctx && it.자리 ? varRows(ctx, (v) => v.자리 === it.자리) : []; + return [row, ...notes, ...after]; } /** 790-3 — 새 문서로 바꿔 끼움(상세가 쥔 같은 객체) → 다시 셈 */ @@ -360,6 +387,10 @@ export function createSangeunSheet( covered: (s) => coverKinds(d.doc as ConstDoc, s).length > 0, }); const spots = new Set(items.map((it) => it.자리).filter(Boolean)); + const names = new Set( + ((d.doc as { 변수줄?: { 이름: string }[] }).변수줄 ?? []).map((v) => v.이름), + ); + const varSpot = (s: string): boolean => s.startsWith("mid:") && names.has(s.slice(4)); const out: HTMLElement[] = ctx ? lostBanner(ctx) : []; const add = (label: keyof typeof TEXT, click: () => void): HTMLElement[] => opt.edit @@ -375,7 +406,9 @@ export function createSangeunSheet( ? add("AddRate", opt.addRate) : sec === "근거" && ctx ? addTopNote(ctx) - : []; + : sec === "상수" && ctx + ? addVarButton(ctx) + : []; if (!mine.length && !tools.length) continue; out.push(head(sec, tools)); if (sec === "자원") @@ -383,6 +416,8 @@ export function createSangeunSheet( ...removedStrip(d.removed, opt.edit, (cid) => revive(d, cid)), ...(ctx ? removedReasons(ctx, d.removed) : []), ); + if (sec === "상수" && ctx) + out.push(...varRows(ctx, (v) => !v.자리 || (!spots.has(v.자리) && !varSpot(v.자리)))); for (const it of mine) out.push(...rowOf(d, it)); if (sec === "근거" && ctx) out.push(...noteRows(ctx, (x) => !spots.has(x.자리))); } @@ -418,6 +453,10 @@ export function createSangeunSheet( spots: spotsOf(d.ans), apply: (next) => apply(d, next), focus: (key) => void (focusNext = key), + scope: opt.scope, + open, + drafts, + redraw: () => last && draw(last), }; drawStatus(d); const active = document.activeElement as HTMLElement | null; diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetEdit.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetEdit.ts index 2698fc4c1..f1f459a35 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetEdit.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetEdit.ts @@ -103,7 +103,7 @@ function numBox( /** 식 칸 — 숫자 · 표 값을 칸으로(편집 · 서버 차례와 맞을 때만) · 못 그리면 null */ export function formulaEditor(c: EditCtx, spot: string): HTMLElement | null { const info = c.spots?.자리[spot]; - const parts = c.edit ? formulaParts(info) : null; + const parts = c.edit && !info?.식덮개 ? formulaParts(info) : null; if (!info || !parts) return null; const nums = c.doc.상수덮개?.[spot] ?? {}; const finds = c.doc.표값덮개?.[spot] ?? {}; @@ -251,7 +251,8 @@ export function restoreAll(c: EditCtx): HTMLElement[] { Object.keys(d.덮개 ?? {}).length + (d.뺀줄 ?? []).length + Object.keys(d.상수덮개 ?? {}).length + - Object.keys(d.표값덮개 ?? {}).length; + Object.keys(d.표값덮개 ?? {}).length + + Object.keys((d.식덮개 as object | undefined) ?? {}).length; if (!c.edit || !any) return []; const b = createButton({ label: t("RestoreAll"), diff --git a/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetFormula.ts b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetFormula.ts new file mode 100644 index 000000000..d95517573 --- /dev/null +++ b/M02_MasterTemplete/M02_MasterTemplete_Sangeun_SheetFormula.ts @@ -0,0 +1,287 @@ +/* ============================================================================= + * M02_MasterTemplete_Sangeun_SheetFormula.ts + * 단가산출(SK) 시트 식 칸(PLAN 792 · E4 · _분석_650-H3 2 장 「식 고침」) — 시트(`_Sangeun_Sheet`)가 부름. + * 식 칸 = 이름 꼴 글(`[노임 보통인부] × 무게 / 운반량`) · 칠 때마다 검사 길(`formula-check` · 옛 요청 끊기) + * → 틀리면 빨간 칸 + 까닭 한 줄 · 되면 바꿀 때(Enter · 떠남) 엔진 식을 문서에(`setFormula`) → 서버 셈 + * 변수 줄 = 이름 · 식 · 값 · [+ 변수 줄] · 빼기(`addVar` · `editVar` · `dropVar`) + * 빈 칸 · M01 식 그대로 = 식 덮개 풂 · 같은 자리 상수 · 표값 · 품 덮개는 식 덮개와 같이 못 둠 → 걷고 넣음 + * 문서 고치기 = `logic/M02_Sangeun_Formula_Model`(792-1 짝) · 금액은 늘 서버 셈 · 자동저장 없음 + * ========================================================================== */ + +import { createBadge } from "@ui/ui_template_badge"; +import { createButton } from "@ui/ui_template_button"; +import { createCalcSheetRow } from "@ui/ui_template_calc_row"; +import { el } from "@ui/ui_template_elements"; +import { createInputField } from "@ui/ui_template_input"; +import { currentLanguageIndex } from "@ui/ui_template_locale"; +import { createText } from "@ui/ui_template_text"; +import { call, type Layer } from "./M02_MasterTemplete_Api_Fetch"; +import type { EditCtx } from "./M02_MasterTemplete_Sangeun_SheetEdit"; +import { + addVar, + dropVar, + editVar, + setFormula, + type FormulaDoc, + type VarLine, +} from "./logic/M02_Sangeun_Formula_Model"; +import { num, type SpotInfo } from "./logic/M02_Sangeun_Sheet_Model"; + +const TEXT = { + Formula: ["식", "Formula"], + Edit: ["식 고침", "Edit formula"], + Close: ["식 닫기", "Close formula"], + Hint: ["예) 무게 / 운반량 × (1 + 할증율 / 100)", "e.g. weight / load × 1.2"], + Var: ["변수", "Variable"], + VarName: ["변수 이름", "Variable name"], + AddVar: ["+ 변수 줄", "+ Variable"], + DropVar: ["변수 빼기", "Remove variable"], + Bad: ["식이 틀려 안 넣음", "Invalid formula — not applied"], + Checking: ["검사 중", "Checking…"], +} as const satisfies Record; +const t = (k: keyof typeof TEXT): string => TEXT[k][currentLanguageIndex as 0 | 1] ?? TEXT[k][0]; + +export interface CheckAnswer { + 되나: boolean; + 까닭: string[]; + 알림: string[]; + /** 엔진 식(문서에 넣을 것) */ + 식: string; + /** 이름 꼴 글 */ + 글: string; +} + +export interface FormulaCtx extends EditCtx { + scope: { layer: Layer; owner: string | null }; + /** 식 칸 연 자리(화면 캐시) */ + open: Set; + /** 치는 중 글(다시 그려도 안 잃음) — 넣으면 지움 */ + drafts: Map; + redraw: () => void; +} + +const q = (c: FormulaCtx): string => + c.scope.owner && c.scope.layer !== "system" ? `?owner=${encodeURIComponent(c.scope.owner)}` : ""; + +/** 검사 길(792-1) — 셈 · 저장 없음 */ +export const checkFormula = ( + c: FormulaCtx, + spot: string, + text: string, + signal?: AbortSignal, +): Promise => + call(`/layers/${c.scope.layer}/sangeun/formula-check${q(c)}`, { + method: "POST", + body: JSON.stringify({ 문서: c.doc, 자리: spot, 식: text }), + signal, + }); + +/** 식 덮개와 같이 못 두는 같은 자리 상수 · 표값 · 품 덮개 걷음(배율은 둠) */ +export function clearNums(doc: T, spot: string): T { + const out: FormulaDoc = { ...doc }; + for (const name of ["상수덮개", "표값덮개"] as const) { + const all = { ...(doc[name] ?? {}) }; + delete all[spot]; + if (Object.keys(all).length) out[name] = all; + else delete out[name]; + } + const one = doc.덮개?.[spot]; + if (one && "qty" in one) { + const rest = { ...one }; + delete rest.qty; + const covers = { ...doc.덮개 }; + if (Object.keys(rest).length) covers[spot] = rest; + else delete covers[spot]; + out.덮개 = covers; + } + return out as T; +} + +/** 식 칸 — 칠 때마다 검사(앞 요청 끊음) · 바꿀 때 넣음 · `put(엔진 식 | null)` */ +function formulaBox( + c: FormulaCtx, + spot: string, + key: string, + shown: string, + origin: string, + put: (engine: string | null) => void, +): HTMLElement { + const box = createInputField({ + value: c.drafts.get(key) ?? shown, + compact: true, + size: 24, + placeholder: t("Hint"), + }); + box.input.setAttribute("aria-label", t("Formula")); + box.input.dataset.sheetCell = key; + box.input.spellcheck = false; + const note = createText("", "muted", 12); + note.dataset.sheetCheck = key; + let timer = 0; + let ctl: AbortController | null = null; + let last: Promise | null = null; + const run = (text: string): Promise => { + ctl?.abort(); + const mine = (ctl = new AbortController()); + note.textContent = t("Checking"); + last = checkFormula(c, spot, text, mine.signal).then( + (got) => { + if (mine !== ctl) return got; + box.setError(got.되나 ? undefined : got.까닭.join(" · ")); + note.textContent = got.알림.join(" · "); + return got; + }, + (error: unknown) => { + if (mine !== ctl || mine.signal.aborted) return null; + box.setError(error instanceof Error ? error.message : String(error)); + note.textContent = ""; + return null; + }, + ); + return last; + }; + box.input.addEventListener("input", () => { + c.drafts.set(key, box.input.value); + window.clearTimeout(timer); + const text = box.input.value.trim(); + if (!text) return void (ctl?.abort(), box.setError(undefined), (note.textContent = "")); + timer = window.setTimeout(() => void run(text), 150); + }); + box.input.addEventListener("change", async () => { + window.clearTimeout(timer); + const text = box.input.value.trim(); + if (!text || text === origin) { + c.drafts.delete(key); + return put(null); + } + const got = await run(text); + if (!got?.되나) return void (note.textContent = t("Bad")); + c.drafts.delete(key); + put(got.식); + }); + return el("div", { + className: "m02-sks__fx", + attrs: { "data-sheet-fx": spot }, + children: [box.root, note], + }); +} + +/** 줄 식 칸 — 열었거나 식 덮개 있는 자리만 · 못 고치는 자리(로직 · 율 더한줄) = null */ +export function rowFormula( + c: FormulaCtx, + spot: string, + info: SpotInfo | undefined, +): HTMLElement | null { + if (!c.edit || !spot || !info || (!c.open.has(spot) && !info.식덮개)) return null; + const shown = info.식글 ?? info.식 ?? ""; + const origin = info.원식글 ?? ""; + return formulaBox(c, spot, `${spot}|f`, shown, origin, (engine) => { + const doc = engine ? clearNums(c.doc as FormulaDoc, spot) : (c.doc as FormulaDoc); + c.apply(setFormula(doc, spot, engine)); + }); +} + +/** 꼬리 [식 고침] · [식 닫기] — 식 덮개 있으면 늘 열림(닫기 없음) */ +export function formulaToggle( + c: FormulaCtx, + spot: string, + info: SpotInfo | undefined, +): HTMLElement[] { + const addOne = /^add:#\d+$/.test(spot); // 더한줄 수량식(율 · 로직 줄은 부르는 쪽이 뺌) + if (!c.edit || !spot || !info || info.식덮개 || (info.식 == null && !addOne)) return []; + const on = c.open.has(spot); + const b = createButton({ + label: on ? t("Close") : t("Edit"), + variant: "ghost", + size: 22, + onClick: () => { + if (on) c.open.delete(spot); + else c.open.add(spot); + c.drafts.delete(`${spot}|f`); + c.focus(on ? "" : `${spot}|f`); + c.redraw(); + }, + }); + b.dataset.sheetFxOpen = spot; + return [b]; +} + +const vars = (c: FormulaCtx): VarLine[] => (c.doc as FormulaDoc).변수줄 ?? []; + +/** 자리 뒤 변수 줄들(이어 붙은 변수까지) */ +export function varRows(c: FormulaCtx, after: (v: VarLine) => boolean): HTMLElement[] { + return vars(c).flatMap((v) => + after(v) ? [varRow(c, v), ...varRows(c, (x) => x.자리 === `mid:${v.이름}`)] : [], + ); +} + +function varRow(c: FormulaCtx, v: VarLine): HTMLElement { + const spot = `mid:${v.이름}`; + const info = c.spots?.자리[spot]; + const label = c.edit + ? (() => { + const box = createInputField({ value: v.이름, compact: true, size: 24 }); + box.input.setAttribute("aria-label", t("VarName")); + box.input.dataset.sheetCell = `var|${v.이름}`; + box.input.addEventListener("change", () => { + const to = box.input.value.trim(); + if (!to || to === v.이름 || vars(c).some((x) => x.이름 === to)) + return void (box.input.value = v.이름); + c.focus(`var|${to}`); + c.apply(editVar(c.doc as FormulaDoc, v.이름, { 이름: to })); + }); + return box.root; + })() + : el("span", { className: "ui-calc-row__label", text: v.이름 }); + const middle = c.edit + ? formulaBox(c, spot, `${spot}|f`, info?.식글 ?? v.식, "", (engine) => + c.apply(editVar(c.doc as FormulaDoc, v.이름, { 식: engine ?? "0" })), + ) + : el("span", { className: "ui-calc-formula", text: info?.식글 ?? v.식 }); + const value = el("span", { + className: "ui-calc-row__out", + text: typeof info?.지금 === "number" ? num(info.지금) : "—", + }); + const tail = el("span", { + children: [ + createBadge(t("Var"), "accent"), + ...(c.edit + ? [ + createButton({ + label: t("DropVar"), + variant: "danger-ghost", + size: 22, + onClick: () => c.apply(dropVar(c.doc as FormulaDoc, v.이름)), + }), + ] + : []), + ], + }); + const row = createCalcSheetRow(label, middle, value, tail); + row.dataset.sheetKind = "변수"; + row.dataset.sheetSpot = spot; + row.classList.add("is-changed"); + return row; +} + +/** [+ 변수 줄] — 맨 위(자리 "") · 이름 = 안 겹치는 「변수n」 */ +export function addVarButton(c: FormulaCtx): HTMLElement[] { + if (!c.edit) return []; + const b = createButton({ + label: t("AddVar"), + variant: "ghost", + size: 22, + onClick: () => { + const used = new Set([ + ...vars(c).map((v) => v.이름), + ...(c.spots?.이름표 ?? []).map((x) => x.글), + ]); + let n = 1; + while (used.has(`변수${n}`)) n++; + c.focus(`var|변수${n}`); + c.apply(addVar(c.doc as FormulaDoc, { 이름: `변수${n}`, 식: "0", 자리: "" }, 0)); + }, + }); + b.dataset.sheetAddVar = ""; + return [b]; +} diff --git a/M02_MasterTemplete/logic/M02_Sangeun_Sheet_Model.ts b/M02_MasterTemplete/logic/M02_Sangeun_Sheet_Model.ts index ea5f709b6..82e3ecce3 100644 --- a/M02_MasterTemplete/logic/M02_Sangeun_Sheet_Model.ts +++ b/M02_MasterTemplete/logic/M02_Sangeun_Sheet_Model.ts @@ -232,12 +232,18 @@ export interface SpotInfo { 덮개?: string[]; 까닭?: string | null; M01바뀜?: boolean; + /** 792-1 — 지금 식 · M01 식(이름 꼴) · 식 덮개 있음 */ + 식글?: string; + 원식글?: string; + 식덮개?: boolean; } export interface SheetSpots { 자리: Record; 글줄: NoteLine[]; 짝잃음: { cid: string; 까닭: string }[]; + /** 792-1 식 칸에 쓸 수 있는 이름 */ + 이름표?: { 글: string; 갈래: string }[]; } export const spotsOf = (ans: SkAnswer | null): SheetSpots | null => diff --git a/resources/tester/test_790_1_sangeun_sheet.py b/resources/tester/test_790_1_sangeun_sheet.py index 1894c766d..be9d5658b 100644 --- a/resources/tester/test_790_1_sangeun_sheet.py +++ b/resources/tester/test_790_1_sangeun_sheet.py @@ -272,3 +272,39 @@ def test_790_3_식_조각_글줄_산출서_열쇠_밂(): assert got["lift"]["까닭"] == {"add:#0": "a", "add:#2/x#0": "b", "mid:q": "c"} assert got["lift"]["글줄"] == [{"자리": "add:#3", "글": "g"}] assert got["spotsNone"] is None + + +# ── 792 식 칸 화면(줄 식 고침 · 칠 때마다 검사 · 변수 줄) ──────────────────────────── +FX = M02 / "M02_MasterTemplete_Sangeun_SheetFormula.ts" + + +def test_792_식_칸_정적(): + s = _text(FX) + assert " v.자리 === it.자리)", + "addVarButton(ctx)", + "scope: opt.scope,", + 'coverKinds(d.doc as ConstDoc, r.cid).includes("식")', # 식 덮개 = 품 칸 안 둠 + ): + assert call in sheet, call + assert "addRate: () => rateButton.click(), scope }" in _text(DETAIL) # 탭 등록 줄에 층만 + assert "fetch(" not in s # 길 = Api_Fetch.call(로그인 쿠키)