feat(est-test): 품명정리와 자재·견적단가 등록
- 원본 처리 근거대로 커서 자리 품명 나누기, 삭제할 칸, 빈 단위만 채움 - 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 연결, 중복·호표 거부 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -462,6 +462,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 남음: 당초분·정산분 색상 옵션.
|
||||
- [x] 무관자료 삭제(원본 mnuNotLinkDelete·DelSort → f039f0, MulDelete → 1116db0 — 디스어셈블 근거) — 내역서에서 LinkIndex 5·6·21·101·103·104·105·Code(설계변경 Code_ 포함)로 하위까지 따라가 쓰는 자료만 남김, 묶음 머리 앞 빈 줄 정리, 내역서순 재배열(코드 유지), 내역 미연결 수량산출서 삭제. 원본의 한 번 훑기 누락(기계경비 안에서만 쓰인 일위대가 하위)은 웹에서 끝까지 따라가 막음. LinkIndex 211–218 코드 비우기는 뜻 미확인으로 하지 않음.
|
||||
- 검증완료: `test_est_cleanup.py` 3개(안 쓰는 자재·일위 삭제·합계 불변·오류 0, 내역서순 순서·코드 유지, 빈 줄·수량산출서) 통과. 설치 main·sample·산림토목 ESTX에서 정리 전후 도급액·오류 수 같음. ORCA 브라우저에서 내역서순 정리 → 자재 30 → 17·도급액 79,603,000 유지·되살리기 복원.
|
||||
- [x] 품명정리·자재/견적단가 등록(원본 ActionNameDeivde 0xf5a2b4·TfrmNameDevide 0xf595c0/0xf59640, popMainStuffcReg/EstimateReg → c_f00264/c_f00670 — 디스어셈블 근거) — 커서 자리 나누기·삭제할 칸·빈 단위만 채움·호표 목록 동기화, 등록 시 같은 품명·규격 거부·호표 거부·M/W+PkID 10자리 코드·자재 Cost1=TC=재료단가·견적 NC/GC/JC·원래 행 연결. 원본은 품명정리를 2개 이상 선택 때만 돌리나 웹은 1개도 허용.
|
||||
- 검증완료: `test_est_name_reg.py` 2개(나누기·삭제할 칸·제목 동기화·호표 아님 거부, 자재/견적 등록 코드·가격·연결·금액 불변·중복/호표 거부·계산 오류 0) 통과. ORCA 브라우저에서 내역 행 자재 등록 → M0000000038·연결 5, 일위대가 「레미콘타설(…)」 3자리 나누기 → 품명 「레미콘」·규격 「타설(인력운반타설)철근구조물」, 샘플 복구.
|
||||
- 대조 합계(CAD 제외 882개): 구현 283 · 부분 193 · 없음 273 · 제외 133.
|
||||
- [ ] 남은 핵심(실무 빈도순):
|
||||
1. 통합검색 — 저장된 다른 작업에서 단가·호표 찾기.
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
내역서·일위대가 화면의 `편집 → 단가 고르기`는 자재·노임·기계경비·일위대가·단가산출·견적·실적 자료를 검색어로 찾아 선택 행 아래에 넣거나, 선택 행 하나의 연결을 바꿉니다. 여러 개를 골라 한 번에 넣을 수 있고, 바꿀 때는 변경 전·후를 확인한 뒤 같은 코드를 쓰는 이 표의 다른 행도 함께 바꿀지 고릅니다. 넣거나 바꾼 행만 원천 단가를 다시 읽습니다. 단가산출 산출근거 행은 대상이 아닙니다.
|
||||
|
||||
`편집 → 선택 행 자재단가 등록`·`견적단가 등록`은 원본처럼 내역·일위대가 행을 새 자재(M)·견적(W) 단가로 등록하고 그 행을 새 코드에 연결합니다. 같은 품명·규격이 있으면 거부하고, 일위대가 호표 제목 줄은 등록하지 않습니다. `품명정리`는 고른 일위대가 호표의 품명을 커서 자리에서 나눠 뒤쪽을 규격 앞에 붙이고, 단위가 비어 있으면 채웁니다.
|
||||
|
||||
`편집 → 무관자료 삭제`는 원본처럼 내역서가 쓰는 자재·노임·실적·견적과 일위대가·단가산출·기계경비 호표를 하위까지 따라가 표시하고 나머지를 지웁니다. 내역서·원가계산서·기계경비 적용기준은 지우지 않고, 묶음 머리 앞의 빈 줄을 정리합니다. `(내역서순)`은 표를 내역서 첫 등장 순서로 다시 늘어놓으며 코드는 바꾸지 않습니다. `무관 수량산출서 삭제`는 내역에 연결되지 않은 수량산출서를 지웁니다. 모두 되살리기로 되돌릴 수 있습니다.
|
||||
|
||||
`원가계산서 → 정산원가계산서 작성/해제`는 원본처럼 원가 표에 당초금액 열을 금액 앞에 보이고 금액 열을 정산금액으로 바꿉니다. 처음 켤 때(또는 `당초금액 초기화`) 원가 행마다 현재 금액을 당초금액으로 옮기고 도급액 고정 연결(55)을 풀며, 맨 아래 금액 행의 차이를 `(감)`·`(증)` 금액 행으로 적습니다. 켜져 있는 동안 재계산마다 차액 행을 다시 씁니다. 끄면 차액 행만 비우고 당초금액은 남깁니다.
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<script defer src="native-bid.js"></script>
|
||||
<script defer src="native-forms-library.js"></script>
|
||||
<script defer src="native-cleanup.js"></script>
|
||||
<script defer src="native-name-reg.js"></script>
|
||||
<script defer src="native-actions.js"></script>
|
||||
<script defer src="native-business-actions.js"></script>
|
||||
<script defer src="native-app.js"></script>
|
||||
@@ -103,6 +104,9 @@
|
||||
><button data-action="block-del">블럭 삭제</button>
|
||||
<hr />
|
||||
<button data-action="find">찾기·다음찾기(Ctrl+F)</button>
|
||||
<button data-action="register-stuff">선택 행 자재단가 등록</button
|
||||
><button data-action="register-estimate">선택 행 견적단가 등록</button
|
||||
><button data-action="name-divide">품명정리(일위대가 호표)</button>
|
||||
<button data-action="pick-open">
|
||||
단가 고르기(자재·노임·기계·일위·단가산출·견적·실적)
|
||||
</button>
|
||||
|
||||
@@ -608,7 +608,8 @@ const EstBusinessActions = (() => {
|
||||
(typeof EstBid !== "undefined" && EstBid.handle(name, state)) ||
|
||||
(typeof EstFormsLibrary !== "undefined" && EstFormsLibrary.handle(name, state, node)) ||
|
||||
(typeof EstJungSang !== "undefined" && EstJungSang.handle(name, state)) ||
|
||||
(typeof EstCleanup !== "undefined" && EstCleanup.handle(name, state))
|
||||
(typeof EstCleanup !== "undefined" && EstCleanup.handle(name, state)) ||
|
||||
(typeof EstNameReg !== "undefined" && EstNameReg.handle(name, state))
|
||||
) {
|
||||
EstApp.render();
|
||||
} else if (name === "load-contract" || name === "load-bid") {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
// 원본 품명정리(TfrmHead ActionNameDeivde 0xf5a2b4·TfrmNameDevide 적용 0xf595c0·규격으로지정 0xf59640)와
|
||||
// 자재단가·견적단가 등록(popMainStuffcReg·popMainEstimateReg → c_f00264·c_f00670).
|
||||
// 품명정리: 기존명칭에서 고른 자리 앞 = 품명(다듬지 않음), 뒤에서 「삭제할 칸」만큼 떼고 기존 규격을 붙여 다듬은 값 = 규격,
|
||||
// 단위는 비어 있을 때만 채움. 고른 일위대가 호표마다 같은 자리로 나누고 HeadTitleDat 의 같은 코드도 맞춘다.
|
||||
// 등록: 같은 품명·규격(빈칸·| 무시) 있으면 거부, 코드 = 머리글자(M·W) + PkID 10자리(쓰인 코드면 하나씩 올림),
|
||||
// 자재 Cost1 = TC = 행 재료단가 · 견적 NC/GC/JC = 행 노무/재료/경비단가, 원래 행은 새 코드·연결(5·101)로 바꾼다.
|
||||
const EstNameReg = (() => {
|
||||
const num = (value) => Number(value) || 0;
|
||||
const strip = (value) => String(value ?? "").replace(/[\s|]/g, "");
|
||||
|
||||
function divide(project, indices, position, cut, unit) {
|
||||
if (!Number.isInteger(position) || position < 0) throw Error("나눌 자리를 고르세요.");
|
||||
if (!Number.isInteger(cut) || cut < 0) throw Error("삭제할 칸은 0 이상의 정수입니다.");
|
||||
const rows = project.tables.HeadMainDat,
|
||||
titles = project.tables.HeadTitleDat || [];
|
||||
const picked = indices.filter((i) => num(rows[i]?.Index) > 0);
|
||||
if (!picked.length) throw Error("품명을 정리할 일위대가 호표(제목 줄)를 고르세요.");
|
||||
for (const i of picked) {
|
||||
const row = rows[i],
|
||||
name = String(row.Name ?? "");
|
||||
const rest = name.slice(position).slice(cut);
|
||||
row.Name = name.slice(0, position);
|
||||
row.Size = `${rest}${row.Size ?? ""}`.trim();
|
||||
if (!strip(row.Unite) && unit) row.Unite = unit;
|
||||
const title = titles.find((t) => t.Code === row.Code);
|
||||
if (title) Object.assign(title, { Name: row.Name, Size: row.Size, Unite: row.Unite });
|
||||
}
|
||||
return picked.length;
|
||||
}
|
||||
|
||||
const targets = {
|
||||
stuff: { table: "StuffcDat", kind: 5, letter: "M", label: "자재단가" },
|
||||
estimate: { table: "EstiMateDat", kind: 101, letter: "W", label: "견적단가" },
|
||||
};
|
||||
|
||||
function register(project, view, index, which) {
|
||||
const target = targets[which],
|
||||
row = project.tables[view]?.[index];
|
||||
if (!target || !row) throw Error("등록할 행을 고르세요.");
|
||||
if (view === "HeadMainDat" && num(row.Index) > 0)
|
||||
throw Error(`호표는 ${target.label}에 등록할수 없습니다.`);
|
||||
if (!String(row.Name ?? "").trim()) throw Error("명칭을 입력하세요.");
|
||||
const rows = (project.tables[target.table] ||= []);
|
||||
const same = rows.find(
|
||||
(r) => strip(r.Name) === strip(row.Name) && strip(r.Size) === strip(row.Size),
|
||||
);
|
||||
if (same)
|
||||
throw Error(`같은 명칭의 데이타가 있습니다 . ${target.label} (${num(same.Index)} 번)`);
|
||||
let pk = Math.max(0, ...rows.map((r) => num(r.PkID))) + 1;
|
||||
const used = new Set(rows.map((r) => r.Code));
|
||||
let code = `${target.letter}${String(pk).padStart(10, "0").slice(-10)}`;
|
||||
while (used.has(code)) code = `${target.letter}${String(++pk).padStart(10, "0").slice(-10)}`;
|
||||
const schema = rows[0] ? Object.keys(rows[0]) : ["Name", "Size", "Unite"];
|
||||
const record = {};
|
||||
for (const key of schema)
|
||||
if (!["PkID", "Index", "Code"].includes(key) && Object.hasOwn(row, key))
|
||||
record[key] = row[key];
|
||||
Object.assign(record, { PkID: pk, Code: code, Index: rows.length + 1 });
|
||||
if (which === "stuff")
|
||||
Object.assign(record, { Cost1: num(row.GU), TC: num(row.GU), OTC: num(row.GU) });
|
||||
else {
|
||||
const [nc, gc, jc] = [num(row.NU), num(row.GU), num(row.JU)];
|
||||
Object.assign(record, {
|
||||
NC: nc,
|
||||
GC: gc,
|
||||
JC: jc,
|
||||
TC: nc + gc + jc,
|
||||
ONC: nc,
|
||||
OGC: gc,
|
||||
OJC: jc,
|
||||
OTC: nc + gc + jc,
|
||||
});
|
||||
}
|
||||
rows.push(record);
|
||||
row.Code = code;
|
||||
row.LinkIndex = target.kind;
|
||||
return code;
|
||||
}
|
||||
|
||||
function divideDialog(state) {
|
||||
if (state.view !== "HeadMainDat") throw Error("일위대가 화면에서 품명을 정리하세요.");
|
||||
const rows = state.project.tables.HeadMainDat,
|
||||
picked = [...state.selection].sort((a, b) => a - b).filter((i) => num(rows[i]?.Index) > 0);
|
||||
if (!picked.length) throw Error("품명을 정리할 일위대가 호표(제목 줄)를 고르세요.");
|
||||
const { esc } = EstApp;
|
||||
EstApp.showDialog(
|
||||
"품명정리",
|
||||
`<p>기존명칭에서 품명과 규격을 나눌 자리에 커서를 두고 「규격으로 지정」을 누르세요. 고른 ${picked.length}개 호표에 같은 자리로 적용합니다.</p><div class="form-fields"><label>기존명칭<input name="nd-all" value="${esc(rows[picked[0]].Name ?? "")}"></label><label>명칭<input name="nd-name" readonly></label><label>규격<input name="nd-size" readonly></label><label>규격에서 삭제할 칸<input name="nd-cut" type="number" value="0"></label><label>단위(비어 있을 때만)<input name="nd-unit"></label></div><button data-action="name-divide-set">규격으로 지정</button> <button data-action="name-divide-apply">적용</button>`,
|
||||
);
|
||||
state.nameDivide = { picked, position: null };
|
||||
}
|
||||
function field(name) {
|
||||
return document.querySelector(`#dialog-content [name="${name}"]`);
|
||||
}
|
||||
|
||||
function handle(name, state) {
|
||||
if (name === "name-divide") divideDialog(state);
|
||||
else if (name === "name-divide-set") {
|
||||
const all = field("nd-all"),
|
||||
position = all.selectionStart ?? 0;
|
||||
state.nameDivide.position = position;
|
||||
field("nd-name").value = all.value.slice(0, position);
|
||||
field("nd-size").value = all.value.slice(position);
|
||||
} else if (name === "name-divide-apply") {
|
||||
const info = state.nameDivide;
|
||||
if (info?.position == null) throw Error("먼저 「규격으로 지정」으로 나눌 자리를 정하세요.");
|
||||
const cut = Number(field("nd-cut").value),
|
||||
unit = field("nd-unit").value.trim();
|
||||
EstApp.byId("dialog").close();
|
||||
let count;
|
||||
EstApp.change((project) => (count = divide(project, info.picked, info.position, cut, unit)));
|
||||
state.notice = `${count}개 호표의 품명을 정리했습니다.`;
|
||||
} else if (name === "register-stuff" || name === "register-estimate") {
|
||||
const which = name === "register-stuff" ? "stuff" : "estimate",
|
||||
index = [...state.selection][0],
|
||||
row = state.project.tables[state.view]?.[index];
|
||||
if (!["EstMainDat", "HeadMainDat"].includes(state.view) || state.selection.size !== 1 || !row)
|
||||
throw Error("내역서·일위대가에서 등록할 행 하나를 고르세요.");
|
||||
const label = targets[which].label;
|
||||
register(structuredClone(state.project), state.view, index, which);
|
||||
if (!confirm(`"${row.Name ?? ""}" ${row.Size ?? ""} 를 ${label}에 등록하시겠습니까?`))
|
||||
return true;
|
||||
let code;
|
||||
EstApp.change((project) => (code = register(project, state.view, index, which)));
|
||||
state.notice = `${label}에 ${code}로 등록하고 이 행을 연결했습니다.`;
|
||||
} else return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return { divide, register, handle };
|
||||
})();
|
||||
@@ -68,6 +68,7 @@ const allowed = new Set([
|
||||
"native-bid.js",
|
||||
"native-forms-library.js",
|
||||
"native-cleanup.js",
|
||||
"native-name-reg.js",
|
||||
"native-issues.js",
|
||||
"native-attachments.js",
|
||||
"native-standard.js",
|
||||
|
||||
Reference in New Issue
Block a user