diff --git a/src/api/QuizServices.ts b/src/api/QuizServices.ts index 7633ad0..798c199 100644 --- a/src/api/QuizServices.ts +++ b/src/api/QuizServices.ts @@ -6,25 +6,6 @@ import type { ChannelAPI } from '@/interfaces/ChannelAPI'; import type { PostAPI } from '@/interfaces/PostAPI'; import type { Quiz } from '@/interfaces/Quiz'; -function shuffle(array: T[], count: number): T[] { - const ret = [...array]; - for (let i = 0; i < array.length - 1; i += 1) { - const j = Math.floor(Math.random() * (i + 1)); - [ret[i], ret[j]] = [ret[j], ret[i]]; - } - return ret.slice(0, count < ret.length ? count : ret.length); -} - -function getPostIds() { - return api - .get('/channels') - .then((response) => response.data) - .then((data) => data.flatMap((channel) => channel.posts)) - .catch(() => { - throw new Error('error occured at getAllPostIds.'); - }); -} - function getPostsFromPostIds(postIds: string[]) { return axios.all( postIds.map((postId) => @@ -38,18 +19,14 @@ function getPostsFromPostIds(postIds: string[]) { ); } -function getPosts() { - return api - .get('/posts') - .then((response) => response.data) - .catch(() => { - throw new Error('error occured at getPosts.'); - }); -} - -function getShuffledPosts(count: number) { - return getPosts().then((posts) => shuffle(posts, count)); -} +export const getPosts = async () => { + try { + const response = await api.get('/posts'); + return response.data; + } catch (error) { + throw new Error('error occurred at getPosts.'); + } +}; function parseQuiz(post: PostAPI) { const postCopy: Partial = { ...post }; @@ -58,36 +35,12 @@ function parseQuiz(post: PostAPI) { return { ...postCopy, ...JSON.parse(quizContent) } as Quiz; } -function getPostsFromChannel(channelId: string): Promise { +export function getPostsFromChannel(channelId: string): Promise { return api .get(`/posts/channel/${channelId}`) .then((response) => response.data); } -/** - * @deprecated - */ -export function getPostIdsFromChannel(channelName: string): Promise { - return api - .get(`/channels/${channelName}`) - .then((response) => response.data) - .then((data) => (data.posts ? data.posts : [])) - .catch(() => { - throw new Error('error occured at getPostIdsFromChannel.'); - }); -} - -/** - * @deprecated - */ -export function getShuffledPostIds(count: number) { - return getPostIds() - .then((postIds) => shuffle(postIds, count)) - .catch(() => { - throw new Error('error occured at getShuffledPostIds.'); - }); -} - export function getQuizzesFromPostIds(postIds: string[]): Promise { return getPostsFromPostIds(postIds) .then((response) => response.map((post) => parseQuiz(post))) @@ -96,28 +49,6 @@ export function getQuizzesFromPostIds(postIds: string[]): Promise { }); } -export function getQuizzesFromChannel(channelId: string) { - return getPostsFromChannel(channelId) - .then((posts) => posts.map((post) => parseQuiz(post))) - .then((quiz) => quiz.reverse()); -} - -export function getShuffledQuizzes(count: number) { - return getShuffledPosts(count).then((posts) => - posts.map((post) => parseQuiz(post)) - ); -} - -export function caculateScore(quizzes: Quiz[], userAnswers: string[]) { - // 전부 선택하지 않았거나 user가 임의로 조작했다면 0점을 부여한다. - if (quizzes.length !== userAnswers.filter((answer) => answer).length) - return 0; - // filter corrected quizzes and add scores - return quizzes - .filter((quiz, index) => quiz.answer === userAnswers[index]) - .reduce((acc, cur) => acc + cur.difficulty * 10, 0); -} - export async function getChannels() { return api .get('/channels') diff --git a/src/assets/QuizCreateMockData.ts b/src/assets/QuizCreateMockData.ts index f82d971..d5876db 100644 --- a/src/assets/QuizCreateMockData.ts +++ b/src/assets/QuizCreateMockData.ts @@ -1,4 +1,4 @@ -import { QuizClientContent } from '@/interfaces/Quiz'; +import type { QuizClientContent } from '@/interfaces/Quiz'; const QUIZ_ITEM_DEFAULT_STATE: QuizClientContent = { _id: 0, diff --git a/src/assets/QuizMockData.ts b/src/assets/QuizMockData.ts index 6a0e514..f9f3b95 100644 --- a/src/assets/QuizMockData.ts +++ b/src/assets/QuizMockData.ts @@ -1,4 +1,4 @@ -import { Quiz } from '@/interfaces/Quiz'; +import type { Quiz } from '@/interfaces/Quiz'; const QuizMockData: Quiz[] = [ { diff --git a/src/assets/RankingMockData.ts b/src/assets/RankingMockData.ts index 2b4b980..9adf82d 100644 --- a/src/assets/RankingMockData.ts +++ b/src/assets/RankingMockData.ts @@ -1,4 +1,4 @@ -import { UserAPI } from '@/interfaces/UserAPI'; +import type { UserAPI } from '@/interfaces/UserAPI'; const RankingMockData: UserAPI[] = [ { diff --git a/src/assets/UserInfoMockData.ts b/src/assets/UserInfoMockData.ts index 920c69b..5dbc4a3 100644 --- a/src/assets/UserInfoMockData.ts +++ b/src/assets/UserInfoMockData.ts @@ -1,4 +1,4 @@ -import { UserAPI } from '@/interfaces/UserAPI'; +import type { UserAPI } from '@/interfaces/UserAPI'; // API: GET /user/{userId} const UserInfoMockData: UserAPI = { diff --git a/src/components/Form/Button/styles.ts b/src/components/Form/Button/styles.ts index 32afc3e..5f078fd 100644 --- a/src/components/Form/Button/styles.ts +++ b/src/components/Form/Button/styles.ts @@ -1,3 +1,4 @@ +/* eslint-disable @emotion/syntax-preference */ import styled from '@emotion/styled'; // TODO: 전역 스타일 컬러 적용 diff --git a/src/components/Home/QuizSetCard/styles.tsx b/src/components/Home/QuizSetCard/styles.tsx index 96de69c..8df6e9f 100644 --- a/src/components/Home/QuizSetCard/styles.tsx +++ b/src/components/Home/QuizSetCard/styles.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @emotion/syntax-preference */ import styled from '@emotion/styled'; import { diff --git a/src/components/Quiz/index.tsx b/src/components/Quiz/index.tsx index 69f1723..50ce073 100644 --- a/src/components/Quiz/index.tsx +++ b/src/components/Quiz/index.tsx @@ -2,10 +2,10 @@ import { useState } from 'react'; import * as S from './styles'; -import type { Quiz as QuizInterface } from '@/interfaces/Quiz'; +import type { Quiz as QuizType } from '@/hooks/useQuiz/useQuiz.helper'; interface QuizProps { - quiz: QuizInterface; + quiz: QuizType; index: number; onChangeUserAnswer: (index: number, value: string) => void; } diff --git a/src/components/UserInfo/UserInfoCard/styles.tsx b/src/components/UserInfo/UserInfoCard/styles.tsx index 479e6de..d7c9583 100644 --- a/src/components/UserInfo/UserInfoCard/styles.tsx +++ b/src/components/UserInfo/UserInfoCard/styles.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @emotion/syntax-preference */ import styled from '@emotion/styled'; import { gray } from '@/styles/theme'; diff --git a/src/hooks/useQuiz/index.ts b/src/hooks/useQuiz/index.ts new file mode 100644 index 0000000..5c873d8 --- /dev/null +++ b/src/hooks/useQuiz/index.ts @@ -0,0 +1 @@ +export { default as useQuiz } from './useQuiz'; diff --git a/src/hooks/useQuiz/useQuiz.helper.ts b/src/hooks/useQuiz/useQuiz.helper.ts new file mode 100644 index 0000000..bdf262a --- /dev/null +++ b/src/hooks/useQuiz/useQuiz.helper.ts @@ -0,0 +1,89 @@ +import { getPostsFromChannel, getPosts } from '@/api/QuizServices'; + +import type { PostAPI } from '@/interfaces/PostAPI'; +import type { QuizContent } from '@/interfaces/Quiz'; + +export interface Quiz { + _id: string; + question: string; + answerDescription: string; + category: string; + difficulty: number; + importance: number; + answerType: 'trueOrFalse' | 'multipleChoice' | 'shortAnswer'; + answer: string; +} + +/** + * + * @param array 기존 소스로 사용되는 배열 + * @param count 섞은 뒤 반환할 개수 + * @returns 섞인 count의 길이를 갖는 배열 + */ +const shuffle = (array: T[], count: number): T[] => { + const ret = [...array]; + + for (let i = 0; i < array.length - 1; i += 1) { + const j = Math.floor(Math.random() * (i + 1)); + [ret[i], ret[j]] = [ret[j], ret[i]]; + } + + return ret.slice(0, count < ret.length ? count : ret.length); +}; + +export const calculateScore = (quizzes: Quiz[], userAnswers: string[]) => { + // 전부 선택하지 않았거나 user가 임의로 조작했다면 0점을 부여한다. + if (quizzes.length !== userAnswers.filter((answer) => answer).length) + return 0; + // filter corrected quizzes and add scores + return quizzes + .filter((quiz, index) => quiz.answer === userAnswers[index]) + .reduce((acc, cur) => acc + cur.difficulty * 10, 0); +}; + +/** + * Quiz 인터페이스를 구현하는 팩토리 함수 + */ +const createQuiz = (post: PostAPI): Quiz => { + const quizContent = JSON.parse(post.title) as QuizContent; + + return { + _id: post._id, + question: quizContent.question, + answerDescription: quizContent.answerDescription, + answer: quizContent.answer, + answerType: quizContent.answerType, + category: quizContent.category, + difficulty: quizContent.difficulty, + importance: quizContent.importance, + }; +}; + +/** + * QuizSolve에 사용되는 quiz use cases + */ +class QuizService { + static async getShuffledQuizzes(count: number) { + try { + const posts = await getPosts(); + const quizzes = posts.map((post) => createQuiz(post)); + + return shuffle(quizzes, count); + } catch (error) { + throw new Error('error occurred at QuizService.getShuffledQuizzes.'); + } + } + + static async getQuizzesFromQuizSet(channelId: string) { + try { + const posts = await getPostsFromChannel(channelId); + const quizzes = posts.map((post) => createQuiz(post)).reverse(); + + return quizzes; + } catch (error) { + throw new Error('error occurred at QuizService.getQuizzesFromQuizSet.'); + } + } +} + +export default QuizService; diff --git a/src/hooks/useQuiz/useQuiz.ts b/src/hooks/useQuiz/useQuiz.ts new file mode 100644 index 0000000..f66e4d6 --- /dev/null +++ b/src/hooks/useQuiz/useQuiz.ts @@ -0,0 +1,37 @@ +import { useCallback, useState } from 'react'; + +import QuizServices from './useQuiz.helper'; + +import type { Quiz } from './useQuiz.helper'; + +type ReturnType = [ + Quiz[], + (count: number) => Promise, + (channelId: string) => Promise +]; + +const useQuiz = (): ReturnType => { + const [quizzes, setQuizzes] = useState([]); + + const getRandomQuizzes = useCallback(async (count: number) => { + try { + const data = await QuizServices.getShuffledQuizzes(count); + setQuizzes(data); + } catch (error) { + console.error(error); + } + }, []); + + const getQuizzesFromQuizSet = useCallback(async (channelId: string) => { + try { + const data = await QuizServices.getQuizzesFromQuizSet(channelId); + setQuizzes(data); + } catch (error) { + console.error(error); + } + }, []); + + return [quizzes, getRandomQuizzes, getQuizzesFromQuizSet]; +}; + +export default useQuiz; diff --git a/src/hooks/useStorage/useLocalStorage.ts b/src/hooks/useStorage/useLocalStorage.ts index 4b92fd6..fd3d63e 100644 --- a/src/hooks/useStorage/useLocalStorage.ts +++ b/src/hooks/useStorage/useLocalStorage.ts @@ -1,10 +1,12 @@ -import useStorage, { ReturnTypes } from './useStorage'; +import useStorage from './useStorage'; + +import type { ReturnTypes } from './useStorage'; function useLocalStorage(key: string, defaultValue: T): ReturnTypes { const [value, setItem, removeItem] = useStorage( key, defaultValue, - 'localStorage', + 'localStorage' ); return [value, setItem, removeItem]; diff --git a/src/interfaces/CommentAPI.d.ts b/src/interfaces/CommentAPI.d.ts index 3d5c1be..510d785 100644 --- a/src/interfaces/CommentAPI.d.ts +++ b/src/interfaces/CommentAPI.d.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-cycle -import { UserAPI } from './UserAPI'; +import type { UserAPI } from './UserAPI'; export interface CommentAPI { _id: string; diff --git a/src/interfaces/NotificationAPI.d.ts b/src/interfaces/NotificationAPI.d.ts index 23b52dd..3e77a33 100644 --- a/src/interfaces/NotificationAPI.d.ts +++ b/src/interfaces/NotificationAPI.d.ts @@ -1,6 +1,6 @@ -import { CommentAPI } from './CommentAPI'; -import { LikeAPI } from './LikeAPI'; -import { UserAPI } from './UserAPI'; +import type { CommentAPI } from './CommentAPI'; +import type { LikeAPI } from './LikeAPI'; +import type { UserAPI } from './UserAPI'; export interface NotificationAPI { seen: boolean; diff --git a/src/interfaces/PostAPI.d.ts b/src/interfaces/PostAPI.d.ts index c73ab9f..2796d6d 100644 --- a/src/interfaces/PostAPI.d.ts +++ b/src/interfaces/PostAPI.d.ts @@ -1,8 +1,8 @@ /* eslint-disable import/no-cycle */ -import { ChannelAPI } from './ChannelAPI'; -import { CommentAPI } from './CommentAPI'; -import { LikeAPI } from './LikeAPI'; -import { UserAPI } from './UserAPI'; +import type { ChannelAPI } from './ChannelAPI'; +import type { CommentAPI } from './CommentAPI'; +import type { LikeAPI } from './LikeAPI'; +import type { UserAPI } from './UserAPI'; /** * ANCHOR: title에는 stringify된 QuizContent interface 정보가 들어감 * TODO: Like, Comment, Channel interface 추가 diff --git a/src/interfaces/Quiz.d.ts b/src/interfaces/Quiz.d.ts index 00258fc..4499589 100644 --- a/src/interfaces/Quiz.d.ts +++ b/src/interfaces/Quiz.d.ts @@ -1,4 +1,4 @@ -import { PostAPIBase } from './PostAPI'; +import type { PostAPIBase } from './PostAPI'; export interface QuizContent { question: string; diff --git a/src/interfaces/UserAPI.d.ts b/src/interfaces/UserAPI.d.ts index 241f494..2e3569e 100644 --- a/src/interfaces/UserAPI.d.ts +++ b/src/interfaces/UserAPI.d.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-cycle */ -import { CommentAPI } from './CommentAPI'; -import { PostAPI } from './PostAPI'; +import type { CommentAPI } from './CommentAPI'; +import type { PostAPI } from './PostAPI'; export interface UserInfo { _id: string; diff --git a/src/pages/QuizResultPage/index.tsx b/src/pages/QuizResultPage/index.tsx index d891234..7c79053 100644 --- a/src/pages/QuizResultPage/index.tsx +++ b/src/pages/QuizResultPage/index.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; -import { Redirect, useHistory } from 'react-router'; +import { Redirect } from 'react-router'; import * as QuizServices from '@/api/QuizServices'; import Header from '@/components/Header'; @@ -22,7 +22,6 @@ import type { Quiz as QuizInterface } from '@/interfaces/Quiz'; * 4. random인지, random인지 아닌지 저장해야 한다. */ const QuizResultPage = () => { - const history = useHistory(); const { user, isAuth } = useAuthContext(); const [quizzes, setQuizzes] = useState([]); const [postIds] = useSessionStorage(POST_IDS, []); @@ -37,11 +36,31 @@ const QuizResultPage = () => { return true; }; + // NOTE: 임시 콜백 함수입니다. QuizSolve가 정리된 후 해당 부분 처리 예정입니다. + const getQuizzesFromIds = useCallback(async (_postIds: string[]) => { + try { + const data = await QuizServices.getQuizzesFromPostIds(_postIds); + setQuizzes(data); + } catch (error) { + console.error(error); + } + }, []); + useEffect(() => { - QuizServices.getQuizzesFromPostIds(postIds) - .then((quizArray) => setQuizzes(quizArray)) - .finally(() => setLoading(false)); - }, [history, postIds, userAnswers.length]); + const fetchData = async () => { + try { + setLoading(true); + await getQuizzesFromIds(postIds); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchData(); + }, [getQuizzesFromIds, postIds]); if (loading) return null; if (!isAppropriateAccess()) { diff --git a/src/pages/QuizSolvePage/index.tsx b/src/pages/QuizSolvePage/index.tsx index 7f68b9d..f42f4eb 100644 --- a/src/pages/QuizSolvePage/index.tsx +++ b/src/pages/QuizSolvePage/index.tsx @@ -7,18 +7,18 @@ import 'slick-carousel/slick/slick-theme.css'; import 'slick-carousel/slick/slick.css'; import { v4 } from 'uuid'; -import * as QuizServices from '@/api/QuizServices'; import { updateTotalPoint } from '@/api/UserServices'; import Icon from '@/components/Icon'; import { POINTS, POST_IDS, USER_ANSWERS } from '@/constants'; import { useAuthContext } from '@/contexts/AuthContext'; import { useQuizContext } from '@/contexts/QuizContext'; import Quiz from '@components/Quiz'; +import { useQuiz } from '@hooks/useQuiz'; +import { calculateScore } from '@hooks/useQuiz/useQuiz.helper'; import SliderButton from './SliderButton'; import * as S from './styles'; -import type { Quiz as QuizInterface } from '@/interfaces/Quiz'; import type { UserQuizInfo } from '@/interfaces/UserAPI'; const QuizSolvePage = () => { @@ -28,8 +28,8 @@ const QuizSolvePage = () => { const { channelId, randomQuizCount, setChannelId, setRandomQuizCount } = useQuizContext(); - const [quizzes, setQuizzes] = useState([]); - const [userAnswers, setUserAnswers] = useState([]); + const [quizzes, getRandomQuizzes, getQuizzesFromQuizSet] = useQuiz(); + const [userAnswers, setUserAnswers] = useState(Array(10).fill('')); const [currentIndex, setCurrentIndex] = useState(0); const [loading, setLoading] = useState(true); @@ -41,7 +41,7 @@ const QuizSolvePage = () => { const updateUserPoint = useCallback(async () => { try { - const totalPoint = QuizServices.caculateScore(quizzes, userAnswers); + const totalPoint = calculateScore(quizzes, userAnswers); sessionStorage.setItem(POINTS, JSON.stringify(totalPoint)); const newInfo = { @@ -123,22 +123,29 @@ const QuizSolvePage = () => { sessionStorage.removeItem(USER_ANSWERS); sessionStorage.removeItem(POINTS); - const next = (quizArray: QuizInterface[]) => { - setQuizzes(quizArray); - setUserAnswers(Array(quizArray.length).fill('')); + const fetchData = async () => { + try { + if (randomQuizCount && randomQuizCount > 0) { + await getRandomQuizzes(randomQuizCount); + } else if (channelId) { + await getQuizzesFromQuizSet(channelId); + } + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } }; - (async () => { - if (randomQuizCount && randomQuizCount > 0) - await QuizServices.getShuffledQuizzes(randomQuizCount).then( - (quizArray) => next(quizArray) - ); - else if (channelId) - await QuizServices.getQuizzesFromChannel(channelId).then((quizArray) => - next(quizArray) - ); - })().finally(() => setLoading(false)); - }, [channelId, quizzes.length, randomQuizCount, setUserAnswers]); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fetchData(); + }, [ + channelId, + getQuizzesFromQuizSet, + getRandomQuizzes, + randomQuizCount, + setUserAnswers, + ]); if (loading) return null; if (!(channelId || randomQuizCount)) {