Skip to content

Commit

Permalink
Merge pull request #168 from YAPP-Github/feature/DEV-97
Browse files Browse the repository at this point in the history
[ Feature/dev 97 -> Main ]
  • Loading branch information
soomin9106 authored Aug 30, 2024
2 parents bf3770f + 238091f commit 0593008
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 13 deletions.
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 @@ -18,7 +18,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 @@ -50,6 +50,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 && (
<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

0 comments on commit 0593008

Please sign in to comment.