이름 검색 로직 개선 및 뒤로가기로 검색 페이지로 돌아온 경우에 필터 및 검색 결과가 유지되도록 개선

This commit is contained in:
static
2026-01-15 18:25:01 +09:00
parent b3e3671c09
commit ebcdbd2d83
7 changed files with 82 additions and 7 deletions

View File

@@ -32,6 +32,7 @@
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"axios": "^1.13.2", "axios": "^1.13.2",
"dexie": "^4.2.1", "dexie": "^4.2.1",
"es-hangul": "^2.3.8",
"eslint": "^9.39.2", "eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.14.0", "eslint-plugin-svelte": "^3.14.0",

8
pnpm-lock.yaml generated
View File

@@ -84,6 +84,9 @@ importers:
dexie: dexie:
specifier: ^4.2.1 specifier: ^4.2.1
version: 4.2.1 version: 4.2.1
es-hangul:
specifier: ^2.3.8
version: 2.3.8
eslint: eslint:
specifier: ^9.39.2 specifier: ^9.39.2
version: 9.39.2(jiti@1.21.7) version: 9.39.2(jiti@1.21.7)
@@ -989,6 +992,9 @@ packages:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-hangul@2.3.8:
resolution: {integrity: sha512-VrJuqYBC7W04aKYjCnswomuJNXQRc0q33SG1IltVrRofi2YEE6FwVDPlsEJIdKbHwsOpbBL/mk9sUaBxVpbd+w==}
es-object-atoms@1.1.1: es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2781,6 +2787,8 @@ snapshots:
es-errors@1.3.0: {} es-errors@1.3.0: {}
es-hangul@2.3.8: {}
es-object-atoms@1.1.1: es-object-atoms@1.1.1:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0

View File

@@ -553,7 +553,7 @@ export const searchFiles = async (
: await baseQuery.execute(); : await baseQuery.execute();
return files.map((file) => ({ return files.map((file) => ({
id: file.id, id: file.id,
parentId: file.parent_id ?? "root", parentId: file.parent_id ?? ("root" as const),
userId: file.user_id, userId: file.user_id,
path: file.path, path: file.path,
mekVersion: file.master_encryption_key_version, mekVersion: file.master_encryption_key_version,

View File

@@ -1,4 +1,5 @@
export * from "./concurrency"; export * from "./concurrency";
export * from "./format"; export * from "./format";
export * from "./gotoStateful"; export * from "./gotoStateful";
export * from "./search";
export * from "./sort"; export * from "./sort";

28
src/lib/utils/search.ts Normal file
View File

@@ -0,0 +1,28 @@
import { disassemble, getChoseong } from "es-hangul";
const normalize = (s: string) => {
return s.normalize("NFC").toLowerCase().replace(/\s/g, "");
};
const extractHangul = (s: string) => {
return s.replace(/[^가-힣ㄱ-ㅎㅏ-ㅣ]/g, "");
};
const hangulSearch = (original: string, query: string) => {
original = extractHangul(original);
query = extractHangul(query);
if (!original || !query) return false;
return (
disassemble(original).includes(disassemble(query)) ||
getChoseong(original).includes(getChoseong(query))
);
};
export const searchString = (original: string, query: string) => {
original = normalize(original);
query = normalize(query);
if (!original || !query) return false;
return original.includes(query) || hangulSearch(original, query);
};

View File

@@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Snapshot } from "@sveltejs/kit";
import superjson, { type SuperJSONResult } from "superjson";
import { untrack } from "svelte";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { Chip, FullscreenDiv, RowVirtualizer } from "$lib/components/atoms"; import { Chip, FullscreenDiv, RowVirtualizer } from "$lib/components/atoms";
@@ -8,7 +11,7 @@
type MaybeDirectoryInfo, type MaybeDirectoryInfo,
} from "$lib/modules/filesystem"; } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import { HybridPromise, sortEntries } from "$lib/utils"; import { HybridPromise, searchString, sortEntries } from "$lib/utils";
import Directory from "./Directory.svelte"; import Directory from "./Directory.svelte";
import File from "./File.svelte"; import File from "./File.svelte";
import SearchBar from "./SearchBar.svelte"; import SearchBar from "./SearchBar.svelte";
@@ -17,15 +20,24 @@
let { data } = $props(); let { data } = $props();
interface SearchFilters {
name: string;
includeImages: boolean;
includeVideos: boolean;
includeDirectories: boolean;
searchInDirectory: boolean;
categories: SearchFilter["categories"];
}
let directoryInfo: MaybeDirectoryInfo | undefined = $state(); let directoryInfo: MaybeDirectoryInfo | undefined = $state();
let filters = $state({ let filters: SearchFilters = $state({
name: "", name: "",
includeImages: false, includeImages: false,
includeVideos: false, includeVideos: false,
includeDirectories: false, includeDirectories: false,
searchInDirectory: false, searchInDirectory: false,
categories: [] as SearchFilter["categories"], categories: [],
}); });
let hasCategoryFilter = $derived(filters.categories.length > 0); let hasCategoryFilter = $derived(filters.categories.length > 0);
let hasAnyFilter = $derived( let hasAnyFilter = $derived(
@@ -36,11 +48,12 @@
filters.name.trim().length > 0, filters.name.trim().length > 0,
); );
let isRestoredFromSnapshot = $state(false);
let serverResult: SearchResult | undefined = $state(); let serverResult: SearchResult | undefined = $state();
let result = $derived.by(() => { let result = $derived.by(() => {
if (!serverResult) return []; if (!serverResult) return [];
const nameFilter = filters.name.trim().toLowerCase(); const nameFilter = filters.name.trim();
const hasTypeFilter = const hasTypeFilter =
filters.includeImages || filters.includeVideos || filters.includeDirectories; filters.includeImages || filters.includeVideos || filters.includeDirectories;
@@ -58,7 +71,7 @@
return sortEntries( return sortEntries(
[...directories, ...files].filter( [...directories, ...files].filter(
({ name }) => !nameFilter || name.toLowerCase().includes(nameFilter), ({ name }) => !nameFilter || searchString(name, nameFilter),
), ),
); );
}); });
@@ -81,6 +94,20 @@
} }
}; };
export const snapshot: Snapshot<{
filters: SearchFilters;
serverResult: SuperJSONResult;
}> = {
capture() {
return { filters, serverResult: superjson.serialize(serverResult) };
},
restore(value) {
filters = value.filters;
serverResult = superjson.deserialize(value.serverResult, { inPlace: true });
isRestoredFromSnapshot = true;
},
};
$effect(() => { $effect(() => {
if (data.directoryId) { if (data.directoryId) {
HybridPromise.resolve(getDirectoryInfo(data.directoryId, $masterKeyStore?.get(1)?.key!)).then( HybridPromise.resolve(getDirectoryInfo(data.directoryId, $masterKeyStore?.get(1)?.key!)).then(
@@ -96,6 +123,16 @@
}); });
$effect(() => { $effect(() => {
// Svelte sucks
hasAnyFilter;
filters.searchInDirectory;
filters.categories.length;
if (untrack(() => isRestoredFromSnapshot)) {
isRestoredFromSnapshot = false;
return;
}
if (hasAnyFilter) { if (hasAnyFilter) {
requestSearch( requestSearch(
{ {

View File

@@ -91,5 +91,5 @@ export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey)
), ),
]); ]);
return { directories, files }; return { directories, files } satisfies SearchResult;
}; };