diff --git a/src/lib/modules/filesystem.ts b/src/lib/modules/filesystem.ts
index 1a1ff0f..54b5c10 100644
--- a/src/lib/modules/filesystem.ts
+++ b/src/lib/modules/filesystem.ts
@@ -12,7 +12,11 @@ import {
type DirectoryId,
} from "$lib/indexedDB";
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
-import type { DirectoryInfoResponse, FileInfoResponse } from "$lib/server/schemas";
+import type {
+ CategoryInfoResponse,
+ DirectoryInfoResponse,
+ FileInfoResponse,
+} from "$lib/server/schemas";
export type DirectoryInfo =
| {
@@ -43,8 +47,27 @@ export interface FileInfo {
lastModifiedAt: Date;
}
+type CategoryId = "root" | number;
+
+export type CategoryInfo =
+ | {
+ id: "root";
+ dataKey?: undefined;
+ dataKeyVersion?: undefined;
+ name?: undefined;
+ subCategoryIds: number[];
+ }
+ | {
+ id: number;
+ dataKey?: CryptoKey;
+ dataKeyVersion?: Date;
+ name: string;
+ subCategoryIds: number[];
+ };
+
const directoryInfoStore = new Map
>();
const fileInfoStore = new Map>();
+const categoryInfoStore = new Map>();
const fetchDirectoryInfoFromIndexedDB = async (
id: DirectoryId,
@@ -124,7 +147,7 @@ export const getDirectoryInfo = (id: DirectoryId, masterKey: CryptoKey) => {
directoryInfoStore.set(id, info);
}
- fetchDirectoryInfo(id, info, masterKey);
+ fetchDirectoryInfo(id, info, masterKey); // Intended
return info;
};
@@ -203,6 +226,58 @@ export const getFileInfo = (fileId: number, masterKey: CryptoKey) => {
fileInfoStore.set(fileId, info);
}
- fetchFileInfo(fileId, info, masterKey);
+ fetchFileInfo(fileId, info, masterKey); // Intended
+ return info;
+};
+
+const fetchCategoryInfoFromServer = async (
+ id: CategoryId,
+ info: Writable,
+ masterKey: CryptoKey,
+) => {
+ const res = await callGetApi(`/api/category/${id}`);
+ if (res.status === 404) {
+ info.set(null);
+ return;
+ } else if (!res.ok) {
+ throw new Error("Failed to fetch category information");
+ }
+
+ const { metadata, subCategories }: CategoryInfoResponse = await res.json();
+
+ if (id === "root") {
+ info.set({ id, subCategoryIds: subCategories });
+ } else {
+ const { dataKey } = await unwrapDataKey(metadata!.dek, masterKey);
+ const name = await decryptString(metadata!.name, metadata!.nameIv, dataKey);
+
+ info.set({
+ id,
+ dataKey,
+ dataKeyVersion: new Date(metadata!.dekVersion),
+ name,
+ subCategoryIds: subCategories,
+ });
+ }
+};
+
+const fetchCategoryInfo = async (
+ id: CategoryId,
+ info: Writable,
+ masterKey: CryptoKey,
+) => {
+ await fetchCategoryInfoFromServer(id, info, masterKey);
+};
+
+export const getCategoryInfo = (categoryId: CategoryId, masterKey: CryptoKey) => {
+ // TODO: MEK rotation
+
+ let info = categoryInfoStore.get(categoryId);
+ if (!info) {
+ info = writable(null);
+ categoryInfoStore.set(categoryId, info);
+ }
+
+ fetchCategoryInfo(categoryId, info, masterKey); // Intended
return info;
};
diff --git a/src/routes/(main)/category/+page.svelte b/src/routes/(main)/category/+page.svelte
deleted file mode 100644
index 73d68b7..0000000
--- a/src/routes/(main)/category/+page.svelte
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/src/routes/(main)/category/[[id]]/+page.svelte b/src/routes/(main)/category/[[id]]/+page.svelte
new file mode 100644
index 0000000..91e027b
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/+page.svelte
@@ -0,0 +1,61 @@
+
+
+
+ 카테고리
+
+
+
+ {#if data.id !== "root"}
+
+ {/if}
+ {#if $info}
+
+
+ {#if data.id !== "root"}
+
하위 카테고리
+ {/if}
+ {#key $info}
+
goto(`/category/${id}`)}
+ onCategoryCreateClick={() => {
+ isCreateCategoryModalOpen = true;
+ }}
+ />
+ {/key}
+
+ {#if data.id !== "root"}
+
+ {/if}
+
+ {/if}
+
+
+
diff --git a/src/routes/(main)/category/[[id]]/+page.ts b/src/routes/(main)/category/[[id]]/+page.ts
new file mode 100644
index 0000000..cfa37f8
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/+page.ts
@@ -0,0 +1,17 @@
+import { error } from "@sveltejs/kit";
+import { z } from "zod";
+import type { PageLoad } from "./$types";
+
+export const load: PageLoad = async ({ params }) => {
+ const zodRes = z
+ .object({
+ id: z.coerce.number().int().positive().optional(),
+ })
+ .safeParse(params);
+ if (!zodRes.success) error(404, "Not found");
+ const { id } = zodRes.data;
+
+ return {
+ id: id ? id : ("root" as const),
+ };
+};
diff --git a/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte b/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte
new file mode 100644
index 0000000..37f868a
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/CreateCategoryModal.svelte
@@ -0,0 +1,30 @@
+
+
+
+ 새 카테고리
+
+
+
+
+
+
+
+
diff --git a/src/routes/(main)/category/[[id]]/SubCategories.svelte b/src/routes/(main)/category/[[id]]/SubCategories.svelte
new file mode 100644
index 0000000..1f47fc8
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/SubCategories.svelte
@@ -0,0 +1,41 @@
+
+
+
+ {#each subCategories as subCategory}
+
+ {/each}
+
+
+
+
diff --git a/src/routes/(main)/category/[[id]]/SubCategory.svelte b/src/routes/(main)/category/[[id]]/SubCategory.svelte
new file mode 100644
index 0000000..212b591
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/SubCategory.svelte
@@ -0,0 +1,61 @@
+
+
+{#if $info}
+
+
+
+{/if}
+
+
diff --git a/src/routes/(main)/category/[[id]]/service.ts b/src/routes/(main)/category/[[id]]/service.ts
new file mode 100644
index 0000000..a5d354a
--- /dev/null
+++ b/src/routes/(main)/category/[[id]]/service.ts
@@ -0,0 +1,28 @@
+import { callPostApi } from "$lib/hooks";
+import { generateDataKey, wrapDataKey, encryptString } from "$lib/modules/crypto";
+import type { CategoryCreateRequest } from "$lib/server/schemas";
+import type { MasterKey } from "$lib/stores";
+
+export interface SelectedSubCategory {
+ id: number;
+ dataKey: CryptoKey;
+ dataKeyVersion: Date;
+ name: string;
+}
+
+export const requestCategoryCreation = async (
+ name: string,
+ parentId: "root" | number,
+ masterKey: MasterKey,
+) => {
+ const { dataKey, dataKeyVersion } = await generateDataKey();
+ const nameEncrypted = await encryptString(name, dataKey);
+ await callPostApi("/api/category/create", {
+ parent: parentId,
+ mekVersion: masterKey.version,
+ dek: await wrapDataKey(dataKey, masterKey.key),
+ dekVersion: dataKeyVersion.toISOString(),
+ name: nameEncrypted.ciphertext,
+ nameIv: nameEncrypted.iv,
+ });
+};