diff --git a/src/renderer/handlers/hover-preview.js b/src/renderer/handlers/hover-preview.js new file mode 100644 index 0000000..18f0ca2 --- /dev/null +++ b/src/renderer/handlers/hover-preview.js @@ -0,0 +1,73 @@ +const DELAY = 1000; + +let _el = null; +let _imgEl = null; +let _timer = null; +let _visible = false; + +export function init() { + _el = document.createElement('div'); + _el.className = 'hover-preview'; + + _imgEl = document.createElement('img'); + _el.appendChild(_imgEl); + document.body.appendChild(_el); +} + +export function setupCard(card) { + card.addEventListener('mouseenter', _onEnter); + card.addEventListener('mouseleave', _onLeave); +} + +function _onEnter() { + const card = this; + clearTimeout(_timer); + _timer = setTimeout(() => { + const thumb = card.querySelector('img.card-thumb'); + if (!thumb) return; + _imgEl.src = thumb.src; + _visible = true; + _show(card); + }, DELAY); +} + +function _onLeave() { + clearTimeout(_timer); + _timer = null; + _visible = false; + _el.style.display = 'none'; + _el.style.visibility = 'hidden'; +} + +function _show(card) { + // 위치 계산 전 화면 밖에 렌더링해서 실제 크기를 측정 + _el.style.display = 'block'; + _el.style.visibility = 'hidden'; + _el.style.left = '0px'; + _el.style.top = '0px'; + + requestAnimationFrame(() => { + if (!_visible) return; + + const popup = _el.getBoundingClientRect(); + const cardRect = card.getBoundingClientRect(); + const vw = window.innerWidth; + const vh = window.innerHeight; + const gap = 16; + + // 가로: 카드 오른쪽 우선, 공간 부족 시 왼쪽 + let left = cardRect.right + gap; + if (left + popup.width > vw - 8) { + left = cardRect.left - popup.width - gap; + } + left = Math.max(8, Math.min(left, vw - popup.width - 8)); + + // 세로: 카드와 중앙 정렬, 화면 경계 클램프 + let top = cardRect.top + (cardRect.height - popup.height) / 2; + top = Math.max(8, Math.min(top, vh - popup.height - 8)); + + _el.style.left = left + 'px'; + _el.style.top = top + 'px'; + _el.style.visibility = 'visible'; + }); +} diff --git a/src/renderer/renderer.js b/src/renderer/renderer.js index ac96e02..0b9e0cc 100644 --- a/src/renderer/renderer.js +++ b/src/renderer/renderer.js @@ -1,7 +1,8 @@ -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 { 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'; // ── DOM ────────────────────────────────────────────── const grid = document.getElementById('grid'); @@ -38,6 +39,7 @@ const callbacks = { pinchZoom.init({ gridContainer, thumbSizeHint }); selection.init({ gridContainer, rubberBand, grid, state, callbacks }); dragDrop.init({ grid, gridContainer, dragGhostEl, state, callbacks }); +hoverPreview.init(); // ── 버튼 이벤트 ────────────────────────────────── inputPrefix.addEventListener('input', updatePreview); @@ -146,6 +148,7 @@ function createCard(file, idx, dataUrl) { card.appendChild(body); dragDrop.setupCard(card); + hoverPreview.setupCard(card); card.addEventListener('click', selection.onCardClick); return card; diff --git a/src/renderer/style.css b/src/renderer/style.css index f15b10b..eab4416 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -269,6 +269,29 @@ body { @keyframes spin { to { transform: rotate(360deg); } } #loading p { color: var(--text); font-size: 14px; } +/* ── 호버 이미지 팝업 ── */ +.hover-preview { + position: fixed; + display: none; + visibility: hidden; + z-index: 8000; + pointer-events: none; + background: #0d0d1a; + border: 1px solid var(--border); + border-radius: 12px; + padding: 8px; + box-shadow: 0 20px 60px rgba(0,0,0,0.85), 0 0 0 1px rgba(124,106,247,0.15); +} +.hover-preview img { + display: block; + max-width: 840px; + max-height: 840px; + width: auto; + height: auto; + border-radius: 6px; + object-fit: contain; +} + /* ── 토스트 ── */ #toast { position: fixed; bottom: 40px; left: 50%;