/* ============================================================================= * M02_MasterTemplete/logic/M02_Sangeun_Formula_Model.ts * 단가산출(SK) 사용자 식 칸 캐시 고치기(PLAN 792-1 · 요구서_652 · _분석_650-H3 E4) — * 서버 `M02_MasterTemplete/M02_Sangeun_Formula.py`(꼴 검사 · 번역 · 셈)와 같은 꼴 · 서버 짝 함수 없음 * · 시험 `resources/tester/test_792_1_sangeun_formula.py`(여기 결과를 서버 `clean` 이 받음). * 변수줄 [{이름, 식, 표시?, 자리}] 엔진 중간 줄 · 자리 = 그 뒤 중간 `mid:이름`("" = 맨 위) * 식덮개 {자리: 식} 중간 `mid:이름` · 호표 · 덧줄 cid * 더한줄[k].수량식 율 줄 · 로직 줄 빼고 * 식 글 = 검사 길(`formula-check`) 답 `식`(엔진) 을 넣음 · 이름 꼴 글을 넣어도 서버가 번역. * 금액은 늘 서버가 셈 — 여기는 문서 칸 고치기만(새 문서를 돌려줌 · 받은 문서 안 바꿈). * 화면 · fetch · sessionStorage 없음(`CLAUDE.md` 4장). * ========================================================================== */ import type { ConstDoc } from "./M02_Sangeun_Const_Model"; export interface VarLine { 이름: string; 식: string; 표시?: number; 자리: string; } export interface FormulaDoc extends ConstDoc { 변수줄?: VarLine[]; 식덮개?: Record; } const ADD_ONE = /^add:#(\d+)$/; /** 빈 묶음 · 목록 칸은 지움(서버처럼 없으면 안 둠). */ function putField(doc: T, name: string, value: unknown): T { const out: FormulaDoc = { ...doc }; const empty = value == null || (Array.isArray(value) ? value.length === 0 : typeof value === "object" && Object.keys(value as object).length === 0); if (empty) delete out[name]; else out[name] = value; return out as T; } const isVar = (doc: FormulaDoc, spot: string): boolean => spot.startsWith("mid:") && (doc.변수줄 ?? []).some((v) => v.이름 === spot.slice(4)); /** 자리 식 — 변수 줄 = 그 식 · `add:#k` = 수량식 · 그 밖 = 식덮개 · null · "" = 지움(M01 식 · 수). */ export function setFormula(doc: T, spot: string, text: string | null): T { const t = (text ?? "").trim(); if (isVar(doc, spot)) return editVar(doc, spot.slice(4), { 식: t }); const hit = ADD_ONE.exec(spot); if (hit) { const k = Number(hit[1]); const adds = (doc.더한줄 ?? []).map((x, i) => { if (i !== k) return x; const one: Record = { ...x }; if (t) one.수량식 = t; else delete one.수량식; return one; }); return { ...doc, 더한줄: adds }; } const covers = { ...(doc.식덮개 ?? {}) }; if (t) covers[spot] = t; else delete covers[spot]; return putField(doc, "식덮개", covers); } /** 변수 줄 넣기 — `at` 차례(없으면 끝) · 같은 이름이 있으면 그대로. */ export function addVar(doc: T, line: VarLine, at?: number): T { const lines = [...(doc.변수줄 ?? [])]; if (lines.some((v) => v.이름 === line.이름)) return doc; lines.splice(at ?? lines.length, 0, { ...line }); return putField(doc, "변수줄", lines); } /** 변수 줄 고침 — 이름 바꾸면 까닭 · 글줄 · 다른 변수 자리 열쇠 `mid:이름` 도 따라감(식 속 이름은 * 그대로 — 검사 길이 짚음). */ export function editVar(doc: T, name: string, patch: Partial): T { const to = patch.이름 ?? name; const lines = (doc.변수줄 ?? []).map((v) => { if (v.이름 === name) { const one: VarLine = { ...v, ...patch }; if (patch.표시 == null && "표시" in patch) delete one.표시; return one; } return v.자리 === `mid:${name}` ? { ...v, 자리: `mid:${to}` } : v; }); const out = putField(doc, "변수줄", lines); return to === name ? out : moveKey(out, `mid:${name}`, `mid:${to}`); } /** 변수 줄 빼기 — 그 뒤에 붙은 변수는 그 자리를 이어받음 · 까닭 지움 · 글줄은 맨 위로. */ export function dropVar(doc: T, name: string): T { const gone = (doc.변수줄 ?? []).find((v) => v.이름 === name); if (!gone) return doc; const lines = (doc.변수줄 ?? []) .filter((v) => v.이름 !== name) .map((v) => (v.자리 === `mid:${name}` ? { ...v, 자리: gone.자리 } : v)); return moveKey(putField(doc, "변수줄", lines), `mid:${name}`, null); } /** 까닭 · 글줄 열쇠 옮김(null = 까닭 지움 · 글줄 맨 위). */ function moveKey(doc: T, from: string, to: string | null): T { let out: T = doc; if (doc.까닭 && from in doc.까닭) { const why = { ...doc.까닭 }; const text = why[from]; delete why[from]; if (to) why[to] = text; out = putField(out, "까닭", why); } if (doc.글줄) { out = { ...out, 글줄: doc.글줄.map((t) => (t.자리 === from ? { ...t, 자리: to ?? "" } : t)) }; } return out; }