"""도면 → 면적산출 넣을 자리 고르기(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"] == ( "
" '' '' "" "
구분수량
흙<깎기>1500m3
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