diff --git a/src/app/article/[articleId]/layout.tsx b/src/app/article/[articleId]/layout.tsx index f0397479..3a2f73b4 100644 --- a/src/app/article/[articleId]/layout.tsx +++ b/src/app/article/[articleId]/layout.tsx @@ -13,10 +13,8 @@ export default function ArticlePageLayout({ return (
-
- - {children} -
+ +
{children}
diff --git a/src/app/problem/[problemId]/layout.tsx b/src/app/problem/[problemId]/layout.tsx index ef357584..3add46bb 100644 --- a/src/app/problem/[problemId]/layout.tsx +++ b/src/app/problem/[problemId]/layout.tsx @@ -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"; @@ -16,7 +17,10 @@ export default function ProblemLayout({ children }: ProblemLayoutProps) { {children} - +
+ + +
diff --git a/src/app/problem/[problemId]/page.tsx b/src/app/problem/[problemId]/page.tsx index 08671075..daab329d 100644 --- a/src/app/problem/[problemId]/page.tsx +++ b/src/app/problem/[problemId]/page.tsx @@ -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"; diff --git a/src/app/problem/[problemId]/problemFirstIdpage.test.tsx b/src/app/problem/[problemId]/problemFirstIdpage.test.tsx index d78695fc..dd779dfc 100644 --- a/src/app/problem/[problemId]/problemFirstIdpage.test.tsx +++ b/src/app/problem/[problemId]/problemFirstIdpage.test.tsx @@ -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"); @@ -94,6 +95,8 @@ describe("첫 번째 문제풀기 페이지 테스트", () => { await waitFor(() => { expect(result.current.isSuccess).toBe(true); }); + + expect(screen.getByText(BACK_TO_ARTICLE_WORDS.BEFORE)).toBeInTheDocument(); }); it("정답 선택 이후 정답 제출하기 버튼 클릭 시, 해설 컴포넌트 잘 노출되고, 다음 문제로 넘어가기 확인", async () => { @@ -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( "제임스 와트는 증기를 이용하여 공기를 따뜻하게 만드는 라디에이터를 만들었습니다.", ); diff --git a/src/app/problem/[problemId]/problemLastIdPage.test.tsx b/src/app/problem/[problemId]/problemLastIdPage.test.tsx index af00daa3..921b41fe 100644 --- a/src/app/problem/[problemId]/problemLastIdPage.test.tsx +++ b/src/app/problem/[problemId]/problemLastIdPage.test.tsx @@ -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(); @@ -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( @@ -146,6 +149,9 @@ describe("마지막 문제 풀이 페이지 테스트", () => { expect(explanationParagraphy.textContent).toBe( "온돌은 바닥 아래에 연기가 지나가는 길이 있는 반면, 하이포코스트는 바닥 아래가 거의 다 뚫려있는 형태입니다.", ); + + expect(screen.getByText(BACK_TO_ARTICLE_WORDS.AFTER)).toBeInTheDocument(); + act(() => { vi.advanceTimersByTime(5000); }); diff --git a/src/app/workbook/[id]/layout.tsx b/src/app/workbook/[id]/layout.tsx index 2db86c44..c304a47e 100644 --- a/src/app/workbook/[id]/layout.tsx +++ b/src/app/workbook/[id]/layout.tsx @@ -35,9 +35,7 @@ export default async function WorkbookLayout({
-
- -
+ {children}
diff --git a/src/common/components/CancelButton/index.tsx b/src/common/components/CancelButton/index.tsx new file mode 100644 index 00000000..0aec7331 --- /dev/null +++ b/src/common/components/CancelButton/index.tsx @@ -0,0 +1,16 @@ +import { XIcon } from "lucide-react"; +import { HTMLAttributes } from "react"; + +interface CancelButtonProps extends HTMLAttributes { + handleToggle: () => void; +} + +export default function CancelButton({ handleToggle, ...props }: CancelButtonProps) { + const { className } = props + + return ( +
+ +
+ ) +} diff --git a/src/main/components/MainHeader/index.tsx b/src/main/components/MainHeader/index.tsx index e6a2c9d3..e8905034 100644 --- a/src/main/components/MainHeader/index.tsx +++ b/src/main/components/MainHeader/index.tsx @@ -16,7 +16,7 @@ export default function MainHeader() { return (
diff --git a/src/main/components/WorkbookCardDetail/index.tsx b/src/main/components/WorkbookCardDetail/index.tsx index 39c57df6..119115ea 100644 --- a/src/main/components/WorkbookCardDetail/index.tsx +++ b/src/main/components/WorkbookCardDetail/index.tsx @@ -49,6 +49,7 @@ const WorkbookDetailInfoWrapper = ({ "flex flex-col", "rounded-b-lg bg-black", "px-[21px] pb-[25px] pt-[23px]", + "h-[210px]" )} > {children} diff --git a/src/problem/components/ArticleDropDown/index.tsx b/src/problem/components/ArticleDropDown/index.tsx new file mode 100644 index 00000000..f6ba4b96 --- /dev/null +++ b/src/problem/components/ArticleDropDown/index.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { useProblemIdsViewModel } from "@shared/models/useProblemIdsViewModel"; +import ProblemArticleTemplate from "../ProblemArticleTemplate"; + +export function ArticleDropDown() { + const { getArticleId } = useProblemIdsViewModel(); + + return ( +
+ +
+ ); +} diff --git a/src/problem/components/ArticleDropDownWrapper/index.tsx b/src/problem/components/ArticleDropDownWrapper/index.tsx new file mode 100644 index 00000000..7fc1609d --- /dev/null +++ b/src/problem/components/ArticleDropDownWrapper/index.tsx @@ -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 && ( +
+
+ + +
+
+ )} + + ); +} diff --git a/src/problem/components/BackToArticle/BackToArticle.test.tsx b/src/problem/components/BackToArticle/BackToArticle.test.tsx new file mode 100644 index 00000000..10bc7a83 --- /dev/null +++ b/src/problem/components/BackToArticle/BackToArticle.test.tsx @@ -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( + + + , + ); + }; + +describe("BackToArticle Component 동작 테스트", () => { + beforeAll(() => { + vi.mock("next/navigation", async () => { + const actual = + await vi.importActual( + "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(); + }); +}); diff --git a/src/problem/components/BackToArticle/index.tsx b/src/problem/components/BackToArticle/index.tsx new file mode 100644 index 00000000..83ce2d29 --- /dev/null +++ b/src/problem/components/BackToArticle/index.tsx @@ -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 {} + +export default function BackToArticle({ className }: BackToArticleProps) { + const [toggleArticle, setToggleArticle] = useState(false); + + const handleToggleArticle = () => { + setToggleArticle((prev) => !prev); + }; + + const { problemId } = useParams<{ problemId: string }>(); + const problemAnswersInfo = useMutationState({ + filters: { + mutationKey: [QUERY_KEY.POST_PROBLEM_ANSWER, problemId], + }, + select: (mutation) => { + return { + data: mutation.state.data as ApiResponse, + variables: mutation.state.variables as ProblemAnswerBody, + }; + }, + }); + + const answerStatus = new AnswerStatusModel({ + problemAnswerInfo: problemAnswersInfo[0], + }); + + const backToArticleWords = answerStatus.problemSolvedStatus + + return ( +
+ + + {backToArticleWords} + + + {BACK_TO_ARTICLE_WORDS.ARTICLE} + +
+ ); +} diff --git a/src/problem/components/ProblemArticleTemplate/index.tsx b/src/problem/components/ProblemArticleTemplate/index.tsx new file mode 100644 index 00000000..188a1bdd --- /dev/null +++ b/src/problem/components/ProblemArticleTemplate/index.tsx @@ -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 ; + + const { content } = articleInfo; + + return ( + + + + + + +
+ ); +} diff --git a/src/problem/components/ProblemTopbar/index.tsx b/src/problem/components/ProblemTopbar/index.tsx index 264c5e5c..a0cbe2d1 100644 --- a/src/problem/components/ProblemTopbar/index.tsx +++ b/src/problem/components/ProblemTopbar/index.tsx @@ -12,5 +12,5 @@ export default function ProblemTopbar() { back(); }; - return ; + return ; } diff --git a/src/problem/constants/backToArticle.ts b/src/problem/constants/backToArticle.ts new file mode 100644 index 00000000..0d54d616 --- /dev/null +++ b/src/problem/constants/backToArticle.ts @@ -0,0 +1,5 @@ +export const BACK_TO_ARTICLE_WORDS = { + BEFORE: "정답을 모르겠다면?", + AFTER: "복습을 하고 싶다면?", + ARTICLE: "아티클 다시보기" +} \ No newline at end of file diff --git a/src/problem/models/AnswerStatusModel.ts b/src/problem/models/AnswerStatusModel.ts new file mode 100644 index 00000000..20fbd56b --- /dev/null +++ b/src/problem/models/AnswerStatusModel.ts @@ -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; +} diff --git a/src/shared/components/TopBar/index.tsx b/src/shared/components/TopBar/index.tsx index e42c65cf..83aae294 100644 --- a/src/shared/components/TopBar/index.tsx +++ b/src/shared/components/TopBar/index.tsx @@ -5,18 +5,26 @@ import { useRouter } from "next/navigation"; import React, { HTMLAttributes } from "react"; import IcBack from "public/assets/icon25/back_25.svg"; +import { cn } from "@shared/utils/cn"; interface TopBarProps extends HTMLAttributes {} -export default function TopBar({ onClick }: TopBarProps) { +export default function TopBar({ onClick, className }: TopBarProps) { const { back } = useRouter(); const onClickBackIcon = (e: React.MouseEvent) => { if (onClick) onClick(e); - else back() + else back(); }; return ( -
+
); diff --git a/src/shared/models/useProblemIdsViewModel.tsx b/src/shared/models/useProblemIdsViewModel.tsx index 9f924131..1d7edafd 100644 --- a/src/shared/models/useProblemIdsViewModel.tsx +++ b/src/shared/models/useProblemIdsViewModel.tsx @@ -37,6 +37,10 @@ export const useProblemIdsViewModel = () => { return `${process.env.NEXT_PUBLIC_FEW_WEB}/article/${articleId}`; }; + const getArticleId = () => { + return articleId + } + return { setProblemIds, clearProblem, @@ -48,5 +52,6 @@ export const useProblemIdsViewModel = () => { nextSetProblemId, isExistNextProblem, getArticlePathText, + getArticleId }; };