- 원본 처리 근거대로 소계 줄 넣기·지우기, 내역 엔진에 소계 행 합 추가 - 공종 들여쓰기, 단가산출 들여쓰기, 단가산출 레벨 나무 보기 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
7.6 KiB
JavaScript
177 lines
7.6 KiB
JavaScript
// 원본 내역·단가산출 다듬기 도구.
|
||
// 하단으로 집계(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 };
|
||
})();
|