feat(est-test): 연차 데이타 복사 옵션(덮어쓰기·열 삽입, 복사내용 고르기)

- 고른 내역 행을 차수 하나의 놓을 행부터 복사
- 수량·단가금액·명칭규격 고르기, 설계변경 상단/하단, 행 금액 다시 셈
- 내역서 도구줄과 연차 화면에 단추

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-12 19:16:19 +09:00
co-authored by Claude Opus 5.5
parent f6d73458bb
commit 188b50fa2f
8 changed files with 169 additions and 1 deletions
+1
View File
@@ -36,6 +36,7 @@ for (const file of [
"native-extra.js",
"native-applied-cost.js",
"native-progress-view.js",
"native-annual-copy.js",
"native-business-actions.js",
"native-forms.js",
"native-excel-to-est.js",
+46
View File
@@ -0,0 +1,46 @@
"""연차 데이타 복사 옵션: 덮어쓰기·열 삽입·복사내용 게이트·상단(당초) 값·끝 행 넘침."""
from test_est_rea_parity import run
def test_overwrite_copies_chosen_groups_and_recalculates_amounts():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.EstMainDat=[{Code:'A',Name:'a',Size:'s',GN:2,NU:10,GU:5},{Code:'B',Name:'b',GN:3,NU:1,GU:1},
{Code:'C',Name:'c',GN:1,NU:7,GU:0,GN_:4,Name_:'c0'}];
p.annual=[{name:'1차',formula:'[기준]'},{name:'2차',formula:'[기준]',rows:[]}];
const n=EstAnnualCopy.apply(p,{sources:[0,2],target:1,year:0,insert:false,name:false,gn:true,cost:false});
const r=p.annual[0].rows;
const q=structuredClone(p);
const m=EstAnnualCopy.apply(q,{sources:[2],target:2,year:1,insert:false,name:true,gn:true,cost:true,top:true});
const over=EstAnnualCopy.apply(structuredClone(p),{sources:[0,1,2],target:2,year:0,name:true,gn:true,cost:true});
return {n,len:r.length,first:r[0],row1:[r[1].Name,r[1].GN,r[1].NC,r[1].GC,r[1].TC],row2:[r[2].Name,r[2].GN,r[2].NC],
top:[q.annual[1].rows[2].Name,q.annual[1].rows[2].GN,q.annual[1].rows[2].NC],m,over,
parse:EstAnnualCopy.parse('1-3, 5')};
})()""",
)
assert result["n"] == 2
assert result["first"] is None
assert result["row1"] == ["b", 2, 2, 2, 4]
assert result["row2"] == ["c", 1, 7]
assert result["top"] == ["c0", 4, 28]
assert result["m"] == 1
assert result["over"] == 1
assert result["parse"] == [0, 1, 2, 4]
def test_insert_mode_shifts_main_and_every_loaded_year():
result = run(
"math",
"""(()=>{
const p=EstNative.empty();
p.tables.EstMainDat=[{Name:'a',GN:1,NU:1},{Name:'b',GN:1,NU:1}];
p.annual=[{name:'1차',formula:'[기준]'},{name:'2차',formula:'[기준]',rows:[{Name:'x'},{Name:'y'}]}];
EstAnnualCopy.apply(p,{sources:[1],target:1,year:0,insert:true,name:true,gn:true,cost:true});
return {main:p.tables.EstMainDat.map(r=>r.Name??null),y1:p.annual[0].rows.map(r=>r?.Name??null),
y2:p.annual[1].rows.map(r=>r?.Name??null)};
})()""",
)
assert result == {"main": ["a", None, "b"], "y1": [None, "b"], "y2": ["x", None, "y"]}
+2
View File
@@ -499,6 +499,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
- 검증완료: 시험 1개 통과(당초 수량 4·변경 10). ORCA 브라우저에서 고르기 창·회차 loadMethod 확인, 원복.
- [x] 근거자료보기 고치기·붙여넣기(TfrmGunguEdit popPaste · spec_pc §8 · spec_sort_memo §3) — 근거(메모)를 문단·굵게·표·그림(자리표)으로 펼쳐 고치고, 붙여넣기(서식 있는 글·표)·저장. 저장은 원본 WPTools 글 형식(굴림체 보통·굵게, 표 기본 테두리, 그림 원문 그대로, zlib). 문단 간격·들여쓰기·글꼴 크기는 버림. 원래 메모가 있던 행은 ESTX 저장 가능, 새 메모는 JSON 저장만(새 BLOB 할당 미지원).
- 검증완료: 메모 시험 2개·정리 시험 2개 통과. 설치 샘플 메모 201건 글자 201/201 일치·그림 71/71 보존·되쓰기 188/201 동일(나머지는 칸 안 빈 줄만 빠짐), 고친 메모 ESTX 저장 → 다시 읽기 일치. ORCA 브라우저에서 굵게·표 저장 후 다시 열기 확인, 원복.
- [x] 연차 데이타 복사 옵션(TfrmMultiDataCopyMethod · spec_pb2 §1) — 원본의 끌어 놓기 대신 단추: 고른 내역 행(번호 범위)을 차수 하나의 놓을 행부터. 덮어쓰기 / 열 삽입후 내용복사(빈 행 넣고 모든 차수 한 줄 밀림), 복사내용 수량·단가금액·명칭규격(마지막 선택 기억), 설계변경이면 상단(당초 `_`)·하단(변경). 행 금액은 수량×단가로 다시 셈, 끝 행 넘침은 버림. 원본의 집계형 격자(Tag 18 Total*) 끌기는 웹에 그 격자가 없어 뺌.
- 검증완료: 시험 2개·연차 시험 통과. ORCA 브라우저에서 내역 3·4행 → 1차 10행 복사, 차수 계산 오류 없음 확인, 원복.
- [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 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
+2
View File
@@ -24,6 +24,7 @@
<script defer src="native-other-load.js"></script>
<script defer src="native-applied-cost.js"></script>
<script defer src="native-progress-view.js"></script>
<script defer src="native-annual-copy.js"></script>
<script defer src="native-actions-more.js"></script>
<script defer src="native-wonga.js"></script>
<script defer src="native-jungsang.js"></script>
@@ -170,6 +171,7 @@
><button data-action="data-update">현재데이타 갱신</button
><button data-action="jodal-fee">조달수수료 입력(Ctrl+J)</button
><button data-action="cost-to-wonga">원가계산서에 내역 금액 넣기</button
><button data-action="annual-copy">연차 차수로 행 복사(고른 내역 행)</button
><button data-action="wonga-biul-mode">원가 구성비·요율 표시 전환</button
><button data-action="jun-load">전회기성 불러오기</button
><button data-action="excel-to-est">엑셀 데이타 =&gt; EST 데이타(7개 표)</button>
+1
View File
@@ -9,6 +9,7 @@ const EstActionsMore = (() => {
typeof EstOtherLoad !== "undefined" && EstOtherLoad,
typeof EstAppliedCost !== "undefined" && EstAppliedCost,
typeof EstProgressView !== "undefined" && EstProgressView,
typeof EstAnnualCopy !== "undefined" && EstAnnualCopy,
].filter(Boolean);
function handle(name, state, node) {
for (const module of modules()) if (module.handle(name, state, node)) return true;
+113
View File
@@ -0,0 +1,113 @@
// 원본 연차공사 「데이타 복사 옵션」(TfrmMultiDataCopyMethod btnApply 0xedaba4 → TDataCopyMulti eedc54 — spec_pb2 §1):
// 내역서 행을 골라 차수 하나의 놓을 행부터 차례로 옮긴다. 복사방법 = 덮어쓰기 · 열 삽입후 내용복사(놓을 자리에 빈 행을 넣어
// 모든 차수가 한 줄 밀림). 복사내용 = 명칭·규격(Name·Size·Unite) · 수량(GN) · 단가·금액(TU·TC·NU·NC·GU·GC·JU·JC·SU·SC).
// 설계변경 내역이면 상단(당초 = 「_」 짝 칸) / 하단(변경 = 지금 값). 고른 차수 줄이 없으면 지금 내역 줄을 바탕으로 하고,
// 행마다 금액을 수량×단가로 다시 셈(f1a078). 끝 행을 넘는 나머지는 버림. 코드·연결 등 다른 칸은 그대로.
const EstAnnualCopy = (() => {
const num = (v) => Number(v) || 0;
const NAME = ["Name", "Size", "Unite"],
COST = ["TU", "TC", "NU", "NC", "GU", "GC", "JU", "JC", "SU", "SC"];
const KEY = "est-annual-copy";
function apply(project, { sources, target, year, insert, name, gn, cost, top }) {
const main = project.tables.EstMainDat,
chosen = project.annual?.[year];
if (!chosen) throw Error("입력할 차수를 선택하세요.");
if (!(target >= 0 && target <= main.length)) throw Error("놓을 행이 없습니다.");
const picked = sources.map((i) => structuredClone(main[i])).filter(Boolean);
if (insert) {
main.splice(target, 0, {});
for (const y of project.annual) if (Array.isArray(y.rows)) y.rows.splice(target, 0, null);
}
chosen.rows ||= [];
let count = 0;
picked.forEach((src, k) => {
const t = target + k;
if (t >= main.length) return;
const read = (f) => (top && src[f + "_"] != null ? src[f + "_"] : src[f]);
const row = chosen.rows[t] ? { ...chosen.rows[t] } : structuredClone(main[t]);
if (name) for (const f of NAME) row[f] = read(f) ?? "";
if (gn) row.GN = num(read("GN"));
if (cost) for (const f of COST) row[f] = num(read(f));
// 행 금액 계산: 비목 금액 = 수량 × 비목 단가, 합계 = 비목 합
for (const p of ["N", "G", "J", "S"])
row[p + "C"] = Math.trunc(num(row.GN) * num(row[p + "U"]));
row.TC = ["NC", "GC", "JC", "SC"].reduce((s, f) => s + num(row[f]), 0);
row.TU = ["NU", "GU", "JU", "SU"].reduce((s, f) => s + num(row[f]), 0);
while (chosen.rows.length < t) chosen.rows.push(null);
chosen.rows[t] = row;
count++;
});
return count;
}
// 번호 목록 "1-3,5" → 0부터 차례
function parse(text) {
const out = [];
for (const part of String(text)
.split(/[,\s]+/)
.filter(Boolean)) {
const [a, b] = part.split("-").map(Number);
if (!Number.isInteger(a)) throw Error(`행 번호 오류: ${part}`);
for (let i = a; i <= (Number.isInteger(b) ? b : a); i++) out.push(i - 1);
}
return out;
}
function dialog(state) {
const project = state.project,
{ esc } = EstApp;
if (!project.annual?.length) throw Error("차수를 먼저 추가하세요.");
const saved = JSON.parse(localStorage.getItem(KEY) || "{}"),
o = { insert: false, gn: true, cost: true, name: true, ...saved };
const picked = state.view === "EstMainDat" ? [...state.selection].sort((a, b) => a - b) : [];
const range = picked.length ? picked.map((i) => i + 1).join(",") : "1";
const change = project.options.Mode === "설계변경";
const check = (k, label) =>
`<label><input type="checkbox" data-ac="${k}" ${o[k] ? "checked" : ""}> ${label}</label>`;
EstApp.showDialog(
"데이타 복사 옵션",
`<div class="form-fields"><label>복사할 내역 행(예 1-3,5) <input data-ac="rows" value="${esc(range)}"></label><label>놓을 행 번호 <input type="number" min="1" data-ac="target" value="${(picked[0] ?? 0) + 1}"></label>` +
`<label>복사방법 <select data-ac="insert"><option value="0">덮어쓰기</option><option value="1" ${o.insert ? "selected" : ""}>열 삽입후 내용복사</option></select></label>` +
`<fieldset><legend>복사내용</legend>${check("gn", "수량")}${check("cost", "단가, 금액")}${check("name", "명칭, 규격")}</fieldset>` +
(change
? `<label>설계변경 <select data-ac="top"><option value="1">상단데이타 복사</option><option value="0">하단데이타 복사</option></select></label>`
: "") +
`<fieldset><legend>복사위치</legend>${project.annual.map((y, i) => `<label><input type="radio" name="ac-year" value="${i}"> ${esc(y.name)}</label>`).join("")}</fieldset></div>` +
`<button data-action="annual-copy-apply">적 용</button>`,
);
}
function run(state) {
const box = document.getElementById("dialog-content"),
get = (k) => box.querySelector(`[data-ac="${k}"]`);
const year = box.querySelector('[name="ac-year"]:checked');
if (!year) throw Error("입력할 차수를 선택하세요.");
const opts = {
insert: get("insert").value === "1",
gn: get("gn").checked,
cost: get("cost").checked,
name: get("name").checked,
};
localStorage.setItem(KEY, JSON.stringify(opts));
let count = 0;
EstApp.change(
(project) =>
(count = apply(project, {
...opts,
sources: parse(get("rows").value),
target: Number(get("target").value) - 1,
year: Number(year.value),
top: get("top")?.value === "1",
})),
);
EstApp.byId("dialog").close();
state.notice = `${count}행을 복사했습니다.`;
}
function handle(name, state) {
if (name === "annual-copy") dialog(state);
else if (name === "annual-copy-apply") run(state);
else return false;
return true;
}
return { apply, parse, handle };
})();
+3 -1
View File
@@ -134,7 +134,9 @@ const EstExtra = (() => {
state.business = output;
html = box(
"연차공사",
button("add-annual", "차수 추가") + button("reset-annual", "연차 차수 초기화(웹보조)"),
button("add-annual", "차수 추가") +
button("reset-annual", "연차 차수 초기화(웹보조)") +
button("annual-copy", "데이타 복사(내역 행 → 차수)"),
table(
["차수", "수량·금액 배분 수식", "순공사비", "도급액", "작업", ""],
project.annual.map((year, index) => [
+1
View File
@@ -107,6 +107,7 @@ const allowed = new Set([
"native-other-load.js",
"native-applied-cost.js",
"native-progress-view.js",
"native-annual-copy.js",
"native-actions-more.js",
"native-wonga-tools.js",
"native-estimate-print.js",