리펙토링: Node.js 프로젝트 폴더 구조 적용
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();
|
||||
}
|
||||
Reference in New Issue
Block a user