Files
Aislo/A00_Common/formula_doc_edit.ts
T
eomsangdonandClaude Opus 5.5 f5545dd62b feat(803-2a): 수식 탭 세 영역 · 하나의 수식 영역 · 합계 상자 · 설명 글 지움
- 세 영역 가로 — 기호 식(줄바꿈으로 이어진 하나의 수식 영역) | 값 식(같은 높이 줄) | 기호 정의
- 맨 아래 합계 상자 = 단가산출 상세 탭 부품(노무비 · 재료비 · 경비 · 계) · 옵션 아래 계 줄 · 설명 글 · 표지 안내 줄 걷음(오류 = 그 기호 빨강)
- 기호 식 줄 누름 = 그 자리 편집기(치는 대로 조판 — / 분수 · _ 아래첨자 · ^ 위첨자 · 괄호 · 화살표 구조 이동 · Enter 새 줄 · Esc 나옴 · 한글 조합) · 편집 중만 영역 위 수식 도구 줄
- 금액 줄 = 영역 안 식 줄 「노무비 형틀목공 = w × Q」(머리 첫 말 = 비목) · 「+ 식 줄」 「+ 금액 줄」 「+ 묶음」 · 비목 고르기 칸 걷음 · 묶음 = 도구 줄 [묶음]
- 저장 전 새 로직 = 세 영역 빈 화면 · 끊은 단가산출 블록도 같은 편집기
- 새 라이브러리 없음 · 서버 길 · 계산기 · 문서 꼴 그대로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 21:07:59 +09:00

155 lines
5.5 KiB
TypeScript

/* =============================================================================
* formula_doc_edit.ts
* 수식 문서 고침(PLAN 802-3 · 802-4b-2) — 식 줄 · 금액 줄 · 값 식 숫자 고침을 문서에 얹음(화면 · fetch 없음)
* 수식 화면(`formula_doc_screen.ts`)과 끊은 단가산출 블록(`formula_doc_sub.ts`)이 같은 고침을 씀
* 수식 영역 한 줄 글(803-2a) = `editRow`(식 줄 · 금액 줄 「노무비 형틀목공 = w × Q」) · 빈 줄 걷기 `sweep`
* ========================================================================== */
import {
goodId,
splitId,
textOf,
tidy,
varId,
type DocVar,
type FormulaDoc,
type MoneyLine,
} from "../common_util/common_util_formula_doc";
import { swapLit, tryParse } from "./formula_doc_render";
/** 머리 = 식 — 첫 「=」(비교 ==, <=, >=, != 빼고) */
export function splitLine(text: string): [string | null, string] {
const m = /^([^=<>!]*?)\s*(?<![<>!=])=(?!=)\s*([\s\S]*)$/.exec(text);
if (!m) return [null, text.trim()];
return [m[1].trim(), m[2].trim()];
}
export const tidyText = (t: string): string => (t.trim() ? tidy(t.trim()) : "");
/** 기호 정의 — 없으면 「식」 갈래로 새로 */
export function ensureVar(d: FormulaDoc, id: string): DocVar {
const have = (d.변수 ??= []).find((v) => varId(v) === id);
if (have) return have;
const [기호, 아래, 위] = splitId(id);
const v: DocVar = { 열쇠: id, 기호, 아래, 위, 이름: "", 단위: "", 연결: { 갈래: "식" } };
d.변수.push(v);
return v;
}
/** 식 줄 i 를 글로 — 빈 글 = 줄 지움 · 머리 바뀜 = 식 갈래 정의도 같이 */
export function editLine(d: FormulaDoc, i: number, text: string): void {
const lines = (d.식 ??= []);
const line = lines[i];
if (!line) return;
if (!text.trim()) {
lines.splice(i, 1);
return;
}
const [h, body] = splitLine(text);
const old = String(line.기호 ?? "");
if (h !== null && h !== old) {
line.기호 = h;
const was = (d.변수 ?? []).find((v) => varId(v) === old && (v.연결?.갈래 ?? "식") === "식");
if (was && goodId(h) && !(d.변수 ?? []).some((v) => varId(v) === h)) {
const [기호, 아래, 위] = splitId(h);
Object.assign(was, { 기호, 아래, 위 });
}
}
line.식 = tidyText(body);
}
/** 금액 줄 i 고침(null = 지움) — 단가 · 수량 글은 다듬음 · 빈 수량 = 칸 뺌 */
export function editMoney(d: FormulaDoc, i: number, patch: Partial<MoneyLine> | null): void {
const lines = (d.금액 ??= []);
if (!patch) {
lines.splice(i, 1);
return;
}
const line = lines[i];
if (!line) return;
for (const [k, v] of Object.entries(patch)) {
const text = typeof v === "string" && (k === "단가" || k === "수량") ? tidyText(v) : v;
if (k === "수량" && text === "") delete line.수량;
else (line as Record<string, unknown>)[k] = text;
}
}
/** 값 식 숫자 고침 — 식 줄 · 금액 줄 단가 · 수량 글의 at 번째 수 */
export function editLit(
d: FormulaDoc,
target: "line" | "price" | "qty",
i: number,
at: number,
value: number,
): void {
if (target === "line") {
const line = d.식?.[i];
if (line) line.식 = swapLit(String(line.식 ?? ""), at, value);
return;
}
const line = d.금액?.[i];
if (!line) return;
const slot = target === "price" ? "단가" : "수량";
line[slot] = swapLit(String(line[slot] ?? ""), at, value);
}
const KINDS = ["노무비", "재료비", "경비", "묶음"];
/** 금액 줄 몸 → 단가 · 수량(맨 위 × 의 오른쪽 = 수량 · 없으면 단가만) */
function splitMoney(body: string): { 단가: string; 수량: string } {
const n = tryParse(body);
if (n && n[0] === "bin" && n[1] === "*") return { 단가: textOf(n[2]), 수량: textOf(n[3]) };
return { 단가: body, 수량: "" };
}
/** 수식 영역 한 줄 글 → 문서(803-2a) — 머리 첫 말이 비목이면 금액 줄 · 아니면 식 줄(갈래가 바뀌면 옮김)
* 빈 글 = 줄 지움 · 돌려줌 = 고친 뒤 그 줄 객체(지웠으면 null) */
export function editRow(
d: FormulaDoc,
kind: "line" | "money",
i: number,
text: string,
): object | null {
const lines = (d.식 ??= []);
const money = (d.금액 ??= []);
const was = kind === "line" ? lines[i] : money[i];
if (!was) return null;
if (!text.trim()) {
(kind === "line" ? lines : money).splice(i, 1);
return null;
}
const [h, body] = splitLine(text);
const words = (h ?? text).trim().split(/\s+/);
const cost = KINDS.includes(words[0]) ? words[0] : null;
if (!cost) {
if (kind === "line") {
editLine(d, i, text);
return lines[i];
}
money.splice(i, 1);
lines.push({ 기호: "", 식: "" });
editLine(d, lines.length - 1, text);
return lines[lines.length - 1];
}
const patch = {
비목: cost,
이름: words.slice(1).join(" "),
...splitMoney(h === null ? "" : body),
};
if (kind === "money") {
editMoney(d, i, patch);
return money[i];
}
lines.splice(i, 1);
const group = money.length ? money[money.length - 1].묶음 : undefined;
money.push({ 이름: "", 비목: cost, 단가: "", ...(group ? { 묶음: group } : {}) });
editMoney(d, money.length - 1, patch);
return money[money.length - 1];
}
/** 빈 줄 걷기(`keep` 빼고) */
export function sweep(d: FormulaDoc, keep?: object | null): void {
d.식 = (d.식 ?? []).filter((l) => l === keep || String(l.기호 ?? "") || String(l.식 ?? ""));
d.금액 = (d.금액 ?? []).filter((l) => l === keep || l.이름 || l.단가 || l.수량);
}