Merge remote-tracking branch 'origin/dev' into sub_laptop_8
This commit is contained in:
@@ -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,163 @@
|
||||
"""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
|
||||
@@ -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):
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
"""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(add)" 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 = opt.isSystem" in side and "const practice = opt.isSystem" in side
|
||||
hopyo = _text("M02_MasterTemplete_Hopyo_Side.ts")
|
||||
assert "buildHopyoLedger({ scope: pageScope(), canWrite: opt.canWrite() })" in hopyo
|
||||
@@ -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["줄"]] == [["노무비"], ["재료비"]]
|
||||
@@ -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"}]
|
||||
@@ -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 # 상세 제목
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 # 결과 층 = 쓸 수 있는 층만
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 # 고칠 칸 없음
|
||||
|
||||
@@ -72,15 +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]
|
||||
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 "main.confirmLeave()" in go and "setPre(next), void main.open(precomboList(pre))" in go
|
||||
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 "main.confirmLeave()" in sk and "void main.open(sangeunList(sk))" in sk.split("reselect:")[0]
|
||||
# 구조물 조합 층 단추는 그대로 해시 이동 한 벌
|
||||
assert page.count('layerBar("') == 1
|
||||
assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page
|
||||
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 덧붙음
|
||||
|
||||
|
||||
@@ -92,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
|
||||
|
||||
@@ -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_좌측_고름_표시는_열린_것_한_줄뿐():
|
||||
|
||||
Reference in New Issue
Block a user