From c09227facc56005b8549f97fd7a6c5a007c9e397 Mon Sep 17 00:00:00 2001 From: jisangs Date: Thu, 14 May 2026 11:43:54 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A7=88=EC=9A=B0=EC=8A=A4=20=EC=98=A4?= =?UTF-8?q?=EB=B2=84=20=ED=9B=84=201=EC=B4=88=EA=B0=80=20=EC=A7=80?= =?UTF-8?q?=EB=82=98=EB=A9=B4=20=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=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/handlers/hover-preview.js | 73 ++++++++++++++++++++++++++ src/renderer/renderer.js | 11 ++-- src/renderer/style.css | 23 ++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/renderer/handlers/hover-preview.js 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%;