From ac3b527e8e64487c98e25c40a3e74ca6b07097ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=20=ED=95=9C?= Date: Sun, 19 Jul 2026 23:35:13 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=ED=83=90=EC=A7=80=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=9D=84=20=EC=83=81=ED=83=9C=EB=B0=94?= =?UTF-8?q?=EC=97=90=20=ED=91=9C=EC=8B=9C=20=E2=80=94=20=ED=8C=A8=EB=84=90?= =?UTF-8?q?=EC=9D=84=20=EB=8B=AB=EC=95=84=EB=8F=84=20=EC=A7=84=ED=96=89/?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EA=B0=80=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기존엔 중복 탐지 진행이 전체화면 패널 안에서만 보였고, 패널을 닫으면 진행 상황을 알 수 없었다. - 중복 탐지 실행/진행 상태를 store로 이관(runDedupe/dedupeRunning/dedupeProgress) → 패널 열림 여부와 무관하게 유지. - DedupePanel은 store 상태를 사용(로컬 running/progress 제거), 완료 시 결과 갱신. - StatusBar에 진행 표시: "완전 동일 검사 N/M" + 스피너 + 취소(✕), 텍스트 클릭 시 패널 다시 열기. 검증(대용량 3000장): 패널을 닫은 상태에서 상태바가 완전 동일 검사 517→3,000 진행을 실시간 표시, 취소 버튼 노출 확인. Co-Authored-By: Claude Fable 5 --- src/features/dedupe/DedupePanel.tsx | 49 ++++++++++++++--------------- src/shell/StatusBar.tsx | 32 ++++++++++++++++++- src/state/store.ts | 36 +++++++++++++++++++++ 3 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/features/dedupe/DedupePanel.tsx b/src/features/dedupe/DedupePanel.tsx index dc3a039..8ffed25 100644 --- a/src/features/dedupe/DedupePanel.tsx +++ b/src/features/dedupe/DedupePanel.tsx @@ -5,28 +5,30 @@ import { createResource, createSignal, For, + on, Show, type Component, } from "solid-js"; import { ask } from "@tauri-apps/plugin-dialog"; import { ko } from "../../i18n/ko"; import { - cancelDedupe, dismissDupePair, listDupeGroups, resolveDupeGroup, - startDedupe, thumbUrl, trashFiles, - type DedupeProgress, type DupeGroup, } from "../../ipc/commands"; import { closeDedupe, dedupeOpen, + dedupeProgress, + dedupeRunning, info, refreshSidebar, refreshSnapshot, + requestCancelDedupe, + runDedupe, toast, view, } from "../../state/store"; @@ -40,8 +42,6 @@ function fmtSize(bytes: number): string { const KIND_LABEL: Record = { 0: "완전 동일", 1: "유사 이미지", 2: "유사 영상" }; export const DedupePanel: Component = () => { - const [running, setRunning] = createSignal(false); - const [progress, setProgress] = createSignal(); const [includeVideo, setIncludeVideo] = createSignal(false); const [threshold, setThreshold] = createSignal(5); const [groupsVersion, setGroupsVersion] = createSignal(0); @@ -67,28 +67,25 @@ export const DedupePanel: Component = () => { }); const runScan = async () => { - setRunning(true); - setProgress(undefined); const scoped = useCurrentScope() ? currentScope() : null; try { - await new Promise((resolve, reject) => { - startDedupe( - { ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() }, - (p) => { - setProgress(p); - if (p.phase === "done") resolve(); - }, - ).catch(reject); - }); + await runDedupe({ ...(scoped?.args ?? {}), imageThreshold: threshold(), includeVideo: includeVideo() }); } catch (e) { toast(String(e)); - } finally { - setRunning(false); - setProgress(undefined); - setGroupsVersion((v) => v + 1); } }; + // 탐지 완료(실행중 true→false) 시 결과 목록 갱신 — 패널이 닫혀 있다 열려도 재조회됨. + createEffect( + on( + dedupeRunning, + (running, prev) => { + if (prev && !running) setGroupsVersion((v) => v + 1); + }, + { defer: true }, + ), + ); + /** keeper 외 전부 휴지통 */ const resolveGroup = async (g: DupeGroup) => { const losers = g.members.filter((m) => !m.isKeeper).map((m) => m.fileId); @@ -181,20 +178,20 @@ export const DedupePanel: Component = () => {
- + - {progress() - ? ko.dedupe.scanning(progress()!.phase, progress()!.current, progress()!.total) + {dedupeProgress() + ? ko.dedupe.scanning(dedupeProgress()!.phase, dedupeProgress()!.current, dedupeProgress()!.total) : ko.dedupe.starting} - + + + + + {/* 백그라운드 처리 진행 — 썸네일/메타 대기 수 (있을 때만) */} 0 || pending().meta > 0}> diff --git a/src/state/store.ts b/src/state/store.ts index f696412..f90001b 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -3,13 +3,16 @@ import { createSignal } from "solid-js"; import { createStore, produce } from "solid-js/store"; import { appInfo, + cancelDedupe, folderSnapshot, pendingCounts as fetchPendingCounts, searchSnapshot, smartSnapshot, + startDedupe, tagSnapshot, Snapshot, type AppInfo, + type DedupeProgress, type PendingCounts, type ScanProgress, } from "../ipc/commands"; @@ -196,6 +199,39 @@ export { dedupeOpen }; export const openDedupe = () => setDedupeOpen(true); export const closeDedupe = () => setDedupeOpen(false); +// ── 중복 탐지 실행(전역) — 패널을 닫아도 상태바에서 진행이 보이도록 store가 소유 ── +const [dedupeProgress, setDedupeProgress] = createSignal(); +const [dedupeRunning, setDedupeRunning] = createSignal(false); +export { dedupeProgress, dedupeRunning }; + +export interface DedupeRunArgs { + sourceId?: number; + folderId?: number; + recursive?: boolean; + imageThreshold?: number; + includeVideo?: boolean; +} + +/** 중복 탐지 실행. 진행 상태는 store에 유지되어 패널을 닫아도 상태바에서 보인다. */ +export async function runDedupe(args: DedupeRunArgs): Promise { + if (dedupeRunning()) return; + setDedupeRunning(true); + setDedupeProgress(undefined); + try { + await new Promise((resolve, reject) => { + startDedupe(args, (p) => { + setDedupeProgress(p); + if (p.phase === "done") resolve(); + }).catch(reject); + }); + } finally { + setDedupeRunning(false); + setDedupeProgress(undefined); + } +} + +export const requestCancelDedupe = () => void cancelDedupe(); + // ── 상태 메시지 (토스트) ───────────────────────────────── const [statusMessage, setStatusMessage] = createSignal(); export { statusMessage };