diff --git a/resources/tester/test_est_defaults.py b/resources/tester/test_est_defaults.py new file mode 100644 index 000000000..07f701039 --- /dev/null +++ b/resources/tester/test_est_defaults.py @@ -0,0 +1,24 @@ +"""EST 테스트 앱 기본환경 설정·목록에서 삭제(원본 TfrmOption btnSave·TfrmWorkFileList btnDelete).""" + +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] + + +def test_pick_keeps_only_environment_keys_and_store_kind(tmp_path): + script = """ + const fs = require('fs'), vm = require('vm'); + const ctx = vm.createContext({}); + vm.runInContext(fs.readFileSync('tools/est-test/native-defaults.js', 'utf8'), ctx); + const picked = vm.runInContext("EstDefaults.pick({ HeadDigits: 2, TotalFix: true, Mode: '설계변경', AutoCalc: false })", ctx); + const f = require(process.cwd() + '/tools/est-test/forms-store.cjs'); + f.save(process.argv[1], { kind: 'options', name: '기본환경', data: picked }); + process.stdout.write(JSON.stringify({ picked, stored: f.read(process.argv[1]).map((x) => [x.kind, x.name]) })); + """ + out = json.loads( + subprocess.check_output(["node", "-e", script, str(tmp_path)], cwd=ROOT).decode("utf-8") + ) + assert out["picked"] == {"HeadDigits": 2, "TotalFix": True} + assert out["stored"] == [["options", "기본환경"]] diff --git a/tools/est-test/PLAN.md b/tools/est-test/PLAN.md index 41c988bd8..0c48226fe 100644 --- a/tools/est-test/PLAN.md +++ b/tools/est-test/PLAN.md @@ -502,6 +502,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD - 남음: 설정값 일괄적용(간지 글꼴·여백 13항목), 번호자동매김·시작 쪽, 끌어서 차례 바꾸기. - [x] 기계경비 공식 모음(원본 TfrmMachGongSic 0xf146f0·MachText.Dat, TfrmMachText 0xd3d58c, 구공식변환 0xd90c60 — 디스어셈블 근거) — 서버 양식 보관함 종류 machformula, 삽입·추가·삭제·번호 1..N·저장, 단가산출 현재 행 아래로 본문 줄 넣기, 옛 394바이트 레코드 변환. 원본이 기계경비 연결 때 덧붙이는 요약 4줄은 형식 확인 전이라 빼둠. - 검증완료: `test_est_mach_formula.py` 2개(구공식 두 공식·본문 CR·크기 오류, 줄 넣기 PkID·위치, 보관함 종류) 통과. ORCA 브라우저에서 공식 추가·저장 → 보관함 등록 → 단가산출 호표 아래 2줄 넣기 → 보관함 정리·샘플 복구. + - [x] 기본환경으로 설정·목록에서 삭제(원본 TfrmOption btnSave, TfrmWorkFileList btnDelete) — 계산 환경 14개 키를 서버 보관함(종류 options)에 저장해 새 내역서에 적용, 저장 작업은 data/removed 로 옮겨 목록에서만 뺌(지우지 않음). + - 검증완료: `test_est_defaults.py`(환경 키만 고름·보관함 종류) 통과. ORCA 브라우저에서 내역 소수 3자리 → 기본환경 → 새 내역서에 3 적용, 시험 작업 저장 → 목록에서 삭제 → 목록에서 빠지고 removed 로 이동 확인 후 시험 파일·기본환경 정리. - 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133. - [ ] 남은 핵심(실무 빈도순): 3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄. diff --git a/tools/est-test/README.md b/tools/est-test/README.md index 564e6e0e2..5ce2092b4 100644 --- a/tools/est-test/README.md +++ b/tools/est-test/README.md @@ -22,6 +22,8 @@ 내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다. +`원가계산서 → 기본환경으로 설정`은 현재 자릿수·도급액 고정 등 계산 환경을 서버에 기본값으로 저장하고, `새 내역서`를 만들 때 적용합니다. `저장한 작업` 목록의 `목록에서 삭제`는 원본 최근공사목록처럼 목록에서만 빼며 파일은 `data/removed/`로 옮겨 남깁니다. + `편집 → 기계경비 공식`은 원본 기계경비 공식 창처럼 명칭과 공식 본문을 모아 두는 목록(공사와 무관하게 이 앱 서버에 저장)을 편집합니다. 단가산출 화면에서 행을 고르고 `단가산출에 불러오기`를 누르면 공식 본문을 줄마다 그 아래에 넣습니다(계산식으로 풀지 않음). `구공식변환`으로 옛 EST Plus `mach_Text.dat`(394바이트 레코드)를 읽어 덧붙입니다. `편집 → 하단으로 집계`는 원본처럼 공종이 끝나는 자리(다음 공종 머리·순공사비 앞·마지막)에 「소 계」 줄을 넣고, 계산할 때 그 공종 상세 행의 합을 채웁니다. 겹쳐 넣지 않으며 `상단으로 집계`는 소계 줄을 지웁니다. `공종 다듬기`는 레벨마다 공종명을 들여 보입니다. `단가산출 들여쓰기`는 산출근거 줄 앞 빈칸을 같은 칸 수로 맞추고(분수 세 줄은 함께 옮김), `단가산출 레벨 확인`은 현재 호표의 레벨 줄과 비목 소계를 계층으로 보입니다. diff --git a/tools/est-test/forms-store.cjs b/tools/est-test/forms-store.cjs index 5d5a021a1..72024ddde 100644 --- a/tools/est-test/forms-store.cjs +++ b/tools/est-test/forms-store.cjs @@ -3,7 +3,7 @@ const fs = require("node:fs"); const path = require("node:path"); -const kinds = new Set(["wonga", "document", "machformula"]); +const kinds = new Set(["wonga", "document", "machformula", "options"]); const LIMIT = 2 * 1024 * 1024; function file(directory) { diff --git a/tools/est-test/index.html b/tools/est-test/index.html index ee52f8f67..a8ece4079 100644 --- a/tools/est-test/index.html +++ b/tools/est-test/index.html @@ -60,6 +60,7 @@ + @@ -193,6 +194,7 @@ { project = state.project; state.notice = ""; if (name === "new") { - if (confirm("저장하지 않은 입력을 비우고 새 내역서를 만드시겠습니까?")) + if (confirm("저장하지 않은 입력을 비우고 새 내역서를 만드시겠습니까?")) { EstApp.setProject(EstNative.empty()); + if (typeof EstDefaults !== "undefined") await EstDefaults.applyNew(); + } } else if (name === "sample") { if (confirm("현재 작업을 REA 샘플로 바꾸시겠습니까?")) EstApp.setProject(await api("/api/sample")); @@ -312,7 +314,7 @@ const EstActions = (() => { ? projects .map( (p) => - `

${esc(p.modified)}

`, + `

${esc(p.modified)}

`, ) .join("") : "저장된 작업이 없습니다.", diff --git a/tools/est-test/native-business-actions.js b/tools/est-test/native-business-actions.js index ed11e28dc..ebe0aa032 100644 --- a/tools/est-test/native-business-actions.js +++ b/tools/est-test/native-business-actions.js @@ -620,7 +620,8 @@ const EstBusinessActions = (() => { (typeof EstAutoLink !== "undefined" && EstAutoLink.handle(name, state)) || (typeof EstEstTools !== "undefined" && EstEstTools.handle(name, state)) || (typeof EstPrintBatch !== "undefined" && EstPrintBatch.handle(name, state)) || - (typeof EstMachFormula !== "undefined" && EstMachFormula.handle(name, state, node)) + (typeof EstMachFormula !== "undefined" && EstMachFormula.handle(name, state, node)) || + (typeof EstDefaults !== "undefined" && EstDefaults.handle(name, state, node)) ) { EstApp.render(); } else if (name === "load-contract" || name === "load-bid") { diff --git a/tools/est-test/native-defaults.js b/tools/est-test/native-defaults.js new file mode 100644 index 000000000..8ec765b04 --- /dev/null +++ b/tools/est-test/native-defaults.js @@ -0,0 +1,69 @@ +// 원본 환경설정 「기본환경으로설정」(TfrmOption btnSave)과 최근공사목록 「목록에서삭제」(TfrmWorkFileList btnDelete). +// 기본환경: 현재 계산 환경 값을 서버 양식 보관함(종류 options, 이름 「기본환경」)에 두고 새 내역서를 만들 때 덮어 쓴다. +// 목록에서 삭제: 원본은 최근 목록에서만 빼므로, 웹은 저장 파일을 data/removed/ 로 옮겨 목록에서만 사라지게 한다(지우지 않음). +const EstDefaults = (() => { + const keys = [ + "HeadDigits", + "EstDigits", + "CostValueTrunc", + "MidCostDiget", + "TotalFix", + "TotalFit", + "DollarCostCut", + "JDLCalcMethod", + "GCToWonga", + "GangubExceptJodalRatio", + "EstLeftSpace", + "GitaGCRatio", + "GitaBaseRatio", + "AnjunBaseCost", + ]; + const NAME = "기본환경"; + const post = (body) => ({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + + function pick(options) { + return Object.fromEntries( + keys.filter((k) => options?.[k] !== undefined).map((k) => [k, options[k]]), + ); + } + async function saved() { + return (await EstApp.api("/api/forms")).find((f) => f.kind === "options" && f.name === NAME) + ?.data; + } + async function applyNew() { + const data = await saved(); + if (data) EstApp.change((project) => Object.assign(project.options, pick(data))); + return Boolean(data); + } + + async function run(name, state, node) { + if (name === "options-save-default") { + if (!confirm("현재 계산 환경을 새 내역서의 기본환경으로 설정하시겠습니까?")) return; + await EstApp.api( + "/api/forms", + post({ kind: "options", name: NAME, data: pick(state.project.options) }), + ); + state.notice = "기본환경으로 설정했습니다. 새 내역서에 적용됩니다."; + } else if (name === "project-hide") { + if (!confirm("이 작업을 목록에서 빼시겠습니까? 파일은 data/removed 에 남습니다.")) return; + await EstApp.api(`/api/projects/${node.dataset.id}/hide`, { method: "POST" }); + EstApp.byId("dialog").close(); + state.notice = "목록에서 뺐습니다."; + } + EstApp.render(); + } + function handle(name, state, node) { + if (name !== "options-save-default" && name !== "project-hide") return false; + run(name, state, node).catch((error) => { + state.notice = error.message; + EstApp.render(); + }); + return true; + } + + return { keys, pick, applyNew, handle }; +})(); diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs index 69bfc13b0..3f14f4527 100644 --- a/tools/est-test/server.cjs +++ b/tools/est-test/server.cjs @@ -82,6 +82,7 @@ const allowed = new Set([ "native-est-tools.js", "native-print-batch.js", "native-mach-formula.js", + "native-defaults.js", "native-issues.js", "native-attachments.js", "native-standard.js", @@ -194,6 +195,19 @@ const server = http.createServer(async (req, res) => { reply(res, 200, forms.remove(dataDirectory, input.kind, input.name)); return; } + if (req.method === "POST" && /^\/api\/projects\/[a-f0-9]{24}\/hide$/.test(url.pathname)) { + const id = url.pathname.split("/")[3], + file = path.join(dataDirectory, `${id}.json`), + removed = path.join(dataDirectory, "removed"); + if (!fs.existsSync(file)) { + reply(res, 404, { error: "저장된 작업 없음" }); + return; + } + fs.mkdirSync(removed, { recursive: true }); + fs.renameSync(file, path.join(removed, `${id}.json`)); + reply(res, 200, { id }); + return; + } if (req.method === "GET" && url.pathname === "/api/projects") { reply(res, 200, list()); return;