(() => { const gallery = document.querySelector('[data-media-upload="gallery"]'); if (gallery) { const fileInput = gallery.querySelector('[data-media-files]'); const list = gallery.querySelector('[data-media-list]'); fileInput?.addEventListener('change', () => { list.replaceChildren(); const title = document.querySelector('input[name="title"]')?.value.trim() || '게시글'; [...fileInput.files].forEach((file, index) => { const item = document.createElement('article'); item.className = 'admin-new-media-item'; const preview = document.createElement('img'); preview.src = URL.createObjectURL(file); preview.alt = ''; const name = document.createElement('strong'); name.textContent = file.name; const label = document.createElement('label'); label.textContent = '대체 텍스트'; const alt = document.createElement('input'); alt.name = 'new_gallery_alts[]'; alt.value = `${title} - 대표 이미지 ${index + 1}`; alt.maxLength = 255; alt.required = true; label.append(alt); item.append(preview, name, label); list.append(item); }); }); } document.querySelectorAll('.admin-media-delete input[type="checkbox"]').forEach((checkbox) => { checkbox.addEventListener('change', () => { const item = checkbox.closest('.admin-media-item'); const alt = item?.querySelector('input:not([type]), input[type="text"]'); item?.classList.toggle('is-delete', checkbox.checked); if (alt) alt.required = !checkbox.checked; }); }); })(); (() => { const editor = document.querySelector('[data-block-editor]'); const sourceWrap = document.querySelector('[data-body-source]'); const source = document.querySelector('[data-post-body]'); if (!editor || !sourceWrap || !source) return; const list = editor.querySelector('[data-block-list]'); const deleteBin = editor.querySelector('[data-delete-bin]'); const inventory = new Map(); editor.querySelectorAll('[data-existing-inline]').forEach((item) => { inventory.set(Number(item.dataset.token), { id: Number(item.dataset.id), token: Number(item.dataset.token), path: item.dataset.path, alt: item.dataset.alt, }); }); let nextToken = Number(editor.dataset.nextToken || 1); const button = (label, action, className = '') => { const element = document.createElement('button'); element.type = 'button'; element.textContent = label; element.dataset.blockAction = action; if (className) element.className = className; return element; }; const resizeText = (textarea) => { textarea.style.height = 'auto'; textarea.style.height = `${Math.max(112, textarea.scrollHeight)}px`; }; const updateControls = () => { const blocks = [...list.children]; blocks.forEach((block, index) => { block.querySelector('[data-block-action="up"]').disabled = index === 0; block.querySelector('[data-block-action="down"]').disabled = index === blocks.length - 1; }); }; const bindBlockActions = (block) => { block.querySelector('[data-block-action="up"]').addEventListener('click', () => { const previous = block.previousElementSibling; if (previous) list.insertBefore(block, previous); updateControls(); }); block.querySelector('[data-block-action="down"]').addEventListener('click', () => { const next = block.nextElementSibling; if (next) list.insertBefore(next, block); updateControls(); }); block.querySelector('[data-block-action="remove"]').addEventListener('click', () => { if (block.dataset.existingId) { const deleted = document.createElement('input'); deleted.type = 'hidden'; deleted.name = 'delete_images[]'; deleted.value = block.dataset.existingId; deleteBin.append(deleted); } block.remove(); if (!list.children.length) addTextBlock(''); updateControls(); }); }; const createShell = (type, title) => { const block = document.createElement('article'); block.className = `admin-content-block admin-content-block-${type}`; block.dataset.blockType = type; const header = document.createElement('header'); const heading = document.createElement('strong'); heading.textContent = title; const actions = document.createElement('div'); actions.className = 'admin-content-block-actions'; actions.append( button('위로', 'up'), button('아래로', 'down'), button('삭제', 'remove', 'is-danger'), ); header.append(heading, actions); block.append(header); bindBlockActions(block); return block; }; const appendInsertControls = (block) => { const controls = document.createElement('div'); controls.className = 'admin-content-insert-actions'; const addText = document.createElement('button'); addText.type = 'button'; addText.textContent = '아래에 문단 추가'; addText.addEventListener('click', () => { const added = addTextBlock('', block.nextElementSibling); added.querySelector('textarea').focus(); }); const addImage = document.createElement('button'); addImage.type = 'button'; addImage.textContent = '아래에 이미지 추가'; addImage.addEventListener('click', () => chooseImage(block.nextElementSibling)); controls.append(addText, addImage); block.append(controls); }; const addTextBlock = (value, before = null) => { const block = createShell('text', '텍스트 문단'); const textarea = document.createElement('textarea'); textarea.rows = 4; textarea.value = value; textarea.placeholder = '본문 내용을 입력하세요.'; textarea.setAttribute('aria-label', '본문 텍스트 문단'); textarea.addEventListener('input', () => resizeText(textarea)); block.append(textarea); appendInsertControls(block); list.insertBefore(block, before); requestAnimationFrame(() => resizeText(textarea)); updateControls(); return block; }; const addImageBlock = ({ id = null, token, path, alt, fileInput = null }, before = null) => { const block = createShell('image', id ? '등록된 본문 이미지' : '새 본문 이미지'); block.dataset.token = String(token); if (id) block.dataset.existingId = String(id); const preview = document.createElement('img'); preview.src = path; preview.alt = ''; const fields = document.createElement('div'); fields.className = 'admin-content-image-fields'; const label = document.createElement('label'); label.textContent = '대체 텍스트'; const altInput = document.createElement('input'); altInput.name = id ? `media_alt[${id}]` : 'new_inline_alts[]'; altInput.value = alt; altInput.maxLength = 255; altInput.required = true; label.append(altInput); fields.append(label); if (fileInput) { fileInput.name = 'inline_images[]'; fileInput.className = 'admin-hidden-file'; const tokenInput = document.createElement('input'); tokenInput.type = 'hidden'; tokenInput.name = 'new_inline_tokens[]'; tokenInput.value = String(token); fields.append(fileInput, tokenInput); } block.append(preview, fields); appendInsertControls(block); list.insertBefore(block, before); updateControls(); return block; }; const imageCount = () => list.querySelectorAll('[data-block-type="image"]').length; const chooseImage = (before = null) => { if (imageCount() >= 10) { window.alert('본문 이미지는 최대 10장까지 등록할 수 있습니다.'); return; } const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/jpeg,image/png,image/webp'; input.className = 'admin-hidden-file'; input.addEventListener('change', () => { const file = input.files?.[0]; if (!file) return; const title = document.querySelector('input[name="title"]')?.value.trim() || '게시글'; addImageBlock({ token: nextToken++, path: URL.createObjectURL(file), alt: `${title} - 본문 이미지`, fileInput: input, }, before?.isConnected ? before : null); }, { once: true }); deleteBin.append(input); input.click(); }; const usedTokens = new Set(); const initialBody = source.value.trim(); const parts = initialBody ? initialBody.split(/\r?\n(?:[\t ]*\r?\n)+/) : []; parts.forEach((part) => { const match = part.trim().match(/^\[\[image:(\d+)\]\]$/); const media = match ? inventory.get(Number(match[1])) : null; if (media) { addImageBlock(media); usedTokens.add(media.token); } else { addTextBlock(part); } }); inventory.forEach((media) => { if (!usedTokens.has(media.token)) addImageBlock(media); }); if (!list.children.length) addTextBlock(''); editor.querySelector('[data-add-text]').addEventListener('click', () => { const block = addTextBlock(''); block.querySelector('textarea').focus(); }); editor.querySelector('[data-add-image]').addEventListener('click', () => chooseImage()); source.required = false; sourceWrap.classList.add('is-enhanced'); editor.classList.add('is-ready'); source.closest('form').addEventListener('submit', () => { source.value = [...list.children].map((block) => { if (block.dataset.blockType === 'image') return `[[image:${block.dataset.token}]]`; return block.querySelector('textarea').value.trim(); }).filter(Boolean).join('\n\n'); }); const previewDialog = document.querySelector('[data-live-preview]'); const openPreview = document.querySelector('[data-open-live-preview]'); const closePreview = previewDialog?.querySelector('[data-close-live-preview]'); const renderPreview = () => { const previewTitle = previewDialog.querySelector('[data-preview-title]'); const previewCategory = previewDialog.querySelector('[data-preview-category]'); const previewAuthor = previewDialog.querySelector('[data-preview-author]'); const previewBody = previewDialog.querySelector('[data-preview-body]'); const previewGallery = previewDialog.querySelector('[data-preview-gallery]'); const galleryMain = previewDialog.querySelector('[data-preview-gallery-main]'); const galleryThumbs = previewDialog.querySelector('[data-preview-gallery-thumbs]'); const galleryCount = previewDialog.querySelector('[data-preview-gallery-count]'); previewTitle.textContent = document.querySelector('input[name="title"]')?.value.trim() || '제목 없는 글'; previewCategory.textContent = document.querySelector('select[name="category"]')?.value || ''; previewAuthor.textContent = `${document.querySelector('input[name="author"]')?.value.trim() || ''} 기술팀`; previewBody.replaceChildren(); [...list.children].forEach((block) => { if (block.dataset.blockType === 'image') { const figure = document.createElement('figure'); const image = document.createElement('img'); image.src = block.querySelector('img').src; image.alt = block.querySelector('.admin-content-image-fields input')?.value.trim() || ''; figure.append(image); previewBody.append(figure); return; } const value = block.querySelector('textarea').value.trim(); if (!value) return; const paragraph = document.createElement('p'); paragraph.textContent = value; previewBody.append(paragraph); }); if (!previewBody.children.length) { const empty = document.createElement('p'); empty.className = 'is-empty'; empty.textContent = '본문 내용을 입력하면 여기에 표시됩니다.'; previewBody.append(empty); } const galleryItems = [ ...document.querySelectorAll('[data-media-upload="gallery"] .admin-media-item:not(.is-delete), [data-media-upload="gallery"] .admin-new-media-item'), ].map((item) => ({ src: item.querySelector('img')?.src || '', alt: item.querySelector('input:not([type="checkbox"])')?.value.trim() || '', })).filter((item) => item.src); galleryMain.replaceChildren(); galleryThumbs.replaceChildren(); previewGallery.hidden = galleryItems.length === 0; if (galleryItems.length) { const mainImage = document.createElement('img'); mainImage.src = galleryItems[0].src; mainImage.alt = galleryItems[0].alt; galleryMain.append(mainImage); galleryItems.forEach((item, index) => { const thumb = document.createElement('button'); thumb.type = 'button'; thumb.setAttribute('aria-label', `${index + 1}번째 대표 이미지 보기`); if (index === 0) thumb.classList.add('is-active'); const image = document.createElement('img'); image.src = item.src; image.alt = ''; thumb.append(image); thumb.addEventListener('click', () => { mainImage.src = item.src; mainImage.alt = item.alt; [...galleryThumbs.children].forEach((child) => child.classList.toggle('is-active', child === thumb)); }); galleryThumbs.append(thumb); }); galleryCount.textContent = `대표 이미지 ${galleryItems.length}장`; } }; openPreview?.addEventListener('click', () => { renderPreview(); previewDialog.showModal(); }); closePreview?.addEventListener('click', () => previewDialog.close()); previewDialog?.addEventListener('click', (event) => { if (event.target === previewDialog) previewDialog.close(); }); })();