From 72b09a95543504acee5f36ba9f2e595921cb4c3a Mon Sep 17 00:00:00 2001 From: jisangs Date: Thu, 14 May 2026 16:45:52 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=AF=B8?= =?UTF-8?q?=EB=A6=AC=EB=B3=B4=EA=B8=B0=20=ED=8C=9D=EC=97=85=EC=B0=BD?= =?UTF-8?q?=EC=9D=B4=20=EB=8B=A4=EB=A5=B8=20=EC=B9=B4=EB=93=9C=EC=97=90=20?= =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=EC=98=A4=EB=B2=84=EB=90=98?= =?UTF-8?q?=EC=96=B4=EB=8F=84=20=EC=9C=A0=EC=A7=80=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 2 +- .../{hover-preview.js => image-preview.js} | 114 ++++++++++++------ src/renderer/renderer.js | 2 +- 3 files changed, 76 insertions(+), 42 deletions(-) rename src/renderer/handlers/{hover-preview.js => image-preview.js} (50%) 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;