diff --git a/CLAUDE.md b/CLAUDE.md index e587871..bf70cb5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ src/ ├── drag-drop.js # Card drag-and-drop reorder (single and multi-card) ├── selection.js # Rubber-band selection, Shift-click, Cmd+click ├── pinch-zoom.js # Trackpad pinch to resize thumbnails (CSS --thumb-size, 150–400px) - └── hover-preview.js # Hover preview overlay + └── image-preview.js # Hover preview overlay ``` ### IPC surface diff --git a/src/renderer/handlers/hover-preview.js b/src/renderer/handlers/image-preview.js similarity index 50% rename from src/renderer/handlers/hover-preview.js rename to src/renderer/handlers/image-preview.js index 5030af2..9c8ba67 100644 --- a/src/renderer/handlers/hover-preview.js +++ b/src/renderer/handlers/image-preview.js @@ -1,14 +1,17 @@ -const DELAY = 1000; +const SHOW_DELAY = 1000; +const HIDE_DELAY = 100; const SVG_NS = 'http://www.w3.org/2000/svg'; const RADIUS = 20; const CIRC = 2 * Math.PI * RADIUS; // ~125.66 -let _el = null; -let _imgEl = null; -let _timer = null; -let _visible = false; -let _loaderEl = null; +let _el = null; +let _imgEl = null; +let _showTimer = null; +let _hideTimer = null; +let _visible = false; +let _loaderEl = null; +let _activeCard = null; export function init() { _el = document.createElement('div'); @@ -26,35 +29,75 @@ export function setupCard(card) { function _onEnter() { const card = this; - clearTimeout(_timer); + _activeCard = card; + + // 팝업이 사라지려고 대기 중이면 취소 + if (_hideTimer) { + clearTimeout(_hideTimer); + _hideTimer = null; + } + + if (_visible) { + // 이미 팝업이 열려 있으면 즉시 내용 전환 + _switchTo(card); + return; + } + + // 팝업이 닫힌 상태 — 1초 대기 후 표시 + clearTimeout(_showTimer); _showLoader(card); - - _timer = setTimeout(async () => { + _showTimer = setTimeout(async () => { + _showTimer = null; _removeLoader(); - const thumb = card.querySelector('img.card-thumb'); - if (!thumb) return; + if (_activeCard !== card) return; - // 아직 lazy-load가 안 된 경우 직접 로딩 - let src = thumb.src; - if (!src) { - src = await window.api.readImage(card.dataset.filepath) || ''; - if (src) thumb.src = src; // 카드에도 캐싱 - } - if (!src) return; + const src = await _loadSrc(card); + if (!src || _activeCard !== card) return; _imgEl.src = src; _visible = true; _show(card); - }, DELAY); + }, SHOW_DELAY); } function _onLeave() { - clearTimeout(_timer); - _timer = null; - _visible = false; - _removeLoader(); - _el.style.display = 'none'; - _el.style.visibility = 'hidden'; + if (!_visible) { + // 팝업 미표시 상태 — 로더·타이머만 정리 + clearTimeout(_showTimer); + _showTimer = null; + _removeLoader(); + return; + } + + // 팝업 표시 중 — 0.1초 후 닫기 (다른 카드 진입 시 취소됨) + _hideTimer = setTimeout(() => { + _hideTimer = null; + _visible = false; + _el.style.display = 'none'; + _el.style.visibility = 'hidden'; + }, HIDE_DELAY); +} + +// ── 팝업 내용 전환 (이미 열린 상태에서 카드 변경) ── + +async function _switchTo(card) { + const src = await _loadSrc(card); + if (!src || _activeCard !== card) return; + + _imgEl.src = src; + _show(card); +} + +// ── 이미지 소스 반환 (카드 썸네일 캐시 우선) ── + +async function _loadSrc(card) { + const thumb = card.querySelector('img.card-thumb'); + let src = thumb?.src || ''; + if (!src) { + src = await window.api.readImage(card.dataset.filepath) || ''; + if (src && thumb) thumb.src = src; + } + return src; } // ── 원형 로딩바 ────────────────────────────────── @@ -66,26 +109,17 @@ function _showLoader(card) { svg.classList.add('card-hover-loader'); svg.setAttribute('viewBox', '0 0 48 48'); - // 배경 원 const bg = document.createElementNS(SVG_NS, 'circle'); bg.classList.add('loader-bg'); - bg.setAttribute('cx', '24'); - bg.setAttribute('cy', '24'); - bg.setAttribute('r', '24'); + bg.setAttribute('cx', '24'); bg.setAttribute('cy', '24'); bg.setAttribute('r', '24'); - // 트랙 링 const track = document.createElementNS(SVG_NS, 'circle'); track.classList.add('loader-track'); - track.setAttribute('cx', '24'); - track.setAttribute('cy', '24'); - track.setAttribute('r', String(RADIUS)); + track.setAttribute('cx', '24'); track.setAttribute('cy', '24'); track.setAttribute('r', String(RADIUS)); - // 진행 호 const progress = document.createElementNS(SVG_NS, 'circle'); progress.classList.add('loader-progress'); - progress.setAttribute('cx', '24'); - progress.setAttribute('cy', '24'); - progress.setAttribute('r', String(RADIUS)); + progress.setAttribute('cx', '24'); progress.setAttribute('cy', '24'); progress.setAttribute('r', String(RADIUS)); progress.style.strokeDasharray = CIRC; progress.style.strokeDashoffset = CIRC; @@ -103,7 +137,7 @@ function _removeLoader() { } } -// ── 이미지 팝업 ────────────────────────────────── +// ── 이미지 팝업 위치 계산 및 표시 ──────────────── function _show(card) { _el.style.display = 'block'; @@ -116,8 +150,8 @@ function _show(card) { const popup = _el.getBoundingClientRect(); const cardRect = card.getBoundingClientRect(); - const vw = window.innerWidth; - const vh = window.innerHeight; + const vw = window.innerWidth; + const vh = window.innerHeight; const gap = 16; let left = cardRect.right + gap; diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index 8545961..ce5f4af 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -2,7 +2,7 @@ import { state } from './state.js'; import * as pinchZoom from './handlers/pinch-zoom.js'; import * as selection from './handlers/selection.js'; import * as dragDrop from './handlers/drag-drop.js'; -import * as hoverPreview from './handlers/hover-preview.js'; +import * as hoverPreview from './handlers/image-preview.js'; let _cardObserver = null;