diff --git a/package-lock.json b/package-lock.json index 51afa783..af046123 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "pdfjs-dist": "^4.9.155", "prettier": "^2.8.8", "qs": "^6.12.1", + "query-string": "^9.1.1", "react": "^18", "react-canvas-confetti": "^2.0.7", "react-day-picker": "^8.10.1", @@ -13185,6 +13186,14 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decode-uri-component": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.4.1.tgz", + "integrity": "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ==", + "engines": { + "node": ">=14.16" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -21254,6 +21263,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-9.1.1.tgz", + "integrity": "sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==", + "dependencies": { + "decode-uri-component": "^0.4.1", + "filter-obj": "^5.1.0", + "split-on-first": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/query-string/node_modules/filter-obj": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-5.1.0.tgz", + "integrity": "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/querystring-es3": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", @@ -23464,6 +23500,17 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/split-on-first": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-3.0.0.tgz", + "integrity": "sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 47d460f1..4a44860a 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "pdfjs-dist": "^4.9.155", "prettier": "^2.8.8", "qs": "^6.12.1", + "query-string": "^9.1.1", "react": "^18", "react-canvas-confetti": "^2.0.7", "react-day-picker": "^8.10.1", diff --git a/src/features/collection/components/exploration.tsx b/src/features/collection/components/exploration.tsx index 1dd4107d..3b1846b5 100644 --- a/src/features/collection/components/exploration.tsx +++ b/src/features/collection/components/exploration.tsx @@ -3,17 +3,29 @@ import Icon from '@/shared/components/custom/icon' import Collection from './collection' import CollectionList from './collection-list' -import Text from '@/shared/components/ui/text' import { useCollections } from '@/requests/collection/hooks' import Loading from '@/shared/components/custom/loading' import { useUser } from '@/shared/hooks/use-user' import Link from 'next/link' import { useScrollPosition } from '@/shared/hooks/use-scroll-position' - -const controlButtons = ['분야', '퀴즈 유형', '문제 수'] +import SelectMinQuizCountDrawer from './select-min-quiz-count-drawer' +import { useSearchParams } from 'next/navigation' +import { DEFAULT_COLLECTION_QUIZ_COUNT } from '../config' +import SelectQuizTypeDrawer from './select-quiz-type-drawer' +import SelectCategoryDrawer from './select-category-drawer' const Exploration = () => { - const { data: collectionsData, isLoading } = useCollections() + const searchParams = useSearchParams() + const categories = searchParams.getAll('collection-category') as Collection.Field[] + const quizType = searchParams.get('quiz-type') as Quiz.Type + const minQuizCount = Number(searchParams.get('min-quiz-count')) || DEFAULT_COLLECTION_QUIZ_COUNT + + const { data: collectionsData, isLoading } = useCollections({ + collectionSortOption: 'POPULARITY', + collectionCategories: categories, + quizType, + quizCount: minQuizCount, + }) const { user } = useUser() const scrollContainerRef = useScrollPosition({ pageKey: 'exploration' }) @@ -22,17 +34,9 @@ const Exploration = () => { <>
- {controlButtons.map((button) => ( - - ))} + + +
diff --git a/src/features/collection/components/select-category-drawer.tsx b/src/features/collection/components/select-category-drawer.tsx new file mode 100644 index 00000000..4312b5af --- /dev/null +++ b/src/features/collection/components/select-category-drawer.tsx @@ -0,0 +1,117 @@ +'use client' + +import QS from 'query-string' +import FixedBottom from '@/shared/components/custom/fixed-bottom' +import Icon from '@/shared/components/custom/icon' +import { Button } from '@/shared/components/ui/button' +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/shared/components/ui/drawer' +import Text from '@/shared/components/ui/text' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { cn } from '@/shared/lib/utils' +import { useState } from 'react' +import { CATEGORIES } from '@/features/category/config' +import { Checkbox } from '@/shared/components/ui/checkbox' +import Label from '@/shared/components/ui/label' + +interface Props { + categories: Collection.Field[] +} + +const SelectCategoryDrawer = ({ categories }: Props) => { + const [innerCategories, setInnerCategories] = useState(categories) + const searchParamsString = useSearchParams().toString() + const router = useRouter() + const pathname = usePathname() + + const sortByCategories = (categories: Collection.Field[]) => { + const categoryOrder = CATEGORIES.map((category) => category.id) + return categories.sort((a, b) => categoryOrder.indexOf(a) - categoryOrder.indexOf(b)) + } + + return ( + + + + + + + + 분야 + + +
+ +
+ {CATEGORIES.map((category) => ( +
+ { + if (innerCategories.includes(category.id)) { + setInnerCategories( + sortByCategories(innerCategories.filter((c) => c !== category.id)) + ) + } else { + setInnerCategories(sortByCategories([...innerCategories, category.id])) + } + }} + /> + +
+ ))} +
+ + + + + + + + + ) +} + +export default SelectCategoryDrawer diff --git a/src/features/collection/components/select-min-quiz-count-drawer.tsx b/src/features/collection/components/select-min-quiz-count-drawer.tsx new file mode 100644 index 00000000..ab3ce691 --- /dev/null +++ b/src/features/collection/components/select-min-quiz-count-drawer.tsx @@ -0,0 +1,112 @@ +'use client' + +import QS from 'query-string' +import FixedBottom from '@/shared/components/custom/fixed-bottom' +import Icon from '@/shared/components/custom/icon' +import { Button } from '@/shared/components/ui/button' +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/shared/components/ui/drawer' +import { Slider } from '@/shared/components/ui/slider' +import Text from '@/shared/components/ui/text' +import { useState } from 'react' +import { DEFAULT_COLLECTION_QUIZ_COUNT } from '../config' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { cn } from '@/shared/lib/utils' + +interface Props { + count: number +} + +const SelectMinQuizCountDrawer = ({ count }: Props) => { + const [innerCount, setInnerCount] = useState(count) + const searchParamsString = useSearchParams().toString() + const router = useRouter() + const pathname = usePathname() + + const isDefaultCount = innerCount === DEFAULT_COLLECTION_QUIZ_COUNT + + return ( + + + + + + + + 문제 수 + + +
+
+ + 최소 문제 수 + + + {innerCount} 문제 + +
+ { + if (value[0] == null || value[0] < DEFAULT_COLLECTION_QUIZ_COUNT) return + + setInnerCount(value[0]) + }} + /> +
+ + 5 문제 + + + 99 문제 + +
+
+
+ + + + + + + + + ) +} + +export default SelectMinQuizCountDrawer diff --git a/src/features/collection/components/select-quiz-type-drawer.tsx b/src/features/collection/components/select-quiz-type-drawer.tsx new file mode 100644 index 00000000..89f7d2c9 --- /dev/null +++ b/src/features/collection/components/select-quiz-type-drawer.tsx @@ -0,0 +1,116 @@ +'use client' + +import QS from 'query-string' +import FixedBottom from '@/shared/components/custom/fixed-bottom' +import Icon from '@/shared/components/custom/icon' +import { Button } from '@/shared/components/ui/button' +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from '@/shared/components/ui/drawer' +import Text from '@/shared/components/ui/text' +import { usePathname, useRouter, useSearchParams } from 'next/navigation' +import { cn } from '@/shared/lib/utils' +import { RadioGroup, RadioGroupItem } from '@/shared/components/ui/radio-group' +import Label from '@/shared/components/ui/label' +import { useState } from 'react' + +interface Props { + quizType?: Quiz.Type +} + +const SelectQuizTypeDrawer = ({ quizType }: Props) => { + const [innerQuizType, setInnerQuizType] = useState('all') + const searchParamsString = useSearchParams().toString() + const router = useRouter() + const pathname = usePathname() + + return ( + + + + + + + + 유형 + + +
+ +
+ +
+ setInnerQuizType('all')} /> + +
+
+ setInnerQuizType('MULTIPLE_CHOICE')} + /> + +
+
+ setInnerQuizType('MIX_UP')} + /> + +
+
+
+ + + + + + + + + ) +} + +export default SelectQuizTypeDrawer diff --git a/src/features/collection/config/index.ts b/src/features/collection/config/index.ts new file mode 100644 index 00000000..006acc3d --- /dev/null +++ b/src/features/collection/config/index.ts @@ -0,0 +1 @@ +export const DEFAULT_COLLECTION_QUIZ_COUNT = 5 diff --git a/src/requests/collection/client.ts b/src/requests/collection/client.ts index 640dae4b..ac1a724b 100644 --- a/src/requests/collection/client.ts +++ b/src/requests/collection/client.ts @@ -3,10 +3,15 @@ import { API_ENDPOINTS } from '@/shared/configs/endpoint' import { http } from '@/shared/lib/axios/http' -export const getAllCollections = async () => { +export const getAllCollections = async (props?: { + collectionSortOption: 'POPULARITY' | 'UPDATED' + collectionCategories: Collection.Field[] + quizType?: 'MIX_UP' | 'MULTIPLE_CHOICE' + quizCount: number +}) => { try { const { data } = await http.get( - API_ENDPOINTS.COLLECTION.GET.ALL + API_ENDPOINTS.COLLECTION.GET.ALL(props) ) return data } catch (error) { diff --git a/src/requests/collection/hooks.ts b/src/requests/collection/hooks.ts index efb6c994..9ac552fd 100644 --- a/src/requests/collection/hooks.ts +++ b/src/requests/collection/hooks.ts @@ -11,10 +11,15 @@ import { getRandomCollectionQuizzes, } from './client' -export const useCollections = () => { +export const useCollections = (props?: { + collectionSortOption: 'POPULARITY' | 'UPDATED' + collectionCategories: Collection.Field[] + quizType?: 'MIX_UP' | 'MULTIPLE_CHOICE' + quizCount: number +}) => { return useQuery({ - queryKey: ['collections'], - queryFn: async () => getAllCollections(), + queryKey: ['collections', JSON.stringify(props)], + queryFn: async () => getAllCollections(props), }) } diff --git a/src/shared/components/custom/fixed-bottom.tsx b/src/shared/components/custom/fixed-bottom.tsx index 96fecab7..042ff027 100644 --- a/src/shared/components/custom/fixed-bottom.tsx +++ b/src/shared/components/custom/fixed-bottom.tsx @@ -1,5 +1,4 @@ import { cn } from '@/shared/lib/utils' -import PortalProvider from './react/portal-provider' interface Props { children: React.ReactNode @@ -8,16 +7,14 @@ interface Props { const FixedBottom = ({ children, className }: Props) => { return ( - -
- {children} -
-
+
+ {children} +
) } diff --git a/src/shared/components/ui/radio-group.tsx b/src/shared/components/ui/radio-group.tsx index fc8a1c47..8d71cdf7 100644 --- a/src/shared/components/ui/radio-group.tsx +++ b/src/shared/components/ui/radio-group.tsx @@ -22,7 +22,7 @@ const RadioGroupItem = React.forwardRef< { + const query = props + ? QS.stringify({ + 'collection-sort-option': props.collectionSortOption, + 'collection-category': + props.collectionCategories.length > 0 + ? props.collectionCategories.join(',') + : undefined, + 'quiz-type': props.quizType, + 'quiz-count': props.quizCount, + }) + : '' + + return `/collections${query ? `?${query}` : ''}` + }, /** GET /collections/{keyword} - 컬렉션 검색하기 */ BY_KEYWORD: (keyword: string) => `/collections/${keyword}`, /** GET /collections/{collection_id}/collection_info - 만든 컬렉션 상세 정보 가져오기 */