팝업창 제목 스타일 적용, 단축키 추가

This commit is contained in:
2026-05-17 21:49:58 +09:00
parent a3d2909bf6
commit 5337ea221b
7 changed files with 157 additions and 51 deletions
+5 -6
View File
@@ -2,17 +2,16 @@ import { state } from './state.js';
import { setStatus, showToast } from './ui.js';
import { renderGrid, showEmptyGrid } from './grid.js';
import * as history from './history.js';
import { openModal, closeModal } from './modal.js';
const deleteModal = document.getElementById('delete-modal');
const deleteFileList = document.getElementById('delete-file-list');
const deleteCancelBtn = document.getElementById('delete-cancel');
const deleteConfirmBtn = document.getElementById('delete-confirm');
const btnRename = document.getElementById('btn-rename');
let _pendingFiles = null;
deleteCancelBtn.addEventListener('click', () => {
deleteModal.style.display = 'none';
deleteModal.addEventListener('modal:closed', () => {
_pendingFiles = null;
});
@@ -20,7 +19,7 @@ deleteConfirmBtn.addEventListener('click', async () => {
if (!_pendingFiles) return;
const files = _pendingFiles;
_pendingFiles = null;
deleteModal.style.display = 'none';
closeModal(deleteModal);
await _doDelete(files);
});
@@ -35,7 +34,7 @@ export function deleteSelected() {
return li;
}));
_pendingFiles = filesToDelete;
deleteModal.style.display = 'flex';
openModal(deleteModal);
}
export function init(gridEl) {
@@ -63,7 +62,7 @@ export function init(gridEl) {
return li;
}));
_pendingFiles = filesToDelete;
deleteModal.style.display = 'flex';
openModal(deleteModal);
});
}
+18 -13
View File
@@ -79,58 +79,63 @@
<div id="toast"></div>
<div id="move-modal" class="modal-overlay" style="display:none;">
<div class="modal-box">
<div class="modal-box modal-box-with-title">
<div class="modal-titlebar">이미지 이동</div>
<p class="modal-msg">대상 폴더에 이미 같은 이름의 파일이 존재합니다.<br>새로운 이름으로 파일들을 이동하시겠습니까?</p>
<div class="modal-fields">
<label>파일명 포맷<input type="text" id="move-prefix" value="photo" placeholder="접두사" /></label>
<label>자리 순번 숫자<input type="number" id="move-digits" value="4" min="1" max="6" /></label>
</div>
<div class="modal-actions">
<button id="move-cancel" class="btn btn-ghost">취소</button>
<button id="move-confirm" class="btn btn-success">실행</button>
<button id="move-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="move-confirm" class="btn btn-success modal-action">실행</button>
</div>
</div>
</div>
<div id="rename-confirm-modal" class="modal-overlay" style="display:none;">
<div class="modal-box modal-box-with-title">
<div class="modal-titlebar" id="rename-confirm-title"></div>
<div class="modal-titlebar rename-title-selected" hidden>선택된 이미지 저장</div>
<div class="modal-titlebar rename-title-all">전체 이미지 저장</div>
<p class="modal-msg" id="rename-confirm-msg"></p>
<ul id="rename-confirm-list" class="modal-conflict-list modal-preview-list"></ul>
<p class="modal-warning" id="rename-confirm-selection-warning" style="display:none;">선택된 파일들만 저장합니다</p>
<div class="modal-actions">
<button id="rename-confirm-cancel" class="btn btn-ghost">취소</button>
<button id="rename-confirm-ok" class="btn btn-success">실행</button>
<button id="rename-confirm-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="rename-confirm-ok" class="btn btn-success modal-action">실행</button>
</div>
</div>
</div>
<div id="rename-conflict-modal" class="modal-overlay" style="display:none;">
<div class="modal-box">
<div class="modal-box modal-box-with-title">
<div class="modal-titlebar modal-titlebar-warning">이름 충돌</div>
<p class="modal-msg">변경할 이름의 파일이 이미 존재합니다.</p>
<ul id="rename-conflict-list" class="modal-conflict-list"></ul>
<div class="modal-actions">
<button id="rename-conflict-close" class="btn btn-ghost">닫기</button>
<button id="rename-conflict-close" class="btn btn-ghost modal-close">닫기</button>
</div>
</div>
</div>
<div id="delete-modal" class="modal-overlay" style="display:none;">
<div class="modal-box">
<div class="modal-box modal-box-with-title">
<div class="modal-titlebar modal-titlebar-danger">이미지 삭제</div>
<p class="modal-msg">아래 파일을 삭제합니다.</p>
<ul id="delete-file-list" class="modal-conflict-list"></ul>
<div class="modal-actions">
<button id="delete-cancel" class="btn btn-ghost">취소</button>
<button id="delete-confirm" class="btn btn-warning">확인</button>
<button id="delete-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="delete-confirm" class="btn btn-success modal-action">실행</button>
</div>
</div>
</div>
<div id="move-error-modal" class="modal-overlay" style="display:none;">
<div class="modal-box">
<div class="modal-box modal-box-with-title">
<div class="modal-titlebar modal-titlebar-warning">이동 오류</div>
<p class="modal-msg">새로운 이름의 파일도 존재합니다.</p>
<div class="modal-actions">
<button id="move-error-close" class="btn btn-ghost">닫기</button>
<button id="move-error-close" class="btn btn-ghost modal-close">닫기</button>
</div>
</div>
</div>
+94
View File
@@ -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);
}
+13 -18
View File
@@ -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);
}
}
+14 -14
View File
@@ -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;
+2
View File
@@ -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,
+11
View File
@@ -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;
}