이미지 lazy loading 적용
This commit is contained in:
@@ -29,11 +29,20 @@ function _onEnter() {
|
||||
clearTimeout(_timer);
|
||||
_showLoader(card);
|
||||
|
||||
_timer = setTimeout(() => {
|
||||
_timer = setTimeout(async () => {
|
||||
_removeLoader();
|
||||
const thumb = card.querySelector('img.card-thumb');
|
||||
if (!thumb) return;
|
||||
_imgEl.src = thumb.src;
|
||||
|
||||
// 아직 lazy-load가 안 된 경우 직접 로딩
|
||||
let src = thumb.src;
|
||||
if (!src) {
|
||||
src = await window.api.readImage(card.dataset.filepath) || '';
|
||||
if (src) thumb.src = src; // 카드에도 캐싱
|
||||
}
|
||||
if (!src) return;
|
||||
|
||||
_imgEl.src = src;
|
||||
_visible = true;
|
||||
_show(card);
|
||||
}, DELAY);
|
||||
|
||||
+28
-26
@@ -4,6 +4,8 @@ import * as selection from './handlers/selection.js';
|
||||
import * as dragDrop from './handlers/drag-drop.js';
|
||||
import * as hoverPreview from './handlers/hover-preview.js';
|
||||
|
||||
let _cardObserver = null;
|
||||
|
||||
// ── DOM ──────────────────────────────────────────────
|
||||
const grid = document.getElementById('grid');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
@@ -70,40 +72,47 @@ async function openFolder() {
|
||||
state.files.push(...list);
|
||||
state.selectedIdxs.clear();
|
||||
setStatus(`${state.files.length}개 파일 로드됨`, true);
|
||||
await renderGrid();
|
||||
renderGrid();
|
||||
updatePreview();
|
||||
btnRename.disabled = false;
|
||||
btnReveal.disabled = false;
|
||||
}
|
||||
|
||||
// ── 그리드 렌더링 ────────────────────────────────
|
||||
async function renderGrid() {
|
||||
function renderGrid() {
|
||||
if (_cardObserver) { _cardObserver.disconnect(); _cardObserver = null; }
|
||||
|
||||
grid.innerHTML = '';
|
||||
emptyState.style.display = 'none';
|
||||
grid.style.display = 'grid';
|
||||
fileCount.textContent = `${state.files.length}개 파일`;
|
||||
updateSelCount();
|
||||
|
||||
showLoading(`썸네일 로딩 중... (0/${state.files.length})`);
|
||||
// 카드 껍데기만 즉시 생성 (이미지 없음)
|
||||
state.files.forEach((file, idx) => grid.appendChild(createCard(file, idx)));
|
||||
|
||||
const BATCH = 10;
|
||||
for (let i = 0; i < state.files.length; i += BATCH) {
|
||||
const batch = state.files.slice(i, i + BATCH);
|
||||
const results = await Promise.all(batch.map(f => window.api.readImage(f.path)));
|
||||
results.forEach((dataUrl, bi) => {
|
||||
grid.appendChild(createCard(state.files[i + bi], i + bi, dataUrl));
|
||||
// 뷰포트 ±400px 범위에 들어온 카드만 이미지 로딩
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(async (entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(entry.target);
|
||||
const img = entry.target.querySelector('img.card-thumb');
|
||||
if (!img || img.src) return;
|
||||
const dataUrl = await window.api.readImage(entry.target.dataset.filepath);
|
||||
if (dataUrl) img.src = dataUrl;
|
||||
});
|
||||
loadingText.textContent = `썸네일 로딩 중... (${Math.min(i + BATCH, state.files.length)}/${state.files.length})`;
|
||||
}
|
||||
}, { root: gridContainer, rootMargin: '400px 0px', threshold: 0 });
|
||||
|
||||
Array.from(grid.children).forEach(card => observer.observe(card));
|
||||
_cardObserver = observer;
|
||||
|
||||
hideLoading();
|
||||
refreshCardSelection();
|
||||
updateNumberBadges();
|
||||
updateNewNameLabels();
|
||||
}
|
||||
|
||||
// ── 카드 생성 ────────────────────────────────────
|
||||
function createCard(file, idx, dataUrl) {
|
||||
function createCard(file, idx) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.dataset.idx = idx;
|
||||
@@ -118,18 +127,11 @@ function createCard(file, idx, dataUrl) {
|
||||
checkBadge.className = 'card-check';
|
||||
checkBadge.textContent = '✓';
|
||||
|
||||
let thumb;
|
||||
if (dataUrl) {
|
||||
thumb = document.createElement('img');
|
||||
thumb.className = 'card-thumb';
|
||||
thumb.src = dataUrl;
|
||||
thumb.alt = file.name;
|
||||
thumb.draggable = false;
|
||||
} else {
|
||||
thumb = document.createElement('div');
|
||||
thumb.className = 'card-thumb-placeholder';
|
||||
thumb.textContent = '🖼️';
|
||||
}
|
||||
// src 없이 생성 — IntersectionObserver가 뷰포트 진입 시 로딩
|
||||
const thumb = document.createElement('img');
|
||||
thumb.className = 'card-thumb';
|
||||
thumb.alt = file.name;
|
||||
thumb.draggable = false;
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
@@ -208,7 +210,7 @@ async function doRename() {
|
||||
state.files.length = 0;
|
||||
state.files.push(...newList);
|
||||
state.selectedIdxs.clear();
|
||||
await renderGrid();
|
||||
renderGrid();
|
||||
} catch (e) {
|
||||
hideLoading();
|
||||
alert('오류 발생: ' + e.message);
|
||||
|
||||
@@ -165,6 +165,16 @@ body {
|
||||
background: #08080f;
|
||||
display: block;
|
||||
}
|
||||
/* 이미지 로딩 전 shimmer */
|
||||
.card-thumb:not([src]) {
|
||||
background: linear-gradient(90deg, #08080f 25%, #14141f 50%, #08080f 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: thumb-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes thumb-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
.card-thumb-placeholder {
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
Reference in New Issue
Block a user