- 로직 상세 보기 첫 칸 「시트」(기본) — 이름 | 식 | 값 | 비목 · 근거 · 옵션(시험 값) · 변수 · 계산 · 자원 · 율 · 덧줄 · 단계 · 끝수 · 소계 · 합계 · 상태줄 지금 · 고치기 전 - 식 글 = 이름 꼴(M02 단가산출 시트와 같은 번역 · 새 서버 M01_MasterData_Sheet.py · POST /api/m01/logic/sheet · /logic/sheet/check · 저장 때와 같은 엔진 검사 · 쓰기 없음) - 식 고침 = 시스템 관리자만 · 되는 식만 줄 사본에 · 캐시 → 기존 [저장](개정 확인 · 서버 검사 422) · [원래로] - 식 칸 몸 = 공용 A00_Common/sheet_formula_cell.ts — M02 단가산출 시트도 같은 것을 씀(동작 그대로) - 자체 로직(GX) 편집기 식 날 입력칸 → 읽기만(시트 식 칸으로 갈음) - 시험: test_794_1_m01_sheet 새로 · test_790_1 식 칸 문구 자리를 공용 파일로 옮김(식 칸 몸이 공용으로 감) · test_717_2b · test_722 LogicLab.ts 줄 상한 627 → 637(시트 이음 10 줄 · 셈 · 고침은 시트 파일로 뺌 · 700 아래) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.9 KiB
TypeScript
102 lines
3.9 KiB
TypeScript
/* =============================================================================
|
||
* A00_Common/sheet_formula_cell.ts
|
||
* 줄 식 시트 식 칸 한 벌(PLAN 792 · 794-1) — M02 단가산출 시트 · M01 로직 시트가 같이 씀.
|
||
* 칠 때마다 검사(`check` · 150 ms 쉼 · 앞 요청 끊음) → 틀리면 빨간 칸 + 까닭 · 알림 한 줄
|
||
* 바꿀 때(Enter · 떠남) — 빈 칸 · 원래 식 그대로 = `put(null)` · 되면 `put(엔진 식)` · 틀리면 안 넣음
|
||
* 치는 중 글 = `drafts`(다시 그려도 안 잃음) · 부품 = 입력 · 글 · 서버 검사 길은 부르는 쪽 몫
|
||
* ========================================================================== */
|
||
|
||
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";
|
||
|
||
const TEXT = {
|
||
Formula: ["식", "Formula"],
|
||
Hint: ["예) 무게 / 운반량 × (1 + 할증율 / 100)", "e.g. weight / load × 1.2"],
|
||
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 FormulaVerdict {
|
||
되나: boolean;
|
||
까닭: string[];
|
||
알림?: string[];
|
||
식: string | null;
|
||
}
|
||
|
||
export interface FormulaCellOptions {
|
||
/** 시트 칸 열쇠(초점 되찾기 · 치는 중 글) */
|
||
key: string;
|
||
/** 칸 그릇 이름표 값(`data-sheet-fx`) */
|
||
spot: string;
|
||
shown: string;
|
||
/** 원래 식 글 — 그대로 두면 `put(null)` */
|
||
origin: string;
|
||
drafts: Map<string, string>;
|
||
check: (text: string, signal: AbortSignal) => Promise<FormulaVerdict>;
|
||
put: (engine: string | null) => void;
|
||
className?: string;
|
||
}
|
||
|
||
export function createFormulaCell(o: FormulaCellOptions): HTMLElement {
|
||
const box = createInputField({
|
||
value: o.drafts.get(o.key) ?? o.shown,
|
||
compact: true,
|
||
size: 24,
|
||
placeholder: t("Hint"),
|
||
});
|
||
box.input.setAttribute("aria-label", t("Formula"));
|
||
box.input.dataset.sheetCell = o.key;
|
||
box.input.spellcheck = false;
|
||
const note = createText("", "muted", 12);
|
||
note.dataset.sheetCheck = o.key;
|
||
let timer = 0;
|
||
let ctl: AbortController | null = null;
|
||
const run = (text: string): Promise<FormulaVerdict | null> => {
|
||
ctl?.abort();
|
||
const mine = (ctl = new AbortController());
|
||
note.textContent = t("Checking");
|
||
return o.check(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;
|
||
},
|
||
);
|
||
};
|
||
box.input.addEventListener("input", () => {
|
||
o.drafts.set(o.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 === o.origin) {
|
||
o.drafts.delete(o.key);
|
||
return o.put(null);
|
||
}
|
||
const got = await run(text);
|
||
if (!got?.되나 || !got.식) return void (note.textContent = t("Bad"));
|
||
o.drafts.delete(o.key);
|
||
o.put(got.식);
|
||
});
|
||
return el("div", {
|
||
className: o.className ?? "m02-sks__fx",
|
||
attrs: { "data-sheet-fx": o.spot },
|
||
children: [box.root, note],
|
||
});
|
||
}
|