test(b09-print): 수량산출 영역 이음매 — 302-3 쪽 → 인쇄 페이지 서버 몸 → 표 PDF(지도 그림) · 엑셀 차례 (302-1)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
/* =============================================================================
|
||||
* print_quantity_entry.ts (PLAN 302-1 시험 진입)
|
||||
* 수량산출 영역(`collectQuantitySheets`) → 인쇄 페이지와 같은 서버 몸(`pdfBody` · `xlsxBody`).
|
||||
* `print_quantity_seam.mjs` 가 vite 로 묶어 씀.
|
||||
* ========================================================================== */
|
||||
|
||||
export { collectQuantitySheets } from "../../../B09_Estimation/draft/B09_Draft_Print_Quantity";
|
||||
export { pdfBody, xlsxBody } from "../../../B09_Estimation/draft/B09_Draft_Print_Table";
|
||||
@@ -0,0 +1,110 @@
|
||||
// 302-1 수량산출 이음매 — 영역 모으기 → 인쇄 페이지 서버 몸(pdfBody · xlsxBody) 을 node 로.
|
||||
// node print_quantity_seam.mjs <저장소 뿌리>
|
||||
// 표준입력 = { project, responses: {"/quantity/…": 응답}, image_b64 } (302-3 시험 자료 꼴)
|
||||
// 표준출력 끝 줄 = { pdf: {title, sheets}, xlsx: {title, sheets} }
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const root = path.resolve(process.argv[2] ?? ".");
|
||||
const { build } = await import(
|
||||
pathToFileURL(path.join(root, "config/node_modules/vite/dist/node/index.js")).href
|
||||
);
|
||||
const input = JSON.parse(fs.readFileSync(0, "utf-8"));
|
||||
|
||||
// 화면 · 세션만 흉내 — 이 길은 DOM 을 안 그림
|
||||
class Node {
|
||||
constructor() {
|
||||
this.children = [];
|
||||
this.classList = { toggle() {}, add() {}, remove() {}, contains: () => false };
|
||||
this.style = {};
|
||||
this.dataset = {};
|
||||
}
|
||||
append(...kids) {
|
||||
this.children.push(...kids);
|
||||
}
|
||||
appendChild(kid) {
|
||||
this.children.push(kid);
|
||||
return kid;
|
||||
}
|
||||
addEventListener() {}
|
||||
setAttribute() {}
|
||||
}
|
||||
const session = new Map();
|
||||
const storage = {
|
||||
getItem: (k) => (session.has(k) ? session.get(k) : null),
|
||||
setItem: (k, v) => session.set(k, String(v)),
|
||||
removeItem: (k) => session.delete(k),
|
||||
};
|
||||
globalThis.document = {
|
||||
createElement: () => new Node(),
|
||||
createElementNS: () => new Node(),
|
||||
createTextNode: () => new Node(),
|
||||
head: new Node(),
|
||||
body: new Node(),
|
||||
addEventListener() {},
|
||||
documentElement: new Node(),
|
||||
};
|
||||
globalThis.window = {
|
||||
sessionStorage: storage,
|
||||
localStorage: storage,
|
||||
location: { origin: "http://localhost", hash: "" },
|
||||
addEventListener() {},
|
||||
setTimeout: (...a) => setTimeout(...a),
|
||||
clearTimeout: (id) => clearTimeout(id),
|
||||
};
|
||||
globalThis.sessionStorage = storage;
|
||||
globalThis.localStorage = storage;
|
||||
|
||||
const png = Buffer.from(input.image_b64, "base64");
|
||||
globalThis.fetch = async (url) => {
|
||||
const at = String(url)
|
||||
.replace(/^.*\/projects\/[^/]+/, "")
|
||||
.split("?")[0];
|
||||
if (at === "/quantity/milestone/image")
|
||||
return new Response(png, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
const body = input.responses[at];
|
||||
return body
|
||||
? new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
: new Response(JSON.stringify({ detail: `없음 ${at}` }), { status: 404 });
|
||||
};
|
||||
|
||||
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "aislo-302-1-"));
|
||||
try {
|
||||
await build({
|
||||
configFile: false,
|
||||
logLevel: "silent",
|
||||
root,
|
||||
resolve: {
|
||||
alias: {
|
||||
"@ui": path.join(root, "ui_template"),
|
||||
"@config": path.join(root, "config"),
|
||||
"@util": path.join(root, "common_util"),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
outDir,
|
||||
minify: false,
|
||||
target: "es2022",
|
||||
lib: {
|
||||
entry: path.join(root, "resources/tester/print_frame/print_quantity_entry.ts"),
|
||||
formats: ["es"],
|
||||
fileName: "entry",
|
||||
},
|
||||
},
|
||||
});
|
||||
const mod = await import(pathToFileURL(path.join(outDir, "entry.js")).href);
|
||||
const sheets = await mod.collectQuantitySheets(input.project);
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
pdf: mod.pdfBody("수량산출", sheets),
|
||||
xlsx: mod.xlsxBody("수량산출", sheets),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -337,3 +337,57 @@ def test_앱_전체_길_목록에_겹친_앞머리_없음():
|
||||
assert not [p for p in paths if "/api/projects/api/projects" in p]
|
||||
for tail in ("drawings", "drawings-pdf", "table-pdf", "table-xlsx"):
|
||||
assert paths.count(f"/api/projects/{{project_id}}/print/{tail}") == 1, tail
|
||||
|
||||
|
||||
# ── 영역 이음매 — 수량산출(302-3) 쪽 → 페이지와 같은 서버 몸 → 표 PDF · 엑셀 ────────────
|
||||
|
||||
#: 1×1 PNG — 거리이정표 지도 그림 자리
|
||||
TINY_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==" # noqa: E501
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def quantity_bodies() -> dict:
|
||||
sys.path.insert(0, str(ROOT / "resources/tester"))
|
||||
import test_302_3_print_quantity as t302_3
|
||||
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
job = {"project": "p302", "responses": t302_3._responses(), "image_b64": TINY_PNG}
|
||||
out = subprocess.run(
|
||||
["node", str(ROOT / "resources/tester/print_frame/print_quantity_seam.mjs"), str(ROOT)],
|
||||
input=json.dumps(job, ensure_ascii=False), cwd=ROOT, capture_output=True, text=True,
|
||||
encoding="utf-8", timeout=300,
|
||||
) # fmt: skip
|
||||
assert out.returncode == 0, out.stderr
|
||||
return json.loads(out.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def test_수량_영역_엑셀_차례_그대로(quantity_bodies):
|
||||
from openpyxl import load_workbook
|
||||
|
||||
body = quantity_bodies["xlsx"]
|
||||
assert len(body["sheets"]) >= 20
|
||||
res = _client().post(f"/api/projects/{PID}/print/table-xlsx", json=body)
|
||||
assert res.status_code == 200, res.text
|
||||
wb = load_workbook(io.BytesIO(res.content))
|
||||
used: set[str] = set()
|
||||
assert wb.sheetnames == [pr.sheet_title(s["name"], used) for s in body["sheets"]]
|
||||
assert all(
|
||||
wb[n].max_row >= s["grid"]["head_rows"] for n, s in zip(wb.sheetnames, body["sheets"])
|
||||
)
|
||||
|
||||
|
||||
def test_수량_영역_PDF_시트_이름_지도_그림(quantity_bodies):
|
||||
pymupdf = pytest.importorskip("pymupdf")
|
||||
body = quantity_bodies["pdf"]
|
||||
assert any('src="data:image/png' in s["html"] for s in body["sheets"]) # 그림 = data: 주소
|
||||
res = _client().post(f"/api/projects/{PID}/print/table-pdf", json=body)
|
||||
if res.status_code == 503:
|
||||
pytest.skip(res.json()["detail"])
|
||||
assert res.status_code == 200, res.text
|
||||
doc = pymupdf.open(stream=res.content)
|
||||
assert doc.page_count >= len(body["sheets"])
|
||||
text = "".join(p.get_text() for p in doc).replace("\n", " ")
|
||||
missing = [s["name"] for s in body["sheets"] if s["name"] not in text]
|
||||
assert not missing
|
||||
assert sum(len(p.get_images()) for p in doc) >= 1 # 거리이정표 지도 그림이 들어감
|
||||
|
||||
Reference in New Issue
Block a user