feat(est-test): 작업모드 표준내역서·표준설계변경, 설치 원가계산서 양식목록, 최근 계약심사목록
- 작업모드 값(CurrentWorkMode) ESTX 읽기·쓰기 - 일반↔표준 묶음 바꿀 때 원가계산서 보관·꺼내기, 없으면 설치 양식(일반·표준 각 30종)에서 고르기 - 저장한 작업 중 계약심사 작업 목록 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ for (const file of [
|
||||
"native-progress-view.js",
|
||||
"native-annual-copy.js",
|
||||
"native-print-range.js",
|
||||
"native-work-mode.js",
|
||||
"native-business-actions.js",
|
||||
"native-forms.js",
|
||||
"native-excel-to-est.js",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""작업모드: 일반↔표준 원가계산서 묶음 보관·꺼내기, 설치 양식목록 읽기."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from test_est_rea_parity import run
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
INSTALL = Path("C:/Dasansoft/Est Plus 2/System/MainSys/SWongaFileList.dat")
|
||||
|
||||
|
||||
def test_swap_keeps_each_group_wonga():
|
||||
result = run(
|
||||
"math",
|
||||
"""(()=>{
|
||||
const p=EstNative.empty();p.tables.Wonga=[{Name:'일반'}];
|
||||
const a=EstWorkMode.swap(p,3);const std=p.tables.Wonga.length;
|
||||
p.tables.Wonga=[{Name:'표준'}];
|
||||
const b=EstWorkMode.swap(p,4);
|
||||
const c=EstWorkMode.swap(p,1);const back=p.tables.Wonga.map(r=>r.Name);
|
||||
const d=EstWorkMode.swap(p,3);
|
||||
return {a,std,b,c,back,d,now:p.tables.Wonga.map(r=>r.Name),mode:p.options.WorkMode,
|
||||
kept:Object.keys(p.wongaByGroup)};
|
||||
})()""",
|
||||
)
|
||||
assert result["a"] == {"changed": True, "found": False}
|
||||
assert result["std"] == 1
|
||||
assert result["b"] == {"changed": False}
|
||||
assert result["c"] == {"changed": True, "found": True}
|
||||
assert result["back"] == ["일반"]
|
||||
assert result["d"] == {"changed": True, "found": True}
|
||||
assert result["now"] == ["표준"]
|
||||
assert result["mode"] == 3
|
||||
assert result["kept"] == ["general"]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not INSTALL.exists(), reason="원본 설치 폴더 없음")
|
||||
def test_install_wonga_formats_read():
|
||||
out = subprocess.check_output(
|
||||
[
|
||||
"node",
|
||||
"-e",
|
||||
"const w=require(process.argv[1]);const g=w.get(false),s=w.get(true);"
|
||||
"const f=w.get(true,s[0].index);"
|
||||
"console.log(JSON.stringify({g:g.length,s:s.length,rows:f.rows.length,"
|
||||
"cost:f.rows.some(r=>'Cost' in r),first:f.rows[0].LinkIndex}))",
|
||||
str(ROOT / "tools/est-test/wonga-formats.cjs"),
|
||||
],
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = json.loads(out)
|
||||
assert result["g"] == 30 and result["s"] == 30
|
||||
assert result["rows"] > 10 and result["cost"] is False and result["first"] == 54
|
||||
@@ -503,6 +503,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 검증완료: 시험 2개·연차 시험 통과. ORCA 브라우저에서 내역 3·4행 → 1차 10행 복사, 차수 계산 오류 없음 확인, 원복.
|
||||
- [x] 인쇄범위·인쇄양식목록(spec_pc §6.2·§6.4) — 미리보기 「인쇄범위」: 부수 · 현재면(보이는 쪽) · 현재항목 전체(쪽 범위, 0 = 전체, 넘침·거꾸로 알림, 부수마다 그 서식 간지) · 일괄인쇄(묶음 인쇄). 묶음 인쇄 창 「인쇄양식목록」: 지금 인쇄표·설정·묶음 고르기를 이름 붙여 등록 · 열면 통째로 바꿈 · 삭제 · .pfot 파일 불러오기/저장. 기성·연차 따로 목록은 두지 않음.
|
||||
- 검증완료: 시험 2개·묶음 인쇄 시험 통과. ORCA 브라우저에서 3쪽 중 1~2쪽 × 2부 = 4쪽, 범위 넘침 알림, 양식 등록·열기(묶음 고르기 되돌아옴)·삭제 확인, 원복.
|
||||
- [x] 작업모드 표준내역서·표준설계변경·최근 계약심사목록(spec_pc §4) — 작업모드 메뉴 내역서·설계변경·표준내역서·표준설계변경이 모드 값(CurrentWorkMode, ESTX 읽기·쓰기)을 바꿈. 일반↔표준 묶음이 바뀌면 지금 원가계산서를 옛 묶음 칸에 두고 새 묶음 것을 꺼냄(없으면 설치 양식목록 — 일반 WongaFileList·표준 SWongaFileList 각 30종 — 에서 고름, 번호 기준 계산). 원본은 모드를 바꿀 때 문서를 닫고 최근 목록을 띄우나 웹은 지금 작업 유지. 최근 계약심사목록 = 저장한 작업 중 계약심사 입력이 있는 것.
|
||||
- 미결: 표준 양식의 산업안전보건관리비 식 「A × 0.496」 의 A 가 무엇인지 미확인 — 웹은 「참조 항목 없음: A」 로 알림(원본 확인 필요).
|
||||
- 검증완료: 작업모드 시험 2개·ESTX 저장 시험 34개 통과. ORCA 브라우저에서 표준내역서로 바꿈 → 표준 양식 30종 → 열기, 내역서로 돌아오면 원래 원가계산서 31행 복원, 계약심사목록 창 확인, 원복.
|
||||
- [x] 다른 공사 불러오기 창(원본 barbtnSelfLoad → TfrmOtherLoad, 찾기 popSerch) — 저장된 다른 작업을 골라 같은 표를 띄우고 찾기(품명·규격, A&B 둘 다)·고른 줄을 하위 자료와 함께 가져오기(내역서는 고른 줄 아래에 줄, 나머지는 자료·호표 합침, 같은 코드는 이 공사 것). 검증완료: 바인딩 통과, ORCA 브라우저에서 산불진화임도 작업의 일위대가 3개 → 「취수장」 1개 → 가져오기 호표 8→9·하위 41건, 샘플 복구.
|
||||
- [x] 사용위치 검색 창(원본 TfrmUsesDataSerch btnSerch·btnAllDisplay) — 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기. 검증완료: 바인딩 통과, ORCA 브라우저에서 노임 9개 → 「보 통」 1개 → 보통인부 사용위치 11곳.
|
||||
- [x] 인쇄양식 불러오기·저장하기(원본 mnuPrintFormatLoad·Save 0x10ed318·0x10ed000 — spec_pc §3) — .pfot(인쇄정보표 MIDAS 이진) 읽기·쓰기 `print-format.cjs`(설명 BLOB·빈 칸 표지까지 보존, 원본 파일 그대로 다시 쓰면 바이트 같음). 불러오면 공사 인쇄표를 그 값으로, 저장은 지금 인쇄표를 원본 설치 main.pfot 필드 설명자 위에(`EST_PFOT_TEMPLATE`). .gfot(cxGrid 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
|
||||
|
||||
@@ -192,6 +192,7 @@ function exportEstx(project) {
|
||||
DollarCostCut: ["TMachOption", "DollarCostCut"],
|
||||
JDLCalcMethod: ["TCommonOption", "JDLCalcMethod"],
|
||||
OldRecalcMethod: ["TChangeOption", "OldRecalcMethod"],
|
||||
WorkMode: ["TCommonOption", "CurrentWorkMode"],
|
||||
};
|
||||
const optionChanges = {};
|
||||
const setOption = (cls, name, data) => {
|
||||
|
||||
@@ -350,6 +350,7 @@ function importEstx(buffer, name = "가져온 내역서") {
|
||||
DollarCostCut: props.TMachOption?.DollarCostCut || false,
|
||||
JDLCalcMethod: props.TCommonOption?.JDLCalcMethod || false,
|
||||
OldRecalcMethod: props.TChangeOption?.OldRecalcMethod || 0,
|
||||
WorkMode: props.TCommonOption?.CurrentWorkMode ?? 1,
|
||||
};
|
||||
const quantitySheets = [];
|
||||
let printCatalog = [];
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<script defer src="native-progress-view.js"></script>
|
||||
<script defer src="native-annual-copy.js"></script>
|
||||
<script defer src="native-print-range.js"></script>
|
||||
<script defer src="native-work-mode.js"></script>
|
||||
<script defer src="native-actions-more.js"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
@@ -252,6 +253,7 @@
|
||||
><button data-action="options-save-default">기본환경으로 설정</button
|
||||
><button data-action="reset-wonga">원가계산서 초기화</button>
|
||||
<button data-action="wonga-forms">원가계산서 양식 목록·등록</button>
|
||||
<button data-action="wonga-install-list">설치 원가계산서 양식목록(일반·표준)</button>
|
||||
<button data-action="jungsang-toggle">정산원가계산서 작성/해제</button
|
||||
><button data-action="jungsang-init">당초금액 초기화</button>
|
||||
<button data-action="wonga-gita">기타경비 계산</button
|
||||
@@ -264,7 +266,9 @@
|
||||
<details>
|
||||
<summary>작업모드(M)</summary>
|
||||
<div class="menu">
|
||||
<button data-view="EstMainDat">내역서</button
|
||||
<button data-action="work-mode" data-mode="1">내역서</button
|
||||
><button data-action="work-mode" data-mode="3">표준내역서</button
|
||||
><button data-action="work-mode" data-mode="4">표준설계변경</button
|
||||
><button data-view="standard">표준시장단가 사용 내역</button
|
||||
><button data-view="standard-change">표준시장단가 설계변경</button
|
||||
><button data-view="estimate-usage">견적단가 사용 내역</button
|
||||
@@ -275,11 +279,13 @@
|
||||
><button data-view="labor-change">노임단가 설계변경</button
|
||||
><button data-view="machine-usage">기계경비 사용 내역</button
|
||||
><button data-view="machine-change">기계경비 설계변경</button
|
||||
><button data-view="change">설계변경</button><button data-view="progress">기성고</button
|
||||
><button data-action="work-mode" data-mode="2">설계변경</button
|
||||
><button data-view="progress">기성고</button
|
||||
><button data-view="escalation">물가변동</button
|
||||
><button data-action="es-info">ES 정보</button
|
||||
><button data-action="jdl-compare">JDL엑셀과 단가비교</button
|
||||
><button data-view="annual">연차공사</button><button data-view="review">계약심사</button
|
||||
><button data-action="invest-work-list">최근 계약심사목록</button
|
||||
><button data-view="resources">동원인원·자재·장비</button>
|
||||
<button data-view="schedule">예정공정표</button>
|
||||
<button data-view="issues">자료 오류 검색</button>
|
||||
@@ -321,7 +327,8 @@
|
||||
><button id="haljung-calc-btn" data-action="haljung-calc" hidden>할증계산</button
|
||||
><button id="vat-calc-btn" data-action="vat-calc" hidden>부가세계산</button
|
||||
><button id="unit-calc-btn" data-action="unit-calc" hidden>단위환산</button
|
||||
><button id="sources-btn" data-action="sources" hidden title="발췌근거 삭제,삽입 및 변경">발췌근거</button
|
||||
><button id="sources-btn" data-action="sources" hidden title="발췌근거 삭제,삽입 및 변경">
|
||||
발췌근거</button
|
||||
><button id="uses-search-btn" data-action="uses-data-search">사용위치</button
|
||||
><button data-action="uses-list">사용위치 검색</button
|
||||
><button data-action="other-load">다른 공사 불러오기</button
|
||||
|
||||
@@ -11,6 +11,7 @@ const EstActionsMore = (() => {
|
||||
typeof EstProgressView !== "undefined" && EstProgressView,
|
||||
typeof EstAnnualCopy !== "undefined" && EstAnnualCopy,
|
||||
typeof EstPrintRange !== "undefined" && EstPrintRange,
|
||||
typeof EstWorkMode !== "undefined" && EstWorkMode,
|
||||
].filter(Boolean);
|
||||
function handle(name, state, node) {
|
||||
for (const module of modules()) if (module.handle(name, state, node)) return true;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// 원본 작업모드 바꾸기(0x10cb8b4 — spec_pc §4): 내역서 1 · 설계변경 2 · 표준내역서 3(mnuProgramSEst) · 표준설계변경 4(mnuProgramSCEst).
|
||||
// 일반(1·2) ↔ 표준(3·4) 묶음이 바뀌면 지금 원가계산서를 옛 묶음 칸에 두고 새 묶음 원가계산서를 꺼낸다(0x113c80c) — 그 묶음에
|
||||
// 둔 것이 없으면 설치 원가계산서 양식목록(일반 WongaFileList · 표준 SWongaFileList)에서 고른다. 모드 값은 TCommonOption.CurrentWorkMode.
|
||||
// 원본은 모드를 바꿀 때 지금 문서를 닫고 최근 작업 목록을 띄우지만, 웹은 지금 작업을 그대로 두고 화면만 옮긴다.
|
||||
// 그리고 「최근 계약심사목록」(mnuInvestWorkFileList 0x10d0c88): 저장한 작업 중 계약심사 작업만.
|
||||
const EstWorkMode = (() => {
|
||||
const NAMES = { 1: "내역서", 2: "설계변경", 3: "표준내역서", 4: "표준설계변경" };
|
||||
const standard = (mode) => [3, 4, 12].includes(Number(mode));
|
||||
const groupOf = (mode) => (standard(mode) ? "standard" : "general");
|
||||
const mode = (project) => Number(project.options.WorkMode) || 1;
|
||||
|
||||
// 묶음 바꾸기: 옛 원가계산서를 보관하고 새 묶음 것을 돌려줌(없으면 null)
|
||||
function swap(project, next) {
|
||||
const from = groupOf(mode(project)),
|
||||
to = groupOf(next);
|
||||
project.options.WorkMode = next;
|
||||
if (from === to) return { changed: false };
|
||||
project.wongaByGroup ||= {};
|
||||
project.wongaByGroup[from] = structuredClone(project.tables.Wonga || []);
|
||||
const kept = project.wongaByGroup[to];
|
||||
if (kept) {
|
||||
project.tables.Wonga = structuredClone(kept);
|
||||
delete project.wongaByGroup[to];
|
||||
}
|
||||
return { changed: true, found: Boolean(kept) };
|
||||
}
|
||||
|
||||
async function picker(state, std, after = "") {
|
||||
const { esc } = EstApp,
|
||||
list = await EstApp.api(`/api/wonga-formats?std=${std ? 1 : 0}`);
|
||||
EstApp.showDialog(
|
||||
`${std ? "표준" : "일반"} 원가계산서 양식목록`,
|
||||
`<p>${esc(after)}</p><table class="sheet"><thead><tr><th>No</th><th>양식 이름</th><th></th></tr></thead><tbody>${list
|
||||
.map(
|
||||
(f) =>
|
||||
`<tr><td>${f.index}</td><td style="text-align:left">${esc(f.name)}</td><td><button data-action="wonga-install-open" data-std="${std ? 1 : 0}" data-index="${f.index}">열 기</button></td></tr>`,
|
||||
)
|
||||
.join("")}</tbody></table>`,
|
||||
);
|
||||
}
|
||||
async function openForm(state, std, index) {
|
||||
const form = await EstApp.api(`/api/wonga-formats?std=${std}&index=${index}`);
|
||||
// 설치 양식은 번호(NumStr)로 서로 가리키는 꼴 — 번호 기준 계산(VerFormat 끔)
|
||||
EstApp.change((project) => {
|
||||
project.options.TotalFix = false;
|
||||
project.options.VerFormat = false;
|
||||
project.tables.Wonga = structuredClone(form.rows);
|
||||
});
|
||||
EstApp.byId("dialog").close();
|
||||
state.notice = `원가계산서를 「${form.name}」 양식으로 바꿨습니다.`;
|
||||
EstApp.render();
|
||||
}
|
||||
|
||||
async function switchTo(state, next) {
|
||||
const project = state.project;
|
||||
if (mode(project) === next) {
|
||||
state.view = next === 2 || next === 4 ? "change" : "EstMainDat";
|
||||
return EstApp.render();
|
||||
}
|
||||
let result;
|
||||
EstApp.change((p) => (result = swap(p, next)));
|
||||
state.view = next === 2 || next === 4 ? "change" : "EstMainDat";
|
||||
state.notice = `작업모드: ${NAMES[next]}`;
|
||||
EstApp.render();
|
||||
if (result.changed && !result.found)
|
||||
await picker(
|
||||
state,
|
||||
standard(next),
|
||||
`${NAMES[next]} 모드의 원가계산서 양식을 고르세요. (지금 원가계산서는 ${standard(next) ? "일반" : "표준"} 쪽에 보관)`,
|
||||
);
|
||||
}
|
||||
|
||||
async function investList(state) {
|
||||
const { esc } = EstApp,
|
||||
list = (await EstApp.api("/api/projects")).filter((p) => p.invest);
|
||||
EstApp.showDialog(
|
||||
"최근 계약심사목록",
|
||||
`<button data-action="projects">다른공사열기</button> <button data-action="new">새공사시작</button><table class="sheet" style="margin-top:6px"><thead><tr><th>구 분</th><th>공 사 명</th><th>최종작업일</th><th></th></tr></thead><tbody>${
|
||||
list
|
||||
.map(
|
||||
(p) =>
|
||||
`<tr><td>계약심사</td><td style="text-align:left">${esc(p.name)}</td><td>${esc(String(p.modified).slice(0, 10))}</td><td><button data-action="load-project" data-id="${p.id}">열 기</button><button data-action="project-hide" data-id="${p.id}">목록에서삭제</button></td></tr>`,
|
||||
)
|
||||
.join("") || '<tr><td colspan="4">계약심사 작업이 없습니다.</td></tr>'
|
||||
}</tbody></table>`,
|
||||
);
|
||||
}
|
||||
|
||||
function handle(name, state, node) {
|
||||
const fail = (error) => {
|
||||
state.notice = error.message;
|
||||
EstApp.render();
|
||||
};
|
||||
if (name === "work-mode") switchTo(state, Number(node.dataset.mode)).catch(fail);
|
||||
else if (name === "wonga-install-list")
|
||||
picker(state, standard(mode(state.project))).catch(fail);
|
||||
else if (name === "wonga-install-open")
|
||||
openForm(state, node.dataset.std, node.dataset.index).catch(fail);
|
||||
else if (name === "invest-work-list") investList(state).catch(fail);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
return { swap, groupOf, NAMES, handle };
|
||||
})();
|
||||
@@ -7,6 +7,7 @@ const http = require("node:http"),
|
||||
const { importEstx, table: dbisamTable } = require("./import-estx.cjs");
|
||||
const { readXls } = require("./xls-read.cjs");
|
||||
const bookStore = require("./book-store.cjs");
|
||||
const wongaFormats = require("./wonga-formats.cjs");
|
||||
const printFormat = require("./print-format.cjs");
|
||||
const pfotTemplate =
|
||||
process.env.EST_PFOT_TEMPLATE || "C:/Dasansoft/Est Plus 2/System/EstFormat/main.pfot";
|
||||
@@ -109,6 +110,7 @@ const allowed = new Set([
|
||||
"native-progress-view.js",
|
||||
"native-annual-copy.js",
|
||||
"native-print-range.js",
|
||||
"native-work-mode.js",
|
||||
"native-actions-more.js",
|
||||
"native-wonga-tools.js",
|
||||
"native-estimate-print.js",
|
||||
@@ -170,6 +172,7 @@ function list() {
|
||||
id: name.slice(0, -5),
|
||||
name: record.name,
|
||||
modified: fs.statSync(file).mtime.toISOString(),
|
||||
invest: record.contractReview != null || [11, 12].includes(record.options?.WorkMode),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.modified.localeCompare(a.modified));
|
||||
@@ -340,6 +343,11 @@ const server = http.createServer(async (req, res) => {
|
||||
reply(res, 200, readXls(await body(req)));
|
||||
return;
|
||||
}
|
||||
if (req.method === "GET" && url.pathname === "/api/wonga-formats") {
|
||||
const index = url.searchParams.get("index");
|
||||
reply(res, 200, wongaFormats.get(url.searchParams.get("std") === "1", index ?? undefined));
|
||||
return;
|
||||
}
|
||||
if (req.method === "POST" && url.pathname === "/api/pps-table") {
|
||||
// 조달청 코드 목록(원본 System\MainSys\PPS_STUFFC.dat · PPS_HEAD.dat, DBISAM) — 코드·명칭·규격·단위만
|
||||
const rows = dbisamTable(await body(req), null, []);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// 원본 설치 원가계산서 양식목록 — 일반 WongaFileList(양식 번호 7) · 표준 SWongaFileList(0x16) (System\MainSys, DBISAM 표).
|
||||
// 행마다 이름과 원가 표 한 벌(BLOB = TPF0 메모리 표). 작업모드를 일반 ↔ 표준으로 바꿀 때와 양식목록에서 고를 때 쓴다.
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { table, memoryRows } = require("./import-estx.cjs");
|
||||
|
||||
const root = () => process.env.EST_INSTALL || "C:/Dasansoft/Est Plus 2";
|
||||
const cache = new Map();
|
||||
|
||||
function load(standard) {
|
||||
const name = standard ? "SWongaFileList" : "WongaFileList";
|
||||
if (cache.has(name)) return cache.get(name);
|
||||
const dir = path.join(root(), "System", "MainSys"),
|
||||
dat = path.join(dir, `${name}.dat`);
|
||||
if (!fs.existsSync(dat)) throw Error(`설치 원가계산서 양식(${name})을 찾을 수 없습니다.`);
|
||||
const parsed = table(fs.readFileSync(dat), fs.readFileSync(path.join(dir, `${name}.blb`)), []);
|
||||
const list = (parsed.rows || parsed)
|
||||
.filter((r) => r.BLOB?.binary)
|
||||
.sort((a, b) => (Number(a.Index) || 0) - (Number(b.Index) || 0))
|
||||
.map((r) => ({
|
||||
index: Number(r.Index) || 0,
|
||||
name: String(r.Name ?? ""),
|
||||
rows: memoryRows(Buffer.from(r.BLOB.binary, "base64")).map(({ Cost, ...rest }) => rest),
|
||||
}));
|
||||
cache.set(name, list);
|
||||
return list;
|
||||
}
|
||||
// 목록(이름만) 또는 한 양식(행 포함)
|
||||
function get(standard, index) {
|
||||
const list = load(standard);
|
||||
if (index == null) return list.map(({ index: i, name }) => ({ index: i, name }));
|
||||
const found = list.find((f) => f.index === Number(index));
|
||||
if (!found) throw Error("원가계산서 양식이 없습니다.");
|
||||
return found;
|
||||
}
|
||||
module.exports = { get };
|
||||
Reference in New Issue
Block a user