1- import { useState , useEffect } from "react" ;
1+ import { Suspense , useState , useEffect } from "react" ;
22import { Button } from "@/shared/components/button" ;
33import { Input } from "@/shared/components/input" ;
44import { ThumbnailCard } from "@/shared/components/thumbnail-card" ;
5+ import { CardGridSkeleton } from "@/shared/components/skeletons" ;
56
67import BaseLayout from "@/shared/layouts/base-layout" ;
78import { Link } from "@tanstack/react-router" ;
89import { Search } from "lucide-react" ;
910import { useCardSets } from "@/domain/cardsets/hooks/use-card-sets" ;
1011import { CardSetFilterSection } from "@/domain/cardsets/components/card-set-filter-section" ;
1112import type { CardSetCategory } from "@/domain/cardsets/types" ;
12- import { CardGridSkeleton } from "@/shared/components/skeletons" ;
13+
14+ interface CardSetGridProps {
15+ keyword ?: string ;
16+ category ?: CardSetCategory ;
17+ }
18+
19+ const CardSetGrid = ( { keyword, category } : CardSetGridProps ) => {
20+ const {
21+ data : cardsetsData ,
22+ fetchNextPage,
23+ hasNextPage,
24+ isFetchingNextPage,
25+ } = useCardSets ( { keyword, category, size : 20 } ) ;
26+
27+ return (
28+ < >
29+ < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" >
30+ { cardsetsData ?. cardsets . map ( ( cardset ) => (
31+ < Link
32+ to = "/groups/$groupId/cardsets/$cardsetId"
33+ params = { {
34+ groupId : cardset . groupId . toString ( ) ,
35+ cardsetId : cardset . cardSetId . toString ( ) ,
36+ } }
37+ key = { cardset . cardSetId }
38+ >
39+ < ThumbnailCard
40+ imageUrl = { cardset . imageUrl }
41+ title = { cardset . name }
42+ category = { cardset . category }
43+ subtitle = {
44+ cardset . hashtag
45+ ? `${ cardset . hashtag
46+ . split ( "," )
47+ . map ( ( tag ) => `#${ tag } ` )
48+ . join ( " " ) } `
49+ : undefined
50+ }
51+ />
52+ </ Link >
53+ ) ) }
54+ </ div >
55+
56+ { hasNextPage && (
57+ < div className = "flex justify-center" >
58+ < Button
59+ onClick = { ( ) => fetchNextPage ( ) }
60+ disabled = { isFetchingNextPage }
61+ variant = "outline"
62+ >
63+ { isFetchingNextPage ? "로딩 중..." : "더 보기" }
64+ </ Button >
65+ </ div >
66+ ) }
67+
68+ { ( ! cardsetsData ?. cardsets || cardsetsData . cardsets . length === 0 ) && (
69+ < div className = "flex justify-center items-center min-h-[200px]" >
70+ < div className = "text-gray-500" >
71+ 검색 조건에 맞는 카드셋이 없습니다.
72+ </ div >
73+ </ div >
74+ ) }
75+ </ >
76+ ) ;
77+ } ;
1378
1479const CardSetList = ( ) => {
1580 const [ searchInput , setSearchInput ] = useState ( "" ) ;
@@ -18,67 +83,21 @@ const CardSetList = () => {
1883 CardSetCategory | undefined
1984 > ( undefined ) ;
2085
21- // Debounce 검색어 처리
2286 useEffect ( ( ) => {
2387 const timer = setTimeout ( ( ) => {
2488 setSearchKeyword ( searchInput ) ;
25- } , 300 ) ;
89+ } , 1000 ) ;
2690
2791 return ( ) => clearTimeout ( timer ) ;
2892 } , [ searchInput ] ) ;
2993
30- // API에서 카드셋 데이터 가져오기
31- const {
32- data : cardsetsData ,
33- fetchNextPage,
34- hasNextPage,
35- isFetchingNextPage,
36- isLoading,
37- // error,
38- } = useCardSets ( {
39- keyword : searchKeyword || undefined ,
40- category : selectedCategory ,
41- size : 20 ,
42- } ) ;
43-
44- // 카테고리 체크박스 핸들러
4594 const handleCategoryChange = (
4695 category : CardSetCategory ,
47- checked : boolean
96+ checked : boolean ,
4897 ) => {
4998 setSelectedCategory ( checked ? category : undefined ) ;
5099 } ;
51100
52- // // 정렬 필드 핸들러
53- // const handleSortByChange = (value: string) => {
54- // setSortBy(value);
55- // };
56-
57- // // 정렬 순서 핸들러
58- // const handleOrderChange = (value: string) => {
59- // setOrder(value as "ASC" | "DESC");
60- // };
61-
62- // if (isLoading) {
63- // return (
64- // <BaseLayout>
65- // <div className="flex justify-center items-center min-h-[400px]">
66- // <div className="text-gray-500">로딩 중...</div>
67- // </div>
68- // </BaseLayout>
69- // );
70- // }
71-
72- // if (error) {
73- // return (
74- // <BaseLayout>
75- // <div className="flex justify-center items-center min-h-[400px]">
76- // <div className="text-red-500">데이터를 불러오는데 실패했습니다.</div>
77- // </div>
78- // </BaseLayout>
79- // );
80- // }
81-
82101 return (
83102 < BaseLayout >
84103 < div className = "space-y-6" >
@@ -101,93 +120,23 @@ const CardSetList = () => {
101120 </ div >
102121 </ div >
103122
104- { /* 필터 및 정렬 영역 */ }
123+ { /* 필터 영역 */ }
105124 < div className = "flex items-center justify-between gap-4" >
106125 < div className = "flex-1" >
107126 < CardSetFilterSection
108127 selectedCategories = { selectedCategory ? [ selectedCategory ] : [ ] }
109128 onCategoryChange = { handleCategoryChange }
110129 />
111130 </ div >
112- { /*
113- <div className="flex items-center gap-2 text-sm text-gray-600">
114- <span>정렬:</span>
115- <Select value={sortBy} onValueChange={handleSortByChange}>
116- <SelectTrigger className="w-24 h-8 text-xs">
117- <SelectValue />
118- </SelectTrigger>
119- <SelectContent>
120- <SelectItem value="createdAt">생성일</SelectItem>
121- <SelectItem value="name">이름</SelectItem>
122- <SelectItem value="modifiedAt">수정일</SelectItem>
123- </SelectContent>
124- </Select>
125- <Select value={order} onValueChange={handleOrderChange}>
126- <SelectTrigger className="w-20 h-8 text-xs">
127- <SelectValue />
128- </SelectTrigger>
129- <SelectContent>
130- <SelectItem value="DESC">내림차순↓</SelectItem>
131- <SelectItem value="ASC">오름차순↑</SelectItem>
132- </SelectContent>
133- </Select>
134- </div> */ }
135131 </ div >
136132
137- { /* 카드셋 리스트 */ }
138- { isLoading ? (
139- < CardGridSkeleton />
140- ) : (
141- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6" >
142- { cardsetsData ?. cardsets . map ( ( cardset ) => (
143- < Link
144- to = "/groups/$groupId/cardsets/$cardsetId"
145- params = { {
146- groupId : cardset . groupId . toString ( ) ,
147- cardsetId : cardset . cardSetId . toString ( ) ,
148- } }
149- key = { cardset . cardSetId }
150- >
151- < ThumbnailCard
152- imageUrl = { cardset . imageUrl }
153- title = { cardset . name }
154- category = { cardset . category }
155- subtitle = {
156- cardset . hashtag
157- ? `${ cardset . hashtag
158- . split ( "," )
159- . map ( ( tag ) => `#${ tag } ` )
160- . join ( " " ) } `
161- : undefined
162- }
163- />
164- </ Link >
165- ) ) }
166- </ div >
167- ) }
168-
169- { /* 더 보기 버튼 */ }
170- { hasNextPage && (
171- < div className = "flex justify-center" >
172- < Button
173- onClick = { ( ) => fetchNextPage ( ) }
174- disabled = { isFetchingNextPage }
175- variant = "outline"
176- >
177- { isFetchingNextPage ? "로딩 중..." : "더 보기" }
178- </ Button >
179- </ div >
180- ) }
181-
182- { /* 결과 없음 */ }
183- { ( ! cardsetsData ?. cardsets || cardsetsData . cardsets . length === 0 ) &&
184- ! isLoading && (
185- < div className = "flex justify-center items-center min-h-[200px]" >
186- < div className = "text-gray-500" >
187- 검색 조건에 맞는 카드셋이 없습니다.
188- </ div >
189- </ div >
190- ) }
133+ { /* 카드셋 리스트 - 로딩 중에는 그리드 영역만 스켈레톤으로 대체 */ }
134+ < Suspense fallback = { < CardGridSkeleton /> } >
135+ < CardSetGrid
136+ keyword = { searchKeyword || undefined }
137+ category = { selectedCategory }
138+ />
139+ </ Suspense >
191140 </ div >
192141 </ BaseLayout >
193142 ) ;
0 commit comments