"""도면 → 면적산출 넣을 자리 고르기(PLAN 105-1) — Node 로 판정 · 화면은 ORCA.
못박는 것
- 받은 뒤 작은 창 [새 시트] / [고른 칸부터] · 취소면 아무것도 안 함.
- [고른 칸부터] = 손잡이 `pasteTable`(붙여넣기 길) 한 번 · 표 → `
` 글(병합 = rowspan · colspan · 글 이스케이프).
- 덮을 자리에 값 · 알약 이은 칸이 있으면 물음 · 없으면 안 물음 · 물음 [취소]면 안 붙임.
- 덮을 자리 셈 `pasteHits`(스프레드시트 부품) — 값 · 식 칸 수 · 알약 칸 수(빈 값 · 범위 밖은 뺌).
"""
import json
import shutil
import subprocess
import tempfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
ENTRY = ROOT / "resources" / "tester" / "spreadsheet" / "spreadsheet_paste_hits_entry.ts"
VITE = ROOT / "config" / "node_modules" / "vite" / "bin" / "vite.js"
SCRIPT = r"""
const fs = require("fs");
const ts = require(process.argv[1] + "/config/node_modules/typescript");
const src = fs.readFileSync(process.argv[1] + "/M02_MasterTemplete/M02_MasterTemplete_AreaFromDrawing.ts", "utf8");
const js = ts.transpileModule(src, {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
}).outputText;
const toasts = [];
const asked = [];
let answer = true;
const stubs = {
"@ui/ui_template_elements": {
showToast: (m, k) => toasts.push([k, m]),
showConfirmDialog: async (m, ok) => (asked.push([m, ok]), answer),
},
};
const box = { exports: {} };
new Function("exports", "module", "require", js)(box.exports, box, (n) => stubs[n] ?? require(n));
const { tableToHtml, importDrawingTable } = box.exports;
const table = {
kind: "grid",
rows: [["구분", "수량", ""], ["흙<깎기>", 1500, "m3"], ["", 2.5, ""]],
merges: [{ row: 0, col: 1, rowSpan: 1, colSpan: 2 }, { row: 1, col: 0, rowSpan: 2, colSpan: 1 }],
colWidths: [40, 30, 300],
rowHeights: [10, 7, 1],
};
const book = { 시트: [{ id: "s1", 이름: "면적" }] };
(async () => {
const out = { html: tableToHtml(table) };
const calls = [];
let hit = { 칸: 0, 연결: 0, 범위: "C3:E5" };
const handle = {
getDoc: () => book,
addSheet: (s) => calls.push(["addSheet", s.이름]),
pasteTable: async (html, check) => {
calls.push(["pasteTable", html === out.html]);
return (await check(hit)) ? hit.범위 : null;
},
};
const sizes = [];
const pick = (got) => async (rows, cols) => (sizes.push([rows, cols]), got);
out.cancel = await importDrawingTable(handle, table, pick(null));
out.clean = await importDrawingTable(handle, table, pick("cell"));
out.askedClean = asked.length;
hit = { 칸: 2, 연결: 1, 범위: "C3:E5" };
out.overwrite = await importDrawingTable(handle, table, pick("cell"));
answer = false;
out.refused = await importDrawingTable(handle, table, pick("cell"));
out.sheet = await importDrawingTable(handle, table, pick("sheet"));
out.noPaste = await importDrawingTable({ getDoc: () => book, addSheet: () => 0 }, table, pick("cell"));
Object.assign(out, { calls, sizes, asked, toasts });
console.log(JSON.stringify(out));
})();
"""
@pytest.fixture(scope="module")
def run():
node = shutil.which("node")
if not node:
pytest.skip("node 없음")
done = subprocess.run(
[node, "-e", SCRIPT, str(ROOT)],
capture_output=True,
text=True,
encoding="utf-8",
check=True,
)
return json.loads(done.stdout)
def test_표는_html_표로(run):
assert run["html"] == (
""
'| 구분 | 수량 |
'
'| 흙<깎기> | 1500 | m3 |
'
"| 2.5 | |
"
"
"
)
def test_넣을_자리_고르기(run):
assert run["sizes"][0] == [3, 3] # 창 글 — 3행 × 3열
assert run["cancel"] is None # 취소 = 아무것도 안 함
assert run["clean"] == "C3:E5" and run["askedClean"] == 0 # 빈 자리 = 안 물음
assert run["overwrite"] == "C3:E5" # 덮음 [덮어쓰기]
assert run["refused"] is None # 덮음 [취소]
assert run["sheet"] == "도면에서 1"
assert run["noPaste"] is None
assert run["calls"] == [
["pasteTable", True],
["pasteTable", True],
["pasteTable", True],
["addSheet", "도면에서 1"],
]
def test_덮으면_물음(run):
assert len(run["asked"]) == 2
message, ok = run["asked"][0]
assert "C3:E5" in message and "값 있는 칸 2개" in message and "알약 이은 칸 1개" in message
assert ok == "덮어쓰기"
kinds = [kind for kind, _ in run["toasts"]]
assert kinds == ["success", "success", "success", "error"]
assert "C3:E5" in run["toasts"][0][1]
def test_손잡이와_붙여넣기_길():
sheet = (ROOT / "A00_Common/spreadsheet/spreadsheet.ts").read_text(encoding="utf-8")
assert "pasteTable: (html, check) => pasteTableAt(ctx, html, check)," in sheet
clip = (ROOT / "A00_Common/spreadsheet/spreadsheet_clipboard.ts").read_text(encoding="utf-8")
body = clip.split("export async function pasteTableAt")[1].split("\n}\n")[0]
assert "parseClipboard(html, " in body and "pasteBlock(ctx, block, at)" in body
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_덮을_자리_셈():
with tempfile.TemporaryDirectory(prefix="spreadsheet_paste_hits_") as outdir:
entry_rel = "../" + str(ENTRY.relative_to(ROOT)).replace("\\", "/")
build = subprocess.run(
[
"node",
str(VITE),
"build",
"--configLoader",
"runner",
"--ssr",
entry_rel,
"--outDir",
outdir,
],
cwd=str(ROOT),
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=120,
)
assert build.returncode == 0, f"vite ssr 번들 실패:\n{build.stdout}\n{build.stderr}"
got = subprocess.run(
["node", str(Path(outdir) / "spreadsheet_paste_hits_entry.js")],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
timeout=60,
)
assert got.stdout.strip(), got.stderr
result = json.loads(got.stdout)
assert result["fail"] == 0, result["failures"]
assert result["pass"] == 4