402 lines
15 KiB
JavaScript
402 lines
15 KiB
JavaScript
(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;
|
|
})();
|