Files
Aislo/tools/est-test/native-est-tools.js
eomsangdonandClaude Opus 5.5 ceab1ab8c5 feat(est-test): 하단으로 집계·공종 다듬기·들여쓰기·레벨 확인
- 원본 처리 근거대로 소계 줄 넣기·지우기, 내역 엔진에 소계 행 합 추가
- 공종 들여쓰기, 단가산출 들여쓰기, 단가산출 레벨 나무 보기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 21:03:37 +09:00

177 lines
7.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 원본 내역·단가산출 다듬기 도구.
// 하단으로 집계(popMainDownSum → f01a90·f018d0): 공종 머리(TU 0 또는 57) 앞에서 열린 공종(레벨 1..L)마다, 순공사비(54)·경계(Index 59)
// 앞과 마지막 행 뒤에서 열린 공종 전부에 「소 계」(LinkIndex 53, Level −10) 행을 넣는다. 바로 위 구간에 이미 소계가 있으면 넣지 않는다.
// 금액은 재계산이 채운다(EstRows.estimate). 상단으로 집계(mnuSumUp f01750)는 53 행을 모두 지운다.
// 이미 소계가 있는 자리는 닫힌 것으로 보아 다시 눌러도 겹치지 않게 한다(원본은 열린 표시가 남아 끝에 덧붙을 수 있음).
// 공종 다듬기(mnuEstLeftSpace 0x113eb44): LeftSpace = 상위 공종 들여쓰기 + 간격, 54·55·58 행은 0.
// 단가산출 들여쓰기(ActionLeftMargin 0xf0770c): 제목·레벨 행 말고 LineData 앞 빈칸을 n 칸으로 바꾸고, 분수(─) 세 줄은 같은 만큼 옮김.
// 레벨 확인(TfrmCostLevelCheck 0xd10840): 현재 호표의 레벨 줄([레벨])과 비목 소계 줄을 계층으로 보인다(읽기 전용).
const EstEstTools = (() => {
const num = (value) => Number(value) || 0;
function hasSubtotalAbove(rows, at) {
for (let i = at - 1; i >= 0; i--) {
const r = rows[i];
if (num(r.LinkIndex) === 53) return true;
if (num(r.Level) > 0 || [54, 55, 58, 59].includes(num(r.LinkIndex))) return false;
}
return false;
}
function downSum(project) {
const rows = project.tables.EstMainDat || [],
out = [],
open = new Set();
const subtotal = () => ({ Index: 0, Name: "소 계", LinkIndex: 53, Level: -10 });
let added = 0;
// 바로 위에 소계가 이미 있으면 넣지 않고 닫은 것으로 본다(다시 눌러도 겹치지 않게).
const close = (max, at) => {
const exists = hasSubtotalAbove(rows, at);
for (let level = 1; level <= max; level++)
if (open.has(level)) {
open.delete(level);
if (exists) continue;
out.push(subtotal());
added++;
}
};
rows.forEach((row, i) => {
const level = num(row.Level),
link = num(row.LinkIndex);
if (level > 0 && num(row.Index) !== 53) {
if (!num(row.TU) || link === 57) close(level, i);
open.add(level);
} else if (link === 54 || num(row.Index) === 59) close(9, i);
out.push(row);
if (i === rows.length - 1) close(9, i + 1);
});
let pk = Math.max(0, ...rows.map((r) => num(r.PkID)));
for (const row of out) if (row.PkID == null) row.PkID = ++pk;
project.tables.EstMainDat = out;
return added;
}
function upSum(project) {
const rows = project.tables.EstMainDat || [];
project.tables.EstMainDat = rows.filter((r) => num(r.LinkIndex) !== 53);
return rows.length - project.tables.EstMainDat.length;
}
function leftSpace(project, step) {
if (!Number.isInteger(step) || step < 0 || step > 10)
throw Error("들여쓰기 간격은 0~10 입니다.");
const open = [];
for (const row of project.tables.EstMainDat || []) {
if ([54, 55, 58].includes(num(row.LinkIndex))) {
row.LeftSpace = 0;
continue;
}
const level = num(row.Level) > 0 ? num(row.Level) : 0;
while (open.length && open.at(-1).level <= level) open.pop();
const indent = open.length ? open.at(-1).indent + step : 0;
row.LeftSpace = indent;
if (level > 0) open.push({ level, indent });
}
project.options.EstLeftSpace = step;
}
function costIndent(project, n) {
if (!Number.isInteger(n) || n < 0 || n > 50) throw Error("들여쓰기 칸 수는 0~50 입니다.");
const rows = project.tables.CostMainDat || [];
const lead = (s) => String(s ?? "").length - String(s ?? "").trimStart().length;
const shift = (s, delta) => {
const text = String(s ?? "");
return delta >= 0 ? " ".repeat(delta) + text : text.slice(Math.min(-delta, lead(text)));
};
const done = new Set();
rows.forEach((row, i) => {
if (done.has(i) || num(row.Index) > 0 || num(row.Level) > 0 || !row.LineData) return;
if (String(row.LineData).includes("─")) {
const delta = n - lead(row.LineData);
for (const j of [i - 1, i, i + 1])
if (rows[j] && !(num(rows[j].Index) > 0) && !done.has(j)) {
rows[j].LineData = shift(rows[j].LineData, delta);
done.add(j);
}
return;
}
const next = rows[i + 1];
if (next && String(next.LineData ?? "").includes("─")) return;
row.LineData = " ".repeat(n) + String(row.LineData).trimStart();
done.add(i);
});
}
function levelTree(project, code) {
const rows = project.tables.CostMainDat || [],
group = EstNative.groups(rows).get(code);
if (!group) throw Error("레벨을 볼 단가산출 호표를 고르세요.");
const head = group.rows[0],
label = {
[-5]: " 계 : ",
[-3]: "경 비 : ",
[-2]: "재료비 : ",
[-1]: "노무비 : ",
[-4]: "",
};
const lines = [];
for (const row of group.rows.slice(1)) {
const level = num(row.Level),
plain = EstText.clean(row.LineData || "").trim();
if (level > 0) {
const s = plain.padEnd(104, " ");
lines.push({ level, text: (s.slice(0, Math.max(0, 60 - level)) + `[${level}]`).trimEnd() });
} else if (num(row.SCheck) <= -1 && num(row.SCheck) >= -5) {
const value = String(row.LineData || "").match(/\x12([-\d.,]+)\x12/)?.[1] ?? "";
lines.push({ level: 0, text: `${label[num(row.SCheck)]}${value}` });
}
}
return { title: `${head.Name ?? ""} (${head.Size ?? ""})`, lines };
}
function handle(name, state) {
if (name === "est-down-sum" || name === "est-up-sum") {
if (state.view !== "EstMainDat") throw Error("내역서 화면에서 집계하세요.");
const down = name === "est-down-sum";
if (
!confirm(
down ? "공사비 집계를 하단으로 하시겠습니까?" : "공사비 집계를 상단으로 하시겠습니까?",
)
)
return true;
let count;
EstApp.change((project) => (count = down ? downSum(project) : upSum(project)));
state.notice = down ? `소계 ${count}줄을 넣었습니다.` : `소계 ${count}줄을 지웠습니다.`;
} else if (name === "est-left-space") {
EstActions.form(
"공종 다듬기",
[
{
name: "step",
label: "레벨당 들여쓰기 간격(0~10)",
type: "number",
value: state.project.options.EstLeftSpace ?? 2,
},
],
({ step }) => EstApp.change((project) => leftSpace(project, step), false),
);
} else if (name === "cost-indent") {
if (state.view !== "CostMainDat") throw Error("단가산출 화면에서 들여쓰기하세요.");
EstActions.form(
"들여쓰기",
[{ name: "n", label: "들여쓰기 칸", type: "number", value: 4 }],
({ n }) => EstApp.change((project) => costIndent(project, n)),
);
} else if (name === "cost-level-check") {
if (state.view !== "CostMainDat") throw Error("단가산출 화면에서 레벨을 확인하세요.");
const tree = levelTree(state.project, state.group),
{ esc } = EstApp;
EstApp.showDialog(
"단가산출 레벨(계층) 보기",
`<p><b>${esc(tree.title)}</b></p><pre style="max-height:420px;overflow:auto">${tree.lines.map((l) => esc(l.text)).join("\n") || "레벨 지정 줄이 없습니다."}</pre>`,
);
} else return false;
return true;
}
return { downSum, upSum, leftSpace, costIndent, levelTree, handle };
})();