feat(est-test): 근거자료보기 고치기·붙여넣기·원본 서식 저장
- 근거(메모)를 문단·굵게·표·그림 자리표로 펼쳐 편집, 붙여넣기(서식 있는 글·표), 저장 - WPTools 글 형식 읽기·쓰기(native-memo-wpt.cjs) — 그림 원문 보존, ASCII 로 써서 cp949 변환 없음 - ESTX 저장: 원래 메모가 있던 행만, 새 메모는 JSON 저장 안내 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
"""근거(메모) WPTools 글 형식 읽기·쓰기: 문단·굵게·표 칸·그림 원문 보존."""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE = (
|
||||
"<!WPTools_Format V=560/>\r\n<stylesheet></stylesheet>"
|
||||
"<cs nr=1 wpsty=[[CharFont:'A';]]/><cs nr=2 wpsty=[[CharStyleMask:1;CharStyleON:1;]]/>"
|
||||
"<div cs=2><c nr=2/><근거><c nr=1/> A&B</div>\r\n<div cs=1></div>\r\n"
|
||||
"<table cs=1><tr><td cs=1><c nr=1/>공종</td>\r\n<td cs=1><c nr=2/>1.5</td></tr></table>"
|
||||
'<div cs=1><c nr=1/>x <txtobj wpsty=[[Type:12;]] type="TWPOImage">\r\n<!--QUJD--></txtobj></div>\r\n'
|
||||
)
|
||||
|
||||
|
||||
def node(script):
|
||||
code = (
|
||||
"const W=require(process.argv[1]);const S=process.argv[2];"
|
||||
f"console.log(JSON.stringify(({script})()))"
|
||||
)
|
||||
out = subprocess.check_output(
|
||||
["node", "-e", code, str(ROOT / "tools/est-test/native-memo-wpt.cjs"), SOURCE],
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def test_read_paragraphs_bold_table_and_object():
|
||||
nodes = node("()=>W.read(S)")
|
||||
assert nodes[0] == {
|
||||
"p": [{"text": "<근거>", "bold": True}, {"text": " A&B", "bold": False}]
|
||||
}
|
||||
assert nodes[1] == {"p": []}
|
||||
assert nodes[2]["table"] == [
|
||||
[[{"p": [{"text": "공종", "bold": False}]}], [{"p": [{"text": "1.5", "bold": True}]}]]
|
||||
]
|
||||
assert nodes[3]["p"][0] == {"text": "x ", "bold": False}
|
||||
assert nodes[3]["p"][1]["object"].startswith("<txtobj") and "QUJD" in nodes[3]["p"][1]["object"]
|
||||
|
||||
|
||||
def test_write_round_trips_and_stays_ascii():
|
||||
result = node(
|
||||
"()=>{const n=W.read(S),w=W.write(n);"
|
||||
"return {same:JSON.stringify(W.read(w))===JSON.stringify(n),ascii:/^[\\x00-\\x7e]*$/.test(w),"
|
||||
"html:W.html(n)}}"
|
||||
)
|
||||
assert result["same"] is True
|
||||
assert result["ascii"] is True
|
||||
assert "<b><근거></b>" in result["html"]
|
||||
assert 'data-obj="0"' in result["html"] and "<table" in result["html"]
|
||||
@@ -497,6 +497,8 @@ ative-standard.js에 표준시장단가 원천(SilJukDat)과 내역 행(EstMainD
|
||||
- 검증완료: 시험 2개 통과. ORCA 브라우저에서 코드 끔·레벨·노재경 구분·사정 끔·수량 0 숨김 반영과 취소 원복 확인, 임시 회차·옵션 원복.
|
||||
- [x] 도급내역 선택(spec_pb1 §3) — 설계변경 내역서에서 기성 회차를 추가하면 당초/변경 고르기. 당초면 행마다 당초 칸(_)으로 Index·품명·규격·단위(당초가 비면 지금 값)·수량·단가·금액. 웹은 `_` = 당초(설계변경 시작 때 복사본) 해석을 따름(spec_pb1 은 원본 분기상 `_` = 변경으로 읽음 — 실무 샘플 3건의 `_` 값이 서로 같아 당초 쪽으로 판단). 원본의 GC 누락 결함은 재현하지 않음.
|
||||
- 검증완료: 시험 1개 통과(당초 수량 4·변경 10). ORCA 브라우저에서 고르기 창·회차 loadMethod 확인, 원복.
|
||||
- [x] 근거자료보기 고치기·붙여넣기(TfrmGunguEdit popPaste · spec_pc §8 · spec_sort_memo §3) — 근거(메모)를 문단·굵게·표·그림(자리표)으로 펼쳐 고치고, 붙여넣기(서식 있는 글·표)·저장. 저장은 원본 WPTools 글 형식(굴림체 보통·굵게, 표 기본 테두리, 그림 원문 그대로, zlib). 문단 간격·들여쓰기·글꼴 크기는 버림. 원래 메모가 있던 행은 ESTX 저장 가능, 새 메모는 JSON 저장만(새 BLOB 할당 미지원).
|
||||
- 검증완료: 메모 시험 2개·정리 시험 2개 통과. 설치 샘플 메모 201건 글자 201/201 일치·그림 71/71 보존·되쓰기 188/201 동일(나머지는 칸 안 빈 줄만 빠짐), 고친 메모 ESTX 저장 → 다시 읽기 일치. ORCA 브라우저에서 굵게·표 저장 후 다시 열기 확인, 원복.
|
||||
- [x] 다른 공사 불러오기 창(원본 barbtnSelfLoad → TfrmOtherLoad, 찾기 popSerch) — 저장된 다른 작업을 골라 같은 표를 띄우고 찾기(품명·규격, A&B 둘 다)·고른 줄을 하위 자료와 함께 가져오기(내역서는 고른 줄 아래에 줄, 나머지는 자료·호표 합침, 같은 코드는 이 공사 것). 검증완료: 바인딩 통과, ORCA 브라우저에서 산불진화임도 작업의 일위대가 3개 → 「취수장」 1개 → 가져오기 호표 8→9·하위 41건, 샘플 복구.
|
||||
- [x] 사용위치 검색 창(원본 TfrmUsesDataSerch btnSerch·btnAllDisplay) — 현재 표 항목을 품명·규격(빈칸·|·괄호 무시)으로 거르고 골라 사용위치 보기. 검증완료: 바인딩 통과, ORCA 브라우저에서 노임 9개 → 「보 통」 1개 → 보통인부 사용위치 11곳.
|
||||
- [x] 인쇄양식 불러오기·저장하기(원본 mnuPrintFormatLoad·Save 0x10ed318·0x10ed000 — spec_pc §3) — .pfot(인쇄정보표 MIDAS 이진) 읽기·쓰기 `print-format.cjs`(설명 BLOB·빈 칸 표지까지 보존, 원본 파일 그대로 다시 쓰면 바이트 같음). 불러오면 공사 인쇄표를 그 값으로, 저장은 지금 인쇄표를 원본 설치 main.pfot 필드 설명자 위에(`EST_PFOT_TEMPLATE`). .gfot(cxGrid 열 배치)는 웹 격자와 호환되지 않아 하지 않음.
|
||||
|
||||
@@ -282,7 +282,11 @@ function exportEstx(project) {
|
||||
} else if (kind === 4 && width === 2) {
|
||||
if (!Number.isInteger(next)) throw Error("ESTX 정수 오류");
|
||||
data.writeInt16LE(next, at + 1);
|
||||
} else if (
|
||||
} else if (kind === 3 && next?.binary && !old?.binary)
|
||||
throw Error(
|
||||
`${key}.${name}: 새 근거(메모)는 ESTX에 저장할 수 없습니다. JSON 저장을 사용하세요.`,
|
||||
);
|
||||
else if (
|
||||
kind === 3 &&
|
||||
width === 8 &&
|
||||
next?.binary &&
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<script defer src="native-excel-edit.cjs"></script>
|
||||
<script defer src="native-quantity-total.js"></script>
|
||||
<script defer src="native-quantity-wpt.cjs"></script>
|
||||
<script defer src="native-memo-wpt.cjs"></script>
|
||||
<script defer src="native-quantity.cjs"></script>
|
||||
<script defer src="native-quantity-table.js"></script>
|
||||
<script defer src="native-quantity-list.js"></script>
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// 근거(메모) DifineMemo 의 WPTools 글 형식(V=560, zlib 안) 읽기·쓰기 — 근거자료보기 창(TfrmGunguEdit · spec_sort_memo §3)이
|
||||
// 고친 글을 원본 형식으로 되돌려 담는다. 읽기: 문단(<div>)·표(<table><tr><td>)·그림 개체(<txtobj> 원문 그대로)와
|
||||
// 글자 굵게(문자 서식 cs 의 CharStyleON 1번 비트). 쓰기: 굴림체(영문 이름 GulimChe) 보통·굵게 두 서식, 표는 테두리 있는 기본 꼴,
|
||||
// 그림은 읽은 원문 그대로, ASCII 밖 글자는 &#코드; — cp949 변환 없이 원본이 읽는 꼴. 문단 서식(간격·들여쓰기·글꼴 크기)은 버린다.
|
||||
const EstMemoWpt = (() => {
|
||||
const decode = (text) =>
|
||||
text
|
||||
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/ /g, " ")
|
||||
.replace(/&/g, "&");
|
||||
const encode = (text) =>
|
||||
[...String(text)]
|
||||
.map((ch) => {
|
||||
if (ch === "&") return "&";
|
||||
if (ch === "<") return "<";
|
||||
if (ch === ">") return ">";
|
||||
const code = ch.codePointAt(0);
|
||||
return code < 32 || code > 126 ? `&#${code};` : ch;
|
||||
})
|
||||
.join("");
|
||||
|
||||
// source → 마디 목록: { p: [{ text, bold } | { object }] } · { table: [[ [마디…] ]] }
|
||||
function read(source) {
|
||||
const objects = [];
|
||||
const text = source.replace(
|
||||
/<txtobj[\s\S]*?<\/txtobj>/g,
|
||||
(m) => `<obj ${objects.push(m) - 1}/>`,
|
||||
);
|
||||
const bold = new Map();
|
||||
for (const m of text.matchAll(/<cs nr=(\d+) wpsty=\[\[([\s\S]*?)\]\]\/>/g)) {
|
||||
const on = /CharStyleON:(\d+)/.exec(m[2]);
|
||||
bold.set(m[1], Boolean(on && Number(on[1]) & 1));
|
||||
}
|
||||
const root = [],
|
||||
stack = [{ list: root }];
|
||||
let para = null,
|
||||
current = false;
|
||||
const top = () => stack.at(-1);
|
||||
const push = (run) => {
|
||||
if (!para) {
|
||||
para = { p: [] };
|
||||
top().list.push(para);
|
||||
}
|
||||
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);
|
||||
};
|
||||
for (const part of text.split(/(<[^>]*>)/)) {
|
||||
if (!part) continue;
|
||||
if (!part.startsWith("<")) {
|
||||
const t = decode(part.replace(/\r?\n/g, ""));
|
||||
if (t && para) push({ text: t, bold: current });
|
||||
continue;
|
||||
}
|
||||
const tag = /^<\/?([a-zA-Z]+)/.exec(part)?.[1]?.toLowerCase(),
|
||||
closing = part.startsWith("</");
|
||||
if (tag === "div") {
|
||||
if (closing) para = null;
|
||||
else {
|
||||
// 칸 첫 문단이 비어 있으면(칸 자체가 문단) 그 자리를 씀
|
||||
if (!(para && !para.p.length && top().list.at(-1) === para)) {
|
||||
para = { p: [] };
|
||||
top().list.push(para);
|
||||
}
|
||||
const cs = /\bcs=(\d+)/.exec(part);
|
||||
current = cs ? bold.get(cs[1]) || false : false;
|
||||
}
|
||||
} else if (tag === "c") current = bold.get(/nr=(\d+)/.exec(part)?.[1]) || false;
|
||||
else if (tag === "obj") push({ object: objects[Number(/obj (\d+)/.exec(part)[1])] });
|
||||
else if (tag === "table" && !closing) {
|
||||
const table = { table: [] };
|
||||
top().list.push(table);
|
||||
stack.push({ table });
|
||||
} else if (tag === "tr" && !closing) {
|
||||
const row = [];
|
||||
stack.findLast((s) => s.table)?.table.table.push(row);
|
||||
stack.push({ row });
|
||||
} else if (tag === "td" && !closing) {
|
||||
const cell = [];
|
||||
stack.findLast((s) => s.row)?.row.push(cell);
|
||||
stack.push({ list: cell });
|
||||
// 칸 = 문단 하나(글이 td 바로 안에) — 안에 div 가 더 있으면 이어지는 문단
|
||||
para = { p: [] };
|
||||
cell.push(para);
|
||||
const cs = /\bcs=(\d+)/.exec(part);
|
||||
current = cs ? bold.get(cs[1]) || false : false;
|
||||
} else if (["table", "tr", "td"].includes(tag) && closing) {
|
||||
const key = { table: "table", tr: "row", td: "list" }[tag];
|
||||
while (stack.length > 1 && !stack.at(-1)[key]) stack.pop();
|
||||
if (stack.length > 1) stack.pop();
|
||||
para = null;
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
function write(nodes) {
|
||||
const font = "CharFont:'GulimChe';CharFontSize:1000;";
|
||||
const runs = (p) =>
|
||||
p.map((r) => (r.object ? r.object : `<c nr=${r.bold ? 2 : 1}/>${encode(r.text)}`)).join("");
|
||||
const list = (items) =>
|
||||
items
|
||||
.map((n) =>
|
||||
n.table
|
||||
? `<table cs=1>${n.table
|
||||
.map(
|
||||
(row) =>
|
||||
`<tr wpsty=[[BorderFlags:15;BorderType:0;]]>${row.map((cell) => `<td cs=1 wpsty=[[BorderFlags:15;]]>${runs(cell[0]?.p || [])}${list(cell.slice(1))}</td>`).join("")}</tr>`,
|
||||
)
|
||||
.join("")}</table>\r\n`
|
||||
: `<div cs=1>${runs(n.p)}</div>\r\n`,
|
||||
)
|
||||
.join("");
|
||||
return (
|
||||
"<!WPTools_Format V=560/>\r\n" +
|
||||
'<GlobalPageFormat wpcss="landscape:0;paperw:11906;paperh:16838;margr:1880;margl:1880;margt:1440;margb:1440;marg_header:720;marg_footer:720;deftabstop:720;marginmirror:0;"/>' +
|
||||
"<StandardFont wpcss=\"CharFont:'Arial';CharFontSize:1100;\"/>\r\n" +
|
||||
"<numberstyles></numberstyles>\r\n<stylesheet></stylesheet>" +
|
||||
`<cs nr=1 wpsty=[[${font}]]/><cs nr=2 wpsty=[[${font}CharStyleMask:1;CharStyleON:1;]]/>` +
|
||||
list(nodes)
|
||||
);
|
||||
}
|
||||
|
||||
// 편집 창용 html(그림은 고칠 수 없는 자리표 — data-obj 번호로 원문을 되찾음)
|
||||
function html(nodes, objects = []) {
|
||||
const esc = (s) =>
|
||||
s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
||||
const runs = (p) =>
|
||||
p
|
||||
.map((r) =>
|
||||
r.object
|
||||
? `<span contenteditable="false" data-obj="${objects.push(r.object) - 1}" style="border:1px dashed #888;padding:0 4px">[그림]</span>`
|
||||
: r.bold
|
||||
? `<b>${esc(r.text)}</b>`
|
||||
: esc(r.text),
|
||||
)
|
||||
.join("") || "<br>";
|
||||
const list = (items) =>
|
||||
items
|
||||
.map((n) =>
|
||||
n.table
|
||||
? `<table border="1" style="border-collapse:collapse">${n.table.map((row) => `<tr>${row.map((cell) => `<td>${list(cell)}</td>`).join("")}</tr>`).join("")}</table>`
|
||||
: `<div>${runs(n.p)}</div>`,
|
||||
)
|
||||
.join("");
|
||||
return list(nodes);
|
||||
}
|
||||
return { read, write, html, encode, decode };
|
||||
})();
|
||||
if (typeof module !== "undefined" && module.exports) module.exports = EstMemoWpt;
|
||||
@@ -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 "<div><br></div>";
|
||||
if (typeof memo === "string")
|
||||
return memo
|
||||
.split(/\r?\n/)
|
||||
.map((l) => `<div>${EstApp.esc(l) || "<br>"}</div>`)
|
||||
.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);
|
||||
}
|
||||
// 편집 창 → 마디 목록(문단·표·그림): 블록 요소·<br> 마다 문단을 끊고 굵게는 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
|
||||
? `<pre style="white-space:pre-wrap;max-height:400px;overflow:auto">${EstApp.esc(body)}</pre>${typeof row.DifineMemo === "string" ? "" : "<p>원본 서식 메모라 웹에서는 읽기만 합니다.</p>"}`
|
||||
: "<p>이 행에는 근거(메모)가 없습니다.</p>",
|
||||
`근거자료보기 — ${row.Name ?? row.Code ?? ""}`,
|
||||
`<div id="memo-editor" contenteditable="true" style="min-height:260px;max-height:420px;overflow:auto;border:1px solid #999;padding:6px;white-space:pre-wrap">${await memoHtml(row.DifineMemo, memo.objects)}</div><button data-action="memo-paste">붙여넣기</button> <button data-action="memo-save">저장</button>`,
|
||||
);
|
||||
} 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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user