Skip to content

Commit

Permalink
Merge pull request #45 from YAPP-Github/feature/quiz-30
Browse files Browse the repository at this point in the history
[ Feature/quiz 30 ] 문제풀이 단위 컴포넌트 및 테스트 코드 개발
  • Loading branch information
Happhee authored Jun 14, 2024
2 parents d1a957c + a302993 commit d0efa5a
Show file tree
Hide file tree
Showing 49 changed files with 811 additions and 247 deletions.
4 changes: 4 additions & 0 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import localFont from "next/font/local";

import { Suspense } from "react";

import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

import QueryClientProviders from "@shared/components/queryClientProvider";
import { cn } from "@shared/utils/cn";

Expand Down Expand Up @@ -63,6 +65,8 @@ export default function RootLayout({
<MSWProviders>
<Suspense>{children}</Suspense>
</MSWProviders>

<ReactQueryDevtools />
</body>
</html>
</QueryClientProviders>
Expand Down
22 changes: 22 additions & 0 deletions src/app/problem/[problemId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";

import AnswerChoiceList from "@problem/components/AnswerChoiceList";
import AnswerSubmitButton from "@problem/components/AnswerSubmitButton";
import ProblemTitle from "@problem/components/ProblemTitle";
import TagList from "@problem/components/TagList";
import { ProblemProvider } from "@problem/context/problemContext";

export default function ProblemPage() {
return (
<ProblemProvider>
<>
<div className="flex h-full flex-col">
<TagList />
<ProblemTitle />
<AnswerChoiceList />
</div>
<AnswerSubmitButton />
</>
</ProblemProvider>
);
}
File renamed without changes.
18 changes: 0 additions & 18 deletions src/app/quiz/page.tsx

This file was deleted.

8 changes: 8 additions & 0 deletions src/common/remotes/testQueryClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient();
export const testQueryClientWrapper = ({
children,
}: React.PropsWithChildren) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
3 changes: 3 additions & 0 deletions src/common/types/constKeyObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type ConstKeyObject<Key extends string | number | symbol, Value> = {
[key in Key]: Value;
};
10 changes: 7 additions & 3 deletions src/mocks/MSWProviders.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
"use client";

import { PropsWithChildren, useEffect, useState } from "react";
import { PropsWithChildren, useEffect, useRef, useState } from "react";

const isMockingMode = process.env.NEXT_PUBLIC_API_MOCKING === "enable";

export default function MSWProviders({ children }: PropsWithChildren) {
const [mswReady, setMSWReady] = useState(() => !isMockingMode);
const isMockingModeRef = useRef(!isMockingMode);

useEffect(
function setMswMocks() {
const init = async () => {
if (isMockingMode) {
if (isMockingMode && !isMockingModeRef.current) {
isMockingModeRef.current = true;
const initMocks = await import("@mocks/index").then(
(res) => res.initMocks,
);
await initMocks();
setMSWReady(true);
}
};
if (!mswReady) init();
if (!mswReady) {
init();
}
},
[mswReady],
);
Expand Down
23 changes: 16 additions & 7 deletions src/mocks/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,31 @@ import { getWorkbookId } from "@workbook/utils";

import response from "./response";

export const quizHandler = http.get(apiRoutes.quiz, () =>
HttpResponse.json(response[apiRoutes.quiz]),
);
export const tagsHandler = http.get(apiRoutes.tags, () =>
HttpResponse.json(response[apiRoutes.tags]),
);

export const problemsHandler = http.get(apiRoutes.problems, ({ request }) => {
const url = new URL(request.url);
const articleId = url.searchParams.get("articleId");
export const problemsHandler = http.get(apiRoutes.problems, ({ params }) => {
const articleId = params?.articleId;
if (!articleId) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(response[apiRoutes.problems]);
});

export const submitAnswerHandler = http.post(
apiRoutes.submitAnswer,
async ({ request, params }) => {
const problemId = params?.problemId;
const result: any = await request.json();
const choiceAns = result?.choiceAns;

if (!choiceAns && problemId) {
return new HttpResponse(null, { status: 404 });
}
return HttpResponse.json(response[apiRoutes.submitAnswer]);
},
);
export const workbookHandler = http.get(apiRoutes.workbook, ({ request, params }) => {
const workbookId = params

Expand All @@ -37,8 +46,8 @@ export const workbookHandler = http.get(apiRoutes.workbook, ({ request, params }
});

export const handlers = [
quizHandler,
tagsHandler,
problemsHandler,
submitAnswerHandler,
workbookHandler,
];
3 changes: 2 additions & 1 deletion src/mocks/response/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { apiRoutes } from "@shared/constants/apiRoutes";

import problems from "./problems.json";
import quiz from "./quiz.json";
import submitAnswer from "./submitAnswer.json";
import tags from "./tags.json";
import workbook from "./workbook.json";

// eslint-disable-next-line import/no-anonymous-default-export
export default {
[apiRoutes.quiz]: quiz,
[apiRoutes.tags]: tags,
[apiRoutes.problems]: problems,
[apiRoutes.submitAnswer]: submitAnswer,
[apiRoutes.workbook]: workbook,
};
43 changes: 20 additions & 23 deletions src/mocks/response/problems.json
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
{
"message": "String",
"data": {
"problems": [
"id": 1,
"title": "ETF(상장지수펀드)의 특징이 아닌것은?",
"day": "Day1",
"contents": [
{
"id": 1,
"title": "ETF(상장지수펀드)의 특징이 아닌것은?",
"contents": [
{
"number": 1,
"content": "분산투자"
},
{
"number": 2,
"content": "높은 운용 비용"
},
{
"number": 3,
"content": "유동성"
},
{
"number": 4,
"content": "투명성"
}
],
"creatorId": 1
"number": 1,
"content": "분산투자"
},
{
"number": 2,
"content": "높은 운용 비용"
},
{
"number": 3,
"content": "유동성"
},
{
"number": 4,
"content": "투명성"
}
]
],
"creatorId": 1
}
}
5 changes: 5 additions & 0 deletions src/mocks/response/submitAnswer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"explanation": "ETF는 일반적으로 낮은 운용 비용을 특징으로 합니다.이는 ETF가 보통 지수 추종(passive management) 방식으로 운용되기 때문입니다. 지수를 추종하는 전략은 액티브 매니지먼트(active management)에 비해 관리가 덜 복잡하고, 따라서 비용이 낮습니다.",
"isSolved": true,
"answer": "높은 운용 비용"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import AnswerChoiceButton from ".";
import { Meta, StoryObj } from "@storybook/react";

const meta = {
component: AnswerChoiceButton,
} satisfies Meta<typeof AnswerChoiceButton>;
export default meta;

type Story = StoryObj<typeof meta>;

// TODO : msw 사용해서 다르게 보여주는 story 작성 필요
export const InitChoiceAnswer = {
args: {
// className: ANSWER_CHOICHE_BUTTON_INFO.INIT_CHOICE_ANSWER.className,
content: "유동성",
},
} satisfies Story;

export const CurrentChoiceAnswer = {
args: {
// className: ANSWER_CHOICHE_BUTTON_INFO.CURRENT_CHOICE_ANSWER.className,
content: "유동성",
},
} satisfies Story;

export const ChoiceAnswerCorrect = {
args: {
// className: ANSWER_CHOICHE_BUTTON_INFO.CHOICE_ANSWER_CORRECT.className,
content: "유동성",
},
} satisfies Story;

export const ChoiceAnswerFail = {
args: {
// className: ANSWER_CHOICHE_BUTTON_INFO.CHOICE_ANSWER_FAIL.className,
content: "유동성",
},
} satisfies Story;
98 changes: 98 additions & 0 deletions src/problem/components/AnswerChoiceButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useContext, useEffect, useState } from "react";

import { useMutationState } from "@tanstack/react-query";

import { Button } from "@shared/components/ui/button";
import { cn } from "@shared/utils/cn";

import ChoiceFillCircleSvg from "../ChoiceFillCircleSvg";
import { ANSWER_CHOICHE_BUTTON_INFO } from "@problem/constants/problemInfo";
import ProblemContext from "@problem/context/problemContext";
import { AnswerCheckInfo, AnswerChoiceInfo } from "@problem/types/problemInfo";

interface AnswerChoiceButtonProps extends Pick<AnswerChoiceInfo, "content"> {}

export default function AnswerChoiceButton({
content,
}: AnswerChoiceButtonProps) {
const [className, setClassName] = useState(
ANSWER_CHOICHE_BUTTON_INFO.INIT_CHOICE_ANSWER.className,
);
const {
states: { choiceAnswer },
actions: { updateChoiceAnswer },
} = useContext(ProblemContext);

const problemAnswerInfo = useMutationState({
filters: {
mutationKey: ["get-problem-answer"],
},
select: (mutation) => mutation.state.data as AnswerCheckInfo,
});

const onClickAnswerChoice = () => {
if (problemAnswerInfo.length === 0) updateChoiceAnswer(content);
};

useEffect(
function setButtonClassName() {
if (!problemAnswerInfo.length) {
if (choiceAnswer === content)
setClassName(
ANSWER_CHOICHE_BUTTON_INFO.CURRENT_CHOICE_ANSWER.className,
);

if (choiceAnswer !== content)
setClassName(ANSWER_CHOICHE_BUTTON_INFO.INIT_CHOICE_ANSWER.className);
}

if (problemAnswerInfo.length) {
const problemAnswerData = problemAnswerInfo[0];
if (problemAnswerData) {
if (problemAnswerData.answer === content)
setClassName(
ANSWER_CHOICHE_BUTTON_INFO.CHOICE_ANSWER_CORRECT.className,
);
if (
problemAnswerData.isSolved === false &&
choiceAnswer === content
) {
setClassName(
ANSWER_CHOICHE_BUTTON_INFO.CHOICE_ANSWER_FAIL.className,
);
}
}
}
},
[choiceAnswer, content, problemAnswerInfo],
);

if (!problemAnswerInfo) return <div>정답제출 실패</div>;
const problemAnswerData = problemAnswerInfo[0];

return (
<Button
className={cn(
"flex w-full justify-between rounded-s border-[1px] border-text-gray3 px-3",
className,
)}
onClick={onClickAnswerChoice}
>
<span className="sub2-bold">{content}</span>
<ChoiceFillCircleSvg
fill={
(!problemAnswerData && choiceAnswer === content && "white") ||
(!problemAnswerData && choiceAnswer !== content && "#A5A5A5") ||
(problemAnswerData &&
problemAnswerData.answer === content &&
"#0166B3") ||
(problemAnswerData &&
problemAnswerData.isSolved === false &&
choiceAnswer === content &&
"#B00020") ||
"#A5A5A5"
}
/>
</Button>
);
}
Loading

0 comments on commit d0efa5a

Please sign in to comment.