refactor(M02): 일위대가 조합 남은 은퇴 — 갈라 내기 · 키 적기(use-combo) · 조합 만들기 · 지우기 · 알약 길 지움 · Branch.py 지움 · 시험은 저장소 함수 · 은퇴 길 404 · 405 로(39-1d)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rJwJMVL4EE31y77TB11AP
This commit is contained in:
2026-10-04 20:58:51 +09:00
co-authored by Claude Opus 5.5
parent a6d7851e83
commit 022a8d854d
9 changed files with 86 additions and 427 deletions
@@ -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
)
@@ -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:
"""`{판, 표시, 종류}` — 표시 = 숨긴 것만 · 종류 = 옵션 있는 종류와 그 옵션."""
@@ -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) — 시스템 관리자만 · 마스터 조합 표 줄을 모두 비움 ·
@@ -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
+11 -3
View File
@@ -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) == []
+18 -13
View File
@@ -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) 은퇴 — 저장소 함수로
+9 -110
View File
@@ -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})
+41 -54
View File
@@ -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)
+4 -10
View File
@@ -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")["이름"]