Skip to content

Commit d06c180

Browse files
Merge pull request #19 from MurakawaTakuya/optimize-fetching-favorite-list
お気に入り一覧の取得を最適化
2 parents 928363b + 18fcdf1 commit d06c180

File tree

7 files changed

+355
-242
lines changed

7 files changed

+355
-242
lines changed

src/app/api/favorites/route.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export async function GET(request: NextRequest) {
2121
// フォルダIDでフィルタリングする場合
2222
const { searchParams } = new URL(request.url);
2323
const folderId = searchParams.get("folderId");
24+
const idsOnly = searchParams.get("idsOnly") === "true";
2425

2526
const conditions = [eq(schema.favorites.userId, userId)];
2627

@@ -36,10 +37,33 @@ export async function GET(request: NextRequest) {
3637
}
3738
}
3839

40+
if (idsOnly) {
41+
// 軽量なデータのみ取得 (Context用)
42+
const results = await db
43+
.select({
44+
id: schema.favorites.id,
45+
paperId: schema.favorites.paperId,
46+
folderId: schema.favorites.folderId,
47+
})
48+
.from(schema.favorites)
49+
.where(and(...conditions));
50+
return NextResponse.json(results);
51+
}
52+
53+
// 通常の取得
3954
const results = await db
4055
.select({
4156
favorite: schema.favorites,
42-
paper: schema.papers,
57+
paper: {
58+
id: schema.papers.id,
59+
title: schema.papers.title,
60+
url: schema.papers.url,
61+
abstract: schema.papers.abstract,
62+
authors: schema.papers.authors,
63+
conferenceName: schema.papers.conferenceName,
64+
conferenceYear: schema.papers.conferenceYear,
65+
createdAt: schema.papers.createdAt,
66+
},
4367
folder: schema.folders,
4468
})
4569
.from(schema.favorites)

src/app/favorites/page.tsx

Lines changed: 84 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@ import { Input } from "@/components/ui/input";
1717
import { SidebarInset, SidebarTrigger } from "@/components/ui/sidebar";
1818
import { Spinner } from "@/components/ui/spinner";
1919
import { useAuth } from "@/contexts/AuthContext";
20-
import { useFavorites } from "@/hooks/use-favorites";
20+
import { useFavorites } from "@/contexts/FavoritesContext";
21+
import { Favorite, Folder } from "@/db/schema";
2122
import { Paper } from "@/lib/types";
2223
import { Check, Pencil, Trash2, X } from "lucide-react";
2324
import { useRouter, useSearchParams } from "next/navigation";
24-
import { Suspense, useMemo, useState } from "react";
25+
import { Suspense, useEffect, useMemo, useState } from "react";
26+
import { toast } from "sonner";
27+
28+
// FavoriteWithPaper type definition for local use
29+
type FavoriteWithPaper = Favorite & {
30+
paper: Paper;
31+
folder: Folder | null;
32+
};
2533

2634
export default function FavoritesPage() {
2735
return (
@@ -40,19 +48,83 @@ export default function FavoritesPage() {
4048
function FavoritesContent() {
4149
const { user, loading: authLoading } = useAuth();
4250
const {
43-
favorites,
44-
loading: favoritesLoading,
4551
folders,
52+
loading: contextLoading,
4653
renameFolder,
4754
deleteFolder,
4855
} = useFavorites();
56+
4957
const searchParams = useSearchParams();
5058
const router = useRouter();
5159
const folderIdParam = searchParams.get("folderId");
60+
61+
// Local state for full favorite papers
62+
const [favorites, setFavorites] = useState<FavoriteWithPaper[]>([]);
63+
const [favoritesLoading, setFavoritesLoading] = useState(true);
64+
5265
const [isEditing, setIsEditing] = useState(false);
5366
const [editName, setEditName] = useState("");
5467
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
5568

69+
const papers = useMemo(() => {
70+
const uniqueMap = new Map<number, Paper>();
71+
favorites.forEach((f) => {
72+
if (!uniqueMap.has(f.paperId)) {
73+
uniqueMap.set(f.paperId, {
74+
...f.paper,
75+
cosineSimilarity: null,
76+
});
77+
}
78+
});
79+
return Array.from(uniqueMap.values());
80+
}, [favorites]);
81+
82+
// Fetch favorites when folderIdParam changes
83+
useEffect(() => {
84+
if (!user) return;
85+
86+
const fetchFavorites = async () => {
87+
setFavoritesLoading(true);
88+
try {
89+
const token = await user.getIdToken();
90+
const url = new URL("/api/favorites", window.location.href);
91+
if (folderIdParam) {
92+
url.searchParams.set("folderId", folderIdParam);
93+
}
94+
95+
const res = await fetch(url.toString(), {
96+
headers: { Authorization: `Bearer ${token}` },
97+
});
98+
99+
if (res.ok) {
100+
const data = await res.json();
101+
// 「すべて」表示の際は、論文が重複しないようにクライアント側でユニークにする
102+
if (!folderIdParam) {
103+
const uniqueMap = new Map<number, FavoriteWithPaper>();
104+
data.forEach((f: FavoriteWithPaper) => {
105+
if (!uniqueMap.has(f.paperId)) {
106+
uniqueMap.set(f.paperId, f);
107+
}
108+
});
109+
setFavorites(Array.from(uniqueMap.values()));
110+
} else {
111+
setFavorites(data);
112+
}
113+
} else {
114+
console.error("Failed to fetch favorites");
115+
toast.error("お気に入りの取得に失敗しました");
116+
}
117+
} catch (error) {
118+
console.error("Failed to fetch favorites:", error);
119+
toast.error("お気に入りの取得に失敗しました");
120+
} finally {
121+
setFavoritesLoading(false);
122+
}
123+
};
124+
125+
fetchFavorites();
126+
}, [user, folderIdParam]);
127+
56128
const canRename = useMemo(() => {
57129
if (!folderIdParam || folderIdParam === "null") return false;
58130
const fid = parseInt(folderIdParam);
@@ -76,32 +148,15 @@ function FavoritesContent() {
76148
}
77149
};
78150

79-
const filteredFavorites = useMemo(() => {
80-
if (!folderIdParam) {
81-
const uniqueMap = new Map();
82-
favorites.forEach((f) => {
83-
if (!uniqueMap.has(f.paperId)) {
84-
uniqueMap.set(f.paperId, f);
85-
}
86-
});
87-
return Array.from(uniqueMap.values());
88-
}
89-
if (folderIdParam === "null") {
90-
return favorites.filter((f) => f.folderId === null);
91-
}
92-
const fid = parseInt(folderIdParam);
93-
if (isNaN(fid)) return [];
94-
return favorites.filter((f) => f.folderId === fid);
95-
}, [favorites, folderIdParam]);
96-
97151
const currentFolderName = useMemo(() => {
98152
if (!folderIdParam) return "すべて";
99153
if (folderIdParam === "null") return "デフォルト";
100154
const folder = folders.find((f) => f.id === parseInt(folderIdParam));
101155
return folder ? folder.name : "不明なフォルダ";
102156
}, [folders, folderIdParam]);
103157

104-
if (authLoading || favoritesLoading) {
158+
if (authLoading || (contextLoading && folders.length === 0)) {
159+
// Wait for auth and initial folder load (at least to know if they exist)
105160
return (
106161
<div className="flex bg-sidebar h-screen w-full items-center justify-center">
107162
<Spinner className="h-8 w-8" />
@@ -117,13 +172,6 @@ function FavoritesContent() {
117172
);
118173
}
119174

120-
const papers = filteredFavorites.map(
121-
(f): Paper => ({
122-
...f.paper,
123-
cosineSimilarity: null,
124-
})
125-
);
126-
127175
return (
128176
<SidebarInset>
129177
<header className="flex h-16 shrink-0 items-center px-4 mb-5 sticky top-0 z-50 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b justify-between">
@@ -162,7 +210,7 @@ function FavoritesContent() {
162210
</div>
163211
) : (
164212
<div className="flex items-center gap-1">
165-
{currentFolderName} ({papers.length})
213+
{currentFolderName} ({favoritesLoading ? "..." : papers.length})
166214
{canRename && (
167215
<>
168216
<Button
@@ -196,7 +244,11 @@ function FavoritesContent() {
196244
</header>
197245

198246
<div className="flex flex-col items-center gap-8 w-full max-w-7xl px-4 mx-auto pb-24">
199-
{papers.length > 0 ? (
247+
{favoritesLoading ? (
248+
<div className="flex justify-center py-12">
249+
<Spinner className="h-8 w-8" />
250+
</div>
251+
) : papers.length > 0 ? (
200252
<PapersTable
201253
papers={papers}
202254
selectedPapers={new Set()}

src/app/layout.tsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AppSidebar } from "@/components/app-sidebar";
22
import { SidebarProvider } from "@/components/ui/sidebar";
33
import { Toaster } from "@/components/ui/sonner";
44
import { AuthProvider } from "@/contexts/AuthContext";
5+
import { FavoritesProvider } from "@/contexts/FavoritesContext";
56
import { SearchHistoryProvider } from "@/contexts/SearchHistoryContext";
67
import type { Metadata } from "next";
78
import { Geist, Geist_Mono } from "next/font/google";
@@ -45,12 +46,14 @@ export default function RootLayout({
4546
</head>
4647
<body className={`${geistSans.variable} ${geistMono.variable}`}>
4748
<AuthProvider>
48-
<SearchHistoryProvider>
49-
<SidebarProvider>
50-
<AppSidebar />
51-
{children}
52-
</SidebarProvider>
53-
</SearchHistoryProvider>
49+
<FavoritesProvider>
50+
<SearchHistoryProvider>
51+
<SidebarProvider>
52+
<AppSidebar />
53+
{children}
54+
</SidebarProvider>
55+
</SearchHistoryProvider>
56+
</FavoritesProvider>
5457
<Toaster />
5558
</AuthProvider>
5659
</body>

src/components/app-sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ import {
5353
SidebarMenuSubItem,
5454
useSidebar,
5555
} from "@/components/ui/sidebar";
56+
import { useFavorites } from "@/contexts/FavoritesContext";
5657
import { useSearchHistory } from "@/contexts/SearchHistoryContext";
57-
import { useFavorites } from "@/hooks/use-favorites";
5858

5959
interface NavItem {
6060
title: string;

src/components/categorization-results.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from "@/components/ui/dialog";
1717
import { Input } from "@/components/ui/input";
1818
import { Spinner } from "@/components/ui/spinner";
19-
import { useFavorites } from "@/hooks/use-favorites";
19+
import { useFavorites } from "@/contexts/FavoritesContext";
2020
import { CategorizedPaper } from "@/lib/types";
2121
import { cn } from "@/lib/utils";
2222
import { ChevronRight, Star } from "lucide-react";

src/components/papers-table.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { FavoriteButton } from "@/components/favorite-button";
44
import { Button } from "@/components/ui/button";
5-
import { useFavorites } from "@/hooks/use-favorites";
5+
import { useFavorites } from "@/contexts/FavoritesContext";
66
import { Paper } from "@/lib/types";
77
import { useState } from "react";
88

@@ -35,7 +35,6 @@ export function PapersTable({
3535

3636
const {
3737
favoritePaperIds,
38-
favorites, // used for getFolderId logic replacement helpers
3938
folders,
4039
removeFavoriteByPaperId,
4140
createFolder,

0 commit comments

Comments
 (0)