diff --git a/src/renderer/grid.js b/src/renderer/grid.js new file mode 100644 index 0000000..347d942 --- /dev/null +++ b/src/renderer/grid.js @@ -0,0 +1,133 @@ +import { state } from './state.js'; +import * as dragDrop from './handlers/drag-drop.js'; +import * as hoverPreview from './handlers/image-preview.js'; +import * as selection from './handlers/selection.js'; + +const grid = document.getElementById('grid'); +const emptyState = document.getElementById('empty-state'); +const gridContainer = document.getElementById('grid-container'); +const fileCount = document.getElementById('file-count'); +const selCount = document.getElementById('sel-count'); +const inputPrefix = document.getElementById('input-prefix'); +const inputDigits = document.getElementById('input-digits'); + +let _cardObserver = null; + +export 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))); + + 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(); +} + +export 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; +} + +export function refreshCardSelection() { + Array.from(grid.children).forEach((card, i) => { + card.classList.toggle('selected', state.selectedIdxs.has(i)); + }); +} + +export 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; + }); +} + +export 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}`; + }); +} + +export function updateSelCount() { + const n = state.selectedIdxs.size; + if (n > 0) { + selCount.textContent = `${n}개 선택됨`; + selCount.classList.add('visible'); + } else { + selCount.textContent = ''; + selCount.classList.remove('visible'); + } +} + +export function showEmptyGrid() { + grid.innerHTML = ''; + grid.style.display = 'none'; + emptyState.style.display = ''; + fileCount.textContent = '0개 파일'; + updateSelCount(); +} diff --git a/src/renderer/move.js b/src/renderer/move.js new file mode 100644 index 0000000..bd114cb --- /dev/null +++ b/src/renderer/move.js @@ -0,0 +1,99 @@ +import { state } from './state.js'; +import { showLoading, hideLoading, setStatus, showToast } from './ui.js'; +import { renderGrid, updateSelCount, showEmptyGrid } from './grid.js'; + +const btnRename = document.getElementById('btn-rename'); +const moveModal = document.getElementById('move-modal'); +const movePrefixInput = document.getElementById('move-prefix'); +const moveDigitsInput = document.getElementById('move-digits'); +const moveCancelBtn = document.getElementById('move-cancel'); +const moveConfirmBtn = document.getElementById('move-confirm'); +const moveErrorModal = document.getElementById('move-error-modal'); +const moveErrorCloseBtn = document.getElementById('move-error-close'); + +let _pendingMoveTarget = null; +let _pendingMoveFiles = null; + +moveCancelBtn.addEventListener('click', () => { + moveModal.style.display = 'none'; + _pendingMoveTarget = null; + _pendingMoveFiles = null; +}); + +moveConfirmBtn.addEventListener('click', async () => { + const prefix = movePrefixInput.value.trim() || 'photo'; + const digits = Math.max(1, Math.min(6, parseInt(moveDigitsInput.value) || 4)); + + const newNames = _pendingMoveFiles.map((f, i) => { + const ext = f.name.split('.').pop().toLowerCase(); + return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; + }); + + const conflicts = await window.api.checkFilesExist(_pendingMoveTarget, newNames); + if (conflicts.length) { + moveModal.style.display = 'none'; + moveErrorModal.style.display = 'flex'; + return; + } + + moveModal.style.display = 'none'; + const targetPath = _pendingMoveTarget; + const filesToMove = _pendingMoveFiles.map((f, i) => ({ ...f, newName: newNames[i] })); + _pendingMoveTarget = null; + _pendingMoveFiles = null; + await _doMoveFiles(targetPath, filesToMove); +}); + +moveErrorCloseBtn.addEventListener('click', () => { + moveErrorModal.style.display = 'none'; +}); + +export async function handleFolderDrop(targetPath, fileIndices) { + if (!fileIndices.length || !state.currentFolder) return; + if (targetPath === state.currentFolder) return; + + const sorted = [...fileIndices].sort((a, b) => a - b); + const selectedFiles = sorted.map(i => state.files[i]).filter(Boolean); + if (!selectedFiles.length) return; + + const names = selectedFiles.map(f => f.name); + const conflicts = await window.api.checkFilesExist(targetPath, names); + + if (!conflicts.length) { + await _doMoveFiles(targetPath, selectedFiles); + } else { + _pendingMoveTarget = targetPath; + _pendingMoveFiles = selectedFiles; + movePrefixInput.value = 'temp'; + moveModal.style.display = 'flex'; + } +} + +async function _doMoveFiles(targetPath, files) { + showLoading('파일 이동 중...'); + try { + const result = await window.api.moveFiles({ targetFolder: targetPath, files }); + hideLoading(); + if (result.errors.length) { + alert('일부 오류:\n' + result.errors.join('\n')); + } else { + showToast(`✅ ${result.moved.length}개 파일 이동 완료!`); + setStatus(`${result.moved.length}개 파일 이동 완료`, true); + } + const newList = await window.api.listImages(state.currentFolder); + state.files.length = 0; + state.files.push(...newList); + state.selectedIdxs.clear(); + if (!newList.length) { + showEmptyGrid(); + btnRename.disabled = true; + setStatus('이미지 파일 없음', false); + } else { + renderGrid(); + setStatus(`${state.files.length}개 파일 로드됨`, true); + } + } catch (e) { + hideLoading(); + alert('오류 발생: ' + e.message); + } +} diff --git a/src/renderer/rename.js b/src/renderer/rename.js new file mode 100644 index 0000000..3d4099b --- /dev/null +++ b/src/renderer/rename.js @@ -0,0 +1,78 @@ +import { state } from './state.js'; +import { showLoading, hideLoading, setStatus, showToast } from './ui.js'; +import { renderGrid } from './grid.js'; + +const inputPrefix = document.getElementById('input-prefix'); +const inputDigits = document.getElementById('input-digits'); +const renameConflictModal = document.getElementById('rename-conflict-modal'); +const renameConflictCloseBtn = document.getElementById('rename-conflict-close'); +const renameConflictList = document.getElementById('rename-conflict-list'); + +renameConflictCloseBtn.addEventListener('click', () => { + renameConflictModal.style.display = 'none'; +}); + +export 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 selectedArr = [...state.selectedIdxs].sort((a, b) => a - b); + const filesToRename = selectedArr.length > 0 + ? selectedArr.map(i => state.files[i]).filter(Boolean) + : state.files; + + if (!filesToRename.length) return; + + // 선택된 파일이 있으면 변경될 이름이 미선택 파일 이름과 충돌하는지 확인 + if (selectedArr.length > 0) { + const unselectedNames = new Set( + state.files.filter((_, i) => !state.selectedIdxs.has(i)).map(f => f.name) + ); + const newNames = filesToRename.map((f, i) => { + const ext = f.name.split('.').pop().toLowerCase(); + return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; + }); + const conflicting = newNames.filter(n => unselectedNames.has(n)); + if (conflicting.length) { + renameConflictList.innerHTML = conflicting.map(n => `
  • ${n}
  • `).join(''); + renameConflictModal.style.display = 'flex'; + return; + } + } + + const examples = filesToRename.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 = filesToRename.length > 3 ? `\n... 외 ${filesToRename.length - 3}개` : ''; + + if (!confirm(`📂 ${filesToRename.length}개 파일을 이름 변경합니다:\n\n${examples}${more}\n\n계속하시겠습니까?`)) return; + + showLoading('파일 이름 변경 중...'); + setStatus('이름 변경 중...', false); + + try { + const result = await window.api.renameFiles({ + folderPath: state.currentFolder, + files: filesToRename, + 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); + } +} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index d2bf384..d705c14 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -1,68 +1,47 @@ -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'; -import * as fileExplorer from './handlers/file-explorer.js'; - -let _cardObserver = null; +import { state } from './state.js'; +import { showLoading, hideLoading, setStatus } from './ui.js'; +import { + renderGrid, updateSelCount, updateNumberBadges, + updateNewNameLabels, refreshCardSelection, showEmptyGrid, +} from './grid.js'; +import { doRename } from './rename.js'; +import { handleFolderDrop } from './move.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'; +import * as fileExplorer from './handlers/file-explorer.js'; // ── 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 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 btnPreviewToggle = document.getElementById('btn-preview-toggle'); -const moveModal = document.getElementById('move-modal'); -const movePrefixInput = document.getElementById('move-prefix'); -const moveDigitsInput = document.getElementById('move-digits'); -const moveCancelBtn = document.getElementById('move-cancel'); -const moveConfirmBtn = document.getElementById('move-confirm'); -const moveErrorModal = document.getElementById('move-error-modal'); -const moveErrorCloseBtn = document.getElementById('move-error-close'); -const renameConflictModal = document.getElementById('rename-conflict-modal'); -const renameConflictCloseBtn = document.getElementById('rename-conflict-close'); -const renameConflictList = document.getElementById('rename-conflict-list'); +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 inputPrefix = document.getElementById('input-prefix'); +const inputDigits = document.getElementById('input-digits'); +const previewName = document.getElementById('preview-name'); +const dirPath = document.getElementById('dir-path'); +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 btnPreviewToggle = document.getElementById('btn-preview-toggle'); +const fileExplorerContainer = document.querySelector('.file-explorer'); // ── 핸들러 초기화 ───────────────────────────────── -const callbacks = { - refreshCardSelection, - updateSelCount, - updateNumberBadges, - updateNewNameLabels, -}; - -const fileExplorerContainer = document.querySelector('.file-explorer'); +const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels }; pinchZoom.init({ gridContainer, thumbSizeHint }); selection.init({ gridContainer, rubberBand, grid, state, callbacks }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop }); hoverPreview.init(); - fileExplorer.init(fileExplorerContainer, loadFolder); window.api.onFullscreenChange((isFullscreen) => { document.body.classList.toggle('fullscreen', isFullscreen); }); -// 앱 시작 시 마지막 폴더 또는 홈 디렉토리 로드 +// ── 앱 시작 ─────────────────────────────────────── (async () => { const [lastFolder, homeDir, previewOn] = await Promise.all([ window.api.getLastFolder(), @@ -78,32 +57,35 @@ window.api.onFullscreenChange((isFullscreen) => { await loadFolder(target); })(); -// ── 버튼 이벤트 ────────────────────────────────── +// ── 버튼 / 키보드 이벤트 ───────────────────────── 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(); - if (e.code === 'KeyA') { state.files.forEach((_, i) => state.selectedIdxs.add(i)); refreshCardSelection(); updateSelCount(); } -}); +btnSortName.addEventListener('click', () => sortFiles('name')); +btnSortDate.addEventListener('click', () => sortFiles('date')); +btnRename.addEventListener('click', doRename); btnPreviewToggle.addEventListener('click', () => { const on = btnPreviewToggle.classList.toggle('active'); hoverPreview.setEnabled(on); window.api.setPreviewOn(on); }); -btnSortName.addEventListener('click', () => sortFiles('name')); -btnSortDate.addEventListener('click', () => sortFiles('date')); -btnRename.addEventListener('click', doRename); + +document.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT') return; + if (e.code === 'KeyO') btnOpen.click(); + if (e.code === 'KeyS' && !btnRename.disabled) btnRename.click(); + if (e.code === 'KeyA') { + state.files.forEach((_, i) => state.selectedIdxs.add(i)); + refreshCardSelection(); + updateSelCount(); + } +}); // ── 폴더 열기 (다이얼로그) ─────────────────────── async function openFolder() { const folder = await window.api.selectFolder(); if (!folder) return; const homeDir = await window.api.getHomeDir(); - // 선택한 폴더가 홈 디렉토리 하위인지 확인해서 트리 업데이트 if (folder.startsWith(homeDir)) { await fileExplorer.loadTree(homeDir, folder); } else { @@ -127,11 +109,7 @@ async function loadFolder(folder) { if (!list.length) { state.files.length = 0; state.selectedIdxs.clear(); - grid.innerHTML = ''; - grid.style.display = 'none'; - emptyState.style.display = ''; - fileCount.textContent = '0개 파일'; - updateSelCount(); + showEmptyGrid(); btnRename.disabled = true; setStatus('이미지 파일 없음', false); return; @@ -152,84 +130,6 @@ async function loadFolder(folder) { btnRename.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; @@ -247,7 +147,7 @@ function updateSortButtons() { 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)); @@ -255,226 +155,3 @@ function updatePreview() { previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`; updateNewNameLabels(); } - -// ── 이름 변경 ──────────────────────────────────── -renameConflictCloseBtn.addEventListener('click', () => { - renameConflictModal.style.display = 'none'; -}); - -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 selectedArr = [...state.selectedIdxs].sort((a, b) => a - b); - const filesToRename = selectedArr.length > 0 - ? selectedArr.map(i => state.files[i]).filter(Boolean) - : state.files; - - if (!filesToRename.length) return; - - // 선택된 파일이 있으면 변경될 이름이 미선택 파일 이름과 충돌하는지 확인 - if (selectedArr.length > 0) { - const unselectedNames = new Set( - state.files.filter((_, i) => !state.selectedIdxs.has(i)).map(f => f.name) - ); - const newNames = filesToRename.map((f, i) => { - const ext = f.name.split('.').pop().toLowerCase(); - return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; - }); - const conflicting = newNames.filter(n => unselectedNames.has(n)); - if (conflicting.length) { - renameConflictList.innerHTML = conflicting.map(n => `
  • ${n}
  • `).join(''); - renameConflictModal.style.display = 'flex'; - return; - } - } - - const examples = filesToRename.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 = filesToRename.length > 3 ? `\n... 외 ${filesToRename.length - 3}개` : ''; - - if (!confirm(`📂 ${filesToRename.length}개 파일을 이름 변경합니다:\n\n${examples}${more}\n\n계속하시겠습니까?`)) return; - - showLoading('파일 이름 변경 중...'); - setStatus('이름 변경 중...', false); - - try { - const result = await window.api.renameFiles({ - folderPath: state.currentFolder, - files: filesToRename, - 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); - } -} - -// ── 폴더로 파일 이동 ───────────────────────────── - -let _pendingMoveTarget = null; -let _pendingMoveFiles = null; - -async function handleFolderDrop(targetPath, fileIndices) { - if (!fileIndices.length || !state.currentFolder) return; - if (targetPath === state.currentFolder) return; - - const sorted = [...fileIndices].sort((a, b) => a - b); - const selectedFiles = sorted.map(i => state.files[i]).filter(Boolean); - if (!selectedFiles.length) return; - - const names = selectedFiles.map(f => f.name); - const conflicts = await window.api.checkFilesExist(targetPath, names); - - if (!conflicts.length) { - await _doMoveFiles(targetPath, selectedFiles); - } else { - _pendingMoveTarget = targetPath; - _pendingMoveFiles = selectedFiles; - movePrefixInput.value = 'temp'; - moveModal.style.display = 'flex'; - } -} - -moveCancelBtn.addEventListener('click', () => { - moveModal.style.display = 'none'; - _pendingMoveTarget = null; - _pendingMoveFiles = null; -}); - -moveConfirmBtn.addEventListener('click', async () => { - const prefix = movePrefixInput.value.trim() || 'photo'; - const digits = Math.max(1, Math.min(6, parseInt(moveDigitsInput.value) || 4)); - - const newNames = _pendingMoveFiles.map((f, i) => { - const ext = f.name.split('.').pop().toLowerCase(); - return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; - }); - - const conflicts = await window.api.checkFilesExist(_pendingMoveTarget, newNames); - if (conflicts.length) { - moveModal.style.display = 'none'; - moveErrorModal.style.display = 'flex'; - return; - } - - moveModal.style.display = 'none'; - const targetPath = _pendingMoveTarget; - const filesToMove = _pendingMoveFiles.map((f, i) => ({ ...f, newName: newNames[i] })); - _pendingMoveTarget = null; - _pendingMoveFiles = null; - await _doMoveFiles(targetPath, filesToMove); -}); - -moveErrorCloseBtn.addEventListener('click', () => { - moveErrorModal.style.display = 'none'; -}); - -async function _doMoveFiles(targetPath, files) { - showLoading('파일 이동 중...'); - try { - const result = await window.api.moveFiles({ targetFolder: targetPath, files }); - hideLoading(); - if (result.errors.length) { - alert('일부 오류:\n' + result.errors.join('\n')); - } else { - showToast(`✅ ${result.moved.length}개 파일 이동 완료!`); - setStatus(`${result.moved.length}개 파일 이동 완료`, true); - } - const newList = await window.api.listImages(state.currentFolder); - state.files.length = 0; - state.files.push(...newList); - state.selectedIdxs.clear(); - if (!newList.length) { - grid.innerHTML = ''; - grid.style.display = 'none'; - emptyState.style.display = ''; - fileCount.textContent = '0개 파일'; - updateSelCount(); - btnRename.disabled = true; - setStatus('이미지 파일 없음', false); - } else { - renderGrid(); - setStatus(`${state.files.length}개 파일 로드됨`, true); - } - } 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); -} diff --git a/src/renderer/ui.js b/src/renderer/ui.js new file mode 100644 index 0000000..2324856 --- /dev/null +++ b/src/renderer/ui.js @@ -0,0 +1,27 @@ +const loading = document.getElementById('loading'); +const loadingText = document.getElementById('loading-text'); +const toast = document.getElementById('toast'); +const statusDot = document.getElementById('status-dot'); +const statusText = document.getElementById('status-text'); + +export function showLoading(msg = '처리 중...') { + loadingText.textContent = msg; + loading.classList.add('show'); +} + +export function hideLoading() { + loading.classList.remove('show'); +} + +export function setStatus(msg, ready) { + statusText.textContent = msg; + statusDot.className = 'status-dot' + (ready ? ' ready' : ''); +} + +export 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); +}