Merge remote-tracking branch 'origin/sub_laptop_3' into main_laptop_1
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<string>(); // 식 칸 연 자리
|
||||
const drafts = new Map<string, string>(); // 치는 중 식 글
|
||||
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;
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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<string, readonly [string, string]>;
|
||||
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<string>;
|
||||
/** 치는 중 글(다시 그려도 안 잃음) — 넣으면 지움 */
|
||||
drafts: Map<string, string>;
|
||||
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<CheckAnswer> =>
|
||||
call<CheckAnswer>(`/layers/${c.scope.layer}/sangeun/formula-check${q(c)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ 문서: c.doc, 자리: spot, 식: text }),
|
||||
signal,
|
||||
});
|
||||
|
||||
/** 식 덮개와 같이 못 두는 같은 자리 상수 · 표값 · 품 덮개 걷음(배율은 둠) */
|
||||
export function clearNums<T extends FormulaDoc>(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<CheckAnswer | null> | null = null;
|
||||
const run = (text: string): Promise<CheckAnswer | null> => {
|
||||
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];
|
||||
}
|
||||
@@ -232,12 +232,18 @@ export interface SpotInfo {
|
||||
덮개?: string[];
|
||||
까닭?: string | null;
|
||||
M01바뀜?: boolean;
|
||||
/** 792-1 — 지금 식 · M01 식(이름 꼴) · 식 덮개 있음 */
|
||||
식글?: string;
|
||||
원식글?: string;
|
||||
식덮개?: boolean;
|
||||
}
|
||||
|
||||
export interface SheetSpots {
|
||||
자리: Record<string, SpotInfo>;
|
||||
글줄: NoteLine[];
|
||||
짝잃음: { cid: string; 까닭: string }[];
|
||||
/** 792-1 식 칸에 쓸 수 있는 이름 */
|
||||
이름표?: { 글: string; 갈래: string }[];
|
||||
}
|
||||
|
||||
export const spotsOf = (ans: SkAnswer | null): SheetSpots | null =>
|
||||
|
||||
@@ -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 "<button" not in s and "<input" not in s and "document.createElement" not in s
|
||||
assert "sessionStorage." not in s and len(s.splitlines()) < 700
|
||||
for call in (
|
||||
"/sangeun/formula-check", # 검사 길(셈 · 저장 없음)
|
||||
"ctl?.abort();", # 칠 때마다 · 옛 요청 끊기
|
||||
"signal,",
|
||||
'box.input.addEventListener("input"',
|
||||
'box.setError(got.되나 ? undefined : got.까닭.join(" · "))', # 빨간 칸 + 까닭
|
||||
"if (!got?.되나) return", # 틀린 식은 문서에 안 넣음
|
||||
"setFormula(doc, spot, engine)", # 엔진 식을 문서에
|
||||
"clearNums(c.doc as FormulaDoc, spot)", # 같은 자리 상수 · 표값 · 품 덮개 걷음
|
||||
"addVar(c.doc as FormulaDoc,",
|
||||
"editVar(c.doc as FormulaDoc, v.이름, { 이름: to })",
|
||||
"dropVar(c.doc as FormulaDoc, v.이름)",
|
||||
):
|
||||
assert call in s, call
|
||||
sheet = _text(SHEET)
|
||||
for call in (
|
||||
"rowFormula(ctx, it.자리,",
|
||||
"formulaToggle(ctx, it.자리,",
|
||||
"varRows(ctx, (v) => 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(로그인 쿠키)
|
||||
|
||||
Reference in New Issue
Block a user