From ed21a9cd31c1f851774e714103939b989d389664 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 26 Dec 2025 22:29:44 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B0=A4=EB=9F=AC=EB=A6=AC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 18 +++ .../molecules/Categories/Categories.svelte | 2 +- .../molecules/Gallery/Gallery.svelte | 128 ++++++++++++++++++ .../molecules/Gallery/Thumbnail.svelte | 42 ++++++ src/lib/components/molecules/Gallery/index.ts | 1 + src/lib/components/molecules/index.ts | 1 + .../organisms/Category/Category.svelte | 2 +- src/lib/{modules/util.ts => utils/format.ts} | 36 +---- src/lib/utils/index.ts | 2 + src/lib/utils/sort.ts | 57 ++++++++ .../file/[id]/DownloadStatus.svelte | 2 +- .../(fullscreen)/file/downloads/File.svelte | 2 +- .../(fullscreen)/file/uploads/File.svelte | 2 +- src/routes/(fullscreen)/gallery/+page.svelte | 25 ++++ src/routes/(fullscreen)/gallery/+page.ts | 7 + .../(fullscreen)/settings/cache/+page.svelte | 2 +- .../(fullscreen)/settings/cache/File.svelte | 2 +- .../(fullscreen)/settings/thumbnail/+page.ts | 10 +- .../settings/thumbnail/File.svelte | 2 +- .../[[id]]/CategoryDeleteModal.svelte | 2 +- .../DirectoryEntries/DirectoryEntries.svelte | 2 +- .../[[id]]/DirectoryEntries/File.svelte | 2 +- .../DirectoryEntries/UploadingFile.svelte | 2 +- .../[[id]]/DuplicateFileModal.svelte | 2 +- .../directory/[[id]]/EntryDeleteModal.svelte | 2 +- src/routes/(main)/menu/+page.ts | 10 +- 27 files changed, 307 insertions(+), 59 deletions(-) create mode 100644 src/lib/components/molecules/Gallery/Gallery.svelte create mode 100644 src/lib/components/molecules/Gallery/Thumbnail.svelte create mode 100644 src/lib/components/molecules/Gallery/index.ts rename src/lib/{modules/util.ts => utils/format.ts} (64%) create mode 100644 src/lib/utils/sort.ts create mode 100644 src/routes/(fullscreen)/gallery/+page.svelte create mode 100644 src/routes/(fullscreen)/gallery/+page.ts diff --git a/package.json b/package.json index ce46336..bd44d2c 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@sveltejs/adapter-node": "^5.4.0", "@sveltejs/kit": "^2.49.2", "@sveltejs/vite-plugin-svelte": "^6.2.1", + "@tanstack/svelte-virtual": "^3.13.13", "@trpc/client": "^11.8.1", "@types/file-saver": "^2.0.7", "@types/ms": "^0.7.34", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3de21a..9503e44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ importers: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 version: 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@1.21.7)(yaml@2.8.0)) + '@tanstack/svelte-virtual': + specifier: ^3.13.13 + version: 3.13.13(svelte@5.46.1) '@trpc/client': specifier: ^11.8.1 version: 11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3) @@ -610,6 +613,14 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 + '@tanstack/svelte-virtual@3.13.13': + resolution: {integrity: sha512-VDOvbRw3R+XBQdFodEJ4E7AOmEyo3Bmr4zL4DLVnJ0fxICdbvY5F5t8zSwJ4f7lqjckXi0yKFzY8WBtjaNbsGQ==} + peerDependencies: + svelte: ^3.48.0 || ^4.0.0 || ^5.0.0 + + '@tanstack/virtual-core@3.13.13': + resolution: {integrity: sha512-uQFoSdKKf5S8k51W5t7b2qpfkyIbdHMzAn+AMQvHPxKUPeo1SsGaA4JRISQT87jm28b7z8OEqPcg1IOZagQHcA==} + '@trpc/client@11.8.1': resolution: {integrity: sha512-L/SJFGanr9xGABmuDoeXR4xAdHJmsXsiF9OuH+apecJ+8sUITzVT1EPeqp0ebqA6lBhEl5pPfg3rngVhi/h60Q==} peerDependencies: @@ -2367,6 +2378,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@tanstack/svelte-virtual@3.13.13(svelte@5.46.1)': + dependencies: + '@tanstack/virtual-core': 3.13.13 + svelte: 5.46.1 + + '@tanstack/virtual-core@3.13.13': {} + '@trpc/client@11.8.1(@trpc/server@11.8.1(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@trpc/server': 11.8.1(typescript@5.9.3) diff --git a/src/lib/components/molecules/Categories/Categories.svelte b/src/lib/components/molecules/Categories/Categories.svelte index a11313e..54368c6 100644 --- a/src/lib/components/molecules/Categories/Categories.svelte +++ b/src/lib/components/molecules/Categories/Categories.svelte @@ -3,7 +3,7 @@ import type { SvelteHTMLElements } from "svelte/elements"; import { get, type Writable } from "svelte/store"; import type { CategoryInfo } from "$lib/modules/filesystem"; - import { SortBy, sortEntries } from "$lib/modules/util"; + import { SortBy, sortEntries } from "$lib/utils"; import Category from "./Category.svelte"; import type { SelectedCategory } from "./service"; diff --git a/src/lib/components/molecules/Gallery/Gallery.svelte b/src/lib/components/molecules/Gallery/Gallery.svelte new file mode 100644 index 0000000..1bcb335 --- /dev/null +++ b/src/lib/components/molecules/Gallery/Gallery.svelte @@ -0,0 +1,128 @@ + + +
+
+ {#each $virtualizer.getVirtualItems() as virtualRow (virtualRow.key)} + {@const row = rows[virtualRow.index]!} +
+ {#if row.type === "header"} +

{row.label}

+ {:else} +
+ {#each row.items as { info }} + + {/each} +
+ {/if} +
+ {/each} +
+ {#if $virtualizer.getVirtualItems().length === 0} +
+

+ {#if files.length === 0} + 업로드된 파일이 없어요. + {:else if filesWithDate.length === 0} + 파일 목록을 불러오고 있어요. + {:else} + 사진 또는 동영상이 없어요. + {/if} +

+
+ {/if} +
diff --git a/src/lib/components/molecules/Gallery/Thumbnail.svelte b/src/lib/components/molecules/Gallery/Thumbnail.svelte new file mode 100644 index 0000000..463419a --- /dev/null +++ b/src/lib/components/molecules/Gallery/Thumbnail.svelte @@ -0,0 +1,42 @@ + + +{#if $info} + +{/if} diff --git a/src/lib/components/molecules/Gallery/index.ts b/src/lib/components/molecules/Gallery/index.ts new file mode 100644 index 0000000..9a269ab --- /dev/null +++ b/src/lib/components/molecules/Gallery/index.ts @@ -0,0 +1 @@ +export { default as Gallery } from "./Gallery.svelte"; diff --git a/src/lib/components/molecules/index.ts b/src/lib/components/molecules/index.ts index 8edc84a..b11ed5f 100644 --- a/src/lib/components/molecules/index.ts +++ b/src/lib/components/molecules/index.ts @@ -2,6 +2,7 @@ export * from "./ActionModal.svelte"; export { default as ActionModal } from "./ActionModal.svelte"; export * from "./Categories"; export { default as Categories } from "./Categories"; +export * from "./Gallery"; export { default as IconEntryButton } from "./IconEntryButton.svelte"; export * from "./labels"; export { default as SubCategories } from "./SubCategories.svelte"; diff --git a/src/lib/components/organisms/Category/Category.svelte b/src/lib/components/organisms/Category/Category.svelte index 295bf99..ce3abcd 100644 --- a/src/lib/components/organisms/Category/Category.svelte +++ b/src/lib/components/organisms/Category/Category.svelte @@ -4,8 +4,8 @@ import { CheckBox } from "$lib/components/atoms"; import { SubCategories, type SelectedCategory } from "$lib/components/molecules"; import { getFileInfo, type FileInfo, type CategoryInfo } from "$lib/modules/filesystem"; - import { SortBy, sortEntries } from "$lib/modules/util"; import { masterKeyStore } from "$lib/stores"; + import { SortBy, sortEntries } from "$lib/utils"; import File from "./File.svelte"; import type { SelectedFile } from "./service"; diff --git a/src/lib/modules/util.ts b/src/lib/utils/format.ts similarity index 64% rename from src/lib/modules/util.ts rename to src/lib/utils/format.ts index 3fff89d..ce3b35c 100644 --- a/src/lib/modules/util.ts +++ b/src/lib/utils/format.ts @@ -7,6 +7,13 @@ export const formatDate = (date: Date) => { return `${year}. ${month}. ${day}.`; }; +export const formatDateSortable = (date: Date) => { + const year = date.getFullYear(); + const month = pad2(date.getMonth() + 1); + const day = pad2(date.getDate()); + return `${year}${month}${day}`; +}; + export const formatDateTime = (date: Date) => { const dateFormatted = formatDate(date); const hours = date.getHours(); @@ -32,32 +39,3 @@ export const truncateString = (str: string, maxLength = 20) => { if (str.length <= maxLength) return str; return `${str.slice(0, maxLength)}...`; }; - -export enum SortBy { - NAME_ASC, - NAME_DESC, -} - -type SortFunc = (a?: string, b?: string) => number; - -const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); - -const sortByNameAsc: SortFunc = (a, b) => { - if (a && b) return collator.compare(a, b); - if (a) return -1; - if (b) return 1; - return 0; -}; - -const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); - -export const sortEntries = (entries: T[], sortBy: SortBy) => { - let sortFunc: SortFunc; - if (sortBy === SortBy.NAME_ASC) { - sortFunc = sortByNameAsc; - } else { - sortFunc = sortByNameDesc; - } - - entries.sort((a, b) => sortFunc(a.name, b.name)); -}; diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 4c24322..1db9577 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1 +1,3 @@ +export * from "./format"; export * from "./gotoStateful"; +export * from "./sort"; diff --git a/src/lib/utils/sort.ts b/src/lib/utils/sort.ts new file mode 100644 index 0000000..2385e55 --- /dev/null +++ b/src/lib/utils/sort.ts @@ -0,0 +1,57 @@ +interface SortEntry { + name?: string; + date?: Date; +} + +export enum SortBy { + NAME_ASC, + NAME_DESC, + DATE_ASC, + DATE_DESC, +} + +type SortFunc = (a: SortEntry, b: SortEntry) => number; + +const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }); + +const sortByNameAsc: SortFunc = ({ name: a }, { name: b }) => { + if (a && b) return collator.compare(a, b); + if (a) return -1; + if (b) return 1; + return 0; +}; + +const sortByNameDesc: SortFunc = (a, b) => -sortByNameAsc(a, b); + +const sortByDateAsc: SortFunc = ({ date: a }, { date: b }) => { + if (a && b) return a.getTime() - b.getTime(); + if (a) return -1; + if (b) return 1; + return 0; +}; + +const sortByDateDesc: SortFunc = (a, b) => -sortByDateAsc(a, b); + +export const sortEntries = (entries: T[], sortBy: SortBy) => { + let sortFunc: SortFunc; + + switch (sortBy) { + case SortBy.NAME_ASC: + sortFunc = sortByNameAsc; + break; + case SortBy.NAME_DESC: + sortFunc = sortByNameDesc; + break; + case SortBy.DATE_ASC: + sortFunc = sortByDateAsc; + break; + case SortBy.DATE_DESC: + sortFunc = sortByDateDesc; + break; + default: + const exhaustive: never = sortBy; + sortFunc = exhaustive; + } + + entries.sort(sortFunc); +}; diff --git a/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte b/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte index 21774cd..150669e 100644 --- a/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte +++ b/src/routes/(fullscreen)/file/[id]/DownloadStatus.svelte @@ -1,7 +1,7 @@ + + + 사진 및 동영상 + + + + + goto(`/file/${id}`)} /> + diff --git a/src/routes/(fullscreen)/gallery/+page.ts b/src/routes/(fullscreen)/gallery/+page.ts new file mode 100644 index 0000000..1a241c5 --- /dev/null +++ b/src/routes/(fullscreen)/gallery/+page.ts @@ -0,0 +1,7 @@ +import { trpc } from "$trpc/client"; +import type { PageLoad } from "./$types"; + +export const load: PageLoad = async ({ fetch }) => { + const files = await trpc(fetch).file.list.query(); + return { files }; +}; diff --git a/src/routes/(fullscreen)/settings/cache/+page.svelte b/src/routes/(fullscreen)/settings/cache/+page.svelte index af375c2..cf8192d 100644 --- a/src/routes/(fullscreen)/settings/cache/+page.svelte +++ b/src/routes/(fullscreen)/settings/cache/+page.svelte @@ -6,8 +6,8 @@ import type { FileCacheIndex } from "$lib/indexedDB"; import { getFileCacheIndex, deleteFileCache as doDeleteFileCache } from "$lib/modules/file"; import { getFileInfo, type FileInfo } from "$lib/modules/filesystem"; - import { formatFileSize } from "$lib/modules/util"; import { masterKeyStore } from "$lib/stores"; + import { formatFileSize } from "$lib/utils"; import File from "./File.svelte"; interface FileCache { diff --git a/src/routes/(fullscreen)/settings/cache/File.svelte b/src/routes/(fullscreen)/settings/cache/File.svelte index f21445b..581d144 100644 --- a/src/routes/(fullscreen)/settings/cache/File.svelte +++ b/src/routes/(fullscreen)/settings/cache/File.svelte @@ -2,7 +2,7 @@ import type { Writable } from "svelte/store"; import type { FileCacheIndex } from "$lib/indexedDB"; import type { FileInfo } from "$lib/modules/filesystem"; - import { formatDate, formatFileSize } from "$lib/modules/util"; + import { formatDate, formatFileSize } from "$lib/utils"; import IconDraft from "~icons/material-symbols/draft"; import IconScanDelete from "~icons/material-symbols/scan-delete"; diff --git a/src/routes/(fullscreen)/settings/thumbnail/+page.ts b/src/routes/(fullscreen)/settings/thumbnail/+page.ts index f435b72..4d5520c 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/+page.ts +++ b/src/routes/(fullscreen)/settings/thumbnail/+page.ts @@ -1,13 +1,7 @@ -import { error } from "@sveltejs/kit"; import { trpc } from "$trpc/client"; import type { PageLoad } from "./$types"; export const load: PageLoad = async ({ fetch }) => { - try { - const files = await trpc(fetch).file.listWithoutThumbnail.query(); - return { files }; - } catch { - // TODO: Error Handling - error(500, "Internal server error"); - } + const files = await trpc(fetch).file.listWithoutThumbnail.query(); + return { files }; }; diff --git a/src/routes/(fullscreen)/settings/thumbnail/File.svelte b/src/routes/(fullscreen)/settings/thumbnail/File.svelte index 8977fc7..93c23ad 100644 --- a/src/routes/(fullscreen)/settings/thumbnail/File.svelte +++ b/src/routes/(fullscreen)/settings/thumbnail/File.svelte @@ -14,7 +14,7 @@ import { ActionEntryButton } from "$lib/components/atoms"; import { DirectoryEntryLabel } from "$lib/components/molecules"; import type { FileInfo } from "$lib/modules/filesystem"; - import { formatDateTime } from "$lib/modules/util"; + import { formatDateTime } from "$lib/utils"; import type { GenerationStatus } from "./service.svelte"; import IconCamera from "~icons/material-symbols/camera"; diff --git a/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte index 5e792d1..289b971 100644 --- a/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte +++ b/src/routes/(main)/category/[[id]]/CategoryDeleteModal.svelte @@ -1,6 +1,6 @@