메뉴 거버넌스 설계 — 중앙 공급 공통 관리자 메뉴 (L3)
| 항목 | 내용 |
|---|---|
| 상태 | 공급 경로 구현(2026-09-20) — 저장·저작(L3b)은 §7-2·§7-6 확정 후. 구현·검증은 아래 문서 참조 |
| 범위 | PortalConsole 이 공통(FOUNDATION) 관리자 메뉴를 저장·저작·배포하고, 소비 포털이 이를 소비해 PortalAdminShell 로 표시하는 경로 |
| 전제 | ADMIN_MENU_CONVENTION.md — 소유·진입 규약. 사용자 메뉴·포털(PORTAL) 관리자 메뉴는 이 설계의 대상이 아니다 |
| 구현(현재) | 콘솔 server/portalAdminMenu.ts · server/portalAdminMenuRoute.ts · docs/portal-admin-menu-supply.md / AgentCenter server/adapters/identity/consoleAdminMenu.ts · shared/adminMenu.ts |
| 관련 | MODULAR_PORTAL_INSTALLATION.md(bundle · pull-and-cache), ROUTING_CONVENTION.md(경로가 화면의 원천) |
1. 현재 상태 (사실)
| 구분 | 현재 구현 |
|---|---|
| 사용자 메뉴 | 포털 자기 DB — ProcessCenter server/portalMenuRouter.ts + portal_menu_items 계열 테이블, trpc.portalMenu.navigation 으로 제공. 공통 모델(menuManagement)의 visibility(PUBLIC/AUTHENTICATED/ADMIN)·accessRule·시드·검증을 사용 |
| 관리자 메뉴 | 공통 구간은 중앙 공급, 포털 구간은 포털 로컬(owner 로 구분) — PortalAdminMenuItem[] |
| PortalConsole | 메뉴 공급 경로 있음(GET /api/directory/v1/menu) — 항목 정의는 내장 시드 + PORTAL_ADMIN_MENU_JSON(저장소 없음) |
| 포털 소비 | AgentCenter 가 중앙 응답을 인메모리 캐시(TTL 기본 15분)로 보관하고 trpc.system.adminMenu 로 클라이언트에 전달. 미연결·장애면 로컬 시드 폴백 |
즉 공급 경로(L3)는 열렸고, 남은 것은 저장·저작 계층(L3b)이다.
2. 목표
- 플랫폼 관리자가 한 곳에서 공통 관리자 메뉴(사용자·조직·역할, 사용자 그룹, 용어 사전, 메뉴 관리, 테마 정책 등)를 저작·배포한다.
- 소비 포털은 그 결과를 공통 관리자 구간(
commonAdminItems)으로 표시하고, 포털 자체 관리자 구간(portalAdminItems)은 계속 포털이 소유한다. - 콘솔이 없거나 응답하지 않아도 포털 관리자 화면은 마지막 정상 상태로 동작한다.
3. 콘솔 책임
3.1 저장 (L3b — 미구현)
현재 구현: 항목 정의는 콘솔 내장 시드 +
PORTAL_ADMIN_MENU_JSON환경변수다(server/portalAdminMenu.ts). 저장소(테이블)를 붙이기 전에 스키마를 실행 검증할 수 있는 환경(마이그레이션·운영 DB)이 필요하다 — 검증할 수 없는 마이그레이션을 먼저 넣으면 "스키마는 준비되어 있다"는 기록만 남고 실제 반영은 깨진다(D-006 계열 교훈). 아래는 저장소를 붙일 때의 스키마 초안이다.
portal_menu_items (PortalConsole)
| 열 | 설명 |
|---|---|
portalId | 대상 포털(예: agent-center). * 는 전체 공통 |
구현 시에는 포털별 행을 두는 편이 조회·감사에 단순하다(공통 항목도 포털별로 시드).
| 열 | 설명 |
|---|---|
key | 항목 키(포털 내 유일) |
label · labelKo · labelEn | 표시명(로케일) |
type | LINK · GROUP · EXTERNAL_LINK · DIVIDER |
href · openInNewWindow | 경로(경로 원천 규약을 따른다) |
iconKey | 공통 아이콘 카탈로그 키 |
visibility | PUBLIC · AUTHENTICATED · ADMIN |
accessRule | { mode, scopes[], roles[], groupIds[] } |
sortOrder · enabled · systemReserved | 정렬·사용·시스템 보호(삭제 금지) |
owner | 이 설계에서는 항상 FOUNDATION |
visibleFrom · visibleUntil | 예약 노출(선택) |
3.2 저작 (L3b — 미구현)
- portal-foundation 의
PortalMenuManagement·validatePortalMenu·DEFAULT_PORTAL_MENU_SEED를 재사용한다(포털 L2 와 같은 모델·검증). - 변경은 감사 이벤트로 남긴다(누가·언제·무엇을).
systemReserved항목은 삭제할 수 없다(경로 원천 규약 위반 방지).
3.3 배포 (pull-and-cache) — 구현 완료
UiPlatform profile 과 같은 방식으로 배포한다 — MODULAR_PORTAL_INSTALLATION.md 의 pull-and-cache 계약.
http
GET {PORTAL_CONSOLE_ORIGIN}/api/directory/v1/menu?portalId=agent-center
Authorization: Bearer <포털 API 키>json
{
"data": { "portalId": "agent-center", "version": "a1b2c3d4e5f6",
"items": [ /* owner: FOUNDATION, visibility: ADMIN 인 공통 관리자 메뉴 */ ] },
"meta": { "requestId": "...", "generatedAt": "..." }
}- 인증·권한은 기존 포털 API 키 계약을 따른다 —
directory:read재사용으로 확정했다(§7-1). scope 를 신설하면 기존 포털 키를 재발급해야 하는데, 얻는 이득이 없다. - 포털은 기동 후 첫 조회에서 pull 하고, 이후 TTL(기본 15분)이 지난 조회에서만 다시 받는다.
version은 내용 해시다(저장소가 없어updatedAt이 정본이 될 수 없다). 포털은 캐시 신선도를 이 값으로 판단한다 — 항목이 바뀌면 버전이 바뀐다.- 계약 전문:
PortalConsole/docs/portal-admin-menu-supply.md.
4. 포털 소비
- 포털은 콘솔 응답을 캐시한다(마지막 정상 응답 + version). 현재는 인메모리이며 DB·파일 저장은 후속이다(§7-2).
- 사용자 메뉴는 기존 경로(L1/L2) 그대로 두고, 공통 관리자 항목만 콘솔 캐시에서 채운다.
buildPortalNavigationItems(items, { subject })로 필터한 결과를PortalAdminShell.commonAdminItems로 넘기고, 포털 항목은portalAdminItems로 넘긴다.- 폴백 순서: 콘솔 최신 응답 → 마지막 정상 캐시 → 포털 로컬 시드(코드 기본값). 어느 단계에서도 빈 관리자 메뉴를 만들지 않는다.
- 콘솔 장애·미연결은 포털 기능 장애가 아니다(
MODULAR_PORTAL_INSTALLATION.md원칙) — 관리자 화면만 캐시 기준으로 동작한다. - 중앙이 빈 목록을 주면 그대로 반영한다 — 공급 주체는 중앙이므로 포털이 항목을 되살리지 않는다. 로컬 시드는 콘솔에 닿지 못할 때의 안전판이다(응답 자체를 받은 경우와 구분한다).
5. 게이트·경계
| 항목 | 규칙 |
|---|---|
| 공통 관리자 항목 | visibility: "ADMIN" + accessRule — 서버는 subject(isAdmin·roles·scopes·groupIds)로 거른다 |
| 포털(PORTAL) 관리자 항목 | 콘솔이 건드리지 않는다(포털 소유) |
| 경로 | ROUTING_CONVENTION.md 를 따른다 — 경로는 메뉴 정의가 단일 원천 |
| 화면 | 노출은 보조 수단, 최종 권한은 서버가 강제한다 |
6. 이전 경로 (L2 → L3)
- 지금: 관리자 메뉴는 L1(포털 로컬), 공통 항목은
owner: FOUNDATION으로 태깅만 해 둔다(포털 렌더는 이미 두 구간으로 분리). ✅ 완료 - 콘솔 배포 경로 준비: §3 배포 구현 +
agent-center시드. ✅ 완료(저장·저작은 L3b) - 포털 전환:
commonAdminItems의 공급처를 로컬 시드에서 콘솔 캐시로 교체(렌더러·타입 불변). ✅ AgentCenter 완료 - 정리: 공통 항목의 포털 로컬 사본을 "폴백 시드"로만 남긴다. ✅ (
shared/adminMenu.ts의seedCommonAdminItems만 사용) - 남은 소비 포털: GuideCenter · DocumentLibrary · lunchapp — 같은 전환(공급처 교체)만 하면 된다. 콘솔 시드에 포털별 항목을 추가해야 그 포털의 공통 구간이 채워진다(시드가 없으면 빈 목록 → 로컬 시드 폴백).
7. 미결 (착수 전 확정)
| 번호 | 결정 | 상태 |
|---|---|---|
| 1 | scope: directory:read 재사용. 신설(menu:read)은 기존 포털 키 재발급을 강제하는데 얻는 이득이 없다 | ✅ 확정 |
| 2 | 저장 위치: 현재는 포털 인메모리 캐시(+ TTL). DB·파일 영속화는 저장 계층(L3b)과 함께 결정한다 — 지금 영속화해도 콘솔이 항목을 저장하지 않으므로 다시 받아오는 값이 같다 | 🟡 부분 확정(영속화 후속) |
| 3 | 갱신 주기·무효화: TTL 기반 pull(기본 15분, PORTAL_CONSOLE_MENU_TTL_MS) + invalidate(). 관리자 수동 새로고침 UI 는 화면 단계에서 붙인다 | ✅ 확정 |
| 4 | 중앙 role claim: 공통 관리자 메뉴 게이트에 필요한 역할 claim 이 아직 확정 전이다(§17 브리지 DIRECTORY_ROLE_ASSIGNMENTS) — 중앙 항목은 visibility: ADMIN 만 오므로 현재는 포털 관리자 게이트(ADMIN 역할)로 충분하다 | 🟡 미확정(현재 요구 없음) |
| 5 | 공통 관리자 메뉴의 범위: 어떤 항목이 실제로 공통인지(사용자·조직·역할 외 후보) 미확정 — 항목을 늘릴 때마다 콘솔 시드(PORTAL_ADMIN_MENU_JSON)에 포털별 경로를 등록해야 한다 | 🟡 미확정 |
| 6 | 감사·보존: 메뉴 변경 감사는 저장 계층(L3b)과 함께 결정한다(사내 보존 정책 확정 전 임의 값 금지) | 🟡 미확정 |
8. 검증 (구현된 테스트)
| 대상 | 검증 | 위치 |
|---|---|---|
| 콘솔 정의 | 항목 정규화·정렬·버전 해시·환경 override·JSON 파손 폴백·경로 없는 LINK 제거 | PortalConsole/server/portalAdminMenu.test.ts |
| 콘솔 경로 | 인증(401)·scope(403)·portalId 불일치(403)·중앙 시드가 없는 포털의 빈 목록 | PortalConsole/server/portalAdminMenuRoute.test.ts |
| 포털 소비 | 중앙 응답 채택·TTL·동시 조회 1회 호출·장애 시 캐시/시드 폴백·portalId 불일치 거부·셸 부적합 항목 필터·invalidate | AgentCenter/tests/unit/consoleAdminMenu.test.ts |
| 포털 노출 | trpc.system.adminMenu 인증 요구·시드 보장 | AgentCenter/tests/integration/router.test.ts |
| 화면 | 중앙 항목이 공통 구간으로 렌더됨·빈 목록 반영·null 시 로컬 시드·두 구간 순서 연속 | AgentCenter/client/src/navigation.test.tsx |
| 환경 | API 키·TTL 기본값 | AgentCenter/tests/unit/env.test.ts |