- 원본 처리 근거대로 기초단가 가나다 정렬, 위치정리, 호표 번호정렬 - 원본 서식 근거(메모) 글자 보기와 삭제 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
164 lines
7.2 KiB
JavaScript
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 };
|
|
})();
|