Compare commits

..

2 Commits

Author SHA1 Message Date
devmrko
8baaa3fe56 [Designer] #575 focus dashboard on primary actions 2026-06-30 17:31:29 +09:00
devmrko
d72dbbe842 ui: collapse DDS dashboard guidance 2026-06-30 16:42:56 +09:00
7 changed files with 458 additions and 96 deletions

View File

@@ -63,6 +63,10 @@ body { margin: 0; background: var(--dds-bg); color: var(--dds-ink); font-family:
.login-card { background: #fff; border: 1px solid var(--dds-line); border-radius: 16px; box-shadow: 0 15px 45px rgba(31, 51, 93, .12); max-width: 420px; padding: 35px; width: 100%; } .login-card { background: #fff; border: 1px solid var(--dds-line); border-radius: 16px; box-shadow: 0 15px 45px rgba(31, 51, 93, .12); max-width: 420px; padding: 35px; width: 100%; }
.login-card h1 { font-size: 1.65rem; margin: 8px 0; } .login-card h1 { font-size: 1.65rem; margin: 8px 0; }
.login-card p { color: var(--dds-muted); margin-bottom: 26px; } .login-card p { color: var(--dds-muted); margin-bottom: 26px; }
.journey-card-compact { align-items: center; min-height: 0; padding: .95rem 1.1rem; }
.journey-card-compact h2 { margin: 0 0 .25rem; }
.journey-card-compact strong { font-size: .8rem; }
.compact-context { padding: .75rem; }
@media (max-width: 800px) { @media (max-width: 800px) {
.flow-grid, .matrix-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .flow-grid, .matrix-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.query-grid, .context-grid { grid-template-columns: 1fr; } .query-grid, .context-grid { grid-template-columns: 1fr; }

View File

@@ -6,10 +6,10 @@
<main class="container py-4"> <main class="container py-4">
<header class="page-title guided-hero"> <header class="page-title guided-hero">
<span class="architecture-kicker">DEEP DATA SECURITY · INDEPENDENT TRACK</span> <span class="architecture-kicker">DEEP DATA SECURITY · INDEPENDENT TRACK</span>
<h1>같은 권한 기준을 DDS 방식으로 적용하고 확인합니다.</h1> <h1>DDS 권한 관리</h1>
<p class="context-summary">권한 설계 → 객체별 DATA GRANT → 토큰 Context 또는 END USER → 보호 결과 확인</p> <p class="context-summary">권한 설계 · 보호 연결 · 검색 검증</p>
<details class="explanation-details"> <details class="explanation-details">
<summary>이 인스턴스의 역할 보기</summary> <summary>이 인스턴스가 하는 일</summary>
<p>8083 DDS 인스턴스는 8082 VPD 인스턴스와 별도로 실행됩니다. 두 화면은 사용자·그룹·역할·권한 규칙을 같은 관리 흐름으로 보여주며, DDS는 보호 객체별 DATA GRANT로 그 규칙을 집행합니다.</p> <p>8083 DDS 인스턴스는 8082 VPD 인스턴스와 별도로 실행됩니다. 두 화면은 사용자·그룹·역할·권한 규칙을 같은 관리 흐름으로 보여주며, DDS는 보호 객체별 DATA GRANT로 그 규칙을 집행합니다.</p>
<p class="mb-0">일상적인 권한 변경은 권한 관리에서 하고, DDS 보호 객체와 실제 결과는 이 인스턴스에서 확인합니다.</p> <p class="mb-0">일상적인 권한 변경은 권한 관리에서 하고, DDS 보호 객체와 실제 결과는 이 인스턴스에서 확인합니다.</p>
</details> </details>
@@ -18,16 +18,21 @@
<div class="alert alert-warning" th:if="${runtimeError}">DDS 관리 데이터를 불러오지 못했습니다. <span th:text="${runtimeError}"></span></div> <div class="alert alert-warning" th:if="${runtimeError}">DDS 관리 데이터를 불러오지 못했습니다. <span th:text="${runtimeError}"></span></div>
<section class="journey-grid" aria-label="DDS 권한 적용 네 단계"> <section class="journey-grid" aria-label="DDS 권한 적용 네 단계">
<a class="journey-card" href="/permissions"><span class="journey-number">1</span><div><h2>1. 권한 설계</h2><p>사용자·그룹·역할에 객체와 TAG 규칙을 연결합니다.</p><strong>권한 규칙 만들기 →</strong></div></a> <a class="journey-card journey-card-compact" href="/permissions"><span class="journey-number">1</span><div><h2>권한 설계</h2><strong>기 →</strong></div></a>
<a class="journey-card" href="/vpd-policies"><span class="journey-number">2</span><div><h2>2. DDS 보호 연결</h2><p>공통 규칙을 DDS 전용 VIEW와 DATA GRANT에 연결합니다.</p><strong>DDS 보호 객체 확인</strong></div></a> <a class="journey-card journey-card-compact" href="/vpd-policies"><span class="journey-number">2</span><div><h2>DDS 보호 연결</h2><strong>열기</strong></div></a>
<a class="journey-card" href="/dds-provision"><span class="journey-number">3</span><div><h2>3. DDS 권한 연결</h2><p>보호 VIEW별 DATA GRANT와 공통 권한 predicate를 확인합니다.</p><strong>Grant 미리보기·게시</strong></div></a> <a class="journey-card journey-card-compact" href="/dds-provision"><span class="journey-number">3</span><div><h2>DDS 권한 연결</h2><strong>열기</strong></div></a>
<a class="journey-card" href="/vector-knowledge"><span class="journey-number">4</span><div><h2>4. 토큰 권한 검색</h2><p>하나의 기술 사용자에 Bearer 토큰을 연결해 공통 권한을 적용합니다.</p><strong>토큰 기반 검색</strong></div></a> <a class="journey-card journey-card-compact" href="/vector-knowledge"><span class="journey-number">4</span><div><h2>권한 기반 검색</h2><strong>열기</strong></div></a>
<a class="journey-card" href="/dds"><span class="journey-number">5</span><div><h2>5. 보호 객체 구조</h2><p>테이블·컬럼·분류값 단위의 DDS 적용 범위를 확인합니다.</p><strong>보호 구조 보기 →</strong></div></a> <a class="journey-card journey-card-compact" href="/dds"><span class="journey-number">5</span><div><h2>보호 객체 확인</h2><strong>기 →</strong></div></a>
</section> </section>
<section class="content-band macro-micro-grid"> <section class="content-band compact-context">
<div><span class="architecture-kicker">MACRO · 전체 관점</span><h2>관리 기준은 하나입니다.</h2><p>누가 어떤 지식자료를 볼 수 있는지는 사용자·그룹·역할·권한 규칙에서 설명합니다. VPD와 DDS는 같은 기준을 서로 다른 DB 보호 방식으로 집행합니다.</p></div> <details class="explanation-details m-0">
<div><span class="architecture-kicker">MICRO · 실행 관점</span><h2>객체별 DATA GRANT가 최종 집행합니다.</h2><p>토큰 경로는 <code>Bearer → CB_AGENT_CTX → DATA GRANT predicate → 보호 VIEW</code>, 직접 비교는 <code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>입니다. 권한이 없으면 객체가 보이지 않아 <code>ORA-00942</code>가 될 수 있습니다.</p></div> <summary>권한 적용 흐름 설명 보기</summary>
<div class="macro-micro-grid mt-3">
<div><span class="architecture-kicker">MACRO · 전체 관점</span><h2>관리 기준은 하나입니다.</h2><p>누가 어떤 지식자료를 볼 수 있는지는 사용자·그룹·역할·권한 규칙에서 설명합니다. VPD와 DDS는 같은 기준을 서로 다른 DB 보호 방식으로 집행합니다.</p></div>
<div><span class="architecture-kicker">MICRO · 실행 관점</span><h2>객체별 DATA GRANT가 최종 집행합니다.</h2><p>토큰 경로는 <code>Bearer → CB_AGENT_CTX → DATA GRANT predicate → 보호 VIEW</code>, 직접 비교는 <code>END USER → DATA ROLE → DATA GRANT → 보호 VIEW</code>입니다. 권한이 없으면 객체가 보이지 않아 <code>ORA-00942</code>가 될 수 있습니다.</p></div>
</div>
</details>
</section> </section>
<section class="summary-grid" aria-label="현재 DDS 관리 현황"> <section class="summary-grid" aria-label="현재 DDS 관리 현황">
@@ -37,10 +42,14 @@
</section> </section>
<section class="content-band"> <section class="content-band">
<div class="section-heading"><div><h2>현재 DDS 검증 대상</h2><p class="section-subtitle">주 시나리오는 업무 분류값이 붙은 벡터 지식자료 검색입니다. 원본 테이블은 보조 기술 검증으로만 제공합니다.</p></div></div> <div class="section-heading"><div><h2>현재 DDS 검증 대상</h2></div></div>
<div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>대상</th><th>보호 방식</th><th>검증</th></tr></thead><tbody> <div class="table-responsive"><table class="table table-sm align-middle"><thead><tr><th>대상</th><th>보호 방식</th><th>검증</th></tr></thead><tbody>
<tr><td><code th:text="${vectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code></td><td>객체별 분류값 DATA GRANT + 토큰 Context + VECTOR_DISTANCE</td><td><a href="/vector-knowledge">지식 검색</a></td></tr> <tr><td><code th:text="${vectorObject}">ADMIN.CB_DDS_VECTOR_SEARCH_DOCUMENTS</code></td><td>객체별 분류값 DATA GRANT + 토큰 Context + VECTOR_DISTANCE</td><td><a href="/vector-knowledge">지식 검색</a></td></tr>
</tbody></table></div> </tbody></table></div>
<details class="explanation-details mt-3">
<summary>검증 대상 설명 보기</summary>
<p class="mb-0">주 시나리오는 업무 분류값이 붙은 벡터 지식자료 검색입니다. 원본 테이블은 DDS 권한 동작을 비교하는 보조 기술 검증에만 사용합니다.</p>
</details>
</section> </section>
</main> </main>
</body> </body>

View File

@@ -77,26 +77,36 @@
<div class="architecture-step" th:classappend="${activeLayer == 'permission'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'permission'} ? ' active'">
<span class="architecture-kicker">1 · WHO / WHAT</span> <span class="architecture-kicker">1 · WHO / WHAT</span>
<strong>권한 설계</strong> <strong>권한 설계</strong>
<p>누가 어떤 데이터의 어느 행과 컬럼을 볼지 정합니다.</p> <p th:if="${backofficeTrack != 'DDS'}">누가 어떤 데이터의 어느 행과 컬럼을 볼지 정합니다.</p>
</div> </div>
<div class="architecture-arrow"></div> <div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'vpd'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'vpd'} ? ' active'">
<span class="architecture-kicker" th:text="${backofficeTrack == 'DDS' ? '2 · ENFORCE' : '2 · ENFORCE'}">2 · ENFORCE</span> <span class="architecture-kicker" th:text="${backofficeTrack == 'DDS' ? '2 · ENFORCE' : '2 · ENFORCE'}">2 · ENFORCE</span>
<strong th:text="${backofficeTrack == 'DDS' ? 'DDS 보호 연결' : 'DB 보호 연결'}">DB 보호 연결</strong> <strong th:text="${backofficeTrack == 'DDS' ? 'DDS 보호 연결' : 'DB 보호 연결'}">DB 보호 연결</strong>
<p th:text="${backofficeTrack == 'DDS' ? '보호 객체별 DATA GRANT predicate가 관리된 권한을 행·컬럼에 적용합니다.' : 'VPD가 저장된 권한체계를 매번 읽어 DB에서 행을 자동 제한합니다.'}">DB 보호 정책이 권한을 적용합니다.</p> <p th:if="${backofficeTrack != 'DDS'}" th:text="${'VPD가 저장된 권한체계를 매번 읽어 DB에서 행을 자동 제한합니다.'}">DB 보호 정책이 권한을 적용합니다.</p>
</div> </div>
<div class="architecture-arrow"></div> <div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'token'} ? ' active'">
<span class="architecture-kicker">3 · IDENTITY</span> <span class="architecture-kicker">3 · IDENTITY</span>
<strong th:text="${backofficeTrack == 'DDS' ? 'END USER 컨텍스트' : '검증 세션'}">검증 세션</strong> <strong th:text="${backofficeTrack == 'DDS' ? 'END USER 컨텍스트' : '검증 세션'}">검증 세션</strong>
<p th:text="${backofficeTrack == 'DDS' ? '토큰 Context 또는 DDS END USER로 조회 주체를 전달합니다.' : '확인할 사용자를 나타내는 일회성 토큰을 준비합니다.'}">조회 주체를 준비합니다.</p> <p th:if="${backofficeTrack != 'DDS'}">확인할 사용자를 나타내는 일회성 토큰을 준비합니다.</p>
</div> </div>
<div class="architecture-arrow"></div> <div class="architecture-arrow"></div>
<div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'"> <div class="architecture-step" th:classappend="${activeLayer == 'ords'} ? ' active'">
<span class="architecture-kicker">4 · EVIDENCE</span> <span class="architecture-kicker">4 · EVIDENCE</span>
<strong>결과 확인</strong> <strong>결과 확인</strong>
<p>보호 정책을 통과한 데이터만 실제 결과로 확인합니다.</p> <p th:if="${backofficeTrack != 'DDS'}">보호 정책을 통과한 데이터만 실제 결과로 확인합니다.</p>
</div> </div>
</section> </section>
<details th:if="${backofficeTrack == 'DDS'}" class="explanation-details architecture-explanation">
<summary>DDS 적용 흐름 설명 보기</summary>
<ol>
<li>권한 관리에서 사용자·그룹·역할과 TAG 규칙을 정의합니다.</li>
<li>DDS 보호 VIEW/TABLE에 객체별 DATA GRANT를 연결합니다.</li>
<li>토큰 Context 또는 DDS END USER가 조회 주체를 전달합니다.</li>
<li>DATA GRANT를 통과한 행과 컬럼만 결과로 반환됩니다.</li>
</ol>
</details>
</body> </body>
</html> </html>

View File

@@ -0,0 +1,35 @@
# 설계서: 대시보드 작업 중심 정보 구조 (#575)
> **상태**: Implemented · QA passed
> **작성**: [AI] Architect · **최종수정**: 2026-06-30
> **추적성** — Redmine: #575 · 구현: `dashboard.html`, `app.css`, `GuidedFlowTemplateTest`
## 목적
대시보드 첫 화면에서 설명·카운트·객체 테이블이 같은 비중으로 노출되는 문서형 구성을 줄인다. 운영자가 즉시 할 수 있는 작업과 현재 상태를 먼저 제시하고, 원래의 상세 설명과 보호 객체 목록은 숨기지 않고 접어서 제공한다.
## 화면 구조
```text
제목 + [권한 결과 확인] [권한 규칙 관리]
┌ 지금 할 일 (두 핵심 작업) ──────────┬ 현재 상태 ┐
└──────────────────────────────────────┴───────────┘
권장 순서: 1 권한 설계 · 2 DB 보호 연결 · 3 검증 세션 · 4 결과 확인
▸ 작동 방식과 등록 대상 보기
└ Macro/Micro 설명 + 보호 객체/ORDS 경로 테이블
```
## 인수조건
- [x] 첫 화면에 핵심 작업 두 개와 현재 상태가 함께 보인다.
- [x] 네 단계는 장문 카드가 아닌 짧은 진행 레일로 표시된다.
- [x] Macro/Micro 설명과 보호 객체 테이블은 접힌 상세 영역에서 보존된다.
- [x] 모바일에서 핵심 작업, 상태, 단계가 한 열로 읽힌다.
- [x] 템플릿 테스트와 전체 테스트가 통과한다.
## QA 결과
- `mvn test`: 59 tests, 0 failures.
- 배포 후 인증된 HTTP 점검: dashboard 및 users/groups/roles/permissions/tokens/probe/ORDS 등 주요 18개 화면이 모두 `200`을 반환했다.
- dashboard 응답에서 작업 중심 영역(`dashboard-command-center`), 4단계 레일(`workflow-steps`), 기본으로 접힌 상세 영역(`dashboard-details`)을 확인했다.
- 개발 중 발견한 dashboard fragment 문법 오류는 수정하고 템플릿 테스트가 fragment 형식을 검사하도록 보완했다.

View File

@@ -1559,6 +1559,260 @@ body {
} }
/* Guided permission journey (#557) */ /* Guided permission journey (#557) */
.dashboard-page {
max-width: 1120px;
}
.dashboard-hero {
align-items: end;
border-bottom: 1px solid var(--rw-border);
display: grid;
gap: 1rem 2rem;
grid-template-columns: minmax(0, 1fr) auto;
margin-bottom: 1.25rem;
padding: .5rem 0 1.25rem;
}
.dashboard-hero h1 {
font-size: clamp(1.7rem, 3vw, 2.25rem);
font-weight: 750;
letter-spacing: -.035em;
line-height: 1.2;
margin: .25rem 0 0;
}
.dashboard-hero p {
color: var(--rw-muted);
margin: .5rem 0 0;
}
.dashboard-hero-actions {
display: flex;
flex-wrap: wrap;
gap: .5rem;
justify-content: flex-end;
}
.dashboard-explanation {
grid-column: 1 / -1;
margin-top: 0;
}
.dashboard-command-center {
background: var(--rw-surface);
border: 1px solid var(--rw-border);
border-radius: 12px;
box-shadow: var(--rw-shadow);
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(230px, .32fr);
margin-bottom: 1.25rem;
overflow: hidden;
}
.dashboard-primary-action {
min-width: 0;
padding: 1.3rem;
}
.dashboard-primary-action h2,
.dashboard-section-heading h2 {
font-size: 1.12rem;
font-weight: 800;
margin: .25rem 0 0;
}
.dashboard-primary-action > p {
color: var(--rw-muted);
margin: .4rem 0 1rem;
}
.dashboard-action-grid {
display: grid;
gap: .7rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.dashboard-action {
align-items: center;
background: var(--rw-surface-muted);
border: 1px solid var(--rw-border);
border-radius: 8px;
color: var(--rw-text);
display: grid;
gap: .65rem;
grid-template-columns: auto minmax(0, 1fr) auto;
min-width: 0;
padding: .8rem;
text-decoration: none;
}
.dashboard-action:hover,
.dashboard-action:focus {
border-color: var(--rw-primary);
color: var(--rw-text);
}
.dashboard-action-emphasis {
background: var(--rw-primary-soft);
border-color: color-mix(in srgb, var(--rw-primary) 35%, var(--rw-border));
}
.dashboard-action-icon {
align-items: center;
background: var(--rw-surface);
border: 1px solid var(--rw-border);
border-radius: 999px;
color: var(--rw-primary);
display: inline-flex;
font-size: .72rem;
font-weight: 900;
height: 2rem;
justify-content: center;
width: 2rem;
}
.dashboard-action strong,
.dashboard-action small {
display: block;
}
.dashboard-action strong {
font-size: .9rem;
}
.dashboard-action small {
color: var(--rw-muted);
font-size: .78rem;
line-height: 1.35;
margin-top: .15rem;
}
.dashboard-action-arrow {
color: var(--rw-primary);
font-weight: 900;
}
.dashboard-state {
background: var(--rw-surface-muted);
border-left: 1px solid var(--rw-border);
display: flex;
flex-direction: column;
padding: 1.3rem;
}
.dashboard-state dl {
display: grid;
gap: .65rem;
margin: .75rem 0 auto;
}
.dashboard-state dl > div {
align-items: baseline;
display: flex;
justify-content: space-between;
}
.dashboard-state dt {
color: var(--rw-muted);
font-size: .82rem;
font-weight: 600;
}
.dashboard-state dd {
font-size: 1.35rem;
font-weight: 850;
line-height: 1;
margin: 0;
}
.dashboard-state > a {
color: var(--rw-primary);
font-size: .84rem;
font-weight: 800;
margin-top: 1rem;
text-decoration: none;
}
.dashboard-workflow {
margin-bottom: 1.25rem;
}
.dashboard-section-heading {
margin-bottom: .7rem;
}
.workflow-steps {
display: grid;
gap: .6rem;
grid-template-columns: repeat(4, minmax(0, 1fr));
list-style: none;
margin: 0;
padding: 0;
}
.workflow-steps a {
border-top: 2px solid var(--rw-border-strong);
color: var(--rw-text);
display: grid;
gap: .2rem;
grid-template-columns: auto minmax(0, 1fr);
padding: .65rem 0;
text-decoration: none;
}
.workflow-steps a:hover,
.workflow-steps a:focus {
border-color: var(--rw-primary);
color: var(--rw-primary);
}
.workflow-steps span {
color: var(--rw-primary);
font-size: .76rem;
font-weight: 900;
grid-row: span 2;
margin-right: .1rem;
}
.workflow-steps strong {
font-size: .88rem;
}
.workflow-steps small {
color: var(--rw-muted);
font-size: .76rem;
line-height: 1.3;
}
.dashboard-details {
background: var(--rw-surface);
border: 1px solid var(--rw-border);
border-radius: 8px;
margin-bottom: 1rem;
}
.dashboard-details > summary {
color: var(--rw-primary);
cursor: pointer;
font-size: .9rem;
font-weight: 800;
padding: .85rem 1rem;
}
.dashboard-details[open] > summary {
border-bottom: 1px solid var(--rw-border);
}
.dashboard-details-body {
padding: 1rem;
}
.dashboard-object-list {
border-top: 1px solid var(--rw-border);
margin-top: 1rem;
padding-top: 1rem;
}
.guided-hero { .guided-hero {
max-width: 920px; max-width: 920px;
} }
@@ -1882,6 +2136,24 @@ body {
} }
@media (max-width: 920px) { @media (max-width: 920px) {
.dashboard-hero,
.dashboard-command-center {
grid-template-columns: 1fr;
}
.dashboard-hero-actions {
justify-content: flex-start;
}
.dashboard-state {
border-left: 0;
border-top: 1px solid var(--rw-border);
}
.dashboard-state dl {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.journey-grid, .journey-grid,
.macro-micro-grid, .macro-micro-grid,
.default-vpd-form { .default-vpd-form {
@@ -1899,6 +2171,21 @@ body {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.dashboard-primary-action,
.dashboard-state {
padding: 1rem;
}
.dashboard-action-grid,
.workflow-steps,
.dashboard-state dl {
grid-template-columns: 1fr;
}
.dashboard-hero-actions .btn {
flex: 1 1 12rem;
}
.journey-card { .journey-card {
padding: .9rem; padding: .9rem;
} }

View File

@@ -3,13 +3,19 @@
<head th:replace="~{fragments/layout :: head('VPD 권한 백오피스')}"></head> <head th:replace="~{fragments/layout :: head('VPD 권한 백오피스')}"></head>
<body> <body>
<nav th:replace="~{fragments/layout :: nav}"></nav> <nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4"> <main class="container py-4 dashboard-page">
<header class="page-title guided-hero"> <header class="dashboard-hero">
<span class="architecture-kicker">권한을 정하면 DB가 그대로 지킵니다</span> <div>
<h1>누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.</h1> <span class="architecture-kicker">VPD 권한 운영</span>
<p class="context-summary">권한 설계 → DB 보호 연결 → 검증 세션 → 실제 결과 확인</p> <h1>권한 설계하고, DB가 지키는 결과 확인하세요.</h1>
<details class="explanation-details"> <p>사용자·역할·데이터 범위를 정한 뒤, 실제 ORDS 조회 결과로 바로 검증합니다.</p>
<summary>전체 흐름 설명 보기</summary> </div>
<div class="dashboard-hero-actions" aria-label="주요 작업">
<a class="btn rw-btn-primary" href="/probe">권한 결과 확인</a>
<a class="btn rw-btn-secondary" href="/permissions">권한 규칙 관리</a>
</div>
<details class="explanation-details dashboard-explanation">
<summary>이 화면의 전체 흐름 보기</summary>
<p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p> <p>사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.</p>
</details> </details>
</header> </header>
@@ -20,82 +26,88 @@
<div class="mt-2" th:if="${showSupportCommand}"><code>./run.sh backoffice-support</code></div> <div class="mt-2" th:if="${showSupportCommand}"><code>./run.sh backoffice-support</code></div>
</div> </div>
<section class="journey-grid" aria-label="권한 적용 네 단계"> <section class="dashboard-command-center" aria-label="주요 권한 작업">
<a class="journey-card" href="/permissions"> <div class="dashboard-primary-action">
<span class="journey-number">1</span> <span class="architecture-kicker">지금 할 일</span>
<div> <h2>권한을 바꾸거나, 결과를 확인하세요.</h2>
<h2>1. 권한 설계</h2> <p>일상적인 변경은 권한 규칙에서 하고, 적용 결과는 검증 세션으로 확인합니다.</p>
<p>사용자와 그룹에 역할을 주고, 역할마다 볼 수 있는 객체·행·컬럼을 정합니다.</p> <div class="dashboard-action-grid">
<strong>권한 규칙 만들기 →</strong> <a class="dashboard-action" href="/permissions">
<span class="dashboard-action-icon" aria-hidden="true">01</span>
<span><strong>권한 규칙 관리</strong><small>역할별 객체·행·컬럼 접근을 설정합니다.</small></span>
<span class="dashboard-action-arrow" aria-hidden="true"></span>
</a>
<a class="dashboard-action dashboard-action-emphasis" href="/probe">
<span class="dashboard-action-icon" aria-hidden="true">02</span>
<span><strong>권한 결과 확인</strong><small>토큰으로 실제 DB 조회 결과를 검증합니다.</small></span>
<span class="dashboard-action-arrow" aria-hidden="true"></span>
</a>
</div> </div>
</a> </div>
<a class="journey-card" href="/vpd-policies"> <aside class="dashboard-state" aria-label="현재 설정 현황">
<span class="journey-number">2</span> <span class="architecture-kicker">현재 상태</span>
<div> <dl>
<h2>2. DB 보호 연결</h2> <div><dt>보호 객체</dt><dd th:text="${#lists.size(objects)}">0</dd></div>
<p>보호할 TABLE/VIEW를 고르면 동적 VPD가 1단계의 권한체계를 자동으로 적용합니다.</p> <div><dt>역할</dt><dd th:text="${#lists.size(roles)}">0</dd></div>
<strong>보호 객체 연결하기 →</strong> <div><dt>검증 세션</dt><dd th:text="${#lists.size(tokens)}">0</dd></div>
</div> </dl>
</a> <a href="/objects">등록 대상 보기 <span aria-hidden="true"></span></a>
<a class="journey-card" href="/tokens"> </aside>
<span class="journey-number">3</span>
<div>
<h2>3. 검증 세션</h2>
<p>검증할 사용자를 선택해 토큰을 발급하고, 한 번만 보이는 원문을 복사합니다.</p>
<strong>검증 세션 발급하기 →</strong>
</div>
</a>
<a class="journey-card" href="/probe">
<span class="journey-number">4</span>
<div>
<h2>4. 결과 확인</h2>
<p>토큰으로 ORDS를 호출해 사용자·상속 역할과 실제로 보이는 행을 함께 확인합니다.</p>
<strong>권한 결과 확인하기 →</strong>
</div>
</a>
</section> </section>
<section class="content-band macro-micro-grid"> <section class="dashboard-workflow" aria-label="권장 권한 적용 순서">
<div> <div class="dashboard-section-heading">
<span class="architecture-kicker">MACRO · 전체 관점</span> <span class="architecture-kicker">권장 순서</span>
<h2>권한체계가 유일한 기준입니다.</h2> <h2>권한 적용 4단계</h2>
<p>일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.</p>
</div>
<div>
<span class="architecture-kicker">MICRO · 실행 관점</span>
<h2>VPD가 요청마다 조건을 계산합니다.</h2>
<p>토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 마지막 검증 단계에서 확인합니다.</p>
</div> </div>
<ol class="workflow-steps">
<li><a href="/permissions"><span>1</span><strong>권한 설계</strong><small>사용자·역할·데이터 범위</small></a></li>
<li><a href="/vpd-policies"><span>2</span><strong>DB 보호 연결</strong><small>VPD 정책 적용</small></a></li>
<li><a href="/tokens"><span>3</span><strong>검증 세션</strong><small>사용자 토큰 발급</small></a></li>
<li><a href="/probe"><span>4</span><strong>결과 확인</strong><small>ORDS 실제 조회</small></a></li>
</ol>
</section> </section>
<section class="summary-grid" aria-label="현재 등록 현황"> <details class="dashboard-details">
<a class="summary-tile" href="/permissions"><span class="label">보호 객체</span><strong th:text="${#lists.size(objects)}">0</strong></a> <summary>작동 방식과 등록 대상 보기</summary>
<a class="summary-tile" href="/roles"><span class="label">역할</span><strong th:text="${#lists.size(roles)}">0</strong></a> <div class="dashboard-details-body">
<a class="summary-tile" href="/tokens"><span class="label">발급 이력</span><strong th:text="${#lists.size(tokens)}">0</strong></a> <section class="macro-micro-grid" aria-label="권한 적용 방식">
</section> <div>
<span class="architecture-kicker">전체 관점</span>
<h2>권한체계가 유일한 기준입니다.</h2>
<p>일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.</p>
</div>
<div>
<span class="architecture-kicker">실행 관점</span>
<h2>VPD가 요청마다 조건을 계산합니다.</h2>
<p>토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 결과 확인 단계에서 봅니다.</p>
</div>
</section>
<section class="content-band"> <section class="dashboard-object-list">
<div class="section-heading"> <div class="section-heading">
<div> <div>
<h2>현재 검증 가능한 데이터</h2> <h2>현재 검증 가능한 데이터</h2>
<p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p> <p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
</div> </div>
<a class="btn btn-sm rw-btn-primary" href="/probe">결과 확인</a> <a class="btn btn-sm rw-btn-primary" href="/probe">결과 확인</a>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead><tr><th>데이터 객체</th><th>검증 경로</th><th>상태</th></tr></thead>
<tbody>
<tr th:each="object : ${objects}">
<td><strong th:text="${object.displayName()}">ADMIN.OBJECT</strong></td>
<td><code th:text="${object.ordsPath()}">path</code></td>
<td><span class="badge text-bg-success">사용 가능</span></td>
</tr>
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 권한 규칙에서 객체 권한을 먼저 등록하세요.</td></tr>
</tbody>
</table>
</div>
</section>
</div> </div>
<div class="table-responsive"> </details>
<table class="table table-sm align-middle">
<thead><tr><th>데이터 객체</th><th>검증 경로</th><th>상태</th></tr></thead>
<tbody>
<tr th:each="object : ${objects}">
<td><strong th:text="${object.displayName()}">ADMIN.OBJECT</strong></td>
<td><code th:text="${object.ordsPath()}">path</code></td>
<td><span class="badge text-bg-success">사용 가능</span></td>
</tr>
<tr th:if="${#lists.isEmpty(objects)}"><td colspan="3" class="text-muted">아직 검증할 보호 객체가 없습니다. 1단계에서 객체 권한을 먼저 등록하세요.</td></tr>
</tbody>
</table>
</div>
</section>
</main> </main>
</body> </body>
</html> </html>

View File

@@ -10,14 +10,19 @@ import org.junit.jupiter.api.Test;
class GuidedFlowTemplateTest { class GuidedFlowTemplateTest {
@Test @Test
void dashboardExplainsTheFourStepPermissionJourney() throws IOException { void dashboardPrioritizesActionsAndKeepsDetailCollapsed() throws IOException {
String html = template("dashboard.html"); String html = template("dashboard.html");
assertThat(html) assertThat(html)
.contains("1. 권한 설계") .contains("th:replace=\"~{fragments/layout :: head('VPD 권한 백오피스')}\"")
.contains("2. DB 보호 연결") .contains("권한 결과 확인")
.contains("3. 검증 세션") .contains("권한 규칙 관리")
.contains("4. 결과 확인"); .contains("권한 적용 4단계")
.contains("workflow-steps")
.contains("현재 상태")
.contains("작동 방식과 등록 대상 보기")
.contains("<details class=\"dashboard-details\">")
.doesNotContain("journey-card");
} }
@Test @Test