리펙토링: Node.js 프로젝트 폴더 구조 적용

This commit is contained in:
2026-05-14 11:01:11 +09:00
parent 65ac35929a
commit dcd3c9a6e9
13 changed files with 4 additions and 4 deletions
+219
View File
@@ -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}"]`);
}
+29
View File
@@ -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);
}
+90
View File
@@ -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();
}