(function () { function createFlowEditorMedia(options) { const { elements, getEditingKey, getEditMode, getCurrentEditorLabel, getCurrentImageIndex, setCurrentImageIndex, getModalImageIndex, setModalImageIndex, ensureMeta, getAllStepMeta, syncLegacyMetaFromScreens, saveStepMeta } = options; const { editorPathInputEl, editorPathViewEl, editorImageInputEl, editorUploaderEl, editorFileStatusEl, editorClearImageEl, editorPreviewEl, editorPrevImageEl, editorNextImageEl, editorPageInfoEl, editorImageNoteInputEl, editorImageNoteViewEl, imageModalEl, imageModalImgEl, imageModalCloseEl, imageModalPrevEl, imageModalNextEl, imageModalPageEl, imageModalNoteEl } = elements; const DEFAULT_PREVIEW_RATIO = 2; const imageRatioCache = new Map(); let previewRatioTaskId = 0; function setViewerValue(element, value, emptyText = '정보 없음') { if (!element) return; const text = String(value || '').trim(); element.textContent = text || emptyText; element.classList.toggle('is-empty', !text); } function getScreens(info) { if (!Array.isArray(info.screens)) { info.screens = []; } return info.screens; } function applyPreviewRatio(ratio) { const safeRatio = Number.isFinite(ratio) && ratio > 0 ? ratio : DEFAULT_PREVIEW_RATIO; if (editorPreviewEl) { editorPreviewEl.style.setProperty('--preview-ratio', String(safeRatio)); } } function loadImageRatio(imageSrc) { const src = String(imageSrc || '').trim(); if (!src) return Promise.resolve(DEFAULT_PREVIEW_RATIO); if (imageRatioCache.has(src)) return imageRatioCache.get(src); const task = new Promise((resolve) => { const img = new Image(); img.onload = () => { const width = Number(img.naturalWidth || 0); const height = Number(img.naturalHeight || 0); if (width > 0 && height > 0) { resolve(width / height); return; } resolve(DEFAULT_PREVIEW_RATIO); }; img.onerror = () => resolve(DEFAULT_PREVIEW_RATIO); img.src = src; }); imageRatioCache.set(src, task); return task; } function collectAllImageSources() { const allMeta = (typeof getAllStepMeta === 'function' ? getAllStepMeta() : null) || {}; const sources = new Set(); Object.values(allMeta).forEach((info) => { const screens = Array.isArray(info && info.screens) ? info.screens : []; screens.forEach((screen) => { const src = String(screen && screen.image ? screen.image : '').trim(); if (src) sources.add(src); }); }); return Array.from(sources); } function refreshGlobalPreviewRatio() { applyPreviewRatio(2); return; const taskId = ++previewRatioTaskId; const sources = collectAllImageSources(); if (!sources.length) { applyPreviewRatio(DEFAULT_PREVIEW_RATIO); return; } Promise.all(sources.map((src) => loadImageRatio(src))) .then((ratios) => { if (taskId !== previewRatioTaskId) return; const validRatios = ratios.filter((ratio) => Number.isFinite(ratio) && ratio > 0); if (!validRatios.length) { applyPreviewRatio(DEFAULT_PREVIEW_RATIO); return; } // Use the most portrait image as the global frame ratio for consistency across all pages. const minRatio = validRatios.reduce((acc, ratio) => Math.min(acc, ratio), validRatios[0]); applyPreviewRatio(minRatio); }) .catch(() => { if (taskId !== previewRatioTaskId) return; applyPreviewRatio(DEFAULT_PREVIEW_RATIO); }); } function ensureScreenAt(info, index, { createIfMissing = false } = {}) { const screens = getScreens(info); if (createIfMissing && !screens.length) { screens.push({ path: '', note: '', image: '' }); } while (createIfMissing && index >= screens.length) { screens.push({ path: '', note: '', image: '' }); } const screen = screens[index] || null; if (screen) { screen.path = String(screen.path || ''); screen.note = String(screen.note || ''); screen.image = String(screen.image || ''); } return screen; } function applyEditMode(mode = getEditMode()) { const isEditing = Boolean(mode); if (editorUploaderEl) { editorUploaderEl.hidden = !isEditing; } editorPathInputEl.readOnly = !isEditing; editorImageInputEl.disabled = !isEditing; editorClearImageEl.disabled = !isEditing; editorImageNoteInputEl.readOnly = !isEditing; } function renderEditorPreview(label) { const editingKey = getEditingKey(); if (!editingKey) return; refreshGlobalPreviewRatio(); const info = ensureMeta(editingKey); const screens = getScreens(info); let currentIndex = getCurrentImageIndex(); if (currentIndex >= screens.length) { currentIndex = Math.max(0, screens.length - 1); setCurrentImageIndex(currentIndex); } const currentScreen = screens[currentIndex] || null; const imageSrc = currentScreen ? currentScreen.image : ''; editorPreviewEl.innerHTML = ''; if (!imageSrc) { const empty = document.createElement('p'); empty.className = 'preview-empty'; empty.textContent = '이미지가 없습니다.'; editorPreviewEl.appendChild(empty); } else { const img = document.createElement('img'); img.src = imageSrc; img.alt = `${label} 이미지`; img.style.cursor = 'zoom-in'; img.addEventListener('click', () => openImageModal(currentIndex, `${label} 확대 이미지`)); editorPreviewEl.appendChild(img); } editorPageInfoEl.textContent = `${screens.length ? (currentIndex + 1) : 0} / ${screens.length}`; editorFileStatusEl.textContent = screens.length ? `저장된 화면 ${screens.length}장` : '저장된 화면 0장'; editorPrevImageEl.disabled = screens.length <= 1 || currentIndex <= 0; editorNextImageEl.disabled = screens.length <= 1 || currentIndex >= screens.length - 1; editorPathInputEl.value = currentScreen ? currentScreen.path : ''; editorImageNoteInputEl.value = currentScreen ? currentScreen.note : ''; setViewerValue(editorPathViewEl, currentScreen ? currentScreen.path : '', ''); setViewerValue(editorImageNoteViewEl, currentScreen ? currentScreen.note : '', ''); editorImageNoteInputEl.disabled = !currentScreen; editorImageNoteInputEl.readOnly = !getEditMode(); editorClearImageEl.disabled = !currentScreen || !getEditMode(); } function updateImageModalView(altText) { const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screens = getScreens(info); if (!screens.length) return; let modalIndex = getModalImageIndex(); if (modalIndex < 0) modalIndex = 0; if (modalIndex > screens.length - 1) modalIndex = screens.length - 1; setModalImageIndex(modalIndex); const currentScreen = screens[modalIndex]; imageModalImgEl.src = currentScreen.image || ''; imageModalImgEl.alt = altText || '확대 이미지'; imageModalPageEl.textContent = `${modalIndex + 1} / ${screens.length}`; imageModalPrevEl.disabled = modalIndex <= 0; imageModalNextEl.disabled = modalIndex >= screens.length - 1; const note = String(currentScreen.note || '').trim(); if (note) { imageModalNoteEl.textContent = note; imageModalNoteEl.classList.add('show'); } else { imageModalNoteEl.textContent = ''; imageModalNoteEl.classList.remove('show'); } } function openImageModal(index, altText) { const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screens = getScreens(info); if (!screens.length) return; const targetScreen = screens[index] || null; if (!targetScreen || !targetScreen.image) return; setModalImageIndex(Number.isInteger(index) ? index : 0); updateImageModalView(altText); imageModalEl.classList.add('open'); imageModalEl.setAttribute('aria-hidden', 'false'); } function closeImageModal() { imageModalEl.classList.remove('open'); imageModalEl.setAttribute('aria-hidden', 'true'); imageModalImgEl.src = ''; imageModalPageEl.textContent = '0 / 0'; imageModalNoteEl.textContent = ''; imageModalNoteEl.classList.remove('show'); } function bind() { refreshGlobalPreviewRatio(); editorClearImageEl.addEventListener('click', () => { if (!getEditMode()) return; const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screens = getScreens(info); const currentIndex = getCurrentImageIndex(); if (!screens.length) return; screens.splice(currentIndex, 1); let nextIndex = currentIndex; if (nextIndex >= screens.length) { nextIndex = Math.max(0, screens.length - 1); } setCurrentImageIndex(nextIndex); syncLegacyMetaFromScreens(info); saveStepMeta(); renderEditorPreview(getCurrentEditorLabel()); }); editorImageInputEl.addEventListener('change', (event) => { if (!getEditMode()) { event.target.value = ''; return; } const editingKey = getEditingKey(); if (!editingKey) return; const files = Array.from(event.target.files || []); if (!files.length) return; const imageFiles = files.filter((file) => file.type.startsWith('image/')); if (!imageFiles.length) { alert('이미지 파일만 선택할 수 있습니다.'); event.target.value = ''; return; } const targetKey = editingKey; const readJobs = imageFiles.map((file) => new Promise((resolve) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || '')); reader.onerror = () => resolve(''); reader.readAsDataURL(file); })); Promise.all(readJobs).then((images) => { const validImages = images.filter(Boolean); if (!validImages.length) return; const info = ensureMeta(targetKey); const screens = getScreens(info); const currentScreen = ensureScreenAt(info, getCurrentImageIndex()); const pendingImages = [...validImages]; if (currentScreen && !currentScreen.image && pendingImages.length) { currentScreen.image = pendingImages.shift(); } pendingImages.forEach((imageSrc, offset) => { screens.push({ path: currentScreen && offset === 0 ? String(currentScreen.path || '') : '', note: '', image: imageSrc }); }); syncLegacyMetaFromScreens(info); if (getEditingKey() === targetKey) { setCurrentImageIndex(Math.max(0, screens.length - Math.max(1, pendingImages.length))); renderEditorPreview(getCurrentEditorLabel()); } saveStepMeta(); }); event.target.value = ''; }); editorPrevImageEl.addEventListener('click', () => { const editingKey = getEditingKey(); let currentIndex = getCurrentImageIndex(); if (!editingKey || currentIndex <= 0) return; currentIndex -= 1; setCurrentImageIndex(currentIndex); renderEditorPreview(getCurrentEditorLabel()); }); editorNextImageEl.addEventListener('click', () => { const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screens = getScreens(info); let currentIndex = getCurrentImageIndex(); if (currentIndex >= screens.length - 1) return; currentIndex += 1; setCurrentImageIndex(currentIndex); renderEditorPreview(getCurrentEditorLabel()); }); editorImageNoteInputEl.addEventListener('input', () => { if (!getEditMode()) return; const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screen = ensureScreenAt(info, getCurrentImageIndex(), { createIfMissing: true }); screen.note = editorImageNoteInputEl.value; syncLegacyMetaFromScreens(info); setViewerValue(editorImageNoteViewEl, screen.note, ''); saveStepMeta(); }); editorPathInputEl.addEventListener('input', () => { if (!getEditMode()) return; const editingKey = getEditingKey(); if (!editingKey) return; const info = ensureMeta(editingKey); const screen = ensureScreenAt(info, getCurrentImageIndex(), { createIfMissing: true }); screen.path = editorPathInputEl.value; syncLegacyMetaFromScreens(info); setViewerValue(editorPathViewEl, screen.path, ''); saveStepMeta(); }); imageModalCloseEl.addEventListener('click', closeImageModal); imageModalPrevEl.addEventListener('click', () => { if (!imageModalEl.classList.contains('open')) return; setModalImageIndex(getModalImageIndex() - 1); updateImageModalView('확대 이미지'); }); imageModalNextEl.addEventListener('click', () => { if (!imageModalEl.classList.contains('open')) return; setModalImageIndex(getModalImageIndex() + 1); updateImageModalView('확대 이미지'); }); imageModalEl.addEventListener('click', (event) => { if (event.target === imageModalEl) closeImageModal(); }); document.addEventListener('keydown', (event) => { if (!imageModalEl.classList.contains('open')) return; if (event.key === 'ArrowLeft') { setModalImageIndex(getModalImageIndex() - 1); updateImageModalView('확대 이미지'); } if (event.key === 'ArrowRight') { setModalImageIndex(getModalImageIndex() + 1); updateImageModalView('확대 이미지'); } if (event.key === 'Escape') { closeImageModal(); } }); applyEditMode(); } return { renderEditorPreview, updateImageModalView, openImageModal, closeImageModal, bind, applyEditMode }; } window.createFlowEditorMedia = createFlowEditorMedia; })();