fix #489: add vpd target filtering

This commit is contained in:
devmrko
2026-06-26 10:13:45 +09:00
parent a906dd0128
commit f39cc49021
4 changed files with 274 additions and 3 deletions

View File

@@ -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;
}

View File

@@ -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 = '<td colspan="7" class="text-muted">필터 조건에 맞는 TABLE/VIEW가 없습니다.</td>';
body.appendChild(empty);
const matchesFilters = (entry) => {
const query = (search?.value || '').trim().toUpperCase();
if (query && !entry.object.includes(query)) {
return false;
}
if (type?.value && entry.objectType !== type.value) {
return false;
}
if (vpd?.value && entry.vpdApplied !== vpd.value) {
return false;
}
if (protectedObject?.value && entry.protectedObject !== protectedObject.value) {
return false;
}
if (!includeSupport?.checked && isBackofficeSupportObject(entry.objectName)) {
return false;
}
return true;
};
const applyFilters = () => {
const maxRows = pageSize?.value === 'ALL' ? Number.POSITIVE_INFINITY : Number(pageSize?.value || 50);
let matched = 0;
let visible = 0;
rows.forEach((entry) => {
const matchedRow = matchesFilters(entry);
if (matchedRow) {
matched += 1;
}
const show = matchedRow && visible < maxRows;
if (show) {
visible += 1;
}
entry.row.hidden = !show;
if (entry.detail) {
entry.detail.hidden = !show;
}
});
empty.hidden = matched !== 0;
if (visibleCount) {
visibleCount.textContent = String(visible);
}
if (matchedCount) {
matchedCount.textContent = String(matched);
}
if (totalCount) {
totalCount.textContent = String(rows.length);
}
};
[search, type, vpd, protectedObject, includeSupport, pageSize].forEach((control) => {
control?.addEventListener(control === search ? 'input' : 'change', applyFilters);
});
reset?.addEventListener('click', () => {
if (search) {
search.value = '';
}
if (type) {
type.value = '';
}
if (vpd) {
vpd.value = '';
}
if (protectedObject) {
protectedObject.value = '';
}
if (includeSupport) {
includeSupport.checked = false;
}
if (pageSize) {
pageSize.value = '50';
}
applyFilters();
search?.focus();
});
applyFilters();
}
document.addEventListener('DOMContentLoaded', () => {
initPersistentMenus();
renderMarkdownViews();
initVpdTargetFilters();
const master = document.getElementById('userRoleMaster');
if (master) {
master.addEventListener('change', filterUserRoleDetail);

View File

@@ -46,6 +46,56 @@
<option value="">관리 대상 스키마</option>
<option th:each="owner : ${formOptions.schemaOwners()}" th:value="${owner}"></option>
</datalist>
<div class="vpd-target-controls" data-vpd-target-controls th:if="${!#lists.isEmpty(vpdTargets)}">
<label>
Object 검색
<input class="form-control form-control-sm" type="search" placeholder="예: BOARD, CUSTOMER"
data-vpd-target-search>
</label>
<label>
Type
<select class="form-select form-select-sm" data-vpd-target-type>
<option value="">전체</option>
<option value="TABLE">TABLE</option>
<option value="VIEW">VIEW</option>
</select>
</label>
<label>
VPD 상태
<select class="form-select form-select-sm" data-vpd-target-vpd>
<option value="">전체</option>
<option value="true">VPD 적용됨</option>
<option value="false">미적용</option>
</select>
</label>
<label>
권한 등록
<select class="form-select form-select-sm" data-vpd-target-protected>
<option value="">전체</option>
<option value="true">등록됨</option>
<option value="false">미등록</option>
</select>
</label>
<label>
표시
<select class="form-select form-select-sm" data-vpd-target-page-size>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="ALL">전체</option>
</select>
</label>
<label class="form-check vpd-support-toggle">
<input class="form-check-input" type="checkbox" data-vpd-target-include-support>
<span class="form-check-label">백오피스 지원 테이블 포함</span>
</label>
<button class="btn btn-sm rw-btn-secondary" type="button" data-vpd-target-reset>초기화</button>
<div class="vpd-target-count" aria-live="polite">
표시 <strong data-vpd-target-visible>0</strong>
/ 일치 <strong data-vpd-target-matched>0</strong>
/ 전체 <span data-vpd-target-total th:text="${#lists.size(vpdTargets)}">0</span>
</div>
</div>
<div class="table-responsive">
<table class="table table-sm align-middle">
<thead>
@@ -59,9 +109,14 @@
<th>검증용 ORDS 경로</th>
</tr>
</thead>
<tbody>
<tbody data-vpd-target-body>
<th:block th:each="target, iter : ${vpdTargets}">
<tr>
<tr data-vpd-target-row
th:attr="data-object=${target.objectDisplayName()},
data-object-name=${target.objectName()},
data-object-type=${target.objectType()},
data-vpd-applied=${target.vpdApplied()},
data-protected-object=${target.protectedObject()}">
<td><code th:text="${target.objectDisplayName()}">ADMIN.TABLE</code></td>
<td th:text="${target.objectType()}">TABLE</td>
<td>
@@ -98,7 +153,7 @@
<span th:unless="${target.ordsPath()}" class="text-muted">검증 경로 미등록</span>
</td>
</tr>
<tr th:if="${target.vpdApplied()}">
<tr th:if="${target.vpdApplied()}" data-vpd-target-detail>
<td colspan="7" class="policy-source-cell">
<div th:id="${'target-filter-detail-' + iter.index}" class="text-muted small">
Filter 이름을 클릭하면 policy/filter source가 표시됩니다.