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