Files
Aislo/tools/est-test/native-book.js
T
eomsangdonandClaude Opus 5.5 db9bc2dee4 feat(est-test): 품셈 보기와 품셈자료 붙여넣기
- 원본 BookData 7권 해독(DBISAM 암호 표 Blowfish-CBC, zlib RTF) 서버 book-store.cjs, /api/book 경로
- 목차 트리·본문 표·제목/본문/키워드 검색·본문 강조·복사하기, 단가산출·내역·일위·기계 붙여넣기

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 17:41:02 +09:00

188 lines
9.0 KiB
JavaScript
Raw 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.
// 원본 품셈 보기(barbtnBook → ProductBook.exe · 같은 화면 TfrmBookViewer)와 품셈자료 붙여넣기(popupPasteBook 10d6e28).
// 보기: 품셈 고르기 → 장 트리(펼치기·접기) → 본문(표는 칸 나눔), 검색(제목 · 제목+본문 · 키워드, 검색어 빈칸·'|' 지움),
// 본문 검색(현재 쪽 강조), 복사하기(고른 글 또는 쪽 전체 → 클립보드, 붙여넣기 켬). 자료는 서버 book-store.cjs 가 원본 BookData 를 읽음.
// 붙여넣기(원본은 품셈에서 복사한 뒤에만 켜짐): 단가산출은 첫 줄을 마지막 산출근거 칸 커서 자리에, 둘째 줄부터 그 줄 아래 새 줄로(다듬지 않음).
// 내역서·일위대가·기계경비는 마지막으로 누른 칸 하나에 마지막 줄(글자 칸은 Trim, 수치 칸은 수치만).
const EstBook = (() => {
let lastCell = null;
if (typeof document !== "undefined" && document.addEventListener)
document.addEventListener("focusin", (event) => {
const t = event.target.closest?.("[data-table][data-field][data-row]");
if (t) lastCell = t;
});
const api = (p) => EstApp.api(`/api/book${p}`);
const st = (state) => (state.book ||= { name: "", open: new Set(), hits: null, pkid: null });
function treeHtml(b, nodes) {
const { esc } = EstApp,
kids = new Map();
for (const n of nodes) {
const k = n.parent ?? "root";
if (!kids.has(k)) kids.set(k, []);
kids.get(k).push(n);
}
for (const list of kids.values())
list.sort((a, c) => (a.order == null) - (c.order == null) || (a.order ?? 0) - (c.order ?? 0));
const walk = (key, depth) =>
(kids.get(key) || [])
.map((n) => {
const has = kids.has(n.pkid),
open = b.open.has(n.pkid);
return `<div style="padding-left:${depth * 14}px">${has ? `<button class="tiny" data-action="book-fold" data-pkid="${n.pkid}">${open ? "−" : "+"}</button>` : "<span style='display:inline-block;width:18px'></span>"}<a href="#" data-book-node="${n.pkid}"${n.pkid === b.pkid ? ' style="font-weight:bold"' : ""}>${esc(n.chapter)} ${esc(n.title)}</a></div>${has && open ? walk(n.pkid, depth + 1) : ""}`;
})
.join("");
return walk("root", 0);
}
function pageHtml(page, mark) {
const { esc } = EstApp;
const hl = (t) =>
mark
? esc(t)
.split(esc(mark))
.join(`<mark>${esc(mark)}</mark>`)
: esc(t);
const lines = String(page.text || "").split("\n");
let html = "",
table = [];
const flush = () => {
if (table.length)
html += `<table class="sheet" style="font-size:12px">${table.map((cells) => `<tr>${cells.map((c) => `<td style="text-align:left">${hl(c)}</td>`).join("")}</tr>`).join("")}</table>`;
table = [];
};
for (const line of lines) {
if (line.includes("\t")) table.push(line.replace(/\t$/, "").split("\t"));
else {
flush();
html += `<div style="white-space:pre-wrap">${hl(line) || "&nbsp;"}</div>`;
}
}
flush();
return `<h4>${esc(page.chapter)} ${esc(page.title)}</h4>${html}`;
}
async function render(state) {
const b = st(state),
{ esc } = EstApp;
b.books ||= await api("");
b.name ||= b.books[0]?.name;
b.tree = b.treeName === b.name ? b.tree : await api(`/${b.name}/tree`);
b.treeName = b.name;
const left = b.hits
? `<p>검색 결과 ${b.hits.length}건 <button data-action="book-tree">목차로</button></p>${b.hits.map((h) => `<div><a href="#" data-book-node="${h.pkid}">${esc(h.chapter)} ${esc(h.title)}</a></div>`).join("")}`
: treeHtml(b, b.tree);
EstApp.showDialog(
"품셈",
`<div class="business-tools"><select data-book-name>${b.books.map((x) => `<option value="${x.name}"${x.name === b.name ? " selected" : ""}>${esc(x.desc)}</option>`).join("")}</select><input data-book-word value="${esc(b.word || "")}" placeholder="검색어"><button data-action="book-search" data-mode="title">검 색</button><button data-action="book-search" data-mode="body">전체검색(제목+본문)</button><button data-action="book-search" data-mode="keyword">키워드검색</button><button data-action="book-expand">펼치기</button><button data-action="book-collapse">접기</button><input data-book-mark value="${esc(b.mark || "")}" placeholder="본문 검색" size="10"><button data-action="book-mark">본문 검색</button><button data-action="book-copy">복사하기</button></div><div style="display:flex;gap:10px;height:62vh"><div style="flex:0 0 38%;overflow:auto;border:1px solid #ccc;padding:4px;font-size:12px">${left}</div><div id="book-page" style="flex:1;overflow:auto;border:1px solid #ccc;padding:6px">${b.page ? pageHtml(b.page, b.mark) : "<p>왼쪽에서 항목을 고르세요.</p>"}</div></div>`,
);
}
async function run(name, state, node) {
const b = st(state),
v = (sel) => document.querySelector(`#dialog-content ${sel}`)?.value;
if (name === "book") return render(state);
if (name === "book-fold") {
const id = Number(node.dataset.pkid);
if (b.open.has(id)) b.open.delete(id);
else b.open.add(id);
} else if (name === "book-expand") b.open = new Set(b.tree.map((n) => n.pkid));
else if (name === "book-collapse") b.open = new Set();
else if (name === "book-tree") b.hits = null;
else if (name === "book-search") {
b.word = v("[data-book-word]") || "";
b.hits = await api(
`/${b.name}/search?q=${encodeURIComponent(b.word)}&mode=${node.dataset.mode}`,
);
} else if (name === "book-mark") b.mark = (v("[data-book-mark]") || "").trim();
else if (name === "book-copy") {
const sel = String(getSelection()).trim(),
text = sel || b.page?.text || "";
if (!text) throw Error("복사할 품셈 내용을 고르세요.");
state.bookClip = text;
navigator.clipboard?.writeText(text).catch(() => {});
state.notice = "품셈 내용을 복사했습니다. 품셈자료 붙여넣기로 넣을 수 있습니다.";
} else if (name === "book-paste") return paste(state);
return render(state);
}
function paste(state) {
if (!state.bookClip) throw Error("품셈에서 먼저 복사하기를 하세요.");
const lines = state.bookClip.split(/\r?\n/);
if (!lastCell) throw Error("붙여넣을 칸을 먼저 누르세요.");
const table = lastCell.dataset.table,
row = Number(lastCell.dataset.row),
field = lastCell.dataset.field;
if (table === "CostMainDat") {
if (field !== "LineData") throw Error("단가산출 산출근거 칸을 누른 뒤 붙여넣으세요.");
const caret = lastCell.selectionStart ?? lastCell.value.length,
shown = lastCell.value,
first = shown.slice(0, caret) + lines[0] + shown.slice(lastCell.selectionEnd ?? caret);
EstApp.change((project) => {
const rows = project.tables.CostMainDat;
rows[row].LineData = EstText.encode(first);
let pk = Math.max(0, ...rows.map((r) => Number(r.PkID) || 0));
rows.splice(
row + 1,
0,
...lines.slice(1).map((LineData) => ({ PkID: ++pk, Index: 0, LineData })),
);
});
} else if (["EstMainDat", "HeadMainDat", "MachMainDat"].includes(table)) {
const line = lines.at(-1).trim();
if (["Code", "SCheck"].includes(field)) throw Error("이 칸에는 붙여넣을 수 없습니다.");
let value = line;
if (EstModel.numeric.has(field)) {
if (!/^[+-]?[\d,.]+$/.test(line)) throw Error("현재 칼럼에는 수치만 붙여넣을수 있습니다.");
value = Number(line) || 0;
}
EstApp.change((project) => (project.tables[table][row][field] = value));
} else throw Error("내역서·일위대가·단가산출·기계경비 칸에 붙여넣으세요.");
return true;
}
function install() {
document.addEventListener("click", (event) => {
const a = event.target.closest("[data-book-node]");
if (!a) return;
event.preventDefault();
const state = EstApp.state,
b = st(state);
b.pkid = Number(a.dataset.bookNode);
api(`/${b.name}/text/${b.pkid}`)
.then((page) => {
b.page = page;
return render(state);
})
.catch((error) => (state.notice = error.message));
});
document.addEventListener("change", (event) => {
if (event.target.dataset.bookName === undefined) return;
const b = st(EstApp.state);
Object.assign(b, { name: event.target.value, open: new Set(), hits: null, page: null });
render(EstApp.state).catch((error) => (EstApp.state.notice = error.message));
});
}
if (typeof document !== "undefined" && document.addEventListener) install();
const names = new Set([
"book",
"book-fold",
"book-expand",
"book-collapse",
"book-tree",
"book-search",
"book-mark",
"book-copy",
"book-paste",
]);
function handle(name, state, node) {
if (!names.has(name)) return false;
run(name, state, node).catch((error) => {
state.notice = error.message;
EstApp.render();
});
return true;
}
return { handle };
})();