feat(est-test): 설계참고철(Main Data) 검색과 단가 범위 검색
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)으로 저장해 묶음 설정 없는 공사에서 열 때 씀.
|
||||
|
||||
@@ -23,11 +23,16 @@ const EstMultiSearch = (() => {
|
||||
.join("");
|
||||
EstApp.showDialog(
|
||||
"통합 검색",
|
||||
`<p>검색위치: 이 앱에 저장된 다른 작업</p><div class="form-fields">${kinds.map(([k, label]) => `<label><input type="checkbox" name="ms-kind" value="${k}" ${saved.kinds.includes(k) ? "checked" : ""}>${label}</label>`).join("")}<label>검색어<input name="ms-query" value="${esc(saved.query)}"></label></div><button data-action="multi-search-run">검색</button> <button data-action="multi-search-take">고른 자료를 이 공사로 가져오기</button><div style="max-height:360px;overflow:auto;border:1px solid #ccc;margin-top:6px"><table class="sheet" style="font-size:12px"><thead><tr><th></th><th>작업</th><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>단가</th></tr></thead><tbody>${rows}</tbody></table></div><p>찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}</p>`,
|
||||
`<p>검색위치: <label><input type="radio" name="ms-where" value="all" ${saved.where !== "main" ? "checked" : ""}>저장된 모든 작업</label> <label><input type="radio" name="ms-where" value="main" ${saved.where === "main" ? "checked" : ""}>설계참고철(Main Data)</label> <button data-action="main-list">설계참고철 관리</button> · 검색어에 「10000~20000」처럼 쓰면 단가 범위</p><div class="form-fields">${kinds.map(([k, label]) => `<label><input type="checkbox" name="ms-kind" value="${k}" ${saved.kinds.includes(k) ? "checked" : ""}>${label}</label>`).join("")}<label>검색어<input name="ms-query" value="${esc(saved.query)}"></label></div><button data-action="multi-search-run">검색</button> <button data-action="multi-search-take">고른 자료를 이 공사로 가져오기</button><div style="max-height:360px;overflow:auto;border:1px solid #ccc;margin-top:6px"><table class="sheet" style="font-size:12px"><thead><tr><th></th><th>작업</th><th>구분</th><th>코드</th><th>품명</th><th>규격</th><th>단위</th><th>단가</th></tr></thead><tbody>${rows}</tbody></table></div><p>찾은 자료 ${saved.total}건${saved.total > saved.hits.length ? ` 중 앞 ${saved.hits.length}건` : ""}</p>`,
|
||||
);
|
||||
}
|
||||
|
||||
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(설계참고철) 추가 및 삭제",
|
||||
`<p>통합 검색 「설계참고철」에서 찾을 작업을 고르세요(최대 10개).</p><div style="max-height:360px;overflow:auto">${projects.map((p) => `<label style="display:block"><input type="checkbox" name="ml-id" value="${esc(p.id)}" ${ids.includes(p.id) ? "checked" : ""}>${esc(p.name || p.id)}</label>`).join("")}</div><button data-action="main-list-save">적용</button> <button data-action="multi-search">통합 검색으로</button>`,
|
||||
);
|
||||
}
|
||||
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) => {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user