파일 페이지에 카테고리 목록 및 카테고리에 추가 버튼 구현

This commit is contained in:
static
2025-01-22 13:50:36 +09:00
parent a2402f37a0
commit 4c0d668cc1
7 changed files with 80 additions and 28 deletions

View File

@@ -46,6 +46,7 @@ export interface FileInfo {
name: string; name: string;
createdAt?: Date; createdAt?: Date;
lastModifiedAt: Date; lastModifiedAt: Date;
categoryIds: number[];
} }
type CategoryId = "root" | number; type CategoryId = "root" | number;
@@ -160,7 +161,7 @@ const fetchFileInfoFromIndexedDB = async (id: number, info: Writable<FileInfo |
const file = await getFileInfoFromIndexedDB(id); const file = await getFileInfoFromIndexedDB(id);
if (!file) return; if (!file) return;
info.set(file); info.set({ ...file, categoryIds: [] });
}; };
const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => { const decryptDate = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
@@ -204,6 +205,7 @@ const fetchFileInfoFromServer = async (
name, name,
createdAt, createdAt,
lastModifiedAt, lastModifiedAt,
categoryIds: metadata.categories,
}); });
await storeFileInfo({ await storeFileInfo({
id, id,

View File

@@ -12,8 +12,10 @@
let { categories, onCategoryClick }: Props = $props(); let { categories, onCategoryClick }: Props = $props();
</script> </script>
<div class="space-y-1"> {#if categories.length > 0}
<div class="space-y-1">
{#each categories as category} {#each categories as category}
<Category info={category} onclick={onCategoryClick} /> <Category info={category} onclick={onCategoryClick} />
{/each} {/each}
</div> </div>
{/if}

View File

@@ -441,6 +441,15 @@ export const addFileToCategory = async (fileId: number, categoryId: number) => {
}); });
}; };
export const getAllFileCategories = async (fileId: number) => {
const categories = await db
.selectFrom("file_category")
.select("category_id")
.where("file_id", "=", fileId)
.execute();
return categories.map(({ category_id }) => ({ id: category_id }));
};
export const removeFileFromCategory = async (fileId: number, categoryId: number) => { export const removeFileFromCategory = async (fileId: number, categoryId: number) => {
await db.transaction().execute(async (trx) => { await db.transaction().execute(async (trx) => {
const res = await trx const res = await trx

View File

@@ -18,6 +18,7 @@ export const fileInfoResponse = z.object({
createdAtIv: z.string().base64().nonempty().optional(), createdAtIv: z.string().base64().nonempty().optional(),
lastModifiedAt: z.string().base64().nonempty(), lastModifiedAt: z.string().base64().nonempty(),
lastModifiedAtIv: z.string().base64().nonempty(), lastModifiedAtIv: z.string().base64().nonempty(),
categories: z.number().int().positive().array(),
}); });
export type FileInfoResponse = z.infer<typeof fileInfoResponse>; export type FileInfoResponse = z.infer<typeof fileInfoResponse>;

View File

@@ -13,6 +13,7 @@ import {
getFile, getFile,
setFileEncName, setFileEncName,
unregisterFile, unregisterFile,
getAllFileCategories,
type NewFile, type NewFile,
} from "$lib/server/db/file"; } from "$lib/server/db/file";
import type { Ciphertext } from "$lib/server/db/schema"; import type { Ciphertext } from "$lib/server/db/schema";
@@ -24,6 +25,7 @@ export const getFileInformation = async (userId: number, fileId: number) => {
error(404, "Invalid file id"); error(404, "Invalid file id");
} }
const categories = await getAllFileCategories(fileId);
return { return {
parentId: file.parentId ?? ("root" as const), parentId: file.parentId ?? ("root" as const),
mekVersion: file.mekVersion, mekVersion: file.mekVersion,
@@ -34,6 +36,7 @@ export const getFileInformation = async (userId: number, fileId: number) => {
encName: file.encName, encName: file.encName,
encCreatedAt: file.encCreatedAt, encCreatedAt: file.encCreatedAt,
encLastModifiedAt: file.encLastModifiedAt, encLastModifiedAt: file.encLastModifiedAt,
categories: categories.map(({ id }) => id),
}; };
}; };

View File

@@ -2,18 +2,29 @@
import FileSaver from "file-saver"; import FileSaver from "file-saver";
import { untrack } from "svelte"; import { untrack } from "svelte";
import { get, type Writable } from "svelte/store"; import { get, type Writable } from "svelte/store";
import { goto } from "$app/navigation";
import { TopBar } from "$lib/components"; import { TopBar } from "$lib/components";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; import { EntryButton } from "$lib/components/buttons";
import {
getFileInfo,
getCategoryInfo,
type FileInfo,
type CategoryInfo,
} from "$lib/modules/filesystem";
import Categories from "$lib/molecules/Categories";
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores"; import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte"; import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
import DownloadStatus from "./DownloadStatus.svelte"; import DownloadStatus from "./DownloadStatus.svelte";
import { requestFileDownload, requestFileAdditionToCategory } from "./service"; import { requestFileDownload, requestFileAdditionToCategory } from "./service";
import IconAddCircle from "~icons/material-symbols/add-circle";
let { data } = $props(); let { data } = $props();
let info: Writable<FileInfo | null> | undefined = $state(); let info: Writable<FileInfo | null> | undefined = $state();
let categories: Writable<CategoryInfo | null>[] = $state([]);
let isAddToCategoryBottomSheetOpen = $state(true); let isAddToCategoryBottomSheetOpen = $state(false);
const downloadStatus = $derived( const downloadStatus = $derived(
$fileDownloadStatusStore.find((statusStore) => { $fileDownloadStatusStore.find((statusStore) => {
@@ -50,6 +61,7 @@
const addToCategory = async (categoryId: number) => { const addToCategory = async (categoryId: number) => {
await requestFileAdditionToCategory(data.id, categoryId); await requestFileAdditionToCategory(data.id, categoryId);
isAddToCategoryBottomSheetOpen = false; isAddToCategoryBottomSheetOpen = false;
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
}; };
$effect(() => { $effect(() => {
@@ -58,6 +70,13 @@
viewerType = undefined; viewerType = undefined;
}); });
$effect(() => {
categories =
$info?.categoryIds.map((id) => getCategoryInfo(id, $masterKeyStore?.get(1)?.key!)) ?? [];
// TODO: Sorting
});
$effect(() => { $effect(() => {
if ($info && $info.dataKey && $info.contentIv) { if ($info && $info.dataKey && $info.contentIv) {
untrack(() => { untrack(() => {
@@ -89,15 +108,15 @@
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<TopBar title={$info?.name} /> <TopBar title={$info?.name} />
<div class="space-y-4 pb-4">
<DownloadStatus status={downloadStatus} /> <DownloadStatus status={downloadStatus} />
<div class="flex w-full flex-grow flex-col items-center pb-4"> {#if $info && viewerType}
<div class="flex w-full justify-center">
{#snippet viewerLoading(message: string)} {#snippet viewerLoading(message: string)}
<div class="flex flex-grow items-center justify-center">
<p class="text-gray-500">{message}</p> <p class="text-gray-500">{message}</p>
</div>
{/snippet} {/snippet}
{#if $info && viewerType === "image"} {#if viewerType === "image"}
{#if fileBlobUrl} {#if fileBlobUrl}
<img src={fileBlobUrl} alt={$info.name} /> <img src={fileBlobUrl} alt={$info.name} />
{:else} {:else}
@@ -112,6 +131,20 @@
{/if} {/if}
{/if} {/if}
</div> </div>
{/if}
<div class="space-y-2">
<p class="text-lg font-bold">카테고리</p>
<div class="space-y-1">
<Categories {categories} onCategoryClick={({ id }) => goto(`/category/${id}`)} />
<EntryButton onclick={() => (isAddToCategoryBottomSheetOpen = true)}>
<div class="flex h-8 items-center gap-x-4">
<IconAddCircle class="text-lg text-gray-600" />
<p class="font-medium text-gray-700">카테고리에 추가하기</p>
</div>
</EntryButton>
</div>
</div>
</div>
</div> </div>
<AddToCategoryBottomSheet <AddToCategoryBottomSheet

View File

@@ -26,6 +26,7 @@ export const GET: RequestHandler = async ({ locals, params }) => {
encName, encName,
encCreatedAt, encCreatedAt,
encLastModifiedAt, encLastModifiedAt,
categories,
} = await getFileInformation(userId, id); } = await getFileInformation(userId, id);
return json( return json(
fileInfoResponse.parse({ fileInfoResponse.parse({
@@ -41,6 +42,7 @@ export const GET: RequestHandler = async ({ locals, params }) => {
createdAtIv: encCreatedAt?.iv, createdAtIv: encCreatedAt?.iv,
lastModifiedAt: encLastModifiedAt.ciphertext, lastModifiedAt: encLastModifiedAt.ciphertext,
lastModifiedAtIv: encLastModifiedAt.iv, lastModifiedAtIv: encLastModifiedAt.iv,
categories,
} satisfies FileInfoResponse), } satisfies FileInfoResponse),
); );
}; };