feat(b09-print): 인쇄 미리보기 수량산출 영역 — collectQuantitySheets · B08 탭 차례 PrintSheet(구조물 · 자재 셋 · 토공 넷 · 수리 측점마다 · 거리이정표 + 지도 그림 data:) · 화면과 같은 표 짜기 · 캐시 값 · 영역 실패 = 까닭 한 장 (302-3)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Se7ytqbohmBQnfYpwYFnYf
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/* =============================================================================
|
||||
* B09_Draft_Print_Quantity.ts
|
||||
* 인쇄 미리보기(PLAN 302-3 · 초안) 「수량산출」 영역 — B08 탭 표를 화면 없이 PrintSheet 로 모음.
|
||||
*
|
||||
* 차례 = B08 탭 차례 — 구조물 집계표 → 자재 셋(총괄 · 콘크리트소요 · 자재수량) → 토공 넷(토공집계표 ·
|
||||
* 토적표 · 평균운반거리 · 종방향 무대운반거리) → 수리집수면적표(배수지점 측점마다 한 장) → 거리이정표
|
||||
* 값 = B08 화면과 같은 길(서버 GET + 화면 캐시 · [저장] 전 값이면 화면에 보이는 그 값) ·
|
||||
* 표 짜기 = B08 화면이 쓰는 같은 함수(`B08_Quantity_*_Table(s)`)
|
||||
* 수리 = 화면 계산서(두 카드)를 구분 · 항목 · 값 표로 · 계산 = 서버(화면과 같은 POST)
|
||||
* 거리이정표 = 표 + 경로 지도 그림(사용자 그림 우선 · 없으면 서버 캡처 PNG · data: 주소)
|
||||
* 영역 하나가 못 읽히면 그 자리에 까닭 한 장(나머지는 그대로)
|
||||
* 구조물 조합 도면은 여기 없음(도면 영역 · 302-2)
|
||||
* ========================================================================== */
|
||||
|
||||
import { API_BASE_URL } from "@config/config_frontend";
|
||||
import type { DataTableOptions } from "@ui/ui_template_data_table";
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { stationLabel } from "../../common_util/common_util_station";
|
||||
import {
|
||||
EARTHWORK_SHEETS,
|
||||
earthworkSheetOptions,
|
||||
earthworkSummaryOptions,
|
||||
HAUL_SHEETS,
|
||||
haulOptions,
|
||||
loadEarthwork,
|
||||
} from "../../B08_Quantity/B08_Quantity_Earthwork_Tables";
|
||||
import {
|
||||
hydroCalc,
|
||||
hydroInputs,
|
||||
hydroTable,
|
||||
loadHydro,
|
||||
} from "../../B08_Quantity/B08_Quantity_Hydro_Table";
|
||||
import {
|
||||
loadMaterials,
|
||||
materialOptions,
|
||||
materialTable,
|
||||
type MaterialSheetId,
|
||||
} from "../../B08_Quantity/B08_Quantity_Material_Tables";
|
||||
import { milestoneTable } from "../../B08_Quantity/B08_Quantity_Milestone_Table";
|
||||
import { loadSummary, summaryTable } from "../../B08_Quantity/B08_Quantity_Summary_Table";
|
||||
import {
|
||||
liveMilestone,
|
||||
loadMilestone,
|
||||
routeImages,
|
||||
siteInfo,
|
||||
} from "../../B08_Quantity/B08_Quantity_UI_Milestone_State";
|
||||
import type { CollectSheets, PrintSheet } from "./B09_Draft_Print_Types";
|
||||
|
||||
function L(key: keyof typeof ui_locales): string {
|
||||
return ui_locales[key][currentLanguageIndex];
|
||||
}
|
||||
|
||||
/** 자재 보조 탭 차례(B08 화면 `SHEETS`) */
|
||||
const MATERIAL_SHEETS: Array<[MaterialSheetId, keyof typeof ui_locales]> = [
|
||||
["material", "B08_Quantity_Tab_MaterialTotal"],
|
||||
["concrete", "B08_Quantity_Tab_ConcreteMaterial"],
|
||||
["quantity", "B08_Quantity_Tab_MaterialQuantity"],
|
||||
];
|
||||
|
||||
/** 표 한 장 — 열이 많으면 가로 쪽 */
|
||||
const sheet = (id: string, name: string, table: DataTableOptions): PrintSheet => ({
|
||||
id,
|
||||
name,
|
||||
table,
|
||||
orientation: table.columns.length > 8 ? "landscape" : "portrait",
|
||||
});
|
||||
|
||||
/** 까닭 한 장 — 못 읽은 영역 · 빈 표 자리 */
|
||||
const noteSheet = (id: string, name: string, text: string): PrintSheet =>
|
||||
sheet(id, name, {
|
||||
columns: [{ key: "note", label: "알림", width: 480 }],
|
||||
rows: [{ note: text }],
|
||||
});
|
||||
|
||||
/** 영역 하나 — 실패하면 까닭 한 장 */
|
||||
const area = (id: string, name: string, make: () => Promise<PrintSheet[]>): Promise<PrintSheet[]> =>
|
||||
make().catch((error: unknown) => [
|
||||
noteSheet(
|
||||
id,
|
||||
name,
|
||||
`불러오지 못했습니다 — ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
]);
|
||||
|
||||
async function summarySheets(projectId: string): Promise<PrintSheet[]> {
|
||||
const name = L("B08_Quantity_Tab_StructureSummary");
|
||||
const view = await loadSummary(projectId);
|
||||
if (!view.stations.length)
|
||||
return [noteSheet("summary", name, L("B08_Quantity_Summary_NoStation"))];
|
||||
return [sheet("summary", name, summaryTable(view).options)];
|
||||
}
|
||||
|
||||
async function materialSheets(projectId: string): Promise<PrintSheet[]> {
|
||||
const { payload, overrides, calc } = await loadMaterials(projectId);
|
||||
const sheets = calc(payload, overrides);
|
||||
return MATERIAL_SHEETS.map(([id, label]) =>
|
||||
sheet(`material:${id}`, L(label), materialOptions(materialTable(id, sheets, overrides))),
|
||||
);
|
||||
}
|
||||
|
||||
async function earthworkSheets(projectId: string): Promise<PrintSheet[]> {
|
||||
const { table, interval } = await loadEarthwork(projectId);
|
||||
return EARTHWORK_SHEETS.map(({ id, label }) => {
|
||||
const key = `earthwork:${id}`;
|
||||
if (id === "summary") return sheet(key, label, earthworkSummaryOptions(table));
|
||||
if (id === "sheet")
|
||||
return table.rows.length
|
||||
? sheet(key, label, earthworkSheetOptions(table, interval))
|
||||
: noteSheet(key, label, "측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요.");
|
||||
const { groups, caption } = HAUL_SHEETS[id](table);
|
||||
const { options, averages } = haulOptions(groups, caption);
|
||||
// 화면 표 아래 평균운반거리 줄 = 쪽엔 본문 끝 줄(번호 칸 글)
|
||||
return sheet(key, label, {
|
||||
...options,
|
||||
rows: [...options.rows, ...averages.map((no) => ({ no }))],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function hydroSheets(projectId: string): Promise<PrintSheet[]> {
|
||||
const head = L("B08_Quantity_Tab_Hydrology");
|
||||
const { sheet: payload, interval, drafts } = await loadHydro(projectId);
|
||||
if (!payload.stations.length)
|
||||
return [
|
||||
noteSheet(
|
||||
"hydro",
|
||||
head,
|
||||
"저장된 배수지점(관 지점)이 없습니다 — 배수유역 · 종단에서 관을 저장하세요.",
|
||||
),
|
||||
];
|
||||
return Promise.all(
|
||||
payload.stations.map(async (station) => {
|
||||
const inputs = hydroInputs(payload, station, drafts);
|
||||
const result = await hydroCalc(projectId, inputs).catch(() => null);
|
||||
const label = stationLabel(station.chainage_m, interval);
|
||||
return sheet(
|
||||
`hydro:${station.chainage_m.toFixed(3)}`,
|
||||
`${head} ${label}${station.kind ? ` ${station.kind}` : ""}`,
|
||||
hydroTable(label, station.kind, inputs, result, payload),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** 그림 → data: 주소(서버 PDF 가 쿠키 없이 그림) */
|
||||
async function dataUrl(url: string): Promise<string> {
|
||||
const response = await fetch(url, { credentials: "include" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const blob = await response.blob();
|
||||
const bytes = new Uint8Array(await blob.arrayBuffer());
|
||||
let text = "";
|
||||
for (let i = 0; i < bytes.length; i += 0x8000)
|
||||
text += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
return `data:${blob.type || "image/png"};base64,${btoa(text)}`;
|
||||
}
|
||||
|
||||
async function milestoneSheets(projectId: string): Promise<PrintSheet[]> {
|
||||
const name = L("B08_Quantity_Tab_Milestone");
|
||||
await loadMilestone(projectId, true);
|
||||
const data = liveMilestone(projectId);
|
||||
const { options } = milestoneTable(data, siteInfo()?.mid_default_m ?? null);
|
||||
const images = routeImages();
|
||||
const pictures = await Promise.all(
|
||||
data.routes
|
||||
.filter((route) => images[route.id])
|
||||
.map(async (route, i) => ({
|
||||
src: await dataUrl(
|
||||
`${API_BASE_URL}/projects/${projectId}/quantity/milestone/image?route=${encodeURIComponent(route.id)}`,
|
||||
).catch(() => ""),
|
||||
caption: route.name.trim() || route.rows[0]?.item || `거래처 ${i + 1}`,
|
||||
})),
|
||||
);
|
||||
return [{ ...sheet("milestone", name, options), images: pictures.filter((p) => p.src) }];
|
||||
}
|
||||
|
||||
export const collectQuantitySheets: CollectSheets = async (projectId) => {
|
||||
const parts = await Promise.all([
|
||||
area("summary", L("B08_Quantity_Tab_StructureSummary"), () => summarySheets(projectId)),
|
||||
area("material", L("B08_Quantity_Tab_Materials"), () => materialSheets(projectId)),
|
||||
area("earthwork", L("B08_Quantity_Tab_Earthwork"), () => earthworkSheets(projectId)),
|
||||
area("hydro", L("B08_Quantity_Tab_Hydrology"), () => hydroSheets(projectId)),
|
||||
area("milestone", L("B08_Quantity_Tab_Milestone"), () => milestoneSheets(projectId)),
|
||||
]);
|
||||
return parts.flat();
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
// 302-3 인쇄 미리보기 수량산출 — 화면 코드를 node 로 묶어 모으기 결과와 B08 화면 표 짜기를 나란히 냄.
|
||||
// node helper_302_3_print_quantity.mjs <저장소 뿌리>
|
||||
// 표준입력 = { project, responses: {"/quantity/…": 응답}, image_b64, drafts: [[이름, 값]], screen }
|
||||
// (screen = false → 화면 표 짜기 생략 · 응답을 일부러 뺀 실패 견본)
|
||||
// 표준출력 끝 줄 = { sheets: [모으기 쪽 요약], screen: {B08 화면 표 짜기 요약}, posts: [hydro/calc 본문] }
|
||||
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 posts = [];
|
||||
const png = Buffer.from(input.image_b64, "base64");
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
const at = String(url)
|
||||
.replace(/^.*\/projects\/[^/]+/, "")
|
||||
.split("?")[0];
|
||||
if (at === "/quantity/hydro/calc") posts.push(JSON.parse(init.body));
|
||||
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 shape = (t) => ({
|
||||
heads: t.columns.map((c) => [...(c.group ?? []), c.label].join("›")),
|
||||
rows: t.rows,
|
||||
total: t.total ?? null,
|
||||
mergeSame: t.mergeSame ?? null,
|
||||
frozen: t.frozen ?? 0,
|
||||
});
|
||||
|
||||
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), "aislo-302-3-"));
|
||||
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/helper_302_3_print_quantity_entry.ts"),
|
||||
formats: ["es"],
|
||||
fileName: "entry",
|
||||
},
|
||||
},
|
||||
});
|
||||
const mod = await import(pathToFileURL(path.join(outDir, "entry.js")).href);
|
||||
const p = input.project;
|
||||
for (const [name, value] of input.drafts ?? []) mod.writeState(name, value, p);
|
||||
|
||||
const sheets = (await mod.collectQuantitySheets(p)).map((s) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
orientation: s.orientation ?? null,
|
||||
images: s.images ?? [],
|
||||
...shape(s.table),
|
||||
}));
|
||||
|
||||
if (input.screen === false) {
|
||||
console.log(JSON.stringify({ sheets, screen: null, posts }));
|
||||
process.exit(0);
|
||||
}
|
||||
// B08 화면이 부르는 같은 함수 — 같은 응답 · 같은 캐시
|
||||
const screen = {};
|
||||
screen.summary = shape(mod.summaryTable(await mod.loadSummary(p)).options);
|
||||
const m = await mod.loadMaterials(p);
|
||||
const ms = m.calc(m.payload, m.overrides);
|
||||
for (const id of ["material", "concrete", "quantity"])
|
||||
screen[`material:${id}`] = shape(mod.materialOptions(mod.materialTable(id, ms, m.overrides)));
|
||||
const { table, interval } = await mod.loadEarthwork(p);
|
||||
screen["earthwork:summary"] = shape(mod.earthworkSummaryOptions(table));
|
||||
screen["earthwork:sheet"] = shape(mod.earthworkSheetOptions(table, interval));
|
||||
for (const id of ["haul", "free"]) {
|
||||
const { groups, caption } = mod.HAUL_SHEETS[id](table);
|
||||
const { options, averages } = mod.haulOptions(groups, caption);
|
||||
screen[`earthwork:${id}`] = { ...shape(options), averages };
|
||||
}
|
||||
screen.earthworkNames = mod.EARTHWORK_SHEETS.map((s) => s.label);
|
||||
const h = await mod.loadHydro(p);
|
||||
screen.hydroInputs = h.sheet.stations.map((st) => mod.hydroInputs(h.sheet, st, h.drafts));
|
||||
screen.milestone = shape(
|
||||
mod.milestoneTable(mod.liveMilestone(p), mod.siteInfo()?.mid_default_m ?? null).options,
|
||||
);
|
||||
console.log(JSON.stringify({ sheets, screen, posts }));
|
||||
} finally {
|
||||
fs.rmSync(outDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/* =============================================================================
|
||||
* helper_302_3_print_quantity_entry.ts
|
||||
* 인쇄 미리보기 수량산출 시험(`test_302_3_print_quantity.py`)이 묶는 진입점 —
|
||||
* 모으기(`collectQuantitySheets`) + B08 화면이 쓰는 같은 자료 읽기 · 표 짜기 함수 + 캐시 쓰기.
|
||||
* ========================================================================== */
|
||||
|
||||
export { collectQuantitySheets } from "../../B09_Estimation/draft/B09_Draft_Print_Quantity";
|
||||
export { writeState } from "../../A00_Common/b_page_state";
|
||||
export { loadSummary, summaryTable } from "../../B08_Quantity/B08_Quantity_Summary_Table";
|
||||
export {
|
||||
loadMaterials,
|
||||
materialOptions,
|
||||
materialTable,
|
||||
} from "../../B08_Quantity/B08_Quantity_Material_Tables";
|
||||
export {
|
||||
EARTHWORK_SHEETS,
|
||||
earthworkSheetOptions,
|
||||
earthworkSummaryOptions,
|
||||
HAUL_SHEETS,
|
||||
haulOptions,
|
||||
loadEarthwork,
|
||||
} from "../../B08_Quantity/B08_Quantity_Earthwork_Tables";
|
||||
export { hydroInputs, loadHydro } from "../../B08_Quantity/B08_Quantity_Hydro_Table";
|
||||
export { milestoneTable } from "../../B08_Quantity/B08_Quantity_Milestone_Table";
|
||||
export { liveMilestone, siteInfo } from "../../B08_Quantity/B08_Quantity_UI_Milestone_State";
|
||||
@@ -0,0 +1,189 @@
|
||||
"""302-3 인쇄 미리보기 「수량산출」 — B08 탭 표를 화면 없이 PrintSheet 로 모음.
|
||||
|
||||
견본 = 검증_sub8 서버 GET 응답(구조물 집계표만 손 견본 · `fixtures/b08_302_3_print_quantity.json`).
|
||||
`helper_302_3_print_quantity.mjs` 가 화면 코드를 node 로 묶어 같은 응답 · 같은 캐시로
|
||||
모으기(`collectQuantitySheets`) 와 B08 화면이 부르는 표 짜기를 나란히 냄 → 쪽 차례 · 이름 · 열 머리 · 줄 대조.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HELPER = Path(__file__).with_name("helper_302_3_print_quantity.mjs")
|
||||
FIXTURE = json.loads(
|
||||
(Path(__file__).with_name("fixtures") / "b08_302_3_print_quantity.json").read_text("utf-8")
|
||||
)
|
||||
PNG = b"\x89PNG\r\n\x1a\n302-3"
|
||||
B08 = ROOT / "B08_Quantity"
|
||||
|
||||
|
||||
def _run(responses: dict, drafts: list | None = None, screen: bool = True) -> dict:
|
||||
if shutil.which("node") is None:
|
||||
pytest.skip("node 가 없음")
|
||||
job = {
|
||||
"project": "p302",
|
||||
"responses": responses,
|
||||
"image_b64": base64.b64encode(PNG).decode(),
|
||||
"drafts": drafts or [],
|
||||
"screen": screen,
|
||||
}
|
||||
out = subprocess.run(
|
||||
["node", str(HELPER), str(ROOT)],
|
||||
input=json.dumps(job, ensure_ascii=False),
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
check=True,
|
||||
)
|
||||
return json.loads(out.stdout.strip().splitlines()[-1])
|
||||
|
||||
|
||||
def _responses() -> dict:
|
||||
data = copy.deepcopy(FIXTURE)
|
||||
routes = data["/quantity/milestone"]["data"]["routes"]
|
||||
# 경로 그림 둘 — 사용자 그림 · 서버 캡처(auto) 꼴
|
||||
data["/quantity/milestone"]["images"] = {
|
||||
routes[0]["id"]: "a.png",
|
||||
routes[2]["id"]: "auto/b.png",
|
||||
}
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def base() -> dict:
|
||||
return _run(_responses())
|
||||
|
||||
|
||||
def _same(sheet: dict, screen: dict) -> None:
|
||||
for key in ("heads", "rows", "total", "mergeSame", "frozen"):
|
||||
assert sheet[key] == screen[key], (sheet["id"], key)
|
||||
|
||||
|
||||
def test_쪽_차례_이름(base):
|
||||
stations = FIXTURE["/quantity/hydro"]["stations"]
|
||||
names = [s["name"] for s in base["sheets"]]
|
||||
head = ["구조물 집계표", "총괄자재수량집계표", "콘크리트소요자재집계표", "자재수량집계표"]
|
||||
assert names[:4] == head
|
||||
assert names[4:8] == base["screen"]["earthworkNames"] == [
|
||||
"토공집계표", "토적표", "평균운반거리", "종방향 무대운반거리"
|
||||
] # fmt: skip
|
||||
hydro = names[8:-1]
|
||||
assert len(hydro) == len(stations) == 11 # 배수지점 측점마다 한 장
|
||||
assert all(n.startswith("수리집수면적표 ") for n in hydro)
|
||||
kinds = [n for n, st in zip(hydro, stations) if st["kind"]]
|
||||
assert kinds and all(n.endswith(" 세월교") for n in kinds)
|
||||
assert names[-1] == "거리이정표"
|
||||
assert len({s["id"] for s in base["sheets"]}) == len(names)
|
||||
|
||||
|
||||
def test_표_짜기_B08_화면과_같음(base):
|
||||
sheets = {s["id"]: s for s in base["sheets"]}
|
||||
screen = base["screen"]
|
||||
_same(sheets["summary"], screen["summary"])
|
||||
for sid in ("material:material", "material:concrete", "material:quantity",
|
||||
"earthwork:summary", "earthwork:sheet"): # fmt: skip
|
||||
_same(sheets[sid], screen[sid])
|
||||
# 운반 = 화면 표 + 아래 평균운반거리 줄(번호 칸 글)
|
||||
for sid in ("earthwork:haul", "earthwork:free"):
|
||||
want = screen[sid]
|
||||
assert sheets[sid]["heads"] == want["heads"] and sheets[sid]["total"] == want["total"]
|
||||
assert sheets[sid]["rows"] == want["rows"] + [{"no": a} for a in want["averages"]]
|
||||
assert "평균운반거리" in want["averages"][0]
|
||||
_same(sheets["milestone"], {**screen["milestone"]})
|
||||
|
||||
|
||||
def test_견본_수치(base):
|
||||
sheets = {s["id"]: s for s in base["sheets"]}
|
||||
summary = sheets["summary"]
|
||||
assert summary["heads"] == ["측점", "관 공›Φ800m/m›관매설›m", "관 공›Φ800m/m›커플링›ea"]
|
||||
assert len(summary["rows"]) == 2 and summary["total"]["pipe:len|800"] == 20.5
|
||||
sheet = sheets["earthwork:sheet"]
|
||||
assert (
|
||||
len(sheet["heads"]) == 35 and len(sheet["rows"]) == 8
|
||||
) # 견본 32칸 + 측점 · 거리 · 보정량계
|
||||
assert (
|
||||
sheet["orientation"] == "landscape"
|
||||
and sheets["earthwork:summary"]["orientation"] == "portrait"
|
||||
)
|
||||
assert sheets["material:concrete"]["rows"] and len(sheets["material:concrete"]["rows"]) == 5
|
||||
hydro = next(s for s in base["sheets"] if s["id"].startswith("hydro:"))
|
||||
assert hydro["heads"] == ["구분", "항목", "값"]
|
||||
value = {r["item"]: r["value"] for r in hydro["rows"]}
|
||||
calc = FIXTURE["/quantity/hydro/calc"]
|
||||
assert value["Q : 홍수량 (㎥/s)"] == f"{calc['flow']:.2f}"
|
||||
assert value["Qd : 설계 홍수량 (㎥/s) = Q × 2.0"] == f"{calc['design_flow']:.2f}"
|
||||
assert value["R24 : 확률강우량 (mm/day)"] == f"{calc['r24_mm']:.1f}"
|
||||
# 서버 계산 = 화면과 같은 입력(기본 < 장 기본 < 연결 < 저장 < 초안)
|
||||
assert base["posts"] == base["screen"]["hydroInputs"]
|
||||
|
||||
|
||||
def test_거리이정표_지도_그림(base):
|
||||
milestone = base["sheets"][-1]
|
||||
routes = FIXTURE["/quantity/milestone"]["data"]["routes"]
|
||||
assert len(milestone["images"]) == 2
|
||||
for image, route in zip(milestone["images"], (routes[0], routes[2])):
|
||||
head, body = image["src"].split(",", 1)
|
||||
assert head == "data:image/png;base64" and base64.b64decode(body) == PNG
|
||||
assert image["caption"] == (route["name"].strip() or route["rows"][0]["item"])
|
||||
|
||||
|
||||
def test_캐시_값이면_화면에_보이는_그_값():
|
||||
responses = _responses()
|
||||
stations = responses["/quantity/hydro"]["stations"]
|
||||
key = f"{stations[0]['chainage_m']:.3f}"
|
||||
milestone = copy.deepcopy(responses["/quantity/milestone"]["data"])
|
||||
milestone["routes"][0]["name"] = "가나상회"
|
||||
got = _run(responses, [["b08-hydro", {key: {"area_km2": 0.5}}], ["b08-milestone", milestone]])
|
||||
assert got["posts"][0]["area_km2"] == 0.5 == got["screen"]["hydroInputs"][0]["area_km2"]
|
||||
hydro = next(s for s in got["sheets"] if s["id"] == f"hydro:{key}")
|
||||
assert any(r["value"].startswith("0.5 (") for r in hydro["rows"])
|
||||
sheet = got["sheets"][-1]
|
||||
assert any(r.get("company") == "가나상회" for r in sheet["rows"])
|
||||
_same(sheet, got["screen"]["milestone"])
|
||||
|
||||
|
||||
def test_영역_하나_실패_까닭_한_장():
|
||||
responses = _responses()
|
||||
del responses["/quantity/earthwork"]
|
||||
got = _run(responses, screen=False)
|
||||
earth = [s for s in got["sheets"] if s["id"].startswith("earthwork")]
|
||||
assert len(earth) == 1 and earth[0]["name"] == "토공"
|
||||
assert earth[0]["rows"][0]["note"].startswith("불러오지 못했습니다")
|
||||
assert got["sheets"][0]["name"] == "구조물 집계표" and got["sheets"][-1]["name"] == "거리이정표"
|
||||
|
||||
|
||||
def test_B08_화면이_같은_함수를_씀():
|
||||
read = lambda name: (B08 / f"B08_Quantity_{name}.ts").read_text("utf-8") # noqa: E731
|
||||
assert "summaryTable(view)" in read("UI_Summary") and "loadSummary(projectId)" in read(
|
||||
"UI_Summary"
|
||||
)
|
||||
material = read("UI_Material")
|
||||
assert "materialOptions(built)" in material and "loadMaterials(projectId)" in material
|
||||
assert "earthworkSheetOptions(table, interval)" in read("UI_Earthwork")
|
||||
sheets = read("UI_Earthwork_Sheets")
|
||||
assert "earthworkSummaryOptions(table)" in sheets and "haulOptions(groups, caption)" in sheets
|
||||
assert "hydroInputs(sheet, station, drafts)" in read("UI_Hydro")
|
||||
assert "milestoneTable(data, siteInfo()" in read("UI_Milestone")
|
||||
# 표 짜기 파일은 화면 없음
|
||||
for name in (
|
||||
"Summary_Table",
|
||||
"Material_Tables",
|
||||
"Earthwork_Tables",
|
||||
"Hydro_Table",
|
||||
"Milestone_Table",
|
||||
):
|
||||
text = read(name)
|
||||
assert "document." not in text and "createDataTable" not in text, name
|
||||
assert len(text.splitlines()) <= 700
|
||||
printer = (ROOT / "B09_Estimation/draft/B09_Draft_Print_Quantity.ts").read_text("utf-8")
|
||||
assert 'from "./B09_Draft_Print_Types"' in printer and "createDataTable" not in printer
|
||||
Reference in New Issue
Block a user