feat(규격 열 고르기): summary_item spec · 사본이 고른 열 굳힘 · 같은 항목 = 열까지 · B07 한 장 = 항목 + 열 · 쓰는 열 지움 409 (PLAN 704-4)
- summary_item spec(파이썬 · TS 짝) — 없으면 기본 열 · 모르는 키 거절 그대로 - 사본 = 고른 열 수량을 손으로 굳힘 · manifest 규격 · same_item 에 spec(열 바꿈 = 새 사본 · 덮개 열쇠 S:폴더 그대로) - B07 구조물 도면 키 = 항목 + 열 해시 — 열이 다르면 따로 장 - M02 일위대가 [저장] — 쓰는 사본 있는 열 지움 · 이름 바꿈 409 - 항목 목록 labor.규격(열 · 열마다 합계) — 고르기 창 열 단추 - 시험 test_704_4_spec 7(열 바꿈 뒤 덮개 남음 · 짝 잃음 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,14 @@
|
|||||||
"""B07 구조물 도면(376 · 요구 241-1) — B05 · B06 배치에 연결된 구조물 조합 도면 목록 · 단건.
|
"""B07 구조물 도면(376 · 요구 241-1) — B05 · B06 배치에 연결된 구조물 조합 도면 목록 · 단건.
|
||||||
|
|
||||||
조합 키 = 항목 사본 폴더 이름(`snapshot_folder_name`) — 같은 조합을 여러 배치가 써도 한 장 ·
|
조합 키 = 항목 사본 폴더 이름(`snapshot_folder_name`) + 규격 열(704-4 · `-s해시`) — 같은 조합 ·
|
||||||
|
같은 열을 여러 배치가 써도 한 장 · 열이 다르면 따로(표제 = 항목 이름 「… (H=2.0)」) ·
|
||||||
더한장은 장마다 · id `structure_{키}[_{장id}]` · 차례 = 도번 · 도면 = 프로젝트 사본 `도면.json` +
|
더한장은 장마다 · id `structure_{키}[_{장id}]` · 차례 = 도번 · 도면 = 프로젝트 사본 `도면.json` +
|
||||||
얹는 도각(M02 `structure_frame` · 도번 · 도면명). 고치기는 M02 · B08 — 여기는 읽기만.
|
얹는 도각(M02 `structure_frame` · 도번 · 도면명). 고치기는 M02 · B08 — 여기는 읽기만.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -38,6 +40,8 @@ def structure_sheets(project_root: Path) -> list[dict[str, Any]]:
|
|||||||
if "도면" not in copy["부분"] or not item.get("key"):
|
if "도면" not in copy["부분"] or not item.get("key"):
|
||||||
continue
|
continue
|
||||||
key = snapshot_folder_name(item)
|
key = snapshot_folder_name(item)
|
||||||
|
if item.get("spec"): # 규격 열(704-4) — 열 이름은 아무 글이라 id 에는 해시만
|
||||||
|
key += f"-s{hashlib.sha1(str(item['spec']).encode()).hexdigest()[:8]}"
|
||||||
if key in docs:
|
if key in docs:
|
||||||
continue
|
continue
|
||||||
doc = read_json(snapshot.structures_dir(project_root) / copy["폴더"] / "도면.json")
|
doc = read_json(snapshot.structures_dir(project_root) / copy["폴더"] / "도면.json")
|
||||||
|
|||||||
@@ -86,10 +86,11 @@ def now_text() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def same_item(manifest: Any, item: dict[str, Any]) -> bool:
|
def same_item(manifest: Any, item: dict[str, Any]) -> bool:
|
||||||
|
"""같은 항목 · 같은 규격 열(704-4 · 열이 바뀌면 다른 사본 · 덮개는 폴더 열쇠라 그대로 남음)."""
|
||||||
origin = manifest.get("원본") if isinstance(manifest, dict) else None
|
origin = manifest.get("원본") if isinstance(manifest, dict) else None
|
||||||
if not isinstance(origin, dict):
|
if not isinstance(origin, dict):
|
||||||
return False
|
return False
|
||||||
return all(origin.get(k) == item.get(k) for k in ("layer", "owner", "key"))
|
return all(origin.get(k) == item.get(k) for k in ("layer", "owner", "key", "spec"))
|
||||||
|
|
||||||
|
|
||||||
def origin_of(item: dict[str, Any]) -> dict[str, Any]:
|
def origin_of(item: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -142,6 +143,9 @@ def write_snapshot(target: Path, item: dict[str, Any], owner_id: str, extra: dic
|
|||||||
atomic_write_json(target / name, content)
|
atomic_write_json(target / name, content)
|
||||||
stamp = now_text()
|
stamp = now_text()
|
||||||
source = version_of(source_files(files))
|
source = version_of(source_files(files))
|
||||||
|
spec = ((files.get(LABOR_FILE) or {}).get("줄") or {}).get("규격") # 굳힌 규격 열(704-4)
|
||||||
|
if isinstance(spec, dict):
|
||||||
|
extra = {**extra, "규격": {"이름": spec.get("이름"), "열": spec.get("기본")}}
|
||||||
manifest = {
|
manifest = {
|
||||||
"원본": origin_of(item),
|
"원본": origin_of(item),
|
||||||
"판": source,
|
"판": source,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
모으는 것 = `{파일 이름: 내용}`:
|
모으는 것 = `{파일 이름: 내용}`:
|
||||||
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA ·
|
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA ·
|
||||||
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄)
|
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄 ·
|
||||||
|
규격 열 = summary_item `spec` 열 수량을 손으로 굳힘 · 704-4)
|
||||||
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) +
|
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) +
|
||||||
참조사본(단가승격 · 사전조합을 값으로 굳힘) ·
|
참조사본(단가승격 · 사전조합을 값으로 굳힘) ·
|
||||||
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
|
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
|
||||||
@@ -146,7 +147,9 @@ def build_source(item: dict[str, Any]) -> dict[str, Any]:
|
|||||||
files[name] = part
|
files[name] = part
|
||||||
doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None
|
doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None
|
||||||
if isinstance(doc, dict):
|
if isinstance(doc, dict):
|
||||||
labor = labor_doc_rows(doc)
|
from M02_MasterTemplete import M02_Labor_Spec as labor_spec
|
||||||
|
|
||||||
|
labor = labor_doc_rows(labor_spec.freeze(doc, item.get("spec"))) # 고른 규격 열(704-4)
|
||||||
else:
|
else:
|
||||||
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
|
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
|
||||||
if labor is not None:
|
if labor is not None:
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ def freeze(doc: dict, col: Any = None) -> dict:
|
|||||||
use = column(doc, col)
|
use = column(doc, col)
|
||||||
if use is None:
|
if use is None:
|
||||||
return doc
|
return doc
|
||||||
got = at(doc, use)
|
got = {k: v for k, v in at(doc, use).items() if k != "계산값"} # 기본 열 계산값은 버림
|
||||||
return {**got, "규격": {"이름": doc["규격"].get("이름"), "열": [use], "기본": use, "수량": {}}}
|
return {**got, "규격": {"이름": doc["규격"].get("이름"), "열": [use], "기본": use, "수량": {}}}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,3 +161,24 @@ def users(layer_root: Path, keys: set[str], cols: set[str]) -> list[str]:
|
|||||||
if same:
|
if same:
|
||||||
found.append(f"{path.parents[3].name}/{path.parent.name}")
|
found.append(f"{path.parents[3].name}/{path.parent.name}")
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def guard(path: Path, new: Any, column: str) -> None:
|
||||||
|
"""[저장] 때 빠진 규격 열(지움 · 이름 바꿈)을 쓰는 프로젝트 사본이 있으면 409 —
|
||||||
|
지금 층(`store.root`) 그 열 항목(SS 키 · 옛 키) 기준."""
|
||||||
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||||
|
from M02_MasterTemplete.M02_Item_Source import read_json
|
||||||
|
|
||||||
|
old = (read_json(path) or {}).get("규격") if path.is_file() else None
|
||||||
|
gone = set((old or {}).get("열") or []) - set(((new or {}).get("규격") or {}).get("열") or [])
|
||||||
|
if not gone:
|
||||||
|
return
|
||||||
|
rows = [r for r in store.read_combos().get("줄", []) if str(r.get("열")) == column]
|
||||||
|
keys = {str(k) for r in rows for k in [r.get("키"), *(r.get("옛키") or [])] if k}
|
||||||
|
used = users(store.root(), keys, gone)
|
||||||
|
if used:
|
||||||
|
raise store.StoreError(
|
||||||
|
409,
|
||||||
|
f"규격 열 「{', '.join(sorted(gone))}」 을 쓰는 프로젝트 사본 {len(used)}개 — 지울 수 없음 "
|
||||||
|
f"({', '.join(used[:3])}{' …' if len(used) > 3 else ''})",
|
||||||
|
)
|
||||||
|
|||||||
@@ -119,6 +119,14 @@ def _list_extras(folder: Path, col: str) -> dict[str, Any]:
|
|||||||
"기준": _number(calc.get("기준합")), # 수량 없는 일위대가 = 단가 합(39-8 B2)
|
"기준": _number(calc.get("기준합")), # 수량 없는 일위대가 = 단가 합(39-8 B2)
|
||||||
"단위": doc.get("단위"), # 일위대가 문서 단위(구조물 열 단위 아님 · B3)
|
"단위": doc.get("단위"), # 일위대가 문서 단위(구조물 열 단위 아님 · B3)
|
||||||
}
|
}
|
||||||
|
spec = doc.get("규격") if isinstance(doc.get("규격"), dict) else None
|
||||||
|
if spec and spec.get("열"): # 규격 열(704-4) — 고르기 창 열 단추 · 열마다 합계
|
||||||
|
sums = calc.get("규격합") or {}
|
||||||
|
labor["규격"] = {
|
||||||
|
"이름": spec.get("이름"),
|
||||||
|
"열": [{"이름": c, "합계": _number(sums.get(c))} for c in spec["열"]],
|
||||||
|
"기본": spec.get("기본"),
|
||||||
|
}
|
||||||
return {"수정일": stamps, "labor": labor}
|
return {"수정일": stamps, "labor": labor}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,10 @@ def write(kind: str, name: str, version: str, doc: Any, screen: bool = False) ->
|
|||||||
if kind == "table" and name == STRUCTURE_TABLE:
|
if kind == "table" and name == STRUCTURE_TABLE:
|
||||||
doc = tree.normalize_table(doc) # 경로 · 머리 줄 수 · 빈 폴더(표 저장 · 나무 고침 한 줄기)
|
doc = tree.normalize_table(doc) # 경로 · 머리 줄 수 · 빈 폴더(표 저장 · 나무 고침 한 줄기)
|
||||||
doc = _server(kind, doc)
|
doc = _server(kind, doc)
|
||||||
|
if kind == "labor": # 쓰는 규격 열 지움 막음(704-4)
|
||||||
|
from M02_MasterTemplete import M02_Labor_Spec as labor_spec
|
||||||
|
|
||||||
|
labor_spec.guard(path, doc, name)
|
||||||
fresh: list[dict[str, Any]] = []
|
fresh: list[dict[str, Any]] = []
|
||||||
with _LOCK:
|
with _LOCK:
|
||||||
have = version_of(path.read_bytes()) if path.is_file() else ""
|
have = version_of(path.read_bytes()) if path.is_file() else ""
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
"""구조물 집계표 항목 참조 `summary_item` — 모양 검사 · 스냅샷 폴더 이름 (PLAN 21-1).
|
"""구조물 집계표 항목 참조 `summary_item` — 모양 검사 · 스냅샷 폴더 이름 (PLAN 21-1).
|
||||||
|
|
||||||
summary_item = {layer: "system"|"company"|"personal",
|
summary_item = {layer: "system"|"company"|"personal",
|
||||||
owner: null|"{회사}"|"{회사}/{사용자}", key: "SS000004", name: "…"}
|
owner: null|"{회사}"|"{회사}/{사용자}", key: "SS000004", name: "…",
|
||||||
|
spec?: "2.0"}
|
||||||
|
|
||||||
|
`spec`(PLAN 704-4) = 그 항목 일위대가 규격 열 이름 — 없으면 기본 열(옛 기록 · 규격 없는 항목).
|
||||||
|
|
||||||
항목 키(PLAN 26-4a) — 마스터 `SS000004` · 회사 `SC{회사}-000001` · 개인 `SP{사용자}-000001`
|
항목 키(PLAN 26-4a) — 마스터 `SS000004` · 회사 `SC{회사}-000001` · 개인 `SP{사용자}-000001`
|
||||||
(머리의 번호 = owner 의 회사 · 사용자). 옛 `SS` 키는 어느 층이든 그대로 받음 — 회사 · 개인 층이 새
|
(머리의 번호 = owner 의 회사 · 사용자). 옛 `SS` 키는 어느 층이든 그대로 받음 — 회사 · 개인 층이 새
|
||||||
@@ -28,15 +31,17 @@ _OWNER = {
|
|||||||
"personal": re.compile(r"^\d+/\d+$"),
|
"personal": re.compile(r"^\d+/\d+$"),
|
||||||
}
|
}
|
||||||
_NAME_MAX = 200
|
_NAME_MAX = 200
|
||||||
|
_SPEC_MAX = 20 # 규격 열 이름 상한(`M02_Labor_Spec.NAME_MAX`)
|
||||||
|
|
||||||
|
|
||||||
def parse_summary_item(value: Any) -> dict[str, Any] | None:
|
def parse_summary_item(value: Any) -> dict[str, Any] | None:
|
||||||
"""모양이 맞으면 `{layer, owner, key, name}` 새 dict · 빈 값은 None · 틀리면 ValueError."""
|
"""모양이 맞으면 `{layer, owner, key, name, spec?}` 새 dict · 빈 값은 None ·
|
||||||
|
틀리면 ValueError."""
|
||||||
if value is None:
|
if value is None:
|
||||||
return None
|
return None
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise ValueError("집계 항목(summary_item)은 객체여야 합니다.")
|
raise ValueError("집계 항목(summary_item)은 객체여야 합니다.")
|
||||||
extra = set(value) - {"layer", "owner", "key", "name"}
|
extra = set(value) - {"layer", "owner", "key", "name", "spec"}
|
||||||
if extra:
|
if extra:
|
||||||
raise ValueError(f"집계 항목에 모르는 칸이 있습니다: {', '.join(sorted(extra))}")
|
raise ValueError(f"집계 항목에 모르는 칸이 있습니다: {', '.join(sorted(extra))}")
|
||||||
layer = value.get("layer")
|
layer = value.get("layer")
|
||||||
@@ -59,7 +64,11 @@ def parse_summary_item(value: Any) -> dict[str, Any] | None:
|
|||||||
name = value.get("name")
|
name = value.get("name")
|
||||||
if not isinstance(name, str) or not name.strip() or len(name) > _NAME_MAX:
|
if not isinstance(name, str) or not name.strip() or len(name) > _NAME_MAX:
|
||||||
raise ValueError(f"집계 항목 name 은 1~{_NAME_MAX}자 글입니다.")
|
raise ValueError(f"집계 항목 name 은 1~{_NAME_MAX}자 글입니다.")
|
||||||
return {"layer": layer, "owner": owner, "key": key, "name": name}
|
spec = value.get("spec")
|
||||||
|
if spec is not None and (not isinstance(spec, str) or not 0 < len(spec.strip()) <= _SPEC_MAX):
|
||||||
|
raise ValueError(f"집계 항목 spec(규격 열)은 1~{_SPEC_MAX}자 글입니다.")
|
||||||
|
out = {"layer": layer, "owner": owner, "key": key, "name": name}
|
||||||
|
return {**out, "spec": spec.strip()} if spec is not None else out
|
||||||
|
|
||||||
|
|
||||||
#: 옵션 그룹 id(PLAN 46 · `M02_SO_Groups.GROUP_ID` 와 같은 규칙)
|
#: 옵션 그룹 id(PLAN 46 · `M02_SO_Groups.GROUP_ID` 와 같은 규칙)
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
* 구조물 집계표 항목 참조(PLAN 21-1) — 구조물 · 관 지점이 「어느 집계표 항목」인지 가리키는 값.
|
* 구조물 집계표 항목 참조(PLAN 21-1) — 구조물 · 관 지점이 「어느 집계표 항목」인지 가리키는 값.
|
||||||
*
|
*
|
||||||
* summary_item: { layer: "system"|"company"|"personal", owner: null|"{회사}"|"{회사}/{사용자}",
|
* summary_item: { layer: "system"|"company"|"personal", owner: null|"{회사}"|"{회사}/{사용자}",
|
||||||
* key: "SS000004", name: "돌기슭막이 메쌓기 1.2m" }
|
* key: "SS000004", name: "돌기슭막이 메쌓기 1.2m", spec?: "2.0" }
|
||||||
|
*
|
||||||
|
* spec(PLAN 704-4) = 그 항목 일위대가 규격 열 이름 — 없으면 기본 열(서버 `parse_summary_item` 과 짝).
|
||||||
*
|
*
|
||||||
* 없으면 null(= 미지정 · 집계표에 안 들어감). 옵션(options) 밖의 칸이다.
|
* 없으면 null(= 미지정 · 집계표에 안 들어감). 옵션(options) 밖의 칸이다.
|
||||||
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
* import 없음 — 시험이 이 파일만 옮겨 돌림.
|
||||||
@@ -16,6 +18,8 @@ export interface SummaryItem {
|
|||||||
owner: string | null;
|
owner: string | null;
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
/** 규격 열 이름(704-4) — 없으면 기본 열 */
|
||||||
|
spec?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LAYERS: readonly string[] = ["system", "company", "personal"];
|
const LAYERS: readonly string[] = ["system", "company", "personal"];
|
||||||
@@ -30,7 +34,8 @@ export function isSummaryItem(value: unknown): value is SummaryItem {
|
|||||||
(item.owner === null || typeof item.owner === "string") &&
|
(item.owner === null || typeof item.owner === "string") &&
|
||||||
typeof item.key === "string" &&
|
typeof item.key === "string" &&
|
||||||
item.key !== "" &&
|
item.key !== "" &&
|
||||||
typeof item.name === "string"
|
typeof item.name === "string" &&
|
||||||
|
(item.spec === undefined || typeof item.spec === "string")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
"""704-4 규격 열 — 설계 고르기 = 열(summary_item `spec`) · 사본이 고른 열을 굳힘 ·
|
||||||
|
열 바꿈 뒤 B09 덮개 남음 · B07 한 장 판정 = 항목 + 열 · 열 지움 막음(쓰는 사본).
|
||||||
|
임시 저장소(실제 마스터 · storage 안 씀 · M01 읽기만).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from B07_DesignDetail.B07_DesignDetail_Router_Support_Structure import structure_sheets
|
||||||
|
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
|
||||||
|
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
|
||||||
|
from common_util.common_util_summary_item import parse_summary_item
|
||||||
|
from config import config_system
|
||||||
|
from M02_MasterTemplete import M02_Item_Snapshot as snap
|
||||||
|
from M02_MasterTemplete import M02_Labor_Spec as spec
|
||||||
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
||||||
|
from resources.tester.helper_m02_master_sample import SAMPLE
|
||||||
|
|
||||||
|
MS = {"layer": "system", "owner": None, "key": "SS000022", "name": "돌쌓기"}
|
||||||
|
SPEC = {
|
||||||
|
"이름": "H",
|
||||||
|
"열": ["1.5", "2.5"],
|
||||||
|
"기본": "1.5",
|
||||||
|
"수량": {"GF000219": {"1.5": 2, "2.5": 3}},
|
||||||
|
}
|
||||||
|
LABOR = {
|
||||||
|
"종류": "일위대가",
|
||||||
|
"열": "ms",
|
||||||
|
"단위": "m",
|
||||||
|
"담은로직": [
|
||||||
|
{"로직": "GF000219", "수량": {"손": 2}}, # 메쌓기 — 열마다 수량
|
||||||
|
{"로직": "GF000119", "수량": {"손": 0.5}}, # 노체포설 — 공통
|
||||||
|
],
|
||||||
|
"자재": [],
|
||||||
|
"규격": SPEC,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── summary_item spec ──────────────────────────────────────────────────────
|
||||||
|
def test_summary_item_spec_있음_없음():
|
||||||
|
assert parse_summary_item({**MS, "spec": " 2.5 "}) == {**MS, "spec": "2.5"}
|
||||||
|
assert parse_summary_item(MS) == MS # 옛 기록 = 칸 없음 그대로
|
||||||
|
with pytest.raises(ValueError, match="모르는 칸"):
|
||||||
|
parse_summary_item({**MS, "열": "2.5"})
|
||||||
|
for bad in ("", "x" * 21, 2.5):
|
||||||
|
with pytest.raises(ValueError, match="spec"):
|
||||||
|
parse_summary_item({**MS, "spec": bad})
|
||||||
|
|
||||||
|
|
||||||
|
# ── 굳히기 ─────────────────────────────────────────────────────────────────
|
||||||
|
def test_굳히기_고른_열_지운_열_옛_문서():
|
||||||
|
got = spec.freeze({**LABOR, "계산값": {"x": 1}}, "2.5")
|
||||||
|
assert got["담은로직"][0]["수량"] == {"손": 3} and got["담은로직"][1]["수량"] == {"손": 0.5}
|
||||||
|
assert got["규격"] == {"이름": "H", "열": ["2.5"], "기본": "2.5", "수량": {}}
|
||||||
|
assert "계산값" not in got
|
||||||
|
assert spec.freeze(LABOR, "지운열")["담은로직"][0]["수량"] == {"손": 2} # = 기본
|
||||||
|
old = {k: v for k, v in LABOR.items() if k != "규격"}
|
||||||
|
assert spec.freeze(old, "2.5") is old
|
||||||
|
|
||||||
|
|
||||||
|
# ── 임시 저장소 사본 ───────────────────────────────────────────────────────
|
||||||
|
@pytest.fixture
|
||||||
|
def world(tmp_path: Path, monkeypatch) -> dict[str, Any]:
|
||||||
|
system = tmp_path / "master"
|
||||||
|
system.mkdir()
|
||||||
|
shutil.copy2(SAMPLE / layers.COMBO_FILE, system / layers.COMBO_FILE)
|
||||||
|
for kind in ("table", "structure", "area"):
|
||||||
|
shutil.copytree(SAMPLE / kind, system / kind)
|
||||||
|
(system / "labor").mkdir()
|
||||||
|
(system / "labor" / "ms.json").write_text(json.dumps(LABOR), encoding="utf-8")
|
||||||
|
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||||||
|
storage = tmp_path / "storage"
|
||||||
|
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
|
||||||
|
root = storage / "3" / "12" / "proj"
|
||||||
|
(root / "B05_Profile/route").mkdir(parents=True)
|
||||||
|
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
|
||||||
|
return {"system": system, "root": root}
|
||||||
|
|
||||||
|
|
||||||
|
def _save(root: Path, specs: list[str | None]) -> dict[str, list[str]]:
|
||||||
|
rows = [
|
||||||
|
{
|
||||||
|
"structure_id": f"st{n}",
|
||||||
|
"type_id": "masonry_dry",
|
||||||
|
"placement": "interval",
|
||||||
|
"chainage_m": 85 + n * 100,
|
||||||
|
"start_m": 80 + n * 100,
|
||||||
|
"end_m": 90 + n * 100,
|
||||||
|
"options": {"height_m": 2.0, "length_m": 10},
|
||||||
|
"summary_item": {**MS, **({"spec": s} if s else {})},
|
||||||
|
}
|
||||||
|
for n, s in enumerate(specs)
|
||||||
|
]
|
||||||
|
(root / "B05_Profile/route/structures.json").write_text(
|
||||||
|
json.dumps({"revision": 1, "structures": rows}, ensure_ascii=False), encoding="utf-8"
|
||||||
|
)
|
||||||
|
(root / "B04_PreProcess/drainage/edits/pipe_points.json").write_text(
|
||||||
|
'{"points": []}', encoding="utf-8"
|
||||||
|
)
|
||||||
|
return snap.sync_snapshots(root)
|
||||||
|
|
||||||
|
|
||||||
|
def _block(root: Path, owner: str) -> tuple[dict, dict]:
|
||||||
|
got = daega_inputs(root)
|
||||||
|
return got, next(b for b in got["blocks"] if b["id"] == got["owners"][owner])
|
||||||
|
|
||||||
|
|
||||||
|
def test_사본이_고른_열을_굳힘_manifest_규격(world):
|
||||||
|
root = world["root"]
|
||||||
|
assert _save(root, ["2.5"])["만듦"] == ["st0"]
|
||||||
|
folder = snap.structures_dir(root) / "st0"
|
||||||
|
doc = json.loads((folder / "일위대가.json").read_text(encoding="utf-8"))["줄"]
|
||||||
|
assert [x["수량"] for x in doc["담은로직"]] == [{"손": 3}, {"손": 0.5}]
|
||||||
|
manifest = json.loads((folder / snap.MANIFEST_NAME).read_text(encoding="utf-8"))
|
||||||
|
assert manifest["규격"] == {"이름": "H", "열": "2.5"} and manifest["원본"]["spec"] == "2.5"
|
||||||
|
assert _save(root, ["2.5"])["만듦"] == [] # 같은 항목 · 같은 열 = 그대로
|
||||||
|
|
||||||
|
|
||||||
|
def test_열_바꿈_뒤_덮개_남음(world):
|
||||||
|
root = world["root"]
|
||||||
|
_save(root, ["1.5"])
|
||||||
|
_, before = _block(root, "st0")
|
||||||
|
cids = [ln["cid"] for ln in before["lines"]]
|
||||||
|
assert before["cover"] == "S:st0" and [ln["qty"] for ln in before["lines"]] == [2, 0.5]
|
||||||
|
cover = {before["cover"]: {cids[1]: {"qty": 0.7}}} # 공통 줄 품을 고침
|
||||||
|
got = _save(root, ["2.5"])
|
||||||
|
assert got["지움"] == ["st0"] and got["만듦"] == ["st0"] # 열 바꿈 = 새 사본
|
||||||
|
after_all, after = _block(root, "st0")
|
||||||
|
assert after["cover"] == before["cover"] and [ln["cid"] for ln in after["lines"]] == cids
|
||||||
|
sheets = {"labor": [], "material": [], "machine": [], "gaps": []}
|
||||||
|
out = compute_labor(after_all, sheets, {}, cover)
|
||||||
|
lines = next(b for b in out["daega_blocks"] if b["id"] == after["id"])["lines"]
|
||||||
|
assert lines[0]["qty"] == 3 # 새 열 수량
|
||||||
|
assert lines[1]["qty"] == 0.7 and lines[1]["covered"] == ["qty"] # 덮개 그대로
|
||||||
|
assert out["daega_orphans"] == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_B07_한_장_판정_항목_더하기_열(world):
|
||||||
|
root = world["root"]
|
||||||
|
_save(root, ["1.5", "1.5", "2.5"])
|
||||||
|
ids = [s["id"] for s in structure_sheets(root) if s["id"].startswith("structure_system")]
|
||||||
|
assert len(set(ids)) == 2 # 같은 열 둘 = 한 장 · 다른 열 = 따로
|
||||||
|
|
||||||
|
|
||||||
|
def test_열_지움_쓰는_사본_찾기(world):
|
||||||
|
root = world["root"]
|
||||||
|
_save(root, ["2.5", None])
|
||||||
|
assert spec.users(world["system"], {"SS000022"}, {"2.5"}) == ["proj/st0"]
|
||||||
|
assert spec.users(world["system"], {"SS000022"}, {"1.5"}) == []
|
||||||
|
assert spec.users(world["system"], {"SS000001"}, {"2.5"}) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_쓰는_열_지움_409_안_쓰는_열_됨(world):
|
||||||
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
||||||
|
|
||||||
|
root = world["root"]
|
||||||
|
_save(root, ["2.5"])
|
||||||
|
path = world["system"] / "labor" / "ms.json"
|
||||||
|
with store.at(world["system"]):
|
||||||
|
with pytest.raises(store.StoreError) as got:
|
||||||
|
spec.guard(path, {"규격": {**SPEC, "열": ["1.5"]}}, "ms")
|
||||||
|
assert got.value.status == 409 and "proj/st0" in str(got.value.detail)
|
||||||
|
spec.guard(path, {"규격": {**SPEC, "열": ["2.5", "3.0"]}}, "ms") # 1.5 = 안 씀
|
||||||
|
spec.guard(path, {"규격": SPEC}, "ms")
|
||||||
Reference in New Issue
Block a user