- 도면 표 받은 뒤 작은 창 [새 시트] / [고른 칸부터] · 칸 크기 · 글 크기는 기본(도면 mm 안 옮김) - 스프레드시트 공개 손잡이 pasteTable — 붙여넣기 길(parseClipboard + pasteBlock) · 덮을 값 · 알약 칸 셈(pasteHits) - 값 · 알약 이은 칸을 덮으면 물음 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KYZ2HWWdUwZWYnn3tQz7X
174 lines
6.4 KiB
Python
174 lines
6.4 KiB
Python
"""도면 → 면적산출 넣을 자리 고르기(PLAN 105-1) — Node 로 판정 · 화면은 ORCA.
|
||
|
||
못박는 것
|
||
- 받은 뒤 작은 창 [새 시트] / [고른 칸부터] · 취소면 아무것도 안 함.
|
||
- [고른 칸부터] = 손잡이 `pasteTable`(붙여넣기 길) 한 번 · 표 → `<table>` 글(병합 = 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"] == (
|
||
"<table>"
|
||
'<tr><td>구분</td><td colspan="2">수량</td></tr>'
|
||
'<tr><td rowspan="2">흙<깎기></td><td>1500</td><td>m3</td></tr>'
|
||
"<tr><td>2.5</td><td></td></tr>"
|
||
"</table>"
|
||
)
|
||
|
||
|
||
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
|