/* ============================================================================= * M01_MasterData_UI_Test_Pick.ts * 재료·기계 바꿔 고르기 부품 — 방식 A·B·C 가 같이 씀 · 구분 → 상세구분 → 후보 순으로 좁힘 * 바꾼 것은 시험 계산용 복사본에만 — 정본 로직은 그대로 * ========================================================================== */ import { createSelectField, el } from "@ui/ui_template_elements"; import { fetchMaterials, searchElements, searchPrice, type ElementBrief, type HoLine, type LogicOne, type LogicRow, } from "./M01_MasterData_UI_Logic_Api"; import { formatNumber } from "./M01_MasterData_UI_Logic_Edit"; import { tt } from "./M01_MasterData_UI_Test_Text"; export type Swaps = Map; const PICKABLE = ["재료", "기계"]; /** 고를 수 있는 줄이 있는지 */ export const hasPickable = (logic: LogicRow): boolean => (logic.호표 ?? []).some((l) => PICKABLE.includes(l.종류)); /** 바꿔 본 줄이 있으면 고친 복사본 — 없으면 undefined(정본 그대로 셈) */ export function swappedRow(logic: LogicRow, swaps: Swaps): LogicRow | undefined { if (!swaps.size) return undefined; return { ...logic, 호표: (logic.호표 ?? []).map((l, i) => { const s = swaps.get(i); return s ? { ...l, 요소: s.ref, 이름: s.이름 ?? l.이름, 규격: s.규격 ?? l.규격 } : l; }), }; } type Brief = ElementBrief & { 구분?: string; 상세구분?: string; 값들?: Record; }; const distinct = (items: Brief[], key: "구분" | "상세구분"): string[] => [ ...new Set(items.map((i) => i[key]).filter((v): v is string => !!v)), ]; /** 후보 글 — 값 열이 여럿이면 있는 열을 다 보임(계산은 그 가운데 낮은 값) */ const label = (it: Brief): string => { const cols = Object.entries(it.값들 ?? {}) .filter(([, v]) => v !== null && v !== undefined) .map(([k, v]) => `${k} ${formatNumber(v)}`); const prices = cols.length ? cols : [formatNumber(it.값) || tt("Mat_NoPrice")]; return [`${it.이름 ?? ""} ${it.규격 ?? ""}`.trim(), ...prices].join(" · "); }; const oldNames = (one: LogicOne, line: HoLine): string[] => { const brief = one.prices[line.요소 as string] as { 이름?: string; 원문번호?: string } | null; const names = [brief?.이름, brief?.원문번호, (line.이름 ?? "").split(/[\s(·]/)[0]]; return [...new Set(names.filter((n): n is string => !!n))]; }; /** 이름을 차례로 찾아 후보가 나오는 첫 결과 — 다 비면 마지막 결과 */ async function firstFound(names: string[]): Promise>> { let last = await searchPrice(names[0] ?? ""); for (const name of names.slice(1)) { if (last.items.length) break; last = await searchPrice(name); } return last; } function pickRow(one: LogicOne, line: HoLine, i: number, swaps: Swaps, onChange: () => void) { const cond = typeof line.요소 === "object" ? (line.요소 as unknown as Record) : null; const price = cond ? undefined : one.prices[line.요소]?.값; let all: Brief[] = []; let sub = ""; let detail = String(cond?.["상세구분"] ?? ""); const subBox = createSelectField({ options: [], value: "", compact: true, onChange: (v) => ((sub = v), (detail = ""), fill()), }); const detailBox = createSelectField({ options: [], value: "", compact: true, onChange: (v) => ((detail = v), fill()), }); const itemBox = createSelectField({ options: [{ value: "", text: tt("Mat_Default") }], value: "", onChange: (ref) => { const item = all.find((it) => it.ref === ref); if (item) swaps.set(i, item); else swaps.delete(i); onChange(); }, }); const subRow = el("div", { className: "m01-test__filters", children: [subBox.root, detailBox.root], }); // 구분 → 상세구분 → 후보 — 앞 단을 고르면 뒷 단 목록이 좁혀짐(구분 칸이 없는 후보면 그 단은 숨김) const fill = (): void => { const subs = distinct(all, "구분"); const inSub = all.filter((it) => !sub || it.구분 === sub); const details = distinct(inSub, "상세구분"); const inDetail = inSub.filter((it) => !detail || it.상세구분 === detail); const all_ = { value: "", text: tt("Pick_All") }; subBox.root.hidden = subs.length < 1; detailBox.root.hidden = details.length < 1; subBox.setOptions([all_, ...subs.map((v) => ({ value: v, text: v }))], sub); detailBox.setOptions([all_, ...details.map((v) => ({ value: v, text: v }))], detail); itemBox.setOptions( [ { value: "", text: tt("Mat_Default") }, ...inDetail.map((it) => ({ value: it.ref, text: label(it) })), ], swaps.get(i)?.ref ?? "", ); }; // 고르기 조건({구분, 상세구분, 규격}) 줄 = 조건 안 후보 · 옛 줄 = 이름으로 찾은 후보 const found = cond ? fetchMaterials({ sub: String(cond["구분"] ?? ""), spec: String(cond["규격"] ?? ""), region: "울진", }) : line.종류 === "재료" ? firstFound(oldNames(one, line)) // 옛 줄(고르기 조건 없음) — 품셈재료 이름 → 원문번호 → 이름 첫 낱말 순으로 찾아 나온 첫 것 : searchElements(line.종류, line.이름 ?? ""); void found .then((r) => { all = r.items as Brief[]; if (cond) sub = String(cond["구분"] ?? ""); fill(); }) .catch(() => undefined); const spec = line.규격 ? ` ${line.규격}` : ""; const cost = price !== undefined && price !== null ? ` · ${tt("Mat_Price")} ${formatNumber(price)}` : ""; return el("div", { className: "m01-test__pick", children: [ el("strong", { text: `${line.종류} · ${line.이름 ?? String(line.요소)}${spec}` }), el("span", { className: "m01-test__muted", text: `${line.단위 ?? ""}${cost}` }), subRow, itemBox.root, ], }); } /** 재료·기계 줄마다 한 칸씩 — 한 번 만들어 두고 다시 그릴 때 같은 노드를 씀(고른 것이 안 날아감) */ export function pickPanel(one: LogicOne, swaps: Swaps, onChange: () => void): HTMLElement { const rows = (one.logic.호표 ?? []) .map((line, i) => ({ line, i })) .filter((x) => PICKABLE.includes(x.line.종류)) .map((x) => pickRow(one, x.line, x.i, swaps, onChange)); return el("div", { className: "m01-test__picks", children: rows.length ? rows : [el("p", { text: tt("Mat_None") })], }); }