Undo, Redo 기능 추가

This commit is contained in:
2026-05-15 22:24:25 +09:00
parent 31ecf94142
commit a1df424485
4 changed files with 96 additions and 2 deletions
+2
View File
@@ -266,6 +266,7 @@ function _moveSingle(srcIdx, targetIdx) {
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
_cb.updateNumberBadges();
_cb.updateNewNameLabels();
_cb.pushHistory?.([moved.path]);
}
function _moveMulti(targetIdx) {
@@ -301,6 +302,7 @@ function _moveMulti(targetIdx) {
_cb.updateNewNameLabels();
_cb.refreshCardSelection();
_cb.updateSelCount();
_cb.pushHistory?.(movedFiles.map(f => f.path));
}
// ── 자동 스크롤 ───────────────────────────────────
+33
View File
@@ -0,0 +1,33 @@
const MAX = 50;
let _stack = []; // each entry: { files: [], affectedPaths: Set | null }
let _cursor = -1;
export function push(files, affectedPaths = null) {
_stack = _stack.slice(0, _cursor + 1);
_stack.push({ files: [...files], affectedPaths: affectedPaths ? new Set(affectedPaths) : null });
if (_stack.length > MAX) _stack.shift();
_cursor = _stack.length - 1;
}
// Returns { files, affectedPaths } where affectedPaths are from the entry being undone
export function undo() {
if (!canUndo()) return null;
const affectedPaths = _stack[_cursor].affectedPaths;
--_cursor;
return { files: [..._stack[_cursor].files], affectedPaths };
}
// Returns { files, affectedPaths } from the entry being redone
export function redo() {
if (!canRedo()) return null;
++_cursor;
return { files: [..._stack[_cursor].files], affectedPaths: _stack[_cursor].affectedPaths };
}
export function canUndo() { return _cursor > 0; }
export function canRedo() { return _cursor < _stack.length - 1; }
export function reset() {
_stack = [];
_cursor = -1;
}
+3
View File
@@ -32,6 +32,9 @@
</div>
<div class="separator"></div>
<button class="btn btn-info" id="btn-preview-toggle">미리보기</button>
<div class="separator"></div>
<button class="btn btn-ghost" id="btn-undo" disabled>↩ 되돌리기</button>
<button class="btn btn-ghost" id="btn-redo" disabled>↪ 다시하기</button>
</nav>
<form id="format-bar">
+58 -2
View File
@@ -1,5 +1,6 @@
import { state } from './state.js';
import { showLoading, hideLoading, setStatus } from './ui.js';
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import * as history from './history.js';
import {
renderGrid, updateSelCount, updateNumberBadges,
updateNewNameLabels, refreshCardSelection, showEmptyGrid,
@@ -28,10 +29,15 @@ 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 btnUndo = document.getElementById('btn-undo');
const btnRedo = document.getElementById('btn-redo');
const fileExplorerContainer = document.querySelector('.file-explorer');
// ── 핸들러 초기화 ─────────────────────────────────
const callbacks = { refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels };
const callbacks = {
refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels,
pushHistory: (affectedPaths) => { history.push(state.files, affectedPaths); updateHistoryButtons(); },
};
pinchZoom.init({ gridContainer, thumbSizeHint });
selection.init({ gridContainer, rubberBand, grid, state, callbacks });
@@ -67,6 +73,8 @@ btnOpen.addEventListener('click', openFolder);
btnSortName.addEventListener('click', () => sortFiles('name'));
btnSortDate.addEventListener('click', () => sortFiles('date'));
btnRename.addEventListener('click', doRename);
btnUndo.addEventListener('click', performUndo);
btnRedo.addEventListener('click', performRedo);
btnPreviewToggle.addEventListener('click', () => {
const on = btnPreviewToggle.classList.toggle('active');
hoverPreview.setEnabled(on);
@@ -82,6 +90,8 @@ document.addEventListener('keydown', (e) => {
refreshCardSelection();
updateSelCount();
}
if (e.code === 'KeyU') performUndo();
if (e.code === 'KeyR') performRedo();
});
// ── 폴더 열기 (다이얼로그) ───────────────────────
@@ -131,6 +141,9 @@ async function loadFolder(folder) {
renderGrid();
updatePreview();
btnRename.disabled = false;
history.reset();
history.push(state.files);
updateHistoryButtons();
}
// ── 정렬 ─────────────────────────────────────────
@@ -143,6 +156,8 @@ async function sortFiles(mode) {
updateSortButtons();
await renderGrid();
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
history.push(state.files);
updateHistoryButtons();
}
function updateSortButtons() {
@@ -150,6 +165,47 @@ function updateSortButtons() {
btnSortDate.classList.toggle('active', state.sortMode === 'date');
}
// ── Undo / Redo ───────────────────────────────────
function updateHistoryButtons() {
btnUndo.disabled = !history.canUndo();
btnRedo.disabled = !history.canRedo();
}
function performUndo() {
if (!history.canUndo()) {
showToast('더 이상 되돌릴 수 없습니다');
return;
}
_applyHistorySnapshot(history.undo());
}
function performRedo() {
if (!history.canRedo()) {
showToast('마지막 작업 상태입니다');
return;
}
_applyHistorySnapshot(history.redo());
}
function _applyHistorySnapshot({ files, affectedPaths }) {
state.files.length = 0;
state.files.push(...files);
state.selectedIdxs.clear();
if (affectedPaths) {
const pathToIdx = new Map(state.files.map((f, i) => [f.path, i]));
affectedPaths.forEach(path => {
const idx = pathToIdx.get(path);
if (idx !== undefined) state.selectedIdxs.add(idx);
});
}
state.sortMode = null;
updateSortButtons();
renderGrid();
updateHistoryButtons();
}
// ── 포맷 미리보기 ─────────────────────────────────
function updatePreview() {
const prefix = inputPrefix.value.trim() || 'photo';