Files
Aislo/tools/est-test/native-tidy.js
eomsangdonandClaude Opus 5.5 b06b737437 feat(est-test): 정렬·위치정리·호표 정리와 근거 메모
- 원본 처리 근거대로 기초단가 가나다 정렬, 위치정리, 호표 번호정렬
- 원본 서식 근거(메모) 글자 보기와 삭제

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:35 +09:00

164 lines
7.2 KiB
JavaScript

// 원본 표 정리 — 정렬(BarSort 0x110ADEC·TfrmSiljukSort), 데이타 위치정리(mnuTableSort 0x10CCAEC·TFRMTABLESORT),
// 호표 정리(popTitleSort 0x10D88EC), 근거(메모) 보기·삭제(mnuGunguEdit·mnuGunguDel, 필드 DifineMemo).
// 정렬: 기초단가 표를 관급구분(Gangub) 0·1·2 차례로 묶고 각 묶음을 품명+규격(실적은 코드 선택 가능) 순으로, Index 1..n.
// Index ≤ 0 이거나 품명·규격·수량·금액이 모두 빈 행은 버린다. 원본의 빈 행 30줄 채우기는 웹에서 하지 않는다.
// 위치정리: 기초단가·일위대가·단가산출·기계경비를 품명 오름/내림으로 다시 놓고 시작번호부터 Index — 묶음은 통째로 옮긴다.
// 원본 콤보 글자는 오름/내림이 뒤바뀌어 보이나(분석 추정) 웹은 실제 순서대로 적는다.
// 호표 정리: 현재 차례 그대로 묶음 머리 Index 를 시작번호부터 다시 매기고 제목표 같은 코드도 맞춘다(행·코드 불변).
const EstTidy = (() => {
const num = (value) => Number(value) || 0;
const base = ["StuffcDat", "LaborcDat", "SilJukDat", "EstiMateDat"];
const grouped = {
HeadMainDat: "HeadTitleDat",
CostMainDat: "CostTitleDat",
MachMainDat: "MachTitleDat",
};
const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
const key = (row) => `${row.Name ?? ""}${row.Size ?? ""}`;
function sortBase(project, table, byCode = false) {
if (!base.includes(table)) throw Error("자재·노임·실적·견적 단가 화면에서 정렬하세요.");
const rows = project.tables[table] || [];
const kept = rows.filter(
(row) => num(row.Index) > 0 && (key(row).trim() || num(row.GN) || num(row.TC)),
);
const out = [];
for (const gangub of [0, 1, 2])
out.push(
...kept
.filter((row) => num(row.Gangub) === gangub)
.sort((a, b) =>
byCode ? compare(String(a.Code ?? ""), String(b.Code ?? "")) : compare(key(a), key(b)),
),
);
out.forEach((row, i) => (row.Index = i + 1));
project.tables[table] = out;
return rows.length - out.length;
}
function arrange(project, table, descending, start) {
if (!Number.isInteger(start)) throw Error("시작번호는 정수입니다.");
const order = (a, b) => compare(String(a ?? ""), String(b ?? "")) * (descending ? -1 : 1);
if (base.includes(table)) {
const rows = (project.tables[table] || []).filter((row) => row.Code);
rows.sort((a, b) => order(a.Name, b.Name)).forEach((row, i) => (row.Index = start + i));
project.tables[table] = rows;
return rows.length;
}
if (!grouped[table]) throw Error("이 화면은 위치정리를 할 수 없습니다.");
const groups = [...EstNative.groups(project.tables[table] || []).values()].sort((a, b) =>
order(a.rows[0].Name, b.rows[0].Name),
);
project.tables[table] = groups.flatMap((g) => g.rows);
numberHeads(project, table, start);
return groups.length;
}
function numberHeads(project, table, start) {
if (!grouped[table]) throw Error("일위대가·단가산출·기계경비 화면에서 호표를 정리하세요.");
if (!Number.isInteger(start)) throw Error("시작번호는 정수입니다.");
const titles = new Map((project.tables[grouped[table]] || []).map((t) => [t.Code, t]));
let n = start;
for (const row of project.tables[table] || [])
if (num(row.Index) > 0) {
row.Index = n;
const title = titles.get(row.Code);
if (title) title.Index = n;
n++;
}
return n - start;
}
// 근거(메모): 원본 DifineMemo 는 WPTools 서식(zlib) — 웹은 글자만 꺼내 읽기 전용으로 보인다.
async function memoText(memo) {
if (memo == null) return "";
if (typeof memo === "string") return memo;
if (!memo.binary) return "";
let bytes = Uint8Array.from(atob(memo.binary), (c) => c.charCodeAt(0));
if (bytes[0] === 0x78)
bytes = new Uint8Array(
await new Response(
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("deflate")),
).arrayBuffer(),
);
const source = new TextDecoder("euc-kr").decode(bytes);
return EstQuantity.entities(
source
.replace(/<(numberstyles|stylesheet)[\s\S]*?<\/\1>/gi, "")
.replace(/<\/(div|p)>|<br\s*\/?>/gi, "\n")
.replace(/<[^>]*>/g, ""),
)
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function memoTarget(state) {
const index = [...state.selection][0],
row = state.project.tables[state.view]?.[index];
if (state.selection.size !== 1 || !row) throw Error("근거(메모)를 볼 행 하나를 고르세요.");
return { index, row };
}
async function run(name, state) {
if (name === "tidy-sort") {
const table = state.view;
let byCode = false;
if (table === "SilJukDat") byCode = !confirm("품명,규격으로 정렬할까요? [취소] 코드로 정렬");
if (!confirm("가,나,다 순으로 정렬합니다. 빈 행과 번호 없는 행은 빠집니다.")) return;
let dropped;
EstApp.change((project) => (dropped = sortBase(project, table, byCode)));
state.notice = `정렬했습니다${dropped ? ` (빈 행 ${dropped}개 제외)` : ""}.`;
} else if (name === "tidy-arrange" || name === "tidy-number") {
EstActions.form(
name === "tidy-arrange" ? "데이타 위치정리" : "호표 정리(번호정렬)",
[
...(name === "tidy-arrange"
? [
{
name: "order",
label: "품명 정렬",
value: "asc",
choices: [
["asc", "오름차순(가나다)"],
["desc", "내림차순"],
],
},
]
: []),
{ name: "start", label: "시작번호", type: "number", value: 1 },
],
(values) =>
EstApp.change((project) =>
name === "tidy-arrange"
? arrange(project, state.view, values.order === "desc", values.start)
: numberHeads(project, state.view, values.start),
),
);
} else if (name === "memo-view") {
const { row } = memoTarget(state),
body = await memoText(row.DifineMemo);
EstApp.showDialog(
`근거(메모) — ${row.Name ?? row.Code ?? ""}`,
body
? `<pre style="white-space:pre-wrap;max-height:400px;overflow:auto">${EstApp.esc(body)}</pre>${typeof row.DifineMemo === "string" ? "" : "<p>원본 서식 메모라 웹에서는 읽기만 합니다.</p>"}`
: "<p>이 행에는 근거(메모)가 없습니다.</p>",
);
} else if (name === "memo-delete") {
const { index } = memoTarget(state);
EstApp.change((project) => (project.tables[state.view][index].DifineMemo = null), false);
state.notice = "근거(메모)를 지웠습니다.";
}
}
const names = new Set(["tidy-sort", "tidy-arrange", "tidy-number", "memo-view", "memo-delete"]);
function handle(name, state) {
if (!names.has(name)) return false;
run(name, state).catch((error) => {
state.notice = error.message;
EstApp.render();
});
return true;
}
return { sortBase, arrange, numberHeads, memoText, handle };
})();