- 새 `M02_Machine_Find` — 기종 = 기계표 이름 · 괄호 붙임 · 상세구분(낱말표 24 안 씀) · 소리 표기 한 글(로우더 = 로더 · 로울러 = 롤러 · 브레이카 = 브레이커 · 굴삭기 = 굴착기 · 램 머 = 래머 …) · 기계표 괄호 낱말로 좁힘(표에 없는 낱말 발파 · 리핑 = 안 봄) · 규격 = 수 + 단위 전부(범위 · 복수 · 인치 = ㎝) · 하나 = 이음 · 여럿 = 「기계 후보 n — 고르기」 · 0 = 「기계 못 찾음」 표지(가까운 규격으로 대신 잇지 않음) - 별칭 먼저 — 마스터 층 + 지금 층 별칭 장부 기계 짝(이름 + 규격 · 이름만 · 규격 속 할증 글 뺌) · `M02_Doc_Match_Note._ALIAS` 여섯 쌍 걷음(소리 표기 = 정규화 · 같은 기종 = 기계표 · 별칭 장부) - 조합 기계 = 기계마다 항목 하나(요구 738-3 · 제목 = 그 기계 · 줄 쌓기 차림 `M02_Doc_NativeLayout`) · 줄 둘(「+」 · 괄호 속 다른 기종 · 「…조합」 · 덤프트럭(자동덮개) = 덤프트럭 + 자동덮개시설) — 같은 수량 식 · 엑셀 값은 첫 줄 · 다음 줄 0 + 표지 「조합 기계 — 경비는 한 덩이」 · 우리 단가로 = 각자 시간당 경비 - 원파일 EST · STmate · 엑셀 읽기기 · 일위대가 호표(`Hopyo_Elem` · `Hopyo_Fixed._eqs` · `Hopyo_Row.m01_picks`)가 같은 찾기 · STmate 묶음 기계 줄도 시간당 경비 이음 - 문서 머리 `가져온.기계`(기호마다 판정 · 후보 · 찾기어) · 못 이은 기계 줄도 기계 줄(이름 단추 · `owned_of` 파이썬 · TS 짝) · 이름 단추 조회어 = 기종 이름만(규격 뺌) · 고르면 엑셀 값 기억 그대로(금액 안 바뀜) · 문서 [저장] = 고른 EQ → 이 층 별칭 장부 줄 → M01 별칭 장부 후보(승인 흐름 그대로) · 장부 짝 글 = 「기계 EQ…」 - 마스터 층 별칭 장부(resources/master_template/실무자료/맞춤.json) = 사용자 승인 다섯(요구 735-5 · 736) — 아스팔트페이버(피니셔) = 아스팔트 피니셔 · 디젤 엔진 = 엔진(디젤) · 워터제트 = 워터젯트 · 버킷준설기 = 버킷식준설기 · 바이브레이트 봉상후렉시블 = 콘크리트 진동기 엔진식 ø45(2.6㎾) - 시험 — 새 test_812_9(예 전부 · 정규화 · 괄호 · 범위 · 조합 둘 · 별칭 먼저 · 표지 · 고르면 별칭 줄 · 금액 전후) · test_812_3 전석쌓기 · 측구터파기 연암 항목을 기계마다 항목으로 고침(조합 기계 둘로 가름 · 요구 738-3 이 바꾼 까닭) - 전수 목록 docs/raw/요구_1104/_목록_812-9_기계이음.xlsx Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
8.6 KiB
TypeScript
207 lines
8.6 KiB
TypeScript
/* =============================================================================
|
|
* 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<string, unknown>;
|
|
|
|
/** 표 조건 가운데 옵션(선택 · 입력 열쇠)에 매인 것 — {조건 칸: 옵션 열쇠들} */
|
|
export function boundConds(doc: FormulaDoc, link: Link | undefined): Record<string, string[]> {
|
|
const opts = new Map(
|
|
(doc.변수 ?? []).filter(isOption).map((v) => [varId(v), String(v.열쇠)] as const),
|
|
);
|
|
const out: Record<string, string[]> = {};
|
|
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<string, string> = {};
|
|
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<string, string>;
|
|
}
|
|
|
|
const SIGN: Record<string, string> = { 인력: "w", 재료: "P", 기계: "P", 단가산출: "B" };
|
|
const COST: Record<string, string> = { 인력: "노무비", 재료: "재료비" };
|
|
|
|
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<string, string> {
|
|
const taken = new Set([
|
|
...symbols(doc),
|
|
...(doc.금액 ?? []).map((l) => String(l.기호 ?? "")),
|
|
"노무비",
|
|
"재료비",
|
|
"경비",
|
|
]);
|
|
const out: Record<string, string> = {};
|
|
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<string, string>, 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 ?? ""));
|