Files
vpd-permission-poc/src/main/resources/static/js/app.js
2026-06-30 13:23:49 +09:00

1040 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
document.body.addEventListener('htmx:responseError', (event) => {
const target = event.detail.target;
if (target) {
target.innerHTML = '<div class="alert alert-danger">요청 처리 중 오류가 발생했습니다.</div>';
}
});
function resetFilterToggle(button, target) {
if (!button || !target) {
return;
}
button.setAttribute('aria-expanded', 'false');
button.classList.remove('active');
target.innerHTML = button.dataset.toggleDefaultText || 'Filter 이름을 클릭하면 policy/filter source가 표시됩니다.';
}
function setFilterToggleLoading(button, target) {
if (!button || !target) {
return;
}
button.setAttribute('aria-expanded', 'true');
button.classList.add('active');
target.innerHTML = button.dataset.toggleLoadingText || 'policy/filter source를 조회하는 중입니다.';
}
function findHtmxTarget(button) {
const selector = button?.getAttribute('hx-target') || button?.getAttribute('data-hx-target');
return selector ? document.querySelector(selector) : null;
}
function syncPolicyTemplate(select) {
const form = select?.closest('form');
if (!form) {
return;
}
const option = select.options[select.selectedIndex];
const hasTemplate = Boolean(option?.dataset.policyName);
const policyName = form.querySelector('[data-policy-template-field="policyName"]');
const functionKey = form.querySelector('[data-policy-template-field="functionKey"]');
const enabled = form.querySelector('[data-policy-template-field="enabled"]');
const updateCheck = form.querySelector('[data-policy-template-field="updateCheck"]');
if (policyName) {
policyName.value = option?.dataset.policyName || '';
}
if (functionKey) {
functionKey.value = option?.dataset.functionKey || '';
}
if (enabled) {
enabled.checked = hasTemplate ? option?.dataset.enabled === 'true' : true;
}
if (updateCheck) {
updateCheck.checked = hasTemplate ? option?.dataset.updateCheck === 'true' : false;
}
const selectedStatements = (hasTemplate ? option?.dataset.statementTypes || '' : 'SELECT')
.split(',')
.map((value) => value.trim())
.filter(Boolean);
form.querySelectorAll('[data-policy-template-statement]').forEach((checkbox) => {
checkbox.checked = selectedStatements.includes(checkbox.value);
});
const preview = (name, value) => {
const target = form.querySelector(`[data-policy-template-preview="${name}"]`);
if (target) {
target.textContent = value;
}
};
preview('policyName', option?.dataset.policyName || '선택 전');
preview('functionKey', option?.dataset.functionKey || '선택 전');
preview('statementTypes', selectedStatements.join(', ') || 'SELECT');
preview(
'options',
`Enabled: ${enabled?.checked ? 'YES' : 'NO'} / Check: ${updateCheck?.checked ? 'YES' : 'NO'}`
);
}
function closeMenuGroup(group) {
if (!group) {
return;
}
group.classList.remove('open');
group.querySelector('.rw-menu-trigger')?.setAttribute('aria-expanded', 'false');
}
function closeAllMenuGroups(exceptGroup = null) {
document.querySelectorAll('.rw-menu-group.open').forEach((group) => {
if (group !== exceptGroup) {
closeMenuGroup(group);
}
});
}
function initPersistentMenus() {
document.querySelectorAll('.rw-menu-trigger').forEach((button) => {
button.addEventListener('click', (event) => {
event.stopPropagation();
const group = button.closest('.rw-menu-group');
if (!group) {
return;
}
const nextOpen = !group.classList.contains('open');
closeAllMenuGroups(group);
group.classList.toggle('open', nextOpen);
button.setAttribute('aria-expanded', String(nextOpen));
});
});
document.addEventListener('click', (event) => {
if (!event.target.closest('.rw-menu')) {
closeAllMenuGroups();
}
});
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeAllMenuGroups();
}
});
}
function initQuestionPresets(root = document) {
root.querySelectorAll('.question-preset[data-target]').forEach((button) => {
if (button.dataset.presetReady === 'true') {
return;
}
button.dataset.presetReady = 'true';
button.addEventListener('click', () => {
const target = document.getElementById(button.dataset.target || '');
if (target) {
target.value = button.dataset.question || '';
target.focus();
}
});
});
}
function initCopyButtons(root = document) {
root.querySelectorAll('[data-copy-source]').forEach((button) => {
if (button.dataset.copyReady === 'true') {
return;
}
button.dataset.copyReady = 'true';
button.addEventListener('click', async () => {
const source = document.getElementById(button.dataset.copySource || '');
const value = source?.textContent?.trim();
if (!value) {
return;
}
try {
await navigator.clipboard.writeText(value);
button.textContent = '복사했습니다';
} catch (error) {
button.textContent = '복사 실패 · 직접 선택하세요';
}
});
});
}
function escapeHtml(value) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function renderInlineMarkdown(value) {
return escapeHtml(value)
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
.replace(/`([^`]+)`/g, '<code>$1</code>');
}
function isMarkdownTable(lines, index) {
if (index + 1 >= lines.length) {
return false;
}
return lines[index].trim().startsWith('|')
&& lines[index + 1].trim().startsWith('|')
&& /^\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?$/.test(lines[index + 1].trim());
}
function splitMarkdownTableRow(line) {
return line.trim()
.replace(/^\|/, '')
.replace(/\|$/, '')
.split('|')
.map((cell) => cell.trim());
}
function renderMarkdownTable(lines, start) {
const headers = splitMarkdownTableRow(lines[start]);
let cursor = start + 2;
const rows = [];
while (cursor < lines.length && lines[cursor].trim().startsWith('|')) {
rows.push(splitMarkdownTableRow(lines[cursor]));
cursor += 1;
}
const headerHtml = headers.map((cell) => `<th>${renderInlineMarkdown(cell)}</th>`).join('');
const bodyHtml = rows.map((row) => {
const cells = headers.map((_, index) => `<td>${renderInlineMarkdown(row[index] || '')}</td>`).join('');
return `<tr>${cells}</tr>`;
}).join('');
return {
html: `<table><thead><tr>${headerHtml}</tr></thead><tbody>${bodyHtml}</tbody></table>`,
next: cursor
};
}
function renderMarkdownText(markdown) {
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
const html = [];
let listType = null;
const closeList = () => {
if (listType) {
html.push(`</${listType}>`);
listType = null;
}
};
for (let i = 0; i < lines.length; i += 1) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed) {
closeList();
continue;
}
if (isMarkdownTable(lines, i)) {
closeList();
const table = renderMarkdownTable(lines, i);
html.push(table.html);
i = table.next - 1;
continue;
}
const heading = trimmed.match(/^(#{2,4})\s+(.+)$/);
if (heading) {
closeList();
const level = Math.min(heading[1].length, 3);
html.push(`<h${level}>${renderInlineMarkdown(heading[2])}</h${level}>`);
continue;
}
const unordered = trimmed.match(/^[-*]\s+(.+)$/);
if (unordered) {
if (listType !== 'ul') {
closeList();
listType = 'ul';
html.push('<ul>');
}
html.push(`<li>${renderInlineMarkdown(unordered[1])}</li>`);
continue;
}
const ordered = trimmed.match(/^\d+\.\s+(.+)$/);
if (ordered) {
if (listType !== 'ol') {
closeList();
listType = 'ol';
html.push('<ol>');
}
html.push(`<li>${renderInlineMarkdown(ordered[1])}</li>`);
continue;
}
closeList();
html.push(`<p>${renderInlineMarkdown(trimmed)}</p>`);
}
closeList();
return html.join('');
}
function renderMarkdownViews(root = document) {
root.querySelectorAll('[data-markdown-view]').forEach((view) => {
if (view.dataset.rendered === 'true') {
return;
}
view.innerHTML = renderMarkdownText(view.textContent || '');
view.dataset.rendered = 'true';
});
}
function filterUserRoleDetail() {
const master = document.getElementById('userRoleMaster');
const table = document.getElementById('userRoleDetail');
if (!master || !table) {
return;
}
const selected = master.value;
let shown = 0;
table.querySelectorAll('tbody tr[data-user-id]').forEach((row) => {
const visible = row.dataset.userId === selected;
row.hidden = !visible;
shown += visible ? 1 : 0;
});
let empty = table.querySelector('tbody tr.empty-user-role-runtime');
if (!empty) {
empty = document.createElement('tr');
empty.className = 'empty-user-role-runtime';
empty.innerHTML = '<td colspan="3" class="text-muted">선택한 사용자에게 부여된 역할이 없습니다.</td>';
table.querySelector('tbody').appendChild(empty);
}
empty.hidden = shown !== 0;
}
function filterGroupDetail(masterId) {
const master = document.getElementById(masterId);
const table = document.querySelector(`[data-group-detail-table="${masterId}"]`);
if (!master || !table) {
return;
}
const selected = master.value;
let shown = 0;
table.querySelectorAll('tbody tr[data-group-id]').forEach((row) => {
const visible = row.dataset.groupId === selected;
row.hidden = !visible;
shown += visible ? 1 : 0;
});
let empty = table.querySelector('tbody tr.empty-group-detail-runtime');
if (!empty) {
empty = document.createElement('tr');
empty.className = 'empty-group-detail-runtime';
empty.innerHTML = `<td colspan="3" class="text-muted">${escapeHtml(table.dataset.emptyMessage || '선택한 그룹에 등록된 항목이 없습니다.')}</td>`;
table.querySelector('tbody').appendChild(empty);
}
empty.hidden = shown !== 0;
}
function renderRuleColumnOptions(columns) {
document.querySelectorAll('.rule-column-select').forEach((select) => {
const current = select.value;
select.innerHTML = '<option value="">기본 컬럼</option>' + columns
.map((column) => `<option value="${column}">${column}</option>`)
.join('');
if (columns.includes(current)) {
select.value = current;
}
});
}
function renderMaskableColumnOptions(option) {
const list = document.querySelector('[data-maskable-column-list]');
if (!list) {
return;
}
const columns = (option?.dataset.maskableColumns || '').split(',').filter(Boolean);
const labels = (option?.dataset.maskableLabels || '').split('|').filter(Boolean);
if (!columns.length) {
list.innerHTML = '<span class="text-muted small">선택한 객체에 권한별로 허용할 마스킹 컬럼이 없습니다.</span>';
return;
}
list.innerHTML = columns.map((column, index) => (
`<button class="btn btn-sm btn-outline-secondary question-preset" type="button" data-mask-column="${column}">`
+ `${escapeHtml(labels[index] || column)}</button>`
)).join('');
list.querySelectorAll('[data-mask-column]').forEach((button) => {
button.addEventListener('click', () => {
const input = document.querySelector('input[name="visibleColumns"]');
if (!input) {
return;
}
const current = input.value.split(',').map((value) => value.trim()).filter(Boolean);
const column = button.dataset.maskColumn;
if (!current.includes(column)) {
current.push(column);
}
input.value = current.join(', ');
renderSelectedVisibleColumns();
updatePermissionWizardPreview(document);
});
});
}
function renderSelectedVisibleColumns() {
const input = document.querySelector('input[name="visibleColumns"]');
const list = document.querySelector('[data-selected-visible-columns]');
if (!input || !list) {
return;
}
const columns = input.value.split(',').map((value) => value.trim().toUpperCase()).filter(Boolean);
list.innerHTML = columns.map((column) => (
`<button type="button" class="badge text-bg-secondary border-0 me-1 mb-1" data-remove-visible-column="${column}">`
+ `${escapeHtml(column)} ×</button>`
)).join('');
list.querySelectorAll('[data-remove-visible-column]').forEach((button) => {
button.addEventListener('click', () => {
const remove = button.dataset.removeVisibleColumn;
input.value = columns.filter((column) => column !== remove).join(', ');
renderSelectedVisibleColumns();
updatePermissionWizardPreview(document);
});
});
}
function syncRuleTypeHints(root = document) {
root.querySelectorAll('.rule-row').forEach((row) => {
const typeSelect = row.querySelector('.rule-type-select');
const columnSelect = row.querySelector('.rule-column-select');
const valueInput = row.querySelector('input[name="ruleValue"]');
if (!typeSelect || !columnSelect || !valueInput) {
return;
}
const type = typeSelect.value;
const valueRequired = ['=', '!=', 'DEPT', 'EMP_NO', 'TAG'].includes(type);
const columnOptional = ['ALL', 'MY_DEPT', 'SELF', 'DEPT', 'EMP_NO', 'TAG'].includes(type);
const placeholderByType = {
ALL: '값 불필요',
MY_DEPT: '값 불필요',
SELF: '값 불필요',
DEPT: '예: HR',
EMP_NO: '예: E2001',
TAG: '예: SPRING_BOOT',
'=': '비교 값',
'!=': '비교 값'
};
columnSelect.required = !columnOptional;
valueInput.required = valueRequired;
valueInput.disabled = ['ALL', 'MY_DEPT', 'SELF'].includes(type);
valueInput.placeholder = placeholderByType[type] || '비교 값';
if (valueInput.disabled) {
valueInput.value = '';
}
});
}
async function syncRuleColumnOptions() {
const objectSelect = document.querySelector('select[name="objectRef"]');
if (!objectSelect) {
return;
}
const selectedOption = objectSelect.options[objectSelect.selectedIndex];
const fallbackColumns = (selectedOption?.dataset.columns || '').split(',').filter(Boolean);
const applyColumns = (columns) => {
if (selectedOption) {
selectedOption.dataset.columns = columns.join(',');
}
renderRuleColumnOptions(columns);
updatePermissionWizardPreview(document);
};
renderMaskableColumnOptions(selectedOption);
applyColumns(fallbackColumns);
try {
const response = await fetch(`/permissions/object-columns?objectRef=${encodeURIComponent(objectSelect.value)}`);
if (!response.ok) {
return;
}
const columns = await response.json();
applyColumns(Array.isArray(columns) ? columns : fallbackColumns);
} catch (error) {
applyColumns(fallbackColumns);
}
}
async function syncObjectCatalogSelection() {
const catalog = document.getElementById('objectCatalogSelect');
if (!catalog || !catalog.value) {
return;
}
const selected = catalog.options[catalog.selectedIndex];
const ownerInput = document.querySelector('input[name="owner"]');
const objectInput = document.querySelector('input[name="objectName"]');
const ordsPathInput = document.querySelector('input[name="ordsPath"]');
if (ownerInput) {
ownerInput.value = selected.dataset.owner || '';
}
if (objectInput) {
objectInput.value = selected.dataset.objectName || '';
}
if (ordsPathInput) {
const owner = (selected.dataset.owner || '').toLowerCase();
const objectName = (selected.dataset.objectName || '').toLowerCase();
ordsPathInput.value = owner && objectName ? `cb-ords/cb-object-query/${owner}/${objectName}` : '';
ordsPathInput.readOnly = true;
}
const ordsPathEdit = document.querySelector('[data-ords-path-edit]');
if (ordsPathEdit) {
ordsPathEdit.checked = false;
}
}
function initOrdsPathEditToggle() {
const toggle = document.querySelector('[data-ords-path-edit]');
const input = document.querySelector('[data-ords-path-input]');
if (!toggle || !input) {
return;
}
toggle.addEventListener('change', () => {
input.readOnly = !toggle.checked;
if (toggle.checked) {
input.focus();
}
});
}
function initProbeObjectDescription() {
const select = document.querySelector('select[name="objectId"]');
const target = document.querySelector('[data-probe-object-description]');
if (!select || !target) {
return;
}
const update = () => {
const option = selectedOption(select);
target.textContent = option?.dataset.description
|| '선택한 대상의 설명이 등록되지 않았습니다. ORDS 조회 대상에서 설명을 추가하세요.';
};
select.addEventListener('change', update);
update();
}
function initProbeVectorInput() {
const select = document.querySelector('select[name="objectId"]');
const blocks = document.querySelectorAll('[data-vector-probe-input]');
const input = document.querySelector('[data-vector-probe-input] textarea[name="requestBody"]');
const controls = document.querySelectorAll('[data-vector-probe-input] textarea, [data-vector-probe-input] select, [data-vector-probe-input] input');
if (!select || !blocks.length || !input) {
return;
}
const update = () => {
const option = selectedOption(select);
const isVectorSearch = option?.dataset.vectorSearch === 'true';
blocks.forEach(block => {
block.hidden = !isVectorSearch;
});
controls.forEach(control => {
control.disabled = !isVectorSearch;
});
input.required = isVectorSearch;
if (!isVectorSearch) {
input.value = '';
}
};
select.addEventListener('change', update);
update();
}
function selectedText(select) {
if (!select || select.selectedIndex < 0) {
return '-';
}
return select.options[select.selectedIndex].textContent.trim();
}
function selectedOption(select) {
if (!select || select.selectedIndex < 0) {
return null;
}
return select.options[select.selectedIndex];
}
function splitList(value, separator = '|') {
return (value || '')
.split(separator)
.map((item) => item.trim())
.filter(Boolean);
}
function formatList(values, emptyText) {
if (!values.length) {
return emptyText;
}
const shown = values.slice(0, 4).join(', ');
return values.length > 4 ? `${shown}${values.length - 4}` : shown;
}
function setWizardPreview(wizard, name, value) {
wizard.querySelectorAll(`[data-preview="${name}"]`).forEach((target) => {
target.textContent = value;
});
}
function setTokenPreview(preview, name, value) {
preview.querySelectorAll(`[data-token-preview="${name}"]`).forEach((target) => {
target.textContent = value;
});
}
function updateTokenContextPreview(select) {
const form = select.closest('form');
const preview = form?.querySelector('[data-token-context-preview]');
if (!preview) {
return;
}
const option = selectedOption(select);
if (!option || !option.value) {
setTokenPreview(preview, 'status', '미선택');
setTokenPreview(preview, 'username', '원문 직접 입력');
setTokenPreview(preview, 'prefix', '-');
setTokenPreview(preview, 'expiresAt', '-');
setTokenPreview(preview, 'directRoles', '-');
setTokenPreview(preview, 'groups', '-');
setTokenPreview(preview, 'inheritedRoles', '-');
setTokenPreview(preview, 'description', '토큰을 선택하면 등록된 사용자/역할 컨텍스트를 먼저 확인할 수 있습니다.');
return;
}
const directRoles = splitList(option.dataset.directRoles || '');
const groups = splitList(option.dataset.groups || '');
const inheritedRoles = splitList(option.dataset.inheritedRoles || '');
setTokenPreview(preview, 'status', option.dataset.status || '-');
setTokenPreview(preview, 'username', option.dataset.username || '-');
setTokenPreview(preview, 'prefix', option.dataset.prefix || '****');
setTokenPreview(preview, 'expiresAt', option.dataset.expiresAt || '-');
setTokenPreview(preview, 'directRoles', formatList(directRoles, '직접 역할 없음'));
setTokenPreview(preview, 'groups', formatList(groups, '소속 그룹 없음'));
setTokenPreview(preview, 'inheritedRoles', formatList(inheritedRoles, '그룹 상속 역할 없음'));
setTokenPreview(preview, 'description', option.dataset.description || '설명 없음');
}
function initTokenContextPreviews() {
document.querySelectorAll('[data-token-context-select]').forEach((select) => {
select.addEventListener('change', () => updateTokenContextPreview(select));
updateTokenContextPreview(select);
});
}
function filterEffectiveMatrixTable(select) {
const table = document.getElementById(select.dataset.effectiveFilter || '');
if (!table) {
return;
}
const value = select.value || '';
table.querySelectorAll('.effective-empty-row').forEach((row) => row.remove());
let visibleRows = 0;
table.querySelectorAll('tbody tr[data-effective-id]').forEach((row) => {
const visible = !value || row.dataset.effectiveId === value;
row.hidden = !visible;
if (visible) {
visibleRows += 1;
}
});
if (value && visibleRows === 0) {
const body = table.querySelector('tbody');
const columnCount = table.querySelectorAll('thead th').length || 1;
const row = document.createElement('tr');
row.className = 'effective-empty-row';
row.innerHTML = `<td colspan="${columnCount}" class="text-muted">${table.dataset.emptyMessage || '선택한 항목이 없습니다.'}</td>`;
body?.appendChild(row);
}
}
function initEffectiveMatrixFilters() {
document.querySelectorAll('[data-effective-filter]').forEach((select) => {
select.addEventListener('change', () => filterEffectiveMatrixTable(select));
filterEffectiveMatrixTable(select);
});
}
function sqlLiteral(value) {
return `'${String(value || '').replaceAll("'", "''")}'`;
}
function collectWizardRules(root) {
return Array.from(root.querySelectorAll('.rule-row')).map((row) => {
const column = row.querySelector('[name="ruleColumn"]')?.value || '';
const type = row.querySelector('[name="ruleType"]')?.value || '';
const value = row.querySelector('[name="ruleValue"]')?.value || '';
const displayColumn = column || {
MY_DEPT: 'DEPT_CODE',
SELF: 'OWNER_EMP_NO',
DEPT: 'DEPT_CODE',
EMP_NO: 'OWNER_EMP_NO',
TAG: 'TECH_TAG',
ALL: ''
}[type] || '';
if (type === 'ALL') {
return 'ALL';
}
if (['MY_DEPT', 'SELF'].includes(type)) {
return `${displayColumn} ${type}`;
}
if (type === 'TAG') {
return `${displayColumn} TAG ${value.toUpperCase()}`;
}
return [displayColumn, type, value].filter(Boolean).join(' ');
}).filter(Boolean);
}
function collectWizardPredicates(root) {
return Array.from(root.querySelectorAll('.rule-row')).map((row) => {
const column = row.querySelector('[name="ruleColumn"]')?.value || '';
const type = row.querySelector('[name="ruleType"]')?.value || '';
const value = row.querySelector('[name="ruleValue"]')?.value || '';
const displayColumn = column || {
MY_DEPT: 'DEPT_CODE',
SELF: 'OWNER_EMP_NO',
DEPT: 'DEPT_CODE',
EMP_NO: 'OWNER_EMP_NO',
TAG: 'TECH_TAG',
ALL: ''
}[type] || '';
if (type === 'ALL') {
return '1 = 1';
}
if (type === 'MY_DEPT') {
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'DEPT_CODE')`;
}
if (type === 'SELF') {
return `${displayColumn} = SYS_CONTEXT('CB_AGENT_CTX', 'EMP_NO')`;
}
if (type === 'DEPT' || type === 'EMP_NO') {
return `${displayColumn} = ${sqlLiteral(value)}`;
}
if (type === 'TAG') {
return `REGEXP_LIKE(UPPER(${displayColumn}), ${sqlLiteral(`(^|,)${value.toUpperCase()}(,|$)`)})`;
}
if (type === '=') {
return `TO_CHAR(${displayColumn}) = ${sqlLiteral(value)}`;
}
if (type === '!=') {
return `TO_CHAR(${displayColumn}) <> ${sqlLiteral(value)}`;
}
return '';
}).filter(Boolean);
}
function updatePermissionWizardPreview(root = document) {
const wizard = root.querySelector('[data-permission-wizard]');
if (!wizard) {
return;
}
const roleSelect = wizard.querySelector('[name="roleId"]');
const objectSelect = wizard.querySelector('[name="objectRef"]');
const effectSelect = wizard.querySelector('[name="permissionEffect"]');
const visibleColumns = wizard.querySelector('[name="visibleColumns"]')?.value.trim() || '';
const roleOption = selectedOption(roleSelect);
const objectOption = selectedOption(objectSelect);
const effect = effectSelect?.value || 'ALLOW';
const rules = collectWizardRules(wizard);
const predicates = collectWizardPredicates(wizard);
const ruleText = rules.length ? rules.join(', ') : '행 규칙 없음';
const directUsers = splitList(roleOption?.dataset.directUsers || '');
const groups = splitList(roleOption?.dataset.groups || '');
const groupUsers = splitList(roleOption?.dataset.groupUsers || '');
const objectColumns = splitList(objectOption?.dataset.columns || '', ',');
const maskableColumns = splitList(objectOption?.dataset.maskableColumns || '', ',');
const visibleColumnList = splitList(visibleColumns, ',').map((column) => column.toUpperCase());
const nullColumns = maskableColumns.filter((column) => !visibleColumnList.includes(column.toUpperCase()));
const rowPolicy = effect === 'DENY'
? `거부 규칙: ${ruleText}`
: `허용 규칙: ${ruleText}`;
const predicateText = effect === 'DENY'
? (rules.includes('ALL') ? 'DENY ALL: 1 = 0' : `DENY 후보: NOT (${predicates.join(' OR ') || '조건 없음'})`)
: `ALLOW 후보: ${predicates.join(' OR ') || '조건 없음'}`;
const columnPolicy = visibleColumns
? `이 권한에서 원문 표시 허용: ${visibleColumns}`
: '마스킹 대상 컬럼은 기본 정책대로 NULL/마스킹 처리';
const nullPolicy = !maskableColumns.length
? '마스킹 대상 컬럼 없음'
: (nullColumns.length ? `NULL 처리: ${nullColumns.join(', ')}` : '선택한 마스킹 컬럼 모두 원문 표시 허용');
const affectedPrincipals = `직접 사용자 ${directUsers.length}명 / 그룹 ${groups.length}개 / 그룹 상속 사용자 ${groupUsers.length}`;
wizard.querySelector('[data-wizard-summary="role"]').textContent = selectedText(roleSelect);
wizard.querySelector('[data-wizard-summary="object"]').textContent = selectedText(objectSelect);
wizard.querySelector('[data-wizard-summary="effect"]').textContent = effect;
wizard.querySelector('[data-wizard-summary="rules"]').textContent = ruleText;
setWizardPreview(wizard, 'role', selectedText(roleSelect));
setWizardPreview(wizard, 'sensitivity', roleOption?.dataset.maxSensitivity || 'PUBLIC');
setWizardPreview(wizard, 'object', objectOption?.dataset.objectDisplay || selectedText(objectSelect));
setWizardPreview(wizard, 'directUsers', formatList(directUsers, '직접 부여 사용자 없음'));
setWizardPreview(wizard, 'roleGroups', formatList(groups, '역할이 부여된 그룹 없음'));
setWizardPreview(wizard, 'groupUsers', formatList(groupUsers, '그룹 상속 사용자 없음'));
setWizardPreview(wizard, 'affectedPrincipals', affectedPrincipals);
setWizardPreview(wizard, 'objectColumns', formatList(objectColumns, '컬럼 조회 전'));
setWizardPreview(wizard, 'objectColumnsFinal', formatList(objectColumns, '컬럼 조회 전'));
setWizardPreview(wizard, 'maskableColumns', formatList(maskableColumns, '마스킹 대상 컬럼 없음'));
setWizardPreview(wizard, 'rowPolicy', rowPolicy);
setWizardPreview(wizard, 'predicatePreview', predicateText);
setWizardPreview(wizard, 'columnPolicy', columnPolicy);
setWizardPreview(wizard, 'nullPolicy', nullPolicy);
}
function activatePermissionWizardStep(wizard, step) {
const panels = Array.from(wizard.querySelectorAll('[data-wizard-step]'));
const maxStep = panels.length;
const nextStep = Math.max(1, Math.min(step, maxStep));
panels.forEach((panel) => {
panel.classList.toggle('active', Number(panel.dataset.wizardStep) === nextStep);
});
document.querySelectorAll('[data-wizard-target]').forEach((button) => {
button.classList.toggle('active', Number(button.dataset.wizardTarget) === nextStep);
});
wizard.dataset.currentStep = String(nextStep);
const prev = wizard.querySelector('[data-wizard-prev]');
const next = wizard.querySelector('[data-wizard-next]');
const submit = wizard.querySelector('[data-wizard-submit]');
if (prev) {
prev.disabled = nextStep === 1;
}
if (next) {
next.hidden = nextStep === maxStep;
}
if (submit) {
submit.hidden = nextStep !== maxStep;
}
updatePermissionWizardPreview(document);
}
function initPermissionWizard() {
const wizard = document.querySelector('[data-permission-wizard]');
if (!wizard) {
return;
}
wizard.dataset.currentStep = wizard.dataset.currentStep || '1';
wizard.querySelector('[data-wizard-prev]')?.addEventListener('click', () => {
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') - 1);
});
wizard.querySelector('[data-wizard-next]')?.addEventListener('click', () => {
activatePermissionWizardStep(wizard, Number(wizard.dataset.currentStep || '1') + 1);
});
document.querySelectorAll('[data-wizard-target]').forEach((button) => {
button.addEventListener('click', () => activatePermissionWizardStep(wizard, Number(button.dataset.wizardTarget)));
});
wizard.addEventListener('input', () => updatePermissionWizardPreview(document));
wizard.addEventListener('change', () => updatePermissionWizardPreview(document));
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();
initQuestionPresets();
initCopyButtons();
renderMarkdownViews();
initVpdTargetFilters();
const master = document.getElementById('userRoleMaster');
if (master) {
master.addEventListener('change', filterUserRoleDetail);
filterUserRoleDetail();
}
document.querySelectorAll('.group-master-select').forEach((select) => {
select.addEventListener('change', () => filterGroupDetail(select.id));
filterGroupDetail(select.id);
});
initTokenContextPreviews();
initEffectiveMatrixFilters();
const objectSelect = document.querySelector('select[name="objectRef"]');
if (objectSelect) {
objectSelect.addEventListener('change', () => {
syncRuleColumnOptions();
updatePermissionWizardPreview(document);
});
syncRuleColumnOptions();
}
initPermissionWizard();
document.querySelectorAll('.rule-type-select').forEach((select) => {
select.addEventListener('change', () => {
syncRuleTypeHints();
updatePermissionWizardPreview(document);
});
});
syncRuleTypeHints();
document.querySelector('input[name="visibleColumns"]')?.addEventListener('input', renderSelectedVisibleColumns);
renderSelectedVisibleColumns();
const catalog = document.getElementById('objectCatalogSelect');
if (catalog) {
catalog.addEventListener('change', syncObjectCatalogSelection);
syncObjectCatalogSelection();
}
initOrdsPathEditToggle();
initProbeObjectDescription();
initProbeVectorInput();
document.querySelectorAll('[data-policy-template-select]').forEach((select) => {
select.addEventListener('change', () => syncPolicyTemplate(select));
syncPolicyTemplate(select);
});
document.querySelectorAll('[data-rule-add]').forEach((button) => {
button.addEventListener('click', () => {
const row = button.closest('.rule-row');
const list = document.getElementById('rowRuleList');
if (!row || !list) {
return;
}
const clone = row.cloneNode(true);
clone.querySelectorAll('input').forEach((input) => input.value = '');
const cloneButton = clone.querySelector('[data-rule-add]');
cloneButton.textContent = '삭제';
cloneButton.addEventListener('click', () => {
clone.remove();
updatePermissionWizardPreview(document);
});
clone.querySelectorAll('.rule-type-select').forEach((select) => {
select.addEventListener('change', () => {
syncRuleTypeHints();
updatePermissionWizardPreview(document);
});
});
list.appendChild(clone);
syncRuleColumnOptions();
syncRuleTypeHints(clone);
updatePermissionWizardPreview(document);
});
});
updatePermissionWizardPreview(document);
});
document.body.addEventListener('htmx:beforeRequest', (event) => {
const button = event.detail.elt;
if (!button?.matches?.('[data-filter-toggle]')) {
return;
}
const target = findHtmxTarget(button);
if (button.getAttribute('aria-expanded') === 'true') {
event.preventDefault();
resetFilterToggle(button, target);
return;
}
setFilterToggleLoading(button, target);
});
document.body.addEventListener('htmx:afterSwap', (event) => {
const button = event.detail.elt;
if (button?.matches?.('[data-filter-toggle]')) {
button.setAttribute('aria-expanded', 'true');
button.classList.add('active');
}
renderMarkdownViews(event.detail.target || document);
});