document.body.addEventListener('htmx:responseError', (event) => { const target = event.detail.target; if (target) { target.innerHTML = '
요청 처리 중 오류가 발생했습니다.
'; } }); 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('&', '&') .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('