feat(est-test): 공내역 파일로 저장
- 원본 처리 근거대로 내역·일위대가 금액을 비우고 공람 방법에 따라 단가 또는 수량을 비움 - JSON 또는 원본 구조 ESTX 로 내려받기, 사본은 자동계산 꺼 빈칸 유지 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
"""EST 테스트 앱 공내역 파일 저장(원본 mnuFileNullSave · PrintGongMethod 0/1/2)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SAMPLE = Path("C:/Dasansoft/Est Plus 2/Data/ProjectData/sample.estx")
|
||||
LOAD = """
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({ structuredClone, console });
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/native-blank.js', 'utf8'), ctx);
|
||||
"""
|
||||
|
||||
|
||||
def node(script: str, *args: str):
|
||||
out = subprocess.check_output(["node", "-e", LOAD + script, *args], cwd=ROOT)
|
||||
return json.loads(out.decode("utf-8"))
|
||||
|
||||
|
||||
def test_methods_clear_amounts_and_chosen_values_only():
|
||||
result = node("""
|
||||
ctx.project = JSON.parse(fs.readFileSync('tools/est-test/sample.json', 'utf8'));
|
||||
const out = vm.runInContext(`(() => {
|
||||
const pick = (p) => p.tables.EstMainDat.find((r) => Number(r.GN) > 0 && Number(r.TU) > 0);
|
||||
const src = pick(project), at = project.tables.EstMainDat.indexOf(src);
|
||||
const r = (m) => { const b = EstBlank.make(project, m), x = b.tables.EstMainDat[at];
|
||||
return [x.TC, x.NC, x.TU > 0, x.GN > 0, b.options.AutoCalc, b.name.endsWith('(공내역)'), 'id' in b]; };
|
||||
const head = EstBlank.make(project, 0).tables.HeadMainDat.every((x) => !Number(x.TC) && !Number(x.TU));
|
||||
const wonga = EstBlank.make(project, 2).tables.Wonga.every((x) => !Number(x.Cost));
|
||||
let bad = ''; try { EstBlank.make(project, 3); } catch (e) { bad = e.message; }
|
||||
return { m0: r(0), m1: r(1), m2: r(2), head, wonga, bad,
|
||||
untouched: Number(src.TC) > 0 && Number(src.TU) > 0 };
|
||||
})()`, ctx);
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
""")
|
||||
assert result == {
|
||||
"m0": [0, 0, False, True, False, True, False],
|
||||
"m1": [0, 0, True, False, False, True, False],
|
||||
"m2": [0, 0, True, True, False, True, False],
|
||||
"head": True,
|
||||
"wonga": True,
|
||||
"bad": "공내역 방법을 고르세요.",
|
||||
"untouched": True,
|
||||
}
|
||||
|
||||
|
||||
def test_blank_estx_round_trip_keeps_quantities_and_clears_prices():
|
||||
if not SAMPLE.exists():
|
||||
pytest.skip("installed EST sample unavailable")
|
||||
result = node(
|
||||
"""
|
||||
const imp = require('./tools/est-test/import-estx.cjs'), ex = require('./tools/est-test/export-estx.cjs');
|
||||
const project = imp.importEstx(fs.readFileSync(process.argv[1]));
|
||||
ctx.project = project;
|
||||
const blank = vm.runInContext('EstBlank.make(project, 0)', ctx);
|
||||
const back = imp.importEstx(ex.exportEstx(blank));
|
||||
const rows = (p) => p.tables.EstMainDat.filter((r) => Number(r.GN) > 0);
|
||||
process.stdout.write(JSON.stringify({
|
||||
count: rows(back).length === rows(project).length,
|
||||
quantities: rows(back).every((r, i) => Number(r.GN) === Number(rows(project)[i].GN)),
|
||||
prices: back.tables.EstMainDat.every((r) => !Number(r.TU) && !Number(r.TC)),
|
||||
heads: back.tables.HeadMainDat.every((r) => !Number(r.TC)),
|
||||
sources: back.tables.LaborcDat.length === project.tables.LaborcDat.length,
|
||||
}));
|
||||
""",
|
||||
str(SAMPLE),
|
||||
)
|
||||
assert result == {
|
||||
"count": True,
|
||||
"quantities": True,
|
||||
"prices": True,
|
||||
"heads": True,
|
||||
"sources": True,
|
||||
}
|
||||
@@ -435,10 +435,11 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- [x] 찾기 창(원본 TFRMESTSERCH·TFRMCOSTWORDCHANGE) — 검색어·처음부터/현재위치부터·다음찾기로 행 이동(호표 자동 전환·쪽 이동), 단가산출 한 건씩 바꾸기 후 다음 위치, Ctrl+F. 이전 값(`*_`) 필드는 찾지 않음.
|
||||
- 검증완료: `test_est_find.py` 3개, 단추·단가 고르기·행 동작 포함 24개 통과. ORCA 브라우저에서 Ctrl+F로 창 열기, 내역 「합판거푸집」 8·15·22·26행 순서 이동, 단가산출 마지막 호표 행 찾기(호표 D0000000006 전환·선택 표시), 바꾸기 후 다음 위치(323행)·되돌리기 원상 확인.
|
||||
- 남음: 통합검색(TFRMMULTISERCH — 다른 공사·폴더 자료 검색).
|
||||
- [x] 공내역 파일로 저장(원본 mnuFileNullSaveClick 0x10cdc44 → 0x113cf38) — 디스어셈블 근거: EstMainDat·HeadTitleDat·HeadMainDat의 TC·NC·GC·JC·SC 비움, PrintGongMethod 0 = TU~SU 비움·1 = GN 비움·2 = 금액만. 웹 사본은 자동계산 끔·원가 금액 비움(원본 WongaDat 처리는 미확인). JSON 또는 원본 구조 ESTX로 내려받기.
|
||||
- 검증완료: `test_est_blank.py` 2개(세 방법·원본 불변·잘못된 방법 거부, 설치 sample.estx 공내역 ESTX 저장·재읽기에서 수량 유지·단가·금액 0·기초자료 유지), 관련 11개 통과. ORCA 브라우저에서 메뉴 → 수량만 남김 → JSON 내려받기(GN 133·TU 0·TC 0), 현재 작업 불변, 사본을 열어 합계 0 유지 확인 후 샘플 복구.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기.
|
||||
2. 공내역 파일로 저장(단가 비운 사본).
|
||||
3. 수량산출서 — 기존 줄 수량 지정·해제, 누계·4방향 합계, 산출서 복사·붙여넣기·삽입·순서, 제출용 전체 인쇄.
|
||||
4. Excel → EST 열 맞춤 가져오기(남의 공내역 Excel).
|
||||
5. 다른 공사 불러오기 시 딸린 일위·단가산출 함께 복사, 프로젝트 추가(합치기).
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
||||
|
||||
`파일 → 공내역 파일로 저장`은 원본처럼 내역·일위대가의 금액을 비운 사본을 내려받습니다. 남길 값은 수량만(단가 비움)·단가만(수량 비움)·수량·단가(금액만 비움) 중에서 고르고, 원본 ESTX를 가져온 작업은 ESTX로도 받을 수 있습니다. 사본은 자동계산을 끈 상태로 저장해 열어도 빈칸이 유지되며, 원가계산서 금액도 비웁니다. 현재 작업은 바뀌지 않습니다.
|
||||
|
||||
`편집 → 찾기·다음찾기`(Ctrl+F)는 원본 찾기 창처럼 검색어를 처음부터 또는 현재 위치부터 찾아 해당 행으로 이동합니다. 일위대가·단가산출·기계경비는 찾은 행의 호표로 넘어갑니다. 단가산출에서는 찾은 행의 문구를 한 건씩 바꾸고 다음 위치로 이동하며, 모두 바꾸기는 `산출근거 문구 바꾸기`를 씁니다. 표 위 검색창은 걸러 보기로 그대로 남습니다.
|
||||
|
||||
수량산출 화면의 수량 항목마다 `끝수 처리`에서 AUTO(산출서 기본)·절사·올림·반올림 0~5자리를 고릅니다. 원본처럼 결과 코드 뒤에 방식과 자리수를 붙이며(`r000001` → `r00000131`), 산출 행의 결과 표지도 함께 바꿉니다. 이 변경이 있는 작업은 ESTX 수정 저장을 거부하고 JSON 저장을 안내합니다.
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
<script defer src="native-block-actions.js"></script>
|
||||
<script defer src="native-pick.js"></script>
|
||||
<script defer src="native-find.js"></script>
|
||||
<script defer src="native-blank.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -68,6 +69,7 @@
|
||||
<button data-action="export-xml">조달청 XML 내보내기</button>
|
||||
<button data-action="import-xml">조달청 XML 불러오기</button>
|
||||
<button data-action="export-estx">원본 구조 유지 ESTX 수정 저장</button>
|
||||
<button data-action="blank-save">공내역 파일로 저장</button>
|
||||
<button data-action="export-jdl">JDL 기본 표 내보내기</button>
|
||||
<button data-action="import-jdl">JDL 불러오기</button>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// 원본 공내역 파일로저장(TfrmEstMain.mnuFileNullSaveClick 0x10cdc44 → 0x113cf38).
|
||||
// 내역·일위대가 제목·일위대가 상세의 금액(TC·NC·GC·JC·SC)을 비우고, 공람 방법(PrintGongMethod)에 따라
|
||||
// 0 = 단가(TU·NU·GU·JU·SU)도 비움(수량만 남김) · 1 = 수량(GN)을 비움(단가만 남김) · 2 = 금액만 비움.
|
||||
const EstBlank = (() => {
|
||||
const tables = ["EstMainDat", "HeadTitleDat", "HeadMainDat"];
|
||||
const amounts = ["TC", "NC", "GC", "JC", "SC"];
|
||||
const units = ["TU", "NU", "GU", "JU", "SU"];
|
||||
const methods = [
|
||||
[0, "수량만 남김(단가·금액 비움)"],
|
||||
[1, "단가만 남김(수량·금액 비움)"],
|
||||
[2, "수량·단가 남김(금액만 비움)"],
|
||||
];
|
||||
|
||||
function make(project, method) {
|
||||
if (![0, 1, 2].includes(method)) throw Error("공내역 방법을 고르세요.");
|
||||
const blank = structuredClone(project);
|
||||
const clear =
|
||||
method === 0 ? [...amounts, ...units] : method === 1 ? [...amounts, "GN"] : amounts;
|
||||
for (const table of tables)
|
||||
for (const row of blank.tables[table] || [])
|
||||
for (const key of clear) if (row[key] != null) row[key] = 0;
|
||||
// 금액을 비운 원가계산서가 옛 합계를 보이지 않도록 원가 금액도 비운다(원본 WongaDat 처리 확인 전 웹 보조).
|
||||
for (const row of blank.tables.Wonga || []) if (row.Cost != null) row.Cost = 0;
|
||||
delete blank.id;
|
||||
blank.name = `${project.name || "내역서"} (공내역)`;
|
||||
blank.options = { ...blank.options, AutoCalc: false, SourceRefresh: false };
|
||||
blank.calculation = { errors: [], warnings: [], direct: {}, total: 0 };
|
||||
blank.blankMethod = method;
|
||||
return blank;
|
||||
}
|
||||
|
||||
function open(state) {
|
||||
const project = state.project;
|
||||
EstActions.form(
|
||||
"공내역 파일로 저장",
|
||||
[
|
||||
{
|
||||
name: "method",
|
||||
label: "남길 값",
|
||||
value: Number(project.printSettings?.PrintGongMethod ?? 0),
|
||||
choices: methods,
|
||||
},
|
||||
{
|
||||
name: "format",
|
||||
label: "파일 형식",
|
||||
value: project.originalArchive ? "estx" : "json",
|
||||
choices: project.originalArchive
|
||||
? [
|
||||
["estx", "원본 구조 ESTX"],
|
||||
["json", "웹 작업 JSON"],
|
||||
]
|
||||
: [["json", "웹 작업 JSON"]],
|
||||
},
|
||||
],
|
||||
async ({ method, format }) => {
|
||||
const blank = make(project, Number(method));
|
||||
if (format === "estx") {
|
||||
const response = await fetch("/api/export-estx", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(blank),
|
||||
});
|
||||
if (!response.ok) throw Error((await response.json()).error);
|
||||
EstApp.download(`${blank.name}.estx`, await response.blob(), "application/octet-stream");
|
||||
} else EstApp.download(`${blank.name}.estweb.json`, JSON.stringify(blank, null, 2));
|
||||
state.notice = "공내역 파일을 내려받았습니다. 현재 작업은 바뀌지 않았습니다.";
|
||||
EstApp.render();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name !== "blank-save") return false;
|
||||
open(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
return { methods, make, handle };
|
||||
})();
|
||||
@@ -600,7 +600,8 @@ const EstBusinessActions = (() => {
|
||||
} else if (
|
||||
EstBlockActions.handle(name, state) ||
|
||||
(typeof EstPick !== "undefined" && EstPick.handle(name, state)) ||
|
||||
(typeof EstFind !== "undefined" && EstFind.handle(name, state))
|
||||
(typeof EstFind !== "undefined" && EstFind.handle(name, state)) ||
|
||||
(typeof EstBlank !== "undefined" && EstBlank.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -59,6 +59,7 @@ const allowed = new Set([
|
||||
"native-block-actions.js",
|
||||
"native-pick.js",
|
||||
"native-find.js",
|
||||
"native-blank.js",
|
||||
"native-issues.js",
|
||||
"native-attachments.js",
|
||||
"native-standard.js",
|
||||
|
||||
Reference in New Issue
Block a user