리펙토링 - 전체 프로젝트 코드 (Codex)

This commit is contained in:
2026-05-17 22:18:01 +09:00
parent 5337ea221b
commit 0f72f4a0f0
16 changed files with 508 additions and 356 deletions
+42 -178
View File
@@ -1,13 +1,16 @@
import { state } from './state.js';
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import * as history from './history.js';
import {
renderGrid, updateSelCount, updateNumberBadges,
updateNewNameLabels, refreshCardSelection, showEmptyGrid,
updateSelCount, updateNumberBadges,
updateNewNameLabels, refreshCardSelection,
} from './grid.js';
import { doRename } from './rename.js';
import { handleFolderDrop } from './move.js';
import { initModalControls } from './modal.js';
import { clampDigits, getExtension } from './utils/file-names.js';
import { initKeyboardShortcuts } from './keyboard.js';
import { createFolderController } from './folder-controller.js';
import { createHistoryController } from './history-controller.js';
import * as deleteHandler from './delete.js';
import * as pinchZoom from './handlers/pinch-zoom.js';
import * as selection from './handlers/selection.js';
@@ -15,8 +18,6 @@ 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 _homeDir = null;
// ── DOM ──────────────────────────────────────────────
const btnOpen = document.getElementById('btn-open');
const btnSortName = document.getElementById('btn-sort-name');
@@ -36,8 +37,23 @@ const btnUndo = document.getElementById('btn-undo');
const btnRedo = document.getElementById('btn-redo');
const fileExplorerContainer = document.querySelector('.file-explorer');
const folderController = createFolderController({
btnRename,
btnSortName,
btnSortDate,
dirPath,
inputPrefix,
updatePreview,
});
const historyController = createHistoryController({
btnRename,
btnUndo,
btnRedo,
updateSortButtons: folderController.updateSortButtons,
});
// ── 핸들러 초기화 ─────────────────────────────────
history.setOnUpdate(updateHistoryButtons);
history.setOnUpdate(historyController.updateButtons);
initModalControls();
const callbacks = {
@@ -50,7 +66,7 @@ selection.init({ gridContainer, rubberBand, grid, state, callbacks });
dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks, fileExplorerEl: fileExplorerContainer, onFolderDrop: handleFolderDrop });
hoverPreview.init();
deleteHandler.init(grid);
fileExplorer.init(fileExplorerContainer, loadFolder);
fileExplorer.init(fileExplorerContainer, folderController.loadFolder);
window.api.onFullscreenChange((isFullscreen) => {
document.body.classList.toggle('fullscreen', isFullscreen);
@@ -63,202 +79,50 @@ window.api.onFullscreenChange((isFullscreen) => {
window.api.getHomeDir(),
window.api.getPreviewOn(),
]);
_homeDir = homeDir;
folderController.setHomeDir(homeDir);
btnPreviewToggle.classList.toggle('active', previewOn);
hoverPreview.setEnabled(previewOn);
const target = lastFolder || homeDir;
await fileExplorer.loadTree(homeDir, target);
await loadFolder(target);
await folderController.loadFolder(target);
})();
// ── 버튼 / 키보드 이벤트 ─────────────────────────
inputPrefix.addEventListener('input', updatePreview);
inputDigits.addEventListener('input', updatePreview);
btnOpen.addEventListener('click', openFolder);
btnSortName.addEventListener('click', () => sortFiles('name'));
btnSortDate.addEventListener('click', () => sortFiles('date'));
btnOpen.addEventListener('click', folderController.openFolder);
btnSortName.addEventListener('click', () => folderController.sortFiles('name'));
btnSortDate.addEventListener('click', () => folderController.sortFiles('date'));
btnRename.addEventListener('click', doRename);
btnUndo.addEventListener('click', performUndo);
btnRedo.addEventListener('click', performRedo);
btnUndo.addEventListener('click', historyController.performUndo);
btnRedo.addEventListener('click', historyController.performRedo);
btnPreviewToggle.addEventListener('click', () => {
const on = btnPreviewToggle.classList.toggle('active');
hoverPreview.setEnabled(on);
window.api.setPreviewOn(on);
});
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));
initKeyboardShortcuts({
btnOpen,
btnRename,
btnPreviewToggle,
deleteSelected: deleteHandler.deleteSelected,
performUndo: historyController.performUndo,
performRedo: historyController.performRedo,
refreshSelection: () => {
refreshCardSelection();
updateSelCount();
}
if (e.metaKey && !e.shiftKey && e.code === 'KeyZ') { e.preventDefault(); performUndo(); }
if (e.metaKey && e.shiftKey && e.code === 'KeyZ') { e.preventDefault(); performRedo(); }
if (e.code === 'KeyP') btnPreviewToggle.click();
if (e.code === 'Backspace' || e.code === 'Delete') deleteHandler.deleteSelected();
},
selectAll: () => state.files.forEach((_, i) => state.selectedIdxs.add(i)),
});
// ── 폴더 열기 (다이얼로그) ───────────────────────
async function openFolder() {
const folder = await window.api.selectFolder();
if (!folder) return;
const homeDir = _homeDir;
if (folder.startsWith(homeDir)) {
await fileExplorer.loadTree(homeDir, folder);
} else {
await fileExplorer.loadTree(folder, folder);
}
await loadFolder(folder);
}
// ── 폴더 로드 (트리 클릭 또는 다이얼로그 공통) ──
async function loadFolder(folder) {
if (!folder) return;
state.currentFolder = folder;
dirPath.textContent = folder;
await window.api.setLastFolder(folder);
await fileExplorer.setSelectedPath(folder);
showLoading('이미지 목록 불러오는 중...');
const list = await window.api.listImages(folder);
hideLoading();
if (!list.length) {
state.files.length = 0;
state.selectedIdxs.clear();
showEmptyGrid();
btnRename.disabled = true;
setStatus('이미지 파일 없음', false);
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;
history.reset();
history.pushReorder(state.files);
}
// ── 정렬 ─────────────────────────────────────────
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);
history.pushReorder(state.files);
}
function updateSortButtons() {
btnSortName.classList.toggle('active', state.sortMode === 'name');
btnSortDate.classList.toggle('active', state.sortMode === 'date');
}
// ── Undo / Redo ───────────────────────────────────
let _historyBusy = false;
function updateHistoryButtons() {
btnUndo.disabled = !history.canUndo();
btnRedo.disabled = !history.canRedo();
}
async function performUndo() {
if (_historyBusy || !history.canUndo()) {
if (!history.canUndo()) showToast('더 이상 되돌릴 수 없습니다');
return;
}
_historyBusy = true;
try {
const { entry, targetFiles } = history.undo();
if (entry.type === 'delete') {
const { restored, errors } = await window.api.restoreStagedFiles(entry.deletedEntries);
if (errors.length) showToast('일부 파일 복원 실패: ' + errors[0]);
_applyHistorySnapshot(targetFiles, new Set(restored));
if (restored.length) setStatus(`${restored.length}개 파일 복원됨`, true);
} else {
_applyHistorySnapshot(targetFiles, entry.affectedPaths);
}
} finally {
_historyBusy = false;
}
}
async function performRedo() {
if (_historyBusy || !history.canRedo()) {
if (!history.canRedo()) showToast('마지막 작업 상태입니다');
return;
}
_historyBusy = true;
try {
const nextEntry = history.peekRedo();
if (nextEntry.type === 'delete') {
const pathsToRedo = nextEntry.deletedEntries.map(e => e.originalPath);
const { staged, errors } = await window.api.stageDeleteFiles(pathsToRedo);
if (errors.length) showToast('일부 파일 삭제 실패: ' + errors[0]);
const stagedByPath = new Map(staged.map(s => [s.originalPath, s.stagingPath]));
nextEntry.deletedEntries.forEach(e => {
if (stagedByPath.has(e.originalPath)) e.stagingPath = stagedByPath.get(e.originalPath);
});
const { targetFiles } = history.redo();
_applyHistorySnapshot(targetFiles, null);
if (staged.length) setStatus(`${staged.length}개 파일 삭제됨`, true);
} else {
const { entry, targetFiles } = history.redo();
_applyHistorySnapshot(targetFiles, entry.affectedPaths);
}
} finally {
_historyBusy = false;
}
}
function _applyHistorySnapshot(targetFiles, affectedPaths = null) {
state.files.length = 0;
state.files.push(...targetFiles);
state.selectedIdxs.clear();
if (affectedPaths && affectedPaths.size > 0) {
const pathToIdx = new Map(state.files.map((f, i) => [f.path, i]));
affectedPaths.forEach(p => {
const idx = pathToIdx.get(p);
if (idx !== undefined) state.selectedIdxs.add(idx);
});
}
state.sortMode = null;
updateSortButtons();
if (!state.files.length) {
showEmptyGrid();
btnRename.disabled = true;
} else {
renderGrid();
btnRename.disabled = false;
}
updateHistoryButtons();
}
// ── 포맷 미리보기 ─────────────────────────────────
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';
const digits = clampDigits(inputDigits.value);
const ext = state.files.length ? getExtension(state.files[0].name) : 'jpg';
previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`;
updateNewNameLabels();
}