리펙토링: 마우스 이벤트 코드
This commit is contained in:
@@ -0,0 +1,219 @@
|
|||||||
|
let _grid, _gridContainer, _dragGhostEl;
|
||||||
|
let _state, _cb;
|
||||||
|
|
||||||
|
let dragMode = null;
|
||||||
|
let dragSrcIdx = null;
|
||||||
|
|
||||||
|
let autoScrollRAF = null;
|
||||||
|
let autoScrollSpeed = 0;
|
||||||
|
let lastScrollTs = null;
|
||||||
|
|
||||||
|
export function init({ grid, gridContainer, dragGhostEl, state, callbacks }) {
|
||||||
|
_grid = grid;
|
||||||
|
_gridContainer = gridContainer;
|
||||||
|
_dragGhostEl = dragGhostEl;
|
||||||
|
_state = state;
|
||||||
|
_cb = callbacks;
|
||||||
|
|
||||||
|
window.addEventListener('dragover', _onWindowDragOver);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 카드 생성 시 호출해 드래그 이벤트를 부착
|
||||||
|
export function setupCard(card) {
|
||||||
|
card.addEventListener('dragstart', _onDragStart);
|
||||||
|
card.addEventListener('dragover', _onDragOver);
|
||||||
|
card.addEventListener('dragenter', _onDragEnter);
|
||||||
|
card.addEventListener('dragleave', _onDragLeave);
|
||||||
|
card.addEventListener('drop', _onDrop);
|
||||||
|
card.addEventListener('dragend', _onDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── window-level dragover: 고스트 위치 + 자동 스크롤 ──
|
||||||
|
|
||||||
|
function _onWindowDragOver(e) {
|
||||||
|
if (!dragMode) return;
|
||||||
|
if (dragMode === 'multi') _moveGhost(e);
|
||||||
|
_trackScroll(e.clientY);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 카드 드래그 핸들러 ─────────────────────────────
|
||||||
|
|
||||||
|
function _onDragStart(e) {
|
||||||
|
const idx = parseInt(this.dataset.idx);
|
||||||
|
|
||||||
|
if (_state.selectedIdxs.has(idx) && _state.selectedIdxs.size > 1) {
|
||||||
|
dragMode = 'multi';
|
||||||
|
dragSrcIdx = idx;
|
||||||
|
_state.selectedIdxs.forEach(i => {
|
||||||
|
const c = _getCardEl(i);
|
||||||
|
if (c) c.classList.add('multi-dragging');
|
||||||
|
});
|
||||||
|
_showGhost(e, `📦 ${_state.selectedIdxs.size}개 이동 중`);
|
||||||
|
const img = new Image();
|
||||||
|
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
||||||
|
e.dataTransfer.setDragImage(img, 0, 0);
|
||||||
|
} else {
|
||||||
|
dragMode = 'single';
|
||||||
|
dragSrcIdx = idx;
|
||||||
|
this.classList.add('dragging');
|
||||||
|
_state.selectedIdxs.clear();
|
||||||
|
_cb.refreshCardSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
|
e.dataTransfer.setData('text/plain', String(idx));
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onDragOver(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.dataTransfer.dropEffect = 'move';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onDragEnter(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const idx = parseInt(this.dataset.idx);
|
||||||
|
const isSrc = dragMode === 'single' ? idx === dragSrcIdx : _state.selectedIdxs.has(idx);
|
||||||
|
if (!isSrc) this.classList.add('drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onDragLeave() {
|
||||||
|
this.classList.remove('drag-over');
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onDrop(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.classList.remove('drag-over');
|
||||||
|
const targetIdx = parseInt(this.dataset.idx);
|
||||||
|
|
||||||
|
if (dragMode === 'single') {
|
||||||
|
if (dragSrcIdx === null || dragSrcIdx === targetIdx) return;
|
||||||
|
_moveSingle(dragSrcIdx, targetIdx);
|
||||||
|
} else if (dragMode === 'multi') {
|
||||||
|
if (_state.selectedIdxs.has(targetIdx)) return;
|
||||||
|
_moveMulti(targetIdx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onDragEnd() {
|
||||||
|
_stopScroll();
|
||||||
|
_hideGhost();
|
||||||
|
document.querySelectorAll('.card').forEach(c =>
|
||||||
|
c.classList.remove('dragging', 'multi-dragging', 'drag-over')
|
||||||
|
);
|
||||||
|
dragMode = null;
|
||||||
|
dragSrcIdx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 이동 로직 ─────────────────────────────────────
|
||||||
|
|
||||||
|
function _moveSingle(srcIdx, targetIdx) {
|
||||||
|
const [moved] = _state.files.splice(srcIdx, 1);
|
||||||
|
_state.files.splice(targetIdx, 0, moved);
|
||||||
|
|
||||||
|
const cards = Array.from(_grid.children);
|
||||||
|
const src = cards[srcIdx];
|
||||||
|
const target = cards[targetIdx];
|
||||||
|
if (srcIdx < targetIdx) {
|
||||||
|
_grid.insertBefore(src, target.nextSibling);
|
||||||
|
} else {
|
||||||
|
_grid.insertBefore(src, target);
|
||||||
|
}
|
||||||
|
Array.from(_grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
||||||
|
|
||||||
|
_cb.updateNumberBadges();
|
||||||
|
_cb.updateNewNameLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _moveMulti(targetIdx) {
|
||||||
|
const sorted = [..._state.selectedIdxs].sort((a, b) => a - b);
|
||||||
|
const movedFiles = sorted.map(i => _state.files[i]);
|
||||||
|
|
||||||
|
const removedBefore = sorted.filter(i => i < targetIdx).length;
|
||||||
|
const adjustedTarget = targetIdx - removedBefore;
|
||||||
|
|
||||||
|
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||||
|
_state.files.splice(sorted[i], 1);
|
||||||
|
}
|
||||||
|
_state.files.splice(adjustedTarget, 0, ...movedFiles);
|
||||||
|
|
||||||
|
const newSel = new Set();
|
||||||
|
for (let i = 0; i < movedFiles.length; i++) newSel.add(adjustedTarget + i);
|
||||||
|
_state.selectedIdxs = newSel;
|
||||||
|
|
||||||
|
// DOM 재정렬 (이미지 재로딩 없이)
|
||||||
|
const byPath = new Map();
|
||||||
|
Array.from(_grid.children).forEach(c => byPath.set(c.dataset.filepath, c));
|
||||||
|
_grid.innerHTML = '';
|
||||||
|
_state.files.forEach((f, idx) => {
|
||||||
|
const c = byPath.get(f.path);
|
||||||
|
if (c) { c.dataset.idx = idx; _grid.appendChild(c); }
|
||||||
|
});
|
||||||
|
|
||||||
|
_cb.updateNumberBadges();
|
||||||
|
_cb.updateNewNameLabels();
|
||||||
|
_cb.refreshCardSelection();
|
||||||
|
_cb.updateSelCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 자동 스크롤 ───────────────────────────────────
|
||||||
|
|
||||||
|
function _trackScroll(clientY) {
|
||||||
|
const vh = window.innerHeight;
|
||||||
|
const topLine = vh * 0.1;
|
||||||
|
const botLine = vh * 0.9;
|
||||||
|
|
||||||
|
if (clientY < topLine) {
|
||||||
|
const ratio = clientY / topLine;
|
||||||
|
_startScroll(-vh * (1 - 0.8 * ratio));
|
||||||
|
} else if (clientY > botLine) {
|
||||||
|
const ratio = (clientY - botLine) / (vh * 0.1);
|
||||||
|
_startScroll(vh * (0.2 + 0.8 * ratio));
|
||||||
|
} else {
|
||||||
|
_stopScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _startScroll(speed) {
|
||||||
|
autoScrollSpeed = speed;
|
||||||
|
if (!autoScrollRAF) {
|
||||||
|
lastScrollTs = performance.now();
|
||||||
|
autoScrollRAF = requestAnimationFrame(_tickScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _stopScroll() {
|
||||||
|
autoScrollSpeed = 0;
|
||||||
|
if (autoScrollRAF) {
|
||||||
|
cancelAnimationFrame(autoScrollRAF);
|
||||||
|
autoScrollRAF = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _tickScroll(now) {
|
||||||
|
if (!dragMode) { _stopScroll(); return; }
|
||||||
|
const dt = (now - lastScrollTs) / 1000;
|
||||||
|
lastScrollTs = now;
|
||||||
|
_gridContainer.scrollTop += autoScrollSpeed * dt;
|
||||||
|
autoScrollRAF = requestAnimationFrame(_tickScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 드래그 고스트 ─────────────────────────────────
|
||||||
|
|
||||||
|
function _showGhost(e, text) {
|
||||||
|
_dragGhostEl.textContent = text;
|
||||||
|
_dragGhostEl.style.display = 'block';
|
||||||
|
_moveGhost(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _moveGhost(e) {
|
||||||
|
_dragGhostEl.style.left = (e.clientX + 16) + 'px';
|
||||||
|
_dragGhostEl.style.top = (e.clientY - 14) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _hideGhost() {
|
||||||
|
_dragGhostEl.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _getCardEl(idx) {
|
||||||
|
return _grid.querySelector(`.card[data-idx="${idx}"]`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const THUMB_MIN = 150;
|
||||||
|
const THUMB_MAX = 400;
|
||||||
|
|
||||||
|
let _thumbSizeHint;
|
||||||
|
let thumbSize = 250;
|
||||||
|
let thumbHintTimer = null;
|
||||||
|
|
||||||
|
export function init({ gridContainer, thumbSizeHint }) {
|
||||||
|
_thumbSizeHint = thumbSizeHint;
|
||||||
|
|
||||||
|
gridContainer.addEventListener('wheel', (e) => {
|
||||||
|
if (!e.ctrlKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
setThumbSize(thumbSize + (-e.deltaY * 0.8));
|
||||||
|
}, { passive: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setThumbSize(size) {
|
||||||
|
thumbSize = Math.round(Math.max(THUMB_MIN, Math.min(THUMB_MAX, size)));
|
||||||
|
document.documentElement.style.setProperty('--thumb-size', thumbSize + 'px');
|
||||||
|
_showHint(thumbSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _showHint(size) {
|
||||||
|
_thumbSizeHint.textContent = `${size}px`;
|
||||||
|
_thumbSizeHint.classList.add('show');
|
||||||
|
clearTimeout(thumbHintTimer);
|
||||||
|
thumbHintTimer = setTimeout(() => _thumbSizeHint.classList.remove('show'), 1000);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
let _rubberBand, _grid;
|
||||||
|
let _state, _cb;
|
||||||
|
|
||||||
|
let rbActive = false;
|
||||||
|
let rbStartX = 0;
|
||||||
|
let rbStartY = 0;
|
||||||
|
|
||||||
|
export function init({ gridContainer, rubberBand, grid, state, callbacks }) {
|
||||||
|
_rubberBand = rubberBand;
|
||||||
|
_grid = grid;
|
||||||
|
_state = state;
|
||||||
|
_cb = callbacks;
|
||||||
|
|
||||||
|
gridContainer.addEventListener('mousedown', _onStart);
|
||||||
|
window.addEventListener('mousemove', _onMove);
|
||||||
|
window.addEventListener('mouseup', _onEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 카드에 부착할 cmd+클릭 핸들러 (this = card element)
|
||||||
|
export function onCardClick(e) {
|
||||||
|
if (!e.metaKey) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
const idx = parseInt(this.dataset.idx);
|
||||||
|
if (_state.selectedIdxs.has(idx)) {
|
||||||
|
_state.selectedIdxs.delete(idx);
|
||||||
|
} else {
|
||||||
|
_state.selectedIdxs.add(idx);
|
||||||
|
}
|
||||||
|
_cb.refreshCardSelection();
|
||||||
|
_cb.updateSelCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onStart(e) {
|
||||||
|
if (e.target.closest('.card')) return;
|
||||||
|
if (e.target.closest('button, input, label')) return;
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
rbActive = true;
|
||||||
|
rbStartX = e.clientX;
|
||||||
|
rbStartY = e.clientY;
|
||||||
|
|
||||||
|
Object.assign(_rubberBand.style, {
|
||||||
|
display: 'block',
|
||||||
|
left: rbStartX + 'px',
|
||||||
|
top: rbStartY + 'px',
|
||||||
|
width: '0px',
|
||||||
|
height: '0px',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!e.shiftKey) {
|
||||||
|
_state.selectedIdxs.clear();
|
||||||
|
_cb.refreshCardSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onMove(e) {
|
||||||
|
if (!rbActive) return;
|
||||||
|
|
||||||
|
const left = Math.min(rbStartX, e.clientX);
|
||||||
|
const top = Math.min(rbStartY, e.clientY);
|
||||||
|
const width = Math.abs(e.clientX - rbStartX);
|
||||||
|
const height = Math.abs(e.clientY - rbStartY);
|
||||||
|
|
||||||
|
Object.assign(_rubberBand.style, {
|
||||||
|
left: left + 'px', top: top + 'px',
|
||||||
|
width: width + 'px', height: height + 'px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const sel = { left, top, right: left + width, bottom: top + height };
|
||||||
|
Array.from(_grid.children).forEach((card, i) => {
|
||||||
|
const r = card.getBoundingClientRect();
|
||||||
|
const overlaps = !(r.right < sel.left || r.left > sel.right ||
|
||||||
|
r.bottom < sel.top || r.top > sel.bottom);
|
||||||
|
if (overlaps) {
|
||||||
|
_state.selectedIdxs.add(i);
|
||||||
|
} else if (!e.shiftKey) {
|
||||||
|
_state.selectedIdxs.delete(i);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_cb.refreshCardSelection();
|
||||||
|
_cb.updateSelCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _onEnd() {
|
||||||
|
if (!rbActive) return;
|
||||||
|
rbActive = false;
|
||||||
|
_rubberBand.style.display = 'none';
|
||||||
|
_cb.updateSelCount();
|
||||||
|
}
|
||||||
+1
-1
@@ -60,6 +60,6 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
|
|
||||||
<script src="renderer.js"></script>
|
<script type="module" src="renderer.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+91
-457
@@ -1,44 +1,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════
|
import { state } from './state.js';
|
||||||
// Photo Renamer v2 — renderer.js
|
import * as pinchZoom from './handlers/pinchZoom.js';
|
||||||
//
|
import * as selection from './handlers/selection.js';
|
||||||
// 기능:
|
import * as dragDrop from './handlers/dragDrop.js';
|
||||||
// • 썸네일 그리드 (정사각형, object-fit:cover, 기본 250px)
|
|
||||||
// • 트랙패드 핀치로 썸네일 크기 조절 (150~400px)
|
|
||||||
// • Rubber-band(클릭 드래그) 다중 선택
|
|
||||||
// • Shift + 클릭 드래그 → 기존 선택에 추가
|
|
||||||
// • Command + 클릭 → 개별 토글 선택
|
|
||||||
// • 선택된 카드들 한꺼번에 드래그 → 드롭 이동
|
|
||||||
// • 단일 카드 드래그 → 드롭 이동
|
|
||||||
// • 드래그 중 화면 상/하단 10% 진입 시 자동 스크롤
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
// ── 썸네일 크기 상수 ─────────────────────────────────
|
|
||||||
const THUMB_MIN = 150;
|
|
||||||
const THUMB_MAX = 400;
|
|
||||||
const THUMB_DEFAULT = 250;
|
|
||||||
let thumbSize = THUMB_DEFAULT;
|
|
||||||
|
|
||||||
// ── 전역 상태 ────────────────────────────────────────
|
|
||||||
let files = [];
|
|
||||||
let currentFolder = null;
|
|
||||||
let selectedIdxs = new Set();
|
|
||||||
|
|
||||||
// 드래그 상태
|
|
||||||
let dragMode = null; // 'single' | 'multi'
|
|
||||||
let dragSrcIdx = null;
|
|
||||||
|
|
||||||
// Rubber-band 상태
|
|
||||||
let rbActive = false;
|
|
||||||
let rbStartX = 0;
|
|
||||||
let rbStartY = 0; // viewport 기준
|
|
||||||
|
|
||||||
// 자동 스크롤 상태
|
|
||||||
let autoScrollRAF = null;
|
|
||||||
let autoScrollSpeed = 0;
|
|
||||||
let lastScrollTs = null;
|
|
||||||
|
|
||||||
// 핀치 힌트 타이머
|
|
||||||
let thumbHintTimer = null;
|
|
||||||
|
|
||||||
// ── DOM ──────────────────────────────────────────────
|
// ── DOM ──────────────────────────────────────────────
|
||||||
const grid = document.getElementById('grid');
|
const grid = document.getElementById('grid');
|
||||||
@@ -64,7 +27,19 @@ const gridContainer = document.getElementById('grid-container');
|
|||||||
const dragGhostEl = document.getElementById('drag-ghost');
|
const dragGhostEl = document.getElementById('drag-ghost');
|
||||||
const thumbSizeHint = document.getElementById('thumb-size-hint');
|
const thumbSizeHint = document.getElementById('thumb-size-hint');
|
||||||
|
|
||||||
// ── 초기화 ───────────────────────────────────────────
|
// ── 핸들러 초기화 ─────────────────────────────────
|
||||||
|
const callbacks = {
|
||||||
|
refreshCardSelection,
|
||||||
|
updateSelCount,
|
||||||
|
updateNumberBadges,
|
||||||
|
updateNewNameLabels,
|
||||||
|
};
|
||||||
|
|
||||||
|
pinchZoom.init({ gridContainer, thumbSizeHint });
|
||||||
|
selection.init({ gridContainer, rubberBand, grid, state, callbacks });
|
||||||
|
dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks });
|
||||||
|
|
||||||
|
// ── 버튼 이벤트 ──────────────────────────────────
|
||||||
inputPrefix.addEventListener('input', updatePreview);
|
inputPrefix.addEventListener('input', updatePreview);
|
||||||
inputDigits.addEventListener('input', updatePreview);
|
inputDigits.addEventListener('input', updatePreview);
|
||||||
btnOpen.addEventListener('click', openFolder);
|
btnOpen.addEventListener('click', openFolder);
|
||||||
@@ -72,47 +47,14 @@ btnSortName.addEventListener('click', () => sortFiles('name'));
|
|||||||
btnSortDate.addEventListener('click', () => sortFiles('date'));
|
btnSortDate.addEventListener('click', () => sortFiles('date'));
|
||||||
btnRename.addEventListener('click', doRename);
|
btnRename.addEventListener('click', doRename);
|
||||||
btnReveal.addEventListener('click', () => {
|
btnReveal.addEventListener('click', () => {
|
||||||
if (currentFolder) window.api.revealFolder(currentFolder);
|
if (state.currentFolder) window.api.revealFolder(state.currentFolder);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rubber-band: grid-container 위에서 시작
|
// ── 폴더 열기 ────────────────────────────────────
|
||||||
gridContainer.addEventListener('mousedown', onRubberBandStart);
|
|
||||||
window.addEventListener('mousemove', onRubberBandMove);
|
|
||||||
window.addEventListener('mouseup', onRubberBandEnd);
|
|
||||||
|
|
||||||
// 드래그 중 window-level dragover → 고스트 이동 + 자동 스크롤
|
|
||||||
window.addEventListener('dragover', onWindowDragOver);
|
|
||||||
|
|
||||||
// ── 핀치 줌 (트랙패드 두 손가락) ─────────────────────
|
|
||||||
// macOS 트랙패드 핀치 = ctrlKey + wheel 이벤트
|
|
||||||
gridContainer.addEventListener('wheel', (e) => {
|
|
||||||
if (!e.ctrlKey) return;
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
// deltaY: 음수 = 벌리기(크게), 양수 = 오므리기(작게)
|
|
||||||
const delta = -e.deltaY * 0.8;
|
|
||||||
setThumbSize(thumbSize + delta);
|
|
||||||
}, { passive: false });
|
|
||||||
|
|
||||||
// ── 썸네일 크기 변경 ─────────────────────────────────
|
|
||||||
function setThumbSize(size) {
|
|
||||||
thumbSize = Math.round(Math.max(THUMB_MIN, Math.min(THUMB_MAX, size)));
|
|
||||||
document.documentElement.style.setProperty('--thumb-size', thumbSize + 'px');
|
|
||||||
showThumbSizeHint(thumbSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
function showThumbSizeHint(size) {
|
|
||||||
thumbSizeHint.textContent = `${size}px`;
|
|
||||||
thumbSizeHint.classList.add('show');
|
|
||||||
clearTimeout(thumbHintTimer);
|
|
||||||
thumbHintTimer = setTimeout(() => thumbSizeHint.classList.remove('show'), 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 폴더 열기 ────────────────────────────────────────
|
|
||||||
async function openFolder() {
|
async function openFolder() {
|
||||||
const folder = await window.api.selectFolder();
|
const folder = await window.api.selectFolder();
|
||||||
if (!folder) return;
|
if (!folder) return;
|
||||||
currentFolder = folder;
|
state.currentFolder = folder;
|
||||||
dirPath.textContent = folder;
|
dirPath.textContent = folder;
|
||||||
showLoading('이미지 목록 불러오는 중...');
|
showLoading('이미지 목록 불러오는 중...');
|
||||||
const list = await window.api.listImages(folder);
|
const list = await window.api.listImages(folder);
|
||||||
@@ -122,34 +64,34 @@ async function openFolder() {
|
|||||||
showToast('⚠️ 이미지 파일이 없습니다', '#f76a6a', '#0a0a0a');
|
showToast('⚠️ 이미지 파일이 없습니다', '#f76a6a', '#0a0a0a');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
files = list;
|
state.files.length = 0;
|
||||||
selectedIdxs.clear();
|
state.files.push(...list);
|
||||||
setStatus(`${files.length}개 파일 로드됨`, true);
|
state.selectedIdxs.clear();
|
||||||
|
setStatus(`${state.files.length}개 파일 로드됨`, true);
|
||||||
await renderGrid();
|
await renderGrid();
|
||||||
updatePreview();
|
updatePreview();
|
||||||
btnRename.disabled = false;
|
btnRename.disabled = false;
|
||||||
btnReveal.disabled = false;
|
btnReveal.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 그리드 렌더링 ────────────────────────────────────
|
// ── 그리드 렌더링 ────────────────────────────────
|
||||||
async function renderGrid() {
|
async function renderGrid() {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
emptyState.style.display = 'none';
|
emptyState.style.display = 'none';
|
||||||
grid.style.display = 'grid';
|
grid.style.display = 'grid';
|
||||||
fileCount.textContent = `${files.length}개 파일`;
|
fileCount.textContent = `${state.files.length}개 파일`;
|
||||||
updateSelCount();
|
updateSelCount();
|
||||||
|
|
||||||
showLoading(`썸네일 로딩 중... (0/${files.length})`);
|
showLoading(`썸네일 로딩 중... (0/${state.files.length})`);
|
||||||
|
|
||||||
const BATCH = 10;
|
const BATCH = 10;
|
||||||
for (let i = 0; i < files.length; i += BATCH) {
|
for (let i = 0; i < state.files.length; i += BATCH) {
|
||||||
const batch = files.slice(i, i + BATCH);
|
const batch = state.files.slice(i, i + BATCH);
|
||||||
const results = await Promise.all(batch.map(f => window.api.readImage(f.path)));
|
const results = await Promise.all(batch.map(f => window.api.readImage(f.path)));
|
||||||
results.forEach((dataUrl, bi) => {
|
results.forEach((dataUrl, bi) => {
|
||||||
const idx = i + bi;
|
grid.appendChild(createCard(state.files[i + bi], i + bi, dataUrl));
|
||||||
grid.appendChild(createCard(files[idx], idx, dataUrl));
|
|
||||||
});
|
});
|
||||||
loadingText.textContent = `썸네일 로딩 중... (${Math.min(i + BATCH, files.length)}/${files.length})`;
|
loadingText.textContent = `썸네일 로딩 중... (${Math.min(i + BATCH, state.files.length)}/${state.files.length})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
hideLoading();
|
hideLoading();
|
||||||
@@ -158,7 +100,7 @@ async function renderGrid() {
|
|||||||
updateNewNameLabels();
|
updateNewNameLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 카드 생성 ────────────────────────────────────────
|
// ── 카드 생성 ────────────────────────────────────
|
||||||
function createCard(file, idx, dataUrl) {
|
function createCard(file, idx, dataUrl) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
@@ -187,14 +129,12 @@ function createCard(file, idx, dataUrl) {
|
|||||||
thumb.textContent = '🖼️';
|
thumb.textContent = '🖼️';
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = document.createElement('div');
|
const body = document.createElement('div');
|
||||||
body.className = 'card-body';
|
body.className = 'card-body';
|
||||||
|
const nameEl = document.createElement('div');
|
||||||
const nameEl = document.createElement('div');
|
|
||||||
nameEl.className = 'card-name';
|
nameEl.className = 'card-name';
|
||||||
nameEl.title = file.name;
|
nameEl.title = file.name;
|
||||||
nameEl.textContent = file.name;
|
nameEl.textContent = file.name;
|
||||||
|
|
||||||
const newNameEl = document.createElement('div');
|
const newNameEl = document.createElement('div');
|
||||||
newNameEl.className = 'card-new-name';
|
newNameEl.className = 'card-new-name';
|
||||||
|
|
||||||
@@ -205,309 +145,78 @@ function createCard(file, idx, dataUrl) {
|
|||||||
card.appendChild(thumb);
|
card.appendChild(thumb);
|
||||||
card.appendChild(body);
|
card.appendChild(body);
|
||||||
|
|
||||||
card.addEventListener('dragstart', onCardDragStart);
|
dragDrop.setupCard(card);
|
||||||
card.addEventListener('dragover', onCardDragOver);
|
card.addEventListener('click', selection.onCardClick);
|
||||||
card.addEventListener('dragenter', onCardDragEnter);
|
|
||||||
card.addEventListener('dragleave', onCardDragLeave);
|
|
||||||
card.addEventListener('drop', onCardDrop);
|
|
||||||
card.addEventListener('dragend', onCardDragEnd);
|
|
||||||
card.addEventListener('click', onCardClick);
|
|
||||||
|
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ── 정렬 ─────────────────────────────────────────
|
||||||
// Command+클릭 → 개별 토글 선택
|
async function sortFiles(mode) {
|
||||||
// ═══════════════════════════════════════════════════════
|
if (!state.files.length) return;
|
||||||
|
if (mode === 'name') state.files.sort((a, b) => a.name.localeCompare(b.name, 'ko'));
|
||||||
function onCardClick(e) {
|
else state.files.sort((a, b) => a.mtime - b.mtime);
|
||||||
if (!e.metaKey) return;
|
state.selectedIdxs.clear();
|
||||||
e.stopPropagation();
|
await renderGrid();
|
||||||
const idx = parseInt(this.dataset.idx);
|
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
|
||||||
if (selectedIdxs.has(idx)) {
|
|
||||||
selectedIdxs.delete(idx);
|
|
||||||
} else {
|
|
||||||
selectedIdxs.add(idx);
|
|
||||||
}
|
|
||||||
refreshCardSelection();
|
|
||||||
updateSelCount();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
// ── 미리보기 ─────────────────────────────────────
|
||||||
// RUBBER-BAND 다중 선택
|
function updatePreview() {
|
||||||
// ═══════════════════════════════════════════════════════
|
const prefix = inputPrefix.value.trim() || 'photo';
|
||||||
|
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
||||||
function onRubberBandStart(e) {
|
const ext = state.files.length ? state.files[0].name.split('.').pop().toLowerCase() : 'jpg';
|
||||||
if (e.target.closest('.card')) return;
|
previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`;
|
||||||
if (e.target.closest('button, input, label')) return;
|
updateNewNameLabels();
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
rbActive = true;
|
|
||||||
rbStartX = e.clientX;
|
|
||||||
rbStartY = e.clientY;
|
|
||||||
|
|
||||||
rubberBand.style.display = 'block';
|
|
||||||
rubberBand.style.left = rbStartX + 'px';
|
|
||||||
rubberBand.style.top = rbStartY + 'px';
|
|
||||||
rubberBand.style.width = '0px';
|
|
||||||
rubberBand.style.height = '0px';
|
|
||||||
|
|
||||||
if (!e.shiftKey) {
|
|
||||||
selectedIdxs.clear();
|
|
||||||
refreshCardSelection();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onRubberBandMove(e) {
|
// ── 이름 변경 ────────────────────────────────────
|
||||||
if (!rbActive) return;
|
async function doRename() {
|
||||||
|
if (!state.files.length || !state.currentFolder) return;
|
||||||
|
const prefix = inputPrefix.value.trim() || 'photo';
|
||||||
|
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
||||||
|
|
||||||
const curX = e.clientX;
|
const examples = state.files.slice(0, 3).map((f, i) => {
|
||||||
const curY = e.clientY;
|
const ext = f.name.split('.').pop().toLowerCase();
|
||||||
|
return `${f.name} → ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`;
|
||||||
|
}).join('\n');
|
||||||
|
const more = state.files.length > 3 ? `\n... 외 ${state.files.length - 3}개` : '';
|
||||||
|
|
||||||
const left = Math.min(rbStartX, curX);
|
if (!confirm(`📂 ${state.files.length}개 파일을 이름 변경합니다:\n\n${examples}${more}\n\n계속하시겠습니까?`)) return;
|
||||||
const top = Math.min(rbStartY, curY);
|
|
||||||
const width = Math.abs(curX - rbStartX);
|
|
||||||
const height = Math.abs(curY - rbStartY);
|
|
||||||
|
|
||||||
rubberBand.style.left = left + 'px';
|
showLoading('파일 이름 변경 중...');
|
||||||
rubberBand.style.top = top + 'px';
|
setStatus('이름 변경 중...', false);
|
||||||
rubberBand.style.width = width + 'px';
|
|
||||||
rubberBand.style.height = height + 'px';
|
|
||||||
|
|
||||||
// rubber-band 사각형 (viewport 기준)
|
try {
|
||||||
const selRect = { left, top, right: left + width, bottom: top + height };
|
const result = await window.api.renameFiles({
|
||||||
|
folderPath: state.currentFolder,
|
||||||
Array.from(grid.children).forEach((card, i) => {
|
files: state.files,
|
||||||
const cr = card.getBoundingClientRect();
|
prefix,
|
||||||
const overlaps = !(cr.right < selRect.left ||
|
digits,
|
||||||
cr.left > selRect.right ||
|
|
||||||
cr.bottom < selRect.top ||
|
|
||||||
cr.top > selRect.bottom);
|
|
||||||
if (overlaps) {
|
|
||||||
selectedIdxs.add(i);
|
|
||||||
} else if (!e.shiftKey) {
|
|
||||||
selectedIdxs.delete(i);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshCardSelection();
|
|
||||||
updateSelCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRubberBandEnd() {
|
|
||||||
if (!rbActive) return;
|
|
||||||
rbActive = false;
|
|
||||||
rubberBand.style.display = 'none';
|
|
||||||
updateSelCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// 드래그 앤 드롭
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
function onCardDragStart(e) {
|
|
||||||
const idx = parseInt(this.dataset.idx);
|
|
||||||
|
|
||||||
if (selectedIdxs.has(idx) && selectedIdxs.size > 1) {
|
|
||||||
// 다중 드래그
|
|
||||||
dragMode = 'multi';
|
|
||||||
dragSrcIdx = idx;
|
|
||||||
|
|
||||||
selectedIdxs.forEach(i => {
|
|
||||||
const card = getCardEl(i);
|
|
||||||
if (card) card.classList.add('multi-dragging');
|
|
||||||
});
|
});
|
||||||
|
hideLoading();
|
||||||
showDragGhost(e, `📦 ${selectedIdxs.size}개 이동 중`);
|
if (result.errors.length) {
|
||||||
const img = new Image();
|
alert('일부 오류:\n' + result.errors.join('\n'));
|
||||||
img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
} else {
|
||||||
e.dataTransfer.setDragImage(img, 0, 0);
|
showToast(`✅ ${result.renamed.length}개 파일 이름 변경 완료!`);
|
||||||
|
setStatus(`${result.renamed.length}개 파일 이름 변경 완료`, true);
|
||||||
} else {
|
|
||||||
// 단일 드래그
|
|
||||||
dragMode = 'single';
|
|
||||||
dragSrcIdx = idx;
|
|
||||||
this.classList.add('dragging');
|
|
||||||
selectedIdxs.clear();
|
|
||||||
refreshCardSelection();
|
|
||||||
}
|
|
||||||
|
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
|
||||||
e.dataTransfer.setData('text/plain', String(idx));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardDragOver(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.dataTransfer.dropEffect = 'move';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardDragEnter(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const idx = parseInt(this.dataset.idx);
|
|
||||||
const isSource = dragMode === 'single' ? idx === dragSrcIdx : selectedIdxs.has(idx);
|
|
||||||
if (!isSource) this.classList.add('drag-over');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardDragLeave() {
|
|
||||||
this.classList.remove('drag-over');
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardDrop(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
this.classList.remove('drag-over');
|
|
||||||
const targetIdx = parseInt(this.dataset.idx);
|
|
||||||
|
|
||||||
if (dragMode === 'single') {
|
|
||||||
if (dragSrcIdx === null || dragSrcIdx === targetIdx) return;
|
|
||||||
moveSingle(dragSrcIdx, targetIdx);
|
|
||||||
} else if (dragMode === 'multi') {
|
|
||||||
if (selectedIdxs.has(targetIdx)) return;
|
|
||||||
moveMulti(targetIdx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onCardDragEnd() {
|
|
||||||
stopAutoScroll();
|
|
||||||
hideDragGhost();
|
|
||||||
document.querySelectorAll('.card').forEach(c => {
|
|
||||||
c.classList.remove('dragging', 'multi-dragging', 'drag-over');
|
|
||||||
});
|
|
||||||
dragMode = null;
|
|
||||||
dragSrcIdx = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── window-level dragover: 고스트 위치 + 자동 스크롤 ──
|
|
||||||
function onWindowDragOver(e) {
|
|
||||||
if (!dragMode) return;
|
|
||||||
|
|
||||||
if (dragMode === 'multi') moveDragGhost(e);
|
|
||||||
trackAutoScroll(e.clientY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 단일 이동 ─────────────────────────────────────────
|
|
||||||
function moveSingle(srcIdx, targetIdx) {
|
|
||||||
const [moved] = files.splice(srcIdx, 1);
|
|
||||||
files.splice(targetIdx, 0, moved);
|
|
||||||
reorderGridSingle(srcIdx, targetIdx);
|
|
||||||
updateNumberBadges();
|
|
||||||
updateNewNameLabels();
|
|
||||||
}
|
|
||||||
|
|
||||||
function reorderGridSingle(srcIdx, targetIdx) {
|
|
||||||
const cards = Array.from(grid.children);
|
|
||||||
const srcCard = cards[srcIdx];
|
|
||||||
const targetCard = cards[targetIdx];
|
|
||||||
if (srcIdx < targetIdx) {
|
|
||||||
grid.insertBefore(srcCard, targetCard.nextSibling);
|
|
||||||
} else {
|
|
||||||
grid.insertBefore(srcCard, targetCard);
|
|
||||||
}
|
|
||||||
Array.from(grid.children).forEach((c, i) => { c.dataset.idx = i; });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 다중 이동 ─────────────────────────────────────────
|
|
||||||
function moveMulti(targetIdx) {
|
|
||||||
const sortedSel = [...selectedIdxs].sort((a, b) => a - b);
|
|
||||||
const movedFiles = sortedSel.map(i => files[i]);
|
|
||||||
|
|
||||||
const removedBefore = sortedSel.filter(i => i < targetIdx).length;
|
|
||||||
const adjustedTarget = targetIdx - removedBefore;
|
|
||||||
|
|
||||||
for (let i = sortedSel.length - 1; i >= 0; i--) {
|
|
||||||
files.splice(sortedSel[i], 1);
|
|
||||||
}
|
|
||||||
files.splice(adjustedTarget, 0, ...movedFiles);
|
|
||||||
|
|
||||||
const newSelected = new Set();
|
|
||||||
for (let i = 0; i < movedFiles.length; i++) {
|
|
||||||
newSelected.add(adjustedTarget + i);
|
|
||||||
}
|
|
||||||
selectedIdxs = newSelected;
|
|
||||||
|
|
||||||
rerenderCardsInPlace();
|
|
||||||
updateNumberBadges();
|
|
||||||
updateNewNameLabels();
|
|
||||||
refreshCardSelection();
|
|
||||||
updateSelCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── cards를 files[] 순서에 맞게 DOM 재정렬 (이미지 재로딩 없이) ──
|
|
||||||
function rerenderCardsInPlace() {
|
|
||||||
const cardsByPath = new Map();
|
|
||||||
Array.from(grid.children).forEach(card => {
|
|
||||||
cardsByPath.set(card.dataset.filepath, card);
|
|
||||||
});
|
|
||||||
|
|
||||||
grid.innerHTML = '';
|
|
||||||
files.forEach((f, idx) => {
|
|
||||||
const card = cardsByPath.get(f.path);
|
|
||||||
if (card) {
|
|
||||||
card.dataset.idx = idx;
|
|
||||||
grid.appendChild(card);
|
|
||||||
}
|
}
|
||||||
});
|
const newList = await window.api.listImages(state.currentFolder);
|
||||||
}
|
state.files.length = 0;
|
||||||
|
state.files.push(...newList);
|
||||||
// ═══════════════════════════════════════════════════════
|
state.selectedIdxs.clear();
|
||||||
// 자동 스크롤 (드래그 중 화면 상/하단 10% 진입 시)
|
await renderGrid();
|
||||||
// ═══════════════════════════════════════════════════════
|
} catch (e) {
|
||||||
|
hideLoading();
|
||||||
function trackAutoScroll(clientY) {
|
alert('오류 발생: ' + e.message);
|
||||||
const vh = window.innerHeight;
|
|
||||||
const topLine = vh * 0.1;
|
|
||||||
const botLine = vh * 0.9;
|
|
||||||
|
|
||||||
if (clientY < topLine) {
|
|
||||||
// 화면 상단 10% — 위로 스크롤
|
|
||||||
// ratio: 0 = 화면 최상단(빠름), 1 = 10% 선(느림)
|
|
||||||
const ratio = clientY / topLine;
|
|
||||||
// 속도: vh px/s (최상단) → vh/5 px/s (10% 선)
|
|
||||||
const speed = vh * (1 - 0.8 * ratio);
|
|
||||||
startAutoScroll(-speed);
|
|
||||||
|
|
||||||
} else if (clientY > botLine) {
|
|
||||||
// 화면 하단 10% — 아래로 스크롤
|
|
||||||
// ratio: 0 = 90% 선(느림), 1 = 화면 최하단(빠름)
|
|
||||||
const ratio = (clientY - botLine) / (vh * 0.1);
|
|
||||||
const speed = vh * (0.2 + 0.8 * ratio);
|
|
||||||
startAutoScroll(speed);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
stopAutoScroll();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startAutoScroll(speed) {
|
// ── UI 업데이트 ──────────────────────────────────
|
||||||
autoScrollSpeed = speed;
|
|
||||||
if (!autoScrollRAF) {
|
|
||||||
lastScrollTs = performance.now();
|
|
||||||
autoScrollRAF = requestAnimationFrame(tickAutoScroll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopAutoScroll() {
|
|
||||||
autoScrollSpeed = 0;
|
|
||||||
if (autoScrollRAF) {
|
|
||||||
cancelAnimationFrame(autoScrollRAF);
|
|
||||||
autoScrollRAF = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function tickAutoScroll(now) {
|
|
||||||
if (!dragMode) { stopAutoScroll(); return; }
|
|
||||||
const dt = (now - lastScrollTs) / 1000;
|
|
||||||
lastScrollTs = now;
|
|
||||||
gridContainer.scrollTop += autoScrollSpeed * dt;
|
|
||||||
autoScrollRAF = requestAnimationFrame(tickAutoScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
// UI 업데이트 헬퍼
|
|
||||||
// ═══════════════════════════════════════════════════════
|
|
||||||
|
|
||||||
function refreshCardSelection() {
|
function refreshCardSelection() {
|
||||||
Array.from(grid.children).forEach((card, i) => {
|
Array.from(grid.children).forEach((card, i) => {
|
||||||
card.classList.toggle('selected', selectedIdxs.has(i));
|
card.classList.toggle('selected', state.selectedIdxs.has(i));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,17 +232,16 @@ function updateNewNameLabels() {
|
|||||||
const prefix = inputPrefix.value.trim() || 'photo';
|
const prefix = inputPrefix.value.trim() || 'photo';
|
||||||
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
||||||
Array.from(grid.children).forEach((card, i) => {
|
Array.from(grid.children).forEach((card, i) => {
|
||||||
const el = card.querySelector('.card-new-name');
|
const el = card.querySelector('.card-new-name');
|
||||||
if (!el) return;
|
const file = state.files[i];
|
||||||
const file = files[i];
|
if (!el || !file) return;
|
||||||
if (!file) return;
|
|
||||||
const ext = file.name.split('.').pop().toLowerCase();
|
const ext = file.name.split('.').pop().toLowerCase();
|
||||||
el.textContent = `→ ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`;
|
el.textContent = `→ ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSelCount() {
|
function updateSelCount() {
|
||||||
const n = selectedIdxs.size;
|
const n = state.selectedIdxs.size;
|
||||||
if (n > 0) {
|
if (n > 0) {
|
||||||
selCount.textContent = `${n}개 선택됨`;
|
selCount.textContent = `${n}개 선택됨`;
|
||||||
selCount.classList.add('visible');
|
selCount.classList.add('visible');
|
||||||
@@ -543,82 +251,8 @@ function updateSelCount() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCardEl(idx) {
|
// ── 공통 UI 헬퍼 ─────────────────────────────────
|
||||||
return grid.querySelector(`.card[data-idx="${idx}"]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 드래그 고스트 배지 ────────────────────────────────
|
|
||||||
function showDragGhost(e, text) {
|
|
||||||
dragGhostEl.textContent = text;
|
|
||||||
dragGhostEl.style.display = 'block';
|
|
||||||
moveDragGhost(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
function moveDragGhost(e) {
|
|
||||||
dragGhostEl.style.left = (e.clientX + 16) + 'px';
|
|
||||||
dragGhostEl.style.top = (e.clientY - 14) + 'px';
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDragGhost() {
|
|
||||||
dragGhostEl.style.display = 'none';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 정렬 ─────────────────────────────────────────────
|
|
||||||
async function sortFiles(mode) {
|
|
||||||
if (!files.length) return;
|
|
||||||
if (mode === 'name') files.sort((a, b) => a.name.localeCompare(b.name, 'ko'));
|
|
||||||
else files.sort((a, b) => a.mtime - b.mtime);
|
|
||||||
selectedIdxs.clear();
|
|
||||||
await renderGrid();
|
|
||||||
setStatus(`${mode === 'name' ? '이름순' : '날짜순'}으로 정렬됨`, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 미리보기 ─────────────────────────────────────────
|
|
||||||
function updatePreview() {
|
|
||||||
const prefix = inputPrefix.value.trim() || 'photo';
|
|
||||||
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
|
||||||
const ext = files.length ? files[0].name.split('.').pop().toLowerCase() : 'jpg';
|
|
||||||
previewName.textContent = `${prefix}_${'1'.padStart(digits, '0')}.${ext}`;
|
|
||||||
updateNewNameLabels();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 이름 변경 ────────────────────────────────────────
|
|
||||||
async function doRename() {
|
|
||||||
if (!files.length || !currentFolder) return;
|
|
||||||
const prefix = inputPrefix.value.trim() || 'photo';
|
|
||||||
const digits = Math.max(1, Math.min(6, parseInt(inputDigits.value) || 3));
|
|
||||||
|
|
||||||
const examples = files.slice(0, 3).map((f, i) => {
|
|
||||||
const ext = f.name.split('.').pop().toLowerCase();
|
|
||||||
return `${f.name} → ${prefix}_${String(i + 1).padStart(digits, '0')}.${ext}`;
|
|
||||||
}).join('\n');
|
|
||||||
const more = files.length > 3 ? `\n... 외 ${files.length - 3}개` : '';
|
|
||||||
|
|
||||||
if (!confirm(`📂 ${files.length}개 파일을 이름 변경합니다:\n\n${examples}${more}\n\n계속하시겠습니까?`)) return;
|
|
||||||
|
|
||||||
showLoading('파일 이름 변경 중...');
|
|
||||||
setStatus('이름 변경 중...', false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await window.api.renameFiles({ folderPath: currentFolder, files, prefix, digits });
|
|
||||||
hideLoading();
|
|
||||||
if (result.errors.length) {
|
|
||||||
alert('일부 오류:\n' + result.errors.join('\n'));
|
|
||||||
} else {
|
|
||||||
showToast(`✅ ${result.renamed.length}개 파일 이름 변경 완료!`);
|
|
||||||
setStatus(`${result.renamed.length}개 파일 이름 변경 완료`, true);
|
|
||||||
}
|
|
||||||
const newList = await window.api.listImages(currentFolder);
|
|
||||||
files = newList;
|
|
||||||
selectedIdxs.clear();
|
|
||||||
await renderGrid();
|
|
||||||
} catch (e) {
|
|
||||||
hideLoading();
|
|
||||||
alert('오류 발생: ' + e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 유틸 ─────────────────────────────────────────────
|
|
||||||
function showLoading(msg = '처리 중...') {
|
function showLoading(msg = '처리 중...') {
|
||||||
loadingText.textContent = msg;
|
loadingText.textContent = msg;
|
||||||
loading.classList.add('show');
|
loading.classList.add('show');
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const state = {
|
||||||
|
files: [],
|
||||||
|
currentFolder: null,
|
||||||
|
selectedIdxs: new Set(),
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user