2 Commits

Author SHA1 Message Date
static
d1f9018213 사소한 리팩토링 2026-01-02 00:31:58 +09:00
static
2e3cd4f8a2 네트워크 호출 결과가 IndexedDB에 캐시되지 않던 버그 수정 2026-01-01 23:52:47 +09:00
9 changed files with 214 additions and 96 deletions

View File

@@ -8,10 +8,11 @@
count: number;
item: Snippet<[index: number]>;
itemHeight: (index: number) => number;
itemGap?: number;
placeholder?: Snippet;
}
let { class: className, count, item, itemHeight, placeholder }: Props = $props();
let { class: className, count, item, itemHeight, itemGap, placeholder }: Props = $props();
let element: HTMLElement | undefined = $state();
let scrollMargin = $state(0);
@@ -20,6 +21,7 @@
createWindowVirtualizer({
count,
estimateSize: itemHeight,
gap: itemGap,
scrollMargin,
}),
);

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { CheckBox, RowVirtualizer } from "$lib/components/atoms";
import { SubCategories, type SelectedCategory } from "$lib/components/molecules";
import { updateCategoryInfo } from "$lib/indexedDB";
import type { CategoryInfo } from "$lib/modules/filesystem";
import { sortEntries } from "$lib/utils";
import File from "./File.svelte";
@@ -28,6 +29,9 @@
isFileRecursive = $bindable(),
}: Props = $props();
let lastCategoryId = $state<CategoryInfo["id"] | undefined>();
let lastIsFileRecursive = $state<boolean | undefined>();
let files = $derived(
sortEntries(
info.files
@@ -35,6 +39,19 @@
.filter(({ details }) => isFileRecursive || !details.isRecursive) ?? [],
),
);
$effect(() => {
if (info.id === "root" || isFileRecursive === undefined) return;
if (lastCategoryId !== info.id) {
lastCategoryId = info.id;
lastIsFileRecursive = isFileRecursive;
return;
}
if (lastIsFileRecursive === isFileRecursive) return;
lastIsFileRecursive = isFileRecursive;
void updateCategoryInfo(info.id, { isFileRecursive });
});
</script>
<div class="space-y-4">
@@ -58,19 +75,14 @@
<p class="font-medium">하위 카테고리의 파일</p>
</CheckBox>
</div>
<RowVirtualizer
count={files.length}
itemHeight={(index) => 48 + (index + 1 < files.length ? 4 : 0)}
>
<RowVirtualizer count={files.length} itemHeight={() => 48} itemGap={4}>
{#snippet item(index)}
{@const { details } = files[index]!}
<div class={[index + 1 < files.length && "pb-1"]}>
<File
info={details}
onclick={onFileClick}
onRemoveClick={!details.isRecursive ? onFileRemoveClick : undefined}
/>
</div>
{/snippet}
{#snippet placeholder()}
<p class="text-center text-gray-500">이 카테고리에 추가된 파일이 없어요.</p>

View File

@@ -46,7 +46,7 @@ const isFileUploading = (status: FileUploadState["status"]) =>
export const getUploadingFiles = (parentId?: DirectoryId) => {
return uploadingFiles.filter(
(file): file is LiveFileUploadState =>
(file) =>
(parentId === undefined || file.parentId === parentId) && isFileUploading(file.status),
);
};

View File

@@ -52,25 +52,76 @@ const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
metadata,
subCategories: subCategoriesRaw,
files: filesRaw,
} = await trpc().category.get.query({ id });
const [subCategories, files] = await Promise.all([
Promise.all(
subCategoriesRaw.map(async (category) => ({
} = await trpc().category.get.query({ id, recurse: true });
const subCategories = await Promise.all(
subCategoriesRaw.map(async (category) => {
const decrypted = await decryptCategoryMetadata(category, masterKey);
const existing = await IndexedDB.getCategoryInfo(category.id);
await IndexedDB.storeCategoryInfo({
id: category.id,
...(await decryptCategoryMetadata(category, masterKey)),
})),
),
filesRaw
? Promise.all(
filesRaw.map(async (file) => ({
parentId: id,
name: decrypted.name,
files: existing?.files ?? [],
isFileRecursive: existing?.isFileRecursive ?? false,
});
return {
id: category.id,
...decrypted,
};
}),
);
const existingFiles = filesRaw
? await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id))
: [];
const files = filesRaw
? await Promise.all(
filesRaw.map(async (file, index) => {
const decrypted = await decryptFileMetadata(file, masterKey);
const existing = existingFiles[index];
if (existing) {
const categoryIds = file.isRecursive
? existing.categoryIds
: Array.from(new Set([...existing.categoryIds, id as number]));
await IndexedDB.storeFileInfo({
id: file.id,
parentId: existing.parentId,
contentType: file.contentType,
name: decrypted.name,
createdAt: decrypted.createdAt,
lastModifiedAt: decrypted.lastModifiedAt,
categoryIds,
});
}
return {
id: file.id,
contentType: file.contentType,
isRecursive: file.isRecursive,
...(await decryptFileMetadata(file, masterKey)),
})),
...decrypted,
};
}),
)
: undefined,
]);
: undefined;
const decryptedMetadata = metadata
? await decryptCategoryMetadata(metadata, masterKey)
: undefined;
if (id !== "root" && metadata && decryptedMetadata) {
const existingCategory = await IndexedDB.getCategoryInfo(id);
await IndexedDB.storeCategoryInfo({
id: id as number,
parentId: metadata.parent,
name: decryptedMetadata.name,
files:
files?.map((file) => ({
id: file.id,
isRecursive: file.isRecursive,
})) ??
existingCategory?.files ??
[],
isFileRecursive: existingCategory?.isFileRecursive ?? false,
});
}
if (id === "root") {
return {
@@ -84,7 +135,7 @@ const fetchFromServer = async (id: CategoryId, masterKey: CryptoKey) => {
exists: true as const,
subCategories,
files,
...(await decryptCategoryMetadata(metadata!, masterKey)),
...decryptedMetadata!,
};
}
} catch (e) {

View File

@@ -39,22 +39,52 @@ const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
subDirectories: subDirectoriesRaw,
files: filesRaw,
} = await trpc().directory.get.query({ id });
const [subDirectories, files] = await Promise.all([
const existingFiles = await IndexedDB.bulkGetFileInfos(filesRaw.map((file) => file.id));
const [subDirectories, files, decryptedMetadata] = await Promise.all([
Promise.all(
subDirectoriesRaw.map(async (directory) => ({
subDirectoriesRaw.map(async (directory) => {
const decrypted = await decryptDirectoryMetadata(directory, masterKey);
await IndexedDB.storeDirectoryInfo({
id: directory.id,
...(await decryptDirectoryMetadata(directory, masterKey)),
})),
parentId: id,
name: decrypted.name,
});
return {
id: directory.id,
...decrypted,
};
}),
),
Promise.all(
filesRaw.map(async (file) => ({
filesRaw.map(async (file, index) => {
const decrypted = await decryptFileMetadata(file, masterKey);
await IndexedDB.storeFileInfo({
id: file.id,
parentId: id,
contentType: file.contentType,
name: decrypted.name,
createdAt: decrypted.createdAt,
lastModifiedAt: decrypted.lastModifiedAt,
categoryIds: existingFiles[index]?.categoryIds ?? [],
});
return {
id: file.id,
contentType: file.contentType,
...(await decryptFileMetadata(file, masterKey)),
})),
...decrypted,
};
}),
),
metadata ? decryptDirectoryMetadata(metadata, masterKey) : undefined,
]);
if (id !== "root" && metadata && decryptedMetadata) {
await IndexedDB.storeDirectoryInfo({
id,
parentId: metadata.parent,
name: decryptedMetadata.name,
});
}
if (id === "root") {
return {
id,
@@ -69,7 +99,7 @@ const fetchFromServer = async (id: DirectoryId, masterKey: CryptoKey) => {
parentId: metadata!.parent,
subDirectories,
files,
...(await decryptDirectoryMetadata(metadata!, masterKey)),
...decryptedMetadata!,
};
}
} catch (e) {

View File

@@ -66,15 +66,26 @@ const bulkFetchFromIndexedDB = async (ids: number[]) => {
const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
try {
const { categories: categoriesRaw, ...metadata } = await trpc().file.get.query({ id });
const [categories] = await Promise.all([
const [categories, decryptedMetadata] = await Promise.all([
Promise.all(
categoriesRaw.map(async (category) => ({
id: category.id,
...(await decryptCategoryMetadata(category, masterKey)),
})),
),
decryptFileMetadata(metadata, masterKey),
]);
await IndexedDB.storeFileInfo({
id,
parentId: metadata.parent,
contentType: metadata.contentType,
name: decryptedMetadata.name,
createdAt: decryptedMetadata.createdAt,
lastModifiedAt: decryptedMetadata.lastModifiedAt,
categoryIds: categories.map((category) => category.id),
});
return {
id,
exists: true as const,
@@ -82,7 +93,7 @@ const fetchFromServer = async (id: number, masterKey: CryptoKey) => {
contentType: metadata.contentType,
contentIv: metadata.contentIv,
categories,
...(await decryptFileMetadata(metadata, masterKey)),
...decryptedMetadata,
};
} catch (e) {
if (isTRPCClientError(e) && e.data?.code === "NOT_FOUND") {
@@ -97,12 +108,25 @@ const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
const filesRaw = await trpc().file.bulkGet.query({ ids });
const files = await Promise.all(
filesRaw.map(async (file) => {
const categories = await Promise.all(
const [categories, decryptedMetadata] = await Promise.all([
Promise.all(
file.categories.map(async (category) => ({
id: category.id,
...(await decryptCategoryMetadata(category, masterKey)),
})),
);
),
decryptFileMetadata(file, masterKey),
]);
await IndexedDB.storeFileInfo({
id: file.id,
parentId: file.parent,
contentType: file.contentType,
name: decryptedMetadata.name,
createdAt: decryptedMetadata.createdAt,
lastModifiedAt: decryptedMetadata.lastModifiedAt,
categoryIds: categories.map((category) => category.id),
});
return {
id: file.id,
exists: true as const,
@@ -110,7 +134,7 @@ const bulkFetchFromServer = async (ids: number[], masterKey: CryptoKey) => {
contentType: file.contentType,
contentIv: file.contentIv,
categories,
...(await decryptFileMetadata(file, masterKey)),
...decryptedMetadata,
};
}),
);

View File

@@ -1,18 +1,29 @@
<script lang="ts">
import { onMount } from "svelte";
import { FullscreenDiv } from "$lib/components/atoms";
import { TopBar } from "$lib/components/molecules";
import { getDownloadingFiles, clearDownloadedFiles } from "$lib/modules/file";
import { bulkGetFileInfo } from "$lib/modules/filesystem";
import {
getDownloadingFiles,
clearDownloadedFiles,
type FileDownloadState,
} from "$lib/modules/file";
import { bulkGetFileInfo, type MaybeFileInfo } from "$lib/modules/filesystem";
import { masterKeyStore } from "$lib/stores";
import File from "./File.svelte";
const downloadingFiles = getDownloadingFiles();
const filesPromise = $derived(
bulkGetFileInfo(
downloadingFiles.map(({ id }) => id),
let downloadingFiles: { info: MaybeFileInfo; state: FileDownloadState }[] = $state([]);
onMount(async () => {
const states = getDownloadingFiles();
const infos = await bulkGetFileInfo(
states.map(({ id }) => id),
$masterKeyStore?.get(1)?.key!,
),
);
downloadingFiles = states.map((state) => ({
info: infos.get(state.id)!,
state,
}));
});
$effect(() => clearDownloadedFiles);
</script>
@@ -23,14 +34,11 @@
<TopBar />
<FullscreenDiv>
{#await filesPromise then files}
<div class="space-y-2 pb-4">
{#each downloadingFiles as state}
{@const info = files.get(state.id)!}
{#each downloadingFiles as { info, state } (info.id)}
{#if info.exists}
<File {state} {info} />
<File {info} {state} />
{/if}
{/each}
</div>
{/await}
</FullscreenDiv>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { FileDownloadState } from "$lib/modules/file";
import type { FileInfo } from "$lib/modules/filesystem";
import type { SummarizedFileInfo } from "$lib/modules/filesystem";
import { formatNetworkSpeed } from "$lib/utils";
import IconCloud from "~icons/material-symbols/cloud";
@@ -11,7 +11,7 @@
import IconError from "~icons/material-symbols/error";
interface Props {
info: FileInfo;
info: SummarizedFileInfo;
state: FileDownloadState;
}

View File

@@ -33,31 +33,23 @@
const toEntry =
<T extends Exclude<Entry["type"], "parent">>(type: T) =>
(details: Extract<Entry, { type: T }>["details"]) => ({
type,
name: details.name,
details,
});
(details: Extract<Entry, { type: T }>["details"]) => ({ type, name: details.name, details });
let entries = $derived([
...(showParentEntry ? ([{ type: "parent" }] as const) : []),
...sortEntries(info.subDirectories.map(toEntry("directory"))),
...sortEntries([
...info.files.map(toEntry("file")),
...getUploadingFiles(info.id).map(toEntry("uploading-file")),
...(getUploadingFiles(info.id) as LiveFileUploadState[]).map(toEntry("uploading-file")),
]),
]);
</script>
{#if entries.length > 0}
<div class="pb-[4.5rem]">
<RowVirtualizer
count={entries.length}
itemHeight={(index) => 56 + (index + 1 < entries.length ? 4 : 0)}
>
<RowVirtualizer count={entries.length} itemHeight={() => 56} itemGap={4}>
{#snippet item(index)}
{@const entry = entries[index]!}
<div class={index + 1 < entries.length ? "pb-1" : ""}>
{#if entry.type === "parent"}
<ActionEntryButton class="h-14" onclick={onParentClick}>
<DirectoryEntryLabel type="parent-directory" name=".." />
@@ -73,7 +65,6 @@
{:else}
<UploadingFile state={entry.details} />
{/if}
</div>
{/snippet}
</RowVirtualizer>
</div>