Update intranet tools and voucher comparison
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
(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;
|
||||
})();
|
||||
Reference in New Issue
Block a user