From 8baaa3fe5633dc928d7c649604c3ddb4a3d36eec Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 30 Jun 2026 17:31:29 +0900 Subject: [PATCH] [Designer] #575 focus dashboard on primary actions --- .../README.md | 35 +++ src/main/resources/static/css/app.css | 287 ++++++++++++++++++ src/main/resources/templates/dashboard.html | 162 +++++----- .../web/GuidedFlowTemplateTest.java | 15 +- 4 files changed, 419 insertions(+), 80 deletions(-) create mode 100644 docs/design/575-dashboard-action-centered-ui/README.md diff --git a/docs/design/575-dashboard-action-centered-ui/README.md b/docs/design/575-dashboard-action-centered-ui/README.md new file mode 100644 index 0000000..9d21a29 --- /dev/null +++ b/docs/design/575-dashboard-action-centered-ui/README.md @@ -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 형식을 검사하도록 보완했다. diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index 6c00fd5..a277923 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -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; } diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index 73a1587..bc5626c 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -3,13 +3,19 @@ -
-
- 권한을 정하면 DB가 그대로 지킵니다 -

누가 어떤 데이터를 볼 수 있는지 설계하고, 실제 결과까지 확인하세요.

-

권한 설계 → DB 보호 연결 → 검증 세션 → 실제 결과 확인

-
- 전체 흐름 설명 보기 +
+
+
+ VPD 권한 운영 +

권한을 설계하고, DB가 지키는 결과를 확인하세요.

+

사용자·역할·데이터 범위를 정한 뒤, 실제 ORDS 조회 결과로 바로 검증합니다.

+
+ +
+ 이 화면의 전체 흐름 보기

사용자·그룹·역할에 권한을 연결하면 Oracle VPD가 요청할 때마다 그 규칙을 읽어 허용된 행만 반환합니다. 별도 SQL 필터를 만드는 일은 예외적인 고급 작업입니다.

@@ -20,82 +26,88 @@
./run.sh backoffice-support
-
- - 1 -
-

1. 권한 설계

-

사용자와 그룹에 역할을 주고, 역할마다 볼 수 있는 객체·행·컬럼을 정합니다.

- 권한 규칙 만들기 → +
+ +
-
-
- MACRO · 전체 관점 -

권한체계가 유일한 기준입니다.

-

일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.

-
-
- MICRO · 실행 관점 -

VPD가 요청마다 조건을 계산합니다.

-

토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 마지막 검증 단계에서 확인합니다.

+
+
+ 권장 순서 +

권한 적용 4단계

+
    +
  1. 1권한 설계사용자·역할·데이터 범위
  2. +
  3. 2DB 보호 연결VPD 정책 적용
  4. +
  5. 3검증 세션사용자 토큰 발급
  6. +
  7. 4결과 확인ORDS 실제 조회
  8. +
-
- 보호 객체0 - 역할0 - 발급 이력0 -
+
+ 작동 방식과 등록 대상 보기 +
+
+
+ 전체 관점 +

권한체계가 유일한 기준입니다.

+

일상적인 변경은 사용자, 그룹, 역할, 권한 규칙에서만 합니다. 같은 규칙을 화면과 DB 필터에 이중으로 작성하지 않습니다.

+
+
+ 실행 관점 +

VPD가 요청마다 조건을 계산합니다.

+

토큰에서 사용자를 찾고 직접 역할과 그룹 상속 역할을 합친 뒤, 객체의 ALLOW/DENY 및 행·열 규칙을 적용합니다. 근거는 결과 확인 단계에서 봅니다.

+
+
-
-
-
-

현재 검증 가능한 데이터

-

권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.

-
- 결과 확인 +
+
+
+

현재 검증 가능한 데이터

+

권한 규칙과 ORDS 경로가 등록된 보호 객체입니다.

+
+ 결과 확인 +
+
+ + + + + + + + + + +
데이터 객체검증 경로상태
ADMIN.OBJECTpath사용 가능
아직 검증할 보호 객체가 없습니다. 권한 규칙에서 객체 권한을 먼저 등록하세요.
+
+
-
- - - - - - - - - - -
데이터 객체검증 경로상태
ADMIN.OBJECTpath사용 가능
아직 검증할 보호 객체가 없습니다. 1단계에서 객체 권한을 먼저 등록하세요.
-
-
+
diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java index 5444152..0204d92 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java @@ -10,14 +10,19 @@ import org.junit.jupiter.api.Test; class GuidedFlowTemplateTest { @Test - void dashboardExplainsTheFourStepPermissionJourney() throws IOException { + void dashboardPrioritizesActionsAndKeepsDetailCollapsed() throws IOException { String html = template("dashboard.html"); assertThat(html) - .contains("1. 권한 설계") - .contains("2. DB 보호 연결") - .contains("3. 검증 세션") - .contains("4. 결과 확인"); + .contains("th:replace=\"~{fragments/layout :: head('VPD 권한 백오피스')}\"") + .contains("권한 결과 확인") + .contains("권한 규칙 관리") + .contains("권한 적용 4단계") + .contains("workflow-steps") + .contains("현재 상태") + .contains("작동 방식과 등록 대상 보기") + .contains("
") + .doesNotContain("journey-card"); } @Test