폴더/뷰 전환 시 썸네일 미갱신 버그 수정

원인: JustifiedGrid 셀 본문에서 snapshot/id를 한 번만 캡처(const)했다.
<For>는 인덱스 값이 같으면(뷰 전환 시 가시범위 [0..N] 동일) DOM 행을
재사용하므로, 재사용된 셀이 이전 폴더의 id·썸네일을 그대로 유지했다.
(레이아웃 리사이즈 버그와 동일 계열)

- id()/showImg()/isVideo()/version()을 snapshot() 기반 반응형 접근자로 변경
- 클릭/컨텍스트/드래그 핸들러도 호출 시점의 snapshot()을 읽도록
- img에 onLoad 추가 — 재사용된 img가 이전 onError로 display:none에 걸려
  새 썸네일이 안 보이던 문제 방지

검증: 3개 하위폴더 픽스처에서 폴더 클릭 시 가시 썸네일 id가
folderA(1-40)/folderB(41-80)/folderC(83-140)로 즉시 전환 확인.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
강 한
2026-07-19 15:04:05 +09:00
co-authored by Claude Fable 5
parent 31a0487419
commit afc983577f
+33 -12
View File
@@ -370,47 +370,68 @@ export const JustifiedGrid: Component = () => {
</Show> </Show>
<For each={indices()}> <For each={indices()}>
{(i) => { {(i) => {
const snap = snapshot()!; // 모든 파생값은 반응형으로 — For가 같은 인덱스 값에 대해 DOM을 재사용해도
const id = snap.id(i); // 뷰(폴더) 전환 시 스냅샷이 바뀌면 id/썸네일/선택상태가 즉시 갱신되도록 한다.
// 위치/크기는 반응형으로 — layout() 변경(리사이즈/줌) 시 For가 DOM을
// 재사용해도 style이 갱신되도록 한다.
const box = () => { const box = () => {
const l = layout(); const l = layout();
return l return l
? [l.boxes[i * 4], l.boxes[i * 4 + 1], l.boxes[i * 4 + 2], l.boxes[i * 4 + 3]] ? [l.boxes[i * 4], l.boxes[i * 4 + 1], l.boxes[i * 4 + 2], l.boxes[i * 4 + 3]]
: [0, 0, 0, 0]; : [0, 0, 0, 0];
}; };
const version = () => thumbVersions[id] ?? 0; const id = () => {
const showImg = () => snap.hasThumb(i) || version() > 0; const s = snapshot();
return s ? s.id(i) : -1;
};
const version = () => thumbVersions[id()] ?? 0;
const showImg = () => {
const s = snapshot();
return !!s && (s.hasThumb(i) || version() > 0);
};
const isVideo = () => {
const s = snapshot();
return !!s && s.kind(i) === 1;
};
return ( return (
<div <div
class="absolute cursor-pointer overflow-hidden rounded-[3px] bg-[var(--grid-cell-bg)] outline-offset-[-2px]" class="absolute cursor-pointer overflow-hidden rounded-[3px] bg-[var(--grid-cell-bg)] outline-offset-[-2px]"
classList={{ classList={{
"outline outline-2 outline-[var(--accent)]": selected().has(id), "outline outline-2 outline-[var(--accent)]": selected().has(id()),
}} }}
style={{ style={{
transform: `translate(${box()[0]}px, ${box()[1]}px)`, transform: `translate(${box()[0]}px, ${box()[1]}px)`,
width: `${box()[2]}px`, width: `${box()[2]}px`,
height: `${box()[3]}px`, height: `${box()[3]}px`,
}} }}
onClick={(e) => onCellClick(e, snap, i)} onClick={(e) => {
const s = snapshot();
if (s) onCellClick(e, s, i);
}}
onDblClick={() => openLightbox(i)} onDblClick={() => openLightbox(i)}
onContextMenu={(e) => onCellContextMenu(e, snap, i)} onContextMenu={(e) => {
const s = snapshot();
if (s) onCellContextMenu(e, s, i);
}}
draggable={true} draggable={true}
onDragStart={(e) => onCellDragStart(e, snap, i)} onDragStart={(e) => {
const s = snapshot();
if (s) onCellDragStart(e, s, i);
}}
> >
<Show when={showImg() && info()}> <Show when={showImg() && info()}>
<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"
decoding="async" decoding="async"
draggable={false} draggable={false}
onLoad={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "";
}}
onError={(e) => { onError={(e) => {
(e.currentTarget as HTMLImageElement).style.display = "none"; (e.currentTarget as HTMLImageElement).style.display = "none";
}} }}
/> />
</Show> </Show>
<Show when={snap.kind(i) === 1}> <Show when={isVideo()}>
<span class="absolute bottom-1 right-1 rounded bg-black/60 px-1 text-[10px] text-white"> <span class="absolute bottom-1 right-1 rounded bg-black/60 px-1 text-[10px] text-white">
</span> </span>