-
+
+
이동 오류
새로운 이름의 파일도 존재합니다.
-
+
diff --git a/src/renderer/modal.js b/src/renderer/modal.js
new file mode 100644
index 0000000..5cecc28
--- /dev/null
+++ b/src/renderer/modal.js
@@ -0,0 +1,94 @@
+export function initModalControls() {
+ document.addEventListener('click', e => {
+ const closeBtn = e.target.closest('.modal-close');
+ if (!closeBtn) return;
+
+ const modal = closeBtn.closest('.modal-overlay');
+ if (!modal) return;
+
+ closeModal(modal);
+ });
+
+ document.addEventListener('keydown', e => {
+ const modal = getActiveModal();
+ if (!modal) return;
+
+ if (e.code === 'Escape') {
+ const closeBtn = modal.querySelector('.modal-close');
+ if (!closeBtn) return;
+ e.preventDefault();
+ closeBtn.click();
+ return;
+ }
+
+ if (e.code === 'Enter') {
+ const actionBtn = modal.querySelector('.modal-action');
+ if (!actionBtn) return;
+ e.preventDefault();
+ actionBtn.click();
+ return;
+ }
+
+ if (e.code === 'Tab') {
+ trapFocus(e, modal);
+ }
+ });
+}
+
+export function openModal(modal) {
+ modal.style.display = 'flex';
+ const primaryBtn = modal.querySelector('.modal-action');
+ const closeBtn = modal.querySelector('.modal-close');
+ (primaryBtn || closeBtn)?.focus();
+}
+
+export function closeModal(modal) {
+ modal.style.display = 'none';
+ modal.dispatchEvent(new CustomEvent('modal:closed'));
+}
+
+function getActiveModal() {
+ const modals = [...document.querySelectorAll('.modal-overlay')];
+ return modals.find(modal => modal.style.display === 'flex') || null;
+}
+
+function trapFocus(e, modal) {
+ const focusable = getFocusableElements(modal);
+ if (!focusable.length) {
+ e.preventDefault();
+ modal.focus();
+ return;
+ }
+
+ const first = focusable[0];
+ const last = focusable[focusable.length - 1];
+ const active = document.activeElement;
+
+ if (!modal.contains(active)) {
+ e.preventDefault();
+ first.focus();
+ return;
+ }
+
+ if (e.shiftKey && active === first) {
+ e.preventDefault();
+ last.focus();
+ } else if (!e.shiftKey && active === last) {
+ e.preventDefault();
+ first.focus();
+ }
+}
+
+function getFocusableElements(container) {
+ const selector = [
+ 'button:not([disabled])',
+ 'input:not([disabled])',
+ 'select:not([disabled])',
+ 'textarea:not([disabled])',
+ 'a[href]',
+ '[tabindex]:not([tabindex="-1"])',
+ ].join(',');
+
+ return [...container.querySelectorAll(selector)]
+ .filter(el => !el.hidden && el.offsetParent !== null);
+}
diff --git a/src/renderer/move.js b/src/renderer/move.js
index b14b663..78804ad 100644
--- a/src/renderer/move.js
+++ b/src/renderer/move.js
@@ -2,53 +2,48 @@ import { state } from './state.js';
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import { renderGrid, updateSelCount, showEmptyGrid } from './grid.js';
import * as history from './history.js';
+import { openModal, closeModal } from './modal.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';
+moveModal.addEventListener('modal:closed', () => {
_pendingMoveTarget = null;
_pendingMoveFiles = null;
});
moveConfirmBtn.addEventListener('click', async () => {
+ if (!_pendingMoveTarget || !_pendingMoveFiles) return;
+
const prefix = movePrefixInput.value.trim() || 'photo';
const digits = Math.max(1, Math.min(6, parseInt(moveDigitsInput.value) || 4));
+ const targetPath = _pendingMoveTarget;
+ const pendingFiles = _pendingMoveFiles;
- const newNames = _pendingMoveFiles.map((f, i) => {
+ const newNames = pendingFiles.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);
+ const conflicts = await window.api.checkFilesExist(targetPath, newNames);
if (conflicts.length) {
- moveModal.style.display = 'none';
- moveErrorModal.style.display = 'flex';
+ closeModal(moveModal);
+ openModal(moveErrorModal);
return;
}
- moveModal.style.display = 'none';
- const targetPath = _pendingMoveTarget;
- const filesToMove = _pendingMoveFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
- _pendingMoveTarget = null;
- _pendingMoveFiles = null;
+ const filesToMove = pendingFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
+ closeModal(moveModal);
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;
@@ -66,7 +61,7 @@ export async function handleFolderDrop(targetPath, fileIndices) {
_pendingMoveTarget = targetPath;
_pendingMoveFiles = selectedFiles;
movePrefixInput.value = 'temp';
- moveModal.style.display = 'flex';
+ openModal(moveModal);
}
}
diff --git a/src/renderer/rename.js b/src/renderer/rename.js
index 63a473f..1f3344e 100644
--- a/src/renderer/rename.js
+++ b/src/renderer/rename.js
@@ -2,32 +2,28 @@ import { state } from './state.js';
import { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import { renderGrid } from './grid.js';
import * as history from './history.js';
+import { openModal, closeModal } from './modal.js';
const inputPrefix = document.getElementById('input-prefix');
const inputDigits = document.getElementById('input-digits');
const renameConfirmModal = document.getElementById('rename-confirm-modal');
-const renameConfirmTitle = document.getElementById('rename-confirm-title');
+const renameConfirmSelectedTitle = renameConfirmModal.querySelector('.rename-title-selected');
+const renameConfirmAllTitle = renameConfirmModal.querySelector('.rename-title-all');
const renameConfirmMsg = document.getElementById('rename-confirm-msg');
const renameConfirmWarning = document.getElementById('rename-confirm-selection-warning');
const renameConfirmList = document.getElementById('rename-confirm-list');
-const renameConfirmCancelBtn = document.getElementById('rename-confirm-cancel');
const renameConfirmOkBtn = document.getElementById('rename-confirm-ok');
const renameConflictModal = document.getElementById('rename-conflict-modal');
-const renameConflictCloseBtn = document.getElementById('rename-conflict-close');
const renameConflictList = document.getElementById('rename-conflict-list');
let _renameConfirmResolve = null;
-renameConfirmCancelBtn.addEventListener('click', () => {
- closeRenameConfirm(false);
-});
-
renameConfirmOkBtn.addEventListener('click', () => {
closeRenameConfirm(true);
});
-renameConflictCloseBtn.addEventListener('click', () => {
- renameConflictModal.style.display = 'none';
+renameConfirmModal.addEventListener('modal:closed', () => {
+ resolveRenameConfirm(false);
});
export async function doRename() {
@@ -58,7 +54,7 @@ export async function doRename() {
li.textContent = n;
return li;
}));
- renameConflictModal.style.display = 'flex';
+ openModal(renameConflictModal);
return;
}
}
@@ -108,7 +104,8 @@ export async function doRename() {
}
function showRenameConfirm({ count, examples, more, isPartial }) {
- renameConfirmTitle.textContent = isPartial ? '선택된 이미지 저장' : '전체 이미지 저장';
+ renameConfirmSelectedTitle.hidden = !isPartial;
+ renameConfirmAllTitle.hidden = isPartial;
renameConfirmMsg.textContent = `${count}개 파일을 이름 변경합니다. 계속하시겠습니까?`;
renameConfirmWarning.style.display = isPartial ? 'block' : 'none';
@@ -120,8 +117,7 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
return li;
}));
- renameConfirmModal.style.display = 'flex';
- renameConfirmOkBtn.focus();
+ openModal(renameConfirmModal);
return new Promise(resolve => {
_renameConfirmResolve = resolve;
@@ -129,7 +125,11 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
}
function closeRenameConfirm(confirmed) {
- renameConfirmModal.style.display = 'none';
+ resolveRenameConfirm(confirmed);
+ closeModal(renameConfirmModal);
+}
+
+function resolveRenameConfirm(confirmed) {
if (_renameConfirmResolve) {
_renameConfirmResolve(confirmed);
_renameConfirmResolve = null;
diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js
index 74dec9b..4796a3e 100644
--- a/src/renderer/renderer.js
+++ b/src/renderer/renderer.js
@@ -7,6 +7,7 @@ import {
} from './grid.js';
import { doRename } from './rename.js';
import { handleFolderDrop } from './move.js';
+import { initModalControls } from './modal.js';
import * as deleteHandler from './delete.js';
import * as pinchZoom from './handlers/pinch-zoom.js';
import * as selection from './handlers/selection.js';
@@ -37,6 +38,7 @@ const fileExplorerContainer = document.querySelector('.file-explorer');
// ── 핸들러 초기화 ─────────────────────────────────
history.setOnUpdate(updateHistoryButtons);
+initModalControls();
const callbacks = {
refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels,
diff --git a/src/renderer/style.css b/src/renderer/style.css
index b502d8d..1956377 100644
--- a/src/renderer/style.css
+++ b/src/renderer/style.css
@@ -9,6 +9,7 @@
--accent2: #e96af7;
--success: #6af7a0;
--warning: #f7c26a;
+ --danger: #ff6b7a;
--info: #6ac8f7;
--text: #e8e8f0;
--muted: #8f8fb8;
@@ -548,6 +549,16 @@ footer span { font-size: 11px; color: var(--muted); }
font-size: 15px;
font-weight: 700;
}
+.modal-titlebar-warning {
+ color: var(--warning);
+ background: rgba(247,194,106,.12);
+ border-bottom-color: rgba(247,194,106,.35);
+}
+.modal-titlebar-danger {
+ color: var(--danger);
+ background: rgba(255,107,122,.12);
+ border-bottom-color: rgba(255,107,122,.35);
+}
.modal-msg {
font-size: 14px; color: var(--text); line-height: 1.7;
}