diff --git a/.gitignore b/.gitignore index d8f911e..9ea66a6 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ next-env.d.ts # editor .cursor +GEMINI.md diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index 0340b5e..46eb759 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -13,7 +13,7 @@ export default function RootLayout({
-
{children}
+
{children}
); diff --git a/src/app/(main)/ui-preview/auction/page.test.tsx b/src/app/(main)/ui-preview/auction/page.test.tsx index 8c70a10..cfee88d 100644 --- a/src/app/(main)/ui-preview/auction/page.test.tsx +++ b/src/app/(main)/ui-preview/auction/page.test.tsx @@ -13,7 +13,7 @@ Object.defineProperty(window, "localStorage", { }); // Mock components -jest.mock("@/components/page/auction/category", () => { +jest.mock("@/components/page/auction/Category", () => { return function MockAuctionCategory({ selectedId, onSelect, @@ -30,13 +30,13 @@ jest.mock("@/components/page/auction/category", () => { }; }); -jest.mock("@/components/page/auction/search", () => { +jest.mock("@/components/page/auction/Search", () => { return function MockAuctionSearch() { return ; }; }); -jest.mock("@/components/page/auction/list", () => { +jest.mock("@/components/page/auction/List", () => { return function MockAuctionList() { return ; }; diff --git a/src/app/(main)/ui-preview/auction/page.tsx b/src/app/(main)/ui-preview/auction/page.tsx index b7c215d..8310163 100644 --- a/src/app/(main)/ui-preview/auction/page.tsx +++ b/src/app/(main)/ui-preview/auction/page.tsx @@ -1,9 +1,9 @@ "use client"; -import React, { useState, useEffect } from "react"; -import AuctionCategory from "@/components/page/auction/category"; -import AuctionList from "@/components/page/auction/list"; -import AuctionSearch from "@/components/page/auction/search"; +import React, { useState, useEffect, useMemo } from "react"; +import AuctionCategory from "@/components/page/auction/Category"; +import AuctionList from "@/components/page/auction/List"; +import AuctionSearch from "@/components/page/auction/Search"; import { ItemCategory, itemCategories } from "@/data/item-category"; import { mockItems } from "@/data/mock-items"; @@ -13,7 +13,6 @@ export default function Page() { const [selectedId, setSelectedId] = useState("all"); const [isClient, setIsClient] = useState(false); const [expandedIds, setExpandedIds] = useState>(new Set()); - const [categoryPath, setCategoryPath] = useState([]); const findCategoryPath = ( categories: ItemCategory[], @@ -58,6 +57,21 @@ export default function Page() { }); }; + // selectedId가 변경될 때마다 categoryPath와 expandedIds 업데이트 + const categoryPath = useMemo( + () => findCategoryPath(itemCategories, selectedId), + [selectedId], + ); + + // 기존에 열려있던 카테고리들을 유지하면서 선택된 카테고리 경로 추가 + useEffect(() => { + setExpandedIds((prev) => { + const next = new Set(prev); + categoryPath.slice(0, -1).forEach((c) => next.add(c.id)); + return next; + }); + }, [categoryPath]); + // 웹페이지 재접속시에도 기존 카테고리 선택 유지 useEffect(() => { setIsClient(true); @@ -67,21 +81,6 @@ export default function Page() { } }, []); - // selectedId가 변경될 때마다 categoryPath와 expandedIds 업데이트 - useEffect(() => { - const path = findCategoryPath(itemCategories, selectedId); - setCategoryPath(path); - - // 기존에 열려있던 카테고리들을 유지하면서 선택된 카테고리 경로 추가 - setExpandedIds((prev) => { - const newSet = new Set([ - ...prev, - ...path.slice(0, -1).map((category) => category.id), - ]); - return newSet; - }); - }, [selectedId]); - return (
diff --git a/src/app/(main)/ui-preview/error-test/page.tsx b/src/app/(main)/ui-preview/error-test/page.tsx new file mode 100644 index 0000000..2e9371a --- /dev/null +++ b/src/app/(main)/ui-preview/error-test/page.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { notFound } from "next/navigation"; +import { useState, useTransition } from "react"; + +export default function ErrorTestPage() { + const [error, setError] = useState(false); + const [isPending, startTransition] = useTransition(); + + if (error) { + throw new Error("의도적으로 발생시킨 에러입니다!"); + } + + return ( +
+

에러 테스트 페이지

+
+ + +
+
+ ); +} diff --git a/src/app/error.tsx b/src/app/error.tsx new file mode 100644 index 0000000..c992058 --- /dev/null +++ b/src/app/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +import ErrorPage from "@/components/errors/ErrorPage"; + +export default ErrorPage; diff --git a/src/app/login/page.test.tsx b/src/app/login/page.test.tsx index ce89097..ae9299a 100644 --- a/src/app/login/page.test.tsx +++ b/src/app/login/page.test.tsx @@ -178,6 +178,7 @@ describe("LoginPage", () => { it("로그인 성공 후 이전 페이지 이동 테스트", async () => { // 이전 페이지 referrer 설정 Object.defineProperty(document, "referrer", { + configurable: true, value: "/auction", writable: true, }); diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..693fc33 --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,5 @@ +"use client"; + +import NotFoundPage from "@/components/errors/NotFoundPage"; + +export default NotFoundPage; diff --git a/src/components/errors/ErrorPage.test.tsx b/src/components/errors/ErrorPage.test.tsx new file mode 100644 index 0000000..4fee784 --- /dev/null +++ b/src/components/errors/ErrorPage.test.tsx @@ -0,0 +1,36 @@ +import { render, screen, fireEvent } from "@testing-library/react"; +import ErrorPage from "./ErrorPage"; + +describe("ErrorPage", () => { + const mockReset = jest.fn(); + const mockError = new Error("Test Error"); + + beforeEach(() => { + mockReset.mockClear(); + jest.spyOn(console, "error").mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("렌더링 테스트", () => { + render(); + + expect( + screen.getByRole("heading", { name: "문제가 발생했습니다!" }), + ).toBeInTheDocument(); + expect( + screen.getByRole("button", { name: "다시 시도" }), + ).toBeInTheDocument(); + }); + + it("reset 버튼 동작 테스트", () => { + render(); + + const retryButton = screen.getByRole("button", { name: "다시 시도" }); + fireEvent.click(retryButton); + + expect(mockReset).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/components/errors/ErrorPage.tsx b/src/components/errors/ErrorPage.tsx new file mode 100644 index 0000000..e8de4a6 --- /dev/null +++ b/src/components/errors/ErrorPage.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useEffect } from "react"; + +export default function ErrorPage({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + console.error(error); + }, [error]); + + return ( +
+
+

문제가 발생했습니다!

+ +
+
+ ); +} diff --git a/src/components/errors/NotFoundPage.test.tsx b/src/components/errors/NotFoundPage.test.tsx new file mode 100644 index 0000000..882cd4d --- /dev/null +++ b/src/components/errors/NotFoundPage.test.tsx @@ -0,0 +1,15 @@ +import { render, screen } from "@testing-library/react"; +import NotFoundPage from "./NotFoundPage"; + +describe("NotFoundPage", () => { + it("렌더링 테스트", () => { + render(); + + expect(screen.getByText("404")).toBeInTheDocument(); + expect(screen.getByText("페이지를 찾을 수 없습니다.")).toBeInTheDocument(); + + const homeLink = screen.getByRole("link", { name: "홈으로 돌아가기" }); + expect(homeLink).toBeInTheDocument(); + expect(homeLink).toHaveAttribute("href", "/"); + }); +}); diff --git a/src/components/errors/NotFoundPage.tsx b/src/components/errors/NotFoundPage.tsx new file mode 100644 index 0000000..c35bcad --- /dev/null +++ b/src/components/errors/NotFoundPage.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; + +export default function NotFoundPage() { + return ( +
+
+

404

+

페이지를 찾을 수 없습니다.

+ + 홈으로 돌아가기 + +
+
+ ); +} diff --git a/src/components/page/auction/category.test.tsx b/src/components/page/auction/category.test.tsx index 0354f93..0ed5f22 100644 --- a/src/components/page/auction/category.test.tsx +++ b/src/components/page/auction/category.test.tsx @@ -1,5 +1,5 @@ import { render, screen, fireEvent } from "@testing-library/react"; -import AuctionCategory from "./category"; +import AuctionCategory from "./Category"; const createMockProps = () => ({ selectedId: "", diff --git a/src/components/page/auction/category.tsx b/src/components/page/auction/category.tsx index 38255b2..be65529 100644 --- a/src/components/page/auction/category.tsx +++ b/src/components/page/auction/category.tsx @@ -71,7 +71,7 @@ const RecursiveCategoryItem = ({ ); }; -const AuctionCategory = ({ +export default function AuctionCategory({ selectedId, onSelect, expandedIds, @@ -81,7 +81,7 @@ const AuctionCategory = ({ onSelect: (id: string) => void; expandedIds: Set; onToggleExpand: (id: string) => void; -}) => { +}) { return (
@@ -103,6 +103,4 @@ const AuctionCategory = ({
); -}; - -export default AuctionCategory; +} diff --git a/src/components/page/auction/list.tsx b/src/components/page/auction/list.tsx index 674c679..4ba5d06 100644 --- a/src/components/page/auction/list.tsx +++ b/src/components/page/auction/list.tsx @@ -42,7 +42,7 @@ const ListItem = ({ item }: { item: AuctionItem }) => (
); -function AuctionList({ items }: { items: AuctionItem[] }) { +export default function AuctionList({ items }: { items: AuctionItem[] }) { return (
@@ -54,5 +54,3 @@ function AuctionList({ items }: { items: AuctionItem[] }) {
); } - -export default AuctionList; diff --git a/src/components/page/auction/search.test.tsx b/src/components/page/auction/search.test.tsx index 801eb5e..8e313b9 100644 --- a/src/components/page/auction/search.test.tsx +++ b/src/components/page/auction/search.test.tsx @@ -1,5 +1,5 @@ import { render, screen, fireEvent } from "@testing-library/react"; -import AuctionSearch from "./search"; +import AuctionSearch from "./Search"; import { ItemCategory } from "@/data/item-category"; const createMockPath = (): ItemCategory[] => [ diff --git a/src/components/page/auction/search.tsx b/src/components/page/auction/search.tsx index 33fcf34..d4addab 100644 --- a/src/components/page/auction/search.tsx +++ b/src/components/page/auction/search.tsx @@ -5,7 +5,7 @@ import { Label } from "@/components/ui/label"; import { ItemCategory } from "@/data/item-category"; import React from "react"; -function AuctionSearch({ +export default function AuctionSearch({ path, onCategorySelect, }: { @@ -42,5 +42,3 @@ function AuctionSearch({
); } - -export default AuctionSearch; diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..5bdefe0 --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,10 @@ +import { NextRequest, NextResponse } from "next/server"; + +export function middleware(request: NextRequest) { + // TODO: 추후 확인 후 수정 필요 + if (request.nextUrl.pathname === "/") { + return NextResponse.redirect(new URL("/ui-preview/auction", request.url)); + } + + return NextResponse.next(); +}