관리자 메뉴 진입·소유 규약 (ADMIN MENU CONVENTION)
| 항목 | 내용 |
|---|---|
| 상태 | 채택 (2026-09-20) |
| 범위 | portal-foundation 을 소비하는 모든 포털의 관리자 메뉴 진입·표시·소유 |
| 결정 | 소비 포털의 관리자 메뉴는 사이드바 roleSwitch 로 진입하고 PortalAdminShell 로 표시한다. 메뉴 소유는 사용자 메뉴 = 포털, 공통 관리자 메뉴 = 중앙(콘솔), 포털 관리자 메뉴 = 포털 로 나눈다. |
| 대상 | ProcessCenter(참조 구현) · AgentCenter · GuideCenter · DocumentLibrary · lunchapp |
| 예외 | 컨트롤 플레인(PortalConsole · UiPlatform) — 자체 관리 UI 를 소유한다 |
| 관련 | ROUTING_CONVENTION.md(경로가 화면의 원천) · MANUAL_CONVENTION.md(매뉴얼 진입) |
1. 메뉴 소유·공급 모델
| 메뉴 영역 | 소유/통제 | 공급 주체 | 렌더 |
|---|---|---|---|
| 사용자 메뉴 | 포털 | 포털 (로컬 정의 또는 포털 DB 관리 메뉴) | PortalShell.navigationItems |
관리자 메뉴 — 공통(owner: FOUNDATION) | 중앙(콘솔) 통제 | 콘솔 (pull-and-cache) | PortalAdminShell.commonAdminItems |
관리자 메뉴 — 포털(owner: PORTAL) | 포털 | 포털 (포털 특유 기능) | PortalAdminShell.portalAdminItems |
- 공통 관리자 메뉴의 예: 사용자·조직·역할, 사용자 그룹, 용어 사전, 메뉴 관리, 역할·시스템, 적용 테마 설정처럼 플랫폼 기능을 소비 포털에서 쓰기 위한 관리 화면.
- 포털 관리자 메뉴의 예: 프로세스 관리(ProcessCenter), 분류 정책·연계 동기화(AgentCenter)처럼 그 포털의 기능을 제어하는 화면.
- 두 구간은 이미 공통 계약에 인코딩되어 있다:
PortalAdminMenuItem.owner(FOUNDATION/PORTAL) ·section(COMMON/PORTAL) ·PortalAdminShell의 두 입력(commonAdminItems/portalAdminItems).
원칙: 포털 특유 기능의 관리 화면은 중앙이 강제하지 않는다. 포털이 오너십을 갖고 구성하며, 중앙은 공통(플랫폼) 관리 화면만 공급한다.
2. 진입·표시 규약
- 진입은 사이드바
roleSwitch—PortalShell.roleSwitch={{ mode, canAccessAdmin, onSwitch, userLabel, adminLabel }}.canAccessAdmin은roles.includes("ADMIN")처럼 포털의 관리자 판정을 그대로 넘긴다. - 표시는
PortalAdminShell— 관리자 메뉴는 단일 레벨 목록으로 렌더한다(그룹/2 Depth 는 업무 트리와 혼동되므로 만들지 않는다). - 되돌아가기 —
PortalAdminShell의roleSwitch(mode: "admin")로 포털 화면에 돌아온다. - URL — 관리자 화면은 경로를 갖는다(
/admin/*). 경로가 화면의 원천이라는ROUTING_CONVENTION.md를 따른다. - 직접 접근 차단 — 관리자 경로를 직접 열어도 서버가 권한을 강제한다. 화면은 보조로만 숨긴다.
- 매뉴얼 — 매뉴얼 화면은 메뉴 항목이 아니라 셸 슬롯으로 진입한다(
PortalShell/PortalAdminShell의manualLink). 자리·모양은 셸이 소유한다 —MANUAL_CONVENTION.md.
구현 예시
tsx
// 사용자 화면
<PortalShell
navigationItems={buildNavigationItems(activeView, roles)}
roleSwitch={canAccessAdmin ? {
mode: "user",
canAccessAdmin: true,
onSwitch: () => navigate(ADMIN_HOME_PATH),
userLabel: "포털",
adminLabel: "관리자",
} : undefined}
>
{routes}
</PortalShell>
// 관리자 화면
<PortalAdminShell
commonAdminItems={adminMenu.commonAdminItems} // owner: FOUNDATION
portalAdminItems={adminMenu.portalAdminItems} // owner: PORTAL
activeAdminMenuId={activeView ?? undefined}
adminRole="ADMIN"
onReturnToPortal={() => navigate(HOME_PATH)}
>
{adminRoutes}
</PortalAdminShell>3. 게이트 규칙
| 지점 | 규칙 |
|---|---|
| 사용자 메뉴 항목 | 항목별 roles 로 거른다. 미지정이면 모든 인증 사용자, ADMIN 은 항상 통과. 자식이 모두 숨겨진 그룹은 렌더하지 않는다 |
| 관리자 영역 진입 | 관리자 판정(canAccessAdmin)이 참일 때만 진입 경로를 노출한다 |
| 관리자 메뉴 항목 | PortalAdminShell 의 requiredRole === adminRole 로 거른다 |
| 중앙 관리 메뉴(관리형 모델) | PortalMenuItem.visibility: "ADMIN" + accessRule(roles/scopes/groupIds) 로 buildPortalNavigationItems(items, { subject }) 가 거른다 |
| 서버 | 화면 노출과 무관하게 서버가 최종 강제한다(버튼 숨김으로 권한을 대체하지 않는다) |
4. 메뉴 소스의 3단계와 이전 경로
| 단계 | 정의 | 현재 예 |
|---|---|---|
| L1 로컬 정의 | 포털 클라이언트 코드의 배열 | 관리자 메뉴(모든 포털), AgentCenter 사용자 메뉴 |
| L2 포털 DB 관리 | 포털 자기 DB + 시드·검증(menuManagement 계약) | ProcessCenter 사용자 메뉴(portalMenuRouter) |
| L3 콘솔 중앙 관리 | 콘솔이 공통 관리자 메뉴를 배포(GET /api/directory/v1/menu) | AgentCenter 공통 관리자 메뉴(공급 경로 구현 · 저장·저작은 후속) |
이전 경로: L1 → L2 → L3. 어느 단계에서도 렌더러는 동일하다(PortalShell / PortalAdminShell). 공급처(로컬 배열 → 포털 DB → 콘솔 캐시)만 바뀌므로 이전 비용이 작다.
- L1 → L2: 메뉴 정의를 포털 DB 로 옮기고 시드·검증을 붙인다.
- L2 → L3: 공통(FOUNDATION) 관리자 항목만 콘솔 캐시에서 채우고, 포털(PORTAL) 항목은 그대로 포털에 둔다.
- L3 에서도 콘솔 장애 시 마지막 정상 캐시/로컬 시드로 사용자·관리자 메뉴를 유지한다(빈 화면 금지).
- L3 소비 계약: 콘솔
docs/portal-admin-menu-supply.md(요청·응답·인증). 이식 시 서버에 공급원(조회·캐시·폴백)을 두고 클라이언트에는 결과만 전달한다 — API 키를 브라우저로 내려보내지 않는다.
L3 소비 구현 예 (AgentCenter)
| 요소 | 위치 |
|---|---|
| 계약·폴백 시드 | shared/adminMenu.ts (ConsoleAdminMenuItem, seedCommonAdminItems) |
| 공급원(조회·캐시·폴백) | server/adapters/identity/consoleAdminMenu.ts |
| 클라이언트 노출 | trpc.system.adminMenu (인증 필요) |
| 셸 반영 | buildAdminMenu(consoleItems 또는 null) → PortalAdminShell.commonAdminItems |
null(아직 못 받음·콘솔 미설정)에서만 로컬 시드를 쓰고, 중앙이 빈 배열을 주면 그대로 반영한다(공급 주체가 중앙이므로 포털이 항목을 되살리지 않는다).
5. 소비 포털 이식 체크리스트
- 사용자 메뉴 항목에
roles를 지정했는가(미지정 = 전체 인증 사용자). - 관리자 메뉴를
owner: FOUNDATION/owner: PORTAL로 나눴는가. - 관리자 진입을
roleSwitch로 노출하고canAccessAdmin을 서버 판정과 일치시켰는가. - 관리자 화면 경로를 라우팅 표에 등록했는가(직접 접근 시 서버 권한 검사 포함).
PortalAdminShell에 두 구간을 모두 넘겼는가(공통이 비어도 빈 배열로 명시).- 매뉴얼 진입을 셸 슬롯(
manualLink)으로 넘기고 메뉴 정의에서는 뺐는가 —MANUAL_CONVENTION.md.
6. 검증
- 소비 포털 테스트에서 역할별 메뉴 노출(VIEWER/OWNER/검토자/ADMIN)과 관리자 셸 common/portal 구간을 화면 없이 검증한다.
- L3 를 쓰는 포털은 공급 경로도 검증한다 — 중앙 응답 채택·TTL·장애 시 캐시/시드 폴백·
portalId불일치 거부·셸 부적합 항목 필터(예: AgentCentertests/unit/consoleAdminMenu.test.ts). - 배포 후 실제 화면에서 관리자 판정별 사이드바·관리자 셸을 확인한다.