Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] useQuiz hook 구현 #170

Closed
wants to merge 12 commits into from
87 changes: 9 additions & 78 deletions src/api/QuizServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,6 @@ import type { ChannelAPI } from '@/interfaces/ChannelAPI';
import type { PostAPI } from '@/interfaces/PostAPI';
import type { Quiz } from '@/interfaces/Quiz';

function shuffle<T = unknown>(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<ChannelAPI[]>('/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) =>
Expand All @@ -38,18 +19,14 @@ function getPostsFromPostIds(postIds: string[]) {
);
}

function getPosts() {
return api
.get<PostAPI[]>('/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<PostAPI[]>('/posts');
return response.data;
} catch (error) {
throw new Error('error occurred at getPosts.');
}
};

function parseQuiz(post: PostAPI) {
const postCopy: Partial<PostAPI> = { ...post };
Expand All @@ -58,36 +35,12 @@ function parseQuiz(post: PostAPI) {
return { ...postCopy, ...JSON.parse(quizContent) } as Quiz;
}

function getPostsFromChannel(channelId: string): Promise<PostAPI[]> {
export function getPostsFromChannel(channelId: string): Promise<PostAPI[]> {
return api
.get<PostAPI[]>(`/posts/channel/${channelId}`)
.then((response) => response.data);
}

/**
* @deprecated
*/
export function getPostIdsFromChannel(channelName: string): Promise<string[]> {
return api
.get<ChannelAPI>(`/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<Quiz[]> {
return getPostsFromPostIds(postIds)
.then((response) => response.map((post) => parseQuiz(post)))
Expand All @@ -96,28 +49,6 @@ export function getQuizzesFromPostIds(postIds: string[]): Promise<Quiz[]> {
});
}

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<ChannelAPI[]>('/channels')
Expand Down
2 changes: 1 addition & 1 deletion src/assets/QuizCreateMockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { QuizClientContent } from '@/interfaces/Quiz';
import type { QuizClientContent } from '@/interfaces/Quiz';

const QUIZ_ITEM_DEFAULT_STATE: QuizClientContent = {
_id: 0,
Expand Down
2 changes: 1 addition & 1 deletion src/assets/QuizMockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Quiz } from '@/interfaces/Quiz';
import type { Quiz } from '@/interfaces/Quiz';

const QuizMockData: Quiz[] = [
{
Expand Down
2 changes: 1 addition & 1 deletion src/assets/RankingMockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserAPI } from '@/interfaces/UserAPI';
import type { UserAPI } from '@/interfaces/UserAPI';

const RankingMockData: UserAPI[] = [
{
Expand Down
2 changes: 1 addition & 1 deletion src/assets/UserInfoMockData.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserAPI } from '@/interfaces/UserAPI';
import type { UserAPI } from '@/interfaces/UserAPI';

// API: GET /user/{userId}
const UserInfoMockData: UserAPI = {
Expand Down
1 change: 1 addition & 0 deletions src/components/Form/Button/styles.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @emotion/syntax-preference */
import styled from '@emotion/styled';

// TODO: 전역 스타일 컬러 적용
Expand Down
1 change: 1 addition & 0 deletions src/components/Home/QuizSetCard/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @emotion/syntax-preference */
import styled from '@emotion/styled';

import {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Quiz/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2
QuizQuizType 으로 네이밍을 하신 이유가 궁금합니다


interface QuizProps {
quiz: QuizInterface;
quiz: QuizType;
index: number;
onChangeUserAnswer: (index: number, value: string) => void;
}
Expand Down
1 change: 1 addition & 0 deletions src/components/UserInfo/UserInfoCard/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @emotion/syntax-preference */
import styled from '@emotion/styled';

import { gray } from '@/styles/theme';
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useQuiz/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useQuiz } from './useQuiz';
89 changes: 89 additions & 0 deletions src/hooks/useQuiz/useQuiz.helper.ts
Original file line number Diff line number Diff line change
@@ -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 = <T = unknown>(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.');
}
}
}
Comment on lines +65 to +87
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분에 대한 질문 답변드리기 전에 질문이 이해가 안 가네요 ㅠ
"일단 유즈케이스와 관련이 있기 때문에 나중에 데이터 스키마 형태로 사용할 수 있을 것 같아서 설정했다" 는 말이 정확히 무슨 말인가요?

질문과 별개로 리뷰를 드리자면
이 코드에서 비동기 로직을 분리할 수 있을 것 같습니다
이 부분에 대해서 어떻게 생각하시나요?


export default QuizService;
37 changes: 37 additions & 0 deletions src/hooks/useQuiz/useQuiz.ts
Original file line number Diff line number Diff line change
@@ -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<void>,
(channelId: string) => Promise<void>
];
Comment on lines +7 to +11
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1
리턴 타입을 배열로 해두면 배열 인덱스에 종속되어 일부만 사용하는 경우에 문제가 생길 것 같습니다
객체 타입으로 변경하는 것은 어떤가요?


const useQuiz = (): ReturnType => {
const [quizzes, setQuizzes] = useState<Quiz[]>([]);

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;
6 changes: 4 additions & 2 deletions src/hooks/useStorage/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import useStorage, { ReturnTypes } from './useStorage';
import useStorage from './useStorage';

import type { ReturnTypes } from './useStorage';

function useLocalStorage<T>(key: string, defaultValue: T): ReturnTypes<T> {
const [value, setItem, removeItem] = useStorage(
key,
defaultValue,
'localStorage',
'localStorage'
);

return [value, setItem, removeItem];
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/CommentAPI.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/NotificationAPI.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
8 changes: 4 additions & 4 deletions src/interfaces/PostAPI.d.ts
Original file line number Diff line number Diff line change
@@ -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 추가
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces/Quiz.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PostAPIBase } from './PostAPI';
import type { PostAPIBase } from './PostAPI';

export interface QuizContent {
question: string;
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/UserAPI.d.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Loading