diff --git a/M02_MasterTemplete/M02_MasterTemplete_Branch.py b/M02_MasterTemplete/M02_MasterTemplete_Branch.py deleted file mode 100644 index a483f3163..000000000 --- a/M02_MasterTemplete/M02_MasterTemplete_Branch.py +++ /dev/null @@ -1,130 +0,0 @@ -"""조합 갈라 내기 — 구조물 열 하나가 가리키는 조합을 고친 내용으로 새로 만들어 그 열만 새 키를 -가리키게 함(PLAN 15장 · 검토 `14_조합편집검토.md` 1장). - · 일위대가 조합(UA · UC · UP) — 새 줄은 지금 층 조합 저장소가 만듦(`Combo_Store` · PLAN 27-2c: - 마스터 층 = UA · 회사 층 = UC · 개인 층 = UP · 고르기는 세 층 중 이 층이 쓸 수 있는 것) - -조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것(차례 · 메모까지)이 -이미 있는 조합과 같으면 새로 안 만들고 그 키(원래 키 먼저) · 새 줄의 본뜬키 = 원래 키 · 소유 현장. -""" - -from __future__ import annotations - -from typing import Any, Callable - -from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete import M02_MasterTemplete_Store as store - -_OWNER = "현장" - - -def _free_name(name: str, rows: list[dict]) -> str: - taken = {str(r.get("이름") or "") for r in rows} - n, out = 2, name - while out in taken: - out, n = f"{name} ({n})", n + 1 - return out - - -def _branch( - column: str, - version: str, - field: str, - rows: list[dict], - slot: str, - ident: tuple[str, ...], - items: list[dict], - source: str, - make: Callable[[str, list[dict]], dict], - name: str, -) -> dict[str, Any]: - """집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`. - `slot` = 담은 칸(담은로직) · `ident` = 담은 줄에서 견주는 칸(로직 · 메모).""" - table = _table(column, version) - if not isinstance(items, list) or not items: - raise store.StoreError(400, "담은 것이 비었음") - - def body(got) -> tuple: # 차례대로 · 빈 메모 = null - return tuple( - tuple(str(x.get(k) or "") for k in ident) for x in got or [] if isinstance(x, dict) - ) - - same = [r for r in rows if body(r.get(slot)) == body(items)] - hit = next((r for r in same if source and r.get("키") == source), same[0] if same else None) - if hit: - key, made_name, made = str(hit["키"]), str(hit.get("이름") or ""), False - else: - made_name = _free_name(name, rows) - key, made = make(made_name, rows), True - # ponytail: 조합 쓰기와 집계표 쓰기는 한 묶음 아님 — 사이에 집계표가 바뀌면 409 뒤 주인 없는 - # 조합 하나가 남음(다시 하면 같은 내용이라 그 키를 씀) - 판 = _point(table, column, field, key) - return {"키": key, "이름": made_name, "새로만듦": made, "집계표판": 판} - - -def _table(column: str, version: str) -> dict: - """집계표 판 검사 · 구조물 열 있는지 — 읽은 표(`{판, 문서}`).""" - table = store.read("table", store.STRUCTURE_TABLE) - if version and table["판"] != version: - raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": table["판"]}) - if not any(c["id"] == column for c in store._structure_columns()): - raise store.StoreError(404, f"{store.STRUCTURE_TABLE}에 없는 구조물 열 「{column}」") - return table - - -def _point(table: dict, column: str, field: str, key: str) -> str: - """집계표 그 열 `field` = `key` 로 저장 — 새 판.""" - doc = table["문서"] - for c in doc.get("열", []): - if isinstance(c, dict) and c.get("id") == column: - c[field] = key - return store.write("table", store.STRUCTURE_TABLE, table["판"], doc)["판"] # SS 도 여기서 맞춤 - - -def use_combo(column: str, field: str, key: str, version: str = "") -> dict[str, Any]: - """있는 조합을 이 열에 씀 — `field` 일위대가(UA) · 조합은 그대로(고르기만).""" - if field == "일위대가": - name = _combo(key).get("이름") - else: - raise store.StoreError(400, f"모르는 조합 종류 「{field}」") - 판 = _point(_table(column, version), column, field, key) - return {"키": key, "이름": str(name or ""), "새로만듦": False, "집계표판": 판} - - -def _combo(key: str) -> dict[str, Any]: - """이 층이 쓸 수 있는 조합(마스터 · 같은 회사 · 내 것) — 없으면 404 · 남의 층 것은 403.""" - row = combos.find(key) - if row is None: - raise store.StoreError(404, f"없는 조합 「{key}」") - if not key.startswith(combos.heads()): - raise store.StoreError( - 403, f"이 층에서 못 쓰는 조합 「{key}」 — 마스터 · 같은 회사 · 내 것만" - ) - return row - - -def branch_combo( - column: str, source: str, items: list[dict], name: str, version: str = "" -) -> dict[str, Any]: - """일위대가 조합 갈라 내기 — 구분 · 상세구분 · 단위 · 출처 = 본뜬 것 · 새 줄은 지금 층에.""" - was = _combo(source) - items = [ - {"로직": str(x.get("로직") or ""), "메모": x.get("메모")} - for x in items or [] - if isinstance(x, dict) - ] - - def make(made_name: str, _rows) -> str: - row = { - "이름": made_name, - **{k: was.get(k) for k in ("구분", "상세구분", "단위")}, - "담은로직": items, - "본뜬키": source, - "출처": was.get("출처"), - } - return str(combos.create({**row, "소유": _OWNER})["키"]) - - name = name.strip() or str(was.get("이름") or source) - rows = combos.rows() - return _branch( - column, version, "일위대가", rows, "담은로직", ("로직", "메모"), items, source, make, name - ) diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router.py b/M02_MasterTemplete/M02_MasterTemplete_Router.py index 42d171b9c..50e2e8bda 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router.py @@ -10,7 +10,6 @@ from typing import Any from fastapi import APIRouter, HTTPException, Query from pydantic import BaseModel -from M02_MasterTemplete import M02_MasterTemplete_Branch as branch from M02_MasterTemplete import M02_MasterTemplete_Options as options from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -22,13 +21,6 @@ class SaveBody(BaseModel): 문서: Any -class BranchBody(BaseModel): - 본뜬키: str - 담은로직: list[dict[str, Any]] - 이름: str = "" - 판: str = "" # 집계표 판 — 주면 어긋날 때 409 - - class OptionsBody(BaseModel): 판: str = "" # 옵션 표시 표 판 — 어긋나면 409 표시: dict[str, Any] # {종류: {옵션 키: 참 · 거짓}} — 거짓만 남김 @@ -63,24 +55,6 @@ def post_structure(column: str) -> dict: return _call(store.create_structure, column) -@router.post("/structures/{column}/combo") -def post_structure_combo(column: str, body: BranchBody) -> dict: - """조합 갈라 내기 — 고친 담은 로직으로 새 UA(같은 로직 집합이면 있는 키) · 이 열만 가리킴.""" - return _call(branch.branch_combo, column, body.본뜬키, body.담은로직, body.이름, body.판) - - -class UseComboBody(BaseModel): - 종류: str # 일위대가 - 키: str - 판: str = "" # 집계표 판 — 주면 어긋날 때 409 - - -@router.post("/structures/{column}/use-combo") -def post_structure_use_combo(column: str, body: UseComboBody) -> dict: - """있는 조합을 이 열에 씀 — 집계표 그 열 일위대가 키만 바꿈(조합은 그대로).""" - return _call(branch.use_combo, column, body.종류, body.키, body.판) - - @router.get("/structure-options") def get_structure_options() -> dict: """`{판, 표시, 종류}` — 표시 = 숨긴 것만 · 종류 = 옵션 있는 종류와 그 옵션.""" diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py index 55965704d..6cc306192 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Combos.py @@ -1,9 +1,8 @@ """M02 일위대가 조합 층 API(PLAN 27-2a) — `/api/m02/layers/{layer}/combos`(옛 `/api/m01/combo*`). -⚠ 등록은 `main.py`(로그인만) · 층 권한은 `M02_Layer_Access`(`Router_Items._root`) 한 규칙: - system 읽기 = 로그인 누구나 · 쓰기 = 시스템 관리자(마스터 조합 · 키 UA) - company 읽기 = 같은 회사 · 쓰기 = 회사 관리자(키 UC{회사}-######) - personal 읽기 · 쓰기 = 본인(키 UP{사용자}-######) +⚠ 등록은 `main.py`(로그인만) · 층 권한은 `M02_Layer_Access`(`Router_Items._root`) 한 규칙 · +읽기만(목록 · 키 하나) + 실무문서 통째 지우기 — 갈라 내기 · 키 적기 · 만들기 · 고치기 · 지우기 · +알약 길은 은퇴(39-1d · 정본 = 열마다 `labor/<열>.json`). 저장소 = `M02_MasterTemplete_Combo_Store`. 담을 로직 고르기 · 일위대가 입력 모달이 읽는 단가산출 로직(M01 길은 시스템 관리자 전용)은 `/api/m02/logic-lib/…` 읽기만(로그인) — 모양은 M01 길과 같음. @@ -12,10 +11,7 @@ from __future__ import annotations -from typing import Any - from fastapi import APIRouter, Depends, HTTPException, Request -from pydantic import BaseModel from common_util.common_util_auth import verify_session from M01_MasterData import M01_MasterData_Store as m01 @@ -36,11 +32,6 @@ router = APIRouter( ) -class ComboBody(BaseModel): - combo: dict[str, Any] - 판: str | None = None # 새로 만들 때 빈 글 = 지금 판 · 고칠 때 어긋나면 409 - - @router.get("/combos/all") def get_all(session: Session = Depends(verify_session)) -> list[dict]: """고르기(use-combo · 갈라 내기) — 세 층 `[{layer, owner, 판, 폴더, 줄}]` · 회사 없음 빈 층.""" @@ -69,13 +60,6 @@ def get_by_key(key: str, session: Session = Depends(verify_session)) -> dict: return {"layer": layer, "owner": owner, **_call(_root(session, layer, owner), combos.one, key)} -@router.get("/combos/key/{key}/inputs") -def get_inputs_by_key(key: str, session: Session = Depends(verify_session)) -> dict: - """조합 알약(어느 층이든) — 구조물 조합 면적산출 연결이 부름.""" - layer, owner = _where(key) - return _call(_root(session, layer, owner), combos.pills, key) - - @router.get("/layers/{layer}/combos") def get_combos(layer: Layer, owner: str | None = None, session: Session = Depends(verify_session)): return _call(_root(session, layer, owner), combos.listing) @@ -88,36 +72,6 @@ def get_combo( return _call(_root(session, layer, owner), combos.one, key) -@router.get("/layers/{layer}/combos/{key}/inputs") -def get_combo_inputs( - layer: Layer, key: str, owner: str | None = None, session: Session = Depends(verify_session) -): - """조합 알약 — 담은 로직 결과단위 분모마다 물량 하나(M02 면적산출이 받음).""" - return _call(_root(session, layer, owner), combos.pills, key) - - -@router.post("/layers/{layer}/combos") -def post_combo( - layer: Layer, - body: ComboBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - folder = _root(session, layer, owner, write=True) - return _call(folder, combos.create, body.combo, body.판 or None) - - -@router.delete("/layers/{layer}/combos/{key}") -def delete_combo( - layer: Layer, - key: str, - 판: str = "", - owner: str | None = None, - session: Session = Depends(verify_session), -): - return _call(_root(session, layer, owner, write=True), combos.delete, key, 판) - - @router.delete("/practice") def delete_practice(판: str = "", session: Session = Depends(verify_session)) -> dict: """실무문서 통째 지우기(PLAN 39-2) — 시스템 관리자만 · 마스터 조합 표 줄을 모두 비움 · diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py index 1c2801d1c..3eeb6488f 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Items.py @@ -26,14 +26,9 @@ from common_util.common_util_auth import verify_session from config.config_db import get_db_pool from M02_MasterTemplete import M02_Item_Paste as paste_store from M02_MasterTemplete import M02_Layer_Access as access -from M02_MasterTemplete import M02_MasterTemplete_Branch as branch from M02_MasterTemplete import M02_MasterTemplete_Items as items_store from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree -from M02_MasterTemplete.M02_MasterTemplete_Router import ( - BranchBody, - UseComboBody, -) logger = logging.getLogger(__name__) #: 이 요청이 쓴 남의 층(`others_write`) — 길이 성공해 답한 뒤에만 시스템 로그(`NotedRoute`) @@ -306,33 +301,6 @@ def post_structure( return _call(_root(session, layer, owner, write=True), store.create_structure, column) -@router.post("/layers/{layer}/structures/{column}/combo") -def post_combo( - layer: Layer, - column: str, - body: BranchBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - """일위대가 조합 갈라 내기 — 새 조합은 이 층 조합 표에(`Combo_Store`).""" - folder = _root(session, layer, owner, write=True) - return _call( - folder, branch.branch_combo, column, body.본뜬키, body.담은로직, body.이름, body.판 - ) - - -@router.post("/layers/{layer}/structures/{column}/use-combo") -def post_use_combo( - layer: Layer, - column: str, - body: UseComboBody, - owner: str | None = None, - session: Session = Depends(verify_session), -): - folder = _root(session, layer, owner, write=True) - return _call(folder, branch.use_combo, column, body.종류, body.키, body.판) - - def _copy_practice(column: str, key: str) -> dict[str, Any]: """실무문서 조합(마스터 UA) → 이 열 일위대가 문서(입력값 빈 값 · 39-1b).""" from M02_MasterTemplete import M02_Labor_Doc as labor diff --git a/resources/tester/test_m01_combo.py b/resources/tester/test_m01_combo.py index f326dcf7a..8cf31f4f4 100644 --- a/resources/tester/test_m01_combo.py +++ b/resources/tester/test_m01_combo.py @@ -26,6 +26,8 @@ from common_util.common_util_auth import verify_session from M01_MasterData import M01_MasterData_Router as router_module from M01_MasterData import M01_MasterData_Store as store from M01_MasterData import M01_MasterData_Store_Combo as combo_store +from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos +from M02_MasterTemplete import M02_MasterTemplete_Store as m02 from M02_MasterTemplete.M02_MasterTemplete_Router_Combos import router as combos_router sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts")) @@ -72,9 +74,15 @@ def _post(client: TestClient, url: str, body: dict, want: int = 200) -> dict: def _new(client: TestClient, body: dict, want: int = 200) -> dict: - """옛 `/combo/new` 본문 `{combo, owner?}` → 층 길(소유는 줄 칸) · 옛 이름 `key` 도 붙임.""" + """옛 `/combo/new` 본문 `{combo, owner?}` → 마스터 조합 저장소(만들기 길은 은퇴 39-1d · + 소유는 줄 칸) · 옛 이름 `key` 도 붙임.""" combo = {**body["combo"], **({"소유": body["owner"]} if "owner" in body else {})} - got = _post(client, SYS, {"combo": combo}, want) + try: + got = combos.create(combo) + except m02.StoreError as error: + assert error.status == want, error.detail + return {"detail": error.detail} + assert want == 200 return {"key": got.get("키"), **got} @@ -198,7 +206,7 @@ def test_로직을_빼고_더하고_지울_수_있다(client: TestClient) -> Non assert done["combo"]["키"] == key # 키는 안 바뀜 now = _one(client, key) - assert client.delete(f"{SYS}/{key}", params={"판": now["version"]}).status_code == 200 + combos.delete(key, now["version"]) # 지우기 길은 은퇴(39-1d) — 저장소 함수로 assert _list(client) == [] diff --git a/resources/tester/test_m01_combo_inputs.py b/resources/tester/test_m01_combo_inputs.py index 47b805ccb..f59dc3b0a 100644 --- a/resources/tester/test_m01_combo_inputs.py +++ b/resources/tester/test_m01_combo_inputs.py @@ -22,6 +22,8 @@ from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from M01_MasterData import M01_MasterData_Store as store from M01_MasterData import M01_MasterData_Store_Combo as combo_store +from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos +from M02_MasterTemplete import M02_MasterTemplete_Store as m02 from M02_MasterTemplete.M02_MasterTemplete_Router_Combos import router as combos_router SYS = "/api/m02/layers/system/combos" @@ -41,9 +43,8 @@ def client() -> TestClient: def _pills(client: TestClient, key: str) -> dict: - res = client.get(f"{SYS}/{key}/inputs") - assert res.status_code == 200, res.text - return res.json() + """알약 길은 은퇴(39-1d) — 규칙은 저장소 `combo_inputs`(일위대가 문서 알약이 같은 규칙을 씀).""" + return combo_store.combo_inputs(key) def test_기슭막이_찰쌓기_인력_장비는_한_알약(client) -> None: @@ -70,7 +71,9 @@ def test_준공표지석_물량_여섯(client) -> None: def test_없는_조합은_404(client) -> None: - assert client.get(f"{SYS}/UA999999/inputs").status_code == 404 + with pytest.raises(store.StoreError) as caught: + combo_store.combo_inputs("UA999999") + assert caught.value.status == 404 @pytest.fixture @@ -87,10 +90,9 @@ def own(tmp_path: Path, monkeypatch) -> TestClient: def _new(client: TestClient, name: str, codes: list[str]) -> str: + """조합 만들기 길은 은퇴(39-1d) — 저장소 함수로(사본 마스터).""" combo = {"이름": name, "담은로직": [{"로직": c, "메모": None} for c in codes]} - res = client.post(SYS, json={"combo": combo}) - assert res.status_code == 200, res.text - return res.json()["키"] + return combos.create(combo)["키"] def test_알약_키는_로직을_빼고_더하고_차례를_바꿔도_같다(own) -> None: @@ -117,9 +119,10 @@ def test_쓰는_열이_있으면_조합을_못_지우고_구조물_이름을_알 key = _new(own, "쓰는 조합", ["GF000222"]) _use(combo_store.USERS[0], key) version = own.get(f"{SYS}/{key}").json()["판"] - res = own.delete(f"{SYS}/{key}", params={"판": version}) - assert res.status_code == 409 - detail = res.json()["detail"] + with pytest.raises(m02.StoreError) as caught: # 지우기 길은 은퇴(39-1d) — 저장소 함수로 + combos.delete(key, version) + assert caught.value.status == 409 + detail = caught.value.detail assert detail["users"] == ["돌기슭막이 찰쌓기"] assert "돌기슭막이 찰쌓기" in detail["errors"][0] assert own.get(f"{SYS}/{key}").status_code == 200 # 그대로 남음 @@ -129,19 +132,21 @@ def test_안_쓰는_조합은_지워진다(own) -> None: key = _new(own, "안 쓰는 조합", ["GF000222"]) _use(combo_store.USERS[0], "UA000009") # 다른 조합을 씀 version = own.get(f"{SYS}/{key}").json()["판"] - assert own.delete(f"{SYS}/{key}", params={"판": version}).status_code == 200 + combos.delete(key, version) assert own.get(f"{SYS}/{key}").status_code == 404 def test_본뜬키는_목록에_나오고_없는_키_자기_키는_400(own) -> None: base = _new(own, "원본", ["GF000222"]) combo = {"이름": "본뜬 것", "본뜬키": base, "담은로직": [{"로직": "GF000222", "메모": None}]} - got = own.post(SYS, json={"combo": combo}).json() + got = combos.create(combo) assert got["combo"]["본뜬키"] == base rows = {r["키"]: r for r in own.get(SYS).json()["줄"]} assert rows[got["키"]]["본뜬키"] == base and rows[base]["본뜬키"] is None bad = {**combo, "이름": "없는 원본", "본뜬키": "UA999999"} - assert own.post(SYS, json={"combo": bad}).status_code == 400 + with pytest.raises(m02.StoreError) as caught: + combos.create(bad) + assert caught.value.status == 400 version = own.get(f"{SYS}/{base}").json()["판"] me = {"이름": "원본", "본뜬키": base, "담은로직": combo["담은로직"]} with pytest.raises(store.StoreError) as caught: # 조합 고치기 길(PUT) 은퇴 — 저장소 함수로 diff --git a/resources/tester/test_m02_combo_branch.py b/resources/tester/test_m02_combo_branch.py index 2dbb38789..dd9b15f6b 100644 --- a/resources/tester/test_m02_combo_branch.py +++ b/resources/tester/test_m02_combo_branch.py @@ -1,11 +1,7 @@ -"""조합 갈라 내기 · 분류 더하기 복사 (PLAN 15장). +"""조합 갈라 내기 은퇴 · 분류 더하기 복사 (PLAN 15장 · 39-1d). M02 견본 표(`write_sample_table`) + M01 마스터 사본(조합 줄은 비워 시작)으로만 봄. - · 일위대가(UA) — 새로 만듦(본뜬키 = 원래 키) · 이 열만 새 키 · 다른 열은 옛 키 · 원래 조합 그대로 · - SS 장부 맞춤 · 담은 로직(차례 · 메모까지)이 같으면 있는 키(되돌리면 원래 키) · 차례 · 메모만 - 바꿔도 새로 · 또 고치면 또 새로 · 같은 이름 (2) - · 자재 조합(UM)은 없앰 — 키 적기 종류 「자재조합」 = 400 - · 집계표 판 어긋남 = 409 · 조합 안 만듦 + · 갈라 내기 · 키 적기 길은 은퇴(39-1d) — 404 · 분류 더하기로 복사한 열(`본뜬열`) — [저장] 때 원본 키 · 세 파일 내용을 새 번호로 받음 """ @@ -70,114 +66,17 @@ def _source(client) -> str: return got["key"] -def _branch(client, column, source, keys, name="", 판="", want=200): - """`keys` = 로직 키 또는 (로직, 메모)""" - lines = [{"로직": k[0], "메모": k[1]} if isinstance(k, tuple) else {"로직": k} for k in keys] - body = {"본뜬키": source, "담은로직": lines, "이름": name, "판": 판} - res = client.post(f"/api/m02/structures/{column}/combo", json=body) - assert res.status_code == want, res.text - return res.json() - - -def _cols(client, field="일위대가") -> dict[str, str | None]: - return {c["id"]: c.get(field) for c in client.get(TABLE).json()["문서"]["열"]} - - -def _ss(client, field="일위대가") -> dict[str, str | None]: - rows = client.get("/api/m02/structure-combos").json()["줄"] - return {r["열"]: r[field] for r in rows} - - -def test_새로_만듦_이_열만_새_키_원래_조합_그대로(client): +def test_갈라_내기_키_적기_길은_은퇴(client): + """39-1d — 일위대가 정본 = 열마다 labor 문서 · 갈라 내기 · 키 적기 길 없음.""" ua = _source(client) - 판 = client.get(TABLE).json()["판"] - got = _branch(client, "rv", ua, [A, B, C], "기슭막이 · 돌기슭막이", 판) - assert got["새로만듦"] is True and got["키"] != ua - assert got["이름"] == "기슭막이 · 돌기슭막이" - assert got["집계표판"] == client.get(TABLE).json()["판"] != 판 - cols = _cols(client) - assert (cols["rv"], cols["c1"]) == (got["키"], ua) # 다른 열은 옛 키 - assert _ss(client)["rv"] == got["키"] and _ss(client)["c1"] == ua # SS 장부 맞춤 - new = combo_store.combo(got["키"])["combo"] - assert tuple(new) == combo_store.mcb.COLS # 열 한 벌 · 본뜬키 칸 포함 - assert [x["로직"] for x in new["담은로직"]] == [A, B, C] - assert (new["본뜬키"], new["구분"], new["상세구분"], new["단위"], new["출처"], new["소유"]) == ( - ua, - "실무 일위대가", - "13장 구조물", - "m", - "자체", - "현장", - ) - assert [x["로직"] for x in combo_store.combo(ua)["combo"]["담은로직"]] == [A, B] - # M01 에서 고쳐도(화면이 본뜬키를 안 보내도) 계보는 남음 - one = combo_store.combo(got["키"]) - edited = {k: v for k, v in one["combo"].items() if k != "본뜬키"} | {"이름": "고친 이름"} - combo_store.combo_edit(got["키"], one["version"], edited) - assert combo_store.combo(got["키"])["combo"]["본뜬키"] == ua - - -def test_차례_메모까지_같으면_있는_키_또_고치면_또_새로_같은_이름은_2(client): - ua = _source(client) - same = _branch(client, "c1", ua, [(A, ""), (B, None)]) # 빈 메모 = null → 원래 조합 - assert (same["키"], same["새로만듦"]) == (ua, False) - order = _branch(client, "c1", ua, [B, A]) # 차례만 바꿈 = 새로 - assert order["새로만듦"] is True and order["키"] != ua - got = combo_store.combo(order["키"])["combo"] - assert ([x["로직"] for x in got["담은로직"]], got["본뜬키"]) == ([B, A], ua) - memo = _branch(client, "c1", order["키"], [B, (A, "사면 쪽")]) # 메모만 바꿈 = 또 새로 - assert memo["새로만듦"] is True and memo["키"] not in (ua, order["키"]) - assert combo_store.combo(memo["키"])["combo"]["담은로직"][1]["메모"] == "사면 쪽" - back = _branch(client, "c1", memo["키"], [A, B]) # 원래와 똑같이 되돌림 = 원래 키 - assert (back["키"], back["새로만듦"]) == (ua, False) - assert _cols(client)["c1"] == ua and _ss(client)["c1"] == ua - first = _branch(client, "rv", ua, [A, B, C], "갈래") - again = _branch(client, "rv", first["키"], [A, C], "갈래") # 갈라 낸 것을 또 고침 = 또 새로 - assert again["새로만듦"] is True and again["키"] not in (ua, first["키"]) - assert again["이름"] == "갈래 (2)" - assert combo_store.combo(again["키"])["combo"]["본뜬키"] == first["키"] - twin = _branch(client, "c1", ua, [A, B, C]) # 다른 열이 똑같이 고침 = 있는 키 - assert (twin["키"], twin["새로만듦"]) == (first["키"], False) - assert _cols(client)["c1"] == first["키"] and _ss(client)["c1"] == first["키"] - - -def test_집계표_판_어긋나면_409_조합_안_만듦(client): - ua = _source(client) - before = len(m01.read(combo_store.FILE)[0]["줄"]) - _branch(client, "rv", ua, [A, B, C], "x", "0000000000000000", want=409) - assert len(m01.read(combo_store.FILE)[0]["줄"]) == before - assert _cols(client)["rv"] == ua - _branch(client, "no", ua, [A, C], want=404) # 글 열(구조물 아님) - _branch(client, "rv", "UA999999", [A, C], want=404) # 없는 원래 조합 - - -def _use(client, column, kind, key, 판="", want=200): - body = {"종류": kind, "키": key, "판": 판} - res = client.post(f"/api/m02/structures/{column}/use-combo", json=body) - assert res.status_code == want, res.text - return res.json() - - -def test_있는_조합을_이_열에_씀_조합은_그대로(client): - ua = _source(client) - other = _branch(client, "rv", ua, [A, C], "다른") - count = len(m01.read(combo_store.FILE)[0]["줄"]) - 판 = client.get(TABLE).json()["판"] - got = _use(client, "c1", "일위대가", other["키"], 판) - assert (got["키"], got["이름"], got["새로만듦"]) == (other["키"], "다른", False) - assert got["집계표판"] == client.get(TABLE).json()["판"] != 판 - assert _cols(client)["c1"] == other["키"] and _ss(client)["c1"] == other["키"] - assert len(m01.read(combo_store.FILE)[0]["줄"]) == count # 새로 안 만듦 - _use(client, "c1", "일위대가", ua, 판, want=409) # 판 어긋남 — 안 바뀜 - assert _cols(client)["c1"] == other["키"] - _use(client, "c1", "일위대가", "UA999999", want=404) - _use(client, "no", "일위대가", ua, want=404) # 글 열 - _use(client, "c1", "모름", ua, want=400) - _use(client, "c1", "자재조합", "UM000001", want=400) # 자재 조합은 없앰 + body = {"본뜬키": ua, "담은로직": [{"로직": A}], "이름": "", "판": ""} + assert client.post("/api/m02/structures/rv/combo", json=body).status_code == 404 + use = {"종류": "일위대가", "키": ua, "판": ""} + assert client.post("/api/m02/structures/c1/use-combo", json=use).status_code == 404 def test_분류_더하기로_복사한_열은_원본_키_세_파일_내용을_받음(client): - ua = _source(client) + _source(client) drawing = client.get("/api/m02/templates/structure/rv").json() marked = {**drawing["문서"], "도면": {"format": 6, "entities": [{"type": "line"}]}} put = client.put("/api/m02/templates/structure/rv", json={"판": drawing["판"], "문서": marked}) diff --git a/resources/tester/test_m02_combo_layers.py b/resources/tester/test_m02_combo_layers.py index 0053c5cf9..3eab7b262 100644 --- a/resources/tester/test_m02_combo_layers.py +++ b/resources/tester/test_m02_combo_layers.py @@ -5,8 +5,9 @@ - 권한 = 층 규칙(마스터 쓰기 시스템 관리자 · 회사 쓰기 회사 관리자 · 개인 쓰기 본인 · 읽기 규칙). - 검사 = 마스터와 같음(빈 조합 · 이름 겹침 · 없는 본뜬키) · 판 어긋남 409. - 나무 고침 · 층 사이 붙여넣기 길은 없음(PLAN 39-2 · 일위대가는 구조물 열마다). - - 고르기(use-combo) · 갈라 내기 = 세 층 조합 · 남의 층 것 403 · 갈라 낸 새 조합은 그 층에. - - 조합 이름 · 알약 · 스냅샷 원본이 회사 · 개인 조합도 읽음. + - 만들기 · 고치기 · 지우기 · 알약 · 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 만들기 · + 지우기는 층 문(`_root`) + 저장소 함수로 봄. + - 세 층 목록 · 키 하나 읽기 · 조합 이름 · 알약(저장소) · 스냅샷 원본이 회사 · 개인 조합도 읽음. 정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본. """ @@ -18,16 +19,14 @@ from pathlib import Path from typing import Any import pytest -from fastapi import FastAPI +from fastapi import FastAPI, HTTPException from fastapi.testclient import TestClient from common_util.common_util_auth import verify_session from config import config_system from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store -from M02_MasterTemplete import M02_Item_Source as source from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos -from M02_MasterTemplete import M02_MasterTemplete_Items as items_store from M02_MasterTemplete import M02_MasterTemplete_Router_Combos as combos_router from M02_MasterTemplete import M02_MasterTemplete_Router_Items as items_router from M02_MasterTemplete import M02_MasterTemplete_Store as store @@ -66,7 +65,6 @@ def test_조합_길은_시스템_관리자만(world): assert client.get(URL.format("system")).status_code == 403 assert client.get("/api/m02/combos/all").status_code == 403 assert client.get("/api/m02/combos/key/UA000001").status_code == 403 - assert _new(client, "personal", _combo("내 것"), 403) is not None assert client.get("/api/m02/logic-lib/logics").status_code == 200 world["session"]["is_master"] = True assert _as(world, "USER").get(URL.format("system")).status_code == 403 # 회사 마스터도 @@ -86,9 +84,20 @@ def _combo(name: str, codes=(A,), **more) -> dict: def _new(client: TestClient, layer: str, combo: dict, want: int = 200, **params) -> dict: - res = client.post(URL.format(layer), json={"combo": combo}, params=params) - assert res.status_code == want, res.text - return res.json() + """만들기 길은 은퇴(39-1d) — 층 문(`_root` · 권한 · 회사 없음 409) + 저장소 `create`.""" + session = client.app.dependency_overrides[verify_session]() + try: + folder = items_router._root(session, layer, params.get("owner"), write=True) + with store.at(folder): + got = combos.create(combo, None) + except HTTPException as error: + code, detail = error.status_code, error.detail + except store.StoreError as error: + code, detail = error.status, error.detail + else: + code, detail = 200, None + assert code == want, detail + return got if code == 200 else {"detail": detail} def _list(client: TestClient, layer: str, **params) -> dict: @@ -109,8 +118,8 @@ def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world): mine = _new(me, "personal", _combo("내 것", (A, B))) assert mine["키"] == "UP42-000001" assert (world["storage"] / "7" / "42" / "templates" / combos.FILE).is_file() - gone = me.delete(f"{URL.format('personal')}/{mine['키']}", params={"판": mine["판"]}) - assert gone.status_code == 200 + with store.at(layers.personal_dir(7, 42)): # 지우기 길은 은퇴(39-1d) — 저장소 함수로 + combos.delete(mine["키"], mine["판"]) assert _new(me, "personal", _combo("다음 것"))["키"] == "UP42-000002" got = me.get(f"{URL.format('personal')}/UP42-000002").json() assert [x["로직"] for x in got["줄"]] == [A] and got["blocked"] is False @@ -127,23 +136,32 @@ def test_권한_층_규칙(world): _as(world, "ADMIN") _new(user, "company", _combo("회사")) world["session"]["company_id"] = None - assert user.post(URL.format("personal"), json={"combo": _combo("x")}).status_code == 409 + _new(user, "personal", _combo("x"), 409) def test_검사_빈_조합_이름_겹침_본뜬키_판(world): me = _as(world) got = _new(me, "personal", _combo("빈 것", ()), 422) assert any("빈 조합" in x for x in got["detail"]["errors"]) - first = _new(me, "personal", _combo("원본")) + _new(me, "personal", _combo("원본")) assert "이미 있음" in _new(me, "personal", _combo("원본"), 422)["detail"]["errors"][0] _new(me, "personal", _combo("엉터리 계보", 본뜬키="UP42-999999"), 400) master_key = m01.read(combo_store.FILE)[0]["줄"][0]["키"] branched = _new(me, "personal", _combo("마스터 본뜸", 본뜬키=master_key)) assert branched["combo"]["본뜬키"] == master_key # 층을 넘는 계보 - url = f"{URL.format('personal')}/{first['키']}" - assert ( - me.put(url, json={"combo": _combo("원본 고침")}).status_code == 405 - ) # 조합 고치기 은퇴(39-1d) + + +def test_조합_쓰기_알약_길은_은퇴(world): + """39-1d — 만들기 · 고치기 · 지우기 · 알약 길 없음(읽기 · 실무문서 통째 지우기만 남음).""" + me = _as(world, "SYSTEM_ADMIN") + key = _new(me, "personal", _combo("원본"))["키"] + url = f"{URL.format('personal')}/{key}" + assert me.post(URL.format("personal"), json={"combo": _combo("새")}).status_code == 405 + assert me.put(url, json={"combo": _combo("고침")}).status_code == 405 + assert me.delete(url, params={"판": ""}).status_code == 405 + assert me.get(f"{url}/inputs").status_code == 404 + assert me.get(f"/api/m02/combos/key/{key}/inputs").status_code == 404 + assert me.get(url).status_code == 200 # 읽기는 남음 # ── 마스터 사본 ─────────────────────────────────────────────────────── @@ -184,49 +202,18 @@ def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(wo world["session"]["company_id"] = 8 # 다른 회사 사람 — 남의 회사 조합은 못 봄 assert me.get(f"/api/m02/combos/key/{uc}").status_code == 403 world["session"]["company_id"] = 7 - - column = me.post( - "/api/m02/layers/personal/tree", - json={"op": "add-item", "경로": [], "이름": "돌붙임", "단위": "m", "판": _판(me)}, - ).json()["알림"]["열"] - base = f"/api/m02/layers/personal/structures/{column}" - for key in (uc, up): - res = me.post(f"{base}/use-combo", json={"종류": "일위대가", "키": key}) - assert res.status_code == 200 and res.json()["키"] == key - lines = [{"로직": A}, {"로직": B, "메모": "장비"}] # 이 층에 같은 내용 없음 = 새로 - made = me.post(f"{base}/combo", json={"본뜬키": uc, "담은로직": lines, "이름": "회사 것 고침"}) - assert made.status_code == 200, made.text - new = made.json()["키"] - assert new == "UP42-000002" and combos.find(new)["본뜬키"] == uc - folder = layers.personal_dir(7, 42) - item = next(i for i in items_store.items(folder) if i["열"] == column) - assert item["일위대가"] == new and item["names"]["일위대가"] == "회사 것 고침" - assert source.labor_rows(new)["줄"]["키"] == new # 프로젝트 스냅샷 원본도 층 조합 - - _as(world, "ADMIN") - company_col = boss.post( - "/api/m02/layers/company/tree", - json={ - "op": "add-item", - "경로": [], - "이름": "회사 항목", - "단위": "m", - "판": boss.get("/api/m02/layers/company/tree").json()["판"], - }, - ).json()["알림"]["열"] - res = boss.post( - f"/api/m02/layers/company/structures/{company_col}/use-combo", - json={"종류": "일위대가", "키": up}, - ) - assert res.status_code == 403 # 회사 항목은 개인 조합을 못 씀 + # 고르기(use-combo) · 갈라 내기 길은 은퇴(39-1d) — 열마다 labor 문서 + base = "/api/m02/layers/personal/structures/c1" + assert me.post(f"{base}/use-combo", json={"종류": "일위대가", "키": up}).status_code == 404 + assert me.post(f"{base}/combo", json={"본뜬키": uc, "담은로직": []}).status_code == 404 def test_면적_알약이_개인_조합을_읽음(world): me = _as(world) up = _new(me, "personal", _combo("찰쌓기", (A, B)))["키"] - got = me.get(f"{URL.format('personal')}/{up}/inputs").json() + with store.at(layers.personal_dir(7, 42)): # 알약 길은 은퇴(39-1d) — 저장소 함수로 + got = combos.pills(up) assert [p["키"] for p in got["물량"]] == ["돌쌓기 찰쌓기|㎡"] - assert me.get(f"/api/m02/combos/key/{up}/inputs").json() == got # 항목 알약 길(`/items/{key}/pills`)은 선택 입력과 같이 없앰(39-7b) diff --git a/resources/tester/test_m02_layers_store.py b/resources/tester/test_m02_layers_store.py index 1bd3308f5..139adb8e0 100644 --- a/resources/tester/test_m02_layers_store.py +++ b/resources/tester/test_m02_layers_store.py @@ -108,13 +108,11 @@ def test_회사_층_첫_진입_뼈대와_쓰기_권한(world): 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 # 남의 회사 - # 일위대가 갈라 내기(27-2c) = 회사 층 쓰기 권한 — 일반 사용자 403 · 회사 관리자는 회사 조합(UC) + # 일위대가 갈라 내기 길은 은퇴(39-1d · 정본 = labor 문서) branch = "/api/m02/layers/company/structures/cw/combo" body = {"본뜬키": "UA000001", "담은로직": [{"로직": "GF000221"}], "이름": "x"} - assert _as(world).post(branch, json=body).status_code == 403 admin = _as(world, "ADMIN") - made = admin.post(branch, json=body) - assert made.status_code == 200 and made.json()["키"] == "UC7-000001" + assert admin.post(branch, json=body).status_code == 404 assert admin.delete(f"{url}?판={client.get(url).json()['판']}").status_code == 409 # 쓰는 양식 @@ -129,21 +127,17 @@ def test_개인_층은_본인만_회사_없으면_409(world): groups = lonely.get("/api/m02/items?all=1").json() assert [(g["layer"], g["items"]) for g in groups][1:] == [("company", []), ("personal", [])] assert len(groups[0]["items"]) == len(store.read_combos()["줄"]) # 마스터는 보임 - made = lonely.post( - "/api/m02/layers/personal/structures/p1/use-combo", - json={"종류": "일위대가", "키": "UA000001"}, - ) + made = lonely.post("/api/m02/layers/personal/structures/p1") # 쓰기 길(키 적기는 은퇴) assert made.status_code == 409 and "회사부터" in made.json()["detail"] def test_회사_항목을_개인으로_복사(world): admin = _as(world, "ADMIN") - _add_column(admin, "company", COL) + _add_column(admin, "company", {**COL, "일위대가": "UA000004"}) # 키 적기 길은 은퇴(39-1d) base = "/api/m02/layers/company" drawing = admin.get(f"{base}/templates/structure/cw").json() doc = {**drawing["문서"], "도면": {**drawing["문서"]["도면"], "entities": [{"type": "line"}]}} admin.put(f"{base}/templates/structure/cw", json={"판": drawing["판"], "문서": doc}) - admin.post(f"{base}/structures/cw/use-combo", json={"종류": "일위대가", "키": "UA000004"}) row = admin.get("/api/m02/items?layer=company").json()[0] ss = row["key"] ua_name = combos.find("UA000004")["이름"]