From 760e43c8df2599137911e852d22c2ae69c5eff99 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Wed, 30 Sep 2026 09:04:39 +0900 Subject: [PATCH] =?UTF-8?q?feat(M02):=20=ED=95=AD=EB=AA=A9=20=ED=82=A4=20?= =?UTF-8?q?=EC=B2=B4=EA=B3=84=20=C2=B7=20=ED=92=88=EB=B2=88=20=EC=83=88=20?= =?UTF-8?q?=EB=AA=A8=EC=96=91(PLAN=2026-4a)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 항목 키 = 마스터 SS + 6자리 · 회사 SC{회사}- + 6자리 · 개인 SP{사용자}- + 6자리(장부 key_head · 층 폴더로 가름) · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사 · 개인에 저장 · 새 항목은 그 층 새 키 + 본뜬 곳 - 회사 · 개인 층의 옛 SS 키는 장부를 맞출 때(처음 읽을 때 한 번) 새 모양으로 옮김(숫자 여섯 그대로 · 겹치면 다음 번호) · 옛 키는 줄 옛키 에 남김 · 옛 키로도 찾음(store.find_combo · 나무 고침 · 입력 목록 · 항목 복사 · M02_Item_Source.row_of) - summary_item 키 검사 — SC · SP 는 그 층 · 머리 번호 = owner 일 때만 · 옛 SS 는 어느 층이든 받음 - 품번(도번 SD · 면적번 SA · 재료번 SM) 새 일련 = 붙임표 + 6자리(SD-000001) — 옛 8자리(제목행 자리) · 12장 옛 6자리(SD010101)와 모양이 달라 안 겹침 · 옛 번호는 그대로 유효 · 새 모양끼리 셈 · 지운 번호 안 씀 - 시험 test_m02_item_keys 7(층마다 키 모양 · 안 바뀜 · 다시 안 씀 · 복사 본뜬 곳 · 옛 SS 옮김 · 옛 키로 찾음 · 겹친 숫자 · 참조 키 모양 · 120 형제 깊이 6 번호 · 층마다 번호 따로 · 키대장 안 겹침) · 번호 · 키 시험 기대값을 새 모양으로 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL --- M02_MasterTemplete/M02_Item_Source.py | 3 +- M02_MasterTemplete/M02_MasterTemplete_Book.py | 91 ++++++-- .../M02_MasterTemplete_Inputs.py | 2 +- .../M02_MasterTemplete_Items.py | 2 +- .../M02_MasterTemplete_Router_Items.py | 5 +- .../M02_MasterTemplete_Store.py | 42 ++-- M02_MasterTemplete/M02_MasterTemplete_Tree.py | 2 +- common_util/common_util_summary_item.py | 30 ++- .../spreadsheet/test_spreadsheet_store.py | 8 +- resources/tester/test_m02_item_keys.py | 213 ++++++++++++++++++ resources/tester/test_m02_layers_store.py | 6 +- resources/tester/test_m02_structure.py | 52 ++--- resources/tester/test_m02_tree.py | 6 +- 13 files changed, 387 insertions(+), 75 deletions(-) create mode 100644 resources/tester/test_m02_item_keys.py diff --git a/M02_MasterTemplete/M02_Item_Source.py b/M02_MasterTemplete/M02_Item_Source.py index 895dda70..66b4dca9 100644 --- a/M02_MasterTemplete/M02_Item_Source.py +++ b/M02_MasterTemplete/M02_Item_Source.py @@ -49,7 +49,8 @@ def read_json(path: Path) -> Any: def row_of(document: Any, key: str) -> dict[str, Any] | None: rows = document.get("줄") if isinstance(document, dict) else None for row in rows or []: - if isinstance(row, dict) and row.get("키") == key: + # 옛 키로도 — 회사 · 개인 층 SS 키는 새 모양(SC · SP)으로 옮기며 `옛키` 에 남김(PLAN 26-4a) + if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])): return row return None diff --git a/M02_MasterTemplete/M02_MasterTemplete_Book.py b/M02_MasterTemplete/M02_MasterTemplete_Book.py index 11188397..7cab2074 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Book.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Book.py @@ -1,30 +1,72 @@ -"""M02 구조물집계표조합(SS 장부) — 구조물 열마다 줄 하나 · 경로 옮겨 받기(PLAN 26-2a). +"""M02 구조물집계표조합(SS 장부) — 구조물 열마다 줄 하나 · 경로 옮겨 받기(PLAN 26-2a · 26-4a). 제어용 장부 · 관리자는 집계표 화면(또는 나무)으로 고침 · 서버가 집계표 [저장] 때 맞춤 · -키 = SS + 6자리 · 한 번 준 키는 그 열에 남음 · 지운 열의 키는 다시 안 씀(`다음` 이 기억). +키(층마다 머리가 다름 · 폴더 · 종류 뜻 없는 일련번호 6자리) — + 마스터 `SS000001` · 회사 `SC{회사번호}-000001` · 개인 `SP{사용자번호}-000001` +한 번 준 키는 그 열에 남음 · 지운 열의 키는 다시 안 씀(`다음` 이 기억) · 층 사이 복사 · +개인에 저장 · 프로젝트에서 새 항목은 그 층의 새 키(본뜬 곳은 열 `본뜬`). 회사 · 개인 층에 남은 +옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에 +남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`). 줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 자재조합, -도번, 면적번, 재료번, 바인딩, 펼침}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — 저장소는 -`store.sync_combos` · `store.read_combos` 이름으로 이 파일을 부름. +도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 — +저장소는 `store.sync_combos` · `store.read_combos` · `store.find_combo` 이름으로 이 파일을 부름. """ from __future__ import annotations import json +import re +from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree +from M02_MasterTemplete import M02_Template_Layers as layers -HEAD = "SS" +HEAD = "SS" # 마스터(과 회사 · 개인 · 프로젝트 층 밖 모든 층) +_OLD = re.compile(r"^SS\d{6}$") NOTE = ( "구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · " - "키 SS 는 한 번 주면 안 바뀌고 지운 열의 키는 다시 안 씀 · 경로 = 폴더 이름 배열" + "키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 " + "지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열" ) -def _row(key: str, col: dict[str, Any], numbers: dict[str, str]) -> dict[str, Any]: +def key_head(root: Path) -> str: + """층 폴더 → 키 머리 — 회사 `SC{회사}-` · 개인 `SP{사용자}-` · + 그 밖(마스터 · 프로젝트 사본) `SS`.""" + try: + parts = Path(root).resolve().relative_to(layers.storage_root()).parts + except ValueError: + return HEAD + if len(parts) == 2 and parts[1] == layers.TEMPLATES_DIRNAME: + return f"SC{parts[0]}-" + if len(parts) == 3 and parts[2] == layers.TEMPLATES_DIRNAME: + return f"SP{parts[1]}-" + return HEAD + + +def _number(head: str, key: str) -> int | None: + """그 머리의 키면 여섯 자리 수 · 아니면 None.""" + found = re.fullmatch(re.escape(head) + r"(\d{6})", str(key)) + return int(found.group(1)) if found else None + + +def find(rows: list[dict[str, Any]], key: str) -> dict[str, Any] | None: + """키로 줄 — 지금 키 또는 옮기기 전 옛 키(프로젝트 복사본 · 구조물 기록이 들고 있는 것).""" + key = str(key) + for row in rows: + if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])): + return row + return None + + +def _row( + key: str, col: dict[str, Any], numbers: dict[str, str], olds: list[str] | None = None +) -> dict[str, Any]: path = tree.path_of(col) + extra = {"옛키": olds} if olds else {} return { "키": key, "열": col["id"], @@ -37,28 +79,40 @@ def _row(key: str, col: dict[str, Any], numbers: dict[str, str]) -> dict[str, An **{field: numbers.get(field, "") for _, field, _, _ in store.PARTS}, "바인딩": col.get("바인딩"), "펼침": col.get("펼침"), + **extra, } def sync() -> dict[str, Any]: - """장부를 집계표에 맞춤 — 새 열 = 새 SS 키 · 지운 열 = 줄 지움 · 고친 열 = 칸 갱신 · - 줄 차례 = 집계표 열 차례. 바뀐 것이 없으면 파일을 안 씀.""" + """장부를 집계표에 맞춤 — 새 열 = 그 층 새 키 · 지운 열 = 줄 지움 · 고친 열 = 칸 갱신 · + 옛 모양 키(회사 · 개인 층의 SS)는 새 모양으로(옛 키는 `옛키`) · 줄 차례 = 집계표 열 차례. + 바뀐 것이 없으면 파일을 안 씀.""" path = store.root() / store.COMBO_FILE + head = key_head(store.root()) with store._LOCK: old = json.loads(path.read_bytes()) if path.is_file() else {} - keys = {row["열"]: row["키"] for row in old.get("줄", [])} - taken = [int(k[len(HEAD) :]) for k in keys.values()] + before = {row["열"]: row for row in old.get("줄", []) if isinstance(row, dict)} + taken = {n for row in before.values() if (n := _number(head, row.get("키"))) is not None} after = max(old.get("다음", 1), max(taken, default=0) + 1) numbers = store.structure_numbers() rows = [] for col in store._structure_columns(): - if col["id"] not in keys: - keys[col["id"]] = f"{HEAD}{after:06d}" + was = before.get(col["id"]) or {} + key, olds = was.get("키"), list(was.get("옛키") or []) + if key is not None and _number(head, key) is None: # 옛 모양 — 숫자 여섯 그대로 옮김 + olds.append(key) + digits = int(key[2:]) if _OLD.match(str(key)) else None + key = f"{head}{digits:06d}" if digits and digits not in taken else None + if key: + taken.add(digits) + if key is None: + key = f"{head}{after:06d}" + taken.add(after) after += 1 - rows.append(_row(keys[col["id"]], col, numbers.get(col["id"], {}))) + rows.append(_row(key, col, numbers.get(col["id"], {}), olds)) doc = {"그룹": "구조물집계표조합", "판": 1, "비고": NOTE, "다음": after} if old.get("지운번호"): - doc["지운번호"] = old["지운번호"] # 지운 열의 도번 8자리 — 다시 안 줌 + doc["지운번호"] = old["지운번호"] # 지운 열의 번호 — 다시 안 줌 doc["줄"] = rows if doc != old: atomic_write_json(path, doc) @@ -66,10 +120,13 @@ def sync() -> dict[str, Any]: def read() -> dict[str, Any]: - """장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦 · `경로` 가 없던 옛 줄은 읽을 때 - 종류 · 분류1~3 에서 채움(빈 분류 건너뜀 · 파일은 다음 [저장] 때 새 모양).""" + """장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦 · 회사 · 개인 층의 옛 SS 키는 새 + 모양으로 옮김 · `경로` 가 없던 옛 줄은 읽을 때 종류 · 분류1~3 에서 채움(빈 분류 건너뜀).""" path = store.root() / store.COMBO_FILE doc = json.loads(path.read_bytes()) if path.is_file() else sync() + head = key_head(store.root()) + if head != HEAD and any(_number(head, r.get("키")) is None for r in doc.get("줄") or []): + doc = sync() # 회사 · 개인 층의 옛 SS 키 — 처음 읽을 때 한 번 새 모양으로(옛키 남김) for row in doc.get("줄") or []: if "경로" not in row: row["경로"] = tree.row_path(row) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py index 92692c6f..348278ef 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Inputs.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Inputs.py @@ -45,7 +45,7 @@ def pill_key(logic: str, name: str) -> str: def _item(key: str) -> dict[str, Any]: """지금 층(`store.at`)의 항목 줄 — 없으면 404.""" - row = next((r for r in store.read_combos().get("줄", []) if r.get("키") == key), None) + row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") return row diff --git a/M02_MasterTemplete/M02_MasterTemplete_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Items.py index e08613ff..5fbcc4b8 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Items.py @@ -112,7 +112,7 @@ def copy_item(source: Path, target: Path, key: str, origin: dict[str, Any]) -> d 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키). 돌려줌 `{key, 열, 이름}`.""" with store.at(source): - row = next((r for r in store.read_combos().get("줄", []) if r.get("키") == key), None) + row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") col_id = row["열"] diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 86bfd072..4399ecce 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -230,7 +230,10 @@ def _edit_tree(folder: Path, body: dict[str, Any]) -> dict[str, Any]: got = store.read("table", store.STRUCTURE_TABLE) if got["판"] != body.get("판"): raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": got["판"]}) - ids = {str(r.get("키")): str(r.get("열")) for r in store.read_combos().get("줄", [])} + ids = {} # 키 · 옛 키(회사 · 개인 층 SS) → 열 id + for r in store.read_combos().get("줄", []): + for key in [r.get("키"), *(r.get("옛키") or [])]: + ids[str(key)] = str(r.get("열")) try: doc, note = tree.apply(got["문서"], body, ids) except tree.TreeError as e: diff --git a/M02_MasterTemplete/M02_MasterTemplete_Store.py b/M02_MasterTemplete/M02_MasterTemplete_Store.py index 8f1e4a0b..f1bd4860 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Store.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Store.py @@ -47,8 +47,12 @@ def at(folder: str | Path) -> Iterator[Path]: # 구조물집계표 구조물 열(꼴=수) 마다 세 파일(파일 이름 = 열 id) — # 구조물 도면 `structure/` (도각 없는 전용 화면) · 면적산출 `area/` · 재료산출 `material/` # (둘 다 스프레드시트 통합문서) -# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 8자리 — 한 열의 세 번호는 같은 8자리 · 한 번 준 번호는 -# 파일에 남아 안 바뀜(PLAN 13장) · 새 항목은 자리 뜻 없는 일련번호(PLAN 26-1 — 있던 번호는 그대로) +# 번호 = 도번 SD · 면적번 SA · 재료번 SM + 뒷자리 — 한 열의 세 번호는 같은 뒷자리 · 한 번 준 번호는 +# 파일에 남아 안 바뀜(PLAN 13장). 뒷자리 두 모양: +# 옛 8자리 `SD01020300` = [종류][분류1][분류2][분류3] 두 자리씩(그대로 유효 · 더는 새로 안 줌) +# 새 일련 `SD-000001` = 붙임표 + 6자리 · 자리 뜻 없음(PLAN 26-1 · 26-4a) — 옛 8자리 · +# 12장 옛 6자리(`SD010101`)와 글자 모양이 달라 겹치지 않음 · 층마다 따로 셈 +# (파일은 열 id 로 가름 · 번호는 보이는 이름) # 항목 경로(폴더 이름 배열) = 열 `머리` 의 빈 칸 건너뛴 이름(`M02_MasterTemplete_Tree`) STRUCTURE_TABLE = "구조물집계표" # 프로그램이 이름으로 부르는 양식 — 지우면 그 기능이 죽음(B07 이름은 `B07_DesignDetail_Engine_*`) @@ -66,7 +70,7 @@ PARTS = ( ) _KEY = {kind: key for kind, key, _, _ in PARTS} SHEETS = ("area", "material") # 통합문서 종류 — [저장] 때 서버가 계산값을 새로 적음 -_CODE = re.compile(r"^S[DAM](\d{8})$") +_CODE = re.compile(r"^S[DAM](\d{8}|-\d{6})$") _OLD_CODE = re.compile(r"^S[DCA](\d{6})$") # PLAN 12장 6자리 — 끝에 00 을 붙여 읽음 _BAD_NAME = re.compile(r'[\\/:*?"<>|\x00-\x1f]|\.\.') # 도각 없는 빈 CAD 문서의 기본 도면층 — 없으면 B07 CAD 가 못 엶("Cannot read properties @@ -203,7 +207,8 @@ def _docs(kind: str) -> list[tuple[str, Any]]: def _code(doc: Any, key: str) -> str | None: - """문서의 번호 8자리 — 12장 6자리는 끝에 00 · 옛 `구-01` · `표-01` · 없음은 None.""" + """문서 번호의 뒷자리 — 옛 8자리 · 새 `-` + 6자리 · 12장 6자리는 끝에 00 · + 옛 `구-01` · `표-01` · 없음은 None.""" text = str(doc.get(key) or "") if isinstance(doc, dict) else "" if found := _CODE.match(text): return found.group(1) @@ -212,7 +217,7 @@ def _code(doc: Any, key: str) -> str | None: def _codes() -> dict[str, str]: - """`{열 id: 8자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번.""" + """`{열 id: 뒷자리}` — 구조물 도면 도번 먼저 · 없으면 면적번 · 재료번.""" codes: dict[str, str] = {} for kind, key, _, _ in reversed(PARTS): codes.update({name: c for name, doc in _docs(kind) if (c := _code(doc, key))}) @@ -248,20 +253,24 @@ def _column_ids() -> list[str]: return [col["id"] for col in _structure_columns()] +_SERIAL = re.compile(r"^-(\d{6})$") + + def _serial(taken: dict[str, str], retired: list[str] | None = None) -> str: - """새 항목 번호 — 자리 뜻 없는 일련번호(있던 번호 · 지운 번호 가운데 가장 큰 수 + 1 · 8자리).""" - codes = [int(c) for c in [*taken.values(), *(retired or [])] if str(c).isdigit()] - return f"{max(codes, default=0) + 1:08d}" + """새 항목 번호 뒷자리 — 붙임표 + 일련 6자리(새 모양 번호 · 지운 번호 가운데 가장 큰 수 + 1) · + 옛 8자리는 셈에 안 넣음(모양이 달라 겹칠 일 없음).""" + found = (_SERIAL.match(str(c)) for c in [*taken.values(), *(retired or [])]) + return f"-{max((int(m.group(1)) for m in found if m), default=0) + 1:06d}" def _new_code(column: str) -> str: - """이 열이 쓸 8자리 — 짝 파일에 이미 있으면 그것 · 없으면 새로(잠금 안에서 부름).""" + """이 열이 쓸 번호 뒷자리 — 짝 파일에 이미 있으면 그것 · 없으면 새로(잠금 안에서 부름).""" taken = _codes() return taken[column] if column in taken else _serial(taken, _retired()) def _retired() -> list[str]: - """지운 열의 8자리 — 조합 장부 `지운번호`(한 번 준 번호는 다시 안 줌).""" + """지운 열의 번호 뒷자리 — 조합 장부 `지운번호`(한 번 준 번호는 다시 안 줌).""" path = root() / COMBO_FILE return json.loads(path.read_bytes()).get("지운번호", []) if path.is_file() else [] @@ -289,7 +298,7 @@ def _blank(kind: str, column: str) -> dict[str, Any]: def _ensure(kind: str, column: str, source: str | None = None) -> bool: - """그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 8자리)와 같이 만듦 · 있으면 그대로. + """그 열의 파일이 없으면 빈 문서를 번호(세 파일이 같은 뒷자리)와 같이 만듦 · 있으면 그대로. `source` 열 파일이 있으면 빈 문서 대신 그 내용(알약 `연결` 포함 · 계산값은 서버가 다시 풂).""" path = _path(kind, column) if path.is_file(): @@ -329,8 +338,8 @@ def _take_origins(doc: Any) -> dict[str, str]: def create_structure(column: str) -> dict[str, Any]: - """구조물 열 하나의 구조물 도면 새로 — 도번 SD + 8자리 · 이미 있으면 409. - 면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 8자리).""" + """구조물 열 하나의 구조물 도면 새로 — 도번 SD + 새 일련(`-` + 6자리) · 이미 있으면 409. + 면적산출 · 재료산출 파일이 없으면 빈 통합문서도 같이 만듦(같은 뒷자리).""" if column not in _column_ids(): raise StoreError(404, f"{STRUCTURE_TABLE}에 없는 구조물 열 「{column}」") if not _ensure("structure", column): @@ -471,6 +480,13 @@ def sync_combos() -> dict[str, Any]: return book.sync() +def find_combo(key: str) -> dict[str, Any] | None: + """키(또는 옮기기 전 옛 키)로 장부 줄 — 없으면 None(`Book.find`).""" + from M02_MasterTemplete import M02_MasterTemplete_Book as book + + return book.find(book.read().get("줄", []), key) + + def read_combos() -> dict[str, Any]: """조합 장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦(`Book.read`).""" from M02_MasterTemplete import M02_MasterTemplete_Book as book diff --git a/M02_MasterTemplete/M02_MasterTemplete_Tree.py b/M02_MasterTemplete/M02_MasterTemplete_Tree.py index b70e3e55..4bbdd933 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Tree.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Tree.py @@ -142,7 +142,7 @@ def _path(value: Any, what: str = "경로") -> list[str]: def _find(doc: dict[str, Any], key: str | None, ids: dict[str, str]) -> dict[str, Any]: - """SS 키(또는 열 id)로 항목 열 — 없으면 404.""" + """항목 키 · 옛 키(또는 열 id)로 항목 열 — 없으면 404.""" cid = ids.get(str(key), str(key)) if key else None col = next((c for c in _items(doc) if c["id"] == cid), None) if col is None: diff --git a/common_util/common_util_summary_item.py b/common_util/common_util_summary_item.py index 08f58c67..dee3682e 100644 --- a/common_util/common_util_summary_item.py +++ b/common_util/common_util_summary_item.py @@ -3,6 +3,10 @@ summary_item = {layer: "system"|"company"|"personal", owner: null|"{회사}"|"{회사}/{사용자}", key: "SS000004", name: "…"} +항목 키(PLAN 26-4a) — 마스터 `SS000004` · 회사 `SC{회사}-000001` · 개인 `SP{사용자}-000001` +(머리의 번호 = owner 의 회사 · 사용자). 옛 `SS` 키는 어느 층이든 그대로 받음 — 회사 · 개인 층이 새 +모양으로 옮기기 전에 적힌 기록(층 장부의 `옛키` 로 찾음). + 구조물 기록(`structures.json` `StructureInstance`) · 관 지점 기록(`pipe_points.json` 점) 두 곳이 같은 칸을 씀 — 검사는 여기 한 벌. 없으면 null(옛 파일 호환). """ @@ -13,7 +17,11 @@ import re from typing import Any SUMMARY_LAYERS = ("system", "company", "personal") -_KEY = re.compile(r"^SS\d{6}$") +_KEY = re.compile(r"^SS\d{6}$") # 마스터 · 옛 키 +_LAYER_KEY = { + "company": re.compile(r"^SC(\d+)-\d{6}$"), + "personal": re.compile(r"^SP(\d+)-\d{6}$"), +} _OWNER = { "system": None, "company": re.compile(r"^\d+$"), @@ -43,18 +51,32 @@ def parse_summary_item(value: Any) -> dict[str, Any] | None: shape = "{회사}" if layer == "company" else "{회사}/{사용자}" raise ValueError(f"{layer} 층 집계 항목의 owner 는 「{shape}」 숫자 모양입니다.") key = value.get("key") - if not isinstance(key, str) or not _KEY.match(key): - raise ValueError("집계 항목 key 는 SS 와 숫자 6자리입니다(예: SS000004).") + if not isinstance(key, str) or not _key_ok(layer, owner, key): + raise ValueError( + "집계 항목 key 는 마스터 SS000004 · 회사 SC{회사}-000001 · " + "개인 SP{사용자}-000001 모양입니다(옛 SS 키는 어느 층이든 받음)." + ) name = value.get("name") if not isinstance(name, str) or not name.strip() or len(name) > _NAME_MAX: raise ValueError(f"집계 항목 name 은 1~{_NAME_MAX}자 글입니다.") return {"layer": layer, "owner": owner, "key": key, "name": name} +def _key_ok(layer: str, owner: Any, key: str) -> bool: + """옛 · 마스터 SS 키는 어느 층이든 · SC · SP 는 그 층에서 머리 번호가 owner 와 같을 때만.""" + if _KEY.match(key): + return True + rule = _LAYER_KEY.get(layer) + found = rule.match(key) if rule else None + if not found or not isinstance(owner, str): + return False + return found.group(1) == (owner if layer == "company" else owner.split("/")[-1]) + + def snapshot_folder_name(item: dict[str, Any]) -> str: """`{layer}_{owner 슬러그}_{key}` — owner 없음 = `none` · `/` = `-`. - 예: `system_none_SS000004` · `company_3_SS000001` · `personal_3-12_SS000002`. + 예: `system_none_SS000004` · `company_3_SC3-000001` · `personal_3-12_SP12-000002`. """ owner = item.get("owner") slug = str(owner).replace("/", "-") if owner else "none" diff --git a/resources/tester/spreadsheet/test_spreadsheet_store.py b/resources/tester/spreadsheet/test_spreadsheet_store.py index 5be901bb..edac0a18 100644 --- a/resources/tester/spreadsheet/test_spreadsheet_store.py +++ b/resources/tester/spreadsheet/test_spreadsheet_store.py @@ -68,13 +68,13 @@ def test_새_구조물_도면은_면적_재료_진짜_빈_시트_같은_8자리( made1 = client.post("/api/m02/structures/rv").json()["문서"] made2 = client.post("/api/m02/structures/pv_b").json()["문서"] # 새 항목 번호 = 자리 뜻 없는 일련번호(PLAN 26-1) - assert made1["도번"] == "SD00000001" and made2["도번"] == "SD00000002" + assert made1["도번"] == "SD-000001" and made2["도번"] == "SD-000002" for kind, key, head in (("area", "면적번", "SA"), ("material", "재료번", "SM")): doc = client.get(f"/api/m02/templates/{kind}/pv_b").json()["문서"] - assert doc[key] == f"{head}00000002" + assert doc[key] == f"{head}-000002" assert doc["시트"][0]["칸"] == {} and doc["서식"] == [{}] # 머리 줄 없음 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["pv_b"] == {"도번": "SD00000002", "면적번": "SA00000002", "재료번": "SM00000002"} + assert numbers["pv_b"] == {"도번": "SD-000002", "면적번": "SA-000002", "재료번": "SM-000002"} @pytest.mark.parametrize( @@ -90,7 +90,7 @@ def test_다른_열_내용을_복사해_가져옴_연결도_같이(client, kind, assert r.status_code == 200 ms = client.get(f"/api/m02/templates/{kind}/ms").json()["문서"] assert (ms["시트"][0]["칸"], ms["연결"]) == ({"A1": {"식": "1+1"}}, LINKS) - assert (ms[key], ms["열"]) == (f"{head}00000010", "ms") # 번호 · 열은 받는 쪽 그대로 + assert (ms[key], ms["열"]) == (f"{head}-000010", "ms") # 번호 · 열은 받는 쪽 그대로 assert ms["계산값"] == SERVER # 서버가 다시 풂 assert client.post(f"/api/m02/{kind}/ms/copy", params={"from": "없는열"}).status_code == 404 diff --git a/resources/tester/test_m02_item_keys.py b/resources/tester/test_m02_item_keys.py new file mode 100644 index 00000000..f6c6a82e --- /dev/null +++ b/resources/tester/test_m02_item_keys.py @@ -0,0 +1,213 @@ +"""M02 항목 키 체계 · 품번 겹침 점검(PLAN 26-4a). + +키 — 마스터 `SS` + 6자리 · 회사 `SC{회사}-` + 6자리 · 개인 `SP{사용자}-` + 6자리 · +한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사는 새 키 + 본뜬 곳 · +회사 · 개인 층의 옛 SS 키는 새 모양으로 옮기고 옛 키로도 찾음. +번호 — 새 일련 `SD-000001`(붙임표 + 6자리)이 옛 8자리(`SD01020300`) · 12장 6자리(`SD010101`)와 +모양이 달라 겹치지 않음 · 99 넘는 형제 · 깊이 6 에서도 넘침 없음 · 층마다 따로 셈. +""" + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any + +import pytest + +from common_util.common_util_summary_item import parse_summary_item +from config import config_system +from M02_MasterTemplete import M02_Item_Source as source +from M02_MasterTemplete import M02_MasterTemplete_Book as book +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers +from resources.tester.test_m02_structure import write_sample_table +from resources.tester.test_m02_tree import Tree, _app + +ROOT = Path(__file__).resolve().parents[2] +NEW = re.compile(r"^S[DAM]-\d{6}$") + + +@pytest.fixture() +def world(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]: + system = tmp_path / "system" + system.mkdir() + write_sample_table(system) + monkeypatch.setattr(store, "FOLDER", system) + monkeypatch.setattr(layers, "SYSTEM_ROOT", system) + store.renumber() + storage = tmp_path / "storage" + storage.mkdir() + monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage)) + session: dict[str, Any] = {"user_id": 42, "company_id": 7, "role": "ADMIN", "is_master": False} + return {"client": _app(session), "session": session, "storage": storage, "system": system} + + +def _numbers(root: Path, cid: str) -> set[str]: + return { + json.loads((root / kind / f"{cid}.json").read_bytes())[key] + for kind, key, _, _ in store.PARTS + } + + +# ── 키 ───────────────────────────────────────────── + + +def test_층마다_키_모양_안_바뀜_다시_안_씀(world): + client = world["client"] + company = Tree(client, "company") + company.do(op="add-item", 경로=["돌쌓기"], 이름="찰쌓기") + company.do(op="add-item", 경로=["돌쌓기"], 이름="메쌓기") + keys = [r["key"] for r in company.now["items"]] + assert keys == ["SC7-000001", "SC7-000002"] + # 이름 바꾸기 · 옮기기 · 표 저장에도 키 그대로(폴더 뜻 없음) + company.do(op="rename", 키="SC7-000001", 이름="찰쌓기2") + company.do(op="move", 키="SC7-000002", 새경로=["다른폴더"]) + assert {r["key"]: r["경로"] for r in company.now["items"]} == { + "SC7-000001": ["돌쌓기", "찰쌓기2"], + "SC7-000002": ["다른폴더", "메쌓기"], + } + # 지운 키는 다시 안 씀 + company.do(op="delete", 키="SC7-000002") + company.do(op="add-item", 경로=[], 이름="새것") + assert [r["key"] for r in company.now["items"]] == ["SC7-000001", "SC7-000003"] + # 개인 층 = SP{사용자} · 층 사이 복사 = 새 키 + 본뜬 곳 + personal = Tree(client, "personal") + personal.do(op="add-item", 경로=[], 이름="내것") + assert [r["key"] for r in personal.now["items"]] == ["SP42-000001"] + body = {"from_layer": "company", "from_owner": "7", "key": "SC7-000001"} + made = client.post("/api/m02/layers/personal/items/copy", json=body).json() + assert made["key"] == "SP42-000002" + table = client.get("/api/m02/layers/personal/templates/table/구조물집계표").json()["문서"] + col = next(c for c in table["열"] if c["id"] == made["열"]) + assert col["본뜬"] == {"layer": "company", "owner": "7", "key": "SC7-000001"} + # 마스터는 SS 그대로 + assert Tree(client, "system").now["items"][0]["key"].startswith("SS") + + +def _legacy_personal(storage: Path, rows: list[tuple[str, str]], after: int) -> Path: + """옛 개인 층 — 집계표 열 + SS 키 장부(26-4a 앞 모양).""" + folder = storage / "7" / "42" / "templates" + for kind in store.KINDS: + (folder / kind).mkdir(parents=True, exist_ok=True) + cols = [{"id": cid, "머리": [cid, None, None, None], "꼴": "수"} for _, cid in rows] + table = {"층": ["종류", "분류1", "분류2", "분류3"], "열": cols, "줄": [], "합계줄": []} + (folder / "table" / "구조물집계표.json").write_text(json.dumps(table), encoding="utf-8") + book_rows = [{"키": key, "열": cid, "종류": cid} for key, cid in rows] + doc = {"그룹": "구조물집계표조합", "다음": after, "줄": book_rows} + (folder / store.COMBO_FILE).write_text(json.dumps(doc), encoding="utf-8") + return folder + + +def test_옛_SS_키는_새_모양으로_옮기고_옛_키로도_찾음(world): + folder = _legacy_personal(world["storage"], [("SS000001", "a"), ("SS000003", "b")], 5) + raw = json.loads((folder / store.COMBO_FILE).read_bytes()) # 복사본 manifest 가 든 옛 줄 + client = world["client"] + rows = client.get("/api/m02/items?layer=personal").json() + assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000003"] # 숫자 여섯 그대로 + migrated = json.loads((folder / store.COMBO_FILE).read_bytes()) + assert [r["옛키"] for r in migrated["줄"]] == [["SS000001"], ["SS000003"]] + assert migrated["다음"] == 5 + # 옛 키로도 찾음 — 입력 목록 · 나무 고침 · 복사본 원본 찾기(M02_Item_Source) + assert client.get("/api/m02/layers/personal/items/SS000003/inputs").status_code == 200 + tree_ = Tree(client, "personal") + tree_.do(op="rename", 키="SS000001", 이름="a2") + assert tree_.item(["a2"])["key"] == "SP42-000001" + assert source.row_of(migrated, "SS000003")["키"] == "SP42-000003" + assert source.row_of(raw, "SS000003")["키"] == "SS000003" # 옮기기 전 파일도 그대로 읽힘 + # 새 항목 = 다음 번호(옛 번호 · 지운 번호 안 씀) + tree_.do(op="add-item", 경로=[], 이름="c") + assert tree_.item(["c"])["key"] == "SP42-000005" + + +def test_옛_숫자가_이미_있으면_다음_번호로(world): + folder = _legacy_personal(world["storage"], [("SP42-000001", "a"), ("SS000001", "b")], 2) + rows = world["client"].get("/api/m02/items?layer=personal").json() + assert [r["key"] for r in rows] == ["SP42-000001", "SP42-000002"] + assert json.loads((folder / store.COMBO_FILE).read_bytes())["줄"][1]["옛키"] == ["SS000001"] + + +def test_집계_항목_참조_키_모양(): + ok = [ + {"layer": "system", "owner": None, "key": "SS000004"}, + {"layer": "company", "owner": "7", "key": "SC7-000001"}, + {"layer": "personal", "owner": "7/42", "key": "SP42-000001"}, + {"layer": "company", "owner": "7", "key": "SS000001"}, # 옛 키 + {"layer": "personal", "owner": "7/42", "key": "SS000002"}, # 옛 키 + ] + for item in ok: + assert parse_summary_item({**item, "name": "x"})["key"] == item["key"] + bad = [ + {"layer": "company", "owner": "8", "key": "SC7-000001"}, # 다른 회사 + {"layer": "personal", "owner": "7/43", "key": "SP42-000001"}, # 다른 사람 + {"layer": "personal", "owner": "7/42", "key": "SC7-000001"}, # 층 머리 틀림 + {"layer": "system", "owner": None, "key": "SC7-000001"}, + {"layer": "company", "owner": "7", "key": "SC7-00001"}, # 자리수 + {"layer": "company", "owner": "7", "key": "SD-000001"}, # 번호는 키가 아님 + ] + for item in bad: + with pytest.raises(ValueError): + parse_summary_item({**item, "name": "x"}) + + +# ── 번호 ──────────────────────────────────────────── + + +def test_새_번호는_옛_번호와_모양이_달라_안_겹침(world): + """마스터 옛 8자리는 그대로 · 새 열은 `-` + 6자리 · 99 넘는 형제 · 깊이 6 에서도 한 모양.""" + system = world["system"] + # 견본 열 둘을 옛 8자리(제목행 자리)로 — 마스터 51 항목이 지금 이 모양 + for cid, code in (("rv", "01010100"), ("c1", "01010200")): + for kind, key, head, _ in store.PARTS: + path = system / kind / f"{cid}.json" + doc = json.loads(path.read_bytes()) + path.write_text(json.dumps({**doc, key: f"{head}{code}"}), encoding="utf-8") + old = {cid: _numbers(system, cid) for cid in store._column_ids()} + assert old["rv"] == {"SD01010100", "SA01010100", "SM01010100"} + first = max(int(n[3:]) for nums in old.values() for n in nums if NEW.match(n)) + 1 + with store.at(system): + got = store.read("table", store.STRUCTURE_TABLE) + doc = got["문서"] + deep = ["a", "b", "c", "d", "e", "f"] + doc["열"] += [ + {"id": f"x{i}", "머리": [*deep[:5], f"항목{i}"], "꼴": "수"} for i in range(120) + ] + store.write("table", store.STRUCTURE_TABLE, got["판"], doc) + new = [_numbers(system, f"x{i}") for i in range(120)] + tails = [{n[2:] for n in nums} for nums in new] + assert all(len(t) == 1 for t in tails) # 세 파일 같은 뒷자리 + assert [t.pop() for t in tails] == [f"-{i:06d}" for i in range(first, first + 120)] + assert all(NEW.match(n) for nums in new for n in nums) + assert not any(store._OLD_CODE.match(n) for nums in new for n in nums) # 12장 6자리로 안 읽힘 + assert {cid: _numbers(system, cid) for cid in old} == old # 옛 번호 그대로 + # 지운 번호 다시 안 줌 · 셈은 새 모양끼리 + with store.at(system): + got = store.read("table", store.STRUCTURE_TABLE) + doc = got["문서"] + doc["열"] = [c for c in doc["열"] if c["id"] != "x119"] + store.write("table", store.STRUCTURE_TABLE, got["판"], doc) + got = store.read("table", store.STRUCTURE_TABLE) + got["문서"]["열"].append({"id": "y", "머리": ["y"], "꼴": "수"}) + store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"]) + last = f"-{first + 120:06d}" # 지운 x119 번호(first + 119) 다음 + assert _numbers(system, "y") == {f"SD{last}", f"SA{last}", f"SM{last}"} + + +def test_층마다_번호는_따로_파일은_열_id_로(world): + client = world["client"] + Tree(client, "company").do(op="add-item", 경로=[], 이름="회사것") + Tree(client, "personal").do(op="add-item", 경로=[], 이름="내것") + storage = world["storage"] + company = next((storage / "7" / "templates" / "structure").glob("*.json")) + mine = next((storage / "7" / "42" / "templates" / "structure").glob("*.json")) + assert json.loads(company.read_bytes())["도번"] == json.loads(mine.read_bytes())["도번"] + assert company.parent != mine.parent # 같은 번호여도 층 폴더가 달라 안 겹침 + + +def test_키_머리는_M01_키대장과_안_겹침(): + ledger = json.loads((ROOT / "resources/master_data/_키대장.json").read_text("utf-8"))["다음"] + for head in ("SS", "SC", "SP", "SD", "SA", "SM", "UM"): + assert head not in ledger, head + assert "UA" in ledger # 일위대가 조합은 M01 키대장 몫(M02 는 읽기만) + assert book.HEAD == "SS" diff --git a/resources/tester/test_m02_layers_store.py b/resources/tester/test_m02_layers_store.py index 2afbc3c2..eebcdaa1 100644 --- a/resources/tester/test_m02_layers_store.py +++ b/resources/tester/test_m02_layers_store.py @@ -108,7 +108,7 @@ def test_회사_층_첫_진입_뼈대와_쓰기_권한(world): assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 403 _add_column(_as(world, "ADMIN"), "company", COL) items = _as(world).get("/api/m02/items?layer=company").json() # 같은 회사 일반 사용자 - assert [(r["key"], r["열"]) for r in items] == [("SS000001", "cw")] + assert [(r["key"], r["열"]) for r in items] == [("SC7-000001", "cw")] # 회사 키(26-4a) assert items[0]["도번"].startswith("SD") and (root / "structure/cw.json").is_file() assert client.get("/api/m02/items?layer=company&owner=9").status_code == 403 # 남의 회사 # 일위대가(UA) 갈라 내기는 M01 시스템 마스터에 쓰므로 회사 관리자도 못 함 @@ -163,7 +163,7 @@ def test_회사_항목을_개인으로_복사(world): got = user.post("/api/m02/layers/personal/items/copy", json=body) assert got.status_code == 200, got.text made = got.json() - assert made["열"] == "cw" and made["key"] == "SS000001" # 응답 = 새 SS 키(바로 고름) + assert made["열"] == "cw" and made["key"] == "SP42-000001" # 응답 = 개인 층 새 키(바로 고름) mine_rows = user.get("/api/m02/items?layer=personal").json() assert mine_rows[0]["key"] == made["key"] and mine_rows[0]["names"]["일위대가"] == ua_name mine = "/api/m02/layers/personal" @@ -175,7 +175,7 @@ def test_회사_항목을_개인으로_복사(world): assert mat["이름"] == "회사 자재" and mat["담은자재"][0]["코드"] == _material()["코드"] # 또 복사하면 새 열 id · 새 SS 키 · 자재 조합 이름 (2) again = user.post("/api/m02/layers/personal/items/copy", json=body).json() - assert (again["열"], again["key"]) == ("cw_2", "SS000002") + assert (again["열"], again["key"]) == ("cw_2", "SP42-000002") names = [r["이름"] for r in user.get(f"{mine}/material-combos").json()["줄"]] assert names == ["회사 자재", "회사 자재 (2)"] # 마스터 항목도 개인으로 · 없는 키 404 diff --git a/resources/tester/test_m02_structure.py b/resources/tester/test_m02_structure.py index 6b28e248..c532c67c 100644 --- a/resources/tester/test_m02_structure.py +++ b/resources/tester/test_m02_structure.py @@ -76,7 +76,7 @@ def test_새로_도번_자동_409_없는_열(client): "구조물도면", "structure", "pp_len", - "SD00000001", + "SD-000001", ) # 도각 없는 전용 화면 — 그린 만큼이 설계 영역 · 기본 도면층 하나(없으면 CAD 가 못 엶) assert doc["도면"]["format"] == 6 @@ -87,13 +87,13 @@ def test_새로_도번_자동_409_없는_열(client): for kind in ("area", "material"): assert client.get(f"/api/m02/templates/{kind}/pp_len").json()["문서"]["종류"] == "통합문서" - assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD00000002" + assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002" assert client.post("/api/m02/structures/pp_len").status_code == 409 assert client.post("/api/m02/structures/없는열").status_code == 404 assert client.post("/api/m02/structures/no").status_code == 404 # 글 열은 구조물 아님 assert client.get("/api/m02/structures/numbers").json() == { - "pp_len": _num("00000001"), - "rv": _num("00000002"), + "pp_len": _num("-000001"), + "rv": _num("-000002"), } rows = client.get("/api/m02/templates").json() assert {(r["종류"], r["이름"]) for r in rows if r["종류"] == "structure"} == { @@ -119,7 +119,7 @@ def test_저장_뼈대_거름(client): "/api/m02/templates/structure/pp_len", json={"판": now, "문서": {**doc, "도번": "SD9"}} ) got = client.get("/api/m02/templates/structure/pp_len").json()["문서"] - assert got["도번"] == "SD00000001" # 번호는 서버 것 — 화면 값을 안 받음 + assert got["도번"] == "SD-000001" # 번호는 서버 것 — 화면 값을 안 받음 def test_지워도_번호를_당기지_않음_짝은_같은_번호(client): @@ -127,11 +127,11 @@ def test_지워도_번호를_당기지_않음_짝은_같은_번호(client): client.post(f"/api/m02/structures/{column}") assert client.delete("/api/m02/templates/structure/rv").status_code == 200 numbers = client.get("/api/m02/structures/numbers").json() - assert numbers["rv"] == {**_num("00000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠 - assert numbers["ms"] == _num("00000003") # 다른 열 번호는 그대로 + assert numbers["rv"] == {**_num("-000002"), "도번": ""} # 면적 · 재료는 남아 번호를 쥠 + assert numbers["ms"] == _num("-000003") # 다른 열 번호는 그대로 # 새 열 = 다음 일련번호(자리 뜻 없음) · 다시 만든 도면은 짝(면적산출) 번호를 이어 받음 - assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD00000004" - assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD00000002" + assert client.post("/api/m02/structures/pp_cp").json()["문서"]["도번"] == "SD-000004" + assert client.post("/api/m02/structures/rv").json()["문서"]["도번"] == "SD-000002" def test_번호_없는_열은_열_차례로_일련번호_한_번_옮김(client, tmp_path): @@ -148,15 +148,15 @@ def test_번호_없는_열은_열_차례로_일련번호_한_번_옮김(client, numbers = client.get("/api/m02/structures/numbers").json() assert set(numbers) == set(store._column_ids()) and "sta" not in numbers expect = { # 구조물 열 차례 그대로 1 부터(제목행 자리 뜻 없음) - "rv": "00000001", - "c1": "00000002", - "pv_b": "00000003", - "pv_l": "00000004", - "pp_len": "00000007", - "fb": "00000009", - "be": "00000011", - "h_rip_c": "00000012", - "h_rip_m": "00000013", + "rv": "-000001", + "c1": "-000002", + "pv_b": "-000003", + "pv_l": "-000004", + "pp_len": "-000007", + "fb": "-000009", + "be": "-000011", + "h_rip_c": "-000012", + "h_rip_m": "-000013", } for column, code in expect.items(): assert numbers[column] == _num(code), column @@ -184,10 +184,10 @@ def test_집계표_저장_때_새_열은_만들고_지운_열은_같이_지움(c for kind_ in ("structure", "area", "material"): assert client.get(f"/api/m02/templates/{kind_}/pv_a").status_code == 404 # 새 열 = 열 차례로 다음 일련번호 · 지운 pv_a(5) 는 비워 둠(다른 번호를 안 당김) - assert numbers["pv_x"] == _num("00000014") - assert numbers["zz"] == _num("00000015") - assert numbers["pv_y"] == _num("00000016") - assert numbers["pv_b"] == _num("00000003") # 있던 번호는 그대로 + assert numbers["pv_x"] == _num("-000014") + assert numbers["zz"] == _num("-000015") + assert numbers["pv_y"] == _num("-000016") + assert numbers["pv_b"] == _num("-000003") # 있던 번호는 그대로 def test_지운_열_번호는_다음_저장에도_다시_안_줌(client): @@ -197,12 +197,12 @@ def test_지운_열_번호는_다음_저장에도_다시_안_줌(client): got = client.get(url).json() got["문서"]["열"] = [c for c in got["문서"]["열"] if c["id"] != "h_rip_m"] assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200 - assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["00000013"] + assert client.get("/api/m02/structure-combos").json()["지운번호"] == ["-000013"] got = client.get(url).json() new = {"id": "pv_z", "머리": ["콘크리트포장", "T=(두께마다)", "Z", None], "꼴": "수"} got["문서"]["열"].append(new) assert client.put(url, json={"판": got["판"], "문서": got["문서"]}).status_code == 200 - assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("00000014") + assert client.get("/api/m02/structures/numbers").json()["pv_z"] == _num("-000014") def test_다른_열_도면을_복사해_가져옴(client): @@ -214,8 +214,8 @@ def test_다른_열_도면을_복사해_가져옴(client): assert r.status_code == 200 ms = client.get("/api/m02/templates/structure/ms").json()["문서"] assert ms["도면"]["entities"] == [{"type": "circle"}] - assert (ms["도번"], ms["열"]) == ("SD00000010", "ms") # 번호 · 열은 받는 쪽 그대로 - assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD00000001" + assert (ms["도번"], ms["열"]) == ("SD-000010", "ms") # 번호 · 열은 받는 쪽 그대로 + assert client.get("/api/m02/templates/structure/rv").json()["문서"]["도번"] == "SD-000001" bad = client.post("/api/m02/structures/ms/copy", params={"from": "없는열"}) assert bad.status_code == 404 diff --git a/resources/tester/test_m02_tree.py b/resources/tester/test_m02_tree.py index 02ed8bb9..de7289ce 100644 --- a/resources/tester/test_m02_tree.py +++ b/resources/tester/test_m02_tree.py @@ -154,7 +154,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor assert all(len(c["머리"]) == 5 for c in table["열"]) # 번호 = 자리 뜻 없는 일련번호 · 세 파일 같은 번호 codes = [_numbers(root, t.item(p)["열"]) for p in t.paths()] - assert codes == [{"00000001"}, {"00000002"}, {"00000003"}] + assert codes == [{"-000001"}, {"-000002"}, {"-000003"}] assert ["a", "b", "c", "d"] in t.now["폴더"] and ["돌쌓기"] in t.now["폴더"] # 겹침 409 · 모양 422 · 없는 것 404 t.do(409, op="add-item", 경로=["돌쌓기"], 이름="찰쌓기") @@ -171,7 +171,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor key = t.item(["돌쌓기", "찰쌓기"])["key"] t.do(op="rename", 키=key, 이름="메쌓기") moved = t.item(["돌쌓기", "메쌓기"]) - assert moved["key"] == key and _numbers(root, moved["열"]) == {"00000002"} + assert moved["key"] == key and _numbers(root, moved["열"]) == {"-000002"} t.do(op="rename", 경로=["a"], 이름="A") assert ["A", "b", "c", "d", "e"] in t.paths() and ["a"] not in t.now["폴더"] t.do(409, op="rename", 경로=["A"], 이름="돌쌓기") # 있는 폴더로 합치기 막음 @@ -189,7 +189,7 @@ def test_빈_층에서_폴더_항목_깊이_1_2_5_이름_옮기기_지우기(wor for part in ("structure", "area", "material", "inputs"): assert not (root / part / f"{gone['열']}.json").is_file() t.do(op="add-item", 경로=["돌쌓기"], 이름="새것") # 지운 번호는 다시 안 줌 - assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"00000004"} + assert _numbers(root, t.item(["돌쌓기", "새것"])["열"]) == {"-000004"} t.do(op="delete", 경로=["돌쌓기", "b"]) # 깊은 항목이 빠지면 머리 줄 넷으로 table = json.loads((root / "table/구조물집계표.json").read_bytes()) assert table["층"] == ["종류", "분류1", "분류2", "분류3"]