From 064a5bc31d7a94c86eba81d0590f6f4f408a7d76 Mon Sep 17 00:00:00 2001 From: devmrko Date: Wed, 1 Jul 2026 14:49:49 +0900 Subject: [PATCH] [UX] #577 clarify VPD flow and SQL evidence --- docs/design/567-ords-vpd-sql-trace/README.md | 2 + docs/design/577-persona-ux-audit/README.md | 11 +- src/main/resources/static/css/app.css | 242 +++++++++++++++++- src/main/resources/templates/dashboard.html | 104 ++++++-- .../resources/templates/fragments/layout.html | 18 +- .../templates/fragments/probe-result.html | 20 +- src/main/resources/templates/probe.html | 2 +- .../resources/templates/vpd-policies.html | 2 +- .../web/GuidedFlowTemplateTest.java | 23 +- 9 files changed, 380 insertions(+), 44 deletions(-) diff --git a/docs/design/567-ords-vpd-sql-trace/README.md b/docs/design/567-ords-vpd-sql-trace/README.md index 1bf70c6..1c7b31b 100644 --- a/docs/design/567-ords-vpd-sql-trace/README.md +++ b/docs/design/567-ords-vpd-sql-trace/README.md @@ -148,6 +148,8 @@ Oracle은 VPD의 내부 rewrite 결과를 별도 최종 SQL 문자열로 `V$SQL`에 보관하지 않는다. 따라서 원문 SQL과 실행계획 predicate를 함께 제시하는 것이 실제 실행에 대한 DB 증적이다. `executions`, `rows_processed`, `elapsed_time`, `buffer_gets`는 cursor 누적값이며 단일 HTTP 요청만의 계측값은 아니다. +화면에서는 설명용 표현을 **실행 요청 SQL (재현)**, cursor에서 읽은 원문을 **실제 DB cursor SQL**로 구분한다. `V$SQL` 권한이 없는 환경에서도 재현 SQL은 표시하고, 실제 cursor 증적이 없다는 이유를 별도로 표시한다. + 최근 SQL 매칭은 대상 객체와 최근 2분 실행 시각으로 고르므로, 같은 객체를 동시에 호출하는 운영 환경에서는 다른 요청 cursor가 선택될 가능성이 있다. 현재 UI는 이를 “최근 실행 증적”으로 명시한다. 요청별 완전 상관이 필요해지면 ORDS Handler에 검증 요청 ID를 주입해 고유 SQL comment/module-action으로 cursor를 추적하는 후속 작업으로 확장한다. 실행 증적 연결에는 최소한 `V$SQL`과 `DBMS_XPLAN.DISPLAY_CURSOR`를 조회할 수 있는 catalog 권한이 필요하다. 해당 권한은 ORDS parsing schema 연결에서 먼저 조회하고, 사용할 수 없으면 백오피스 연결로 한 번 더 시도한다. Autonomous의 일반 `ADMIN` 계정은 `SYS.V_$SQL` 권한을 다른 계정에 위임하지 못할 수 있으므로, 이 경우에는 DBA가 [34_agent_ords_execution_evidence_grant.sql](../../sql/adb/34_agent_ords_execution_evidence_grant.sql)을 실행해야 한다. cursor 미발견과 catalog 권한 미보유는 UI에서 서로 다른 안내로 표시하며, 어느 경우도 권한 검증 결과를 실패시키지 않는다. diff --git a/docs/design/577-persona-ux-audit/README.md b/docs/design/577-persona-ux-audit/README.md index b19f83d..3f39b91 100644 --- a/docs/design/577-persona-ux-audit/README.md +++ b/docs/design/577-persona-ux-audit/README.md @@ -104,7 +104,7 @@ UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않 | 별도 Filter 관리 | 고급 접근 조건 | 예외 정책 기능 | | 운영 상태 / 연결 설정 | 운영 현황 / 시스템 설정 | 운영성·연동 설정 기능 | -상단 메뉴는 VPD 업무 범위를 빠짐없이 드러내는 `권한 관리 · 보호·검증 · 연동 도구 · 운영·고급`으로 구성한다. 각 영역은 사용자·역할·접근 규칙, 보호 상태·검증 세션, 조회 연동·지식 검색·MCP, 운영 현황·고급 접근 조건·시스템 설정을 각각 포함한다. DDS와 공통으로 가져가는 것은 메뉴 분류가 아니라 **두 줄 탐색 방식**이다. 첫 줄에는 제품명·1차 메뉴·로그아웃을 함께 두고, 1차 메뉴를 누르면 해당 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면, wizard에도 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 선택된 입력 영역은 역할·대상·접근 범위처럼 의미 있는 이름으로만 표시한다. +상단 메뉴는 VPD 업무 범위를 빠짐없이 드러내는 `권한 관리 · 보호·검증 · 연동 도구 · 운영`으로 구성한다. 각 영역은 사용자·역할·접근 규칙, 보호 상태·검증 세션, 조회 연동·지식 검색·MCP, 운영 현황을 각각 포함한다. 시스템 연결, DB 준비, 고급 접근 조건은 일반 업무 메뉴와 섞지 않고 오른쪽 **관리자** 유틸리티로 분리한다. DDS와 공통으로 가져가는 것은 메뉴 분류가 아니라 **두 줄 탐색 방식**이다. 첫 줄에는 제품명·1차 메뉴·관리자·로그아웃을 함께 두고, 1차 메뉴를 누르면 해당 하위 메뉴가 팝업으로 뜨지 않고 바로 아래 전폭 줄에 표시된다. 현재 페이지가 속한 메뉴는 처음부터 아래 줄을 열어 둔다. 절차나 구현 방식이 아니라 사용자가 하려는 일을 메뉴명으로 쓴다. 메뉴와 기본 화면, wizard에도 `1.`, `2.`, 단계 레일, `01/02` 배지를 두지 않는다. 선택된 입력 영역은 역할·대상·접근 범위처럼 의미 있는 이름으로만 표시한다. 기본 페이지는 제목과 현재 설정·주요 행동만 노출한다. VPD/ORDS 처리 원리, 배경 설명, 주의사항은 상단의 공통 `도움말` 버튼을 눌렀을 때만 펼친다. 이 규칙으로 제품 화면이 관리 매뉴얼처럼 보이는 문제를 줄이고, 설정 자체를 전면에 둔다. @@ -117,3 +117,12 @@ UX-1부터 시작한다. 기존 데이터 모델과 VPD 로직은 바꾸지 않 - 접근 규칙 목록은 ID·고정 ACTION·삭제 경고를 기본 열에서 빼고, 필요한 삭제 영향만 행의 `관리` 상세에서 확인하게 한다. - 보호 상태의 동작 설명은 상단 도움말로 옮기고, 기본 화면은 보호 대상과 적용 작업부터 보이게 한다. - 운영 현황은 긴 ORDS·VPD 값 때문에 오른쪽 열이 밀리지 않도록 고정 열 폭과 줄바꿈을 적용한다. + +## 10. 홈·도움말 정보 구조 보완 — 2026-07-01 + +첫 화면은 설명을 길게 나열하는 대시보드가 아니라, `권한 관리 → 보호·검증 → 연동 도구 → 운영`의 큰 업무 흐름과 바로 시작할 행동, 현재 상태를 보여준다. 메뉴의 상세 역할과 권한 처리 원리는 홈의 도움말에서만 확장한다. + +- 도움말은 각 메뉴의 책임을 짧게 설명하고, 운영자가 길을 잃지 않도록 관리자 기능의 범위를 별도로 명시한다. +- 권한 적용 다이어그램은 하나의 VPD/DDS 실행 계정(`CB_ORDS`)이 요청 토큰으로 실제 업무 사용자(`CB_APP_USER`)를 식별한 뒤 `CB_AGENT_CTX`를 설정하고, 역할·권한 테이블을 읽어 VPD 조건을 계산하는 구조를 보인다. +- ERD는 직접 역할(`CB_USER_ROLE`)과 그룹 상속(`CB_USER_GROUP` → `CB_GROUP_ROLE`)이 같은 역할·권한·규칙 체계로 합쳐지는 점을 보여준다. 이 구조 때문에 실행 DB 계정을 사용자마다 만들지 않아도 요청별로 다른 데이터 범위가 적용된다. +- 접근 검증에는 읽기용 **실행 요청 SQL**과 실제 DB cursor 증적을 구분한다. `V$SQL`/`DBMS_XPLAN` 권한이 있으면 원문 SQL·SQL_ID·Predicate Information을 보여주며, 없으면 재현 SQL은 보여주되 증적 권한이 없다는 상태를 명확히 표시한다. Oracle VPD의 내부 최종 rewrite 문자열은 별도 SQL 텍스트로 보관되지 않는다. diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index e83b5de..9c2d1ad 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -123,6 +123,31 @@ body { margin-right: auto; } +.rw-nav-utility { + align-items: center; + display: flex; + gap: .5rem; +} + +.rw-admin-trigger { + background: var(--rw-surface); + border: 1px solid var(--rw-border-strong); + border-radius: 999px; + color: var(--rw-text); + font-size: .82rem; + font-weight: 750; + min-height: 2rem; + padding: .35rem .7rem; +} + +.rw-admin-trigger:hover, +.rw-admin-trigger:focus, +.rw-admin-trigger.active { + background: var(--rw-primary-soft); + border-color: var(--rw-primary); + color: var(--rw-primary-hover); +} + .rw-menu-trigger { background: transparent; border: 1px solid transparent; @@ -1880,6 +1905,200 @@ body { margin-top: 0; } +.product-help-overview { + display: grid; + gap: 1rem; +} + +.product-help-section + .product-help-section { + border-top: 1px solid var(--rw-border); + padding-top: 1rem; +} + +.product-help-section h2 { + font-size: 1rem; + font-weight: 800; + margin: 0; +} + +.product-help-section p { + max-width: 980px; +} + +.menu-role-grid { + display: grid; + gap: .65rem; + grid-template-columns: repeat(2, minmax(0, 1fr)); + margin: .75rem 0 0; +} + +.menu-role-grid > div { + background: var(--rw-surface); + border: 1px solid var(--rw-border); + border-radius: 8px; + padding: .75rem; +} + +.menu-role-grid dt { + color: var(--rw-text); + font-size: .88rem; + font-weight: 800; +} + +.menu-role-grid dd { + color: var(--rw-muted); + font-size: .82rem; + line-height: 1.45; + margin: .3rem 0 0; +} + +.diagram-canvas { + background: var(--rw-surface); + border: 1px solid var(--rw-border); + border-radius: 8px; + margin-top: .75rem; + overflow-x: auto; +} + +.product-flow-diagram, +.permission-erd-diagram { + display: block; + min-width: 900px; + width: 100%; +} + +.diagram-link { + fill: none; + stroke: var(--rw-primary); + stroke-width: 2.25; +} + +.diagram-link-secondary { + stroke: var(--rw-border-strong); + stroke-dasharray: 5 4; +} + +#flow-arrow path, +#erd-arrow path { + fill: var(--rw-primary); +} + +.diagram-node rect, +.erd-entity rect { + fill: var(--rw-surface-muted); + stroke: var(--rw-border-strong); + stroke-width: 1.5; +} + +.diagram-node-accent rect, +.erd-entity-accent rect { + fill: var(--rw-primary-soft); + stroke: var(--rw-primary); +} + +.diagram-node-data rect, +.erd-entity-data rect { + fill: #e7eee9; + stroke: var(--rw-success); +} + +.diagram-node text, +.erd-entity text { + fill: var(--rw-text); + font-family: "Oracle Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + text-anchor: middle; +} + +.diagram-node-title, +.erd-entity text:first-of-type { + font-size: 15px; + font-weight: 800; +} + +.diagram-node-detail, +.erd-entity text + text { + font-size: 13px; +} + +.diagram-node-note { + fill: var(--rw-muted) !important; + font-size: 11px; +} + +.product-help-sql { + background: var(--rw-primary-soft); + border: 1px solid color-mix(in srgb, var(--rw-primary) 26%, var(--rw-border)); + border-radius: 8px; + padding: .85rem; +} + +.dashboard-menu-flow { + background: var(--rw-surface); + border: 1px solid var(--rw-border); + border-radius: 12px; + box-shadow: var(--rw-shadow); + margin-bottom: 1.25rem; + padding: 1.2rem; +} + +.dashboard-section-heading h2 { + font-size: 1.12rem; + font-weight: 800; + margin: .25rem 0 0; +} + +.dashboard-flow-grid { + display: grid; + gap: .9rem; + grid-template-columns: repeat(4, minmax(0, 1fr)); + margin-top: .9rem; +} + +.dashboard-flow-card { + background: var(--rw-surface-muted); + border: 1px solid var(--rw-border); + border-radius: 8px; + color: var(--rw-text); + min-height: 6.4rem; + padding: .9rem; + position: relative; + text-decoration: none; +} + +.dashboard-flow-card:hover, +.dashboard-flow-card:focus { + border-color: var(--rw-primary); + color: var(--rw-text); +} + +.dashboard-flow-card strong, +.dashboard-flow-card span { + display: block; +} + +.dashboard-flow-card strong { + font-size: .95rem; + font-weight: 800; +} + +.dashboard-flow-card span { + color: var(--rw-muted); + font-size: .8rem; + line-height: 1.4; + margin-top: .3rem; +} + +.dashboard-flow-card:not(:last-child)::after { + color: var(--rw-primary); + content: "→"; + font-size: 1.1rem; + font-weight: 800; + position: absolute; + right: -.72rem; + top: calc(50% - .7rem); + z-index: 1; +} + .dashboard-command-center { background: var(--rw-surface); border: 1px solid var(--rw-border); @@ -2353,6 +2572,12 @@ body { } @media (max-width: 920px) { + .rw-nav-utility { + justify-content: flex-start; + margin-left: 0 !important; + width: 100%; + } + .dashboard-hero, .dashboard-command-center { grid-template-columns: 1fr; @@ -2371,12 +2596,25 @@ body { grid-template-columns: repeat(3, minmax(0, 1fr)); } + .journey-grid, + .dashboard-flow-grid, + .menu-role-grid, + .macro-micro-grid, + .default-vpd-form { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .journey-grid, .macro-micro-grid, .default-vpd-form { grid-template-columns: 1fr; } + .dashboard-flow-card:nth-child(2)::after, + .dashboard-flow-card::after { + display: none; + } + .guided-check-intro { align-items: flex-start; flex-direction: column; @@ -2394,7 +2632,9 @@ body { } .dashboard-action-grid, - .dashboard-state dl { + .dashboard-state dl, + .dashboard-flow-grid, + .menu-role-grid { grid-template-columns: 1fr; } diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html index b00d922..2227da5 100644 --- a/src/main/resources/templates/dashboard.html +++ b/src/main/resources/templates/dashboard.html @@ -7,15 +7,83 @@
VPD 권한 운영 -

권한 관리

+

권한 운영 흐름

+

업무 사용자와 데이터 접근 기준을 관리하고, DB가 적용한 결과까지 확인합니다.

- 도움말 -

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

+ 도움말: 메뉴와 권한 적용 구조 보기 +
+
+

왜 권한 테이블을 따로 관리하나요?

+

DB 연결은 VPD/DDS 실행 계정 하나로 유지하되, 실제 업무 사용자는 CB_APP_USER와 역할·권한 테이블에서 찾습니다. 요청마다 토큰이 사용자를 식별하고 VPD가 그 사용자의 권한만 조건으로 계산하므로, 같은 실행 계정으로도 사용자마다 다른 행과 컬럼을 안전하게 반환할 수 있습니다.

+
+ +
+

메뉴 안내

+ +
+ +
+

요청마다 권한이 적용되는 흐름

+
+ + VPD 권한 적용 흐름 + Bearer Token 요청이 공용 실행 계정과 사용자 컨텍스트를 거쳐 권한 테이블에서 계산한 VPD 조건으로 보호 데이터를 조회하는 흐름 + + + + + + + 요청·토큰Bearer Token + 공용 실행 계정CB_ORDS한 개의 DB 연결 + 사용자 컨텍스트CB_AGENT_CTX사용자별로 설정 + VPD 조건 계산ALLOW · DENY · 행 · 열 + 보호 데이터 조회허용된 행·컬럼만 반환 + 사용자 · 역할 · 권한 테이블요청 시 동적으로 조회 + +
+
+ +
+

권한 데이터 모델

+
+ + VPD 권한 ERD + 사용자와 그룹에서 역할을 얻고 역할에서 권한과 권한 규칙을 거쳐 보호 대상으로 연결되는 데이터 모델 + + + + + CB_APP_USER업무 사용자 + CB_USER_ROLE직접 역할 연결 + CB_APP_ROLE역할 + CB_PERMISSION객체 접근 + CB_PERMISSION_RULE행·열 조건 + 보호 대상TABLE / VIEW + CB_USER_GROUP그룹 소속 + CB_GROUP사용자 그룹 + CB_GROUP_ROLE그룹 역할 연결 + CB_AGENT_BEARER_KEY토큰 → 사용자 식별 + +
+
+ +
+

실행 SQL은 어디에서 확인하나요?

+

실행 요청 SQL은 현재 사용자 컨텍스트와 VPD 조건을 결합한 읽기용 재현 SQL입니다. 실제 DB cursor가 기록한 원문 SQL과 Predicate Information은 V$SQL·DBMS_XPLAN 권한이 있을 때 접근 검증 결과에서 함께 표시됩니다. Oracle은 VPD가 내부적으로 붙인 최종 rewrite 문자열 자체를 별도 SQL 텍스트로 보관하지 않으므로, 실제 적용 근거는 실행계획의 Predicate Information으로 확인합니다.

+
+
@@ -25,9 +93,22 @@
./run.sh backoffice-support
+
+
+ 업무 흐름 + +
+ +
+
- 상세 현황 + 등록 대상 상세 -
- 최근 SQL_ID를 찾지 못했습니다. -
-
-

권한 조건 재현 SQL

+

실행 요청 SQL

- ORDS Handler가 반환한 VPD predicate 또는 같은 토큰 컨텍스트를 재현해 조회한 predicate를 기본 조회문에 합친 형태입니다. + th:text="${vectorSearch ? 'VECTOR_DISTANCE 검색과 현재 사용자의 역할 기반 VPD 조건을 결합한 읽기용 SQL입니다.' : '현재 사용자 컨텍스트에서 계산한 VPD 조건을 ORDS 조회문에 결합한 읽기용 SQL입니다.'}"> + 현재 사용자 컨텍스트에서 계산한 VPD 조건을 ORDS 조회문에 결합한 읽기용 SQL입니다.

DBMS_RLS predicate @@ -193,15 +188,20 @@ ROWNUM <= :row_limit

선택한 사용자의 직접 역할·그룹 상속 역할에 연결된 permission rule에서 계산됩니다. 기본 whitelist 역할의 ALL은 추가 행 필터 없이 조회를 허용하고, 실제로 TAG·부서 조건을 등록한 역할만 그 조건이 SQL에 들어갑니다. 권한이 없으면 1 = 0입니다.

-

권한 적용 SQL

+

실행 요청 SQL (재현)

SELECT ... WHERE (...) AND ROWNUM <= ...

- 위 SQL은 권한 조건을 읽기 쉽게 재현한 표현입니다. 실제 실행 증적은 SQL_ID와 DBMS_XPLAN 영역에서 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다. + 이 SQL은 현재 요청의 권한 조건을 읽기 쉽게 재현한 표현입니다. 실제 DB cursor 원문과 적용 predicate는 SQL_ID·DBMS_XPLAN 증적에서 확인합니다. 컬럼 마스킹은 별도 Redaction 정책입니다.

+
+ 실제 DB cursor SQL + 최근 SQL_ID를 찾지 못했습니다. +
+
이 Handler는 SQL trace 정보를 반환하지 않았고 운영 DB에서도 기본 VPD predicate를 조회하지 못했습니다. 조회 연동에서 trace가 포함된 소스를 적용하고, CB_AGENT_DOC_VPD_FILTER 실행 권한을 확인하세요.
diff --git a/src/main/resources/templates/probe.html b/src/main/resources/templates/probe.html index f0efaf8..ae2215c 100644 --- a/src/main/resources/templates/probe.html +++ b/src/main/resources/templates/probe.html @@ -8,7 +8,7 @@

접근 검증

도움말 -

한 사용자의 토큰으로 실제 데이터를 요청해, 설계한 권한이 DB에서 그대로 적용되는지 확인합니다.

+

한 사용자의 토큰으로 실제 데이터를 요청해, 설계한 권한이 DB에서 그대로 적용되는지 확인합니다. 결과의 실행 요청 SQL은 현재 컨텍스트를 결합한 읽기용 표현이며, DB cursor가 기록한 원문 SQL은 V$SQL·DBMS_XPLAN 진단 권한이 있을 때만 함께 표시됩니다.

diff --git a/src/main/resources/templates/vpd-policies.html b/src/main/resources/templates/vpd-policies.html index 245d1e9..c6c5843 100644 --- a/src/main/resources/templates/vpd-policies.html +++ b/src/main/resources/templates/vpd-policies.html @@ -235,7 +235,7 @@ -

모든 대상에 표준 CB_PERMISSION_SELECT_POLICYCB_AGENT_DOC_VPD_FILTER를 적용합니다. 별도 predicate가 필요하면 운영·고급 메뉴의 가이드를 먼저 확인하세요.

+

모든 대상에 표준 CB_PERMISSION_SELECT_POLICYCB_AGENT_DOC_VPD_FILTER를 적용합니다. 별도 predicate가 필요하면 관리자 메뉴의 고급 접근 조건 가이드를 먼저 확인하세요.

diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java index 49e570f..a2752d4 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/web/GuidedFlowTemplateTest.java @@ -18,11 +18,18 @@ class GuidedFlowTemplateTest { assertThat(html) .contains("th:replace=\"~{fragments/layout :: head('VPD 권한 운영')}\"") - .contains("권한 관리") + .contains("권한 운영 흐름") + .contains("업무 흐름") + .contains("dashboard-menu-flow") + .contains("도움말: 메뉴와 권한 적용 구조 보기") + .contains("CB_APP_USER") + .contains("CB_ORDS") + .contains("CB_AGENT_CTX") + .contains("권한 데이터 모델") .contains("접근 검증") .contains("접근 규칙") .contains("현재 상태") - .contains("상세 현황") + .contains("등록 대상 상세") .contains("
") .doesNotContain("journey-card") .doesNotContain("dashboard-workflow") @@ -32,11 +39,13 @@ class GuidedFlowTemplateTest { .contains("권한 관리") .contains("보호·검증") .contains("연동 도구") - .contains("운영·고급") + .contains("운영") + .contains("관리자") .contains("조회 연동") .contains("MCP 서비스") .contains("고급 접근 조건") .contains("시스템 설정") + .contains("DB 준비 상태") .contains("data-submenu-trigger") .contains("data-submenu-bar") .contains("data-submenu-panel") @@ -101,7 +110,8 @@ class GuidedFlowTemplateTest { assertThat(result) .contains("적용된 사용자와 권한") .contains("DB 실행 증적") - .contains("권한 조건 재현 SQL") + .contains("실행 요청 SQL") + .contains("실제 DB cursor SQL") .contains("set_vpd_context 사용자 컨텍스트") .contains("CB_AGENT_CTX.USER_ID") .contains("벡터 유사도 검색 기준") @@ -258,8 +268,9 @@ class GuidedFlowTemplateTest { .contains("DB가 기록한 원문 SQL") .contains("DBMS_XPLAN Predicate Information") .contains("SQL_ID") - .contains("권한 조건 재현 SQL") - .contains("권한 적용 SQL") + .contains("실행 요청 SQL") + .contains("실제 DB cursor SQL") + .contains("실행 요청 SQL (재현)") .contains("VPD가 추가한 WHERE 조건") .contains("ALL: 추가 행 필터 없음") .contains("기본 whitelist 역할");