document.body.addEventListener('htmx:responseError', (event) => { const target = event.detail.target; if (target) { target.innerHTML = '
요청 처리 중 오류가 발생했습니다.
'; } }); const READ_ONLY_POST_ALLOWLIST = new Set([ '/login', '/logout', '/probe', '/vector-knowledge/search', '/security-sql-scripts/explanation', '/mcp-chatbot', '/mcp-client-demo', '/mcp-reasoning', ]); function normalizeActionPath(value) { if (!value) { return ''; } try { return new URL(value, window.location.origin).pathname; } catch (_error) { return value; } } function isReadOnlyMode() { return document.querySelector('meta[name="backoffice-read-only"]')?.content === 'true'; } function isFormMutation(form) { const method = (form.getAttribute('method') || 'get').toLowerCase(); const hxPost = form.getAttribute('hx-post'); if (hxPost) { return !READ_ONLY_POST_ALLOWLIST.has(normalizeActionPath(hxPost)); } if (method !== 'post') { return false; } return !READ_ONLY_POST_ALLOWLIST.has(normalizeActionPath(form.getAttribute('action'))); } function disableReadOnlyMutationForms() { if (!isReadOnlyMode()) { return; } document.querySelectorAll('form').forEach((form) => { if (!isFormMutation(form)) { return; } if (!form.previousElementSibling?.matches?.('[data-readonly-mutation-notice]')) { const notice = document.createElement('div'); notice.className = 'alert alert-secondary readonly-mutation-notice'; notice.setAttribute('data-readonly-mutation-notice', 'true'); notice.textContent = '읽기 전용 계정은 이 변경 작업을 실행할 수 없습니다.'; form.parentNode?.insertBefore(notice, form); } form.classList.add('readonly-mutation-disabled'); form.setAttribute('aria-disabled', 'true'); form.querySelectorAll('input:not([type="hidden"]), select, textarea, button').forEach((control) => { control.disabled = true; }); }); } 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 initPersistentMenus() { const productNav = document.querySelector('[data-product-nav]'); if (!productNav) { return; } const submenuBar = productNav.querySelector('[data-submenu-bar]'); const triggers = Array.from(productNav.querySelectorAll('[data-submenu-trigger]')); const panels = Array.from(productNav.querySelectorAll('[data-submenu-panel]')); const path = window.location.pathname; const activate = (target) => { const hasTarget = panels.some((panel) => panel.dataset.submenuPanel === target); submenuBar.hidden = !hasTarget; triggers.forEach((trigger) => { const active = trigger.dataset.submenuTrigger === target; trigger.classList.toggle('active', active); trigger.setAttribute('aria-expanded', String(active)); }); panels.forEach((panel) => { panel.hidden = panel.dataset.submenuPanel !== target; }); }; const currentPanel = panels.find((panel) => Array.from(panel.querySelectorAll('a[href]')).some((link) => { const href = new URL(link.href, window.location.origin).pathname; return path === href || (href !== '/' && path.startsWith(`${href}/`)); })); if (currentPanel) { activate(currentPanel.dataset.submenuPanel); } triggers.forEach((trigger) => { trigger.addEventListener('click', () => activate(trigger.dataset.submenuTrigger)); }); } 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('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function renderInlineMarkdown(value) { return escapeHtml(value) .replace(/\*\*([^*]+)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1'); } 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) => `${renderInlineMarkdown(cell)}`).join(''); const bodyHtml = rows.map((row) => { const cells = headers.map((_, index) => `${renderInlineMarkdown(row[index] || '')}`).join(''); return `${cells}`; }).join(''); return { html: `${headerHtml}${bodyHtml}
`, 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 = 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(`${renderInlineMarkdown(heading[2])}`); continue; } const unordered = trimmed.match(/^[-*]\s+(.+)$/); if (unordered) { if (listType !== 'ul') { closeList(); listType = 'ul'; html.push('