Merge remote-tracking branch 'origin/sub_laptop_2' into sub_laptop_3
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// 스프레드시트 크기 재기 — vite 로 줄여 묶은 뒤 gzip 크기(바이트). `test_spreadsheet_size.py` 가 부름.
|
||||
// node spreadsheet_size.mjs <저장소 뿌리> → {"engine": {js, css}, "total": {js, css}}
|
||||
// 앱 번들에 이미 있는 `@ui/ui_template_elements*` 는 빼고 잼(스프레드시트가 새로 더하는 몫만).
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { gzipSync } from "node:zlib";
|
||||
|
||||
const root = path.resolve(process.argv[2] ?? ".");
|
||||
const dir = path.join(root, "A00_Common", "spreadsheet");
|
||||
const { build } = await import(
|
||||
pathToFileURL(path.join(root, "config/node_modules/vite/dist/node/index.js")).href
|
||||
);
|
||||
|
||||
const ENGINE = [
|
||||
"spreadsheet_address",
|
||||
"spreadsheet_parser",
|
||||
"spreadsheet_eval",
|
||||
"spreadsheet_graph",
|
||||
"spreadsheet_refshift",
|
||||
"spreadsheet_commands",
|
||||
"spreadsheet_history",
|
||||
"spreadsheet_recalc",
|
||||
"spreadsheet_functions",
|
||||
"spreadsheet_numfmt",
|
||||
"spreadsheet_fill",
|
||||
];
|
||||
|
||||
async function measure(entries) {
|
||||
const out = await build({
|
||||
configFile: false,
|
||||
logLevel: "silent",
|
||||
root,
|
||||
resolve: { alias: { "@ui": path.join(root, "ui_template") } },
|
||||
build: {
|
||||
write: false,
|
||||
minify: true,
|
||||
target: "es2022",
|
||||
cssCodeSplit: false,
|
||||
lib: { entry: entries, formats: ["es"] },
|
||||
rollupOptions: { external: [/ui_template_elements/] },
|
||||
},
|
||||
});
|
||||
const outputs = (Array.isArray(out) ? out : [out]).flatMap((o) => o.output);
|
||||
const size = { js: 0, css: 0 };
|
||||
for (const o of outputs) {
|
||||
const body = o.type === "chunk" ? o.code : o.source;
|
||||
const gz = gzipSync(typeof body === "string" ? body : Buffer.from(body)).length;
|
||||
if (o.fileName.endsWith(".css")) size.css += gz;
|
||||
else if (o.fileName.endsWith(".js")) size.js += gz;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
const engine = await measure(ENGINE.map((name) => path.join(dir, `${name}.ts`)));
|
||||
const total = await measure([path.join(dir, "spreadsheet.ts")]);
|
||||
console.log(JSON.stringify({ engine, total }));
|
||||
@@ -0,0 +1,129 @@
|
||||
"""F — 금빛 시험: 실무 구조도 xlsx 세 벌의 수식을 새 엔진으로 풀어 엑셀 캐시값과 대조.
|
||||
|
||||
xlsx 는 **시험에서만** openpyxl 로 읽음(앱에는 가져오기 없음). 식 = 엑셀 글 그대로 · 값 = 칸 값 ·
|
||||
바깥 통합문서 참조(`[172]관집계!…`)는 식을 못 푸니 캐시값을 값으로 굳힘(대조에서 뺌).
|
||||
갈린 칸은 조용히 맞추지 않음 — 목록을 `tmp/spreadsheet_golden_report.json` 에 적고 시험을 떨굼.
|
||||
엔진(A · B)이 빈 몸이면 건너뜀 — 엔진이 차는 대로 다시 돌림.
|
||||
"""
|
||||
|
||||
import datetime as dt
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
openpyxl = pytest.importorskip("openpyxl")
|
||||
from openpyxl.utils.datetime import to_excel # noqa: E402
|
||||
|
||||
from common_util import common_util_spreadsheet as spreadsheet # noqa: E402
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
PRACTICE = ROOT / "resources" / "knowledge" / "original" / "실무문서"
|
||||
NAMES = ("07-구조도-소광리.xlsx", "5. 구조도(기번3).xlsx", "구조도(변경).xlsx")
|
||||
FILES = [next(PRACTICE.rglob(name)) for name in NAMES]
|
||||
REPORT = ROOT / "tmp" / "spreadsheet_golden_report.json"
|
||||
EXPECTED_FORMULAS = 2699
|
||||
_EXTERNAL = re.compile(r"\[\d+\]")
|
||||
_ERRORS = {"#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A"}
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def _plain(value):
|
||||
"""칸 값 → JSON(날짜 · 시각 형식 칸은 엑셀 일련수)."""
|
||||
if isinstance(value, (dt.datetime, dt.date, dt.time, dt.timedelta)):
|
||||
return to_excel(value)
|
||||
return value
|
||||
|
||||
|
||||
def _book(path: Path):
|
||||
"""xlsx → 통합문서 + 대조할 칸 `[(시트 id, A1, 캐시값)]`."""
|
||||
formulas = openpyxl.load_workbook(path, data_only=False)
|
||||
cached = openpyxl.load_workbook(path, data_only=True)
|
||||
sheets, checks = [], []
|
||||
for i, ws in enumerate(formulas.worksheets, start=1):
|
||||
sid, cells = f"s{i}", {}
|
||||
values = cached[ws.title]
|
||||
for row in ws.iter_rows():
|
||||
for c in row:
|
||||
if c.value is None:
|
||||
continue
|
||||
keep = _plain(values[c.coordinate].value)
|
||||
if c.data_type == "f" and isinstance(c.value, str):
|
||||
if _EXTERNAL.search(c.value):
|
||||
if keep is not None:
|
||||
cells[c.coordinate] = {"값": keep}
|
||||
continue
|
||||
cells[c.coordinate] = {"식": c.value[1:]}
|
||||
checks.append((sid, c.coordinate, keep))
|
||||
elif c.data_type != "f":
|
||||
cells[c.coordinate] = {"값": _plain(c.value)}
|
||||
sheets.append({"id": sid, "이름": ws.title, "칸": cells})
|
||||
book = {"종류": "통합문서", "판": 1, "열": path.stem, "서식": [{}], "시트": sheets}
|
||||
return book, checks
|
||||
|
||||
|
||||
def _same(want, got) -> bool:
|
||||
if got is None:
|
||||
return False
|
||||
if isinstance(want, bool):
|
||||
return got.get("참") is want
|
||||
if isinstance(want, (int, float)):
|
||||
try:
|
||||
a, b = Decimal(got["수"]), Decimal(repr(float(want)))
|
||||
except (KeyError, InvalidOperation):
|
||||
return False
|
||||
return abs(a - b) <= Decimal("1e-9") * max(Decimal(1), abs(b))
|
||||
if want is None or want == "":
|
||||
return got.get("글") == "" or got.get("수") == "0"
|
||||
if isinstance(want, str) and want in _ERRORS:
|
||||
return got.get("오류") == want
|
||||
return got.get("글") == want
|
||||
|
||||
|
||||
def test_실무_구조도_수식_엑셀과_같음():
|
||||
loaded = [(path, *_book(path)) for path in FILES]
|
||||
total = sum(len(checks) for _p, _b, checks in loaded)
|
||||
external = 6 # 소광리 `[172]관집계` — 값으로 굳혀 대조에서 뺌
|
||||
assert total + external == EXPECTED_FORMULAS
|
||||
try:
|
||||
results = spreadsheet.recalc_workbooks([book for _p, book, _c in loaded])
|
||||
except spreadsheet.RecalcError as error:
|
||||
if "아직 없음" in str(error):
|
||||
pytest.skip(f"엔진 아직 — {error}")
|
||||
raise
|
||||
mismatches = []
|
||||
for (path, book, checks), values in zip(loaded, results, strict=True):
|
||||
names = {s["id"]: s["이름"] for s in book["시트"]}
|
||||
for sid, a1, want in checks:
|
||||
got = values.get(sid, {}).get(a1)
|
||||
if not _same(want, got):
|
||||
formula = next(s for s in book["시트"] if s["id"] == sid)["칸"][a1]["식"]
|
||||
mismatches.append(
|
||||
{
|
||||
"파일": path.name,
|
||||
"시트": names[sid],
|
||||
"칸": a1,
|
||||
"식": formula,
|
||||
"엑셀": want,
|
||||
"엔진": got,
|
||||
}
|
||||
)
|
||||
REPORT.parent.mkdir(exist_ok=True)
|
||||
REPORT.write_text(
|
||||
json.dumps(
|
||||
{"대조": total, "갈림": len(mismatches), "목록": mismatches},
|
||||
ensure_ascii=False,
|
||||
indent=1,
|
||||
default=str,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
head = "\n".join(
|
||||
f"{m['파일']} {m['시트']}!{m['칸']} ={m['식']} 엑셀 {m['엑셀']!r} · 엔진 {m['엔진']}"
|
||||
for m in mismatches[:30]
|
||||
)
|
||||
assert not mismatches, f"{len(mismatches)}/{total} 칸 갈림 — 전체 {REPORT}\n{head}"
|
||||
@@ -0,0 +1,37 @@
|
||||
"""F — 서버 재계산 다리: 파이썬 → Node 번들(`build:spreadsheet`) → 화면과 같은 TS 엔진.
|
||||
|
||||
엔진(A · B)이 아직 빈 몸이면 `아직 없음` 까닭이 그대로 올라와 저장을 막는지 봄 ·
|
||||
엔진이 차면 저장 견본(`spreadsheet_sample.json`)의 `계산값` 과 서버 풀이가 같은지 봄.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from common_util import common_util_spreadsheet as spreadsheet
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SAMPLE = ROOT / "A00_Common" / "spreadsheet" / "spreadsheet_sample.json"
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def test_견본_계산값_같음():
|
||||
sample = json.loads(SAMPLE.read_text(encoding="utf-8"))
|
||||
try:
|
||||
saved = spreadsheet.with_server_values({**sample, "계산값": {"s1": {"H4": {"수": "틀림"}}}})
|
||||
except spreadsheet.RecalcError as error:
|
||||
assert "아직 없음" in str(error), error # 엔진이 빈 몸일 때만 봐줌
|
||||
pytest.skip(f"엔진 아직 — {error}")
|
||||
assert saved["계산값"] == sample["계산값"]
|
||||
assert {k: v for k, v in saved.items() if k != "계산값"} == {
|
||||
k: v for k, v in sample.items() if k != "계산값"
|
||||
}
|
||||
|
||||
|
||||
def test_못_푸는_문서는_까닭과_함께_막음():
|
||||
with pytest.raises(spreadsheet.RecalcError) as caught:
|
||||
spreadsheet.recalc_workbooks([{"종류": "통합문서", "시트": "깨짐"}])
|
||||
assert "스프레드시트 계산 실패" in str(caught.value)
|
||||
@@ -0,0 +1,74 @@
|
||||
"""F — 스프레드시트 크기 예산 · M02 만 늦게 불러옴(계약 `spreadsheet_types.ts` 머리).
|
||||
|
||||
예산(압축 gzip 뒤) — 엔진 A+B 22 KB · 화면 C+D+E 28 KB · css 4 KB · 합 54 KB.
|
||||
엔진은 파일마다 따로 묶어 더하므로 실제보다 조금 크게 잼(넘치면 넘친 것).
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPT = Path(__file__).with_name("spreadsheet_size.mjs")
|
||||
KB = 1024
|
||||
BUDGET = {"engine": 22 * KB, "view": 28 * KB, "css": 4 * KB, "total": 54 * KB}
|
||||
|
||||
pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
|
||||
|
||||
|
||||
def test_크기_예산():
|
||||
out = subprocess.run(
|
||||
["node", str(SCRIPT), str(ROOT)],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
timeout=300,
|
||||
check=True,
|
||||
)
|
||||
size = json.loads(out.stdout.strip().splitlines()[-1])
|
||||
engine, total = size["engine"]["js"], size["total"]
|
||||
view = max(0, total["js"] - engine)
|
||||
print(
|
||||
f"gzip — 엔진 {engine} · 화면 {view} · css {total['css']} · 합 {total['js'] + total['css']}"
|
||||
)
|
||||
assert engine <= BUDGET["engine"], f"엔진 {engine} B > {BUDGET['engine']}"
|
||||
assert view <= BUDGET["view"], f"화면 {view} B > {BUDGET['view']}"
|
||||
assert total["css"] <= BUDGET["css"], f"css {total['css']} B > {BUDGET['css']}"
|
||||
assert total["js"] + total["css"] <= BUDGET["total"]
|
||||
|
||||
|
||||
# 정적 import 금지 — 타입만(`import type`) · 늦게(`import(`) 는 됨
|
||||
_STATIC = re.compile(
|
||||
r"^\s*import\s+(?!type\b)[^;]*?from\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M
|
||||
)
|
||||
_SIDE = re.compile(r"^\s*import\s+[\"'][^\"']*spreadsheet/spreadsheet", re.M)
|
||||
|
||||
|
||||
def test_M02_만_늦게_불러옴():
|
||||
offenders, users = [], []
|
||||
listed = subprocess.run(
|
||||
["git", "ls-files", "--cached", "--others", "--exclude-standard", "*.ts"],
|
||||
cwd=ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
check=True,
|
||||
).stdout.splitlines()
|
||||
for rel in listed:
|
||||
if rel.startswith("A00_Common/spreadsheet/") or not (ROOT / rel).is_file():
|
||||
continue
|
||||
text = (ROOT / rel).read_text(encoding="utf-8", errors="replace")
|
||||
if "spreadsheet/spreadsheet" not in text:
|
||||
continue
|
||||
if _STATIC.search(text) or _SIDE.search(text):
|
||||
offenders.append(rel)
|
||||
if "import(" in text:
|
||||
users.append(rel)
|
||||
# 서버 Node 진입만 정적(브라우저 번들 밖)
|
||||
assert offenders == ["common_util/common_util_spreadsheet_node.ts"], offenders
|
||||
assert users and all(u.startswith("M02_MasterTemplete/") for u in users), users
|
||||
@@ -0,0 +1,138 @@
|
||||
"""F — 산출근거 저장(basis 종류) · 옛 산출근거 옮김 · [저장] 때 서버 계산값 · 임시 시험 주소.
|
||||
|
||||
서버 Node 풀이는 `test_spreadsheet_node.py` 가 따로 봄 — 여기는 `with_server_values` 를 바꿔 끼워
|
||||
「브라우저 값은 버리고 서버 값만 적는다」 · 「못 풀면 저장을 막는다」 만 잼.
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from common_util import common_util_spreadsheet as spreadsheet
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Router as router_module
|
||||
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||
|
||||
REAL = store.FOLDER
|
||||
SERVER = {"s1": {"A1": {"수": "42"}}}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path, monkeypatch):
|
||||
for kind in store.KINDS:
|
||||
(tmp_path / kind).mkdir()
|
||||
shutil.copy(REAL / "table/구조물집계표.json", tmp_path / "table")
|
||||
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
||||
monkeypatch.setattr(router_module, "TRIAL_PATH", tmp_path / "trial.json")
|
||||
|
||||
def fake(book):
|
||||
clean = {k: v for k, v in book.items() if k != "계산값"}
|
||||
return {**clean, "계산값": SERVER}
|
||||
|
||||
monkeypatch.setattr(spreadsheet, "with_server_values", fake)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _old_structure(folder, column, basis):
|
||||
doc = {"양식": "구조물도면", "종류": "structure", "판": 1, "열": column, "도번": "구-01"}
|
||||
doc.update({"도면": {"format": 6, "entities": []}, "산출근거": basis})
|
||||
(folder / "structure" / f"{column}.json").write_text(
|
||||
json.dumps(doc, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def test_옛_산출근거를_따로_파일로_옮김(client, tmp_path):
|
||||
old = {
|
||||
"열": [
|
||||
{"id": "work", "머리": ["공종"]},
|
||||
{"id": "detail", "머리": ["산출", "내역"]},
|
||||
{"id": "qty", "머리": ["수량(m당)"]},
|
||||
],
|
||||
"줄": [{"id": "1", "값": {"work": "터파기", "qty": 1.5}}, {"id": "2", "값": {}}],
|
||||
"보기": {"열너비": {"detail": 210}},
|
||||
}
|
||||
_old_structure(tmp_path, "pp_len", old)
|
||||
done = store.migrate_basis()
|
||||
assert done["옮김"] == ["pp_len"]
|
||||
columns = store._column_ids()
|
||||
assert set(done["새로"]) == set(columns) - {"pp_len"}
|
||||
structure = json.loads((tmp_path / "structure/pp_len.json").read_text(encoding="utf-8"))
|
||||
assert "산출근거" not in structure and structure["도번"] == "구-01"
|
||||
basis = store.read("basis", "pp_len")["문서"]
|
||||
sheet = basis["시트"][0]
|
||||
assert (basis["종류"], basis["열"], sheet["id"]) == ("통합문서", "pp_len", "s1")
|
||||
assert sheet["칸"] == {
|
||||
"A1": {"값": "공종", "서식": 1},
|
||||
"B1": {"값": "산출 내역", "서식": 1},
|
||||
"C1": {"값": "수량(m당)", "서식": 1},
|
||||
"A2": {"값": "터파기"},
|
||||
"C2": {"값": 1.5},
|
||||
}
|
||||
assert sheet["열"] == {"B": {"폭": 30.0}}
|
||||
assert "계산값" not in basis # 식 없는 새 문서 — 첫 [저장] 때 서버가 적음
|
||||
# 두 번 돌려도 같음
|
||||
before = {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()}
|
||||
assert store.migrate_basis() == {"옮김": [], "새로": []}
|
||||
assert before == {p.name: p.read_bytes() for p in (tmp_path / "basis").iterdir()}
|
||||
|
||||
|
||||
def test_새_구조물_도면은_산출근거_따로(client):
|
||||
made = client.post("/api/m02/structures/rv").json()["문서"]
|
||||
assert "산출근거" not in made
|
||||
basis = client.get("/api/m02/templates/basis/rv").json()["문서"]
|
||||
heads = [basis["시트"][0]["칸"][f"{c}1"]["값"] for c in "ABCDE"]
|
||||
assert heads == ["공종", "규격", "산출 내역", "단위", "수량(m당)"]
|
||||
|
||||
|
||||
def test_저장은_서버_계산값만_적음(client):
|
||||
store.ensure_basis("rv")
|
||||
got = client.get("/api/m02/templates/basis/rv").json()
|
||||
doc = {**got["문서"], "계산값": {"s1": {"A1": {"수": "브라우저"}}}}
|
||||
saved = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc})
|
||||
assert saved.status_code == 200
|
||||
assert client.get("/api/m02/templates/basis/rv").json()["문서"]["계산값"] == SERVER
|
||||
# 뼈대 없는 문서 · 판이 다르면 막음
|
||||
bad = client.put("/api/m02/templates/basis/rv", json={"판": saved.json()["판"], "문서": {}})
|
||||
assert bad.status_code == 400
|
||||
stale = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": doc})
|
||||
assert stale.status_code == 409
|
||||
|
||||
|
||||
def test_못_풀면_저장을_막음(client, monkeypatch):
|
||||
store.ensure_basis("rv")
|
||||
got = client.get("/api/m02/templates/basis/rv").json()
|
||||
|
||||
def broken(_book):
|
||||
raise spreadsheet.RecalcError("스프레드시트 계산 실패 — 시험")
|
||||
|
||||
monkeypatch.setattr(spreadsheet, "with_server_values", broken)
|
||||
r = client.put("/api/m02/templates/basis/rv", json={"판": got["판"], "문서": got["문서"]})
|
||||
assert r.status_code == 503
|
||||
assert client.get("/api/m02/templates/basis/rv").json()["판"] == got["판"]
|
||||
|
||||
|
||||
def test_층_규칙과_프로젝트_복사(client, tmp_path, monkeypatch):
|
||||
store.ensure_basis("rv")
|
||||
doc = store.read("basis", "rv")["문서"]
|
||||
layers.check_skeleton("basis", doc)
|
||||
for bad in ({}, {**doc, "시트": {}}):
|
||||
with pytest.raises(ValueError):
|
||||
layers.check_skeleton("basis", bad)
|
||||
layers.check_skeleton("structure", {"도면": {"entities": []}}) # 산출근거 없어도 됨
|
||||
monkeypatch.setattr(layers, "SYSTEM_ROOT", tmp_path)
|
||||
copied = layers.seed_project(tmp_path / "project")
|
||||
assert "basis/rv" in copied["작업본"] and "basis/rv" in copied["초기"]
|
||||
|
||||
|
||||
def test_임시_시험_주소(client):
|
||||
first = client.get("/api/m02/spreadsheet-trial").json()["문서"]
|
||||
assert [s["이름"] for s in first["시트"]] == ["시트1", "시트2", "시트3"]
|
||||
first["시트"][0]["칸"] = {"A1": {"식": "1+41"}}
|
||||
saved = client.put("/api/m02/spreadsheet-trial", json={"문서": first}).json()["문서"]
|
||||
assert saved["계산값"] == SERVER
|
||||
assert client.get("/api/m02/spreadsheet-trial").json()["문서"] == saved
|
||||
@@ -1,4 +1,5 @@
|
||||
"""M02 구조물 도면(structure) — 새로 · 도번 자동 · 409 · 저장 · 지운 뒤 번호 · 프로젝트 복사."""
|
||||
"""M02 구조물 도면(structure) — 새로 · 도번 자동 · 409 · 저장 · 지운 뒤 번호 · 프로젝트 복사.
|
||||
산출근거는 따로 파일(basis) — `resources/tester/spreadsheet/test_spreadsheet_store.py`."""
|
||||
|
||||
import shutil
|
||||
|
||||
@@ -36,11 +37,9 @@ def test_새로_도번_자동_409_없는_열(client):
|
||||
)
|
||||
# 도각 없는 전용 화면 — 그린 만큼이 설계 영역
|
||||
assert doc["도면"] == {"format": 6, "entities": []}
|
||||
basis = doc["산출근거"]
|
||||
assert [c["머리"][0] for c in basis["열"]] == ["공종", "규격", "산출 내역", "단위", "수량(m당)"]
|
||||
# 빈 줄 몇 개를 미리 둠 — 줄 더하기 안 눌러도 바로 입력
|
||||
assert basis["줄"] == [{"id": "1", "값": {}}, {"id": "2", "값": {}}, {"id": "3", "값": {}}]
|
||||
assert basis["종류"] == "표"
|
||||
# 산출근거는 구조물 문서 밖 — 같은 열 id 로 따로 한 통합문서
|
||||
assert "산출근거" not in doc
|
||||
assert client.get("/api/m02/templates/basis/pp_len").json()["문서"]["종류"] == "통합문서"
|
||||
|
||||
assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "구-02"
|
||||
assert client.post("/api/m02/structures/pp_len").status_code == 409
|
||||
@@ -56,10 +55,10 @@ def test_새로_도번_자동_409_없는_열(client):
|
||||
def test_저장_뼈대_거름(client):
|
||||
got = client.post("/api/m02/structures/pp_len").json()
|
||||
doc = got["문서"]
|
||||
doc["산출근거"]["줄"] = [{"id": "r1", "값": {"work": "관 부설"}}]
|
||||
doc["도면"]["entities"] = [{"type": "line"}]
|
||||
saved = client.put("/api/m02/templates/structure/pp_len", json={"판": got["판"], "문서": doc})
|
||||
assert saved.status_code == 200 and saved.json()["판"] != got["판"]
|
||||
for bad in ({}, {**doc, "도면": {}}, {**doc, "산출근거": {"열": "x"}}, {**doc, "도면": []}):
|
||||
for bad in ({}, {**doc, "도면": {}}, {**doc, "도면": {"entities": "x"}}, {**doc, "도면": []}):
|
||||
r = client.put(
|
||||
"/api/m02/templates/structure/pp_len", json={"판": saved.json()["판"], "문서": bad}
|
||||
)
|
||||
@@ -90,4 +89,4 @@ def test_프로젝트_복사와_층_저장에_실림(client, tmp_path, monkeypat
|
||||
assert ("structure", "pp_len") in {(r["종류"], r["이름"]) for r in layers.list_templates(work)}
|
||||
layers.check_skeleton("structure", doc)
|
||||
with pytest.raises(ValueError):
|
||||
layers.check_skeleton("structure", {**doc, "산출근거": None})
|
||||
layers.check_skeleton("structure", {**doc, "도면": None})
|
||||
|
||||
@@ -46,11 +46,11 @@ def test_소스가_700줄을_넘지_않음():
|
||||
["createStructure", "fetchStructureNumbers", "readTemplate", "saveTemplate"],
|
||||
),
|
||||
("M02_MasterTemplete_Drawing.ts", ["mountDrawingTemplate"]),
|
||||
("../ui_template/sheet/ui_template_sheet.ts", ["createSheet"]),
|
||||
("../A00_Common/spreadsheet/spreadsheet.ts", ["createSpreadsheet"]),
|
||||
],
|
||||
)
|
||||
def test_컨테이너가_쓰는_이름이_내보내진_것(source: str, names: list[str]):
|
||||
"""sub2·sub3 가 이름을 바꾸면 여기서 먼저 걸림."""
|
||||
"""도면 부품 · 스프레드시트(D) 가 이름을 바꾸면 여기서 먼저 걸림."""
|
||||
body = _text(M02 / source)
|
||||
panel = _text(PANEL)
|
||||
for name in names:
|
||||
|
||||
Reference in New Issue
Block a user