feat(est-test): 기본환경으로 설정과 저장 작업 목록에서 삭제

- 계산 환경을 기본값으로 저장해 새 내역서에 적용
- 저장 작업을 data/removed 로 옮겨 목록에서만 뺌

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-11 21:17:20 +09:00
co-authored by Claude Opus 5.5
parent 5e7613eef7
commit 39c147a8a4
9 changed files with 120 additions and 4 deletions
+2
View File
@@ -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방향 합계, 제출용 전체 인쇄.
+2
View File
@@ -22,6 +22,8 @@
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
`원가계산서 → 기본환경으로 설정`은 현재 자릿수·도급액 고정 등 계산 환경을 서버에 기본값으로 저장하고, `새 내역서`를 만들 때 적용합니다. `저장한 작업` 목록의 `목록에서 삭제`는 원본 최근공사목록처럼 목록에서만 빼며 파일은 `data/removed/`로 옮겨 남깁니다.
`편집 → 기계경비 공식`은 원본 기계경비 공식 창처럼 명칭과 공식 본문을 모아 두는 목록(공사와 무관하게 이 앱 서버에 저장)을 편집합니다. 단가산출 화면에서 행을 고르고 `단가산출에 불러오기`를 누르면 공식 본문을 줄마다 그 아래에 넣습니다(계산식으로 풀지 않음). `구공식변환`으로 옛 EST Plus `mach_Text.dat`(394바이트 레코드)를 읽어 덧붙입니다.
`편집 → 하단으로 집계`는 원본처럼 공종이 끝나는 자리(다음 공종 머리·순공사비 앞·마지막)에 「소 계」 줄을 넣고, 계산할 때 그 공종 상세 행의 합을 채웁니다. 겹쳐 넣지 않으며 `상단으로 집계`는 소계 줄을 지웁니다. `공종 다듬기`는 레벨마다 공종명을 들여 보입니다. `단가산출 들여쓰기`는 산출근거 줄 앞 빈칸을 같은 칸 수로 맞추고(분수 세 줄은 함께 옮김), `단가산출 레벨 확인`은 현재 호표의 레벨 줄과 비목 소계를 계층으로 보입니다.
+1 -1
View File
@@ -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) {
+2
View File
@@ -60,6 +60,7 @@
<script defer src="native-est-tools.js"></script>
<script defer src="native-print-batch.js"></script>
<script defer src="native-mach-formula.js"></script>
<script defer src="native-defaults.js"></script>
<script defer src="native-actions.js"></script>
<script defer src="native-business-actions.js"></script>
<script defer src="native-app.js"></script>
@@ -193,6 +194,7 @@
<button data-view="Wonga">원가계산서 보기</button
><button data-action="template">원가 서식 선택</button
><button data-action="settings">자릿수·도급액 고정</button
><button data-action="options-save-default">기본환경으로 설정</button
><button data-action="reset-wonga">원가계산서 초기화</button>
<button data-action="wonga-forms">원가계산서 양식 목록·등록</button>
<button data-action="jungsang-toggle">정산원가계산서 작성/해제</button
+4 -2
View File
@@ -137,8 +137,10 @@ const EstActions = (() => {
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) =>
`<p><button data-action="load-project" data-id="${p.id}">${esc(p.name)}</button> <small>${esc(p.modified)}</small></p>`,
`<p><button data-action="load-project" data-id="${p.id}">${esc(p.name)}</button> <small>${esc(p.modified)}</small> <button data-action="project-hide" data-id="${p.id}">목록에서 삭제</button></p>`,
)
.join("")
: "저장된 작업이 없습니다.",
+2 -1
View File
@@ -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") {
+69
View File
@@ -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 };
})();
+14
View File
@@ -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;