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

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 { setStatus, showToast } from './ui.js';
import { renderGrid, showEmptyGrid } from './grid.js'; import { renderGrid, showEmptyGrid } from './grid.js';
import * as history from './history.js'; import * as history from './history.js';
import { openModal, closeModal } from './modal.js';
const deleteModal = document.getElementById('delete-modal'); const deleteModal = document.getElementById('delete-modal');
const deleteFileList = document.getElementById('delete-file-list'); const deleteFileList = document.getElementById('delete-file-list');
const deleteCancelBtn = document.getElementById('delete-cancel');
const deleteConfirmBtn = document.getElementById('delete-confirm'); const deleteConfirmBtn = document.getElementById('delete-confirm');
const btnRename = document.getElementById('btn-rename'); const btnRename = document.getElementById('btn-rename');
let _pendingFiles = null; let _pendingFiles = null;
deleteCancelBtn.addEventListener('click', () => { deleteModal.addEventListener('modal:closed', () => {
deleteModal.style.display = 'none';
_pendingFiles = null; _pendingFiles = null;
}); });
@@ -20,7 +19,7 @@ deleteConfirmBtn.addEventListener('click', async () => {
if (!_pendingFiles) return; if (!_pendingFiles) return;
const files = _pendingFiles; const files = _pendingFiles;
_pendingFiles = null; _pendingFiles = null;
deleteModal.style.display = 'none'; closeModal(deleteModal);
await _doDelete(files); await _doDelete(files);
}); });
@@ -35,7 +34,7 @@ export function deleteSelected() {
return li; return li;
})); }));
_pendingFiles = filesToDelete; _pendingFiles = filesToDelete;
deleteModal.style.display = 'flex'; openModal(deleteModal);
} }
export function init(gridEl) { export function init(gridEl) {
@@ -63,7 +62,7 @@ export function init(gridEl) {
return li; return li;
})); }));
_pendingFiles = filesToDelete; _pendingFiles = filesToDelete;
deleteModal.style.display = 'flex'; openModal(deleteModal);
}); });
} }
+18 -13
View File
@@ -79,58 +79,63 @@
<div id="toast"></div> <div id="toast"></div>
<div id="move-modal" class="modal-overlay" style="display:none;"> <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> <p class="modal-msg">대상 폴더에 이미 같은 이름의 파일이 존재합니다.<br>새로운 이름으로 파일들을 이동하시겠습니까?</p>
<div class="modal-fields"> <div class="modal-fields">
<label>파일명 포맷<input type="text" id="move-prefix" value="photo" placeholder="접두사" /></label> <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> <label>자리 순번 숫자<input type="number" id="move-digits" value="4" min="1" max="6" /></label>
</div> </div>
<div class="modal-actions"> <div class="modal-actions">
<button id="move-cancel" class="btn btn-ghost">취소</button> <button id="move-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="move-confirm" class="btn btn-success">실행</button> <button id="move-confirm" class="btn btn-success modal-action">실행</button>
</div> </div>
</div> </div>
</div> </div>
<div id="rename-confirm-modal" class="modal-overlay" style="display:none;"> <div id="rename-confirm-modal" class="modal-overlay" style="display:none;">
<div class="modal-box modal-box-with-title"> <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> <p class="modal-msg" id="rename-confirm-msg"></p>
<ul id="rename-confirm-list" class="modal-conflict-list modal-preview-list"></ul> <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> <p class="modal-warning" id="rename-confirm-selection-warning" style="display:none;">선택된 파일들만 저장합니다</p>
<div class="modal-actions"> <div class="modal-actions">
<button id="rename-confirm-cancel" class="btn btn-ghost">취소</button> <button id="rename-confirm-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="rename-confirm-ok" class="btn btn-success">실행</button> <button id="rename-confirm-ok" class="btn btn-success modal-action">실행</button>
</div> </div>
</div> </div>
</div> </div>
<div id="rename-conflict-modal" class="modal-overlay" style="display:none;"> <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> <p class="modal-msg">변경할 이름의 파일이 이미 존재합니다.</p>
<ul id="rename-conflict-list" class="modal-conflict-list"></ul> <ul id="rename-conflict-list" class="modal-conflict-list"></ul>
<div class="modal-actions"> <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>
</div> </div>
<div id="delete-modal" class="modal-overlay" style="display:none;"> <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> <p class="modal-msg">아래 파일을 삭제합니다.</p>
<ul id="delete-file-list" class="modal-conflict-list"></ul> <ul id="delete-file-list" class="modal-conflict-list"></ul>
<div class="modal-actions"> <div class="modal-actions">
<button id="delete-cancel" class="btn btn-ghost">취소</button> <button id="delete-cancel" class="btn btn-ghost modal-close">취소</button>
<button id="delete-confirm" class="btn btn-warning">확인</button> <button id="delete-confirm" class="btn btn-success modal-action">실행</button>
</div> </div>
</div> </div>
</div> </div>
<div id="move-error-modal" class="modal-overlay" style="display:none;"> <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> <p class="modal-msg">새로운 이름의 파일도 존재합니다.</p>
<div class="modal-actions"> <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> </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 { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import { renderGrid, updateSelCount, showEmptyGrid } from './grid.js'; import { renderGrid, updateSelCount, showEmptyGrid } from './grid.js';
import * as history from './history.js'; import * as history from './history.js';
import { openModal, closeModal } from './modal.js';
const btnRename = document.getElementById('btn-rename'); const btnRename = document.getElementById('btn-rename');
const moveModal = document.getElementById('move-modal'); const moveModal = document.getElementById('move-modal');
const movePrefixInput = document.getElementById('move-prefix'); const movePrefixInput = document.getElementById('move-prefix');
const moveDigitsInput = document.getElementById('move-digits'); const moveDigitsInput = document.getElementById('move-digits');
const moveCancelBtn = document.getElementById('move-cancel');
const moveConfirmBtn = document.getElementById('move-confirm'); const moveConfirmBtn = document.getElementById('move-confirm');
const moveErrorModal = document.getElementById('move-error-modal'); const moveErrorModal = document.getElementById('move-error-modal');
const moveErrorCloseBtn = document.getElementById('move-error-close');
let _pendingMoveTarget = null; let _pendingMoveTarget = null;
let _pendingMoveFiles = null; let _pendingMoveFiles = null;
moveCancelBtn.addEventListener('click', () => { moveModal.addEventListener('modal:closed', () => {
moveModal.style.display = 'none';
_pendingMoveTarget = null; _pendingMoveTarget = null;
_pendingMoveFiles = null; _pendingMoveFiles = null;
}); });
moveConfirmBtn.addEventListener('click', async () => { moveConfirmBtn.addEventListener('click', async () => {
if (!_pendingMoveTarget || !_pendingMoveFiles) return;
const prefix = movePrefixInput.value.trim() || 'photo'; const prefix = movePrefixInput.value.trim() || 'photo';
const digits = Math.max(1, Math.min(6, parseInt(moveDigitsInput.value) || 4)); 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(); const ext = f.name.split('.').pop().toLowerCase();
return `${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`; 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) { if (conflicts.length) {
moveModal.style.display = 'none'; closeModal(moveModal);
moveErrorModal.style.display = 'flex'; openModal(moveErrorModal);
return; return;
} }
moveModal.style.display = 'none'; const filesToMove = pendingFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
const targetPath = _pendingMoveTarget; closeModal(moveModal);
const filesToMove = _pendingMoveFiles.map((f, i) => ({ ...f, newName: newNames[i] }));
_pendingMoveTarget = null;
_pendingMoveFiles = null;
await _doMoveFiles(targetPath, filesToMove); await _doMoveFiles(targetPath, filesToMove);
}); });
moveErrorCloseBtn.addEventListener('click', () => {
moveErrorModal.style.display = 'none';
});
export async function handleFolderDrop(targetPath, fileIndices) { export async function handleFolderDrop(targetPath, fileIndices) {
if (!fileIndices.length || !state.currentFolder) return; if (!fileIndices.length || !state.currentFolder) return;
if (targetPath === state.currentFolder) return; if (targetPath === state.currentFolder) return;
@@ -66,7 +61,7 @@ export async function handleFolderDrop(targetPath, fileIndices) {
_pendingMoveTarget = targetPath; _pendingMoveTarget = targetPath;
_pendingMoveFiles = selectedFiles; _pendingMoveFiles = selectedFiles;
movePrefixInput.value = 'temp'; 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 { showLoading, hideLoading, setStatus, showToast } from './ui.js';
import { renderGrid } from './grid.js'; import { renderGrid } from './grid.js';
import * as history from './history.js'; import * as history from './history.js';
import { openModal, closeModal } from './modal.js';
const inputPrefix = document.getElementById('input-prefix'); const inputPrefix = document.getElementById('input-prefix');
const inputDigits = document.getElementById('input-digits'); const inputDigits = document.getElementById('input-digits');
const renameConfirmModal = document.getElementById('rename-confirm-modal'); 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 renameConfirmMsg = document.getElementById('rename-confirm-msg');
const renameConfirmWarning = document.getElementById('rename-confirm-selection-warning'); const renameConfirmWarning = document.getElementById('rename-confirm-selection-warning');
const renameConfirmList = document.getElementById('rename-confirm-list'); const renameConfirmList = document.getElementById('rename-confirm-list');
const renameConfirmCancelBtn = document.getElementById('rename-confirm-cancel');
const renameConfirmOkBtn = document.getElementById('rename-confirm-ok'); const renameConfirmOkBtn = document.getElementById('rename-confirm-ok');
const renameConflictModal = document.getElementById('rename-conflict-modal'); const renameConflictModal = document.getElementById('rename-conflict-modal');
const renameConflictCloseBtn = document.getElementById('rename-conflict-close');
const renameConflictList = document.getElementById('rename-conflict-list'); const renameConflictList = document.getElementById('rename-conflict-list');
let _renameConfirmResolve = null; let _renameConfirmResolve = null;
renameConfirmCancelBtn.addEventListener('click', () => {
closeRenameConfirm(false);
});
renameConfirmOkBtn.addEventListener('click', () => { renameConfirmOkBtn.addEventListener('click', () => {
closeRenameConfirm(true); closeRenameConfirm(true);
}); });
renameConflictCloseBtn.addEventListener('click', () => { renameConfirmModal.addEventListener('modal:closed', () => {
renameConflictModal.style.display = 'none'; resolveRenameConfirm(false);
}); });
export async function doRename() { export async function doRename() {
@@ -58,7 +54,7 @@ export async function doRename() {
li.textContent = n; li.textContent = n;
return li; return li;
})); }));
renameConflictModal.style.display = 'flex'; openModal(renameConflictModal);
return; return;
} }
} }
@@ -108,7 +104,8 @@ export async function doRename() {
} }
function showRenameConfirm({ count, examples, more, isPartial }) { function showRenameConfirm({ count, examples, more, isPartial }) {
renameConfirmTitle.textContent = isPartial ? '선택된 이미지 저장' : '전체 이미지 저장'; renameConfirmSelectedTitle.hidden = !isPartial;
renameConfirmAllTitle.hidden = isPartial;
renameConfirmMsg.textContent = `${count}개 파일을 이름 변경합니다. 계속하시겠습니까?`; renameConfirmMsg.textContent = `${count}개 파일을 이름 변경합니다. 계속하시겠습니까?`;
renameConfirmWarning.style.display = isPartial ? 'block' : 'none'; renameConfirmWarning.style.display = isPartial ? 'block' : 'none';
@@ -120,8 +117,7 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
return li; return li;
})); }));
renameConfirmModal.style.display = 'flex'; openModal(renameConfirmModal);
renameConfirmOkBtn.focus();
return new Promise(resolve => { return new Promise(resolve => {
_renameConfirmResolve = resolve; _renameConfirmResolve = resolve;
@@ -129,7 +125,11 @@ function showRenameConfirm({ count, examples, more, isPartial }) {
} }
function closeRenameConfirm(confirmed) { function closeRenameConfirm(confirmed) {
renameConfirmModal.style.display = 'none'; resolveRenameConfirm(confirmed);
closeModal(renameConfirmModal);
}
function resolveRenameConfirm(confirmed) {
if (_renameConfirmResolve) { if (_renameConfirmResolve) {
_renameConfirmResolve(confirmed); _renameConfirmResolve(confirmed);
_renameConfirmResolve = null; _renameConfirmResolve = null;
+2
View File
@@ -7,6 +7,7 @@ import {
} from './grid.js'; } from './grid.js';
import { doRename } from './rename.js'; import { doRename } from './rename.js';
import { handleFolderDrop } from './move.js'; import { handleFolderDrop } from './move.js';
import { initModalControls } from './modal.js';
import * as deleteHandler from './delete.js'; import * as deleteHandler from './delete.js';
import * as pinchZoom from './handlers/pinch-zoom.js'; import * as pinchZoom from './handlers/pinch-zoom.js';
import * as selection from './handlers/selection.js'; import * as selection from './handlers/selection.js';
@@ -37,6 +38,7 @@ const fileExplorerContainer = document.querySelector('.file-explorer');
// ── 핸들러 초기화 ───────────────────────────────── // ── 핸들러 초기화 ─────────────────────────────────
history.setOnUpdate(updateHistoryButtons); history.setOnUpdate(updateHistoryButtons);
initModalControls();
const callbacks = { const callbacks = {
refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels, refreshCardSelection, updateSelCount, updateNumberBadges, updateNewNameLabels,
+11
View File
@@ -9,6 +9,7 @@
--accent2: #e96af7; --accent2: #e96af7;
--success: #6af7a0; --success: #6af7a0;
--warning: #f7c26a; --warning: #f7c26a;
--danger: #ff6b7a;
--info: #6ac8f7; --info: #6ac8f7;
--text: #e8e8f0; --text: #e8e8f0;
--muted: #8f8fb8; --muted: #8f8fb8;
@@ -548,6 +549,16 @@ footer span { font-size: 11px; color: var(--muted); }
font-size: 15px; font-size: 15px;
font-weight: 700; 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 { .modal-msg {
font-size: 14px; color: var(--text); line-height: 1.7; font-size: 14px; color: var(--text); line-height: 1.7;
} }