"""일위대가 사전조합 층 따로(PLAN 134-3) — 페이지 층과 따로 · 명시 owner · 자유 폴더 나무(158-1). - 서버 길: 명시 owner 를 넘기면 그 주인만(null = 자기 층) — 남의 개인을 보는 중에도 사전조합 길에 안 섞임 · 안 넘기면 지금처럼 페이지 층 주인(Node · fetch 대역) - 나무: 자유 폴더(158-1) — 적어 둔 폴더 차례 · 자리 없는 문서 = 뿌리 · 문서 수 = 아래 폴더까지 - 사전조합 층 단추 = 해시를 바꿔 페이지를 다시 세우지 않음(replaceState · 그 컨테이너만 다시 그림) · 구조물 조합 층 단추는 그대로 해시 이동 · 주소 `pre` 에 남음 · 고름에 owner · canWrite """ import json import os import subprocess from pathlib import Path HERE = os.path.dirname(os.path.abspath(__file__)) M02 = Path(HERE).parents[1] / "M02_MasterTemplete" def _text(name: str) -> str: return (M02 / name).read_text(encoding="utf-8") def _run(rows, view=None): proc = subprocess.run( ["node", os.path.join(HERE, "helper_m02_precombo_scope.cjs")], input=json.dumps({"rows": rows, "view": view}, ensure_ascii=False), capture_output=True, text=True, encoding="utf-8", check=True, ) return json.loads(proc.stdout) def test_명시_owner_길은_페이지_층_주인과_안_섞임(): urls = _run([])["urls"] assert urls[0] == "/api/m02/layers/personal/templates/labor/x1?owner=7%2F99" assert urls[1] == "/api/m02/layers/personal/templates/precombo/SC-000001" assert urls[2] == "/api/m02/layers/company/templates?owner=8" assert urls[3] == "/api/m02/layers/system/templates/precombo/SC-000002" def test_나무는_자유_폴더_자리_없는_문서는_뿌리(): rows = [ {"이름": "SC-000001", "종류": "precombo", "구분": "C 사면안정", "상세구분": "옹벽"}, {"이름": "SC-000002", "종류": "precombo"}, {"이름": "SC-000003", "종류": "precombo"}, ] view = { "폴더": [["옹벽"], ["옹벽", "찰쌓기"], ["빈 폴더"]], "자리": {"SC-000001": ["옹벽", "찰쌓기"], "SC-000009": ["옹벽"]}, # 없는 문서 자리 = 무시 } tree = _run(rows, view)["tree"] assert tree["docs"] == ["SC-000002", "SC-000003"] and tree["count"] == 3 # 구분과 무관 wall, empty = tree["folders"] assert (wall["path"], wall["count"], wall["docs"]) == (["옹벽"], 1, []) assert wall["folders"][0] == { "path": ["옹벽", "찰쌓기"], "count": 1, "docs": ["SC-000001"], "folders": [], } assert (empty["path"], empty["count"]) == (["빈 폴더"], 0) # 폴더를 못 받으면(빈 자리) 모두 뿌리 · 폴더 없음 flat = _run(rows)["tree"] assert flat["docs"] == ["SC-000001", "SC-000002", "SC-000003"] and flat["folders"] == [] def test_사전조합_층_단추는_페이지를_다시_안_세움(): page = _text("M02_MasterTemplete_UI_Page.ts") pre = page.split("const setPre = (want: Root): void => {")[1].split("};")[0] assert "history.replaceState(" in pre and "side.refreshPrecombo()" in pre assert "navigateTo" not in pre and "setActiveLayer" not in pre # 페이지 층 · 해시 이동 없음 assert "entry.pre = pre.root;" in pre # 구조물 조합 층 이동(go) 때도 주소에 남음 bar = page.split("const preBar = (): HTMLElement =>")[1].split("const side = buildSide(")[0] assert "go: (next) => setPre(next)," in bar and "main.open(precomboList(pre))" in bar # 구조물 조합 층 단추는 그대로 해시 이동 한 벌 assert page.count('layerBar("') == 1 assert 'layerBar("구조물조합", tree.add, () => void main.openTable(layer)),' in page assert "precombo: { scope: () => pre, bar: preBar }," in page def test_고름에_owner_권한_서고_길에_명시_owner(): side = _text("M02_MasterTemplete_UI_Side_Precombo.ts") assert 'export const PRECOMBO_TITLE = "일위대가 사전조합";' in side assert "owner: scope.owner," in side and "canWrite: scope.canWrite," in side assert "listTemplates(at.layer, null, at.owner)" in side main = _text("M02_MasterTemplete_UI_Main.ts") assert ( "const writable = (at: Selection): boolean => at.canWrite ?? canEdit(at.layer, isAdmin);" in main ) assert "readTemplate(next.layer, next.kind, next.name, next.projectId, next.owner)" in main assert "at.owner,\n );" in main # saveTemplate assert "owner: next.owner ?? null," in main and "mode, owner: at.owner ?? null" in main lib = _text("M02_MasterTemplete_Library_List.ts") assert "getActiveOwner" not in lib and lib.count("opt.owner") >= 6 detail = _text("M02_MasterTemplete_Labor_Detail.ts") assert "opt.owner !== undefined" in detail def test_분류_고정_폴더_지움_자유_폴더_메뉴_끌어_옮기기(): side = _text("M02_MasterTemplete_UI_Side_Precombo.ts") for gone in ("groupRows", "fetchStructureDef", "Lib_Unsorted", "shape"): assert gone not in side, gone assert "fetchPrecomboTree(at.layer, at.owner)" in side assert "editPrecomboTree(at.layer, body, view.판, at.owner)" in side menu = side.split("const menuAt = (")[1].split("\n };")[0] for label in ('L("M02_TreeAddFolder")', 'tc("Lib_NewHere")', 'L("M02_TreeRename")'): assert label in menu, label assert 'L("M02_Delete"), () => void remove(path, count)' in menu assert 'opt.scope().canWrite ? undefined : tc("ReadOnly")' in menu # 읽기만 층 = 흐림 remove = side.split("const remove = async (")[1].split("\n };")[0] assert 'if (count) return void showToast(tc("Lib_FolderFull")' in remove # 빈 폴더만 make = side.split("const make = (path: string[]): void => {")[1].split("\n };")[0] assert 'const 문서 = { 이름: name, 구분: "", 상세구분: "", 단위: null, 담은로직: [] };' in make assert 'if (path.length) await edit({ op: "move", 키: got.이름, 새경로: path });' in make assert "attachTreeDrag({" in side and "if (canWrite) drag.arm(row, node.id);" in side assert "openTreeMenu(event, menuAt([], 0));" in side # 빈 자리 = 뿌리 메뉴 assert "open: toggled.get(id) ?? count > 0," in side lib = _text("M02_MasterTemplete_Library_List.ts") assert "export const nameDialog = (" in lib def test_목록표_빈_구분_미분류_고르기_창_새_분류_거름(): """149-2 — 목록표 빈 구분 = 미분류 · 고르기 창 거름 = 품셈 구분 + 사전조합 새 분류""" lib = _text("M02_MasterTemplete_Library_List.ts") assert 'row.구분 || tc("Lib_Unsorted"),' in lib pick = _text("M02_MasterTemplete_Labor_Pick.ts") assert "new Map([...subs, ...classes].map((s) => [s.name, new Set(s.details)]))" in pick assert "classes = precomboClasses(def);" in pick assert 'x.row.구분 || tc("Lib_Unsorted")' in pick # 서고 줄 작은 글 = 새 분류 · 빈 값 미분류 # 거름은 칸 값 그대로 — 품셈 구분을 고르면 서고 줄 빠지고 새 분류를 고르면 로직 줄 빠짐 assert "(!sub.select.value || x.row.구분 === sub.select.value)" in pick assert "(!sub.select.value || x.구분 === sub.select.value)" in pick