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>
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
// 원본 품셈 보기(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) || " "}</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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user