From 009c58a72d3010bbff4abe3c88e0c9b9cda49ae8 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Thu, 12 Nov 2026 13:28:44 +0900 Subject: [PATCH] =?UTF-8?q?feat(est-test):=20=EC=84=A4=EA=B3=84=EC=B0=B8?= =?UTF-8?q?=EA=B3=A0=EC=B2=A0(Main=20Data)=20=EA=B2=80=EC=83=89=EA=B3=BC?= =?UTF-8?q?=20=EB=8B=A8=EA=B0=80=20=EB=B2=94=EC=9C=84=20=EA=B2=80=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- resources/tester/test_est_multi_search.py | 19 +++++++++ tools/est-test/PLAN.md | 2 + tools/est-test/native-multi-search.js | 50 ++++++++++++++++++++++- tools/est-test/search-store.cjs | 17 ++++++-- tools/est-test/server.cjs | 1 + 5 files changed, 83 insertions(+), 6 deletions(-) diff --git a/resources/tester/test_est_multi_search.py b/resources/tester/test_est_multi_search.py index 69928c982..149df1a1a 100644 --- a/resources/tester/test_est_multi_search.py +++ b/resources/tester/test_est_multi_search.py @@ -34,3 +34,22 @@ def test_search_finds_words_in_selected_kinds(tmp_path): assert result["none"]["total"] == 0 assert result["excluded"]["total"] == 0 assert result["err"] == ["검색어를", "검색할 "] + + +def test_search_price_range_and_main_data_only(tmp_path): + sample = json.loads((ROOT / "tools/est-test/sample.json").read_text(encoding="utf-8")) + (tmp_path / ("a" * 24 + ".json")).write_text(json.dumps(sample), encoding="utf-8") + (tmp_path / ("c" * 24 + ".json")).write_text(json.dumps(sample), encoding="utf-8") + script = """ + const s = require('./tools/est-test/search-store.cjs'), dir = process.argv[1]; + const range = s.search(dir, '1~999999999', ['5']); + const exact = s.search(dir, String(range.hits[0].price), ['5']); + const only = s.search(dir, '1~999999999', ['5'], '', ['c'.repeat(24)]); + process.stdout.write(JSON.stringify({ range: range.hits.map((h) => h.price), exact: exact.total, only: only.hits.map((h) => h.id), total: range.total })); + """ + out = json.loads( + subprocess.check_output(["node", "-e", script, str(tmp_path)], cwd=ROOT).decode("utf-8") + ) + assert out["range"] and all(1 <= p <= 999999999 for p in out["range"]) + assert out["exact"] >= 2 + assert set(out["only"]) == {"c" * 24} and len(out["only"]) == out["total"] // 2 diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 7cd49ce93..331e13903 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -489,6 +489,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 구성비·요율 전환(CalcBase), 약어 표시(SName), 제비율 기준 문서 보기(원본 한글 파일). - [x] 수량산출서 검색·전체보기(원본 TFRMMULLANG btnSerch·btnAllDisplay — 빈칸·|·괄호 무시 포함 검색) — 고르기 줄을 `native-quantity-sheets.js` 로 옮겨 700줄 제한 유지. - 검증완료: `test_est_quantity_sheets.py` 검색 시험 포함 수량 시험 22개 통과. ORCA 브라우저에서 「집수」 검색 → 항목에 집수정이 있는 「배수」만·선택 이동, 전체보기 → 3개 복귀, 샘플 복구. + - [x] 설계참고철(Main Data)과 단가 범위 검색(원본 TfrmMainTableList 「MAIN DATA(설계참고철) 추가 및 삭제」 · TfrmFolderSerch 「Main Data에서 검색」·범위 검색 0xefefb0) — 통합 검색 「설계참고철 관리」에서 저장된 작업 중 최대 10개 지정(양식 보관함 종류 options), 검색위치 「설계참고철」이면 그 작업만, 검색어 「a~b」·숫자 하나는 단가 범위·같음. 원본의 검색중지는 서버 검색이 바로 끝나 두지 않음. + - 검증완료: `test_est_multi_search.py` 2개(범위·같음·지정 작업만) 통과. ORCA 브라우저에서 2개 지정 → 「1000~2000」 24건 모두 지정 작업·범위 안, 목록 지워 복구. - [x] 줄 연결 도구(원본 TfrmHead popupCostNC·GC·JC·All 0xf1b54c · popMainSumIndex · mnuDataLink 0xf00e6c — 디스어셈블 근거) — 비목별 단가 불러오기(일위대가 줄: 하위 견적·일위·단산·기계에서 고른 비목 단가만 남기고 나머지 0, 기준비목 지정, 노임·자재·표준 거부), SUM 색인지정(해제·A~Z → 구간 SKind, 레벨·소계·도급액·조달수수료·호표머리 줄 거부), 하위데이타 연결/끊기(연결 줄은 끊고 값 유지, 끊긴 줄은 일위→단산→기계→표준→견적→자재→노임 차례로 코드 또는 품명·규격·단위 맞춤으로 연결, 자재는 재료단가·노임은 노무단가 = 원본 TC). 원본은 초점 한 줄, 웹은 고른 줄 모두. - 검증완료: `test_est_row_links.py`(경비만 5·전체 40/15/5, 거부, 자재 연결 M1·5000·끊기, 못 찾음 문구, 색인 3·거부 3종) 통과. ORCA 브라우저에서 일위대가 합판거푸집 노무비만 → 0, 재료비만 → 재료단가 34,666·금액 13,173, 내역 일위 연결 줄 끊기 → 다시 연결 103·도급액 불변, SUM 색인 B, 샘플 복구. - [x] 묶음 인쇄 간지 설정값 일괄적용·기본양식에저장·전체선택/선택해제(원본 TFRMESTPRINTPRIVATE btnGangiAllSet·btnSaveToMain, TFRMESTPRINT2 btnNoselect) — 간지 인쇄·간지 겹테두리를 모든 서식에 같게, 고른 서식·간지 설정을 「묶음기본양식」(양식 보관함 종류 print)으로 저장해 묶음 설정 없는 공사에서 열 때 씀. diff --git a/tools/est-test/native-multi-search.js b/tools/est-test/native-multi-search.js index 0825a5727..2ba87dc96 100644 --- a/tools/est-test/native-multi-search.js +++ b/tools/est-test/native-multi-search.js @@ -23,11 +23,16 @@ const EstMultiSearch = (() => { .join(""); EstApp.showDialog( "통합 검색", - `

검색위치: 이 앱에 저장된 다른 작업

${kinds.map(([k, label]) => ``).join("")}
${rows}
작업구분코드품명규격단위단가

찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}

`, + `

검색위치: · 검색어에 「10000~20000」처럼 쓰면 단가 범위

${kinds.map(([k, label]) => ``).join("")}
${rows}
작업구분코드품명규격단위단가

찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}

`, ); } async function run(name, state) { + if (name === "main-list") return mainForm(state); + if (name === "main-list-save") { + await mainSave(); + return form(state); + } if (name === "multi-search") form(state); else if (name === "multi-search-run") { const saved = state.multiSearch; @@ -35,7 +40,14 @@ const EstMultiSearch = (() => { saved.kinds = [...document.querySelectorAll('#dialog-content [name="ms-kind"]:checked')].map( (n) => Number(n.value), ); + saved.where = document.querySelector('#dialog-content [name="ms-where"]:checked')?.value; const params = new URLSearchParams({ q: saved.query, kinds: saved.kinds.join(",") }); + if (saved.where === "main") { + const ids = await mainList(); + if (!ids.length) + throw Error("설계참고철에 등록한 작업이 없습니다. 설계참고철 관리에서 추가하세요."); + params.set("only", ids.join(",")); + } if (state.project.id) params.set("exclude", state.project.id); Object.assign(saved, await EstApp.api(`/api/search?${params}`)); form(state); @@ -65,7 +77,41 @@ const EstMultiSearch = (() => { EstApp.render(); } } - const names = new Set(["multi-search", "multi-search-run", "multi-search-take"]); + // 설계참고철(TfrmMainTableList 「MAIN DATA 추가 및 삭제」): 저장된 작업 중 최대 10개 — 양식 보관함(종류 options, 이름 「설계참고철」) + const MAIN = "설계참고철"; + async function mainList() { + return ( + (await EstApp.api("/api/forms")).find((f) => f.kind === "options" && f.name === MAIN)?.data + ?.ids || [] + ); + } + async function mainForm(state) { + const ids = await mainList(), + projects = await EstApp.api("/api/projects"), + { esc } = EstApp; + EstApp.showDialog( + "MAIN DATA(설계참고철) 추가 및 삭제", + `

통합 검색 「설계참고철」에서 찾을 작업을 고르세요(최대 10개).

${projects.map((p) => ``).join("")}
`, + ); + } + async function mainSave() { + const ids = [...document.querySelectorAll('#dialog-content [name="ml-id"]:checked')].map( + (n) => n.value, + ); + if (ids.length > 10) throw Error("Main Data는 10개 이상 추가할수없습니다."); + await EstApp.api("/api/forms", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ kind: "options", name: MAIN, data: { ids } }), + }); + } + const names = new Set([ + "multi-search", + "multi-search-run", + "multi-search-take", + "main-list", + "main-list-save", + ]); function handle(name, state) { if (!names.has(name)) return false; run(name, state).catch((error) => { diff --git a/tools/est-test/search-store.cjs b/tools/est-test/search-store.cjs index 6823841e1..428f29c8e 100644 --- a/tools/est-test/search-store.cjs +++ b/tools/est-test/search-store.cjs @@ -19,12 +19,14 @@ const fold = (value) => .toLowerCase() .replace(/\s+/g, ""); -function search(directory, query, kinds, exclude = "") { +// 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]); @@ -34,7 +36,7 @@ function search(directory, query, kinds, exclude = "") { 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) continue; + if (id === exclude || (only && !only.includes(id))) continue; let project; try { project = JSON.parse(fs.readFileSync(path.join(directory, name), "utf8")); @@ -45,8 +47,15 @@ function search(directory, query, kinds, exclude = "") { const [table, label] = sources[kind]; for (const row of project.tables?.[table] || []) { if (!row?.Code) continue; - const text = fold(`${row.Code} ${row.Name} ${row.Size}`); - if (!words.every((w) => text.includes(w))) 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({ diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index c3735b188..ba044d91e 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -201,6 +201,7 @@ const server = http.createServer(async (req, res) => { url.searchParams.get("q"), kinds, url.searchParams.get("exclude") || "", + url.searchParams.get("only") ? url.searchParams.get("only").split(",") : null, ), ); return;