/* ============================================================================= * common_util_line_stack_pick.ts * 줄 쌓기 고르기 · 수준(PLAN 804-3 · 요구 678) — 화면 · DOM · fetch 없음(시험이 Node 로 돌림) * 표 칸 고름 = 그 기호 연결을 표 · 칸 + 옵션에 안 매인 조건은 고른 줄 값으로(옵션에 매인 조건은 그대로 = 옵션이 줄을 고름) * 찾아 넣기 고름(인력 · 재료 · 기계 · 다른 단가산출) = 새 금액 줄 + 단가 기호 연결(요소 키 · 단가산출 키 + 넘길 입력) * 산출근거 수준 = 항목(1. 2.) 1 · 가나다 2 · 줄 = 항목 + 1 · 제목 없는 항목 줄 = 1 * ========================================================================== */ import { names, renameText, splitId, symId, varId, type FormulaDoc, type Link, } from "./common_util_formula_doc"; import { findRow, type DocTable } from "./common_util_formula_doc_table"; import { evalNode } from "./common_util_formula_doc_view"; import { freshId, isOption, newVar, nodeOf, symbols, type MoneyKind, type StackItem, } from "./common_util_line_stack"; type Row = Record; /** 표 조건 가운데 옵션(선택 · 입력 열쇠)에 매인 것 — {조건 칸: 옵션 열쇠들} */ export function boundConds(doc: FormulaDoc, link: Link | undefined): Record { const opts = new Map( (doc.변수 ?? []).filter(isOption).map((v) => [varId(v), String(v.열쇠)] as const), ); const out: Record = {}; for (const [c, t] of Object.entries(link?.조건 ?? {})) { const n = nodeOf(t); const keys = n ? names(n).flatMap((x) => (opts.has(x) ? [opts.get(x)!] : [])) : []; if (keys.length) out[c] = keys; } return out; } /** 지금 조건으로 닿는 표 줄(못 찾으면 -1) */ export function currentRow(table: DocTable, link: Link | undefined, vals: Row): number { const conds: Row = {}; for (const [c, t] of Object.entries(link?.조건 ?? {})) { const n = nodeOf(t); if (n) conds[c] = evalNode(n, vals); } try { return table.줄.indexOf(findRow(table, conds, link?.칸)); } catch { return -1; } } /** 표 줄 조건 값 → 식 글(글 = '…' · 범위 = 안쪽 수) */ function literal(v: unknown): string | null { if (typeof v === "number") return String(v); if (Array.isArray(v)) { const [lo, hi] = v as [number | null, number | null]; if (lo !== null && hi !== null) return String((lo + hi) / 2); return lo !== null ? String(lo) : hi !== null ? String(hi) : null; } return v === null || v === undefined ? null : `'${String(v).replace(/'/g, "")}'`; } /** 표 칸 고름 — 기호 연결 = 표 · 칸 · 옵션에 안 매인 조건은 고른 줄 값 · 그 기호 식 줄은 걷음 */ export function pickCell( doc: FormulaDoc, id: string, table: DocTable, row: Row, col: string, ): void { let v = (doc.변수 ?? []).find((x) => varId(x) === id); if (!v) (doc.변수 ??= []).push((v = newVar(id, { 갈래: "표" }))); const was = v.연결?.갈래 === "표" ? v.연결 : undefined; const bound = boundConds(doc, was); const cond: Record = {}; for (const c of Object.keys(table.조건 ?? {})) { if (bound[c] || (table.조건?.[c] === "범위" && was?.조건?.[c])) cond[c] = was!.조건![c]; else { const t = literal(row[c]); if (t !== null) cond[c] = t; } } v.연결 = { 갈래: "표", 표: table.키 ?? String(was?.표 ?? ""), 조건: cond, 칸: col }; doc.식 = (doc.식 ?? []).filter((l) => l.기호 !== id); } /** 찾아 넣기에서 고른 것 — 기계 = 시간당 경비 로직 키 + 입력 {기계: '원문번호'} */ export interface Picked { 키: string; 이름: string; 규격?: string; 단위?: string; 입력?: Record; } const SIGN: Record = { 인력: "w", 재료: "P", 기계: "P", 단가산출: "B" }; const COST: Record = { 인력: "노무비", 재료: "재료비" }; const linkOf = (kind: MoneyKind, p: Picked): Link => kind === "인력" || kind === "재료" ? { 갈래: "요소", 키: p.키 } : { 갈래: "단가산출", 키: p.키, 입력: { ...(p.입력 ?? {}) } }; const unitOf = (kind: MoneyKind, p: Picked): string => kind === "기계" ? "hr" : kind === "인력" ? "인" : (p.단위 ?? ""); /** 고른 것으로 새 금액 줄(수량 1) — 항목 끝에 */ export function addPicked(doc: FormulaDoc, item: StackItem, kind: MoneyKind, p: Picked): string { const id = freshId(doc, SIGN[kind] ?? "B", "새"); (doc.변수 ??= []).push(newVar(id, linkOf(kind, p), p.이름)); const n = (doc.금액 ??= []).length; doc.금액.push({ 이름: p.이름, ...(p.규격 ? { 규격: p.규격 } : {}), 단위: unitOf(kind, p), 비목: COST[kind] ?? "묶음", 수량: "1", 단가: id, } as never); item.줄.push({ 꼴: "금액", i: n }); return id; } /** 있는 줄 바꿔 고름 — 그 줄 단가 기호 연결 · 줄 이름 · 규격(단위 · 수량은 그대로 · 같은 로직이면 넘길 입력 그대로) */ export function repick(doc: FormulaDoc, i: number, id: string, kind: MoneyKind, p: Picked): void { const v = (doc.변수 ?? []).find((x) => varId(x) === id); const line = (doc.금액 ?? [])[i] as { 이름?: string; 규격?: string } | undefined; if (!v || !line) return; const was = v.연결 ?? {}; const now = linkOf(kind, p); if (now.갈래 === "요소" && was.지역) now.지역 = was.지역; // 지역 옵션 이음 그대로 if (now.갈래 === "단가산출" && was.키 === now.키) now.입력 = { ...was.입력, ...now.입력 }; // 같은 로직 = 다른 넘길 입력 그대로 // 가져온 기계 줄(812-9) — 엑셀 값 기억 그대로(엑셀 단가 기준 금액 안 바뀜 · 못 이은 기계 = 숫자 값 · 묶음) const kept = (was as { 엑셀?: unknown }).엑셀 ?? (was.갈래 === "숫자" ? was.값 : undefined); if (kind === "기계" && kept !== undefined) (now as { 엑셀?: unknown }).엑셀 = kept; const cost = (line as { 비목?: string }).비목 ?? ""; if (kind === "기계" && ["노무비", "재료비", "경비"].includes(cost)) now.칸 = cost; // 비목 한 칸 줄 = 그 칸 v.연결 = now; v.이름 = p.이름; line.이름 = p.이름; if (p.규격) line.규격 = p.규격; else delete line.규격; } /** 산출근거 수준 — 항목 머리 · 그 항목 줄(제목 없는 항목 = 머리 없음 · 줄 1) */ export function stackLevels(items: StackItem[]): { item: number; line: number }[] { return items.map((it) => { if (!it.제목.trim()) return { item: 0, line: 1 }; const item = it.가나다 ? 2 : 1; return { item, line: item + 1 }; }); } /** 표 값 기호 화면 이름(804-5 · 요구 683) — 「Q_수량2^표」 → 「Q_작업반장」(고른 줄 글 조건 첫 낱말) · 글 조건 없으면 「^표」만 뗌 * · 「^표」 기호만 · 겹치면 2 · 3 · 문서 기호는 그대로(화면만 · 칸 글은 `aliasText` 로 오감) * · 직접 친 값(숫자 · 식)도 표 · 칸 · 조건을 남기므로 같은 이름(요구 692) */ export function tableAlias(doc: FormulaDoc): Record { const taken = new Set([ ...symbols(doc), ...(doc.금액 ?? []).map((l) => String(l.기호 ?? "")), "노무비", "재료비", "경비", ]); const out: Record = {}; for (const v of doc.변수 ?? []) { const link = v.연결; if (!link?.표) continue; const id = varId(v); const [s, lo, up] = splitId(id); const said = Object.values(link.조건 ?? {}) .map(String) .find((t) => /^'.+'$/.test(t)); const label = said?.slice(1, -1).split(/[\s(,·/]/)[0] ?? ""; if (up !== "표") continue; // 「^표」 꼬리 기호만(r_인력 등 손 이름은 그대로) const base = label || lo.replace(/\d+$/, "") || lo; // 번호 꼬리 뗌(겹치면 다시 2 · 3) for (let n = 1; n < 100; n++) { const now = symId(s, n > 1 ? `${base}${n}` : base); const node = nodeOf(now); if (taken.has(now) || node?.[0] !== "var" || node[1] !== now) continue; taken.add(now); out[id] = now; break; } } return out; } /** 식 글의 기호를 화면 이름으로(또는 `back` = 화면 이름을 기호로) */ export function aliasText(text: string, alias: Record, back = false): string { let t = text; for (const [id, now] of Object.entries(alias)) t = String(back ? renameText(t, now, id) : renameText(t, id, now)); return t; } /** 표 칸 단위 「줄 따름」(줄마다 다른 단위) = 화면에 안 보임(요구 683) */ export const shownUnit = (u: unknown): string => (u === "줄 따름" ? "" : String(u ?? ""));