feat(est-test): 발췌근거 출처 열 이름·보이기
- 원본 barbtnColumn: 자재·노임 단가의 출처 열(Cost1~6) 이름·쪽수·보기 편집, 자재는 8가지 출처 이름·중복 거부 - 숨긴 출처는 최저·평균·최고 단가 고르기에서 제외(브라우저·서버 같은 엔진) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""EST 테스트 앱 발췌근거(원본 barbtnColumn — 가격 출처 열 이름·보이기, 숨긴 출처는 단가 고르기에서 제외)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FILES = [
|
||||
"native-math.js",
|
||||
"native-formula.js",
|
||||
"native-machine.js",
|
||||
"native-rows.js",
|
||||
"native-text.js",
|
||||
"native-quantity.cjs",
|
||||
"native-wonga.js",
|
||||
"native-sources.js",
|
||||
"native-engine.js",
|
||||
"native-model.js",
|
||||
]
|
||||
|
||||
|
||||
def run_js(code: str):
|
||||
runner = f"""
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({{ structuredClone, console }});
|
||||
for (const f of {json.dumps(FILES)})
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/' + f, 'utf8'), ctx, {{ filename: f }});
|
||||
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
|
||||
"""
|
||||
return json.loads(subprocess.check_output(["node", "-e", runner], cwd=ROOT).decode("utf-8"))
|
||||
|
||||
|
||||
def test_hidden_source_leaves_price_choice_and_labels():
|
||||
out = run_js(
|
||||
r"""(() => {
|
||||
const p = EstNative.empty();
|
||||
p.options.SourceRefresh = false;
|
||||
p.tables.StuffcDat = [{ Code: 'M1', Name: '철근', Cost1: 100, Cost2: 80, Cost3: 120, ValueIndex: 0 },
|
||||
{ Code: 'M2', Name: '모래', Cost1: 100, Cost2: 80, Cost3: 120, ValueIndex: 2 }];
|
||||
const tc = (q) => EstNative.calculate(q).tables.StuffcDat.map((r) => r.GC);
|
||||
const before = tc(p);
|
||||
const list = EstSources.get(p.options, 'StuffcDat');
|
||||
list[1] = { caption: '물가자료', page: '120', visible: false };
|
||||
list[2] = { caption: '유통물가', page: '', visible: false };
|
||||
EstSources.set(p.options, 'StuffcDat', list);
|
||||
const after = tc(p);
|
||||
let dup = '';
|
||||
try { EstSources.set(p.options, 'StuffcDat', list.map(() => ({ caption: '가격정보' }))); } catch (e) { dup = e.message; }
|
||||
EstSources.set(p.options, 'LaborcDat', list.map(() => ({ caption: '같음' })));
|
||||
return { before, after, dup, label: EstSources.label(p.options, 'StuffcDat', 'Cost2'),
|
||||
other: EstSources.label(p.options, 'EstMainDat', 'Cost2'),
|
||||
cols: EstSources.columns(p.options, 'StuffcDat', ['Code', 'Cost1', 'Cost2', 'Cost3', 'TC']) };
|
||||
})()"""
|
||||
)
|
||||
assert out["before"] == [80, 120]
|
||||
assert out["after"] == [100, 100]
|
||||
assert out["dup"] == "같은 발췌근거가 있습니다."
|
||||
assert out["label"] == "물가자료 (120)"
|
||||
assert out["other"] == ""
|
||||
assert out["cols"] == ["Code", "Cost1", "TC"]
|
||||
@@ -487,6 +487,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] 발췌근거(원본 barbtnColumn — TfrmHeaderChange 0x110b7ec · 자재 TfrmStuffcHeadChange 0xf61b84) — 자재·노임 화면 「발췌근거」 단추로 출처 열(Cost1~6) 이름·쪽수(자재)·보기 편집, 자재는 8가지 출처 이름에서 고르고 중복 거부, 숨긴 출처는 최저·평균·최고 단가 고르기에서 빠짐(서버 재계산도 같은 코드). 값은 계산 환경 `PriceSources`(ESTX 저장에는 남지 않음 — 원본도 출처 이름은 격자 배치에만 둠).
|
||||
- 검증완료: `test_est_sources.py`(최저 80→100·최고 120→100, 중복 거부, 머리글 「물가자료 (120)」, 숨긴 열 빠짐) 및 REA 대조 39개 통과. ORCA 브라우저에서 자재단가 발췌근거 2번 「물가자료」 쪽 77, 3번 숨김 → 머리글 반영·도급액 불변, 설정 지워 복구.
|
||||
- [x] 조달청 표준공사코드 적용(원본 mnuCodeChange → TfrmCodeChange — 디스어셈블 근거) — 작업데이타 7종 탭(일위·단산·기계·표준·견적·자재·노임), 코드변경(목록 코드·직접 입력, 기계 16자리면 끝 0), 검사 색(EST 코드 노랑·다른 표 중복 연분홍+비고·자리수 진황), 변경취소, 적용(자기 표·본문 머리 Code/TitleCode·참조 행 LinkIndex=종류·Index≤0 만, 재계산 없음), 코드일괄변경(고른 .estx 의 같은 표에서 자동연결 맞춤 규칙, EST→비EST 만·중복 금지, 표준은 옛 코드 부분일치), 자동변경(표준 행 번호 구간 C·A·M), 다산코드정리, 사용자정의코드, 목록·작업데이타 검색(& 두 조각). 조달청 목록은 원본 설치의 PPS_STUFFC.dat·PPS_HEAD.dat 를 사용자가 골라 불러옴(내장 자료 없음, `/api/pps-table`). 원본의 분류 트리(PPS_*TYPE)·다산정리 PkID 쓰기는 하지 않음.
|
||||
- 검증완료: `test_est_code_change.py` 2개(검사·중복·자리수 거부, 취소, 적용 범위·Index>0 제외, 기계 머리 TitleCode, 자동변경·다산·사용자정의코드 꼴, 설치 JodalCodeDB 로 노임 일괄변경 1건·PPS_STUFFC 58,653행) 통과. ORCA 브라우저에서 샘플 노임 「보통인부」 L0000000000→L001010101000001 적용 → 일위·단산 참조 11행 함께 바뀜, 도급액 79,603,000 불변, 샘플 복구.
|
||||
- [x] 기성 원가계산서 편집(원본 TfrmWongaAccomH/V 팝업 A3~A8 — 디스어셈블 근거) — 회차마다 원가 서식 사본(`round.wonga`)에 한줄 삽입·추가(끝줄 표지 이어받기)·삭제(확인), 내용 삭제(연결·금액·요율·수식·자리 비움), 선택항목 절하(도급액 행 허용), 선택데이타 절하(직전 자리, 구성비 기준 행 제외), 공사비역산 자리수(금회 도급액 10^n 미만 절하 → 이윤 역산), 초기화(사본 지움 = 도급 원가 서식), 되살리기(직전 조작 전), 산출근거 약어로 표시(수식칸 잠금만). 기성 행도 Diget>0 이면 자리맞춤. 도급금액 열은 순서가 아닌 기호·이름으로 짝지음. 원본의 번호·참조 재작성은 하지 않음(새 행 기호는 직접 입력). 확정 회차는 거부. 서버 허용 목록에서 빠져 있던 할증·부가세 스크립트(404)도 넣음.
|
||||
|
||||
@@ -76,6 +76,8 @@
|
||||
|
||||
편집(E) › `조달청 표준공사코드 적용`은 노임·자재·기계·일위 등의 코드를 조달청 코드로 바꿉니다. 왼쪽 목록은 원본 설치 폴더 `System\MainSys` 의 `PPS_STUFFC.dat`(노임·자재·장비) 또는 `PPS_HEAD.dat`(일위·단산·표준)를 골라 불러옵니다. 코드를 바꾼 뒤 `적용`을 누르면 그 코드를 쓰는 내역·일위·단산 줄도 함께 바뀝니다. `코드일괄변경`은 고른 .estx 파일(예: 원본 JodalCodeDB.estx)에서 품명·규격이 같은 코드를 찾아 EST 코드만 바꿉니다.
|
||||
|
||||
자재단가·노임단가 화면의 `발췌근거` 단추로 가격 출처 열(출처1~6)의 이름과 쪽수를 바꾸거나 숨길 수 있습니다. 숨긴 출처는 최저·평균·최고 단가를 고를 때 빠집니다.
|
||||
|
||||
원본의 엑셀형 수량산출서(본문이 옛 Excel .xls)는 가져올 때 셀 값과 수식을 함께 읽어 수량산출 화면 아래 격자로 보입니다. 셀을 누르면 수식줄에서 값이나 `=수식`을 고치고 바로 다시 계산합니다(참조·범위·사칙·SUM·ROUND 등). Shift 로 범위를 골라 행·열 넣기·지우기, 잘라내기·복사·붙여넣기, 굵게·정렬·병합·대각선을 쓸 수 있고, 수식의 참조는 원본처럼 따라 옮겨집니다. `우측·위·좌측·아래 합`은 원본처럼 그 방향의 숫자 셀을 더하는 SUM 을 넣고, `수량으로 지정`은 셀을 수량 항목에 연결해 항목 수량이 그 셀 값(산출서 자리수)을 따르게 합니다. 원본 파일에는 셀과 항목의 대응이 보이지 않아, 가져올 때 저장 수량과 값이 같은 합계 셀을 「추정 연결」로 둡니다. 셀을 고친 엑셀형 산출서는 ESTX 저장을 거부합니다.
|
||||
|
||||
산출 줄 옆 `수량 지정`은 줄 글에서 드래그해 고른 숫자를 결과 표지로 감싸고 수량 항목을 새로 만듭니다. 품명·규격·단위 표지가 있으면 항목 이름에 옮깁니다. `지정 해제`는 커서가 놓인 결과 표지를 풀고, 다른 줄에서 쓰지 않는 항목을 지웁니다. 내역에 연결된 항목은 먼저 연결을 비워야 합니다. 이 변경이 있는 작업은 ESTX 수정 저장을 거부합니다.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<script defer src="native-quantity.cjs"></script>
|
||||
<script defer src="native-wonga.js"></script>
|
||||
<script defer src="native-jungsang.js"></script>
|
||||
<script defer src="native-sources.js"></script>
|
||||
<script defer src="native-engine.js"></script>
|
||||
<script defer src="native-row-match.js"></script>
|
||||
<script defer src="native-escalation.js"></script>
|
||||
@@ -268,6 +269,7 @@
|
||||
><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="uses-search-btn" data-action="uses-data-search">사용위치</button
|
||||
><button data-action="add-row">행 추가</button><button data-action="copy-row">복사</button
|
||||
><button data-action="paste-row">붙여넣기</button
|
||||
|
||||
@@ -625,7 +625,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstDefaults !== "undefined" && EstDefaults.handle(name, state, node)) ||
|
||||
(typeof EstExcelView !== "undefined" && EstExcelView.handle(name, state, node)) ||
|
||||
(typeof EstProgressWonga !== "undefined" && EstProgressWonga.handle(name, state)) ||
|
||||
(typeof EstCodeChangeView !== "undefined" && EstCodeChangeView.handle(name, state, node))
|
||||
(typeof EstCodeChangeView !== "undefined" && EstCodeChangeView.handle(name, state, node)) ||
|
||||
(typeof EstSources !== "undefined" && EstSources.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -33,10 +33,11 @@ const EstNative = (() => {
|
||||
"MulTitleDat",
|
||||
];
|
||||
const clone = (value) => structuredClone(value);
|
||||
function price(row, kind) {
|
||||
const candidates = Array.from({ length: 6 }, (_, i) => num(row[`Cost${i + 1}`])).filter(
|
||||
(value) => value !== 0,
|
||||
);
|
||||
function price(row, kind, hidden) {
|
||||
// 발췌근거에서 숨긴 출처(EstSources)는 최저·평균·최고에서 뺀다
|
||||
const candidates = Array.from({ length: 6 }, (_, i) =>
|
||||
hidden?.has(i + 1) ? 0 : num(row[`Cost${i + 1}`]),
|
||||
).filter((value) => value !== 0);
|
||||
let total;
|
||||
switch (num(row.ValueIndex)) {
|
||||
case 0:
|
||||
@@ -144,7 +145,12 @@ const EstNative = (() => {
|
||||
.concat(calculated.filter((row) => !existing.has(row.Code)));
|
||||
}
|
||||
for (const kind of [5, 6, 21, 101])
|
||||
for (const row of tables[sourceTables[kind]]) price(row, kind);
|
||||
for (const row of tables[sourceTables[kind]])
|
||||
price(
|
||||
row,
|
||||
kind,
|
||||
typeof EstSources !== "undefined" ? EstSources.hidden(options, sourceTables[kind]) : null,
|
||||
);
|
||||
const machine = EstMachine.calculate(tables.MachMainDat, tables.MachGijunDat, options);
|
||||
tables.MachMainDat = machine.rows;
|
||||
saveTitles("MachTitleDat", machine.titles);
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// 원본 「발췌근거」(barbtnColumn — 일반 TfrmHeaderChange 0x110b7ec · 자재 TfrmStuffcHeadChange 0xf61b84):
|
||||
// 단가 표의 가격 출처 열(Cost1~6) 머리글 이름·쪽수·보이기를 고친다. 숨긴 출처는 최저·평균·최고 단가 고르기(113e918)에서 빠진다.
|
||||
// 자재 표는 출처 이름을 정해진 8가지에서 고르고 같은 이름이 둘이면 거부한다. 값은 계산 환경 PriceSources[표] 에 둔다.
|
||||
const EstSources = (() => {
|
||||
const NAMES = [
|
||||
"가격정보",
|
||||
"물가자료",
|
||||
"유통물가",
|
||||
"물가정보",
|
||||
"거래가격",
|
||||
"견적가1",
|
||||
"견적가2",
|
||||
"견적가3",
|
||||
];
|
||||
const TABLES = ["StuffcDat", "LaborcDat"];
|
||||
|
||||
function get(options, table) {
|
||||
const saved = options?.PriceSources?.[table] || [];
|
||||
return Array.from({ length: 6 }, (_, i) => ({
|
||||
caption: saved[i]?.caption || `출처${i + 1}`,
|
||||
page: saved[i]?.page ?? "",
|
||||
visible: saved[i]?.visible !== false,
|
||||
}));
|
||||
}
|
||||
function hidden(options, table) {
|
||||
return new Set(
|
||||
get(options, table)
|
||||
.map((s, i) => (s.visible ? -1 : i + 1))
|
||||
.filter((i) => i > 0),
|
||||
);
|
||||
}
|
||||
function columns(options, table, cols) {
|
||||
const off = hidden(options, table);
|
||||
return off.size ? cols.filter((k) => !off.has(Number(/^Cost(\d)$/.exec(k)?.[1]))) : cols;
|
||||
}
|
||||
function label(options, table, key) {
|
||||
const m = /^Cost(\d)$/.exec(key);
|
||||
if (!m || !TABLES.includes(table)) return "";
|
||||
const s = get(options, table)[Number(m[1]) - 1];
|
||||
return s.page ? `${s.caption} (${s.page})` : s.caption;
|
||||
}
|
||||
function set(options, table, list) {
|
||||
if (table === "StuffcDat") {
|
||||
const seen = new Set();
|
||||
for (const s of list) {
|
||||
if (seen.has(s.caption)) throw Error("같은 발췌근거가 있습니다.");
|
||||
seen.add(s.caption);
|
||||
}
|
||||
}
|
||||
options.PriceSources = { ...(options.PriceSources || {}), [table]: list };
|
||||
}
|
||||
|
||||
function dialog(state) {
|
||||
const table = state.view;
|
||||
if (!TABLES.includes(table)) throw Error("자재단가·노임단가 화면에서 쓰세요.");
|
||||
const { esc } = EstApp,
|
||||
list = get(state.project.options, table),
|
||||
stuffc = table === "StuffcDat";
|
||||
EstApp.showDialog(
|
||||
"발췌근거 수정 및 보기",
|
||||
`<table class="sheet"><thead><tr><th>No</th><th>발췌근거</th>${stuffc ? "<th>페이지</th>" : ""}<th>보기</th></tr></thead><tbody>${list
|
||||
.map(
|
||||
(s, i) =>
|
||||
`<tr><td>${i + 1}</td><td>${
|
||||
stuffc
|
||||
? `<select name="src-caption" data-i="${i}">${[...new Set([s.caption, ...NAMES])].map((n) => `<option${n === s.caption ? " selected" : ""}>${esc(n)}</option>`).join("")}</select>`
|
||||
: `<input name="src-caption" data-i="${i}" value="${esc(s.caption)}">`
|
||||
}</td>${stuffc ? `<td><input name="src-page" data-i="${i}" value="${esc(s.page)}" size="6"></td>` : ""}<td><input type="checkbox" name="src-visible" data-i="${i}"${s.visible ? " checked" : ""}></td></tr>`,
|
||||
)
|
||||
.join("")}</tbody></table><button data-action="sources-apply">적 용</button>`,
|
||||
);
|
||||
}
|
||||
function apply(state) {
|
||||
const table = state.view,
|
||||
q = (name, i) => document.querySelector(`#dialog-content [name="${name}"][data-i="${i}"]`);
|
||||
const list = Array.from({ length: 6 }, (_, i) => ({
|
||||
caption: q("src-caption", i).value.trim() || `출처${i + 1}`,
|
||||
page: q("src-page", i)?.value.trim() ?? "",
|
||||
visible: q("src-visible", i).checked,
|
||||
}));
|
||||
set(structuredClone(state.project.options), table, list);
|
||||
EstApp.change((project) => set(project.options, table, list));
|
||||
EstApp.byId("dialog").close();
|
||||
}
|
||||
function handle(name, state) {
|
||||
if (name === "sources") dialog(state);
|
||||
else if (name === "sources-apply") apply(state);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { NAMES, get, hidden, columns, label, set, handle };
|
||||
})();
|
||||
@@ -76,7 +76,7 @@ const EstViews = (() => {
|
||||
}
|
||||
function grid(table, list, columns) {
|
||||
const { esc, number, state } = EstApp;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}"${key === "Name" && Number(row.LeftSpace) > 0 ? ` style="padding-left:${Number(row.LeftSpace) * 6}px"` : ""}>${cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
return `<table class="sheet" data-grid="${table}"><thead><tr><th class="row-number">선택</th>${columns.map((key) => `<th class="col-${key}">${esc((table === "Wonga" && typeof EstJungSang !== "undefined" && EstJungSang.label(state.project, key)) || (typeof EstSources !== "undefined" && EstSources.label(state.project.options, table, key)) || EstModel.labels[key] || key)}</th>`).join("")}</tr></thead><tbody>${list.map(({ row, index }) => `<tr data-index="${index}" class="${state.selection.has(index) ? "selected " : ""}${Number(row.Index) > 0 ? "heading " : ""}${Number(row.Level) > 0 ? "level " : ""}${row.Error ? "error-row" : ""}"><th class="row-number"><label><input type="checkbox" data-select="${index}" ${state.selection.has(index) ? "checked" : ""}>${index + 1}</label></th>${columns.map((key) => `<td class="col-${key}"${key === "Name" && Number(row.LeftSpace) > 0 ? ` style="padding-left:${Number(row.LeftSpace) * 6}px"` : ""}>${cell(table, index, key, row[key])}</td>`).join("")}</tr>`).join("")}</tbody></table>`;
|
||||
}
|
||||
function render(state) {
|
||||
const { project, view } = state,
|
||||
@@ -103,6 +103,8 @@ const EstViews = (() => {
|
||||
if (haljungBtn) haljungBtn.hidden = view !== "HeadMainDat";
|
||||
const vatBtn = byId("vat-calc-btn");
|
||||
if (vatBtn) vatBtn.hidden = view !== "StuffcDat";
|
||||
const sourcesBtn = byId("sources-btn");
|
||||
if (sourcesBtn) sourcesBtn.hidden = !["StuffcDat", "LaborcDat"].includes(view);
|
||||
const unitCalcBtn = byId("unit-calc-btn");
|
||||
if (unitCalcBtn) unitCalcBtn.hidden = view !== "HeadMainDat";
|
||||
byId("move-estimate-up").hidden = ![
|
||||
@@ -124,7 +126,9 @@ const EstViews = (() => {
|
||||
columns =
|
||||
view === "Wonga" && typeof EstJungSang !== "undefined"
|
||||
? EstJungSang.columns(project, EstModel.columns(view))
|
||||
: EstModel.columns(view);
|
||||
: typeof EstSources !== "undefined"
|
||||
? EstSources.columns(project.options, view, EstModel.columns(view))
|
||||
: EstModel.columns(view);
|
||||
byId("work-content").innerHTML = grid(view, list, columns);
|
||||
byId("paging").hidden = false;
|
||||
byId("page-info").textContent =
|
||||
|
||||
@@ -26,6 +26,7 @@ const scripts = [
|
||||
"native-quantity.cjs",
|
||||
"native-wonga.js",
|
||||
"native-jungsang.js",
|
||||
"native-sources.js",
|
||||
"native-engine.js",
|
||||
"native-row-match.js",
|
||||
"native-escalation.js",
|
||||
|
||||
Reference in New Issue
Block a user