- M02 단가산출 줄 쌓기 [+ 줄] 「다른 단가산출 · 일위대가 복사」 = 찾기 창 탭 [M01 전체 | M02 단가산출 | 일위대가](위 층 · 자기 SK 막음)
- 고름 = 그 문서 전체를 복사해 이 단가산출 항목 · 줄로 펼침(참조 줄 없음 · 원본 연결 없음 · 머리 글 「○○에서 복사」 · 옵션 = 고른 값으로 굳힘 · 기호 겹침 = 새 이름)
- 일위대가 = 로직 줄 → M01 문서 × 수량 ÷ 배 · 참조 SK → 그 SK 문서 × 수량 ÷ 배 · 고정 노임 · 자재 · 기계 · 손 단가 · 율(비목 기준) · 못 옮김 = 「⚠」 글 줄
- 율 줄 밑 = 그 조각 줄만(금액번지 복사N) — 앞 줄 · 다른 조각 안 섞임
- 서버 조각 길 GET …/sangeun/{키}/doc/take?src= (M02_Doc_Take) · 화면 넣기 common_util_line_stack_copy.ts
- 빈 SK(바탕 · 줄 없음)에 수식 길로 저장한 문서가 빈 씨로 덮이던 것 고침(M02_Doc_Sk.settle 차례)
- [단가 갱신] = 사본 값 줄 자원 단가만 지금 M01(구조 · 품 · 식 그대로)
- 시험 test_804_4_line_stack_take 6 · helper_804_2 「복사」 할일
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
8.3 KiB
TypeScript
198 lines
8.3 KiB
TypeScript
/* =============================================================================
|
||
* common_util_line_stack_copy.ts
|
||
* 줄 쌓기 복사해 펼침(PLAN 804-4 · 요구 680-1 · 681) — 화면 · DOM · fetch 없음(시험이 Node 로 돌림)
|
||
* M02 단가산출 줄 쌓기에서 M01 단가산출 · M02 단가산출 · M02 일위대가를 고름 = 서버가 준 조각
|
||
* (`M02_Doc_Take.take`)을 이 문서의 항목 · 줄로 넣음 — 원본과 연결 없음 · 머리 글 「○○에서 복사」
|
||
* 문서 조각 = 그 문서 변수 · 식 · 금액 · 차림 전체(기호 겹침 = 아래첨자 끝 수 · 옵션 = 고른 값으로 굳힘 ·
|
||
* 곱 = 금액 줄 수량 × 곱(율 줄 = 그대로 — 밑이 곱해짐))
|
||
* 율 줄 밑(`노무비` 등 앞 줄 누계)은 그 조각 줄만 — 조각마다 금액번지 `복사N` 에 쌓고 `번지.복사N.노무비` 로 바꿈
|
||
* (앞에 있던 줄 · 다른 조각이 섞이지 않음) · 일위대가 율 줄 밑 = 그 일위대가 조각 번지 합
|
||
* 줄 조각 = 인력 · 재료 · 기계(`addPicked` + 수량) · 손 단가 · 율 = 금액 줄 · 글 = 글 줄(못 옮김 「⚠」)
|
||
* ========================================================================== */
|
||
|
||
import {
|
||
names,
|
||
parse,
|
||
textOf,
|
||
varId,
|
||
type DocVar,
|
||
type FormulaDoc,
|
||
type MoneyLine,
|
||
type Node,
|
||
} from "./common_util_formula_doc";
|
||
import {
|
||
isOption,
|
||
nodeOf,
|
||
renameSym,
|
||
stackOf,
|
||
symbols,
|
||
type MoneyKind,
|
||
type StackDoc,
|
||
type StackItem,
|
||
type StackLine,
|
||
} from "./common_util_line_stack";
|
||
import { addPicked, type Picked } from "./common_util_line_stack_pick";
|
||
import { splitId, symId } from "./common_util_formula_doc";
|
||
|
||
export type TakePiece =
|
||
| { 갈래: "문서"; 문서: FormulaDoc; 입력?: Record<string, unknown>; 곱?: string; 이름?: string }
|
||
| { 갈래: "인력" | "재료" | "기계"; 고른: Picked; 수량: string }
|
||
| { 갈래: "율" | "손"; 줄: MoneyLine & { 규격?: string } }
|
||
| { 갈래: "글"; 글: string };
|
||
export interface Taken {
|
||
이름: string;
|
||
출처: string;
|
||
조각: TakePiece[];
|
||
계?: number | null;
|
||
}
|
||
|
||
const COSTS = ["노무비", "재료비", "경비"];
|
||
const clone = <T>(v: T): T => JSON.parse(JSON.stringify(v)) as T;
|
||
const PLACE = "복사";
|
||
|
||
/** 앞 줄 누계(노무비 · 재료비 · 경비) → 그 번지들 합 */
|
||
function scoped(n: Node, places: string[]): Node {
|
||
if (n[0] === "var" && COSTS.includes(n[1])) {
|
||
const one = (p: string): Node => ["field", ["field", ["var", "번지"], p], n[1]] as Node;
|
||
return places.slice(1).reduce<Node>((a, p) => ["bin", "+", a, one(p)] as Node, one(places[0]));
|
||
}
|
||
if (!Array.isArray(n)) return n;
|
||
return n.map((x) => (Array.isArray(x) ? scoped(x as Node, places) : x)) as unknown as Node;
|
||
}
|
||
const usesRunning = (t: unknown): boolean => {
|
||
const n = nodeOf(t);
|
||
return !!n && names(n).some((x) => COSTS.includes(x));
|
||
};
|
||
const rescope = (t: string | null | undefined, places: string[]): string | null | undefined => {
|
||
if (!places.length || !usesRunning(t)) return t;
|
||
return textOf(scoped(parse(t), places));
|
||
};
|
||
|
||
function freshPlace(doc: FormulaDoc): string {
|
||
const have = new Set((doc.금액 ?? []).map((l) => String(l.번지 ?? "")));
|
||
for (let n = 1; ; n++) if (!have.has(`${PLACE}${n}`)) return `${PLACE}${n}`;
|
||
}
|
||
|
||
/** 옵션 → 고른 값으로 굳힘(열쇠 지움 · 수 = 숫자 · 글 = 식 「'글'」) */
|
||
function freeze(s: FormulaDoc, given: Record<string, unknown>): void {
|
||
for (const v of s.변수 ?? []) {
|
||
if (!isOption(v)) continue;
|
||
const link = v.연결 ?? {};
|
||
const key = String(v.열쇠);
|
||
let val: unknown = key in given ? given[key] : link.값;
|
||
const list = (link.목록 ?? []) as unknown[];
|
||
if (link.갈래 === "선택") {
|
||
const hit = list.find((x) => String(x) === String(val));
|
||
val = hit !== undefined ? hit : val === undefined || val === null ? list[0] : val;
|
||
}
|
||
delete (v as DocVar & { 열쇠?: string }).열쇠;
|
||
if (typeof val === "number") v.연결 = { 갈래: "숫자", 값: val };
|
||
else if (val === undefined || val === null || val === "") v.연결 = { 갈래: "숫자", 값: "" };
|
||
else {
|
||
v.연결 = { 갈래: "식" };
|
||
(s.식 ??= []).push({ 기호: varId(v), 식: `'${String(val).replace(/'/g, "")}'` });
|
||
}
|
||
}
|
||
}
|
||
|
||
/** 문서 조각 하나 → 이 문서에 붙임 · 돌려줌 = 그 조각 항목들(금액 차례 = 이 문서) · 번지 이름 */
|
||
export function copyDoc(
|
||
doc: StackDoc,
|
||
src: FormulaDoc,
|
||
given: Record<string, unknown> = {},
|
||
factor?: string,
|
||
): { items: StackItem[]; place: string | null } {
|
||
const s = clone(src) as StackDoc;
|
||
const mine = new Set([
|
||
...symbols(doc),
|
||
...(doc.금액 ?? []).map((l) => String(l.기호 ?? "")).filter(Boolean),
|
||
]);
|
||
const taken = new Set([
|
||
...mine,
|
||
...symbols(s),
|
||
...(s.금액 ?? []).map((l) => String(l.기호 ?? "")),
|
||
]);
|
||
const ids = [...symbols(s), ...(s.금액 ?? []).map((l) => String(l.기호 ?? "")).filter(Boolean)];
|
||
for (const id of ids) {
|
||
if (!mine.has(id)) continue;
|
||
const [sg, lo, up] = splitId(id);
|
||
let n = 2;
|
||
while (taken.has(symId(sg, `${lo}${n}`, up))) n++;
|
||
renameSym(s, id, symId(sg, `${lo}${n}`, up));
|
||
taken.add(symId(sg, `${lo}${n}`, up));
|
||
}
|
||
freeze(s, given);
|
||
const money = s.금액 ?? [];
|
||
const place = money.length ? freshPlace(doc) : null;
|
||
for (const l of money) {
|
||
const running = usesRunning(l.단가) || usesRunning(l.수량);
|
||
if (place) {
|
||
l.단가 = rescope(l.단가, [place]) as string;
|
||
l.수량 = rescope(l.수량, [place]);
|
||
l.번지 = place;
|
||
}
|
||
if (factor && !running) l.수량 = l.수량 ? `(${l.수량}) × (${factor})` : factor;
|
||
}
|
||
const items = stackOf({ ...s, 금액: money }); // 원본 차림(쌓기 · 묶음) 그대로 — 금액 차례 = s
|
||
const at = (doc.금액 ??= []).length;
|
||
doc.금액.push(...money);
|
||
(doc.변수 ??= []).push(...(s.변수 ?? []));
|
||
(doc.식 ??= []).push(...(s.식 ?? []));
|
||
const shift = (l: StackLine): StackLine => (l.꼴 === "금액" ? { 꼴: "금액", i: l.i + at } : l);
|
||
return { items: items.map((it) => ({ ...it, 줄: it.줄.map(shift) })), place };
|
||
}
|
||
|
||
/** 받은 조각 전부 → 새 항목(제목 = 이름 · 첫 줄 = 「출처에서 복사」) · 제목 있는 조각 항목 = 가나다 */
|
||
export function addTaken(doc: StackDoc, items: StackItem[], t: Taken): void {
|
||
const head: StackItem = {
|
||
제목: t.이름,
|
||
줄: [{ 꼴: "글", 글: `${t.출처} 「${t.이름}」에서 복사` }],
|
||
};
|
||
items.push(head);
|
||
let cur = head;
|
||
let inDoc = false; // 지금 항목 = 문서 조각 항목(뒤 줄 조각은 새 항목)
|
||
const places: string[] = [];
|
||
let lone: string | null = null; // 줄 조각(인력 · 재료 · 기계 · 손) 번지 — 율 밑에 듦
|
||
const pushMoney = (line: MoneyLine): void => {
|
||
const n = (doc.금액 ??= []).length;
|
||
doc.금액.push(line);
|
||
cur.줄.push({ 꼴: "금액", i: n });
|
||
};
|
||
const rates: MoneyLine[] = [];
|
||
for (const p of t.조각) {
|
||
if (p.갈래 === "문서") {
|
||
const got = copyDoc(doc, p.문서, p.입력 ?? {}, p.곱);
|
||
if (got.place) places.push(got.place);
|
||
const many = got.items.length > 1 || got.items.some((it) => it.제목);
|
||
if (t.조각.filter((x) => x.갈래 === "문서").length > 1 || many) {
|
||
const name = p.이름 ?? "";
|
||
got.items.forEach((it, k) => {
|
||
const title = k || !name ? it.제목 : it.제목 ? `${name} · ${it.제목}` : name;
|
||
items.push({ ...it, 제목: title, 가나다: true });
|
||
});
|
||
cur = items[items.length - 1];
|
||
inDoc = true;
|
||
} else got.items.forEach((it) => cur.줄.push(...it.줄));
|
||
} else if (p.갈래 === "글") cur.줄.push({ 꼴: "글", 글: p.글 });
|
||
else if (p.갈래 === "율") rates.push({ ...p.줄 });
|
||
else {
|
||
if (inDoc) items.push((cur = { 제목: "", 가나다: true, 줄: [] }));
|
||
inDoc = false;
|
||
lone ??= freshPlace(doc);
|
||
if (!places.includes(lone)) places.push(lone);
|
||
if (p.갈래 === "손") pushMoney({ ...p.줄, 번지: lone });
|
||
else if ("고른" in p) {
|
||
const kind = p.갈래 as MoneyKind;
|
||
addPicked(doc, cur, kind, p.고른);
|
||
const line = doc.금액![doc.금액!.length - 1];
|
||
line.수량 = p.수량;
|
||
line.번지 = lone;
|
||
}
|
||
}
|
||
}
|
||
if (rates.length) {
|
||
cur = items[items.length - 1];
|
||
for (const r of rates) pushMoney({ ...r, 단가: rescope(r.단가, places) as string });
|
||
}
|
||
}
|