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

[ Feature/dev 97 ] 퀴즈에서 아티클로 다시 넘어가기 #165

Merged
merged 9 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/app/article/[articleId]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ export default function ArticlePageLayout({
return (
<main className="flex h-auto w-full">
<section className="flex h-auto w-full flex-col justify-between">
<div className="mx-[20px] mb-[10px] flex flex-col">
<TopBar />
{children}
</div>
<TopBar className="px-[20px]" />
<div className="mx-[20px] mb-[10px] flex flex-col">{children}</div>
<ArticleBottomButton title={"문제풀기"} />
</section>
</main>
Expand Down
6 changes: 5 additions & 1 deletion src/app/problem/[problemId]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AnswerSubmitButton from "@problem/components/AnswerSubmitButton";
import BackToArticle from "@problem/components/BackToArticle";
import ProblemCompleteDialog from "@problem/components/ProblemCompleteDialog";
import ProblemTopbar from "@problem/components/ProblemTopbar";
import { ProblemProvider } from "@problem/context/problemContext";
Expand All @@ -16,7 +17,10 @@ export default function ProblemLayout({ children }: ProblemLayoutProps) {
<ProblemTopbar />
{children}
</div>
<AnswerSubmitButton />
<div className="flex flex-col gap-y-[24px]">
<BackToArticle />
<AnswerSubmitButton />
</div>
<ProblemCompleteDialog />
</section>
</ProblemProvider>
Expand Down
1 change: 1 addition & 0 deletions src/app/problem/[problemId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AnswerChoiceList from "@problem/components/AnswerChoiceList";
import ArticleDropDownWrapper from "@problem/components/ArticleDropDownWrapper";
import LottieWithContext from "@problem/components/LottieWithContext";
import ProblemExplanation from "@problem/components/ProblemExplanation";
import ProblemTagList from "@problem/components/ProblemTagList";
Expand Down
5 changes: 5 additions & 0 deletions src/app/problem/[problemId]/problemFirstIdpage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { render, renderHook, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ProblemLayout from "./layout";
import ProblemPage from "./page";
import { BACK_TO_ARTICLE_WORDS } from "@problem/constants/backToArticle";

const isExistNextProblem = vi.fn(() => true);
const nextSetProblemId = vi.fn(() => "2");
Expand Down Expand Up @@ -94,6 +95,8 @@ describe("첫 번째 문제풀기 페이지 테스트", () => {
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(screen.getByText(BACK_TO_ARTICLE_WORDS.BEFORE)).toBeInTheDocument();
});

it("정답 선택 이후 정답 제출하기 버튼 클릭 시, 해설 컴포넌트 잘 노출되고, 다음 문제로 넘어가기 확인", async () => {
Expand All @@ -118,6 +121,8 @@ describe("첫 번째 문제풀기 페이지 테스트", () => {
expect(problemExplanation.childElementCount).toBe(2);
const explanationParagraphy = screen.getByRole("paragraph");

expect(screen.getByText(BACK_TO_ARTICLE_WORDS.AFTER)).toBeInTheDocument();

expect(explanationParagraphy.textContent).toBe(
"제임스 와트는 증기를 이용하여 공기를 따뜻하게 만드는 라디에이터를 만들었습니다.",
);
Expand Down
6 changes: 6 additions & 0 deletions src/app/problem/[problemId]/problemLastIdPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import userEvent from "@testing-library/user-event";
import ProblemLayout from "./layout";
import ProblemPage from "./page";
import { BACK_TO_ARTICLE_WORDS } from "@problem/constants/backToArticle";

const isExistNextProblem = vi.fn(() => false);
const clearProblem = vi.fn();
Expand Down Expand Up @@ -119,6 +120,8 @@ describe("마지막 문제 풀이 페이지 테스트", () => {
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(screen.getByText(BACK_TO_ARTICLE_WORDS.BEFORE)).toBeInTheDocument();
});
it("정답 선택 이후 정답 제출하기 버튼 클릭 시, 해설 컴포넌트 잘 노출되고, 로띠 재생이후 메인으로 넘어가기", async () => {
const { result } = renderHook(
Expand Down Expand Up @@ -146,6 +149,9 @@ describe("마지막 문제 풀이 페이지 테스트", () => {
expect(explanationParagraphy.textContent).toBe(
"온돌은 바닥 아래에 연기가 지나가는 길이 있는 반면, 하이포코스트는 바닥 아래가 거의 다 뚫려있는 형태입니다.",
);

expect(screen.getByText(BACK_TO_ARTICLE_WORDS.AFTER)).toBeInTheDocument();

act(() => {
vi.advanceTimersByTime(5000);
});
Expand Down
4 changes: 1 addition & 3 deletions src/app/workbook/[id]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export default async function WorkbookLayout({
<HydrationBoundary state={state}>
<section className="flex h-auto w-full flex-col justify-between">
<div className="mb-[10px] flex flex-col">
<div className="mx-[20px]">
<TopBar />
</div>
<TopBar className="px-[20px]" />
{children}
</div>
</section>
Expand Down
16 changes: 16 additions & 0 deletions src/common/components/CancelButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { XIcon } from "lucide-react";
import { HTMLAttributes } from "react";

interface CancelButtonProps extends HTMLAttributes<HTMLDivElement> {
handleToggle: () => void;
}

export default function CancelButton({ handleToggle, ...props }: CancelButtonProps) {
const { className } = props

return (
<div className={className}>
<XIcon data-testid="x-menu" width={36} height={36} className="mr-[23px]" onClick={handleToggle} />
</div>
)
}
2 changes: 1 addition & 1 deletion src/main/components/MainHeader/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function MainHeader() {
return (
<header
className={cn(
"relative flex h-[66px] w-full items-center justify-between",
"relative flex h-[66px] w-full items-center justify-between sticky top-0 z-[9999]",
toggleMenu ? "bg-white" : "bg-main",
)}
>
Expand Down
1 change: 1 addition & 0 deletions src/main/components/WorkbookCardDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const WorkbookDetailInfoWrapper = ({
"flex flex-col",
"rounded-b-lg bg-black",
"px-[21px] pb-[25px] pt-[23px]",
"h-[210px]"
)}
>
{children}
Expand Down
14 changes: 14 additions & 0 deletions src/problem/components/ArticleDropDown/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use client";

import { useProblemIdsViewModel } from "@shared/models/useProblemIdsViewModel";
import ProblemArticleTemplate from "../ProblemArticleTemplate";

export function ArticleDropDown() {
const { getArticleId } = useProblemIdsViewModel();

return (
<div className="absolute left-0 top-[66px] z-20 h-screen w-full bg-white overflow-y-scroll px-[20px] pb-[100px]">
<ProblemArticleTemplate articleId={getArticleId()} />
</div>
);
}
28 changes: 28 additions & 0 deletions src/problem/components/ArticleDropDownWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ArticleDropDown } from "../ArticleDropDown";
import CancelButton from "@common/components/CancelButton";

interface ArticleDropDownWrapperProps {
toggleArticle: boolean;
handleToggleArticle: () => void;
}

export default function ArticleDropDownWrapper({
toggleArticle,
handleToggleArticle,
}: ArticleDropDownWrapperProps) {
return (
<>
{toggleArticle && (
Copy link
Collaborator

Choose a reason for hiding this comment

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

외부에서 제어하지 않고 articleDropdown wrapper에서 제어하신 이유가 있을까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ArticleDropDownWrapper 하위에 있는 ArticleDropDown 은 toggle 에 영향을 받지 않도록 구성하고 싶어서 wrapper 로 하나 더 감쌌습니다!!

<div className="fixed inset-0 z-50 flex justify-center bg-white">
<div className="relative w-full max-w-[480px] bg-white">
<CancelButton
handleToggle={handleToggleArticle}
className="absolute right-0 top-0 p-4"
/>
<ArticleDropDown />
</div>
</div>
)}
</>
);
}
62 changes: 62 additions & 0 deletions src/problem/components/BackToArticle/BackToArticle.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { beforeAll, beforeEach,describe, expect, it, vi } from "vitest";

import BackToArticle from "./";
import { BACK_TO_ARTICLE_WORDS } from "@problem/constants/backToArticle";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import QueryClientProviders from "@shared/components/queryClientProvider";

// Mocking the useParams hook from next/navigation
vi.mock("next/navigation", () => ({
useParams: vi.fn(),
}));

const renderWithQueryClient = () => {
return render(
<QueryClientProviders>
<BackToArticle />
</QueryClientProviders>,
);
};

describe("BackToArticle Component 동작 테스트", () => {
beforeAll(() => {
vi.mock("next/navigation", async () => {
const actual =
await vi.importActual<typeof import("next/navigation")>(
"next/navigation",
);
return {
...actual,
useParams: vi.fn(() => ({
problemId: "1",
})),
};
});
});

beforeEach(() => {
renderWithQueryClient();
});

it("아티클 다시보기 버튼 클릭 시 문제에 해당하는 아티클이 보인다.", async () => {

expect(screen.queryByText(BACK_TO_ARTICLE_WORDS.ARTICLE)).toBeInTheDocument();

const articleLink = screen.getByText(BACK_TO_ARTICLE_WORDS.ARTICLE);
await userEvent.click(articleLink);

expect(screen.getByTestId("x-menu")).toBeInTheDocument();
});

it("X 버튼 클릭 시 아티클 뷰가 없어진다.", async () => {

const articleLink = screen.getByText(BACK_TO_ARTICLE_WORDS.ARTICLE);
await userEvent.click(articleLink);

const cancelButton = screen.getByTestId("x-menu");
await userEvent.click(cancelButton);

expect(screen.queryByTestId("x-menu")).not.toBeInTheDocument();
});
});
74 changes: 74 additions & 0 deletions src/problem/components/BackToArticle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client";
import { useParams } from "next/navigation";

import { HTMLAttributes, useState } from "react";

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

import { ApiResponse } from "@api/fewFetch";

import { cn } from "@shared/utils/cn";

import { BACK_TO_ARTICLE_WORDS } from "@problem/constants/backToArticle";
import { QUERY_KEY } from "@problem/remotes/api";
import {
AnswerCheckInfo,
ProblemAnswerBody,
ProblemAnswerMuationState,
} from "@problem/types/problemInfo";
import { Button } from "@shared/components/ui/button";
import ArticleDropDownWrapper from "../ArticleDropDownWrapper";
import { AnswerStatusModel } from "@problem/models/AnswerStatusModel";

interface BackToArticleProps extends HTMLAttributes<HTMLDivElement> {}

export default function BackToArticle({ className }: BackToArticleProps) {
const [toggleArticle, setToggleArticle] = useState(false);

const handleToggleArticle = () => {
setToggleArticle((prev) => !prev);
};

const { problemId } = useParams<{ problemId: string }>();
const problemAnswersInfo = useMutationState<ProblemAnswerMuationState>({
filters: {
mutationKey: [QUERY_KEY.POST_PROBLEM_ANSWER, problemId],
},
select: (mutation) => {
return {
data: mutation.state.data as ApiResponse<AnswerCheckInfo>,
variables: mutation.state.variables as ProblemAnswerBody,
};
},
});

const answerStatus = new AnswerStatusModel({
problemAnswerInfo: problemAnswersInfo[0],
});

const backToArticleWords = answerStatus.problemSolvedStatus

return (
<div
className={cn(
"flex flex-row space-x-[3px] relative",
className,
!answerStatus.isProblemAnswerInfo && "mt-[91px]",
)}
>
<ArticleDropDownWrapper
toggleArticle={toggleArticle}
handleToggleArticle={handleToggleArticle}
/>
<span className="text-sm font-medium text-black">
{backToArticleWords}
</span>
<span
onClick={handleToggleArticle}
className="cursor-pointer text-sm font-bold text-main underline"
>
{BACK_TO_ARTICLE_WORDS.ARTICLE}
</span>
</div>
);
}
43 changes: 43 additions & 0 deletions src/problem/components/ProblemArticleTemplate/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

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

import ArticleSkeleton from "@article/components/ArticleSkeleton";
import { ARTICLE_INFO_TYPE } from "@article/constants/articleCase";
import { getArticleQueryOptions } from "@article/remotes/getArticleQueryOptions";

interface ProblemArticleTemplateProps {
articleId: string
}

export default function ProblemArticleTemplate({ articleId }: ProblemArticleTemplateProps) {

const results = useQueries({
queries: [
{
...getArticleQueryOptions({ articleId }),
staleTime: Infinity,
},
],
});
const {
data: articleInfo,
isLoading,
isError,
} = results[ARTICLE_INFO_TYPE.ONLY_ARTICLE];

if (isLoading || isError || !articleInfo)
return <ArticleSkeleton.EmailContentTemplateSkeleton />;

const { content } = articleInfo;

return (
<table>
<tbody>
<tr style={{}}>
<td dangerouslySetInnerHTML={{ __html: content }}></td>
</tr>
</tbody>
</table>
);
}
2 changes: 1 addition & 1 deletion src/problem/components/ProblemTopbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ export default function ProblemTopbar() {
back();
};

return <TopBar onClick={handleBackClick} />;
return <TopBar onClick={handleBackClick} className="z-[50]" />;
}
5 changes: 5 additions & 0 deletions src/problem/constants/backToArticle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const BACK_TO_ARTICLE_WORDS = {
BEFORE: "정답을 모르겠다면?",
AFTER: "복습을 하고 싶다면?",
ARTICLE: "아티클 다시보기"
}
24 changes: 24 additions & 0 deletions src/problem/models/AnswerStatusModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { BACK_TO_ARTICLE_WORDS } from "@problem/constants/backToArticle";
import { ProblemAnswerMuationState } from "@problem/types/problemInfo";

export class AnswerStatusModel {
constructor({
problemAnswerInfo,
}: {
problemAnswerInfo: ProblemAnswerMuationState | undefined;
}) {
this.problemAnswerInfo = problemAnswerInfo;
}

get problemSolvedStatus() {
return this.problemAnswerInfo
? BACK_TO_ARTICLE_WORDS.AFTER
: BACK_TO_ARTICLE_WORDS.BEFORE;
}

get isProblemAnswerInfo() {
return Boolean(this.problemAnswerInfo);
}

private problemAnswerInfo: ProblemAnswerMuationState | undefined;
}
Loading
Loading