Files
Aislo/A00_Common/formula_doc_copy.ts
T
eomsangdonandClaude Opus 5.5 8bb22b39b1 fix(823-1): 불러온 단가산출 기계 바꿔 끼움 = 그 줄만 우리 값 즉시 · 바꿈 표시 · 되돌리기
- 원인: 다시 고르기(repick)가 엑셀 값 기억을 일부러 남김(812-9) → 엑셀 단가 기준 보기가 바꾼 기계 줄도 옛 엑셀 값으로 셈 · 이름만 바뀜
- 이은 가져온 줄을 다른 것으로 = 엑셀 값 뗌 · 연결 `바꿈` = 원래 연결 · 이름 · 규격 · 원래 것 다시 고름 = 되돌림 · 못 이은 줄 고름(812-9)은 그대로
- 줄에 「바꿈」 배지 + [되돌리기] · 머리 「바꾼 줄 n — 원본과 다름」 · 인력 · 재료 바꿈도 같은 길
- 서버: 바꾼 줄 있으면 셈 색인 = 로직 사본 위 · 저장 때 셈이 읽은 줄(내 기계 등)을 로직 사본에 박음(822-1 사본 규칙)
- 옛 칸 지문: 파일 24554.0 = 화면 왕복 24554 — 가져온 식 수식 저장 뒤 페이지 [저장] 409(「그 사이 양식이 바뀜」) 막음
- 시험 test_823_1_imported_swap 5(+ 헬퍼 helper_823_1_swap.cjs)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 18:54:16 +09:00

145 lines
6.6 KiB
TypeScript

/* =============================================================================
* formula_doc_copy.ts
* 수식 문서 사본 몫(PLAN 802-4b-2 · 요구 655-2 · 656 · 659-3) — M02 단가산출(SK) 수식 탭 머리
* 「M01 새 판 있음」 배지(값 줄 · 부름 문서가 지금 M01 과 다름) · [단가 갱신](자원 단가만 · 식 그대로) ·
* [재생성](지금 M01 문서로 다시 찍음 · 버리는 것 알림) — 누를 때만(자동 갱신 없음)
* 흐름 = 미리 셈(`{길}/preview` {할일}) → 확인 창(계 전 → 후 · 바뀐 자원 · 버리는 것) → 저장(`{길}/save` {할일, 판})
* 층 쓰기 권한 없으면 「읽기만」 배지
* (821-3) 값 줄만 바뀜(M01 단가 · 내 기계 고침) = 「새 값 있음」(같은 자리 · 같은 꼴) · 지운 키(M01 · 내 기계) = 「지워짐 n」 ·
* [단가 갱신] 확인 창 = 바뀐 자원 줄(이름 · 바뀐 칸 전 → 후 · 지워짐 = 마지막 값 유지)
* (822-1) 값 사본에 없는 셈 줄(옛 저장본 빈틈) = 「사본 없는 줄 n」(지금 값으로 셈 · 저장하면 굳음)
* ========================================================================== */
import { createBadge } from "@ui/ui_template_badge";
import { createButton } from "@ui/ui_template_button";
import { eqText } from "@ui/ui_template_equation";
import { showConfirmDialog } from "@ui/ui_template_elements";
import type { FormulaDoc } from "../common_util/common_util_formula_doc";
import type { Basis } from "../common_util/common_util_formula_doc_import";
import { swappedCount } from "../common_util/common_util_line_stack_pick";
import { ft } from "./formula_doc_text";
export type CopyJob = "단가갱신" | "재생성";
/** 사본 표지(서버 `M02_Doc_Copy.state` + 시점 · 따로) */
export interface CopyMark {
M01새판?: boolean;
지운키?: string[];
단가새값?: boolean;
/** 값 사본에 없는 셈 줄 수(822-1) — 지금 값으로 셈 · 저장하면 굳음 */
사본없음?: number;
문서새판?: string[];
따로?: boolean;
/** 가져온 식(엑셀 불러오기 · 807-1) 단가기준 — 엑셀 = 엑셀 값 기억 · 우리 = 연결 값 */
단가기준?: Basis;
}
export interface CopyOptions {
/** SK 키 · 꼬리(`""` · `/preview` · `/table?key=…`) → 길(`/m02/layers/{층}/sangeun/{키}/doc…` · 주인 물음) */
route: (key: string, path: string) => string;
/** 저장 뒤 그 SK 새 판(페이지 [저장] 409 막음) */
onSaved?: (version: string) => void;
}
type Money = Record<string, number | null> | null;
export interface JobReply {
전?: Money;
후?: Money;
바뀐자원?: unknown[];
버림?: { 문서고침?: boolean; 끊음?: string[] };
}
/** 「M01 새 판 있음」 — 값 줄 · 단가 · 부름 문서 어느 하나라도 지금 M01 과 다름 */
export function isStale(m: CopyMark | undefined): boolean {
return !!m && (!!m.M01새판 || !!m.단가새값 || !!m.지운키?.length || !!m.문서새판?.length);
}
export function copyBadges(m: CopyMark | undefined, edit: boolean): HTMLElement[] {
const out: HTMLElement[] = [];
const doc = !!m?.M01새판 || !!m?.문서새판?.length;
if (doc || m?.단가새값) {
const b = createBadge(ft(doc ? "New_M01" : "New_Value"), "danger");
b.dataset.copyStale = doc ? "doc" : "value";
out.push(b);
}
if (m?.지운키?.length) {
const b = createBadge(`${ft("Gone")} ${m.지운키.length}`, "warning");
b.title = `${m.지운키.join(" · ")} — ${ft("Gone_Note")}`;
b.dataset.copyGone = m.지운키.join(",");
out.push(b);
}
if (m?.사본없음) {
const b = createBadge(`${ft("Gap")} ${m.사본없음}`, "warning");
b.title = ft("Gap_Note");
b.dataset.copyGap = String(m.사본없음);
out.push(b);
}
if (!edit) out.push(createBadge(ft("Read_Only"), "neutral"));
return out;
}
/** 가져온 식 바꾼 줄 배지(823-1) — 「바꾼 줄 n — 원본과 다름」 · 없으면 없음 */
export function swapBadge(doc: FormulaDoc): HTMLElement[] {
const n = swappedCount(doc);
if (!n) return [];
const b = createBadge(ft("Swapped_N").replace("{n}", String(n)), "info");
b.dataset.swappedN = String(n);
return [b];
}
export function copyTools(edit: boolean, run: (job: CopyJob) => void): HTMLElement[] {
if (!edit) return [];
return (["단가갱신", "재생성"] as const).map((job) => {
const b = createButton({
label: job === "단가갱신" ? ft("Price") : ft("Regen"),
variant: "ghost",
size: 28,
onClick: () => run(job),
});
b.dataset.copyJob = job;
return b;
});
}
const sum = (m: Money | undefined): string =>
typeof m?.계 === "number" ? `${eqText(m.계)} 원` : "—";
/** 바뀐 자원 한 줄 — 「이름 칸 전 → 후」(값 묶음에서 다른 칸만) · 지워짐 = 마지막 값 유지 */
export function changedLine(x: unknown): string {
const r = (x ?? {}) as {
키?: string;
이름?: string;
전?: unknown;
후?: unknown;
지워짐?: boolean;
};
const name = r.이름 || r.키 || "";
if (r.지워짐) return `${name} ${ft("Gone")}(${ft("Gone_Note")})`;
const show = (v: unknown): string => (typeof v === "number" ? eqText(v) : String(v ?? "—"));
const a = r.전 && typeof r.전 === "object" ? (r.전 as Record<string, unknown>) : null;
const b = r.후 && typeof r.후 === "object" ? (r.후 as Record<string, unknown>) : null;
if (!a || !b) return `${name} ${show(r.전)} → ${show(r.후)}`;
const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].filter(
(k) => JSON.stringify(a[k]) !== JSON.stringify(b[k]),
);
return `${name} ${keys.map((k) => `${k} ${show(a[k])} → ${show(b[k])}`).join(", ")}`;
}
/** 할일 알림 — 계 전 → 후 · 바뀐 자원 수 · 재생성이 버리는 것(이름) → 확인이면 true */
export function askJob(job: CopyJob, got: JobReply, names: Map<string, string>): Promise<boolean> {
const bits = [
job === "단가갱신" ? ft("Price_Ask") : ft("Regen_Ask"),
`${ft("Before_After")} ${sum(got.전)} → ${sum(got.후)}`,
];
const lines = job === "단가갱신" ? (got.바뀐자원 ?? []).slice(0, 8).map(changedLine) : []; // 821-3
if (job === "단가갱신") bits.push(`${ft("Changed")} ${got.바뀐자원?.length ?? 0}`);
const lost = [
...(got.버림?.문서고침 ? [ft("Regen_Edit")] : []),
...(got.버림?.끊음 ?? []).map((k) => `${ft("Cut")} ${names.get(k) || k}`),
];
if (job === "재생성" && lost.length) bits.push(`${ft("Regen_Lost")} — ${lost.join(" · ")}`);
const text = [bits.join(" · "), ...lines].join("\n");
return showConfirmDialog(text, job === "단가갱신" ? ft("Price") : ft("Regen"));
}