Merge remote-tracking branch 'origin/dev' into sub_laptop_5

This commit is contained in:
2026-11-01 23:05:18 +09:00
86 changed files with 4143 additions and 957 deletions
@@ -96,7 +96,8 @@ def test_참조_줄_재료_연료_뺌_고른_품목(tmp_path, monkeypatch):
price = {"묶음": {"줄": [{"키": "MT9", "이름": "레디믹스트 레미콘", "규격": "25-18-80"}]}}
frozen = {
ref_id(SC): {
"담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP}],
# 굳힌 옛 사본 = 안 줄 손 다 있음(723-1 앞 · 빈 수량 = 이은 칸 값 · 없으면 안 셈)
"담은로직": [{"로직": "L1", "수량": {"손": 2}}, {"참조": SP, "수량": {"손": 1}}],
"알약": [{"로직": ["L1"], "배": 10}],
},
ref_id(SP): {"담은로직": [{"로직": "L2"}], "배": 4},
+54 -25
View File
@@ -2,7 +2,7 @@
175-1 화면 · 저장 — 서고 모드 자재목록 · 로직 자재 세 칸 막힘 풂 · 저장 `_combo_lines` 가
고름 · 자재표시 남김 · 문서 `자재`(일위대가 자재 검사 · 로직 자재와 같은 품목 400).
175-2 셈 — 담은 자재(손 · 빈칸 = 안 셈)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄
175-2 셈 — 담은 자재(수량 = 이은 알약 값 · 723-1 · 손은 저장 안 함)가 사전조합 1 단위 값(`Expand._combo` · 안 줄) · B08 참조 줄
재료(`_ref_mats`) · B09 겹 대가 자재 줄 · 프로젝트 사본 단가 묶음(`frozen_materials`)에 들어감.
"""
@@ -16,7 +16,7 @@ from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture
from resources.tester.test_m02_labor_ref import _area, world # noqa: F401 — 임시 층 fixture
HERE = os.path.dirname(os.path.abspath(__file__))
SRC = os.path.join(HERE, "..", "..", "M02_MasterTemplete")
@@ -35,11 +35,17 @@ def _save(folder, doc):
def test_화면_서고_모드도_자재목록_세_칸_자재_저장(world): # noqa: F811
detail = open(os.path.join(SRC, "M02_MasterTemplete_Labor_Detail.ts"), encoding="utf-8").read()
assert 'section(tc("Labor_Mats"), opt.card ? null : addMat, matHost),' in detail
assert 'lib ? [] : [section(tc("Labor_Mats")' not in detail
assert "mark: markLogicMat," in detail and "lib ? undefined" not in detail
assert "delete out.자재" not in detail and "delete out.열" in detail
"""723-2 — 서고 = 일위대가표만(`Labor_Table`) · [자재 추가] 머리 · 세 칸 = 줄 모달(`Labor_RowModal`)."""
read = lambda name: open(os.path.join(SRC, name), encoding="utf-8").read() # noqa: E731
detail, lib = (
read("M02_MasterTemplete_Labor_Detail.ts"),
read("M02_MasterTemplete_Labor_Table.ts"),
)
modal = read("M02_MasterTemplete_Labor_RowModal.ts")
assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail
assert 'className: "ui-btn-group", children: [addLogic, addMat]' in lib
assert "setLogicMark(row, at, key, v);" in modal and "setMark(o.mat, key, v);" in modal
assert "delete out.자재" not in lib and "delete out.열" in lib
def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811
@@ -51,7 +57,7 @@ def test_저장_왕복_고름_자재표시_자재_남음(world): # noqa: F811
_, got = _save(world["system"], {"담은로직": [line], "자재": mats})
one = got["담은로직"][0]
assert one["고름"] == {"4": NET_ITEM} and one["자재표시"] == {"3": {"자재구분": "관급"}}
assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, {"손": 2}), (WIRE, None)]
assert [(m["코드"], m["수량"]) for m in got["자재"]] == [(MAT, None), (WIRE, None)] # 723-1
assert got["자재"][0]["운반비"] == "미포함" and got["자재"][0]["이름"] != "틀린 이름"
# 자재 없음 = 빈 목록(옛 문서 꼴 그대로 읽힘)
_, empty = _save(world["system"], {"담은로직": [{"로직": NET}]})
@@ -73,8 +79,23 @@ def _price(code):
return Decimal(str(labor.material_price(whole, code, None)["기준단가"]))
def _given(key):
"""안 줄 알약 값(723-1) — STONE 1 · NET 0 · MAT 2 · WIRE 0."""
rid = refs.ref_id({**SYS, "키": key})
return {f"{rid}#0": 1, f"{rid}#1": 0, f"{rid}#자재|{MAT}": 2, f"{rid}#자재|{WIRE}": 0}
def _legacy(world, key): # noqa: F811
"""옛 손 수량 문서(723-1 앞 · 굳힌 사본 꼴) — B08 · B09 셈 길 시험용 · 파일은 안 바꿈."""
doc = json.loads((world["system"] / "precombo" / f"{key}.json").read_bytes())
hands = ({"손": 1}, {"손": 0})
lines = [{**one, "수량": q} for one, q in zip(doc["담은로직"], hands)]
mats = [{**m, "수량": {"손": 2} if m["코드"] == MAT else None} for m in doc["자재"]]
return {**doc, "담은로직": lines, "자재": mats}
def _world_combo(world): # noqa: F811
"""STONE 1 + MAT 손 2 + WIRE 빈칸(안 셈) · NET 고름 4 = NET_ITEM — (키, 손셈 1 단위 값)."""
"""STONE 1 + MAT 2 + WIRE 0 · NET 고름 4 = NET_ITEM — (키, 1 단위 값) · 수량은 `_given`."""
lines = [
{"로직": STONE, "수량": {"손": 1}},
{"로직": NET, "수량": {"손": 0}, "고름": {"4": NET_ITEM}},
@@ -95,34 +116,42 @@ def test_사전조합_값에_자재_금액_빈칸은_안_셈_안_줄에_자재(w
doc = labor.clean(
{"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}]}
)
one = labor.calc(doc, {})["줄"][0]
bare = labor.calc(doc, {})["줄"][0]
one = labor.calc(doc, _given(key))["줄"][0]
assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 723-1 — 안 이으면 값 없음
assert one["기준"]["계"] == pytest.approx(float(want))
assert one["반영"]["계"] == pytest.approx(float(want * 3))
mats = [x for x in one["안"] if x.get("자재")]
assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE 빈칸 = 안 셈
assert [(x["자재"], x["수량"]) for x in mats] == [(MAT, 2)] # WIRE = 기본 별도계상(안 셈)
assert mats[0]["기준"]["재료비"] == pytest.approx(float(_price(MAT)))
# 목록 기준금액(51-4)도 같은 펼침
# 목록 기준금액(51-4) — 이은 값 없음 = 빈 값(723-1)
rows = [{"종류": "precombo", "이름": key}]
refs.summaries(world["system"], rows)
assert rows[0]["기준금액"] == pytest.approx(float(want))
assert rows[0]["기준금액"] is None
def test_B08_참조_줄_재료에_사전조합_자재_손_곱(world): # noqa: F811
from B08_Quantity.B08_Quantity_Router_Material import _ref_mats
key, _ = _world_combo(world)
with store.at(world["system"]):
outer = refs.create(
"precombo",
{
"이름": "겹",
"단위": "㎡",
"담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}],
},
)["이름"]
got = _ref_mats(m01.loaded()[1], refs.read_ref, {**SYS, "키": outer})
outer = {
"이름": "겹",
"단위": "㎡",
"담은로직": [{"참조": {**SYS, "키": key}, "수량": {"손": 3}}],
}
docs = {"SC-겹": outer, key: _legacy(world, key)} # 옛 손 문서(723-1 앞 꼴 · B08 길만 잼)
got = _ref_mats(m01.loaded()[1], lambda r: docs.get(r["키"]), {**SYS, "키": "SC-겹"})
mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")]
assert mats == [(MAT, pytest.approx(6.0))] # 2 × 겹 손 3 · WIRE 빈칸 없음
# 723-1 — 수량 없는 새 문서 = 면적 알약 값(given)으로 · 안 이으면 안 셈
live = {"SC-겹": outer, key: refs.read_ref({**SYS, "키": key})}
bare = _ref_mats(m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"})
assert [m for m, _ in bare if m.get("자재")] == []
got = _ref_mats(
m01.loaded()[1], lambda r: live.get(r["키"]), {**SYS, "키": "SC-겹"}, given=_given(key)
)
mats = [(m["품목"]["키"], q) for m, q in got if m.get("자재")]
assert mats == [(MAT, pytest.approx(6.0))]
def test_B09_겹_대가에_자재_줄(world): # noqa: F811
@@ -131,7 +160,7 @@ def test_B09_겹_대가에_자재_줄(world): # noqa: F811
key, _ = _world_combo(world)
ref = {**SYS, "키": key}
build = Builder()
bid = build.precombo(m01.loaded()[1], ref, refs.read_ref(ref), {})
bid = build.precombo(m01.loaded()[1], ref, _legacy(world, key), {}) # 옛 손 문서(723-1 앞)
mats = [x for x in build.blocks[bid]["lines"] if x["kind"] == "material"]
assert [(x["key"], x["qty"]) for x in mats] == [(MAT, 2.0), (WIRE, None)]
assert mats[0]["name"] and mats[0]["unit"]
@@ -150,5 +179,5 @@ def test_사본_단가_묶음에_사전조합_자재_고름_사본_값_같음(wo
assert "refs.frozen_materials(" in open(source.__file__, encoding="utf-8").read()
bundle = prices.to_json(prices.collect(refs.frozen_logics(rows["참조사본"]), mats))
(world["system"] / "precombo" / f"{key}.json").unlink() # 원본이 사라져도 굳힌 사본
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
got = prices.calc(rows, bundle, {}, {}, _area(_given(key)))["줄"][0] # 사본 면적 칸 = 안 줄 값
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
@@ -27,7 +27,7 @@ if str(ROOT) not in sys.path:
from B09_Estimation import B09_Estimation_Bill_Library as library # noqa: E402
from B09_Estimation import B09_Estimation_Router as router # noqa: E402
from B09_Estimation import B09_Estimation_Store as store_b09 # noqa: E402
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs # noqa: E402
from B09_Estimation.B09_Estimation_Labor_Source import ASK, daega_inputs # noqa: E402
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys # noqa: E402
from config import config_system # noqa: E402
from M02_MasterTemplete import M02_Labor_Ref as refs # noqa: E402
@@ -62,7 +62,9 @@ def world(tmp_path, monkeypatch):
company = layers.company_dir(7)
company.mkdir(parents=True)
with store.at(company):
other = refs.create("precombo", {"이름": "회사 조합", "단위": "㎡", "담은로직": lines[:1]})["이름"]
other = refs.create("precombo", {"이름": "회사 조합", "단위": "㎡", "담은로직": lines[:1]})[
"이름"
]
project = tmp_path / "storage" / "7" / "1" / "proj"
project.mkdir(parents=True)
return {"system": system, "key": key, "company": other, "root": project}
@@ -84,7 +86,10 @@ def test_사전조합_고름_사본_들여옴_새_호표_다시_고르면_재사
got, block = _block(root, folder)
assert (block["kind"], block["name"], block["unit"]) == ("사전조합", "서고 조합", "㎡")
kinds = [(ln["kind"], ln.get("qty")) for ln in block["lines"]]
assert kinds == [("sangeun", 2.0), ("sangeun", 1.0), ("material", 3.0)]
# 723-1 · 723-4 = M02 SC 안 줄 수량 저장 안 함 → 내역에서 바로 쓴 SC = 수량 없음(이 공사에서 입력)
assert kinds == [("sangeun", None), ("sangeun", None), ("material", None)]
assert all(ln["ask"] == ASK for ln in block["lines"])
assert block["cover"] == f"C:{block['origin']['참조']}" # 공사 안 한 덮개 그대로
assert got["owners"] == {} # 구조물 사본 없어도 호표
# 단가 묶음 = 담은 로직 · 사전조합 자재 · 고른 자재(사본 셈이 마스터를 안 읽음)
bundle = json.loads((copy / "단가.json").read_text("utf-8"))
+155
View File
@@ -0,0 +1,155 @@
"""640 라이다 기본 고름 · implicit 점 수 · 격자 칸 뽑기 · 지문 · 틈 닫기 (요구 558).
서버 함수만 — 작은 합성 점군 · 임시 폴더.
"""
from pathlib import Path
import numpy as np
from B04_PreProcess.B04_PreProcess_Engine_Filter_Classification import (
classified_coverage,
has_classified_coverage,
)
from B04_PreProcess.B04_PreProcess_Engine_Ground import resolve_auto_source_filters
from B04_PreProcess.B04_PreProcess_Engine_ModelBuild import implicit_point_count
from B04_PreProcess.B04_PreProcess_Engine_ModelContext import TerrainContext
from B04_PreProcess.B04_PreProcess_Engine_Pipeline import build_all_terrain_models
from config import config_system as cfg
def _plane(size_m: float = 60.0, spacing_m: float = 0.5) -> np.ndarray:
axis = np.arange(0.0, size_m, spacing_m)
x, y = np.meshgrid(axis, axis)
z = 100.0 + 0.2 * x + 0.1 * np.sin(y / 5.0)
return np.column_stack([x.ravel(), y.ravel(), z.ravel()])
def _structured(points: np.ndarray, classification: np.ndarray | None = None) -> dict:
data = {"xyz": points, "bounds": np.array([points.min(axis=0), points.max(axis=0)]).T}
if classification is not None:
data["classification"] = classification
return data
def test_확정_기본값은_implicit_평활_끔() -> None:
assert cfg.SURFACE_CONFIRM_DEFAULT_METHOD == "implicit"
assert cfg.SURFACE_CONFIRM_DEFAULT_SMOOTH is False
assert cfg.SURFACE_AUTO_METHODS == ("dtm",) # D03 자체 라이다는 dtm 만 그대로
def test_일부만_분류된_LAS_는_자동_기본에서_csf() -> None:
points = _plane(100.0)
partial = np.ones(len(points), dtype=np.uint8)
partial[(points[:, 0] < 30) & (np.arange(len(points)) % 4 == 0)] = 2 # 비율 7 % · 덮임 30 %
assert classified_coverage(_structured(points, partial)) < 0.5
assert resolve_auto_source_filters(_structured(points, partial)) == ["csf"]
spread = np.ones(len(points), dtype=np.uint8)
spread[::37] = 2 # 비율 2.7 % · 덮임 100 %
assert classified_coverage(_structured(points, spread)) == 1.0
assert resolve_auto_source_filters(_structured(points, spread)) == ["classification", "csf"]
def test_덮임은_저장본을_다시_씀(tmp_path: Path) -> None:
points = _plane(40.0)
classes = np.full(len(points), 2, dtype=np.uint8)
(tmp_path / "structured.npz").write_bytes(b"x")
assert has_classified_coverage(_structured(points, classes), tmp_path)
assert (tmp_path / "classification_coverage.json").is_file()
class _NoXyz(dict):
def __getitem__(self, key): # type: ignore[no-untyped-def]
assert key != "xyz", "저장본이 맞으면 점군을 풀지 않아야 함"
return dict.__getitem__(self, key)
assert has_classified_coverage(_NoXyz(_structured(points, classes)), tmp_path)
def _config(**override: object) -> dict:
config = cfg.build_surface_model_config()
config.update({"source_filters": ["a", "b"], "precompute": ["dtm", "implicit"]})
config.update(override)
return config
def test_implicit_은_기본_필터에만(tmp_path: Path) -> None:
points = _plane()
data = _structured(points)
masks = {"a": np.ones(len(points), bool), "b": np.arange(len(points)) % 2 == 0}
manifest = build_all_terrain_models(data, masks, tmp_path, _config())
assert set(manifest["source_filters"]["a"]["methods"]) == {"dtm", "implicit"}
assert set(manifest["source_filters"]["b"]["methods"]) == {"dtm"}
assert not (tmp_path / "implicit_b.npz").exists()
def test_implicit_점_수_비율_하한_상한() -> None:
config = cfg.build_surface_model_config()
assert (config["implicit_point_ratio"], config["implicit_min_points"]) == (0.175, 50_000)
assert config["implicit_max_points"] == 2_000_000
assert implicit_point_count(1_140_716, config) == 199_625 # 용화 분류
assert implicit_point_count(768_273, config) == 134_448 # 용화 CSF
assert implicit_point_count(100_000, config) == 50_000 # 하한
assert implicit_point_count(30_000, config) == 30_000 # 하한보다 적으면 전부
assert implicit_point_count(50_000_000, config) == 2_000_000 # 상한
def test_격자_칸_뽑기는_고르고_씨앗_고정() -> None:
rng = np.random.default_rng(1)
dense = rng.uniform(0, 20, (200_000, 2)) # 왼쪽 아래 트인 곳 — 점 몰림
sparse = rng.uniform(0, 100, (30_000, 2)) # 전체 성긴 숲 밑
xy = np.vstack([dense, sparse])
points = np.column_stack([xy, np.zeros(len(xy))])
context = TerrainContext(
xyz=points,
mask=np.ones(len(points), bool),
bounds=np.array([points.min(axis=0), points.max(axis=0)]).T,
config=cfg.build_surface_model_config(),
)
picked = context.grid_sample(5_000)
assert len(picked) == 5_000 and len(np.unique(picked[:, :2], axis=0)) == 5_000
assert np.array_equal(picked, context.grid_sample(5_000))
in_dense = np.count_nonzero((picked[:, 0] < 20) & (picked[:, 1] < 20)) / len(picked)
file_order = context.sample(5_000)
in_dense_file = np.count_nonzero((file_order[:, 0] < 20) & (file_order[:, 1] < 20))
assert in_dense < 0.1 < 0.5 < in_dense_file / len(file_order) # 파일 차례는 몰림 · 격자는 고름
assert len(context.grid_sample(300_000)) == len(points) # 모자라면 전부
def test_설정_지문이_바뀌면_implicit_다시_지음(tmp_path: Path) -> None:
points = _plane()
data = _structured(points)
masks = {"a": np.ones(len(points), bool)}
config = _config(
source_filters=["a"],
precompute=["implicit"],
implicit_min_points=500,
implicit_max_points=1_000,
)
build_all_terrain_models(data, masks, tmp_path, config)
with np.load(tmp_path / "implicit_a.npz") as old:
assert len(old["center_z"]) == 1_000
changed = dict(config, implicit_max_points=2_000)
manifest = build_all_terrain_models(data, masks, tmp_path, changed)
with np.load(tmp_path / "implicit_a.npz") as new:
assert len(new["center_z"]) == 2_000
assert manifest["source_filters"]["a"]["methods"]["implicit"]["center_count"] == 2_000
def test_외곽_틈_닫기_3m_는_성긴_띠를_살림() -> None:
points = _plane(60.0)
gap = (points[:, 0] > 28) & (points[:, 0] < 33) # 5 m 빈 띠 — 1 m 닫기면 두 덩이
points = points[~gap]
def area(close_m: float) -> float:
config = dict(cfg.build_surface_model_config(), footprint_gap_close_meters=close_m)
context = TerrainContext(
xyz=points,
mask=np.ones(len(points), bool),
bounds=np.array([points.min(axis=0), points.max(axis=0)]).T,
config=config,
)
return context.footprint_metadata()["footprint_area_m2"]
assert cfg.SURFACE_FOOTPRINT_GAP_CLOSE_M == 3.0
assert area(3.0) > 1.8 * area(1.0) # 한 덩이(띠 포함) 대 큰 덩이 하나
@@ -0,0 +1,177 @@
"""PLAN 641 — implicit 굳힌 격자(`implicit_{필터}_grid.npz`) · 화면 일치 · B07 · D03 그대로.
641-1: 굳힌 격자 축 · 유효 = 같은 필터 dtm · 유효 칸 값 = RBF 값 · 밖 = dtm 값 · 옛것이면 다시 셈 ·
서버 표본기 · Node 측점(`source`) · B06 재계산 · 브라우저 격자 통로가 같은 파일.
641-2: implicit 확정이어도 B07 음영 · D03 지형은 평활 DTM · dtm 확정은 전과 같음.
"""
from __future__ import annotations
import asyncio
import os
from contextlib import asynccontextmanager
from pathlib import Path
import numpy as np
from B05_Profile import B05_Profile_Router_Terrain as terrain_router
from B06_Section import B06_Section_Server_Calc_Prebuild as prebuild
from common_util.common_util_surface_sampler import (
DtmGridSampler,
build_surface_sampler,
dtm_grid_bytes,
implicit_grid_npz,
implicit_rbf,
)
from D03_PlanAnalysis.D03_PlanAnalysis_TerrainSource import road_dem
IMPLICIT = {"source_filter": "classification", "method": "implicit", "smooth": False}
def _surface(x, y):
return 100.0 + 0.2 * x + 0.1 * y + 2.0 * np.sin(x / 7.0) * np.cos(y / 5.0)
def _project(tmp_path: Path) -> Path:
models = tmp_path / "B04_PreProcess" / "models"
models.mkdir(parents=True)
axis = np.arange(0.0, 40.0, dtype=np.float32)
valid = np.zeros((40, 40), dtype=bool)
valid[5:35, 3:30] = True
for name, z in (("dtm_classification", 50.0), ("dtm_classification_smooth", 60.0)):
np.savez(
models / f"{name}.npz",
x=axis,
y=axis,
z=np.full((40, 40), z, np.float32),
valid_mask=valid,
resolution=np.array([1.0], np.float32),
)
rng = np.random.default_rng(641)
centers = rng.uniform(0.0, 39.0, (600, 2)).astype(np.float32)
np.savez(
models / "implicit_classification.npz",
centers_xy=centers,
center_z=_surface(centers[:, 0], centers[:, 1]).astype(np.float32),
smoothing=np.array([0.1], np.float32),
)
return models
def test_굳힌_격자_축_유효_dtm_같음_값_RBF(tmp_path):
models = _project(tmp_path)
path = implicit_grid_npz(models, "classification")
assert path.name == "implicit_classification_grid.npz"
with np.load(path) as grid, np.load(models / "dtm_classification.npz") as dtm:
assert np.array_equal(grid["x"], dtm["x"]) and np.array_equal(grid["y"], dtm["y"])
assert np.array_equal(grid["valid_mask"], dtm["valid_mask"])
z, valid = grid["z"], grid["valid_mask"]
assert not np.isnan(z).any()
assert np.array_equal(z[~valid], dtm["z"][~valid]) # 밖 = dtm 값
rows, cols = np.nonzero(valid)
query = np.column_stack([grid["x"][cols], grid["y"][rows]]).astype(np.float64)
rbf = implicit_rbf(models / "implicit_classification.npz")
assert np.abs(rbf(query) - z[rows, cols]).max() < 1e-4 # 유효 칸 = RBF(float32)
def test_옛것이면_다시_셈_같으면_그대로(tmp_path):
models = _project(tmp_path)
path = implicit_grid_npz(models, "classification")
stamp = path.stat().st_mtime_ns
assert implicit_grid_npz(models, "classification").stat().st_mtime_ns == stamp
source = models / "implicit_classification.npz"
os.utime(source, ns=(stamp + 10**9, stamp + 10**9))
assert implicit_grid_npz(models, "classification").stat().st_mtime_ns > stamp
def test_서버_Node_B06_브라우저_같은_파일(tmp_path, monkeypatch):
models = _project(tmp_path)
sampler = build_surface_sampler(models, "classification", "implicit", False)
grid_npz = models / "implicit_classification_grid.npz"
assert isinstance(sampler, DtmGridSampler)
assert Path(sampler.source) == grid_npz.resolve() # Node 측점 = dtm_grid_file(source)
# 서버 표본 = 굳힌 격자 표본(같은 측점 같은 표고)
points = np.array([[10.3, 12.7], [20.0, 20.0], [1.0, 1.0]])
z, valid = sampler.sample_xy(points)
z2, valid2 = DtmGridSampler.from_npz(grid_npz).sample_xy(points)
assert np.array_equal(valid, valid2) and np.allclose(z[valid], z2[valid2], atol=0)
assert valid.tolist() == [True, True, False]
# B06 서버 재계산 격자 = 같은 npz 의 `.bin`
bin_path = prebuild.design_grid_file(tmp_path, IMPLICIT)
assert bin_path is not None and bin_path.read_bytes() == dtm_grid_bytes(grid_npz)
# 브라우저 격자 통로 — implicit 모델 행 → 굳힌 격자
row = ("implicit", "B04_PreProcess/models/implicit_classification.npz")
class _Cursor:
async def execute(self, *_args):
return None
async def fetchone(self):
return row
class _Connection:
@asynccontextmanager
async def cursor(self):
yield _Cursor()
async def stored(_connection, _project_id):
return "x"
monkeypatch.setattr(terrain_router, "get_project_storage_relative_path", stored)
monkeypatch.setattr(terrain_router, "resolve_stored_project_path", lambda _s: tmp_path)
served = asyncio.run(terrain_router.dtm_grid_npz(_Connection(), "p", 9, False))
assert served.resolve() == grid_npz.resolve()
info = terrain_router._design_grid(
IMPLICIT, models, (9, "implicit", "B04_PreProcess/models/implicit_classification.npz")
)
assert info is not None and info["model_id"] == 9 and info["smooth"] is False
assert (
terrain_router._design_grid(IMPLICIT, models, (3, "dtm", "x/dtm_classification.npz"))
is None
)
def test_D03_B07_implicit_확정이면_평활_DTM(tmp_path):
models = _project(tmp_path)
assert road_dem(models, IMPLICIT, "design")[1].name == "dtm_classification_smooth.npz"
dtm = {**IMPLICIT, "method": "dtm"}
assert road_dem(models, dtm, "design")[1].name == "dtm_classification.npz" # 전과 같음
assert road_dem(models, {**dtm, "smooth": True}, "design")[1].name.endswith("_smooth.npz")
def test_B07_음영_implicit_확정이면_평활_DTM(tmp_path, monkeypatch):
from types import SimpleNamespace
from B07_DesignDetail import B07_DesignDetail_Router_Support_Basin as basin
_project(tmp_path)
opened: list[str] = []
real_load = np.load
monkeypatch.setattr(
np, "load", lambda path, **kw: opened.append(Path(path).name) or real_load(path, **kw)
)
box = (0.0, 0.0, 39.0, 39.0)
for params, name in (
(IMPLICIT, "dtm_classification_smooth.npz"),
({**IMPLICIT, "method": "dtm"}, "dtm_classification.npz"), # 전과 같음
):
basin._hillshade_for_box(SimpleNamespace(surface_params=params, project_root=tmp_path), box)
assert opened[-1] == name
def test_643_등고선_굳힌_격자_그대로(tmp_path):
from B04_PreProcess.B04_PreProcess_Engine_Contour import extract_contours
models = _project(tmp_path)
contours = extract_contours(
models / "implicit_classification.npz", "local_rbf_height_field", 1.0
)
points = np.array([p for c in contours for p in c["coordinates"]])
sampler = build_surface_sampler(models, "classification", "implicit", False)
z, valid = sampler.sample_xy(points[:, :2])
assert valid.sum() > 0.9 * len(points)
assert np.abs(z[valid] - points[valid, 2]).max() < 0.005 # 등고선 = 표본 면(좌표 mm 자리)
@@ -44,6 +44,10 @@ def test_서고_사본_산근_목록에_듦_대가_단가_채움(world):
by_key = {r["key"]: r for r in sg["sg_list"]}
labor = compute_labor(daega, {**base, "gaps": []}, by_key)
row = next(r for r in labor["daega"] if r["key"] == block["id"])
assert not row["sum"] and row["blank"] == 3 # 723-4 서고 SC 안 줄 수량 없음 = 이 공사에서 입력
cover = {block["cover"]: {ln["cid"]: {"qty": q} for ln, q in zip(block["lines"], (2, 1, 3))}}
labor = compute_labor(daega, {**base, "gaps": []}, by_key, cover)
row = next(r for r in labor["daega"] if r["key"] == block["id"])
assert row["sum"] and not row.get("blank")
+5 -3
View File
@@ -3,7 +3,8 @@
- 복사본 = 덮개 `K:…` 의 `@copy` {from · name · spec} — 셈 짝(`Calc_Labor` 파이썬 · TS 거울)이
원본 블록을 본떠 끝 번호 새 호표 · 덮개 = 제 것 · 원본 없음 = 짝 잃음 ·
저장 다듬기 · 서고 사본 지킴(prune)
- 내 자료집 = 개인 층 사전조합(임시 storage) — 줄 수량 덮개 = 손 · 품 · 단가 덮개 = 안 담음 수 ·
- 내 자료집 = 개인 층 사전조합(임시 storage) — 줄 수량 덮개 = 문서 손(쓸 때 걷힘 · 723-1) ·
품 · 단가 덮개 = 안 담음 수 ·
같은 이름 409 → 덮어쓰기 · 새 이름
"""
@@ -178,7 +179,8 @@ def test_자료집_같은_이름_물음(world):
saved = json.loads(
(world["folder"] / "precombo" / f"{first['이름']}.json").read_text(encoding="utf-8")
)
assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] == {"손": 9}
# 723-1 = M02 SC 안 줄 수량 저장 안 함(덮어쓴 손 9 도 걷힘 · 쓰는 곳 알약 · 이 공사 입력으로 이음)
assert saved["이름"] == "내 돌붙임" and saved["담은로직"][1]["수량"] is None
assert saved["담은로직"][0]["입력값"] == b145.STONE
assert len(list((world["folder"] / "precombo").glob("SC-*.json"))) == 2
@@ -212,5 +214,5 @@ def test_자료집_길_같은_이름_409_덮어쓰기(world, monkeypatch):
over = client.post(url, json={**body, "overwrite": first.json()["키"]})
assert over.status_code == 200 and over.json()["키"] == first.json()["키"]
saved = world["folder"] / "precombo" / f"{first.json()['키']}.json"
assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] == {"손": 4}
assert json.loads(saved.read_text(encoding="utf-8"))["담은로직"][0]["수량"] is None # 723-1
assert client.post(url, json={**body, "cover": "S:없음"}).status_code == 404
+5 -1
View File
@@ -32,8 +32,12 @@ def _sc(folder, lines, unit="㎡"):
def _value(folder, key):
"""안 줄 수량 = 모두 1(723-1 · 저장 안 함 → 쓰는 쪽 알약 값)."""
ref = {**MINE, "키": key}
with store.at(folder):
return refs.Expand(_whole())({**MINE, "키": key})
lines = refs.read_ref(ref)["담은로직"]
given = {f"{refs.ref_id(ref)}#{i}": 1 for i in range(len(lines))}
return refs.Expand(_whole(), given=given)(ref)
def test_logic_line_cover_in_precombo(world):
+10 -9
View File
@@ -27,20 +27,21 @@ def test_sk_list_layer_order_and_ref_shape():
order = re.search(r"LAYER_ORDER = \[([^\]]+)\]", pick).group(1)
assert [x.strip().strip('"') for x in order.split(",")] == ["system", "company", "personal"]
assert "[...scopes].sort((a, b) => rank(a) - rank(b))" in pick
assert "r.종류 === SANGEUN_KIND" in pick and 'SANGEUN_KIND = "sangeun"' in pick
assert "r.종류 === kind" in pick and 'SANGEUN_KIND = "sangeun"' in pick
assert "listLayerDocs(scopes, SANGEUN_KIND)" in pick
assert 'ref: { 층: at.층, 주인: at.층 === "system" ? null : at.주인, 키: r.이름 }' in pick
labor = _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
assert "lib?.pick(x.ref, x.제목 || x.이름);" in labor # SK 탭 줄 누름 = 참조
assert 'if (tab === "sk") return findSangeun(q);' in labor
# 721-2 — SK · 일위대가 탭은 찾아 넣기 창(Find_Pick) · 줄 누름 = 참조
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert "go: take(() => opt.ref?.(x.ref, x.제목 || x.이름))" in find
assert "sangeun: true," in _text(
M02 / "M02_MasterTemplete_Hopyo_Pick.ts"
) # 일위대가 [로직 추가]
) # 일위대가 [로직 추가] → 찾아 넣기 창
store_ts = _text(ROOT / "B09_Estimation" / "B09_Estimation_UI_Store.ts")
assert '["sangeun", "단가산출"],' in store_ts and "sangeun: sangeunItems," in store_ts
# 더한줄 고르기 — 715 이음 꼴 셋
assert 'done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 })' in pick
assert "done({ 참조: ref, 수량: 1 })" in pick
assert 'done({ 종류: "재료", 요소: m.코드, 이름: m.이름, 단위: m.단위, 수량: 1 })' in pick
# 더한줄 고르기 — 715 이음 꼴 셋(인력 · 재료 = 요소 키)
assert 'logic: (x) => done({ 종류: "로직", 로직: x.키, 입력값: {}, 수량: 1 })' in pick
assert "ref: (ref) => done({ 참조: ref, 수량: 1 })" in pick
assert "done({ 종류: e.종류, 요소: e.코드, 이름: e.이름, 단위: e.단위, 수량: 1 })" in pick
def _sk(folder, name):
+12 -3
View File
@@ -93,15 +93,24 @@ def test_B09_서고_사본_값_같음_원본판(world, tmp_path):
"precombo",
{"이름": "조합", "단위": "㎡", "담은로직": [{"참조": ref["ref"], "수량": {"손": 2}}]},
)["이름"]
_, live = _top({"folder": world["personal"], "ref": {**MINE, "키": sc}}, 1)
# 723-1 = SC 안 줄 수량(손 2) 저장 안 함 → 쓰는 쪽 알약 값으로 이음(층 · 사본 같은 값)
pill = f"{refs.ref_id({**MINE, '키': sc})}#0"
with store.at(world["personal"]):
top = labor.clean(
{"열": "rv", "담은로직": [{"참조": {**MINE, "키": sc}, "수량": {"손": 1}}]}
)
live = labor.calc(top, {pill: 2})["줄"][0]
area = {"시트": [{"id": "s", "칸": {"A1": {"값": 2}}, "연결": [{"칸": "A1", "키": pill}]}]}
before = _print(world["personal"])
root = tmp_path / "proj"
made = library.import_copy(root, "precombo", "personal", "7/42", sc)
assert made["만듦"] and _print(world["personal"]) == before
folder = library.library_dir(root) / made["폴더"]
read = {p.name: json.loads(p.read_text(encoding="utf-8")) for p in folder.glob("*.json")}
got = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, None)["줄"][0]
assert not got["까닭"]
bare = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, None)["줄"][0]
assert bare["기준"] is None and "수량 없음" in bare["까닭"] # 이은 값 없음 = 1 로 안 셈
got = prices.calc(read[source.LABOR_FILE], read[source.PRICE_FILE], {}, {}, area)["줄"][0]
assert not got["까닭"] and not live["까닭"]
assert got["기준"]["계"] == pytest.approx(live["기준"]["계"], rel=1e-9)
mark = read["manifest.json"]["원본판"]
built = library._build("precombo", "personal", "7/42", sc)[0]
+35 -4
View File
@@ -21,7 +21,7 @@ from B09_Estimation import B09_Estimation_Refresh as refresh
from B09_Estimation import B09_Estimation_Sangeun as sangeun
from B09_Estimation.B09_Estimation_Calc_Base import compute_base
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
from B09_Estimation.B09_Estimation_Labor_Source import ASK, daega_inputs
from B09_Estimation.B09_Estimation_Linked import linked_resource_keys
from M02_MasterTemplete import M02_Item_Copy as item_copy
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
@@ -122,15 +122,46 @@ def test_SK_단가_덮개_속_일위대가_펼침(world, tmp_path):
if not _engine_ready(world):
pytest.skip("715-1b 엔진 입력 덧칸(단가 · 값묶음) 뒤 — 지금은 M02 SK 셈도 못 함")
key = _mine(world, full=True)
live = _calc(world["personal"], key)
# 723-1 = 속 일위대가 안 줄 수량 저장 안 함 → M02 SK = 수량 없음 · B09 = 그 줄만 빈 줄(이 공사 입력)
assert "수량 없음" in _calc(world["personal"], key)["까닭"]
root, folder = _lib(world, tmp_path, key)
daega, done, _ = _b09(root)
block = done[daega["libs"][folder]]
lines = block["lines"]
assert lines[1]["kind"] == "fixed" and lines[1]["prices"]["labor"] == pytest.approx(200000)
sc_line = next(ln for ln in lines if ln["kind"] == "daega")
assert done[sc_line["key"]]["kind"] == "사전조합" and sc_line["qty"] == 2
_same_money(block, live)
sc = done[sc_line["key"]]
assert sc["kind"] == "사전조합" and sc_line["qty"] == 2
assert sc["blank"] == 1 and sc["lines"][0]["why"] == ASK and block["blank"] >= 1
# 이 공사에서 입력(702 덮개 qty 1) = 옛 손 1 과 같은 값 → M02 셈(알약 값 1) 과 같은 금액
cover = {sc["cover"]: {sc["lines"][0]["cid"]: {"qty": 1}}}
block = _b09(root, cover)[1][daega["libs"][folder]]
assert block["blank"] == 0
sc_ref = next(x for x in _added(world, key) if "참조" in x)["참조"]
_same_money(block, _calc_given(world["personal"], key, {f"{refs.ref_id(sc_ref)}#0": 1}))
def _added(world, key):
with store.at(world["personal"]):
return store.read("sangeun", key)["문서"]["더한줄"]
def _calc_given(folder, key, pills):
"""M02 SK 셈 — 속 일위대가 안 줄 수량 = 알약 값(723-1 `Expand` given)."""
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Sangeun_Calc as calc
from M02_MasterTemplete import M02_Sangeun_Doc as sd
from M02_MasterTemplete.M02_Labor_Expand import Expand
whole = m01.loaded()[1]
with store.at(folder):
clean = sd.clean(store.read("sangeun", key)["문서"], None, whole)
me = refs.ref_id({"층": refs.here()[0], "주인": refs.here()[1], "키": clean["키"]})
got = calc.run(
clean, whole, None, refs.read_ref, Expand(whole, refs.read_ref, pills), (me,)
)
assert not got.get("까닭")
return got
def test_구조물_사본_SK_줄_업데이트_덮개_입력값_고른자재_굳힌키_그대로(
@@ -0,0 +1,100 @@
"""M02 폴더 · 층 · 단가산출 이름(PLAN 720 · 내역 16) — 임시 storage + 화면 글 대조.
720-1 단가산출 · 일위대가 층 단추 = 저장 안 한 고침 물음 → 그 층 목록표 ·
720-2 최상위 폴더 = 메뉴 줄 · 층 줄 「+」 같은 `addFolder([])` ·
720-3 문서 [이름 바꾸기] = 읽고 `이름` 만 갈아 저장(층 목록 제목 새 이름 · 다른 칸 그대로) ·
720-4 구조물 조합 층 = 제자리 바꿈(해시 이동 없음).
"""
from pathlib import Path
from fastapi.testclient import TestClient
from resources.tester.test_m02_precombo_folders import client # noqa: F401 — 같은 임시 층
M02 = Path(__file__).resolve().parents[2] / "M02_MasterTemplete"
ITEM = "/api/m02/layers/personal/templates/{}/{}"
def _text(name: str) -> str:
return (M02 / name).read_text(encoding="utf-8")
def _rows(c: TestClient, kind: str) -> dict[str, dict]:
rows = c.get("/api/m02/layers/personal/templates").json()["양식"]
return {r["이름"]: r for r in rows if r["종류"] == kind}
def _rename(c: TestClient, kind: str, key: str, name: str) -> None:
"""화면 `renameDoc` 과 같은 길 — 읽고 `이름` 만 갈아 같은 판으로 저장"""
got = c.get(ITEM.format(kind, key)).json()
body = {"판": got.get("판") or "", "문서": {**got["문서"], "이름": name}}
res = c.put(ITEM.format(kind, key), json=body)
assert res.status_code == 200, res.text
def test_일위대가_이름_바꾸기는_이름만(client: TestClient): # noqa: F811
before = client.get(ITEM.format("precombo", "SC-000002")).json()["문서"]
_rename(client, "precombo", "SC-000002", "나 고침")
after = client.get(ITEM.format("precombo", "SC-000002")).json()["문서"]
assert after["이름"] == "나 고침"
assert {k: v for k, v in after.items() if k != "이름"} == {
k: v for k, v in before.items() if k != "이름"
}
rows = _rows(client, "precombo")
assert rows["SC-000002"]["제목"] == "나 고침" and rows["SC-000001"]["제목"] == "가"
def test_단가산출_이름_바꾸기_목록_제목(client: TestClient): # noqa: F811
body = {"문서": {"이름": "갑", "단위": "㎡", "바탕": None, "더한줄": []}}
res = client.post("/api/m02/layers/personal/library/sangeun", json=body)
assert res.status_code == 200, res.text
key = res.json()["이름"]
_rename(client, "sangeun", key, "을")
assert _rows(client, "sangeun")[key]["제목"] == "을"
assert client.get(ITEM.format("sangeun", key)).json()["문서"]["단위"] == "㎡"
def test_화면_층_단추_최상위_폴더_이름_바꾸기():
page = _text("M02_MasterTemplete_UI_Page.ts")
lib = _text("M02_MasterTemplete_UI_Side_Library.ts")
# 720-1 — 다른 층 단추 = 물음 뒤 목록표
assert "setPre(next), void main.open(precomboList(pre))" in page
assert "void main.open(sangeunList(sk))" in page.split("const skBar")[1].split("reselect:")[0]
# 720-2 — 층 줄 「+」 = 서고 칸 맨 위 메뉴 · 메뉴 줄 = 같은 addFolder([])
assert page.count(" add,\n") == 3 # 구조물 조합 · 일위대가 · 단가산출
assert "onClick: (event) => openTreeMenu(event, menuAt([]))" in lib
assert "add.hidden = !at.canWrite;" in lib and "opt.bar(addTools)" in lib
assert lib.count('L("M02_TreeAddTopFolder"), () => void addFolder([])') == 1 # 폴더 줄
assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([])' in lib # 문서 줄
tree = _text("M02_MasterTemplete_TreePanel.ts")
assert '{ label: L("M02_TreeAddTopFolder"), run: () => void addFolder([]) }' in tree
# 720-3 — 문서 줄 [이름 바꾸기] · 저장 뒤 머리 제목
assert "run: () => void renameDoc(row)" in lib
assert "이름: name }" in lib and "saveTemplate(at.layer, kind, row.이름, null" in lib
main = _text("M02_MasterTemplete_UI_Main.ts")
assert "title.textContent = at.label = named ? `${at.name} ${named}` : at.name;" in main
def test_구조물_조합_층_제자리_바꿈():
"""720-4 — 해시 이동 없음 · replaceState · 나무 · 층 줄 · 띠를 감싼 칸에 갈아 끼움 · 권한 = 함수 ·
도면 양식 = 누구나(단추는 관리자) · 실무문서 = 시스템 관리자 · 불러오기 장부 = 열 때 층 · 권한."""
page = _text("M02_MasterTemplete_UI_Page.ts")
assert "navigateTo" not in page
switch = page.split("const switchStructure = async")[1].split("const go =")[0]
assert "history.replaceState(" in switch and "build();" in switch
assert "await main.openTable(layer, null, true);" in switch
assert "if (mine !== turn) return;" in switch # 늦게 온 옛 고름 버림
build = page.split("const build = (): void => {")[1].split("const settleLayer")[0]
assert "topHost.replaceChildren(" in build and "treeHost.replaceChildren(" in build
assert "tree: [treeHost]," in page and "top: topHost," in page
assert "() => isAdmin," in page and 'isSystem: user?.role === "SYSTEM_ADMIN",' in page
assert "main.root.isConnected ? void afterItem()" in page # 나무를 갈아도 듣기 그대로
side = _text("M02_MasterTemplete_UI_Side.ts")
assert "const drawing = section(" in side and "const practice = opt.isSystem" in side
# 도면 양식 칸 = 누구나 · 단추만 관리자(720-4 덧)
assert "const drawing = opt.isSystem" not in side
assert 'if (!opt.isSystem) return [];' in side
assert "items = await listTemplates(layer, null);" in side
hopyo = _text("M02_MasterTemplete_Hopyo_Side.ts")
assert "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo
+106
View File
@@ -0,0 +1,106 @@
"""721 M02 찾아 넣기 창 — 단추 이름 · 자리 · 부르는 곳 · 인력 · 재료 줄 꼴(요구서_내역16 1장 5~7 · 2장 1 · 2 · 8).
- 721-1 단가산출 상세 = [기본 로직 선택]이 둘째 줄 [항목 추가] 왼쪽
- 721-2 M02 세 곳(기본 로직 선택 · 항목 추가 · 일위대가 [로직 추가])만 새 창 · B09 · 불러오기는 옛 창
- M01 전체 = 로직 · 인력(job) · 재료(price) · 기계 · 실무 호표 없음
- 고른 인력 · 재료 = 단가산출 더한줄 `{종류, 요소}` · 일위대가 고정 줄(705-1 이음 꼴) — 둘 다 서버가 받아 노임 · 자재 값으로 셈
"""
from __future__ import annotations
from pathlib import Path
import pytest
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Sangeun_Calc as calc
ROOT = Path(__file__).resolve().parents[2]
M02 = ROOT / "M02_MasterTemplete"
LB, MT = "LB000033", "MT000018" # 석공 · 합판(내수) — M01 에 있음
def _text(path: Path) -> str:
return path.read_text(encoding="utf-8")
def test_721_1_단추_이름과_자리():
detail = _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
assert "children: [views.root, pick, addButton, baseHint]" in detail
assert " pick,\n );" not in detail # 머리 줄에서 뺌
locale = _text(ROOT / "ui_template" / "ui_template_locale_m2.ts")
assert 'M02_SangeunPickBase: ["기본 로직 선택"' in locale
assert 'M02_SangeunAddLine: ["항목 추가"' in locale
def test_721_2_부르는_곳_셋만_새_창():
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
labor_pick = _text(M02 / "M02_MasterTemplete_Labor_Pick.ts")
assert 'if (lib?.sangeun && lib.layer !== "project")' in labor_pick
assert "return openFindPicker({" in labor_pick
assert "findSangeun" not in labor_pick # 옛 창의 단가산출 탭 걷음
assert "logicOnly: true," in _text(M02 / "M02_MasterTemplete_Sangeun_Detail.ts")
assert "openFindPicker({" in _text(M02 / "M02_MasterTemplete_Sangeun_Pick.ts")
assert "sangeun: true," in _text(M02 / "M02_MasterTemplete_Hopyo_Pick.ts") # 일위대가
# 옛 창 그대로 — B09 · 불러오기는 sangeun 없이 부름
for f in (
ROOT / "B09_Estimation" / "B09_Estimation_UI_Link.ts",
ROOT / "B09_Estimation" / "B09_Estimation_UI_Tabs_Bill_Link.ts",
M02 / "M02_MasterTemplete_Hopyo_Choose.ts",
M02 / "M02_MasterTemplete_Hopyo_Ledger.ts",
):
text = _text(f)
assert "openLogicPicker(" in text and "sangeun:" not in text, f.name
# 탭 셋 · 찾기 칸 하나 · 거름 고르기 없음 · M01 = 로직 · 인력 · 재료(기계 · 호표 없음)
for key in ('key: "m01"', 'key: "sk"', 'key: "pre"'):
assert key in find
assert "createSelectField" not in find and "Hopyo" not in find
assert '(kind: "job" | "price")' in find and "/elements" not in find
def test_721_2_고정_줄_꼴():
find = _text(M02 / "M02_MasterTemplete_Find_Pick.ts")
assert "이음: { 노임: labor ? e.코드 : null, 자재: labor ? null : e.코드, 기계: null }" in find
assert "go: put ? take(() => put(e))" in find
assert "dim: !put," in find # 받는 쪽 없으면 회색 막힘
def _fx(name: str, unit: str, code: str) -> dict:
"""`fixedOf` 꼴 — 인력 = 노임 이음 · 재료 = 자재 이음 · 단가 = 고를 때 값(셈은 단가 책)"""
labor_ = code.startswith("LB")
return {
"이름": name,
"규격": "",
"단위": unit,
"단가": {"노무비": 1 if labor_ else 0, "재료비": 0 if labor_ else 1, "경비": 0},
"이음": {"노임": code if labor_ else None, "자재": None if labor_ else code, "기계": None},
}
def test_721_2_인력_재료_서버가_받아_셈():
whole = labor.m01.loaded()[1]
wage = whole.get(LB)["값"]
sk = calc.calc_doc(
{
"키": "SK-000001",
"이름": "시험",
"단위": "㎡",
"바탕": None,
"더한줄": [
{"종류": "인력", "요소": LB, "이름": "석공", "단위": "인", "수량": 1},
{"종류": "재료", "요소": MT, "이름": "합판", "단위": "㎡", "수량": 1},
],
}
)["1단위"]
doc = labor.clean(
{
"담은로직": [
{"로직": "", "고정": _fx("석공", "인", LB), "수량": {"손": 1}},
{"로직": "", "고정": _fx("합판", "㎡", MT), "수량": {"손": 1}},
]
}
)
got = labor.calc(doc, {})
assert sk["노무비"] == pytest.approx(wage) and sk["재료비"] > 0
assert got["반영합"]["계"] == pytest.approx(sk["계"])
assert [ln["이은"] for ln in got["줄"]] == [["노무비"], ["재료비"]]
+113
View File
@@ -0,0 +1,113 @@
"""722 M01 단가산출 상세 개편 지킴 시험 — 글 · 순수 함수(화면 · 서버 없음).
- 722-1 공용 단가산출 상세(`M01_MasterData_UI_LogicLab_Ho.ts`) — 하위 박스(서버 묶음) · 장비 한 줄 + 펼침 · 계산식 열 · 박스 옵션
- 722-2 그 자리 고침(`M01_MasterData_UI_LogicLab_Cover.ts`) — M01 = 시험 값(줄 사본) · 정본 · 캐시 안 바뀜
"""
import json
import shutil
import subprocess
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[2]
M01 = ROOT / "M01_MasterData"
HO = M01 / "M01_MasterData_UI_LogicLab_Ho.ts"
COVER = M01 / "M01_MasterData_UI_LogicLab_Cover.ts"
def _text(p: Path) -> str:
return p.read_text(encoding="utf-8")
def _pure(p: Path) -> str:
s = _text(p)
return s[s.index("/* ── 순수 셈 ── */") : s.index("/* ── 순수 셈 끝 ── */")]
def _node(code: str):
out = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", code],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert out.returncode == 0, out.stderr
return json.loads(out.stdout)
def test_상세가_공용_부품을_씀():
detail = _text(M01 / "M01_MasterData_UI_LogicLab_Detail.ts")
assert "buildHo({ ...ctx, edit: trialEdit(ctx) })" in detail
ho = _text(HO)
# 부품 상자만 — 표 · 카드 · 계산 줄 · 변수 칸(calc_sheet) · 입력 칩(텍스트 수식)
for part in (
"tableShell(",
"tableRow(",
"createCard(",
"varRow(",
"inputChip(",
"sheetFormula(",
):
assert part in ho, part
for s in (ho, _text(COVER)):
assert 'el("table"' not in s and 'el("button"' not in s and 'el("input"' not in s
assert "document.createElement" not in s and "sessionStorage" not in s
assert "export function varRow(" in _text(ROOT / "A00_Common" / "calc_sheet.ts")
for p in (HO, COVER, M01 / "M01_MasterData_UI_LogicLab.ts"):
assert len(_text(p).splitlines()) <= (
627 if p.name == "M01_MasterData_UI_LogicLab.ts" else 700
)
def test_시험_고침은_캐시_밖():
lab = _text(M01 / "M01_MasterData_UI_LogicLab.ts")
# 시험 계산에만 덮은 사본 · 고침이 있으면 줄을 보냄 · drafts(캐시)에는 원본 줄
assert ": trial.apply(row);" in lab
assert "dirty: () => current.id in drafts || trial.has(current.row)," in lab
assert "onTrial: drawCalc," in lab
cover = _text(COVER)
assert "drafts" not in cover and "touch" not in cover and "/save" not in cover
modal = _text(M01 / "M01_MasterData_UI_LogicLab_Modal.ts")
assert "...(target.edit ? [target.edit(close)] : [])" in modal
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_박스_묶기():
body = """
const e = (idx, bundle, merge, amount) => ({ idx, bundle, merge, amount });
const got = boxesOf([
e(0, "인력", false, 10), e(1, "경비", true, 100), e(1, "경비", true, 20), e(1, "경비", true, 30),
e(2, "하위", true, 5), e(2, "하위", true, null), e(3, "재료", false, null),
], ["인력", "재료", "경비"]).map((b) => ({ 묶음: b.묶음, 소계: b.소계,
줄: b.줄.map((r) => "몫" in r ? [r.idx, r.금액, r.몫.length] : r.idx) }));
console.log(JSON.stringify(got));
"""
got = _node(_pure(HO) + "\n" + body)
assert [b["묶음"] for b in got] == ["인력", "재료", "경비", "하위"]
assert got[0] == {"묶음": "인력", "소계": 10, "줄": [0]}
assert got[1] == {"묶음": "재료", "소계": 0, "줄": [3]}
assert got[2] == {"묶음": "경비", "소계": 150, "줄": [[1, 150, 3]]} # 장비 한 줄 = 몫 셋 합
assert got[3]["줄"] == [[2, 5, 2]]
empty = _node(_pure(HO) + '\nconsole.log(JSON.stringify(boxesOf([], ["인력"])))')
assert empty == [{"묶음": "인력", "줄": [], "소계": 0}]
@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음")
def test_고침_덮기():
body = """
const row = { 키: "GF1", 호표: [
{ 종류: "인력", 요소: "LB1", 수량: "찾기(Q).수량" },
{ 종류: "로직", 요소: "로직(GC1)", 수량: "1 / 작업량", 값묶음: { 경비: 3 } },
{ 종류: "재료", 요소: "MT1", 수량: "2" } ], 덧줄: [{ 이름: "공구", 식: "노무비 * 3 / 100" }] };
const out = applyCovers(row, { 0: { qty: 0.0000001, labor: 100000 }, 1: { labor: 50000 }, 9: { qty: 1 } });
console.log(JSON.stringify({ out, same: row.호표[0].수량, keep: out.호표[2] === row.호표[2] }));
"""
got = _node(_pure(COVER) + "\n" + body)
out = got["out"]
assert (
out["호표"][0]["수량"] == "0.0000001" and out["호표"][0]["단가"] == 100000
) # 지수 꼴 없음 · 손 단가
assert out["호표"][1]["값묶음"] == {"경비": 3, "노무비": 50000} and "단가" not in out["호표"][1]
assert got["keep"] and got["same"] == "찾기(Q).수량" # 원본 안 바뀜 · 안 고친 줄 그대로
assert out["덧줄"] == [{"이름": "공구", "식": "노무비 * 3 / 100"}]
+156
View File
@@ -0,0 +1,156 @@
"""723-4 B09 공사 사본의 일위대가(SC) 안 줄 수량 = 프로젝트 값 연결.
- 723-1 = M02 일위대가(SC) 안 줄 · 자재 수량 저장 안 함(null = 수량 없음 · 1 아님)
- 구조물 조합에서 온 SC = 그 사본 면적 알약 값(「참조 id#차례」 · 「참조 id#자재|코드」)으로 굳힘
- 내역에서 바로 쓴 SC(서고 사본) = 빈 줄 `ask` → 이 공사에서 입력(702 덮개 qty) → 금액
- 옛 손(B09 굳힌 사본) = 그대로 · 덮개 열쇠 `C:참조` 그대로(기존 공사 금액 · 덮개 같음) ·
알약 값 쓴 블록만 `C:참조|S:사본`(사본마다 값 다름 · 이 공사 입력 덮개와 안 섞임)
- 셈 짝(`B09_Estimation_Calc_Labor` .py ↔ .ts) 거울
"""
import importlib.util
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor # noqa: E402
from B09_Estimation.B09_Estimation_Labor_Source import ASK, Builder # noqa: E402
from M02_MasterTemplete import M02_Labor_Ref as refs # noqa: E402
_spec = importlib.util.spec_from_file_location(
"b145", Path(__file__).with_name("test_145_2_b09_labor_calc.py")
)
b145 = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(b145)
REF = {"층": "project", "주인": "t", "키": "SC-000001"}
RID = refs.ref_id(REF)
MAT = "MT00001"
SHEETS = {"labor": [], "material": [{"key": MAT, "price": 500}], "machine": []}
def _fix(name, labor):
return {"고정": {"이름": name, "단위": "식", "단가": {"노무비": labor}}}
def _doc(hand=None, mat=None):
"""SC 한 벌 — 고정 줄 둘(노무 1000 · 2000) + 자재 하나 · 손 = hand · mat(None = 수량 없음)."""
q = (lambda v: {"수량": {"손": v}}) if hand is not None else (lambda v: {"수량": None})
return {
"이름": "시험 SC",
"단위": "㎡",
"담은로직": [{**_fix("갑", 1000), **q(hand)}, {**_fix("을", 2000), **q(hand)}],
"자재": [{"코드": MAT, "수량": {"손": mat} if mat is not None else None}],
}
def _build(doc, use=({}, "")):
build = Builder()
whole = {MAT: {"이름": "철선", "규격": "#8", "단위": "kg"}} # 사본 단가 대신(_material 만 읽음)
bid = build.precombo(whole, REF, doc, {}, 1, use)
block = build.blocks[bid]
return {"blocks": [block], "owners": {"pipe@100.0": bid}}, block
def test_수량_없는_SC_서고_사본_경고():
daega, block = _build(_doc(), ({}, "L:lib"))
assert [ln["qty"] for ln in block["lines"]] == [None, None, None]
assert all(ln["ask"] == ASK for ln in block["lines"])
assert block["cover"] == f"C:{RID}" # 공사 안 한 덮개(703-1 지난 공사 가져옴 꼴 그대로)
out = compute_labor(daega, SHEETS)
got = out["daega_blocks"][0]
assert got["blank"] == 3 and got["sum"] == 0
assert [ln["why"] for ln in got["lines"]] == [ASK] * 3
assert sum(g["why"] == ASK for g in out["gaps"]) == 3
assert out["daega"][0]["blank"] == 3 # 내역 · 원가계산서 빈 줄(166-6 · 701-3)로 넘어감
def test_이_공사에서_입력_금액():
daega, block = _build(_doc(), ({}, "L:lib"))
cids = [ln["cid"] for ln in block["lines"]]
cover = {block["cover"]: {cids[0]: {"qty": 2}, cids[1]: {"qty": 0.5}, cids[2]: {"qty": 3}}}
got = compute_labor(daega, SHEETS, None, cover)["daega_blocks"][0]
assert [ln["amount"] for ln in got["lines"]] == [2000.0, 1000.0, 1500.0]
assert got["blank"] == 0 and got["sum"] == 4500.0
assert not any("why" in ln for ln in got["lines"]) # 입력 뒤 경고 글 없음
def test_구조물_조합_알약_값_굳힘():
given = {f"{RID}#0": 1.5, f"{RID}#1": 2, f"{RID}#자재|{MAT}": 4}
daega, block = _build(_doc(), (given, "S:pipe@100.0"))
assert [ln["qty"] for ln in block["lines"]] == [1.5, 2.0, 4.0]
assert not any("ask" in ln for ln in block["lines"])
assert block["cover"] == f"C:{RID}|S:pipe@100.0"
got = compute_labor(daega, SHEETS)["daega_blocks"][0]
assert got["sum"] == 1500.0 + 4000.0 + 2000.0 and got["blank"] == 0
def test_옛_손_그대로():
daega, block = _build(_doc(hand=3, mat=2), ({}, "S:pipe@100.0"))
assert [ln["qty"] for ln in block["lines"]] == [3.0, 3.0, 2.0]
assert block["cover"] == f"C:{RID}" # 옛 덮개 열쇠 그대로
got = compute_labor(daega, SHEETS)["daega_blocks"][0]
assert got["sum"] == 3000.0 + 6000.0 + 1000.0
def test_거울_파이썬_TS():
daega, block = _build(_doc(), ({}, "L:lib"))
cid = block["lines"][0]["cid"]
for cover in ({}, {block["cover"]: {cid: {"qty": 2}}}):
py = json.loads(json.dumps(compute_labor(daega, SHEETS, {}, cover), ensure_ascii=False))
ts = b145._ts(daega, SHEETS, {}, cover)
assert b145._same(py, ts["out"])
def _structure(tmp_path, area):
"""구조물 사본(참조 SC 한 줄 · 굳힌 SC = 수량 없음) + 면적.json(SC 안 줄 알약 연결 · 723-1)."""
from M02_MasterTemplete import M02_Item_Snapshot as snapshots
root = b145._project(tmp_path / "proj", [(100, "SS000901")])
sc = {**_doc(), "자재": []}
b145._snap(root, "pipe@100.0", "SS000901", [{"참조": REF, "수량": {"손": 1}}], {}, {RID: sc})
folder = snapshots.structures_dir(root) / snapshots.folder_name("pipe@100.0")
if area is not None:
(folder / "면적.json").write_text(json.dumps(area, ensure_ascii=False), encoding="utf-8")
return root, folder
def _sc_block(root):
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
got = daega_inputs(root)
combo = next(b for b in got["blocks"] if b["id"] == got["owners"]["pipe@100.0"])
return got, next(b for b in got["blocks"] if b["id"] == combo["lines"][0]["key"])
def test_구조물_사본_면적_알약_값_굳힘_설계_연동(tmp_path):
links = [{"칸": "A1", "키": f"{RID}#0"}, {"칸": "A2", "키": f"{RID}#1"}]
area = {"시트": [{"id": "s", "칸": {"A1": {"값": 1.5}, "A2": {"값": 2}}, "연결": links}]}
root, folder = _structure(tmp_path, area)
got, block = _sc_block(root)
assert [ln["qty"] for ln in block["lines"]] == [1.5, 2.0]
assert block["cover"] == f"C:{RID}|S:{folder.name}" and not any(
"ask" in ln for ln in block["lines"]
)
out = compute_labor(got, SHEETS)
by_id = {b["id"]: b for b in out["daega_blocks"]}
assert by_id[block["id"]]["sum"] == 1500.0 + 4000.0
assert by_id[got["owners"]["pipe@100.0"]]["sum"] == 5500.0 # 조합 = SC 1 단위 × 1
# 설계 연동 — 면적 값이 바뀌면 다시 셈(사본 면적.json)
area["시트"][0]["칸"]["A1"]["값"] = 3
(folder / "면적.json").write_text(json.dumps(area, ensure_ascii=False), encoding="utf-8")
assert [ln["qty"] for ln in _sc_block(root)[1]["lines"]] == [3.0, 2.0]
def test_구조물_사본_알약_안_이으면_빈_줄(tmp_path):
root, folder = _structure(tmp_path, None)
got, block = _sc_block(root)
assert [ln["qty"] for ln in block["lines"]] == [None, None]
assert block["cover"] == f"C:{RID}" # 알약 안 씀 = 서고 SC 와 같은 「이 공사 입력」 덮개
out = compute_labor(got, SHEETS)
owner = next(r for r in out["daega"] if r["key"] == got["owners"]["pipe@100.0"])
assert owner["blank"] >= 1 # 부른 SC 빈 줄 = 조합도 빈 줄(166-6)
+100
View File
@@ -0,0 +1,100 @@
"""PLAN 723 — M02 일위대가(사전조합) 수량 = 저장 안 함 · 일위대가표만 · 줄 모달(내역 16 · 17).
723-1 서버 — 저장 때 안 줄 · 자재 수량 null(비율 줄 율만) · 펼침 = 쓰는 쪽 알약 값 · 안 이으면 「수량 없음」 ·
구조물 조합 알약에 안 줄 알약 · 불러오기 엑셀 수량 = `본뜬.실무수량`(셈 안 씀).
723-2 · 723-3 화면 — 서고 상세 = `Labor_Table`(수량 칸 휘발 임시 입력 · 우클릭) · 줄 모달 = 717-2b 상세 줄 모드.
"""
import os
import pytest
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_Labor_Doc as labor
from M02_MasterTemplete import M02_Labor_Ref as refs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from resources.tester.test_m02_labor_ref import world # noqa: F401 — 임시 층 fixture
SRC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "M02_MasterTemplete")
SYS = {"층": "system", "주인": None}
STONE = "GF000219" # 원/㎡
RATE = {"이름": "공구손료", "단위": "%", "단가": {"노무비": 0, "재료비": 0, "경비": 0}}
def _src(name: str) -> str:
return open(os.path.join(SRC, name), encoding="utf-8").read()
def test_저장은_안_줄_수량을_걷고_비율_줄_율만_남김(world): # noqa: F811
rate = {"고정": {**RATE, "비율": {"기준": "노무비", "비목": "경비"}}, "수량": {"손": 3}}
lines = [{"로직": STONE, "수량": {"손": 2.5}}, rate]
with store.at(world["system"]):
key = refs.create("precombo", {"이름": "수량 없음", "단위": "m", "담은로직": lines})["이름"]
doc = store.read("precombo", key)["문서"]
assert doc["담은로직"][0]["수량"] is None
assert doc["담은로직"][1]["수량"] == {"손": 3} # 비율 = 율(기준단위 당 수량 아님)
frozen = refs.with_pills(doc) # 프로젝트 굳힘도 수량 없이
assert frozen["담은로직"][0]["수량"] is None and frozen["알약"]
def test_펼침은_이은_알약만_쓰고_층_원본_옛_손은_안_씀(world): # noqa: F811
with store.at(world["system"]):
key = refs.create("precombo", {"이름": "옛", "단위": "m", "담은로직": [{"로직": STONE}]})[
"이름"
]
ref = {**SYS, "키": key}
path = world["system"] / "precombo" / f"{key}.json"
import json
old = json.loads(path.read_bytes())
old["담은로직"][0]["수량"] = {"손": 9} # 옛 문서(723-1 앞) — 파일은 그대로 둠
path.write_text(json.dumps(old, ensure_ascii=False), encoding="utf-8")
whole = m01.loaded()[1]
with store.at(world["system"]):
bare = refs.Expand(whole)(ref)
got = refs.Expand(whole, given={f"{refs.ref_id(ref)}#0": 2})(ref)
frozen = refs.Expand(whole, read=lambda r: old)(ref) # 굳힌 사본 = 손 그대로
stone = labor.run_line(whole, STONE, {})["기준"]["계"]
assert bare["기준"] is None and "수량 없음" in bare["까닭"]
assert got["기준"]["계"] == pytest.approx(stone * 2)
assert frozen["기준"]["계"] == pytest.approx(stone * 9)
def test_구조물_조합_알약에_안_줄_알약(world): # noqa: F811
with store.at(world["system"]):
key = refs.create(
"precombo", {"이름": "옹벽", "단위": "m", "담은로직": [{"로직": STONE, "별칭": "돌"}]}
)["이름"]
doc = labor.clean({"열": "rv", "담은로직": [{"참조": {**SYS, "키": key}}]})
pills = labor.pills(doc)
rid = refs.ref_id({**SYS, "키": key})
assert [p["키"] for p in pills] == [rid, f"{rid}#0"]
assert pills[1]["이름"] == "옹벽 · 돌" and pills[1]["단위"] == "㎡"
def test_화면_서고_상세는_일위대가표만_임시_수량_휘발():
table, detail = (
_src("M02_MasterTemplete_Labor_Table.ts"),
_src("M02_MasterTemplete_Labor_Detail.ts"),
)
assert 'if (opt.mode === "precombo") return mountLaborTable(host, doc, opt);' in detail
assert "const temp = new Map<string, number>();" in table # 휘발 — 문서 밖
assert "calc = calc && settle(calc, withTemp(), {});" in table # 짝 셈 미리 보기
assert (
'const out: Record<string, unknown> = { ...cur, 종류: "사전조합" };' in table
) # temp 안 실음
for label in ('tc("Up")', 'tc("Down")', 't("Alias")', 'tc("Remove")'):
assert label in table # 우클릭 메뉴
assert "fixed: (fx: LaborFixed, name: string)" in table # 721-2 고정 줄 이음
def test_화면_줄_모달은_SK_상세_줄_모드_적용은_덮개와_옵션():
modal, sk = (
_src("M02_MasterTemplete_Labor_RowModal.ts"),
_src("M02_MasterTemplete_Labor_RowSk.ts"),
)
detail = _src("M02_MasterTemplete_Sangeun_Detail.ts")
assert "mountSangeunDetail(box, doc," in sk and "getValues: () => ({ ...values })" in detail
assert "if (got.patch) row.패치 = got.patch;" in modal and "setValues(got.values);" in modal
assert 'createCheckbox(t("AlsoSk"), { checked: false })' in modal # 기본 꺼짐
assert "openRowSangeun" not in modal # 717-3 두 보기 갈음
+1 -1
View File
@@ -106,7 +106,7 @@ def test_나무는_좌측_패널_찾기_칸_없음():
assert 'info.이름 === STRUCTURE_TABLE ? L("M02_KindTable")' in side # 좌측 줄 이름
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert "createTreePanel({" in page and "tree.select(column)" in page
assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page
assert "layerBar(tree.add, () => void main.openTable(layer))," in page
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
assert 'at.kind === "table" && at.name === STRUCTURE_TABLE && at.layer !== "project"' in main
assert 'isCombo(next) ? L("M02_KindTable")' in main # 상세 제목
+3 -1
View File
@@ -87,7 +87,9 @@ def test_머리_고침은_쓸_수_있는_층만_나무로():
assert "readOnly || !extras.onHeadRename" in main
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert "tree.editFromTable(headRenameEdit(col, level, name), col.id)" in page
assert page.index("...(canEdit(layer, isAdmin)\n ? {\n onHeadRename") > 0
assert (
"onHeadRename: (col: StructureColumn" in page
) # 720-4 — 가림은 편집 칸 readOnly · 나무 canEdit
tree = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
assert "if (opt.canEdit && (await ready())) await edit(body, colId);" in tree
+20 -11
View File
@@ -61,22 +61,24 @@ def test_확인한_줄만_별칭_수량_빠진_줄_장부(client):
assert origin["확인"]["누가"] == {"id": 42, "이름": "갑"} and origin["확인"]["때"]
stone, *made, soil = doc["담은로직"] # 705-3 — 확인 안 한 줄 · 「우리에 없음」 = 고정 줄
assert stone["고정"]["단가"] == {"노무비": 0, "재료비": 1400000, "경비": 0}
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] == {"손": 1}
assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] == {"손": 0.09}
hands = origin["실무수량"]["줄"] # 723-1 — 수량 = 저장 안 함 · 엑셀 수량은 출처 기록만
assert stone["고정"]["출처"]["줄"] == 0 and stone["수량"] is None and hands["0"] == 1
assert soil["고정"]["이름"] == "잔토처리(인력100%)" and soil["수량"] is None
assert hands[str(len(doc["담은로직"]) - 1)] == 0.09
assert origin["품셈"] == [1, 2] and got["줄수"]["고정"] == 2 and got["빠진줄"] == []
assert made == [
{
"로직": "GF000101",
"입력값": {"지역": "서울"},
"별칭": "구조물터파기(인력100%)",
"수량": {"손": 0.14},
"수량": None, # 723-1 — 엑셀 0.14 = 본뜬.실무수량
"메모": "실무 0.14㎥ × 1 · 토사",
},
{
"로직": "GF000113",
"입력값": {},
"별칭": "구조물되메우기(인력100%)",
"수량": {"손": 0.1}, # 엑셀 0.05 × 배 2
"수량": None, # 엑셀 0.05 × 배 2 = 본뜬.실무수량(723-1)
"메모": "실무 0.05㎥ × 2",
},
]
@@ -108,14 +110,20 @@ def test_로직_여럿_같은_별칭_감쌈_자재_몫_덮음(client):
first, second, fx, wrapped = doc["담은로직"]
assert fx["고정"]["출처"]["줄"] == 2 # 확인 안 한 줄 = 고정(705-3)
assert first["별칭"] == second["별칭"] == "구조물터파기(인력100%)" # 한 줄 로직 둘 = 같은 별칭
assert second["수량"] == {"손": 0.014} and second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] == {"손": 0.09}
hands = doc["본뜬"]["실무수량"]["줄"] # 723-1 — 저장 수량 없음 · 엑셀 수량 = 출처 기록
assert second["수량"] is None and hands["1"] == 0.014
assert second["메모"] == "실무 0.14㎥ × 0.1(몫 0.1) · 토사"
assert (
wrapped["별칭"] == "잔토처리(인력100%)" and wrapped["수량"] is None and hands["3"] == 0.09
)
inner = _doc(mine, wrapped["참조"]["키"])
assert inner["본뜬"]["키"] == "HP-000001 제3호표 4째 줄 GF000101"
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] == {"손": 1}
assert inner["담은로직"][0]["로직"] == "GF000101" and inner["담은로직"][0]["수량"] is None
assert inner["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert [(m["코드"], m["수량"], m["별도계상"]) for m in doc["자재"]] == [
("MT004258", {"손": 1}, "직접계산")
("MT004258", None, "직접계산")
]
assert doc["본뜬"]["실무수량"]["자재"] == {"MT004258": 1}
stone = next(v for k, v in _book(mine).items() if k.startswith("임도표지석(받침"))
assert stone["짝"][0]["자재"] == "MT004258"
files = sorted(p.name for p in (mine / "precombo").glob("*.json"))
@@ -232,14 +240,15 @@ def test_하위_호표_먼저_확인_퍼센트_빠짐_통째(client):
assert rate["비율"] == {"기준": "재료비", "비목": "재료비"}
got = _build(client, 45, ref)
mine = layers.personal_dir(7, 42)
line = _doc(mine, got["참조"]["키"])["담은로직"][0]
made = _doc(mine, got["참조"]["키"])
line = made["담은로직"][0]
assert line["참조"] == child["참조"] and line["별칭"] == "고화매트재료비"
assert line["수량"] == {"손": 2.3}
assert line["수량"] is None and made["본뜬"]["실무수량"]["줄"]["0"] == 2.3 # 723-1
assert "고화매트재료비||m2" not in _book(mine) # 호표 참조 줄 = 장부 안 씀
whole = _build(client, 3, 통째={"대상": [{"로직": "GF000101", "배": 1}]})
doc = _doc(mine, whole["참조"]["키"])
assert [x["로직"] for x in doc["담은로직"]] == ["GF000101"]
assert doc["담은로직"][0]["수량"] == {"손": 1}
assert doc["담은로직"][0]["수량"] is None and doc["본뜬"]["실무수량"]["줄"] == {"0": 1}
assert {x["까닭"] for x in whole["빠진줄"]} == {"호표 통째로 대신"}
head = next(v for k, v in _book(mine).items() if k.startswith("호표|임도표지석|"))
assert head["짝"][0]["로직"] == "GF000101" and head["원래"]["품명"] == "임도표지석"
+4 -2
View File
@@ -128,8 +128,10 @@ def test_섞인_줄_몫_배(client):
assert screen["lines"][0]["checkable"] and not screen["lines"][0]["checked"]
one = {k: part[k] for k in ("로직", "입력값", "배", "몫")}
made = _build(client, "HP-000001", no, [{"차례": line["차례"], "대상": [one]}])
row = next(x for x in _doc(made["참조"])["담은로직"] if x.get("로직") == "GF000211")
assert row["수량"]["손"] == round(line["수량"] * 0.1, 6)
doc = _doc(made["참조"])
at = next(i for i, x in enumerate(doc["담은로직"]) if x.get("로직") == "GF000211")
assert doc["담은로직"][at]["수량"] is None # 723-1 — 수량 저장 안 함 · 엑셀 수량 = 출처 기록
assert doc["본뜬"]["실무수량"]["줄"][str(at)] == round(line["수량"] * 0.1, 6)
def test_막는_길(client):
+4 -2
View File
@@ -342,7 +342,7 @@ def test_좌측_판_자리와_권한():
order = ("precomboSec,", "summary,", "createDivider(),", "section(hopyo.title, hopyo.body),")
assert [host.index(k) for k in order] == sorted(host.index(k) for k in order)
assert host.index("section(hopyo.title, hopyo.body),") < host.index("drawing ?")
assert "canWrite: canEdit(layer, opt.isAdmin)," in side # 구조물 조합 층 쓰기 권한
assert "canWrite: opt.canWrite," in side # 구조물 조합 층 쓰기 권한(720-4 함수)
assert "hopyo.paint();" in side.split("function paint(): void {")[1]
hopyo_side = _text("M02_MasterTemplete_Hopyo_Side.ts")
# 718-1 — 제목 = 화면 글 키 · [불러오기] = 구조물 조합 층 단추 꼴 → 본 칸 마법사 · 옛 파일 목록 없음
@@ -351,7 +351,9 @@ def test_좌측_판_자리와_권한():
assert 'label: "불러오기"' in start and 'start.classList.add("m02-layers__btn");' in start
assert "buildHopyoWizard()" in hopyo_side and "listHopyo" not in hopyo_side
assert "uploadHopyo" not in hopyo_side and "openHopyoSheet" not in hopyo_side
assert "buildHopyoLedger({ scope, canWrite: opt.canWrite })" in hopyo_side # [별칭 장부]
assert (
"buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo_side
) # [별칭 장부]
wizard = _text("M02_MasterTemplete_Hopyo_Wizard.ts")
assert 'accept: ".xls,.xlsx"' in wizard and "input.hidden = true;" in wizard
assert "accessFor(layer, null, user).canWrite" in wizard # 결과 층 = 쓸 수 있는 층만
+32 -17
View File
@@ -73,6 +73,13 @@ def _combo(folder, lines, name="조합"):
return refs.create("precombo", {"이름": name, "단위": "㎡", "담은로직": lines})["이름"]
def _area(given):
"""면적산출 사본 꼴 — 알약 키마다 칸 하나(723-1 · 안 줄 수량 = 이은 칸 값)."""
cells = {f"A{i}": {"값": v} for i, v in enumerate(given.values(), 1)}
links = [{"칸": f"A{i}", "키": k} for i, k in enumerate(given, 1)]
return {"시트": [{"id": "s", "칸": cells, "연결": links}]}
def _read(folder, kind, key):
return json.loads((folder / kind / f"{key}.json").read_bytes())
@@ -221,7 +228,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
ref = {**SYS, "키": sc}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
got = labor.calc(doc, {})
bare = labor.calc(doc, {})["줄"][0] # 723-1 — 안 줄 손은 저장 안 함 · 안 이으면 수량 없음
got = labor.calc(doc, {f"{refs.ref_id(ref)}#0": 2}) # 안 줄 알약 = 구조물 조합 칸 값
assert bare["기준"] is None and "수량 없음" in bare["까닭"] and bare["안"][0]["수량"] is None
one = got["줄"][0]
want = Decimal(str(_base(TEN)["계"])) / 10 * 2
assert one["기준"]["계"] == pytest.approx(float(want))
@@ -233,7 +242,9 @@ def test_calc_는_참조를_펼침_승격은_기준_나누기_배_사전조합
assert one["반영"]["계"] == pytest.approx(float(want * 5))
assert one["안"][0]["수량"] == 2 and one["안"][0]["참조"]["키"] == sp
pills = labor.pills(doc)
assert pills == [{"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}]
assert pills[0] == {"키": refs.ref_id(ref), "이름": "둘", "단위": "㎡", "로직": [], "참조": ref}
assert [p["키"] for p in pills[1:]] == [f"{refs.ref_id(ref)}#0"] # 안 줄 알약(723-1)
assert pills[1]["이름"].startswith("둘 · ")
def test_승격_줄_상세는_원_로직_키_표_사전조합은_안_줄만(world):
@@ -386,9 +397,10 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
sp = _promote(world["system"])
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 2}}])
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#0": 2} # 723-1 — 안 줄 수량 = 이은 칸 값
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
live = labor.calc(doc, {})["줄"][0]["기준"]["계"]
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
rows = source.labor_doc_rows(doc)
assert set(rows["참조사본"]) == {refs.ref_id(ref), refs.ref_id({**SYS, "키": sp})}
assert refs.frozen_logics(rows["참조사본"]) == [TEN]
@@ -396,7 +408,7 @@ def test_프로젝트는_참조사본으로_굳힘_원본을_지워도_같은_
for key in (sc, sp): # 원본이 사라져도 프로젝트는 사본만 봄
kind = refs.kind_of(key)
(world["system"] / kind / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, None)
got = prices.calc(rows, bundle, {}, {}, _area(given))
line = got["줄"][0]
assert not line["까닭"] and line["참조"] == ref # 층 셈 꼴(142-1)
assert line["기준"]["계"] == pytest.approx(live, rel=1e-9)
@@ -438,8 +450,8 @@ def test_층_목록_서고_줄은_이름_단위_기준금액을_실음(world):
sc = _combo(world["system"], [{"참조": {**SYS, "키": sp}, "수량": {"손": 3}}], "셋")
rows = [{"종류": "precombo", "이름": sc}, {"종류": "labor", "이름": "rv"}]
refs.summaries(world["system"], rows)
one = float(Decimal(str(_base(TEN)["계"])) / 10)
assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", pytest.approx(one * 3))
# 723-1 — 안 줄 수량 저장 안 함 → 목록 기준금액 없음(쓰는 쪽 알약으로만 값)
assert (rows[0]["제목"], rows[0]["기준금액"]) == ("셋", None)
assert "기준금액" not in rows[1]
@@ -456,16 +468,17 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
]
sc = _combo(world["system"], lines, "섞음")
saved = _read(world["system"], "precombo", sc)["담은로직"]
assert saved[1:] == [
{"로직": STONE, "입력값": {}, "수량": {"손": 3}, "메모": "돌"},
{"로직": TEN, "입력값": {}, "수량": {"손": 1}, "메모": None},
assert saved[1:] == [ # 723-1 — 안 줄 수량 저장 안 함
{"로직": STONE, "입력값": {}, "수량": None, "메모": "돌"},
{"로직": TEN, "입력값": {}, "수량": None, "메모": None},
]
ten, stone = (Decimal(str(_base(c)["계"])) for c in (TEN, STONE))
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 손셈 — 승격 · TEN 은 원/10㎡(배 10)
want = ten / 10 * 2 + stone * 3 + ten / 10 * 1 # 이은 수량 — 승격 · TEN 은 원/10㎡(배 10)
ref = {**SYS, "키": sc}
given = {f"{refs.ref_id(ref)}#{i}": q for i, q in enumerate((2, 3, 1))}
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 5}}]})
one = labor.calc(doc, {})["줄"][0]
one = labor.calc(doc, given)["줄"][0]
assert one["기준"]["계"] == pytest.approx(float(want))
assert one["반영"]["계"] == pytest.approx(float(want * 5))
assert [x.get("로직") or x["참조"]["키"] for x in one["안"]] == [sp, STONE, TEN]
@@ -482,16 +495,16 @@ def test_사전조합_로직_줄_섞임_합은_손셈_지우기_409_굳힘(world
bundle = prices.to_json(prices.collect([TEN, STONE]))
for key in (sc, sp): # 원본이 사라져도 굳힌 사본으로 같은 값
(world["system"] / refs.kind_of(key) / f"{key}.json").unlink()
got = prices.calc(rows, bundle, {}, {}, None)["줄"][0]
got = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]
assert got["기준"]["계"] == pytest.approx(float(want), rel=1e-9)
def test_사전조합이_자기_자신을_담으면_422_수량_기본_1(world):
def test_사전조합이_자기_자신을_담으면_422_수량_없음(world):
sp = _promote(world["system"])
a = _combo(world["system"], [{"참조": {**SYS, "키": sp}}], "가")
with store.at(world["system"]):
have = store.read("precombo", a)
assert have["문서"]["담은로직"][0]["수량"] == {"손": 1}
assert have["문서"]["담은로직"][0]["수량"] is None # 723-1 — 기본 1 걷음
with pytest.raises(store.StoreError) as e:
store.write(
"precombo",
@@ -535,14 +548,16 @@ def test_사전조합_굳힘은_안_로직_알약도_사본_값_142_2(world, mon
with store.at(world["system"]):
doc = labor.clean({"열": "rv", "담은로직": [{"참조": ref, "수량": {"손": 1}}]})
rows = source.labor_doc_rows(doc)
given = {f"{refs.ref_id(ref)}#0": 1}
kept = rows["참조사본"][refs.ref_id(ref)]["알약"]
assert [(p["로직"], p.get("배")) for p in kept] == [([TEN], 10)]
assert "알약" not in _read(world["system"], "precombo", sc) # 층 문서는 그대로
bundle = prices.to_json(prices.collect([TEN]))
before = prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"]
before = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
changed = {"물량": [{"키": "x|㎡", "이름": "x", "단위": "㎡", "로직": [TEN]}]} # 배 1 로 바뀜
monkeypatch.setattr(combo_store, "combo_pills", lambda row: changed)
assert prices.calc(rows, bundle, {}, {}, None)["줄"][0]["기준"]["계"] == pytest.approx(before)
again = prices.calc(rows, bundle, {}, {}, _area(given))["줄"][0]["기준"]["계"]
assert again == pytest.approx(before)
with store.at(world["system"]):
live = labor.calc(doc, {})["줄"][0]["기준"]["계"]
live = labor.calc(doc, given)["줄"][0]["기준"]["계"]
assert live == pytest.approx(before * 10) # 층 셈은 지금 M01(배 1)
+16 -14
View File
@@ -181,7 +181,10 @@ def test_층_단추_셋_남의_회사_사람_나무():
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert "createLayerBar({" in page and "setActiveLayer(layer, owner)" in page
assert "const company = viewCompany(entry, user);" in page
assert "await main.confirmLeave()" in page.split("const go =")[1].split("navigateTo(")[0]
# 720-4 — 다른 층 = 저장 안 한 고침 물음 뒤 제자리 바꿈(해시 이동 · 페이지 다시 세움 없음)
go = page.split("const go =")[1].split("};")[0]
assert "await main.confirmLeave()" in go and "await switchStructure(next, nextOwner);" in go
assert "navigateTo" not in page
assert 'dispose: () => setActiveLayer("system")' in page
assert "side.reveal(entry.open)" in page
roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts")
@@ -192,9 +195,9 @@ def test_층_단추_셋_남의_회사_사람_나무():
assert "manages(company, user);" in roots.split("export const seesOthers")[1] # 맡은 회사(39-6)
assert "const readOnly = !people && !accessFor(" in roots # 사람 나무 = 고침
assert 'L("M02_PeopleEmpty")' in roots
assert 'const people = entry.root === "personal" && seesOthers(company, user);' in page
assert 'const many = want === "personal" && seesOthers(company, user);' in page
assert '(await fetchOwners("personal", company).catch(() => []))[0]?.owner' in page
assert 'peopleTree("구조물조합", tree.root)' in page
assert "peopleTree(tree.root)" in page
assert 'peopleTree("일위대가"' not in page # 일위대가 사람 나무 벌은 39-2 에서 지움
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
assert (
@@ -223,8 +226,8 @@ def test_층_길_접두어와_고정_풀림():
in labor
)
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
assert "const layer = opt.layer;" in side
assert 'layer === "system"' in side # 도면 양식은 시스템 층에만
assert 'const layer: Layer = "system";' in side # 도면 양식은 시스템 층에만
assert "const drawing = opt.isSystem" in side # 720-4 — 시스템 관리자면 늘
main = _text(M02 / "M02_MasterTemplete_UI_Main.ts")
assert 'sel.layer !== "system"' not in main
assert 'return layer === "project" || isAdmin;' in main
@@ -236,7 +239,7 @@ def test_읽기만_회사용_개인용으로_복사_막대():
assert (
"readOnlyBar?.setColumn(column)" in page
) # 열 고름 → 막대 · 구조물 라이브러리가 함께 받음
assert "accessFor(layer, owner, user).needCompany" in page # 회사 없음 = 마스터로
assert "if (accessFor(lay, who, user).needCompany)" in page # 회사 없음 = 마스터로
assert "buildNeedCompany()" in _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts")
assert 'title: L("M02_Title")' in page # 한 화면 한 제목
layer = _text(LAYER)
@@ -290,15 +293,16 @@ def test_켜진_층_단추는_목록으로_복사_끌기():
roots = _text(M02 / "M02_MasterTemplete_TreePanel_Roots.ts")
assert "if (!current) opt.go(root, owner);\n else opt.reselect?.();" in roots
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer))' in page
assert "layerBar(tree.add, () => void main.openTable(layer))" in page
# 39-6 — 쓸 수 있는 층 전부(볼 회사 주인까지 · 지금 층 뺌) · 대상 폴더 고르는 창
assert 'const toOwner = one === "company" ? companyOwner(company, user) : null;' in page
assert "(to === layer && toOwner === here)) return [];" in page
copy_to = _text(M02 / "M02_MasterTemplete_TreePanel_CopyTo.ts")
assert "listFolders(buildFolders(view))" in copy_to and ".catch(() => null)" in copy_to
assert "const dest = await askCopyFolder(api.fetch, to, list.length);" in _text(
assert "const dest = await askCopyFolder(api.fetch, to, clip.refs.length);" in copy_to
assert "void copyToLayer({ fetch: api.fetch, paste: paste_ }, clip, to);" in _text(
M02 / "M02_MasterTemplete_TreePanel.ts"
)
) # 720-2 — 몸은 CopyTo 로 뗌(TreePanel 700 줄)
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
assert "if (opt.canEdit) drag.arm(button, one.id);" in panel
drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts")
@@ -315,12 +319,10 @@ def test_구조물_조합_제목_줄_새_항목():
r"\s*\.\.\.\(extras\.newItem \? \[extras\.newItem\] : \[\]\),?\s*\]",
main,
)
assert "if (extras.newItem) extras.newItem.hidden = !combo;" in main
# 720-4 — 쓸 수 있는 층만 = 편집 칸이 고른 것 권한으로 가림(층이 제자리에서 바뀜)
assert "extras.newItem.hidden = !combo || !sel || !writable(sel);" in main
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert (
"...(canEdit(layer, isAdmin) ? { newItem: newItemButton(() => void openNew()) } : {})"
in page
)
assert "newItem: newItemButton(() => void openNew())," in page
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
assert 'return at?.kind === "folder" ? at.path : [];' in panel # 고른 폴더(39-5 folder())
assert "const newItem = (): void => void addItem(folder());" in panel
+2 -2
View File
@@ -51,9 +51,9 @@ def test_통째_지우기_시스템_관리자만_판_쓰는_곳(world, master_co
def test_화면_시스템_관리자_마스터만_물음_두_번_읽기만():
side = (M02 / "M02_MasterTemplete_UI_Side.ts").read_text("utf-8")
assert 'layer === "system" && opt.isAdmin' in side
assert "const practice = opt.isSystem" in side # 720-4 — 시스템 관리자면 늘
order = side.split('className: "m02-side__lists ui-sidebar-panel",')[1].split("});")[0]
assert order.index("extras.도면") < order.index("practice") # 도면 양식 아래
assert order.index("drawing ?") < order.index("practice ?") # 도면 양식 아래
view = (M02 / "M02_MasterTemplete_Practice.ts").read_text("utf-8")
assert view.count("showConfirmDialog(") == 2
assert "dropPractice(" in view and "createInputField" not in view # 고칠 칸 없음
+13 -7
View File
@@ -72,11 +72,17 @@ def test_사전조합_층_단추는_페이지를_다시_안_세움():
assert "history.replaceState(" in pre and "side.refreshPrecombo()" in pre
assert "navigateTo" not in pre and "setActiveLayer" not in pre # 페이지 층 · 해시 이동 없음
assert "entry.pre = pre.root;" in pre # 구조물 조합 층 이동(go) 때도 주소에 남음
bar = page.split("const preBar = (): HTMLElement =>")[1].split("const side = buildSide(")[0]
assert "go: (next) => setPre(next)," in bar and "main.open(precomboList(pre))" in bar
# 구조물 조합 층 단추는 그대로 해시 이동 한 벌
assert page.count('layerBar("') == 1
assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page
bar = page.split("const preBar = (add: HTMLElement): HTMLElement =>")[1].split(
"const side = buildSide("
)[0]
# 720-1 — 다른 층 = 저장 안 한 고침 물음 → 그 층 나무 + 목록표
go = bar.split("go: (next) =>")[1].split("reselect:")[0]
assert ".confirmLeave()" in go and "setPre(next), void main.open(precomboList(pre))" in go
sk = page.split("const skBar = (")[1].split("const side = buildSide(")[0]
assert ".confirmLeave()" in sk and "void main.open(sangeunList(sk))" in sk.split("reselect:")[0]
# 구조물 조합 층 단추 한 벌(720-4 제자리 바꿈)
assert page.count("layerBar(") == 1
assert "layerBar(tree.add, () => void main.openTable(layer))," in page
assert "precombo: {\n scope: () => pre,\n bar: preBar," in page # 706-1 more 덧붙음
@@ -88,7 +94,7 @@ def test_고름에_owner_권한_서고_길에_명시_owner():
assert "listTemplates(at.layer, null, at.owner)" in lib_side
main = _text("M02_MasterTemplete_UI_Main.ts")
assert (
"const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin);"
"const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin());"
in main
)
assert "readTemplate(next.layer, next.kind, next.name, next.projectId, next.owner)" in main
@@ -148,7 +154,7 @@ def test_왼쪽_칸_차례_일위대가_구조물조합_구분선_불러오기_7
"summary,",
"createDivider()",
"hopyo.title",
"drawing ?",
"drawing,",
"practice ?",
]
at = [order.index(m) for m in marks]
+1 -1
View File
@@ -180,7 +180,7 @@ def test_면적_오른쪽_일위대가_패널은_공용_틀_손잡이():
def test_첫_화면과_닫은_뒤는_구조물집계표():
page = _text(M02 / "M02_MasterTemplete_UI_Page.ts")
assert re.search(r'kind: "table",\s*name: STRUCTURE_TABLE', page)
assert "main.open(sel ?? home)" in page and "void main.open(home);" in page
assert "main.open(sel ?? home())" in page and "void main.open(home());" in page
def test_좌측_고름_표시는_열린_것_한_줄뿐():