diff --git a/src/lib/modules/filesystem/internal.svelte.ts b/src/lib/modules/filesystem/internal.svelte.ts index 30ba83b..918ceef 100644 --- a/src/lib/modules/filesystem/internal.svelte.ts +++ b/src/lib/modules/filesystem/internal.svelte.ts @@ -34,7 +34,7 @@ export class FilesystemCache { } (state.value - ? Promise.resolve(state.value) + ? Promise.resolve($state.snapshot(state.value) as V) : this.options.fetchFromIndexedDB(key).then((loadedInfo) => { if (loadedInfo) { state.value = loadedInfo; diff --git a/src/lib/server/db/directory.ts b/src/lib/server/db/directory.ts index 432b1dd..6c566e7 100644 --- a/src/lib/server/db/directory.ts +++ b/src/lib/server/db/directory.ts @@ -74,28 +74,6 @@ export const getAllDirectoriesByParent = async (userId: number, parentId: Direct return directories.map(toDirectory); }; -export const getAllRecursiveDirectoriesByParent = async (userId: number, parentId: DirectoryId) => { - const directories = await db - .withRecursive("directory_tree", (db) => - db - .selectFrom("directory") - .selectAll() - .$if(parentId === "root", (qb) => qb.where("parent_id", "is", null)) - .$if(parentId !== "root", (qb) => qb.where("parent_id", "=", parentId as number)) - .where("user_id", "=", userId) - .unionAll((db) => - db - .selectFrom("directory") - .innerJoin("directory_tree", "directory.parent_id", "directory_tree.id") - .selectAll("directory"), - ), - ) - .selectFrom("directory_tree") - .selectAll() - .execute(); - return directories.map(toDirectory); -}; - export const getAllFavoriteDirectories = async (userId: number) => { const directories = await db .selectFrom("directory") @@ -117,6 +95,61 @@ export const getDirectory = async (userId: number, directoryId: number) => { return directory ? toDirectory(directory) : null; }; +export const searchDirectories = async ( + userId: number, + filters: { + parentId: DirectoryId; + inFavorites: boolean; + }, +) => { + const directories = await db + .withRecursive("directory_tree", (db) => + db + .selectFrom("directory") + .select("id") + .where("user_id", "=", userId) + .$if(filters.parentId === "root", (qb) => qb.where((eb) => eb.lit(false))) // directory_tree will be empty if parentId is "root" + .$if(filters.parentId !== "root", (qb) => qb.where("id", "=", filters.parentId as number)) + .unionAll( + db + .selectFrom("directory as d") + .innerJoin("directory_tree as dt", "d.parent_id", "dt.id") + .select("d.id"), + ), + ) + .withRecursive("favorite_directory_tree", (db) => + db + .selectFrom("directory") + .select("id") + .where("user_id", "=", userId) + .$if(!filters.inFavorites, (qb) => qb.where((eb) => eb.lit(false))) // favorite_directory_tree will be empty if inFavorites is false + .$if(filters.inFavorites, (qb) => qb.where("is_favorite", "=", true)) + .unionAll((db) => + db + .selectFrom("directory as d") + .innerJoin("favorite_directory_tree as dt", "d.parent_id", "dt.id") + .select("d.id"), + ), + ) + .selectFrom("directory") + .selectAll() + .where("user_id", "=", userId) + .$if(filters.parentId !== "root", (qb) => + qb.where((eb) => + eb.exists(eb.selectFrom("directory_tree as dt").whereRef("dt.id", "=", "parent_id")), + ), + ) + .$if(filters.inFavorites, (qb) => + qb.where((eb) => + eb.exists( + eb.selectFrom("favorite_directory_tree as dt").whereRef("dt.id", "=", "directory.id"), + ), + ), + ) + .execute(); + return directories.map(toDirectory); +}; + export const setDirectoryEncName = async ( userId: number, directoryId: number, diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index af2b6ad..4ad88ab 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -248,6 +248,7 @@ export const searchFiles = async ( userId: number, filters: { parentId: DirectoryId; + inFavorites: boolean; includeCategoryIds: number[]; excludeCategoryIds: number[]; }, @@ -258,7 +259,7 @@ export const searchFiles = async ( .selectFrom("directory") .select("id") .where("user_id", "=", userId) - .where((eb) => eb.val(filters.parentId !== "root")) // directory_tree will be empty if parentId is "root" + .$if(filters.parentId === "root", (qb) => qb.where((eb) => eb.lit(false))) // directory_tree will be empty if parentId is "root" .$if(filters.parentId !== "root", (qb) => qb.where("id", "=", filters.parentId as number)) .unionAll( db @@ -267,6 +268,20 @@ export const searchFiles = async ( .select("d.id"), ), ) + .withRecursive("favorite_directory_tree", (db) => + db + .selectFrom("directory") + .select("id") + .where("user_id", "=", userId) + .$if(!filters.inFavorites, (qb) => qb.where((eb) => eb.lit(false))) // favorite_directory_tree will be empty if inFavorites is false + .$if(filters.inFavorites, (qb) => qb.where("is_favorite", "=", true)) + .unionAll((db) => + db + .selectFrom("directory as d") + .innerJoin("favorite_directory_tree as dt", "d.parent_id", "dt.id") + .select("d.id"), + ), + ) .withRecursive("include_category_tree", (db) => db .selectFrom("category") @@ -295,19 +310,31 @@ export const searchFiles = async ( ) .selectFrom("file") .selectAll("file") - .$if(filters.parentId === "root", (qb) => qb.where("user_id", "=", userId)) // directory_tree isn't used if parentId is "root" + .where("user_id", "=", userId) .$if(filters.parentId !== "root", (qb) => - qb.where("parent_id", "in", (eb) => eb.selectFrom("directory_tree").select("id")), + qb.where((eb) => + eb.exists(eb.selectFrom("directory_tree as dt").whereRef("dt.id", "=", "file.parent_id")), + ), ) - .where((eb) => - eb.not( - eb.exists( - eb - .selectFrom("file_category") - .whereRef("file_id", "=", "file.id") - .where("category_id", "in", (eb) => - eb.selectFrom("exclude_category_tree").select("id"), - ), + .$if(filters.inFavorites, (qb) => + qb.where((eb) => + eb.or([ + eb("is_favorite", "=", true), + eb.exists( + eb.selectFrom("favorite_directory_tree as dt").whereRef("dt.id", "=", "file.parent_id"), + ), + ]), + ), + ) + .$if(filters.excludeCategoryIds.length > 0, (qb) => + qb.where((eb) => + eb.not( + eb.exists( + eb + .selectFrom("file_category") + .innerJoin("exclude_category_tree", "category_id", "exclude_category_tree.id") + .whereRef("file_id", "=", "file.id"), + ), ), ), ); diff --git a/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte b/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte index 5c12fd4..83fff67 100644 --- a/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte +++ b/src/routes/(fullscreen)/file/[id]/TopBarMenu.svelte @@ -46,7 +46,7 @@ {#if isOpen && (directoryId || downloadUrl || fileBlob)}

더보기

@@ -57,7 +57,9 @@ onclick: () => void, )}
diff --git a/src/routes/(fullscreen)/search/service.ts b/src/routes/(fullscreen)/search/service.ts index d9c1e84..9359001 100644 --- a/src/routes/(fullscreen)/search/service.ts +++ b/src/routes/(fullscreen)/search/service.ts @@ -10,6 +10,7 @@ import { trpc } from "$trpc/client"; export interface SearchFilter { ancestorId: DirectoryId; + inFavorites: boolean; categories: { info: LocalCategoryInfo; type: "include" | "exclude" }[]; } @@ -21,6 +22,7 @@ export interface SearchResult { export const requestSearch = async (filter: SearchFilter, masterKey: CryptoKey) => { const { directories: directoriesRaw, files: filesRaw } = await trpc().search.search.query({ ancestor: filter.ancestorId, + inFavorites: filter.inFavorites, includeCategories: filter.categories .filter(({ type }) => type === "include") .map(({ info }) => info.id), diff --git a/src/routes/(main)/category/[[id]]/File.svelte b/src/routes/(main)/category/[[id]]/File.svelte index 90fb659..3909d9e 100644 --- a/src/routes/(main)/category/[[id]]/File.svelte +++ b/src/routes/(main)/category/[[id]]/File.svelte @@ -3,6 +3,7 @@ import { DirectoryEntryLabel } from "$lib/components/molecules"; import { getFileThumbnail } from "$lib/modules/file"; import type { CategoryFileInfo } from "$lib/modules/filesystem"; + import { formatDateTime } from "$lib/utils"; import type { SelectedFile } from "./service.svelte"; import IconClose from "~icons/material-symbols/close"; @@ -19,7 +20,7 @@ onclick(info)} actionButtonIcon={onRemoveClick && IconClose} onActionButtonClick={() => onRemoveClick?.(info)} @@ -28,6 +29,7 @@ type="file" thumbnail={$thumbnail} name={info.name} + subtext={formatDateTime(info.createdAt ?? info.lastModifiedAt)} isFavorite={info.isFavorite} /> diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 98abb79..075bd97 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -103,7 +103,7 @@ - 파일 + 내 파일 diff --git a/src/routes/(main)/favorites/+page.svelte b/src/routes/(main)/favorites/+page.svelte index 5c4c16d..c236c09 100644 --- a/src/routes/(main)/favorites/+page.svelte +++ b/src/routes/(main)/favorites/+page.svelte @@ -44,52 +44,50 @@ 즐겨찾기 - - - -
- {#if isLoading} -
-

- {#if data.favorites.files.length === 0 && data.favorites.directories.length === 0} - 즐겨찾기한 항목이 없어요. - {:else} - 즐겨찾기 목록을 불러오고 있어요. - {/if} -

-
- {:else if entries.length === 0} -
-

즐겨찾기한 항목이 없어요.

-
- {:else} - `${entries[index]!.type}-${entries[index]!.details.id}`} - estimateItemHeight={() => 56} - itemGap={4} +
+ + + +
+ {#if entries.length > 0} + `${entries[index]!.type}-${entries[index]!.details.id}`} + estimateItemHeight={() => 56} + itemGap={4} + > + {#snippet item(index)} + {@const entry = entries[index]!} + {#if entry.type === "directory"} + handleClick(entry)} + onRemoveClick={() => handleRemove(entry)} + /> + {:else} + handleClick(entry)} + onRemoveClick={() => handleRemove(entry)} + /> + {/if} + {/snippet} + + {:else} +
+

+ {#if isLoading} + 즐겨찾기 목록을 불러오고 있어요. + {:else} + 즐겨찾기한 항목이 없어요. + {/if} +

+
+ {/if} +
diff --git a/src/routes/(main)/favorites/File.svelte b/src/routes/(main)/favorites/File.svelte index 54fd07d..3750094 100644 --- a/src/routes/(main)/favorites/File.svelte +++ b/src/routes/(main)/favorites/File.svelte @@ -3,6 +3,7 @@ import { DirectoryEntryLabel } from "$lib/components/molecules"; import { getFileThumbnail } from "$lib/modules/file"; import type { SummarizedFileInfo } from "$lib/modules/filesystem"; + import { formatDateTime } from "$lib/utils"; import IconClose from "~icons/material-symbols/close"; @@ -23,5 +24,11 @@ actionButtonIcon={IconClose} onActionButtonClick={onRemoveClick} > - + diff --git a/src/trpc/routers/search.ts b/src/trpc/routers/search.ts index 07caeb2..e47690a 100644 --- a/src/trpc/routers/search.ts +++ b/src/trpc/routers/search.ts @@ -8,6 +8,7 @@ const searchRouter = router({ .input( z.object({ ancestor: DirectoryIdSchema.default("root"), + inFavorites: z.boolean().default(false), includeCategories: z.number().positive().array().default([]), excludeCategories: z.number().positive().array().default([]), }), @@ -15,10 +16,14 @@ const searchRouter = router({ .query(async ({ ctx, input }) => { const [directories, files] = await Promise.all([ input.includeCategories.length === 0 && input.excludeCategories.length === 0 - ? DirectoryRepo.getAllRecursiveDirectoriesByParent(ctx.session.userId, input.ancestor) + ? DirectoryRepo.searchDirectories(ctx.session.userId, { + parentId: input.ancestor, + inFavorites: input.inFavorites, + }) : [], FileRepo.searchFiles(ctx.session.userId, { parentId: input.ancestor, + inFavorites: input.inFavorites, includeCategoryIds: input.includeCategories, excludeCategoryIds: input.excludeCategories, }),