카테고리에 파일을 추가할 수 있는 BottomSheet 구현 (WiP)

This commit is contained in:
static
2025-01-22 13:22:16 +09:00
parent dbe2262d07
commit a2402f37a0
13 changed files with 199 additions and 72 deletions

View File

@@ -42,7 +42,7 @@
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.9",
"svelte": "^5.17.1",
"svelte": "^5.19.1",
"svelte-check": "^4.1.3",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.3",

82
pnpm-lock.yaml generated
View File

@@ -41,13 +41,13 @@ importers:
version: 1.2.12
'@sveltejs/adapter-node':
specifier: ^5.2.11
version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))
version: 5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))
'@sveltejs/kit':
specifier: ^2.15.2
version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
version: 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
'@sveltejs/vite-plugin-svelte':
specifier: ^4.0.4
version: 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
version: 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
'@types/file-saver':
specifier: ^2.0.7
version: 2.0.7
@@ -77,7 +77,7 @@ importers:
version: 9.1.0(eslint@9.17.0(jiti@2.4.2))
eslint-plugin-svelte:
specifier: ^2.46.1
version: 2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.17.1)
version: 2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1)
eslint-plugin-tailwindcss:
specifier: ^3.17.5
version: 3.17.5(tailwindcss@3.4.17)
@@ -107,16 +107,16 @@ importers:
version: 3.4.2
prettier-plugin-svelte:
specifier: ^3.3.2
version: 3.3.2(prettier@3.4.2)(svelte@5.17.1)
version: 3.3.2(prettier@3.4.2)(svelte@5.19.1)
prettier-plugin-tailwindcss:
specifier: ^0.6.9
version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2)
version: 0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2)
svelte:
specifier: ^5.17.1
version: 5.17.1
specifier: ^5.19.1
version: 5.19.1
svelte-check:
specifier: ^4.1.3
version: 4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3)
version: 4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3)
tailwindcss:
specifier: ^3.4.17
version: 3.4.17
@@ -128,7 +128,7 @@ importers:
version: 8.19.1(eslint@9.17.0(jiti@2.4.2))(typescript@5.7.3)
unplugin-icons:
specifier: ^0.22.0
version: 0.22.0(svelte@5.17.1)
version: 0.22.0(svelte@5.19.1)
vite:
specifier: ^5.4.11
version: 5.4.11(@types/node@22.10.5)
@@ -1114,8 +1114,8 @@ packages:
resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
engines: {node: '>=0.10'}
esrap@1.3.2:
resolution: {integrity: sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==}
esrap@1.4.3:
resolution: {integrity: sha512-Xddc1RsoFJ4z9nR7W7BFaEPIp4UXoeQ0+077UdWLxbafMQFyU79sQJMk7kxNgRwQ9/aVgaKacCHC2pUACGwmYw==}
esrecurse@4.3.0:
resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
@@ -1992,8 +1992,8 @@ packages:
svelte:
optional: true
svelte@5.17.1:
resolution: {integrity: sha512-HitqD0XhU9OEytPuux/XYzxle4+7D8+fIb1tHbwMzOtBzDZZO+ESEuwMbahJ/3JoklfmRPB/Gzp74L87Qrxfpw==}
svelte@5.19.1:
resolution: {integrity: sha512-H/Vs2O51bwILZbaNUSdr4P1NbLpOGsxl4jJAjd88ELjzRgeRi1BHqexcVGannDr7D1pmTYWWajzHOM7bMbtB9Q==}
engines: {node: '>=18'}
tailwindcss@3.4.17:
@@ -2573,17 +2573,17 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.30.1':
optional: true
'@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))':
'@sveltejs/adapter-node@5.2.11(@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))':
dependencies:
'@rollup/plugin-commonjs': 28.0.2(rollup@4.30.1)
'@rollup/plugin-json': 6.1.0(rollup@4.30.1)
'@rollup/plugin-node-resolve': 16.0.0(rollup@4.30.1)
'@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
'@sveltejs/kit': 2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
rollup: 4.30.1
'@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))':
'@sveltejs/kit@2.15.2(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))':
dependencies:
'@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
'@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
'@types/cookie': 0.6.0
cookie: 0.6.0
devalue: 5.1.1
@@ -2595,27 +2595,27 @@ snapshots:
sade: 1.8.1
set-cookie-parser: 2.7.1
sirv: 3.0.0
svelte: 5.17.1
svelte: 5.19.1
tiny-glob: 0.2.9
vite: 5.4.11(@types/node@22.10.5)
'@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))':
'@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))':
dependencies:
'@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
'@sveltejs/vite-plugin-svelte': 4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
debug: 4.4.0
svelte: 5.17.1
svelte: 5.19.1
vite: 5.4.11(@types/node@22.10.5)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))':
'@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.17.1)(vite@5.4.11(@types/node@22.10.5))
'@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.4(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5)))(svelte@5.19.1)(vite@5.4.11(@types/node@22.10.5))
debug: 4.4.0
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.17
svelte: 5.17.1
svelte: 5.19.1
vite: 5.4.11(@types/node@22.10.5)
vitefu: 1.0.5(vite@5.4.11(@types/node@22.10.5))
transitivePeerDependencies:
@@ -3001,7 +3001,7 @@ snapshots:
dependencies:
eslint: 9.17.0(jiti@2.4.2)
eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.17.1):
eslint-plugin-svelte@2.46.1(eslint@9.17.0(jiti@2.4.2))(svelte@5.19.1):
dependencies:
'@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0(jiti@2.4.2))
'@jridgewell/sourcemap-codec': 1.5.0
@@ -3014,9 +3014,9 @@ snapshots:
postcss-safe-parser: 6.0.0(postcss@8.4.49)
postcss-selector-parser: 6.1.2
semver: 7.6.3
svelte-eslint-parser: 0.43.0(svelte@5.17.1)
svelte-eslint-parser: 0.43.0(svelte@5.19.1)
optionalDependencies:
svelte: 5.17.1
svelte: 5.19.1
transitivePeerDependencies:
- ts-node
@@ -3099,7 +3099,7 @@ snapshots:
dependencies:
estraverse: 5.3.0
esrap@1.3.2:
esrap@1.4.3:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
@@ -3683,16 +3683,16 @@ snapshots:
prelude-ls@1.2.1: {}
prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1):
prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1):
dependencies:
prettier: 3.4.2
svelte: 5.17.1
svelte: 5.19.1
prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.17.1))(prettier@3.4.2):
prettier-plugin-tailwindcss@0.6.9(prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.19.1))(prettier@3.4.2):
dependencies:
prettier: 3.4.2
optionalDependencies:
prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.17.1)
prettier-plugin-svelte: 3.3.2(prettier@3.4.2)(svelte@5.19.1)
prettier@3.4.2: {}
@@ -3828,19 +3828,19 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.17.1)(typescript@5.7.3):
svelte-check@4.1.3(picomatch@4.0.2)(svelte@5.19.1)(typescript@5.7.3):
dependencies:
'@jridgewell/trace-mapping': 0.3.25
chokidar: 4.0.3
fdir: 6.4.2(picomatch@4.0.2)
picocolors: 1.1.1
sade: 1.8.1
svelte: 5.17.1
svelte: 5.19.1
typescript: 5.7.3
transitivePeerDependencies:
- picomatch
svelte-eslint-parser@0.43.0(svelte@5.17.1):
svelte-eslint-parser@0.43.0(svelte@5.19.1):
dependencies:
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
@@ -3848,9 +3848,9 @@ snapshots:
postcss: 8.4.49
postcss-scss: 4.0.9(postcss@8.4.49)
optionalDependencies:
svelte: 5.17.1
svelte: 5.19.1
svelte@5.17.1:
svelte@5.19.1:
dependencies:
'@ampproject/remapping': 2.3.0
'@jridgewell/sourcemap-codec': 1.5.0
@@ -3861,7 +3861,7 @@ snapshots:
axobject-query: 4.1.0
clsx: 2.1.1
esm-env: 1.2.2
esrap: 1.3.2
esrap: 1.4.3
is-reference: 3.0.3
locate-character: 3.0.0
magic-string: 0.30.17
@@ -3957,7 +3957,7 @@ snapshots:
undici-types@6.20.0: {}
unplugin-icons@0.22.0(svelte@5.17.1):
unplugin-icons@0.22.0(svelte@5.19.1):
dependencies:
'@antfu/install-pkg': 0.5.0
'@antfu/utils': 0.7.10
@@ -3967,7 +3967,7 @@ snapshots:
local-pkg: 0.5.1
unplugin: 2.1.2
optionalDependencies:
svelte: 5.17.1
svelte: 5.19.1
transitivePeerDependencies:
- supports-color

View File

@@ -28,7 +28,7 @@
<AdaptiveDiv>
<div
onclick={(e) => e.stopPropagation()}
class="flex max-h-[70vh] min-h-[30vh] rounded-t-2xl bg-white px-4"
class="flex max-h-[70vh] min-h-[30vh] overflow-y-auto rounded-t-2xl bg-white px-4"
transition:fly={{ y: 100, duration: 200 }}
>
{@render children?.()}

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import type { CategoryInfo } from "$lib/modules/filesystem";
import Category from "./Category.svelte";
import type { SelectedCategory } from "./service";
interface Props {
categories: Writable<CategoryInfo | null>[];
onCategoryClick: (category: SelectedCategory) => void;
}
let { categories, onCategoryClick }: Props = $props();
</script>
<div class="space-y-1">
{#each categories as category}
<Category info={category} onclick={onCategoryClick} />
{/each}
</div>

View File

@@ -1,14 +1,14 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import type { CategoryInfo } from "$lib/modules/filesystem";
import type { SelectedSubCategory } from "./service";
import type { SelectedCategory } from "./service";
import IconCategory from "~icons/material-symbols/category";
import IconMoreVert from "~icons/material-symbols/more-vert";
interface Props {
info: Writable<CategoryInfo | null>;
onclick: (selectedCategory: SelectedSubCategory) => void;
onclick: (category: SelectedCategory) => void;
}
let { info, onclick }: Props = $props();

View File

@@ -0,0 +1,2 @@
export { default } from "./Categories.svelte";
export * from "./service";

View File

@@ -0,0 +1,6 @@
export interface SelectedCategory {
id: number;
dataKey: CryptoKey;
dataKeyVersion: Date;
name: string;
}

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import type { ClassValue } from "svelte/elements";
import type { Writable } from "svelte/store";
import { EntryButton } from "$lib/components/buttons";
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
import Categories, { type SelectedCategory } from "$lib/molecules/Categories";
import { masterKeyStore } from "$lib/stores";
import IconAddCircle from "~icons/material-symbols/add-circle";
interface Props {
class?: ClassValue;
info: CategoryInfo;
onSubCategoryClick: (subCategory: SelectedCategory) => void;
onSubCategoryCreateClick: () => void;
subCategoryCreatePosition?: "top" | "bottom";
}
let {
info,
onSubCategoryClick,
onSubCategoryCreateClick,
subCategoryCreatePosition = "bottom",
...props
}: Props = $props();
let subCategories: Writable<CategoryInfo | null>[] = $state([]);
$effect(() => {
subCategories = info.subCategoryIds.map((id) =>
getCategoryInfo(id, $masterKeyStore?.get(1)?.key!),
);
// TODO: Sorting
});
</script>
<div class={["space-y-1", props.class]}>
{#snippet subCategoryCreate()}
<EntryButton onclick={onSubCategoryCreateClick}>
<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>
{/snippet}
{#if subCategoryCreatePosition === "top"}
{@render subCategoryCreate()}
{/if}
{#key info}
<Categories categories={subCategories} onCategoryClick={onSubCategoryClick} />
{/key}
{#if subCategoryCreatePosition === "bottom"}
{@render subCategoryCreate()}
{/if}
</div>

View File

@@ -1,23 +1,21 @@
<script lang="ts">
import type { Writable } from "svelte/store";
import { EntryButton } from "$lib/components/buttons";
import {
getFileInfo,
getCategoryInfo,
type FileInfo,
type CategoryInfo,
} from "$lib/modules/filesystem";
import type { SelectedCategory } from "$lib/molecules/Categories";
import SubCategories from "$lib/molecules/SubCategories.svelte";
import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte";
import SubCategory from "./SubCategory.svelte";
import type { SelectedSubCategory, SelectedFile } from "./service";
import IconAddCircle from "~icons/material-symbols/add-circle";
import type { SelectedFile } from "./service";
interface Props {
info: CategoryInfo;
onFileClick: (file: SelectedFile) => void;
onSubCategoryClick: (subCategory: SelectedSubCategory) => void;
onSubCategoryClick: (subCategory: SelectedCategory) => void;
onSubCategoryCreateClick: () => void;
}
@@ -41,19 +39,7 @@
{#if info.id !== "root"}
<p class="text-lg font-bold text-gray-800">하위 카테고리</p>
{/if}
<div class="space-y-1">
{#key info}
{#each subCategories as subCategory}
<SubCategory info={subCategory} onclick={onSubCategoryClick} />
{/each}
{/key}
<EntryButton onclick={onSubCategoryCreateClick}>
<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>
<SubCategories {info} {onSubCategoryClick} {onSubCategoryCreateClick} />
</div>
{#if info.id !== "root"}
<div class="space-y-4 bg-white p-4">

View File

@@ -1,10 +1,3 @@
export interface SelectedSubCategory {
id: number;
dataKey: CryptoKey;
dataKeyVersion: Date;
name: string;
}
export interface SelectedFile {
id: number;
dataKey: CryptoKey;

View File

@@ -5,13 +5,16 @@
import { TopBar } from "$lib/components";
import { getFileInfo, type FileInfo } from "$lib/modules/filesystem";
import { fileDownloadStatusStore, isFileDownloading, masterKeyStore } from "$lib/stores";
import AddToCategoryBottomSheet from "./AddToCategoryBottomSheet.svelte";
import DownloadStatus from "./DownloadStatus.svelte";
import { requestFileDownload } from "./service";
import { requestFileDownload, requestFileAdditionToCategory } from "./service";
let { data } = $props();
let info: Writable<FileInfo | null> | undefined = $state();
let isAddToCategoryBottomSheetOpen = $state(true);
const downloadStatus = $derived(
$fileDownloadStatusStore.find((statusStore) => {
const { id, status } = get(statusStore);
@@ -44,6 +47,11 @@
return fileBlob;
};
const addToCategory = async (categoryId: number) => {
await requestFileAdditionToCategory(data.id, categoryId);
isAddToCategoryBottomSheetOpen = false;
};
$effect(() => {
info = getFileInfo(data.id, $masterKeyStore?.get(1)?.key!);
isDownloadRequested = false;
@@ -105,3 +113,8 @@
{/if}
</div>
</div>
<AddToCategoryBottomSheet
bind:isOpen={isAddToCategoryBottomSheetOpen}
onAddToCategoryClick={addToCategory}
/>

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import { onMount } from "svelte";
import type { Writable } from "svelte/store";
import { BottomSheet } from "$lib/components";
import { Button } from "$lib/components/buttons";
import { BottomDiv } from "$lib/components/divs";
import { getCategoryInfo, type CategoryInfo } from "$lib/modules/filesystem";
import SubCategories from "$lib/molecules/SubCategories.svelte";
import { masterKeyStore } from "$lib/stores";
interface Props {
onAddToCategoryClick: (categoryId: number) => void;
isOpen: boolean;
}
let { onAddToCategoryClick, isOpen = $bindable() }: Props = $props();
let category: Writable<CategoryInfo | null> | undefined = $state();
onMount(() => {
category = getCategoryInfo("root", $masterKeyStore?.get(1)?.key!);
});
</script>
<BottomSheet bind:isOpen>
<div class="flex w-full flex-col justify-between">
{#if $category}
<SubCategories
class="h-fit py-4"
info={$category}
onSubCategoryClick={({ id }) =>
(category = getCategoryInfo(id, $masterKeyStore?.get(1)?.key!))}
subCategoryCreatePosition="top"
/>
{#if $category.id !== "root"}
<BottomDiv>
<Button onclick={() => onAddToCategoryClick($category.id)}> 카테고리에 추가하기</Button>
</BottomDiv>
{/if}
{/if}
</div>
</BottomSheet>

View File

@@ -1,4 +1,6 @@
import { callPostApi } from "$lib/hooks";
import { getFileCache, storeFileCache, downloadFile } from "$lib/modules/file";
import type { CategoryFileAddRequest } from "$lib/server/schemas";
export const requestFileDownload = async (
fileId: number,
@@ -12,3 +14,10 @@ export const requestFileDownload = async (
storeFileCache(fileId, fileBuffer); // Intended
return fileBuffer;
};
export const requestFileAdditionToCategory = async (fileId: number, categoryId: number) => {
const res = await callPostApi<CategoryFileAddRequest>(`/api/category/${categoryId}/file/add`, {
file: fileId,
});
return res.ok;
};