Files
HM_project_Viewer_Board/static/hm-biz-process/flow_editor_media.js
T

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;
})();