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