// 원본 통합 검색(TFRMMULTISERCH: 일위대가·단가산출·기계경비·견적·실적·자재·노임 고르기, 검색어, 검색위치) — // 웹은 이 앱에 저장된 작업(data/*.json)을 검색 위치로 쓴다. 검색어 낱말이 모두 코드·품명·규격에 들어 있으면 찾음. const fs = require("node:fs"); const path = require("node:path"); const sources = { 103: ["HeadTitleDat", "일위대가"], 104: ["CostTitleDat", "단가산출"], 105: ["MachTitleDat", "기계경비"], 101: ["EstiMateDat", "견적단가"], 21: ["SilJukDat", "실적단가"], 5: ["StuffcDat", "자재단가"], 6: ["LaborcDat", "노임단가"], }; const LIMIT = 300; const fold = (value) => String(value ?? "") .normalize("NFKC") .toLowerCase() .replace(/\s+/g, ""); // only = 설계참고철(Main Data) 작업 id 목록 — 있으면 그 작업만. 검색어가 「a~b」 또는 숫자 하나면 단가(TC) 범위·같음(원본 0xefefb0). function search(directory, query, kinds, exclude = "", only = null) { const words = String(query ?? "") .split(/\s+/) .map(fold) .filter(Boolean); if (!words.length) throw Error("검색어를 입력하세요."); const range = /^(\d+(?:\.\d+)?)(?:~(\d+(?:\.\d+)?))?$/.exec(String(query).replace(/[\s,]/g, "")); const wanted = (kinds?.length ? kinds : Object.keys(sources)) .map(Number) .filter((k) => sources[k]); if (!wanted.length) throw Error("검색할 자료를 하나 이상 고르세요."); if (!fs.existsSync(directory)) return { hits: [], total: 0 }; const hits = []; let total = 0; for (const name of fs.readdirSync(directory).filter((n) => /^[a-f0-9]{24}\.json$/.test(n))) { const id = name.slice(0, -5); if (id === exclude || (only && !only.includes(id))) continue; let project; try { project = JSON.parse(fs.readFileSync(path.join(directory, name), "utf8")); } catch { continue; } for (const kind of wanted) { const [table, label] = sources[kind]; for (const row of project.tables?.[table] || []) { if (!row?.Code) continue; if (range) { const tc = Number(row.TC) || 0, lo = Number(range[1]), hi = range[2] == null ? lo : Number(range[2]); if (tc < Math.min(lo, hi) || tc > Math.max(lo, hi)) continue; } else { const text = fold(`${row.Code} ${row.Name} ${row.Size}`); if (!words.every((w) => text.includes(w))) continue; } total++; if (hits.length < LIMIT) hits.push({ id, project: project.name || id, kind, label, Code: row.Code, Name: row.Name ?? "", Size: row.Size ?? "", Unite: row.Unite ?? "", price: Number(row.TC) || 0, }); } } } return { hits, total }; } module.exports = { search, sources };