그리드에 썸네일 '생성 중' 스켈레톤 표시

썸네일 미준비 셀이 빈 칸으로 보이던 것을 개선. 스냅샷 flags에
비트1(생성 실패, thumb_state=2)을 추가해 대기/실패를 구분한다.
- 대기(생성 전): 은은한 pulse 스켈레톤(bg-hover)
- 실패: 정적 아이콘(🖼) — 무한 pulse 방지
- 준비됨: 썸네일 이미지

검증: 619개 대기 상태에서 하단 스크롤 시 pulse 스켈레톤 50개 →
생성 완료되며 이미지로 즉시 전환 확인(bg #33363d, animation pulse).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
강 한
2026-07-19 15:33:13 +09:00
co-authored by Claude Fable 5
parent afc983577f
commit df2bb5dc8b
3 changed files with 32 additions and 2 deletions
+7 -1
View File
@@ -307,7 +307,13 @@ fn pack_snapshot(rows: Vec<(i64, Option<u32>, Option<u32>, Option<u32>, i64, i64
buf.extend_from_slice(&(w.min(65535) as u16).to_le_bytes()); buf.extend_from_slice(&(w.min(65535) as u16).to_le_bytes());
buf.extend_from_slice(&(h.min(65535) as u16).to_le_bytes()); buf.extend_from_slice(&(h.min(65535) as u16).to_le_bytes());
buf.push(kind as u8); buf.push(kind as u8);
let flags = if thumb_state == 1 { 1u8 } else { 0u8 }; // 비트0 = 썸네일 준비됨(state 1), 비트1 = 생성 시도했으나 실패(state 2).
// 둘 다 0이면 생성 대기(state 0) — 프론트에서 스켈레톤/실패표시 구분용.
let flags = match thumb_state {
1 => 1u8,
2 => 2u8,
_ => 0u8,
};
buf.push(flags); buf.push(flags);
} }
buf buf
+21 -1
View File
@@ -387,6 +387,11 @@ export const JustifiedGrid: Component = () => {
const s = snapshot(); const s = snapshot();
return !!s && (s.hasThumb(i) || version() > 0); return !!s && (s.hasThumb(i) || version() > 0);
}; };
// 썸네일 미준비 상태: 실패(state 2)면 정적 표시, 그 외엔 '생성 중' 스켈레톤.
const failed = () => {
const s = snapshot();
return !!s && s.thumbFailed(i);
};
const isVideo = () => { const isVideo = () => {
const s = snapshot(); const s = snapshot();
return !!s && s.kind(i) === 1; return !!s && s.kind(i) === 1;
@@ -417,7 +422,22 @@ export const JustifiedGrid: Component = () => {
if (s) onCellDragStart(e, s, i); if (s) onCellDragStart(e, s, i);
}} }}
> >
<Show when={showImg() && info()}> <Show
when={showImg() && info()}
fallback={
// 준비 전: 실패면 정적 아이콘, 대기면 은은한 스켈레톤(생성 중)
<Show
when={!failed()}
fallback={
<div class="grid h-full w-full place-items-center text-[var(--text-muted)]">
<span class="text-base opacity-60">🖼</span>
</div>
}
>
<div class="h-full w-full animate-pulse bg-[var(--bg-hover)]" />
</Show>
}
>
<img <img
src={`${thumbUrl(info()!, id())}&v=${version()}`} src={`${thumbUrl(info()!, id())}&v=${version()}`}
class="h-full w-full object-cover" class="h-full w-full object-cover"
+4
View File
@@ -98,6 +98,10 @@ export class Snapshot {
hasThumb(i: number): boolean { hasThumb(i: number): boolean {
return (this.view.getUint8(i * ITEM_BYTES + 9) & 1) !== 0; return (this.view.getUint8(i * ITEM_BYTES + 9) & 1) !== 0;
} }
/** 썸네일 생성을 시도했으나 실패함(state 2) — 대기와 구분해 스켈레톤을 멈추기 위함 */
thumbFailed(i: number): boolean {
return (this.view.getUint8(i * ITEM_BYTES + 9) & 2) !== 0;
}
/** 레이아웃 워커 입력용 종횡비 배열 (미지 치수는 1:1) */ /** 레이아웃 워커 입력용 종횡비 배열 (미지 치수는 1:1) */
aspectRatios(): Float32Array { aspectRatios(): Float32Array {
const out = new Float32Array(this.count); const out = new Float32Array(this.count);