feat #475: consolidate vpd apply actions

This commit is contained in:
devmrko
2026-06-26 04:59:21 +09:00
parent 62f8abce5f
commit b64f06d5d5
3 changed files with 179 additions and 2 deletions

View File

@@ -0,0 +1,20 @@
# #475 VPD 적용 화면 통합
## 배경
VPD 적용은 보호 객체 하나를 선택해서 policy를 붙이는 개별 적용이 기본이다. 벌크 적용은 같은 filter/function을 스키마의 여러 TABLE/VIEW에 확장할 때 사용하는 보조 기능이다.
기존 화면은 `/vpd-policies`가 적용 현황 위주이고, 실제 개별/벌크 적용 폼은 `/vpd-filter-policies`에 있어 운영 흐름이 맞지 않았다.
## 결정
- `/vpd-policies`를 VPD 적용의 중심 화면으로 둔다.
- 개별 적용 폼을 먼저 배치한다.
- 벌크 적용은 같은 화면의 접힌 영역으로 둔다.
- `/vpd-filter-policies`는 filter function 등록/수정과 고급 policy 수정 화면으로 유지한다.
## 완료 기준
- 운영자가 VPD 설정 화면에서 바로 개별 적용을 수행할 수 있다.
- 같은 화면에서 필요 시 벌크 적용을 펼쳐 실행할 수 있다.
- 기존 controller/service API는 유지한다.

View File

@@ -449,6 +449,12 @@ body {
margin: 0;
}
.bulk-apply-summary {
cursor: pointer;
font-weight: 700;
min-height: 2rem;
}
.question-presets {
display: flex;
flex-wrap: wrap;

View File

@@ -18,10 +18,150 @@
<section class="content-band">
<div class="section-heading">
<h2>적용 현황</h2>
<h2>VPD 적용</h2>
<a class="btn btn-sm rw-btn-primary" href="/vpd-filter-policies">Filter Policy 관리</a>
</div>
<p class="text-muted mb-0">이 화면은 현재 DB에 적용된 VPD policy와 policy function/filter 상세를 조회합니다. 등록과 벌크 적용은 Filter Policy 관리에서 수행합니다.</p>
<p class="text-muted mb-0">VPD는 개별 보호 객체 적용을 기본으로 하고, 같은 정책을 스키마 TABLE/VIEW에 확장할 때만 벌크 적용을 사용합니다.</p>
</section>
<section class="content-band">
<div class="section-heading">
<h2>개별 적용</h2>
</div>
<form method="post" action="/vpd-policies" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
VPD 적용 대상
<select class="form-select" name="objectKey" required>
<option th:each="object : ${objects}"
th:value="${object.owner() + '.' + object.objectName()}"
th:text="${object.displayName() + ' / ' + object.ordsPath()}"></option>
</select>
</label>
<label>
Policy 이름
<input class="form-control" name="policyName" list="vpd-policy-names"
placeholder="예: CB_AGENT_DOC_POLICY" required>
</label>
<label class="span-2">
기존 Function 선택
<select class="form-select" name="functionKey">
<option value="">Filter predicate로 새 function 자동 생성</option>
<option th:each="function : ${formOptions.functions()}"
th:value="${function.value()}"
th:text="${function.label()}"></option>
</select>
</label>
<label>
Function Owner
<select class="form-select" name="functionOwner">
<option value="">현재 연결 사용자</option>
<option th:each="owner : ${formOptions.owners()}"
th:value="${owner}"
th:text="${owner}"></option>
</select>
</label>
<label>
Function 이름
<input class="form-control" name="functionName" list="vpd-function-names"
placeholder="POLICY_NAME_FILTER 자동 생성">
</label>
<div>
Statement Types
<div class="checkbox-row">
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
<input class="form-check-input" type="checkbox" name="statementTypes"
th:value="${statement}" th:checked="${statement == 'SELECT'}">
<span class="form-check-label" th:text="${statement}">SELECT</span>
</label>
</div>
</div>
<div class="form-check align-self-end">
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked>
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>
</div>
<div class="form-check span-2">
<input class="form-check-input" id="vpd-update-check" type="checkbox" name="updateCheck" value="true">
<label class="form-check-label" for="vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
</div>
<label class="span-2">
Filter predicate
<textarea class="form-control" id="vpd-filter-predicate" name="filterPredicate" rows="4"
placeholder="예: dept_code = SYS_CONTEXT(''CB_AGENT_CTX'', ''DEPT_CODE'')&#10;비우면 기존 Function 이름으로 ADD_POLICY만 실행합니다."></textarea>
</label>
<div class="question-presets span-2" aria-label="Filter predicate 예시">
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=0">전체 차단</button>
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="1=1">전체 허용</button>
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="dept_code = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')">부서 일치</button>
<button class="btn rw-btn-secondary question-preset" type="button" data-target="vpd-filter-predicate" data-question="owner_emp_no = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')">본인 소유</button>
</div>
<button class="btn rw-btn-primary" type="submit">개별 객체에 VPD 적용</button>
</form>
<datalist id="vpd-policy-names">
<option th:each="policyName : ${formOptions.policyNames()}" th:value="${policyName}"></option>
</datalist>
<datalist id="vpd-function-names">
<option th:each="function : ${formOptions.functions()}" th:value="${function.functionName()}"></option>
</datalist>
</section>
<section class="content-band">
<details>
<summary class="bulk-apply-summary">벌크 적용</summary>
<form method="post" action="/vpd-policies/bulk" class="form-grid mt-3">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label>
Schema
<select class="form-select" name="schemaOwner" required>
<option th:each="owner : ${formOptions.schemaOwners()}" th:value="${owner}" th:text="${owner}"></option>
</select>
</label>
<div>
Object Types
<div class="checkbox-row">
<label class="form-check">
<input class="form-check-input" type="checkbox" name="includeTables" value="true" checked>
<span class="form-check-label">TABLE</span>
</label>
<label class="form-check">
<input class="form-check-input" type="checkbox" name="includeViews" value="true" checked>
<span class="form-check-label">VIEW</span>
</label>
</div>
</div>
<label class="span-2">
기존 Function 선택
<select class="form-select" name="functionKey">
<option value="">Filter predicate로 벌크 function 자동 생성</option>
<option th:each="function : ${formOptions.functions()}" th:value="${function.value()}" th:text="${function.label()}"></option>
</select>
</label>
<div>
Statement Types
<div class="checkbox-row">
<label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
<input class="form-check-input" type="checkbox" name="statementTypes"
th:value="${statement}" th:checked="${statement == 'SELECT'}">
<span class="form-check-label" th:text="${statement}">SELECT</span>
</label>
</div>
</div>
<div class="form-check align-self-end">
<input class="form-check-input" id="bulk-vpd-enabled" type="checkbox" name="enabled" value="true" checked>
<label class="form-check-label" for="bulk-vpd-enabled">등록 즉시 활성화</label>
</div>
<div class="form-check span-2">
<input class="form-check-input" id="bulk-vpd-update-check" type="checkbox" name="updateCheck" value="true">
<label class="form-check-label" for="bulk-vpd-update-check">INSERT/UPDATE에도 predicate check 적용</label>
</div>
<label class="span-2">
Filter predicate
<textarea class="form-control" id="bulk-vpd-filter-predicate" name="filterPredicate" rows="4"
placeholder="기존 Function을 선택하지 않으면 이 predicate로 스키마 공통 function을 생성합니다."></textarea>
</label>
<button class="btn rw-btn-primary" type="submit">스키마 TABLE/VIEW에 VPD 일괄 적용</button>
</form>
</details>
</section>
<section class="content-band">
@@ -116,5 +256,16 @@
</div>
</section>
</main>
<script>
document.querySelectorAll('.question-preset').forEach((button) => {
button.addEventListener('click', () => {
const target = document.getElementById(button.dataset.target || '');
if (target) {
target.value = button.dataset.question || '';
target.focus();
}
});
});
</script>
</body>
</html>