Merge remote-tracking branch 'origin/sub_laptop_5' into sub_laptop_1

This commit is contained in:
2026-10-04 18:04:04 +09:00
8 changed files with 27 additions and 510 deletions
+8 -162
View File
@@ -4,8 +4,7 @@
- 키 = 회사 `UC{회사}-######` · 개인 `UP{사용자}-######` · 지운 번호 다시 안 줌 · 마스터 UA.
- 권한 = 층 규칙(마스터 쓰기 시스템 관리자 · 회사 쓰기 회사 관리자 · 개인 쓰기 본인 · 읽기 규칙).
- 검사 = 마스터와 같음(빈 조합 · 이름 겹침 · 없는 본뜬키) · 판 어긋남 409.
- 나무 = 폴더 [구분, 상세구분] 깊이 2 · 더하기 · 이름 · 옮기기 · 지우기(쓰는 조합 409).
- 마스터 나무 고침 = 줄은 M01 저장 길 · 빈 폴더는 머리 `폴더` · 틀 검사 그대로 통과.
- 나무 고침 · 층 사이 붙여넣기 길은 없음(PLAN 39-2 · 일위대가는 구조물 열마다).
- 고르기(use-combo) · 갈라 내기 = 세 층 조합 · 남의 층 것 403 · 갈라 낸 새 조합은 그 층에.
- 조합 이름 · 알약 · 스냅샷 원본이 회사 · 개인 조합도 읽음.
정본(마스터 · 마스터 템플릿)은 읽기만 · 쓰기는 임시 storage 와 마스터 사본.
@@ -98,13 +97,6 @@ def _list(client: TestClient, layer: str, **params) -> dict:
return res.json()
def _tree(client: TestClient, layer: str, want: int = 200, **body) -> dict:
body.setdefault("판", _list(client, layer)["판"])
res = client.post(URL.format(layer) + "/tree", json=body)
assert res.status_code == want, res.text
return res.json()
# ── 키 · 자리 ─────────────────────────────────────────────────────────
def test_회사_개인_키_장부_지운_번호는_다시_안_줌(world):
boss = _as(world, "ADMIN")
@@ -154,60 +146,7 @@ def test_검사_빈_조합_이름_겹침_본뜬키_판(world):
) # 조합 고치기 은퇴(39-1d)
# ── 나무 ─────────────────────────────────────────────────────────────
def test_나무_폴더_깊이_2_이름_옮기기_지우기(world):
me = _as(world)
key = _new(me, "personal", _combo("돌쌓기 한 벌"))["키"]
_tree(me, "personal", op="add-folder", 경로=[], 이름="돌쌓기")
_tree(me, "personal", op="add-folder", 경로=["돌쌓기"], 이름="찰쌓기")
_tree(me, "personal", 422, op="add-folder", 경로=["돌쌓기", "찰쌓기"], 이름="셋째")
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="돌쌓기")
_tree(me, "personal", 404, op="move", 키=key, 새경로=["없음"])
got = _tree(me, "personal", op="move", 키=key, 새경로=["돌쌓기", "찰쌓기"])
row = next(r for r in got["줄"] if r["키"] == key)
assert (row["구분"], row["상세구분"]) == ("돌쌓기", "찰쌓기")
got = _tree(me, "personal", op="rename", 경로=["돌쌓기"], 이름="석축")
assert got["폴더"] == [["석축"], ["석축", "찰쌓기"]]
assert next(r for r in got["줄"] if r["키"] == key)["구분"] == "석축"
_tree(me, "personal", 422, op="move", 경로=["석축"], 새경로=["석축", "찰쌓기"])
got = _tree(me, "personal", op="move", 경로=["석축", "찰쌓기"], 새경로=[])
assert got["폴더"] == [["석축"], ["찰쌓기"]] # 빈 「석축」 은 남음
row = next(r for r in got["줄"] if r["키"] == key)
assert (row["구분"], row["상세구분"]) == ("찰쌓기", None)
_tree(me, "personal", 409, op="add-folder", 경로=[], 이름="석축", 판="낡은판")
_new(me, "personal", _combo("다른 것"))
got = _tree(me, "personal", op="rename", 키=key, 이름="찰쌓기 한 벌")
assert next(r for r in got["줄"] if r["키"] == key)["이름"] == "찰쌓기 한 벌"
assert (
"이미 있음"
in _tree(me, "personal", 422, op="rename", 키=key, 이름="다른 것")["detail"]["errors"][0]
)
other = next(r["키"] for r in _list(me, "personal")["줄"] if r["이름"] == "다른 것")
_tree(me, "personal", op="delete", 키=other)
got = _tree(me, "personal", op="delete", 경로=["찰쌓기"])
assert got["지운조합"] == [key] and got["줄"] == [] and got["폴더"] == [["석축"]]
def test_쓰는_조합이_든_폴더는_못_지움_이름을_알림(world):
me = _as(world)
key = _new(me, "personal", _combo("쓰는 것", 구분="석축"))["키"]
tree = me.post(
"/api/m02/layers/personal/tree",
json={"op": "add-item", "경로": ["옹벽"], "이름": "찰쌓기", "단위": "m", "판": _판(me)},
).json()
column = tree["알림"]["열"]
use = me.post(
f"/api/m02/layers/personal/structures/{column}/use-combo",
json={"종류": "일위대가", "키": key},
)
assert use.status_code == 200, use.text
got = _tree(me, "personal", 409, op="delete", 경로=["석축"])
assert got["detail"]["users"] == ["옹벽 찰쌓기"]
res = me.delete(f"{URL.format('personal')}/{key}", params={"판": _list(me, "personal")["판"]})
assert res.status_code == 409
assert [r["키"] for r in _list(me, "personal")["줄"]] == [key] # 그대로 남음
# ── 마스터 사본 ───────────────────────────────────────────────────────
def _판(client: TestClient) -> str:
return client.get("/api/m02/layers/personal/tree").json()["판"]
@@ -227,28 +166,6 @@ def master_copy(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
return folder
def test_마스터_나무는_M01_저장_길_빈_폴더는_머리_틀_검사_통과(world, master_copy):
admin = _as(world, "SYSTEM_ADMIN")
before = _list(admin, "system")
first = before["줄"][0]["키"]
got = _tree(admin, "system", op="rename", 키=first, 이름="이름 바꾼 견본")
assert next(r for r in got["줄"] if r["키"] == first)["이름"] == "이름 바꾼 견본"
before = _list(admin, "system")
old = next(r["구분"] for r in before["줄"] if r["구분"]) # 정본 폴더 이름에 기대지 않음
assert [old] in before["폴더"]
_tree(admin, "system", op="add-folder", 경로=[], 이름="빈 폴더")
got = _tree(admin, "system", op="rename", 경로=[old], 이름="시험 폴더")
assert ["시험 폴더"] in got["폴더"] and ["빈 폴더"] in got["폴더"]
assert [old] not in got["폴더"]
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
assert ["빈 폴더"] in doc["폴더"]
assert sum(r["구분"] == "시험 폴더" for r in doc["줄"]) == len(
[r for r in before["줄"] if r["구분"] == old]
)
assert m01.cm.check_form(combos.FILE, doc) == [] # 마스터 틀 그대로
_tree(_as(world), "system", 403, op="add-folder", 경로=[], 이름="막힘")
# ── 세 층 고르기 · 갈라 내기 · 읽는 곳 ─────────────────────────────────
def test_세_층_목록_고르기_남의_층_403_갈라_낸_것은_그_층에(world):
boss = _as(world, "ADMIN")
@@ -335,81 +252,10 @@ def test_담을_로직_읽기는_로그인이면_됨(world):
assert user.get("/api/m02/logic-lib/auto", params={"key": A}).status_code == 200
# ── 층 사이 복사 · 여러 개 옮기기(PLAN 30-3) ─────────────────────────────
def _paste(client: TestClient, layer: str, body: dict, want: int = 200, **params) -> dict:
body.setdefault("판", _list(client, layer, **params)["판"] if want != 403 else "")
res = client.post(URL.format(layer) + "/paste", json=body, params=params)
assert res.status_code == want, res.text
return res.json()
def test_마스터_조합_셋을_개인에_붙임_새_키_본뜬키_같은_이름은_2(world):
user = _as(world)
master = _list(user, "system")["줄"][:3]
keys = [r["키"] for r in master]
got = _paste(user, "personal", {"from_layer": "system", "키": keys})
assert [p["새키"] for p in got["새키"]] == ["UP42-000001", "UP42-000002", "UP42-000003"]
rows = {r["키"]: r for r in got["줄"]}
for pair, src in zip(got["새키"], master):
one = rows[pair["새키"]]
assert (one["본뜬키"], one["이름"], one["count"]) == (src["키"], src["이름"], src["count"])
assert (one["구분"], one["상세구분"]) == (None, None) # 붙인 자리 = 맨 위
again = _paste(user, "personal", {"from_layer": "system", "키": keys[:1]})
assert next(r for r in again["줄"] if r["키"] == "UP42-000004")["이름"].endswith(" (2)")
_paste(user, "company", {"from_layer": "system", "키": keys}, 403) # 회사 쓰기 권한 없음
_paste(user, "personal", {"from_layer": "personal", "from_owner": "8/9", "키": keys}, 403)
_paste(user, "personal", {"from_layer": "system", "키": ["UA999999"]}, 404)
stale = {"from_layer": "system", "키": keys, "판": "낡은판"}
assert user.post(URL.format("personal") + "/paste", json=stale).status_code == 409
def test_회사관리자_개인_폴더를_회사에_붙임_깊이_2_넘으면_422(world):
admin = _as(world, "ADMIN")
for name, where in (("가", ["석축"]), ("나", ["석축", "찰쌓기"])):
_new(admin, "personal", _combo(name, 구분=where[0], 상세구분=(where + [None])[1]))
_tree(admin, "personal", op="add-folder", 경로=["석축"], 이름="빈 곳")
body = {"from_layer": "personal", "폴더": [["석축"]]}
got = _paste(admin, "company", body)
assert [p["새키"] for p in got["새키"]] == ["UC7-000001", "UC7-000002"]
assert sorted(got["폴더"]) == [["석축"], ["석축", "빈 곳"], ["석축", "찰쌓기"]]
by = {r["이름"]: (r["구분"], r["상세구분"], r["본뜬키"]) for r in got["줄"]}
assert by["가"] == ("석축", None, "UP42-000001") and by["나"][:2] == ("석축", "찰쌓기")
_tree(admin, "company", op="add-folder", 경로=[], 이름="다른 곳")
deep = {"from_layer": "personal", "폴더": [["석축"]], "경로": ["다른 곳"]}
assert "2 단까지" in _paste(admin, "company", deep, 422)["detail"]
one = {"from_layer": "personal", "폴더": [["석축", "찰쌓기"]], "경로": ["다른 곳"]}
got = _paste(admin, "company", one) # 한 단 폴더는 들어감
assert ["다른 곳", "찰쌓기"] in got["폴더"]
def test_여러_개_옮기기_지우기_한_번에(world):
me = _as(world)
keys = [_new(me, "personal", _combo(n))["키"] for n in ("가", "나", "다")]
_tree(me, "personal", op="add-folder", 경로=[], 이름="석축")
판 = _list(me, "personal")["판"]
ops = [{"op": "move", "키": k, "새경로": ["석축"]} for k in keys[:2]]
res = me.post(URL.format("personal") + "/tree-many", json={"판": 판, "ops": ops})
assert res.status_code == 200, res.text
assert [r["구분"] for r in res.json()["줄"]] == ["석축", "석축", None]
bad = {"판": res.json()["판"], "ops": [*ops[:1], {"op": "move", "키": "UP42-999999"}]}
assert me.post(URL.format("personal") + "/tree-many", json=bad).status_code == 404
assert [r["구분"] for r in _list(me, "personal")["줄"]] == ["석축", "석축", None] # 안 씀
판 = _list(me, "personal")["판"]
ops = [{"op": "delete", "키": k} for k in keys[1:]]
got = me.post(URL.format("personal") + "/tree-many", json={"판": 판, "ops": ops}).json()
assert got["지운조합"] == keys[1:] and [r["키"] for r in got["줄"]] == keys[:1]
stale = {"판": "낡은판", "ops": ops}
assert me.post(URL.format("personal") + "/tree-many", json=stale).status_code == 409
def test_시스템_관리자_개인_조합을_마스터에_붙임_본뜬키는_UA_만(world, master_copy):
# ── 나무 길 없음(PLAN 39-2) ────────────────────────────────────────────
def test_나무_고침_붙여넣기_길은_없음(world):
"""PLAN 39-2 — 일위대가 조합 나무 길(`/tree` · `/tree-many` · `/paste`)을 지움."""
admin = _as(world, "SYSTEM_ADMIN")
up = _new(admin, "personal", _combo("개인 것"))["키"]
before = len(_list(admin, "system")["줄"])
got = _paste(admin, "system", {"from_layer": "personal", "키": [up]})
new = got["새키"][0]["새키"]
assert new.startswith("UA") and len(got["줄"]) == before + 1
row = next(r for r in got["줄"] if r["키"] == new)
assert row["본뜬키"] is None and row["이름"] == "개인 것"
doc = json.loads((master_copy / combos.FILE).read_text(encoding="utf-8"))
assert m01.cm.check_form(combos.FILE, doc) == []
for tail in ("/tree", "/tree-many", "/paste"):
res = admin.post(URL.format("personal") + tail, json={"판": ""})
assert res.status_code in (404, 405), tail
+5 -1
View File
@@ -34,7 +34,11 @@ def test_하위_차례_전체_항목_이름():
kinds = side.split("const COLUMN_KINDS")[1].split("];")[0]
order = [kinds.index(f'kind: "{k}"') for k in ("structure", "labor", "area")]
assert order == sorted(order) # 구조물 도면 → 일위대가 → 면적산출
assert 'label: () => L("M02_KindLabor"), all: true' in kinds
assert 'kind: "labor", key: "일위대가번", label: () => L("M02_KindLabor"), all: true' in kinds
# 일위대가 줄 = 열마다 「SU-… 이름」 · 누름 = onOpen({kind: "labor", name: 열}) — 같은 selectColumn 길
assert "opt.onOpen({ layer, projectId: null, kind, name: col.id" in side
api = _text("M02_MasterTemplete_Api_Fetch.ts")
assert '| "labor";' in api and "일위대가번: string;" in api
assert "onAll?: (kind: AllKind) => void;" in side # 화면은 ItemList(39-5) · 페이지가 이음
assert "if (!opt.onAll || allPicked === kind || !(await opt.confirmLeave())) return;" in side
assert 'return void selectAll("labor");' in side # 해시 open=일위대가