)·그림 개체(
${runs(n.p)} \r\n`,
+ )
+ .join("");
+ return (
+ "\r\n" +
+ '"; + const list = (items) => + items + .map((n) => + n.table + ? `
${runs(n.p)} `,
+ )
+ .join("");
+ return list(nodes);
+ }
+ return { read, write, html, encode, decode };
+})();
+if (typeof module !== "undefined" && module.exports) module.exports = EstMemoWpt;
diff --git a/tools/est-test/native-tidy.js b/tools/est-test/native-tidy.js
index b08900869..51b7ad582 100644
--- a/tools/est-test/native-tidy.js
+++ b/tools/est-test/native-tidy.js
@@ -1,5 +1,5 @@
// 원본 표 정리 — 정렬(BarSort 0x110ADEC·TfrmSiljukSort), 데이타 위치정리(mnuTableSort 0x10CCAEC·TFRMTABLESORT),
-// 호표 정리(popTitleSort 0x10D88EC), 근거(메모) 보기·삭제(mnuGunguEdit·mnuGunguDel, 필드 DifineMemo).
+// 호표 정리(popTitleSort 0x10D88EC), 근거(메모) 보기·고치기·붙여넣기·삭제(mnuGunguEdit·TfrmGunguEdit popPaste·mnuGunguDel, 필드 DifineMemo).
// 정렬: 기초단가 표를 관급구분(Gangub) 0·1·2 차례로 묶고 각 묶음을 품명+규격(실적은 코드 선택 가능) 순으로, Index 1..n.
// Index ≤ 0 이거나 품명·규격·수량·금액이 모두 빈 행은 버린다. 원본의 빈 행 30줄 채우기는 웹에서 하지 않는다.
// 위치정리: 기초단가·일위대가·단가산출·기계경비를 품명 오름/내림으로 다시 놓고 시작번호부터 Index — 묶음은 통째로 옮긴다.
@@ -69,7 +69,7 @@ const EstTidy = (() => {
return n - start;
}
- // 근거(메모): 원본 DifineMemo 는 WPTools 서식(zlib) — 웹은 글자만 꺼내 읽기 전용으로 보인다.
+ // 근거(메모): 원본 DifineMemo 는 WPTools 서식(zlib) — 글자만 꺼낸다.
async function memoText(memo) {
if (memo == null) return "";
if (typeof memo === "string") return memo;
@@ -92,6 +92,82 @@ const EstTidy = (() => {
.trim();
}
+ async function memoHtml(memo, objects) {
+ if (memo == null) return "${EstApp.esc(l) || " `)
+ .join("");
+ 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(),
+ );
+ return EstMemoWpt.html(EstMemoWpt.read(new TextDecoder("euc-kr").decode(bytes)), objects);
+ }
+ async function pack(text) {
+ const bytes = new Uint8Array(
+ await new Response(
+ new Blob([new TextEncoder().encode(text)])
+ .stream()
+ .pipeThrough(new CompressionStream("deflate")),
+ ).arrayBuffer(),
+ );
+ let out = "";
+ for (let i = 0; i < bytes.length; i += 0x8000)
+ out += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
+ return btoa(out);
+ }
+ // 편집 창 → 마디 목록(문단·표·그림): 블록 요소·"} 마다 문단을 끊고 굵게는 b/strong·굵은 글꼴 + function memoNodes(editor, objects) { + const BLOCK = /^(DIV|P|LI|H[1-6]|PRE|BLOCKQUOTE)$/; + function walk(root) { + const out = []; + let para = null; + const close = () => (para = null); + const add = (run) => { + if (!para) out.push((para = { p: [] })); + const last = para.p.at(-1); + if (run.text != null && last?.text != null && last.bold === run.bold) last.text += run.text; + else para.p.push(run); + }; + const visit = (node, bold) => { + if (node.nodeType === 3) { + if (node.data && !/^\s*\n\s*$/.test(node.data)) + add({ text: node.data.replace(/\n/g, " "), bold }); + return; + } + if (node.nodeType !== 1) return; + if (node.dataset.obj != null) return add({ object: objects[Number(node.dataset.obj)] }); + if (node.tagName === "BR") { + if (!para) out.push({ p: [] }); + return close(); + } + if (node.tagName === "TABLE") { + close(); + out.push({ + table: [...node.rows].map((tr) => [...tr.cells].map((td) => walk(td))), + }); + return; + } + const b = + bold || + /^(B|STRONG)$/.test(node.tagName) || + Number(node.style.fontWeight) >= 600 || + node.style.fontWeight === "bold"; + const block = BLOCK.test(node.tagName); + if (block) close(); + for (const child of node.childNodes) visit(child, b); + if (block) close(); + }; + for (const child of root.childNodes) visit(child, false); + return out; + } + return walk(editor); + } function memoTarget(state) { const index = [...state.selection][0], row = state.project.tables[state.view]?.[index]; @@ -135,21 +211,51 @@ const EstTidy = (() => { ), ); } else if (name === "memo-view") { - const { row } = memoTarget(state), - body = await memoText(row.DifineMemo); + // 근거자료보기(TfrmGunguEdit): 원본 서식 메모를 문단·표·그림(자리표)으로 펼쳐 고치고 붙여넣고 저장 + const { index, row } = memoTarget(state); + memo = { view: state.view, index, objects: [] }; EstApp.showDialog( - `근거(메모) — ${row.Name ?? row.Code ?? ""}`, - body - ? ` ${EstApp.esc(body)}${typeof row.DifineMemo === "string" ? "" : "원본 서식 메모라 웹에서는 읽기만 합니다. "}` - : "이 행에는 근거(메모)가 없습니다. ", + `근거자료보기 — ${row.Name ?? row.Code ?? ""}`, + `${await memoHtml(row.DifineMemo, memo.objects)} `,
);
+ } else if (name === "memo-paste") {
+ // popPaste(Ctrl+V): 클립보드 그대로(서식 있는 글·표 포함)
+ const editor = document.getElementById("memo-editor");
+ editor.focus();
+ let html = "";
+ try {
+ for (const item of await navigator.clipboard.read())
+ if (item.types.includes("text/html"))
+ html = await (await item.getType("text/html")).text();
+ } catch {}
+ if (html) document.execCommand("insertHTML", false, EstDocEditor.clean(html));
+ else document.execCommand("insertText", false, await navigator.clipboard.readText());
+ } else if (name === "memo-save") {
+ const editor = document.getElementById("memo-editor");
+ if (!editor || !memo) return;
+ const nodes = memoNodes(editor, memo.objects),
+ empty = !nodes.some((n) => n.table || n.p.some((r) => r.object || r.text.trim()));
+ const value = empty ? null : { binary: await pack(EstMemoWpt.write(nodes)) };
+ const { view, index } = memo;
+ EstApp.change((project) => (project.tables[view][index].DifineMemo = value), false);
+ EstApp.byId("dialog").close();
+ state.notice = "근거(메모)를 저장했습니다.";
} 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"]);
+ let memo = null;
+ const names = new Set([
+ "tidy-sort",
+ "tidy-arrange",
+ "tidy-number",
+ "memo-view",
+ "memo-paste",
+ "memo-save",
+ "memo-delete",
+ ]);
function handle(name, state) {
if (!names.has(name)) return false;
run(name, state).catch((error) => {
diff --git a/tools/est-test/server.cjs b/tools/est-test/server.cjs
index 256ce2a4c..88cd00962 100644
--- a/tools/est-test/server.cjs
+++ b/tools/est-test/server.cjs
@@ -32,6 +32,7 @@ const scripts = [
"native-excel-sheet.cjs",
"native-quantity-total.js",
"native-quantity-wpt.cjs",
+ "native-memo-wpt.cjs",
"native-quantity.cjs",
"native-wonga.js",
"native-jungsang.js",
|