diff --git a/M01_MasterData/M01_MasterData_UI_Side.ts b/M01_MasterData/M01_MasterData_UI_Side.ts index 1c7e90b0e..5dc8f06a7 100644 --- a/M01_MasterData/M01_MasterData_UI_Side.ts +++ b/M01_MasterData/M01_MasterData_UI_Side.ts @@ -8,6 +8,7 @@ import { createSelectField, el, showToast } from "@ui/ui_template_elements"; import { attachCollapsible } from "@ui/ui_template_collapsible"; +import { createDivider } from "@ui/ui_template_divider"; import { currentLanguageIndex, t as L } from "@ui/ui_template_locale"; import { buildOptionsSection, @@ -501,7 +502,7 @@ ${first.file}` const optionsSec = section(options.title, options.body); const root = el("div", { className: "m01-side ui-sidebar-panel", - children: [...groups, logic, el("hr", { className: "ui-sidebar-divider" }), optionsSec], + children: [...groups, logic, createDivider(), optionsSec], }); attachCollapsible(root); void Promise.all(GROUPS.filter((g) => g !== FUEL_GROUP).map(refresh)).catch((error) => diff --git a/resources/tester/test_197_1_dashboard_option_card.py b/resources/tester/test_197_1_dashboard_option_card.py index 9bb89ea86..011bbbe1c 100644 --- a/resources/tester/test_197_1_dashboard_option_card.py +++ b/resources/tester/test_197_1_dashboard_option_card.py @@ -1,10 +1,14 @@ -"""197-1 대시보드 「구조물 옵션」 카드 → M02 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 사용자. +"""197-1 · 716-1 대시보드 「구조물 옵션 정의」 줄 → M01 「구조물 옵션 정의」 화면이 바로 열림 시험 — 조작은 ORCA. + +716-1 — 화면 입구가 M02 에서 M01 로 옮겨감(화면 코드 · 서버 길은 M02 폴더 그대로) · 옛 M02 `open=옵션` 주소는 +시스템 관리자면 M01 `?open=옵션` 으로 돌려보냄. 원인 — 진입 해시 `open=옵션` 이 옵션 줄을 눌러 정의 화면을 띄우는 사이 첫 화면(집계표)의 `mount` 가 모듈 · 번호를 기다리다 뒤늦게 끝나 정의 화면 위에 집계표를 덮음(`mount` 에 낡음 검사 없었음). -· 카드 셋 → 해시(open=구조물조합 · 옵션 · 도면) · 해시 → 진입(`entryFromHash`) Node 로 바로 돌림 -· `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함 +· 줄 → 해시(M01 open=로직 · 옵션 / M02 open=일위대가조합 · 구조물조합 · 도면) · 해시 → 진입(`entryFromHash`) Node +· M02 `reveal` 가 옵션만 줄을 누르고 도면은 펼치기만 · 구조물조합은 아무것도 안 함 · M01 `reveal` 도 같은 꼴 +· 옛 `open=옵션` 돌려보냄(`redirectOldOptions`) · `mount` 집계표 갈래(진짜 소스 조각)를 Node 로 돌림 — 기다리는 사이 다른 화면이 열리면 그리지 않음 """ @@ -41,16 +45,19 @@ def _run(script: str, *args: str) -> object: @needs_node -def test_카드_셋_해시(): +def test_줄_해시_옵션은_M01(): from test_b01_settings_cards import _cards cards = _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) - assert [c[2].split("?")[1].split("&")[0] for c in cards[1:4]] == [ - "open=구조물조합", - "open=옵션", - "open=도면", + heads = [c[2].split("?")[0] + "?" + c[2].split("?")[1].split("&")[0] for c in cards[1:6]] + assert heads == [ + "m01-master-data?open=로직", + "m01-master-data?open=옵션", + "m02-master-template?open=일위대가조합", + "m02-master-template?open=구조물조합", + "m02-master-template?open=도면", ] - assert all("&root=master" in c[2] for c in cards[1:4]) + assert all("&root=master" in c[2] for c in cards[3:6]) @needs_node @@ -69,7 +76,7 @@ def test_해시_진입_셋(): + fn.group(0).replace("export function", "function") + "\nconsole.log(JSON.stringify(entryFromHash(null)));" ) - for want in ("구조물조합", "옵션", "도면"): + for want in ("구조물조합", "옵션", "도면", "일위대가조합"): got = _run(script, f"open={want}&root=master&company=3") assert got["open"] == want and got["root"] == "master" and got["company"] == "3" @@ -139,3 +146,38 @@ def test_집계표_갈래_기다리는_사이_다른_화면이_열리면_그리 assert got["none"] == ["create", "panel:set"] # 안 끼어들면 그대로 그림 assert got["mod"] == ["panel:null"] # 모듈 기다리는 사이 다른 화면 — 그리지 않음 assert got["num"] == ["panel.destroy", "panel:null"] # 번호 기다리는 사이 — 패널도 버림 + + +@needs_node +def test_옛_옵션_주소_M01로_돌려보냄(): + layer = _text(M02 / "M02_MasterTemplete_Layer.ts") + fn = re.search(r"^export function redirectOldOptions\([\s\S]*?^}", layer, re.M) + assert fn, "redirectOldOptions 를 못 찾음" + script = ( + "const currentQuery = () => new URLSearchParams(process.argv[1]);\n" + "const ROUTES = { M01_MASTER_DATA: 'm01' }; const sent: string[] = [];\n" + "const navigateTo = (r: string, q: any) => sent.push(r + JSON.stringify(q));\n" + "type SessionUser = any;\n" + + fn.group(0).replace("export function", "function") + + "\nconst a = redirectOldOptions({ role: 'SYSTEM_ADMIN' });" + + "\nconst b = redirectOldOptions({ role: 'USER' });" + + "\nconst c = redirectOldOptions(null);" + + "\nconsole.log(JSON.stringify([a, b, c, sent]));" + ) + assert _run(script, "open=옵션&root=master") == [True, False, False, ['m01{"open":"옵션"}']] + # 옵션이 아니면 안 돌려보냄 + assert _run(script, "open=도면&root=master") == [False, False, False, []] + page = _text(M02 / "M02_MasterTemplete_UI_Page.ts") + assert "if (redirectOldOptions(user))" in page + assert 'mountSettingsDrawer(user ?? {}, "m02", entry.company);' in page + + +def test_M01_open_인자_로직_옵션(): + m01 = ROOT / "M01_MasterData" + page = _text(m01 / "M01_MasterData_UI_Page.ts") + assert 'if (want === "로직" || want === "옵션") side.reveal(want);' in page + assert "setLeaveGuard(leaveOptions)" in page # 저장 안 한 정의 고침 — 떠날 때 물음 + side = _text(m01 / "M01_MasterData_UI_Side.ts") + assert "buildOptionsSection({ ...optionsOpt, isAdmin: true })" in side + # 로직 칸 아래 구분선 부품 + 옵션 칸 + assert "logic, createDivider(), optionsSec" in side diff --git a/resources/tester/test_309_2_calc_sites.py b/resources/tester/test_309_2_calc_sites.py index 4a366502d..df6a56949 100644 --- a/resources/tester/test_309_2_calc_sites.py +++ b/resources/tester/test_309_2_calc_sites.py @@ -263,7 +263,7 @@ def test_카드_시스템_관리자만_글_다_있음(): links = nav.split("export function mountSettingsDrawer")[0] admin, rest = links.split('if (user.role === "SYSTEM_ADMIN") {')[1].split("// 회사 수정 =") assert 'key: "calcSites"' in admin and "calcSites" not in rest - assert 'settingLinks(user, companyId).filter((link) => link.key !== "calcSites")' in nav + assert "calcSites" not in nav.split("export function pageLinks")[1] # ☰ 는 페이지 둘만(716-2) page = _text("B01_Dashboard/B01_Dashboard_UI_View_SysDefaults.ts") assert 'link.key === "calcSites" ? void openCalcSitesDialog()' in page locale = _text("ui_template/ui_template_locale_m1.ts") diff --git a/resources/tester/test_b01_settings_cards.py b/resources/tester/test_b01_settings_cards.py index 8220ed667..a32175595 100644 --- a/resources/tester/test_b01_settings_cards.py +++ b/resources/tester/test_b01_settings_cards.py @@ -58,7 +58,36 @@ def _cards(user: dict, company_id: int | None = None) -> list: return json.loads(result.stdout) +def _pages(user: dict, company_id: int | None = None) -> list: + # ☰ 메뉴 = pageLinks(settingLinks 의 구조물 조합 줄을 바탕으로 페이지 둘만) + script = ( + "const ROUTES = { M01_MASTER_DATA: 'm01-master-data'," + " M02_MASTER_TEMPLATE: 'm02-master-template', B01_ACCOUNT: 'b01-account' };\n" + "const L = (k: string) => k;\n" + "let went = '';\n" + "const navigateTo = (r: string, q?: Record) =>" + " { went = r + (q ? '?' + new URLSearchParams(q) : ''); };\n" + + _fn(_text(NAV), "settingLinks") + + "\n" + + _fn(_text(NAV), "pageLinks") + + "\nconst user = JSON.parse(process.argv[1]);" + + "\nconst links = pageLinks(user, JSON.parse(process.argv[2]));" + + "\nconsole.log(JSON.stringify(links.map((l: any) => {" + + " navigateTo(l.route, l.query); return [l.key, l.title, decodeURIComponent(went)]; })));" + ) + result = subprocess.run( + ["node", "--experimental-strip-types", "--no-warnings", + "--input-type=module-typescript", "-e", script, json.dumps(user), json.dumps(company_id)], + capture_output=True, text=True, encoding="utf-8", timeout=60, + ) # fmt: skip + assert result.returncode == 0, result.stderr + return json.loads(result.stdout) + + +M01 = "m01-master-data" M02 = "m02-master-template" +READ_SRC = "B01_Dashboard_PermSourceRead" +WRITE_M = "B01_Dashboard_PermMasterWrite" # 계산 자리(504-2) — 시스템 관리자만 · 이동 대신 창 CALC_SITES = ["B01_CalcSites_Title", "B01_CalcSites_Desc", "calc-sites-dialog"] READ_CO = ( @@ -68,21 +97,40 @@ WRITE_CO = READ_CO.replace("CompanyRead", "CompanyWrite") @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") -def test_시스템_관리자_카드_다섯_고른_회사_해시(): - m = "B01_Dashboard_PermMasterWrite" +def test_시스템_관리자_줄_칸별_일곱_고른_회사_해시(): + # 716-2 — 대시보드 설계 기본값 = 칸별 줄: M01 3(마스터 요소 · 단가산출 로직 · 구조물 옵션 정의) + + # M02 3(일위대가 · 구조물 조합 · 도면 양식) + 계산 자리 assert _cards({"role": "SYSTEM_ADMIN", "is_master": False}, 3) == [ - ["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"], - ["B01_Dashboard_CardStructCombo", m, f"{M02}?open=구조물조합&root=master&company=3"], - ["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"], - ["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"], + ["M01_Title", READ_SRC, M01], + ["B01_Dashboard_CardLogic", READ_SRC, f"{M01}?open=로직"], + ["B01_Dashboard_CardStructOptionDef", WRITE_M, f"{M01}?open=옵션"], + ["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=master&company=3"], + ["B01_Dashboard_CardStructCombo", WRITE_M, f"{M02}?open=구조물조합&root=master&company=3"], + ["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"], CALC_SITES, ] # 회사가 없으면 해시에 안 실음 - assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[1][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False})[4][2] == ( f"{M02}?open=구조물조합&root=master" ) +@pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") +def test_메뉴_페이지_둘만_역할별(): + # 716-2 — ☰ = M01 · M02 두 페이지(시스템 관리자) / M02 한 페이지(그 밖) · 열쇠 m01 · m02 + admin = _pages({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) + assert admin == [ + ["m01", "B01_Dashboard_MasterElements", M01], + ["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal&company=3"], + ] + assert _pages({"role": "ADMIN", "is_master": False, "company_id": None}) == [ + ["m02", "M02_Title", f"{M02}?open=구조물조합&root=company"], + ] + assert _pages({"role": "USER", "is_master": False, "company_id": 1}) == [ + ["m02", "M02_Title", f"{M02}?open=구조물조합&root=personal"], + ] + + @pytest.mark.skipif(shutil.which("node") is None, reason="node 가 없음") def test_회사_관리자_개인_카드_하나(): company = [ @@ -191,14 +239,12 @@ def test_회사_찾기_거르기(): def test_구조물_조합_기본_층_개인(): """123-1 — 회사 있는 사람은 역할 무관 구조물 조합 = 개인 · 회사 없으면 지금 규칙 · 다른 곳 그대로 · 134-4 — 층 없는 주소도 개인(회사 없으면 마스터) · 사전조합 기본 층도 같은 규칙.""" - m = "B01_Dashboard_PermMasterWrite" combo = f"{M02}?open=구조물조합&root=personal" - assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) == [ - ["B01_Dashboard_MasterElements", "B01_Dashboard_PermSourceRead", "m01-master-data"], - ["B01_Dashboard_CardStructCombo", m, f"{combo}&company=3"], - ["B01_Dashboard_CardStructOption", m, f"{M02}?open=옵션&root=master&company=3"], - ["B01_Dashboard_CardDrawing", m, f"{M02}?open=도면&root=master&company=3"], - CALC_SITES, + rows = _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": 1}, 3) + assert rows[3:6] == [ + ["B01_Dashboard_CardPrecombo", WRITE_M, f"{M02}?open=일위대가조합&root=personal&company=3"], + ["B01_Dashboard_CardStructCombo", WRITE_M, f"{combo}&company=3"], + ["B01_Dashboard_CardDrawing", WRITE_M, f"{M02}?open=도면&root=master&company=3"], ] assert _cards({"role": "ADMIN", "is_master": False, "company_id": 1}) == [ ["B01_Dashboard_CardStructCombo", WRITE_CO, combo], @@ -206,7 +252,7 @@ def test_구조물_조합_기본_층_개인(): assert _cards({"role": "USER", "is_master": True, "company_id": 1})[0][2] == combo assert _cards({"role": "USER", "is_master": False, "company_id": 1})[0][2] == combo # 개인 층 없음(회사 없음) = 지금 규칙 - assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[1][2] == ( + assert _cards({"role": "SYSTEM_ADMIN", "is_master": False, "company_id": None})[4][2] == ( f"{M02}?open=구조물조합&root=master" ) assert _cards({"role": "ADMIN", "is_master": False, "company_id": None})[0][2] == (