From 0ed048905a78b7b364fc0daf5e0fd75889cafd6a Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 30 Jun 2026 18:05:04 +0900 Subject: [PATCH] [UX] #577 focus effective access on selected user --- docs/design/577-persona-ux-audit/README.md | 77 +++++ src/main/resources/static/css/app.css | 124 ++++++++ src/main/resources/static/js/app.js | 49 ++++ .../resources/templates/effective-matrix.html | 273 ++++++------------ .../EffectiveMatrixTemplateRenderTest.java | 57 ++++ .../web/GuidedFlowTemplateTest.java | 16 + 6 files changed, 419 insertions(+), 177 deletions(-) create mode 100644 docs/design/577-persona-ux-audit/README.md create mode 100644 src/test/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixTemplateRenderTest.java diff --git a/docs/design/577-persona-ux-audit/README.md b/docs/design/577-persona-ux-audit/README.md new file mode 100644 index 0000000..88c6760 --- /dev/null +++ b/docs/design/577-persona-ux-audit/README.md @@ -0,0 +1,77 @@ +# VPD Backoffice 전 페이지 페르소나 UX 리뷰와 개선 로드맵 + +> **상태**: Review complete · UX-1 in progress +> **작성**: [AI] UX Facilitator · **최종수정**: 2026-06-30 +> **추적성** — Redmine: UX audit 상위 이슈 등록 예정 · 선행 개선: #575 대시보드, #576 기본 검증 사용자명 + +## 1. 리뷰 방법 + +세 페르소나가 같은 화면을 다른 기준으로 검토했다. + +| 페르소나 | 실패로 보는 상황 | 판단 기준 | +|---|---|---| +| UI/UX 전문가 | 정보가 같은 밀도로 쏟아져 다음 행동을 고르지 못함 | 시선 순서, 한 화면의 결정 수, 진행 상태, 빈 상태 | +| 권한 설계자 | 편한 조작이 권한 영향·fail-closed 원칙을 숨김 | 대상/주체/효과/영향 범위, 복구 가능성, 증적 | +| 엔드유저 | “그래서 지금 뭘 해야 하지?”를 5초 안에 답하지 못함 | 업무 언어, 기존 값 확인, 성공 기준, 되돌리기 | + +공통 결론은 **설명은 충분하지만, 실행 화면이 ‘관리 매뉴얼’처럼 길다**는 점이다. 모든 페이지를 카드로 더 꾸미는 대신 다음 원칙으로 정리한다. + +1. 첫 화면에는 주 행동 하나와 현재 상태만 둔다. +2. 대상·주체·영향을 입력 전에 보여 주고, 기술 설명은 접는다. +3. 목록은 탐색용, 상세/편집은 선택한 항목 중심으로 분리한다. +4. VPD·토큰·ORDS는 같은 검증 여정으로 연결하고 성공 증적을 다음 화면으로 넘긴다. + +## 2. 페이지별 토론과 개선 백로그 + +| 페이지 | UI/UX 전문가 | 권한 설계자 | 엔드유저 | 합의한 개선 | 우선순위 | +|---|---|---|---|---|---| +| 대시보드 | 설명·현황·표가 동등하게 경쟁 | 보호 상태의 근거가 약함 | 다음 행동이 불명확 | 핵심 작업·현재 상태·4단계 레일만 전면, 상세 접기 — **#575 완료** | 완료 | +| 사용자 | 생성/목록/역할 부여가 세 구역으로 흩어짐 | 역할 해제 영향이 안 보임 | “이 사람에게 지금 어떤 권한이 있나?”를 다시 찾아야 함 | 행 선택 시 현재 역할·그룹·토큰 요약을 보여 주는 사용자 작업 패널, 역할 부여 전 영향 미리보기 | P0 | +| 그룹 | 사용자와 역할 배정이 긴 한 페이지에 누적 | 그룹 상속 범위가 숨음 | 그룹을 바꾸면 누가 영향받는지 모름 | 그룹 선택형 master-detail, 영향 사용자 수·상속 역할을 상단에 고정, 해제 전 영향 확인 | P0 | +| 역할 | 민감도·설명·삭제가 표 안에서 동등 | 역할 삭제/변경 영향이 중요 | 역할이 누구에게 쓰이는지 모름 | 역할 상세에서 연결 사용자·그룹·권한을 먼저 보여 주고, 삭제는 영향 없음일 때만 노출 | P0 | +| 권한 관리 | wizard가 길고 선택의 결과가 늦게 보임 | ALLOW/DENY, 행/열 규칙의 조합 위험 | ‘ALL’을 누르면 무엇이 보이는지 불안 | 역할→객체→행 범위→컬럼→검토의 5단계 progress, 즉시 영향 문장과 변경 요약 고정 | P0 | +| 유효 권한 매트릭스 | 사용자/그룹/역할 표 세 개가 한꺼번에 큼 | 최종 권한 근거를 추적해야 함 | 내 사용자 하나만 보고 싶음 | 기본은 사용자 탭+검색, 선택 시 권한 근거 타임라인; 그룹/역할은 보조 탭으로 분리 | P0 | +| ORDS 조회 대상 | 등록·경로·핸들러 설정이 한 화면에 섞임 | 보호 대상과 handler 연결 상태를 분리해야 함 | 무엇을 등록해야 하는지 모름 | 객체 등록 후 ‘보호 연결하기’ 단계 CTA, 목록에는 VPD/handler/검증 상태 배지 | P1 | +| DB 보호 연결 | 객체 목록·개별 적용·일괄 적용·정책 목록이 매우 김 | 적용/해제의 DB 영향이 중요 | 어떤 객체부터 연결할지 모름 | 미연결 객체를 기본 큐로, 선택 시 영향/복구 설명과 검증 CTA; 일괄 적용은 고급으로 접기 | P0 | +| 검증 세션 | 발급과 이력이 분리되어 다음 행동이 약함 | 토큰 원문 보안과 만료가 핵심 | 발급 뒤 무엇을 복사/어디로 가나 | 사용자 선택→만료→발급→즉시 결과 확인의 단일 handoff, 이력은 보조 탭 | P0 | +| 권한 결과 확인 | 입력과 증적이 좋지만 초보자가 object/token 선택에서 멈춤 | VPD predicate와 실제 결과는 반드시 함께 보여야 함 | 내 상황으로 빠르게 시험하고 싶음 | ‘검증 사용자’ 기본 모드와 ‘Bearer 직접 입력’ 고급 모드 분리, 결과에 다음 조치 추천 | P0 | +| ORDS 핸들러 | 소스·편집이 기술자 화면으로 바로 열림 | handler 변경은 VPD 우회 위험 | 보통 사용자는 건드릴 이유가 없음 | 상태/경로/보호 객체만 기본 표시, 소스·수정은 고급 상세와 영향 경고로 격리 | P1 | +| 지식 검색 관리 | 등록·정책·검색 세 큰 작업이 세로로 길게 누적 | 벡터 검색과 접근 조건을 혼동하기 쉬움 | 검색만 해 보고 싶음 | ‘자료 등록 / 접근 정책 / 검색 검증’ 탭화, 검색을 기본 탭으로 하고 정책은 요약 배지 | P1 | +| MCP Chatbot | 대화, 토큰, 기술 설명의 경계가 약함 | 도구 호출과 권한 결과의 근거 필요 | 질문만 하고 싶음 | 질문 패널 우선, 호출된 도구·권한 근거는 답변 아래 접힌 evidence로 표시 | P1 | +| MCP Reasoning | 도구 목록과 실행 입력이 같은 밀도 | 자동 선택 근거/권한 거부를 보여야 함 | 왜 이 도구가 선택됐는지 알고 싶음 | 질문→선택 도구→권한 결과→답변의 단계형 결과 타임라인 | P1 | +| MCP Client | Java 호출, 흐름, 설정이 한 페이지 | 실행 대상 endpoint 검증 필요 | API 호출 방법만 알고 싶음 | 복사 가능한 최소 호출 예시를 전면, 런타임 진단·프로토콜 설명은 접기 | P2 | +| MCP SSE | endpoint/tool/JSON-RPC 예시가 문서형 | 배포/인증 상태가 우선 | 연결 가능한지부터 알고 싶음 | 연결 상태·endpoint 복사·도구 수를 상단 요약, 프로토콜 예시는 탭/상세로 이동 | P2 | +| 운영 상태 | 표 중심의 상태 나열 | 장애 시 영향 범위/복구가 필요 | 지금 정상인가만 알고 싶음 | 건강/주의/장애를 요약 배너로, 항목별 영향·권장 조치·최근 확인 시각 제공 | P1 | +| 설정 | ORDS, schema preview, 초기화가 한 화면에 있고 위험 | 초기화는 오조작 방지가 필요 | URL만 바꾸고 싶음 | 연결 설정과 DB 초기화를 분리; 초기화는 별도 위험 화면·명시 확인·백업 안내 | P0 | +| 별도 Filter 관리 | 고급 내용을 충분히 경고하지만 길다 | 기본 권한체계 우회 여부가 중요 | 이 화면을 써야 하는지 모르겠음 | 시작 시 ‘기본 권한으로 가능한가?’ 결정 카드, 실제 filter 편집은 조건 충족 시에만 열기 | P1 | +| 로그인 | 업무 시작점/권한 범위 설명 없음 | 운영 계정임을 명확히 해야 함 | 어디에 로그인하는지 모름 | 제품 목적·환경 표시·지원 경로를 최소한으로 추가 | P2 | + +## 3. 우선순위와 실행 묶음 + +| 실행 묶음 | 페이지 | 목표 | 선행/완료 조건 | +|---|---|---|---| +| UX-1 권한 주체와 최종 권한 | 사용자, 그룹, 역할, 유효 권한 매트릭스 | ‘누가 어떤 역할을 통해 무엇을 보는가’를 한 흐름으로 이해 | 영향 미리보기, 선택형 상세, 사용자 중심 매트릭스 | +| UX-2 보호·검증 여정 | 권한, DB 보호 연결, 토큰, probe, 설정 | 권한 생성부터 실제 DB 증적까지 끊기지 않게 연결 | 단계별 handoff, 위험/복구 안내, 고급 분리 | +| UX-3 연동 도구 정리 | objects, ORDS, vector, MCP 4종, 운영 상태, filter | 운영 화면과 개발자 화면의 밀도를 분리 | 요약 상태 우선, 기술 상세 접기/탭화 | +| UX-4 일관성 정리 | 로그인, 빈 상태, 문구, 배지, 모바일 | 모든 화면의 용어·행동·상태 표현을 정렬 | 공통 컴포넌트/카피 가이드 | + +## 4. 첫 구현 범위: UX-1 + +UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않고 화면의 정보 구조만 바꾼다. + +1. 사용자/그룹/역할 화면에 선택된 주체의 현재 연결과 영향 수를 먼저 보여 준다. +2. 유효 권한 매트릭스는 기본을 ‘사용자’로 두고, 선택한 사용자 한 명의 직접 역할·그룹 상속·최종 역할·보호 객체 근거를 순서대로 표시한다. +3. 그룹/역할 기준 전체 표는 보조 탐색으로 유지하되 기본 접힘 또는 탭 뒤로 보낸다. +4. 이 변경 뒤 실제 사용자 `김어드민`, `박파이넨스`, `이에이치알`로 권한 근거와 probe 결과를 교차 검증한다. + +## 5. 완료 정의 + +- 각 실행 묶음은 Redmine 하위 이슈, 설계서, 테스트, 실제 배포 HTTP 확인을 남긴다. +- 화면 변경은 권한 계산·VPD predicate·토큰 보안 동작을 바꾸지 않는다. +- UI/UX 전문가, 설계자, 엔드유저 관점의 미해결 반론이 있으면 이 문서와 Redmine에 기록하고 다음 묶음에서 해소한다. + +## 6. UX-1 진행 기록 + +- 유효 권한 매트릭스를 사용자 중심 탭형 구조로 변경했다. 기본 화면은 선택한 사용자 한 명의 직접 역할, 그룹 상속, 최종 역할, 보호 객체, 권한 수를 보여 준다. +- 그룹/역할 전체 표는 제거하지 않고 보조 탭으로 이동했다. +- 복합 Thymeleaf 속성과 layout fragment를 실제로 처리하는 `EffectiveMatrixTemplateRenderTest`를 추가해 UI template parse 오류를 빌드에서 검출한다. diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index a277923..fdc3a97 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -177,6 +177,10 @@ body { } @media (max-width: 920px) { + .effective-user-focus dl { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .rw-menu-panel { position: static; } @@ -665,6 +669,114 @@ body { margin-top: .25rem; } +.effective-workbench { + background: var(--rw-surface); + border: 1px solid var(--rw-border); + border-radius: 8px; + box-shadow: var(--rw-shadow); + margin-bottom: 1rem; + overflow: hidden; +} + +.effective-tabs { + background: var(--rw-surface-muted); + border-bottom: 1px solid var(--rw-border); + display: flex; + gap: .25rem; + padding: .5rem; +} + +.effective-tab { + background: transparent; + border: 0; + border-radius: 6px; + color: var(--rw-muted); + font-size: .88rem; + font-weight: 800; + padding: .55rem .7rem; +} + +.effective-tab:hover, +.effective-tab:focus, +.effective-tab.is-active { + background: var(--rw-surface); + color: var(--rw-primary); +} + +.effective-pane { + min-width: 0; + padding: 1rem; +} + +.effective-user-focus { + background: var(--rw-primary-soft); + border: 1px solid color-mix(in srgb, var(--rw-primary) 35%, var(--rw-border)); + border-radius: 8px; + margin-bottom: 1rem; + padding: 1rem; +} + +.effective-user-focus-heading strong, +.effective-user-focus-heading small { + display: block; +} + +.effective-user-focus-heading strong { + font-size: 1.2rem; + margin-top: .25rem; +} + +.effective-user-focus-heading small { + color: var(--rw-muted); + margin-top: .15rem; +} + +.effective-user-focus dl { + display: grid; + gap: .7rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin: 1rem 0; +} + +.effective-user-focus dl > div { + min-width: 0; +} + +.effective-user-focus dt { + color: var(--rw-muted); + font-size: .75rem; + font-weight: 800; +} + +.effective-user-focus dd { + font-size: .86rem; + font-weight: 700; + margin: .25rem 0 0; + overflow-wrap: anywhere; +} + +.effective-user-focus-footer { + align-items: center; + border-top: 1px solid var(--rw-border); + display: flex; + flex-wrap: wrap; + font-size: .84rem; + gap: .75rem; + justify-content: space-between; + padding-top: .75rem; +} + +.effective-user-focus-footer a { + color: var(--rw-primary); + font-weight: 800; + text-decoration: none; +} + +.effective-table-detail { + border-top: 1px solid var(--rw-border); + padding-top: 1rem; +} + .setup-steps { color: var(--rw-muted); margin: 0; @@ -1475,6 +1587,18 @@ body { } @media (max-width: 640px) { + .effective-tabs { + overflow-x: auto; + } + + .effective-tab { + flex: 0 0 auto; + } + + .effective-user-focus dl { + grid-template-columns: 1fr; + } + .schema-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); } diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js index 14003ab..dc2ddb7 100644 --- a/src/main/resources/static/js/app.js +++ b/src/main/resources/static/js/app.js @@ -630,6 +630,54 @@ function filterEffectiveMatrixTable(select) { row.innerHTML = `${table.dataset.emptyMessage || '선택한 항목이 없습니다.'}`; body?.appendChild(row); } + if (select.matches('[data-effective-user-select]')) { + updateEffectiveUserFocus(table, value); + } +} + +function updateEffectiveUserFocus(table, userId) { + const focus = document.querySelector('[data-effective-user-focus]'); + if (!focus || !table) { + return; + } + const row = Array.from(table.querySelectorAll('tbody tr[data-effective-id]')) + .find((item) => item.dataset.effectiveId === (userId || '')); + const values = { + username: row?.dataset.username || '사용자 선택', + profile: row?.dataset.profile || '사번 / 부서', + directRoles: row?.dataset.directRoles || '-', + inheritance: row?.dataset.inheritance || '-', + effectiveRoles: row?.dataset.effectiveRoles || '-', + objects: row?.dataset.objects || '-', + permissionCount: row?.dataset.permissionCount || '0' + }; + Object.entries(values).forEach(([field, value]) => { + const target = focus.querySelector(`[data-effective-user-field="${field}"]`); + if (target) { + target.textContent = value; + } + }); +} + +function initEffectiveMatrixTabs() { + const tabs = document.querySelectorAll('[data-effective-tab]'); + const panes = document.querySelectorAll('[data-effective-pane]'); + if (!tabs.length || !panes.length) { + return; + } + tabs.forEach((tab) => { + tab.addEventListener('click', () => { + const target = tab.dataset.effectiveTab; + tabs.forEach((item) => { + const active = item === tab; + item.classList.toggle('is-active', active); + item.setAttribute('aria-selected', String(active)); + }); + panes.forEach((pane) => { + pane.hidden = pane.dataset.effectivePane !== target; + }); + }); + }); } function initEffectiveMatrixFilters() { @@ -955,6 +1003,7 @@ document.addEventListener('DOMContentLoaded', () => { }); initTokenContextPreviews(); initEffectiveMatrixFilters(); + initEffectiveMatrixTabs(); const objectSelect = document.querySelector('select[name="objectRef"]'); if (objectSelect) { objectSelect.addEventListener('change', () => { diff --git a/src/main/resources/templates/effective-matrix.html b/src/main/resources/templates/effective-matrix.html index 8cdd8e3..0de868c 100644 --- a/src/main/resources/templates/effective-matrix.html +++ b/src/main/resources/templates/effective-matrix.html @@ -5,203 +5,122 @@
-

유효 권한 매트릭스

-

사용자별로 최종 적용되는 역할·권한·보호 객체를 확인합니다.

+

유효 권한 확인

+

한 사용자가 어떤 역할 경로를 거쳐 어떤 보호 객체를 볼 수 있는지 확인합니다.

- 이 표를 읽는 방법 -

백오피스 사용자, 그룹, 역할, 권한의 최종 상속 결과를 확인합니다. 직접 역할과 그룹을 통한 상속 역할을 나누어 보여주므로 권한이 어디에서 왔는지 추적할 수 있습니다.

+ 이 화면의 권한 근거 보기 +

직접 역할과 그룹 상속 역할을 합쳐 최종 권한을 계산합니다. 여기의 결과는 토큰을 발급해 ORDS/VPD 조회를 검증하기 전 확인하는 설계 근거입니다.

DB 연결 설정이 필요합니다. message -
- ./run.sh backoffice-support -
+
./run.sh backoffice-support
-
- -
+
-

권한 해석 기준

-

Bearer Token으로 식별되는 백오피스 사용자 기준입니다.

+ 권한 근거 +

누가 어떤 경로로 접근하는지 먼저 확인하세요.

+

사용자 기준으로 시작하고, 필요할 때 그룹과 역할 기준으로 전환합니다.

- 권한 관리로 이동 + 이어서 실제 결과 확인
-
- 사용자 - 0 -
-
- 그룹 - 0 -
-
- 역할 - 0 -
-
- 권한 - 0 -
-
-
- 권한이 계산되는 순서 보기 -
    -
  1. 사용자 관리에서 application user를 등록합니다.
  2. -
  3. 그룹 관리에서 사용자를 그룹에 넣고 그룹에 역할을 부여합니다.
  4. -
  5. 권한 관리에서 역할에 TABLE/VIEW 행 규칙과 컬럼 NULL 정책을 부여합니다.
  6. -
  7. 토큰을 발급하면 ORDS handler가 token user context를 설정하고 VPD가 이 권한을 적용합니다.
  8. -
-
-
- -
-
-
-

사용자 기준

-

직접 역할과 그룹을 통해 상속된 역할을 분리해서 봅니다.

-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
사용자직접 역할소속 그룹그룹 상속 역할최종 역할권한보호 객체
- 이에이치알 -
E100 / HR
- ACTIVE -
----0objects
등록된 사용자가 없습니다. 사용자 관리에서 application user를 먼저 등록하세요.
+
사용자0
+
그룹0
+
역할0
+
권한0
-
-
-
-

그룹 기준

-

그룹에 속한 사용자와 그룹에 부여된 역할이 제공하는 권한을 봅니다.

-
- +
+
+ + +
-
- - - - - - - - - - - - - - - - - - - - - - -
그룹포함 사용자부여 역할권한보호 객체
- GROUP -
그룹명
- ACTIVE -
usersroles0objects
등록된 그룹이 없습니다. 그룹 관리에서 그룹을 먼저 생성하세요.
-
-
-
-
-
-

역할 기준

-

역할이 직접 사용자와 그룹 사용자에게 어떤 영향을 주는지 봅니다.

+
+
+
+

선택한 사용자의 최종 권한

+

직접 부여와 그룹 상속을 구분해, 실제 검증 전 권한 경로를 확인합니다.

+
+
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - -
역할직접 사용자연결 그룹그룹 상속 사용자영향 사용자권한보호 객체
- ROLE -
민감도 PUBLIC
-
usersgroupsgroup usersaffected0objects
등록된 역할이 없습니다. 역할 관리에서 역할을 먼저 생성하세요.
-
+ +
+
+ 선택 사용자 + 사용자 선택 + 사번 / 부서 +
+
+
직접 역할
-
+
그룹 상속
-
+
최종 역할
-
+
보호 객체
-
+
+ +
+ +
+ + + + + + + + + + + + + + + +
사용자직접 역할그룹/상속 역할최종 역할권한보호 객체
이에이치알
E100 / HR
-그룹 없음상속 역할 없음-0objects
등록된 사용자가 없습니다. 사용자 관리에서 application user를 먼저 등록하세요.
+
+
+ + + +
diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixTemplateRenderTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixTemplateRenderTest.java new file mode 100644 index 0000000..31dacf1 --- /dev/null +++ b/src/test/java/com/cloudhandson/vpdbackoffice/web/EffectiveMatrixTemplateRenderTest.java @@ -0,0 +1,57 @@ +package com.cloudhandson.vpdbackoffice.web; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.cloudhandson.vpdbackoffice.domain.effective.EffectiveMatrixView; +import com.cloudhandson.vpdbackoffice.domain.effective.GroupEffectiveAccessView; +import com.cloudhandson.vpdbackoffice.domain.effective.RoleEffectiveImpactView; +import com.cloudhandson.vpdbackoffice.domain.effective.UserEffectiveAccessView; +import java.nio.file.Path; +import java.util.List; +import java.util.Locale; +import org.junit.jupiter.api.Test; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.templateresolver.FileTemplateResolver; + +class EffectiveMatrixTemplateRenderTest { + + @Test + void rendersTheUserFocusedEffectiveMatrixWithItsLayoutFragments() { + var resolver = new FileTemplateResolver(); + resolver.setPrefix(Path.of("src/main/resources/templates").toAbsolutePath() + "/"); + resolver.setSuffix(".html"); + resolver.setTemplateMode("HTML"); + resolver.setCacheable(false); + var engine = new SpringTemplateEngine(); + engine.setTemplateResolver(resolver); + + var matrix = new EffectiveMatrixView( + List.of(new UserEffectiveAccessView( + 103L, "김어드민", "E99999", "HQ", true, + List.of("ALL_DOC_ROLE"), List.of(), List.of(), List.of("ALL_DOC_ROLE"), + 1, List.of("CB_VECTOR_SEARCH_DOCUMENTS"))), + List.of(new GroupEffectiveAccessView( + 1L, "OPS", "운영", true, List.of("김어드민"), List.of("ALL_DOC_ROLE"), + 1, List.of("CB_VECTOR_SEARCH_DOCUMENTS"))), + List.of(new RoleEffectiveImpactView( + 30L, "ALL_DOC_ROLE", "CONFIDENTIAL", List.of("김어드민"), List.of(), List.of(), + List.of("김어드민"), 1, List.of("CB_VECTOR_SEARCH_DOCUMENTS"))), + 1 + ); + var context = new Context(Locale.KOREAN); + context.setVariable("matrix", matrix); + context.setVariable("_csrf", new CsrfFixture("_csrf", "test-token")); + + String rendered = engine.process("effective-matrix", context); + + assertThat(rendered) + .contains("선택한 사용자의 최종 권한") + .contains("김어드민") + .contains("data-effective-user-focus") + .contains("data-inheritance=\"그룹 없음 / 상속 역할 없음\""); + } + + private record CsrfFixture(String parameterName, String token) { + } +} diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java index c0b90e5..32c4b36 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java @@ -113,6 +113,22 @@ class GuidedFlowTemplateTest { .contains("기술 태그 비교 예시"); } + @Test + void effectiveMatrixStartsWithOneUserAndKeepsGroupAndRoleAsSecondaryViews() throws IOException { + String html = template("effective-matrix.html"); + String javascript = Files.readString(Path.of("src/main/resources/static/js/app.js")); + + assertThat(html) + .contains("선택한 사용자의 최종 권한") + .contains("data-effective-user-focus") + .contains("data-effective-tab=\"user\"") + .contains("data-effective-pane=\"group\"") + .contains("data-effective-pane=\"role\""); + assertThat(javascript) + .contains("updateEffectiveUserFocus") + .contains("initEffectiveMatrixTabs"); + } + @Test void mcpPagesDescribeReasoningAndProtocolBoundaries() throws IOException { String chatbot = template("mcp-chatbot.html");