import { state } from './state.js'; import * as pinchZoom from './handlers/pinch-zoom.js'; import * as selection from './handlers/selection.js'; import * as dragDrop from './handlers/drag-drop.js'; import * as hoverPreview from './handlers/image-preview.js'; let _cardObserver = null; // ── DOM ────────────────────────────────────────────── const grid = document.getElementById('grid'); const emptyState = document.getElementById('empty-state'); const btnOpen = document.getElementById('btn-open'); const btnSortName = document.getElementById('btn-sort-name'); const btnSortDate = document.getElementById('btn-sort-date'); const btnRename = document.getElementById('btn-rename'); const btnReveal = document.getElementById('btn-reveal'); const inputPrefix = document.getElementById('input-prefix'); const inputDigits = document.getElementById('input-digits'); const previewName = document.getElementById('preview-name'); const fileCount = document.getElementById('file-count'); const selCount = document.getElementById('sel-count'); const dirPath = document.getElementById('dir-path'); const statusDot = document.getElementById('status-dot'); const statusText = document.getElementById('status-text'); const loading = document.getElementById('loading'); const loadingText = document.getElementById('loading-text'); const toast = document.getElementById('toast'); const rubberBand = document.getElementById('rubber-band'); const gridContainer = document.getElementById('grid-container'); const dragGhostEl = document.getElementById('drag-ghost'); const thumbSizeHint = document.getElementById('thumb-size-hint'); // ── 핸들러 초기화 ───────────────────────────────── const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels, }; pinchZoom.init({ gridContainer, thumbSizeHint }); selection.init({ gridContainer, rubberBand, grid, state, callbacks }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks }); hoverPreview.init(); window.api.onFullscreenChange((isFullscreen) => { document.body.classList.toggle('fullscreen', isFullscreen); }); // ── 버튼 이벤트 ────────────────────────────────── inputPrefix.addEventListener('input', updatePreview); inputDigits.addEventListener('input', updatePreview); btnOpen.addEventListener('click', openFolder); document.addEventListener('keydown', (e) => { if (e.target.tagName === 'INPUT') return; if (e.code === 'KeyO') btnOpen.click(); if (e.code === 'KeyS' && !btnRename.disabled) btnRename.click(); }); btnSortName.addEventListener('click', () => sortFiles('name')); btnSortDate.addEventListener('click', () => sortFiles('date')); btnRename.addEventListener('click', doRename); btnReveal.addEventListener('click', () => { if (state.currentFolder) window.api.revealFolder(state.currentFolder); }); // ── 폴더 열기 ──────────────────────────────────── async function openFolder() { const folder = await window.api.selectFolder(); if (!folder) return; state.currentFolder = folder; dirPath.textContent = folder; showLoading('이미지 목록 불러오는 중...'); const list = await window.api.listImages(folder); hideLoading(); if (!list.length) { setStatus('이미지 파일 없음', false); showToast('⚠️ 이미지 파일이 없습니다', '#f76a6a', '#0a0a0a'); return; } state.files.length = 0; state.files.push(...list); state.selectedIdxs.clear(); state.sortMode = 'name'; updateSortButtons(); setStatus(`${state.files.length}개 파일 로드됨`, true); const folderName = folder.replace(/\\/g, '/').split('/').pop(); if (folderName) inputPrefix.value = folderName; renderGrid(); updatePreview(); btnRename.disabled = false; btnReveal.disabled = false; } // ── 그리드 렌더링 ──────────────────────────────── function renderGrid() { if (_cardObserver) { _cardObserver.disconnect(); _cardObserver = null; } grid.innerHTML = ''; emptyState.style.display = 'none'; grid.style.display = 'grid'; fileCount.textContent = `${state.files.length}개 파일`; updateSelCount(); // 카드 껍데기만 즉시 생성 (이미지 없음) state.files.forEach((file, idx) => grid.appendChild(createCard(file, idx))); // 뷰포트 ±400px 범위에 들어온 카드만 이미지 로딩 const observer = new IntersectionObserver((entries) => { entries.forEach(async (entry) => { if (!entry.isIntersecting) return; observer.unobserve(entry.target); const img = entry.target.querySelector('img.card-thumb'); if (!img || img.src) return; const dataUrl = await window.api.readImage(entry.target.dataset.filepath); if (dataUrl) img.src = dataUrl; }); }, { root: gridContainer, rootMargin: '400px 0px', threshold: 0 }); Array.from(grid.children).forEach(card => observer.observe(card)); _cardObserver = observer; refreshCardSelection(); updateNumberBadges(); updateNewNameLabels(); } // ── 카드 생성 ──────────────────────────────────── function createCard(file, idx) { const card = document.createElement('div'); card.className = 'card'; card.dataset.idx = idx; card.dataset.filepath = file.path; card.draggable = true; const numBadge = document.createElement('div'); numBadge.className = 'card-num'; numBadge.textContent = `#${idx + 1}`; const checkBadge = document.createElement('div'); checkBadge.className = 'card-check'; checkBadge.textContent = '✓'; // src 없이 생성 — IntersectionObserver가 뷰포트 진입 시 로딩 const thumb = document.createElement('img'); thumb.className = 'card-thumb'; thumb.alt = file.name; thumb.draggable = false; const body = document.createElement('div'); body.className = 'card-body'; const nameEl = document.createElement('div'); nameEl.className = 'card-name'; nameEl.title = file.name; nameEl.textContent = file.name; const newNameEl = document.createElement('div'); newNameEl.className = 'card-new-name'; body.appendChild(nameEl); body.appendChild(newNameEl); card.appendChild(numBadge); card.appendChild(checkBadge); card.appendChild(thumb); card.appendChild(body); dragDrop.setupCard(card); hoverPreview.setupCard(card); card.addEventListener('click', selection.onCardClick); return card; } // ── 정렬 ───────────────────────────────────────── async function sortFiles(mode) { if (!state.files.length) return; if (mode === 'name') state.files.sort((a, b) => a.name.localeCompare(b.name, 'ko')); else state.files.sort((a, b) => a.mtime - b.mtime); state.sortMode = mode; state.selectedIdxs.clear(); updateSortButtons(); await renderGrid(); setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true); } function updateSortButtons() { btnSortName.classList.toggle('active', state.sortMode === 'name'); btnSortDate.classList.toggle('active', state.sortMode === 'date'); } // ── 미리보기 ───────────────────────────────────── function updatePreview() { const prefix = inputPrefix.value.trim() || 'photo'; const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3)); const ext = state.files.length ? state.files[0].name.split('.').pop().toLowerCase() : 'jpg'; previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`; updateNewNameLabels(); } // ── 이름 변경 ──────────────────────────────────── async function doRename() { if (!state.files.length || !state.currentFolder) return; const prefix = inputPrefix.value.trim() || 'photo'; const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3)); const examples = state.files.slice(0, 3).map((f, i) => { const ext = f.name.split('.').pop().toLowerCase(); return `${f.name} → ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; }).join('\n'); const more = state.files.length > 3 ? `\n... 외 ${state.files.length - 3}개` : ''; if (!confirm(`📂 ${state.files.length}개 파일을 이름 변경합니다:\n\n${examples}${more}\n\n계속하시겠습니까?`)) return; showLoading('파일 이름 변경 중...'); setStatus('이름 변경 중...', false); try { const result = await window.api.renameFiles({ folderPath: state.currentFolder, files: state.files, prefix, digits, }); hideLoading(); if (result.errors.length) { alert('일부 오류:\n' + result.errors.join('\n')); } else { showToast(`✅ ${result.renamed.length}개 파일 이름 변경 완료!`); setStatus(`${result.renamed.length}개 파일 이름 변경 완료`, true); } const newList = await window.api.listImages(state.currentFolder); state.files.length = 0; state.files.push(...newList); state.selectedIdxs.clear(); renderGrid(); } catch (e) { hideLoading(); alert('오류 발생: ' + e.message); } } // ── UI 업데이트 ────────────────────────────────── function refreshCardSelection() { Array.from(grid.children).forEach((card, i) => { card.classList.toggle('selected', state.selectedIdxs.has(i)); }); } function updateNumberBadges() { Array.from(grid.children).forEach((card, i) => { const badge = card.querySelector('.card-num'); if (badge) badge.textContent = `#${i + 1}`; card.dataset.idx = i; }); } function updateNewNameLabels() { const prefix = inputPrefix.value.trim() || 'photo'; const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3)); Array.from(grid.children).forEach((card, i) => { const el = card.querySelector('.card-new-name'); const file = state.files[i]; if (!el || !file) return; const ext = file.name.split('.').pop().toLowerCase(); el.textContent = `→ ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; }); } function updateSelCount() { const n = state.selectedIdxs.size; if (n > 0) { selCount.textContent = `${n}개 선택됨`; selCount.classList.add('visible'); } else { selCount.textContent = ''; selCount.classList.remove('visible'); } } // ── 공통 UI 헬퍼 ───────────────────────────────── function showLoading(msg = '처리 중...') { loadingText.textContent = msg; loading.classList.add('show'); } function hideLoading() { loading.classList.remove('show'); } function setStatus(msg, ready) { statusText.textContent = msg; statusDot.className = 'status-dot' + (ready ? ' ready' : ''); } function showToast(msg, bg = '#6af7a0', color = '#0a2a1a') { toast.textContent = msg; toast.style.background = bg; toast.style.color = color; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 3000); }