[Designer] #575 focus dashboard on primary actions
This commit is contained in:
@@ -1559,6 +1559,260 @@ body {
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
max-width: 920px;
|
||||
}
|
||||
@@ -1882,6 +2136,24 @@ body {
|
||||
}
|
||||
|
||||
@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,
|
||||
.macro-micro-grid,
|
||||
.default-vpd-form {
|
||||
@@ -1899,6 +2171,21 @@ body {
|
||||
}
|
||||
|
||||
@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 {
|
||||
padding: .9rem;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
<head th:replace="~{fragments/layout :: head('VPD 권한 백오피스')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<header class="page-title guided-hero">
|
||||
<span class="architecture-kicker">권한을 정하면 DB가 그대로 지킵니다</span>
|
||||
<h1>누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.</h1>
|
||||
<p class="context-summary">권한 설계 → DB 보호 연결 → 검증 세션 → 실제 결과 확인</p>
|
||||
<details class="explanation-details">
|
||||
<summary>전체 흐름 설명 보기</summary>
|
||||
<main class="container py-4 dashboard-page">
|
||||
<header class="dashboard-hero">
|
||||
<div>
|
||||
<span class="architecture-kicker">VPD 권한 운영</span>
|
||||
<h1>권한을 설계하고, DB가 지키는 결과를 확인하세요.</h1>
|
||||
<p>사용자·역할·데이터 범위를 정한 뒤, 실제 ORDS 조회 결과로 바로 검증합니다.</p>
|
||||
</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>
|
||||
</details>
|
||||
</header>
|
||||
@@ -20,82 +26,88 @@
|
||||
<div class="mt-2" th:if="${showSupportCommand}"><code>./run.sh backoffice-support</code></div>
|
||||
</div>
|
||||
|
||||
<section class="journey-grid" aria-label="권한 적용 네 단계">
|
||||
<a class="journey-card" href="/permissions">
|
||||
<span class="journey-number">1</span>
|
||||
<div>
|
||||
<h2>1. 권한 설계</h2>
|
||||
<p>사용자와 그룹에 역할을 주고, 역할마다 볼 수 있는 객체·행·컬럼을 정합니다.</p>
|
||||
<strong>권한 규칙 만들기 →</strong>
|
||||
<section class="dashboard-command-center" aria-label="주요 권한 작업">
|
||||
<div class="dashboard-primary-action">
|
||||
<span class="architecture-kicker">지금 할 일</span>
|
||||
<h2>권한을 바꾸거나, 결과를 확인하세요.</h2>
|
||||
<p>일상적인 변경은 권한 규칙에서 하고, 적용 결과는 검증 세션으로 확인합니다.</p>
|
||||
<div class="dashboard-action-grid">
|
||||
<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>
|
||||
</a>
|
||||
<a class="journey-card" href="/vpd-policies">
|
||||
<span class="journey-number">2</span>
|
||||
<div>
|
||||
<h2>2. DB 보호 연결</h2>
|
||||
<p>보호할 TABLE/VIEW를 고르면 동적 VPD가 1단계의 권한체계를 자동으로 적용합니다.</p>
|
||||
<strong>보호 객체 연결하기 →</strong>
|
||||
</div>
|
||||
</a>
|
||||
<a class="journey-card" href="/tokens">
|
||||
<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>
|
||||
</div>
|
||||
<aside class="dashboard-state" aria-label="현재 설정 현황">
|
||||
<span class="architecture-kicker">현재 상태</span>
|
||||
<dl>
|
||||
<div><dt>보호 객체</dt><dd th:text="${#lists.size(objects)}">0</dd></div>
|
||||
<div><dt>역할</dt><dd th:text="${#lists.size(roles)}">0</dd></div>
|
||||
<div><dt>검증 세션</dt><dd th:text="${#lists.size(tokens)}">0</dd></div>
|
||||
</dl>
|
||||
<a href="/objects">등록 대상 보기 <span aria-hidden="true">→</span></a>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="content-band macro-micro-grid">
|
||||
<div>
|
||||
<span class="architecture-kicker">MACRO · 전체 관점</span>
|
||||
<h2>권한체계가 유일한 기준입니다.</h2>
|
||||
<p>일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="architecture-kicker">MICRO · 실행 관점</span>
|
||||
<h2>VPD가 요청마다 조건을 계산합니다.</h2>
|
||||
<p>토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 마지막 검증 단계에서 확인합니다.</p>
|
||||
<section class="dashboard-workflow" aria-label="권장 권한 적용 순서">
|
||||
<div class="dashboard-section-heading">
|
||||
<span class="architecture-kicker">권장 순서</span>
|
||||
<h2>권한 적용 4단계</h2>
|
||||
</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 class="summary-grid" aria-label="현재 등록 현황">
|
||||
<a class="summary-tile" href="/permissions"><span class="label">보호 객체</span><strong th:text="${#lists.size(objects)}">0</strong></a>
|
||||
<a class="summary-tile" href="/roles"><span class="label">역할</span><strong th:text="${#lists.size(roles)}">0</strong></a>
|
||||
<a class="summary-tile" href="/tokens"><span class="label">발급 이력</span><strong th:text="${#lists.size(tokens)}">0</strong></a>
|
||||
</section>
|
||||
<details class="dashboard-details">
|
||||
<summary>작동 방식과 등록 대상 보기</summary>
|
||||
<div class="dashboard-details-body">
|
||||
<section class="macro-micro-grid" aria-label="권한 적용 방식">
|
||||
<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">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>현재 검증 가능한 데이터</h2>
|
||||
<p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
|
||||
</div>
|
||||
<a class="btn btn-sm rw-btn-primary" href="/probe">결과 확인</a>
|
||||
<section class="dashboard-object-list">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>현재 검증 가능한 데이터</h2>
|
||||
<p class="section-subtitle">권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.</p>
|
||||
</div>
|
||||
<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 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">아직 검증할 보호 객체가 없습니다. 1단계에서 객체 권한을 먼저 등록하세요.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</details>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user