diff --git a/docs/design/489-vpd-target-filtering/README.md b/docs/design/489-vpd-target-filtering/README.md new file mode 100644 index 0000000..f5205f5 --- /dev/null +++ b/docs/design/489-vpd-target-filtering/README.md @@ -0,0 +1,47 @@ +# Redmine #489 - VPD 적용 대상 목록 필터/검색 설계 + +## 프로젝트 개요 + +VPD Backoffice는 Oracle Database VPD/ORDS 기능을 백오피스 권한 테이블로 제어하기 위한 Spring Boot 관리 도구다. 운영자는 Oracle catalog의 TABLE/VIEW 중 VPD 적용 대상과 권한 등록 상태를 확인하고, 개별 또는 벌크로 VPD policy를 적용한다. + +## 목표 + +`VPD 설정` 화면의 `VPD 적용 대상 TABLE/VIEW` 목록에서 업무 객체를 빠르게 찾을 수 있게 한다. DB catalog 원본은 유지하되, 기본 화면에서는 백오피스 지원 테이블을 숨기고 검색/필터/표시 건수 제어를 제공한다. + +## 문제 + +캡처 기준: + +- `/tmp/vpd-ux-audit/screenshots/desktop-vpd-policies.png` +- `/tmp/vpd-ux-audit/screenshots/mobile-vpd-policies.png` + +확인된 문제: + +- 지원 테이블(`CB_APP_*`, `CB_PERMISSION*`, `CB_PROTECTED_*`, `CB_USER_*` 등)과 업무 TABLE/VIEW가 섞여 보인다. +- 목록이 길어 개별 적용 폼까지 이동하기 전에 운영자가 맥락을 잃는다. +- schema 조회는 있으나 object 검색, type, VPD 상태, 권한 등록 상태 필터가 없다. + +## 설계 + +- 서버의 catalog 조회 결과는 그대로 둔다. +- 클라이언트 필터를 추가해 현재 페이지의 목록을 즉시 좁힌다. +- 기본값은 `백오피스 지원 테이블 제외`로 둔다. +- 지원 테이블 포함 여부, 검색어, object type, VPD 적용 상태, 권한 등록 상태, 표시 건수를 조합한다. +- 숨김은 primary row와 filter detail row를 한 쌍으로 처리한다. + +## 완료 기준 + +- 기본 화면에서 백오피스 지원 테이블이 숨겨진다. +- 사용자는 checkbox로 지원 테이블을 다시 포함할 수 있다. +- object 검색, TABLE/VIEW, VPD 상태, 권한 등록 상태, 표시 건수 필터가 동작한다. +- 필터 결과 건수와 전체 건수가 표시된다. +- 모바일 390px에서 body horizontal overflow가 없어야 한다. + +## 검증 + +- `mvn test` +- Playwright: + - `/vpd-policies`에서 기본 표시 건수와 지원 테이블 숨김 확인 + - 지원 테이블 포함 checkbox 동작 확인 + - 검색어 `BOARD` 입력 시 matching row 확인 + - 390px viewport overflow 확인 diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index b4faad1..03d0c82 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -526,6 +526,38 @@ body { min-height: 2rem; } +.vpd-target-controls { + align-items: end; + background: var(--rw-surface-muted); + border: 1px solid var(--rw-border); + border-radius: 8px; + display: grid; + gap: .75rem; + grid-template-columns: minmax(min(220px, 100%), 1.5fr) repeat(4, minmax(min(120px, 100%), 1fr)) auto auto; + margin-bottom: .75rem; + min-width: 0; + padding: .75rem; +} + +.vpd-target-controls label { + color: var(--rw-muted); + font-size: .78rem; + font-weight: 700; +} + +.vpd-support-toggle { + align-items: center; + display: flex; + gap: .45rem; + min-height: 2.2rem; +} + +.vpd-target-count { + color: var(--rw-muted); + font-size: .82rem; + white-space: nowrap; +} + .question-presets { display: flex; flex-wrap: wrap; @@ -1146,6 +1178,14 @@ body { flex: 1 1 auto; } + .vpd-target-controls { + grid-template-columns: 1fr; + } + + .vpd-target-count { + white-space: normal; + } + .table-responsive > .table { min-width: 960px; } diff --git a/src/main/resources/static/js/app.js b/src/main/resources/static/js/app.js index e04db4b..bf6261c 100644 --- a/src/main/resources/static/js/app.js +++ b/src/main/resources/static/js/app.js @@ -492,9 +492,138 @@ function initPermissionWizard() { activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1')); } +const backofficeSupportObjectPatterns = [ + /^CB_APP_/, + /^CB_PERMISSION(_|$)/, + /^CB_PROTECTED_/, + /^CB_USER_/, + /^CB_GROUP_/, + /^CB_ORDS_/, + /^CB_AGENT_BEARER_/, + /^CB_BACKOFFICE_/, + /^APP_(GROUP|ROLE|USER)$/, + /^DB_SOURCE$/, + /^PERMISSION$/, + /^USER_GROUP$/ +]; + +function isBackofficeSupportObject(objectName) { + const normalized = (objectName || '').trim().toUpperCase(); + return backofficeSupportObjectPatterns.some((pattern) => pattern.test(normalized)); +} + +function initVpdTargetFilters() { + const controls = document.querySelector('[data-vpd-target-controls]'); + const body = document.querySelector('[data-vpd-target-body]'); + if (!controls || !body) { + return; + } + const rows = Array.from(body.querySelectorAll('[data-vpd-target-row]')).map((row) => ({ + row, + detail: row.nextElementSibling?.matches('[data-vpd-target-detail]') ? row.nextElementSibling : null, + object: (row.dataset.object || '').toUpperCase(), + objectName: (row.dataset.objectName || '').toUpperCase(), + objectType: (row.dataset.objectType || '').toUpperCase(), + vpdApplied: row.dataset.vpdApplied || 'false', + protectedObject: row.dataset.protectedObject || 'false' + })); + const search = controls.querySelector('[data-vpd-target-search]'); + const type = controls.querySelector('[data-vpd-target-type]'); + const vpd = controls.querySelector('[data-vpd-target-vpd]'); + const protectedObject = controls.querySelector('[data-vpd-target-protected]'); + const includeSupport = controls.querySelector('[data-vpd-target-include-support]'); + const pageSize = controls.querySelector('[data-vpd-target-page-size]'); + const visibleCount = controls.querySelector('[data-vpd-target-visible]'); + const matchedCount = controls.querySelector('[data-vpd-target-matched]'); + const totalCount = controls.querySelector('[data-vpd-target-total]'); + const reset = controls.querySelector('[data-vpd-target-reset]'); + const empty = document.createElement('tr'); + empty.className = 'vpd-target-empty-runtime'; + empty.innerHTML = '
| 검증용 ORDS 경로 | - +||||||
|---|---|---|---|---|---|---|
ADMIN.TABLE |
TABLE | @@ -98,7 +153,7 @@ 검증 경로 미등록 | ||||
|
Filter 이름을 클릭하면 policy/filter source가 표시됩니다.
| ||||||