feat(est-test): 데이타 자동연결
- 원본 처리 근거대로 품명·규격·단위 정규화 맞춤으로 현재 공사 단가 표에 연결 - 9개 선택·Over Write·노임 맞바꿈·전체연결 끊기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
"""EST 테스트 앱 데이타 자동연결(원본 TfrmDataAutoLink, 맞춤 10e0e58·행 처리 10d2a48)."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def run(code: str):
|
||||
script = f"""
|
||||
const fs = require('fs'), vm = require('vm');
|
||||
const ctx = vm.createContext({{}});
|
||||
vm.runInContext(fs.readFileSync('tools/est-test/native-autolink.js', 'utf8'), ctx);
|
||||
process.stdout.write(JSON.stringify(vm.runInContext({json.dumps(code)}, ctx)));
|
||||
"""
|
||||
return json.loads(subprocess.check_output(["node", "-e", script], cwd=ROOT).decode("utf-8"))
|
||||
|
||||
|
||||
def test_matching_rules_order_and_overwrite():
|
||||
result = run(
|
||||
"""(() => {
|
||||
const base = () => ({ tables: {
|
||||
LaborcDat: [{ Code: 'L1', Name: '보통 인부', TC: 170000, NC: 170000 }],
|
||||
StuffcDat: [{ Code: 'M1', Name: '레미콘', Size: '25-24-(150)', Unite: 'm3', TC: 90000, GC: 90000 },
|
||||
{ Code: 'M2', Name: '보통인부', TC: 1, GC: 1 }],
|
||||
HeadTitleDat: [{ Code: 'U1', Name: '거푸집', Size: '합판', Unite: '㎡당', TC: 30000, NC: 20000, GC: 10000, SCheck: 3 }],
|
||||
EstMainDat: [
|
||||
{ Name: '공종', Level: 2 },
|
||||
{ Name: '보통인부', Size: '일반', Unite: '인' },
|
||||
{ Name: '레미콘', Size: '25-24-150', Unite: '[m3]' },
|
||||
{ Name: '레미콘', Size: '25-24-150', Unite: 'ton' },
|
||||
{ Name: '거 푸 집', Size: '(합판)', Unite: '㎡' },
|
||||
{ Name: '일반', Size: '보통인부' },
|
||||
{ Name: '순공사비', LinkIndex: 54 },
|
||||
{ Name: '이미', LinkIndex: 5, Code: 'OLD' } ] } });
|
||||
const p = base();
|
||||
const n = EstAutoLink.run(p, 'EstMainDat', 'all', false);
|
||||
const r = p.tables.EstMainDat;
|
||||
const q = base(); q.tables.EstMainDat[7].Name = '레미콘'; q.tables.EstMainDat[7].Size = '25-24-150'; q.tables.EstMainDat[7].Unite = 'm3';
|
||||
EstAutoLink.run(q, 'EstMainDat', '5', true);
|
||||
const u = base(); const cut = EstAutoLink.run(u, 'EstMainDat', 'unlink', false);
|
||||
return { n, rows: r.map((x) => [x.Code ?? null, x.LinkIndex ?? null]), unit: [r[2].GU, r[2].TU],
|
||||
swap: [r[5].Name, r[5].Size], head: [r[4].NU, r[4].GU, r[4].SCheck], over: q.tables.EstMainDat[7].Code,
|
||||
cut, allZero: u.tables.EstMainDat.every((x) => x.LinkIndex === 0) };
|
||||
})()"""
|
||||
)
|
||||
assert result["n"] == 4
|
||||
assert result["rows"] == [
|
||||
[None, None],
|
||||
["L1", 6],
|
||||
["M1", 5],
|
||||
[None, None],
|
||||
["U1", 103],
|
||||
["L1", 6],
|
||||
[None, 54],
|
||||
["OLD", 5],
|
||||
]
|
||||
assert result["unit"] == [90000, 90000]
|
||||
assert result["swap"] == ["보통인부", "일반"]
|
||||
assert result["head"] == [20000, 10000, 3]
|
||||
assert result["over"] == "M1"
|
||||
assert result["cut"] == 8 and result["allZero"] is True
|
||||
@@ -492,6 +492,9 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- [x] 계약심사 자동입력(원본 TfrmInvest 심사평가결과·주요심사내용 0x10bfc98 — 디스어셈블 근거) — 총공사비 행 Cost_/Cost, 절감율 Trunc(×10000)×0.01, cp949 16바이트 맞춤·증액 「+」, 7개 표 NewItem 「○○ 조정 : 이름(규격) 외N 종」(N 원본 셈). 심사 사본에 설계 원가 Cost_ 보존.
|
||||
- 검증완료: `test_est_review_auto.py`(네 줄 글·증액 부호·총공사비 없음·주요심사 두 줄) 통과, 계약심사 관련 시험 44개 통과. ORCA 브라우저에서 수량 0.9배 심사 → 사본 → 자동입력 「설계 97,199,324 · 심사 96,952,167 · 절감 −247,157 · −0.25 %」, 샘플 복구.
|
||||
- 남음: 위치·사업개요·사업기간·기타사항 칸(Invest.Dat 10개 RTF).
|
||||
- [x] 데이타 자동연결(원본 ActionAutoLink 0x10d54f4·TfrmDataAutoLink 0x10d3008·10d2a48·맞춤 10e0e58 — 디스어셈블 근거) — 9개 선택·Over Write, 정규화·맞춤 단계·노임 맞바꿈·기계 주연료·건너뛰기 규칙·쓰는 값 원본대로, 전체 순서 노임→자재→견적→표준→기계(내역·일위만)→단가산출→일위대가.
|
||||
- 검증완료: `test_est_autolink.py`(노임 품명만·규격 재시도 맞바꿈·단위 정규화·일위 비목 단가·SCheck 3·Over Write·이미 연결 건너뜀·전체 끊기) 통과. ORCA 브라우저에서 샘플 전체자동(Over Write) → 7행 연결·순공사비 45,293,066 그대로, 되돌리기 원상.
|
||||
- 남음: 전역변수(`` 표지 — 설치 자료 사용 0건, 단가산출 엔진 깊은 변경이 필요해 보류).
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
3. 수량산출서 — 누계·4방향 합계, 제출용 전체 인쇄.
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
||||
|
||||
`편집 → 데이타 자동연결`은 원본처럼 내역서·일위대가·기계경비 행을 현재 공사의 노임·자재·견적·표준·기계경비·단가산출·일위대가 표와 품명(·규격·단위)으로 맞춰 코드·단가·연결종류를 적습니다. 빈칸·|·괄호를 무시하고 단위의 「당」·[ ]를 지우며, 노임은 품명만 맞추고 못 찾으면 규격으로 찾아 품명·규격을 맞바꿉니다. `Over Write`를 끄면 이미 연결된 행은 건너뜁니다. `전체연결 끊기`는 원본처럼 모든 행의 연결종류를 0으로 합니다.
|
||||
|
||||
`편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다.
|
||||
|
||||
계약심사 화면의 `심사평가결과 자동입력`은 원본처럼 원가계산서의 총공사비 행에서 설계(심사 전)·심사 금액, 절감액, 절감율(소수 둘째 자리 버림)을 적고, `주요심사내용 자동입력`은 노임·자재·표준시장·견적·기계경비·단가산출·일위대가 중 새로 바뀐 자료를 「○○ 조정 : 이름(규격) 외N 종」으로 적습니다. 심사 내역 사본은 설계 원가 금액을 함께 남깁니다.
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
<script defer src="native-estimate-print.js"></script>
|
||||
<script defer src="native-xml-check.js"></script>
|
||||
<script defer src="native-review-auto.js"></script>
|
||||
<script defer src="native-autolink.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -123,6 +124,7 @@
|
||||
<button data-action="register-stuff">선택 행 자재단가 등록</button
|
||||
><button data-action="register-estimate">선택 행 견적단가 등록</button
|
||||
><button data-action="name-divide">품명정리(일위대가 호표)</button>
|
||||
<button data-action="autolink">데이타 자동연결</button>
|
||||
<button data-action="pick-open">
|
||||
단가 고르기(자재·노임·기계·일위·단가산출·견적·실적)
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
// 원본 데이타 자동연결(ActionAutoLink Ctrl+Alt+L 0x10d54f4 → TfrmDataAutoLink 적용 0x10d3008, 행 처리 10d2a48, 맞춤 10e0e58).
|
||||
// 내역서·일위대가·기계경비 화면에서 현재 공사의 노임·자재·견적·표준·기계경비·단가산출·일위대가 표로 품명(·규격·단위)을 맞춰 연결.
|
||||
// 정규화: 빈칸·|·(·) 제거, 단위는 「당」·[·] 를 한 번씩 더 지움. 노임은 품명만(못 찾으면 규격으로 찾고 품명·규격을 맞바꿈),
|
||||
// 규격·단위 비면 품명+규격, 단위 있으면 품명+규격+단위. 노임·기계는 단위 무시. 처음 맞는 행. Over Write 끄면 이미 연결된 행은 건너뜀.
|
||||
// 쓰는 값: Code, TU(=원본 TC), 내역·일위대가는 NU·GU·JU·SU(=원본 NC·GC·JC·SC), LinkIndex, 원본 SCheck 3 이면 SCheck 3.
|
||||
// 「전체연결 끊기」는 원본처럼 모든 행 LinkIndex 를 0 으로 한다(조건 없음).
|
||||
const EstAutoLink = (() => {
|
||||
const num = (value) => Number(value) || 0;
|
||||
const sources = {
|
||||
6: "LaborcDat",
|
||||
5: "StuffcDat",
|
||||
101: "EstiMateDat",
|
||||
21: "SilJukDat",
|
||||
105: "MachTitleDat",
|
||||
104: "CostTitleDat",
|
||||
103: "HeadTitleDat",
|
||||
};
|
||||
const order = [6, 5, 101, 21, 105, 104, 103];
|
||||
const tags = { EstMainDat: 1, HeadMainDat: 2, MachMainDat: 4 };
|
||||
const norm = (s) => String(s ?? "").replace(/[\s|()]/g, "");
|
||||
const unit = (s) => norm(s).replace("당", "").replace("[", "").replace("]", "");
|
||||
|
||||
function key(row, tag) {
|
||||
let name = row.Name ?? "",
|
||||
size = row.Size ?? "";
|
||||
const unite = row.Unite ?? "";
|
||||
if (tag === 4) {
|
||||
name = String(name).replace(/[\s|]/g, "");
|
||||
size = String(size).replace(/[\s|]/g, "");
|
||||
if ([5, 212].includes(num(row.LinkIndex)) && name.includes("주연료")) {
|
||||
name = size;
|
||||
size = "";
|
||||
}
|
||||
}
|
||||
return { name: String(name).trim(), size: String(size).trim(), unite: String(unite).trim() };
|
||||
}
|
||||
|
||||
function find(project, kind, k) {
|
||||
const rows = project.tables[sources[kind]] || [],
|
||||
useUnit = ![6, 105].includes(kind) && k.unite;
|
||||
const mode = !k.size && !useUnit ? (kind === 6 ? "name" : "size") : useUnit ? "unit" : "size";
|
||||
return rows.find((src) => {
|
||||
if (!src.Code) return false;
|
||||
if (kind !== 101 && src.LinkIndex != null && num(src.LinkIndex) !== kind) return false;
|
||||
if (src.DataKind != null && num(src.DataKind) !== 1) return false;
|
||||
if (norm(src.Name) !== norm(k.name)) return false;
|
||||
if (mode === "name" || kind === 6) return true;
|
||||
if (norm(src.Size) !== norm(k.size)) return false;
|
||||
if (mode === "size" || kind === 105) return true;
|
||||
return unit(src.Unite) === unit(k.unite);
|
||||
});
|
||||
}
|
||||
|
||||
function linkKind(project, view, kind, overwrite) {
|
||||
const tag = tags[view],
|
||||
rows = project.tables[view] || [];
|
||||
let linked = 0;
|
||||
for (const row of rows) {
|
||||
if ((tag === 2 || tag === 4) && num(row.Index) > 0) continue;
|
||||
if (tag === 1 && (num(row.Level) > 0 || [54, 55, 58].includes(num(row.LinkIndex)))) continue;
|
||||
const k = key(row, tag);
|
||||
if (!k.name && !k.size && !k.unite) continue;
|
||||
if (!overwrite && num(row.LinkIndex)) continue;
|
||||
let src = find(project, kind, kind === 6 ? { name: k.name, size: "", unite: "" } : k);
|
||||
if (!src && kind === 6 && k.size) {
|
||||
src = find(project, kind, { name: k.size, size: "", unite: "" });
|
||||
if (src) [row.Name, row.Size] = [row.Size, row.Name];
|
||||
}
|
||||
if (!src) continue;
|
||||
row.Code = src.Code;
|
||||
row.TU = num(src.TC);
|
||||
if (tag === 1 || tag === 2) {
|
||||
row.NU = num(src.NC);
|
||||
row.GU = num(src.GC);
|
||||
row.JU = num(src.JC);
|
||||
row.SU = num(src.SC);
|
||||
}
|
||||
row.LinkIndex = kind;
|
||||
if (num(src.SCheck) === 3) row.SCheck = 3;
|
||||
linked++;
|
||||
}
|
||||
return linked;
|
||||
}
|
||||
|
||||
function run(project, view, choice, overwrite) {
|
||||
if (!tags[view]) throw Error("내역서·일위대가·기계경비 화면에서 자동연결하세요.");
|
||||
if (choice === "unlink") {
|
||||
for (const row of project.tables[view] || []) row.LinkIndex = 0;
|
||||
return (project.tables[view] || []).length;
|
||||
}
|
||||
const kinds =
|
||||
choice === "all"
|
||||
? order.filter((k) => k !== 105 || [1, 2].includes(tags[view]))
|
||||
: [Number(choice)];
|
||||
return kinds.reduce((n, kind) => n + linkKind(project, view, kind, overwrite), 0);
|
||||
}
|
||||
|
||||
const choices = [
|
||||
["unlink", "전체연결 끊기"],
|
||||
["all", "전체자동 연결"],
|
||||
["6", "노임단가 연결"],
|
||||
["5", "자재단가 연결"],
|
||||
["101", "견적단가 연결"],
|
||||
["21", "표준단가 연결"],
|
||||
["105", "기계경비 연결"],
|
||||
["104", "단가산출 연결"],
|
||||
["103", "일위대가 연결"],
|
||||
];
|
||||
function open(state) {
|
||||
if (!tags[state.view]) throw Error("내역서·일위대가·기계경비 화면에서 자동연결하세요.");
|
||||
EstApp.showDialog(
|
||||
"데이타 자동연결",
|
||||
`<div class="form-fields"><label>연결데이타 선택<select name="al-choice">${choices.map(([v, l]) => `<option value="${v}" ${v === "all" ? "selected" : ""}>${l}</option>`).join("")}</select></label><label><input type="checkbox" name="al-over">Over Write</label></div><button data-action="autolink-apply">적 용</button><p id="autolink-result" role="status"></p>`,
|
||||
);
|
||||
}
|
||||
function apply(state) {
|
||||
const choice = document.querySelector('#dialog-content [name="al-choice"]').value,
|
||||
overwrite = document.querySelector('#dialog-content [name="al-over"]').checked;
|
||||
const ask =
|
||||
choice === "unlink"
|
||||
? "데이타를 연결을 끊을까요?"
|
||||
: choice === "all"
|
||||
? "전체 데이타를 연결 시킬까요?"
|
||||
: null;
|
||||
if (ask && !confirm(ask)) return;
|
||||
let count;
|
||||
EstApp.change((project) => (count = run(project, state.view, choice, overwrite)));
|
||||
EstApp.byId("autolink-result").textContent =
|
||||
choice === "unlink" ? `${count}행의 연결을 끊었습니다.` : `${count}행을 연결했습니다.`;
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "autolink") open(state);
|
||||
else if (name === "autolink-apply") apply(state);
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { run, find, key, handle };
|
||||
})();
|
||||
@@ -616,7 +616,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstWongaTools !== "undefined" && EstWongaTools.handle(name, state)) ||
|
||||
(typeof EstEstimatePrint !== "undefined" && EstEstimatePrint.handle(name, state)) ||
|
||||
(typeof EstXmlCheck !== "undefined" && EstXmlCheck.handle(name, state)) ||
|
||||
(typeof EstReviewAuto !== "undefined" && EstReviewAuto.handle(name, state))
|
||||
(typeof EstReviewAuto !== "undefined" && EstReviewAuto.handle(name, state)) ||
|
||||
(typeof EstAutoLink !== "undefined" && EstAutoLink.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -78,6 +78,7 @@ const allowed = new Set([
|
||||
"native-estimate-print.js",
|
||||
"native-xml-check.js",
|
||||
"native-review-auto.js",
|
||||
"native-autolink.js",
|
||||
"native-issues.js",
|
||||
"native-attachments.js",
|
||||
"native-standard.js",
|
||||
|
||||
Reference in New Issue
Block a user