diff --git a/package-lock.json b/package-lock.json index 39be576e3..ae69cc9c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@tanstack/react-query-devtools": "^5.66.9", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -1192,9 +1193,20 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.64.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", - "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "version": "5.66.4", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", + "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-devtools": { + "version": "5.65.0", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.65.0.tgz", + "integrity": "sha512-g5y7zc07U9D3esMdqUfTEVu9kMHoIaVBsD0+M3LPdAdD710RpTcLiNvJY1JkYXqkq9+NV+CQoemVNpQPBXVsJg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -1202,18 +1214,36 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.64.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", - "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "version": "5.66.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz", + "integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.66.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-query-devtools": { + "version": "5.66.9", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.9.tgz", + "integrity": "sha512-70G6AR35he53SYUcUK6EdqNR18zejCv1rM6900gjZP408EAex56YLwVSeijzk9lWeU2J42G9Fjh0i1WngUTsgw==", + "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.64.1" + "@tanstack/query-devtools": "5.65.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { + "@tanstack/react-query": "^5.66.9", "react": "^18 || ^19" } }, diff --git a/package.json b/package.json index 570403eb7..c3470a736 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@tanstack/react-query-devtools": "^5.66.9", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/src/app/(auth)/login/action.ts b/src/app/(auth)/login/action.ts deleted file mode 100644 index 85fa71061..000000000 --- a/src/app/(auth)/login/action.ts +++ /dev/null @@ -1,41 +0,0 @@ -"use server"; - -import { signIn } from "@/auth"; -import { signinFormSchmea, SigninFormType } from "@/schemas/auth"; -import { CredentialsSignin } from "next-auth"; - -export default async function action(data: SigninFormType) { - const parsed = signinFormSchmea.safeParse(data); - - if (!parsed.success) { - return { - message: "제출양식에 문제가 있습니다. 확인해주세요", - success: false, - }; - } - - try { - await signIn("credentials", { - email: parsed.data.email, - password: parsed.data.password, - redirect: false, - }); - - return { - message: "로그인 성공", - success: true, - }; - } catch (error) { - if (error instanceof CredentialsSignin) { - return { - message: `로그인 실패 : ${error.code}`, - success: false, - }; - } - - return { - message: `로그인 실패`, - success: false, - }; - } -} diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 070ca13a5..33a755ae1 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import AuthContainer from "../_components/AuthContainer"; -import LoginForm from "../_components/LoginForm"; +import AuthContainer from "@/components/auth/AuthContainer"; +import LoginForm from "@/components/auth/LoginForm"; import { auth } from "@/auth"; export default async function LoginPage() { diff --git a/src/app/(auth)/signup/action.ts b/src/app/(auth)/signup/action.ts deleted file mode 100644 index 00313690c..000000000 --- a/src/app/(auth)/signup/action.ts +++ /dev/null @@ -1,44 +0,0 @@ -"use server"; - -import { signupFormSchema, SignupFormType } from "@/schemas/auth"; -import { signUp } from "@/service/auth"; -import { isAxiosError } from "axios"; - -export default async function action(data: SignupFormType) { - const parsed = signupFormSchema.safeParse(data); - - if (!parsed.success) { - return { - message: "제출양식에 문제가 있습니다. 확인해주세요", - success: false, - }; - } - - try { - await signUp({ - email: parsed.data.email, - nickname: parsed.data.nickname, - password: parsed.data.password, - passwordConfirmation: parsed.data.passwordConfirmation, - }); - - return { - message: "회원가입 성공", - success: true, - }; - } catch (error) { - if (isAxiosError(error)) { - const message = - error.response?.data.message || "알 수 없는 에러가 발생했어요."; - return { - message: `회원가입 실패 : ${message}`, - success: false, - }; - } - - return { - message: "회원가입 실패", - success: false, - }; - } -} diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index 699b27944..1bb1a33a0 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; -import AuthContainer from "../_components/AuthContainer"; -import SignupForm from "../_components/SignupForm"; +import AuthContainer from "@/components/auth/AuthContainer"; +import SignupForm from "@/components/auth/SignupForm"; import { auth } from "@/auth"; export default async function SignupPage() { diff --git a/src/app/(common)/(board)/_components/ArticleAddForm.tsx b/src/app/(common)/(board)/_components/ArticleAddForm.tsx deleted file mode 100644 index ac0ba258e..000000000 --- a/src/app/(common)/(board)/_components/ArticleAddForm.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -import ArticleForm from "./ArticleForm"; -import useArticleActions from "./useArticleActions"; - -export default function ArticleAddForm() { - const { handleArticleAdd } = useArticleActions(); - - return ; -} diff --git a/src/app/(common)/(board)/_components/ArticleModifyForm.tsx b/src/app/(common)/(board)/_components/ArticleModifyForm.tsx deleted file mode 100644 index d0d4b83c7..000000000 --- a/src/app/(common)/(board)/_components/ArticleModifyForm.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { Article } from "@/types/article"; -import ArticleForm from "./ArticleForm"; -import useArticleActions from "./useArticleActions"; - -export default function ArticleModifyForm({ - initialData, -}: { - initialData: Article; -}) { - const { handleArticleModify } = useArticleActions(initialData.id); - - return ( - - ); -} diff --git a/src/app/(common)/(board)/_components/useArticleActions.ts b/src/app/(common)/(board)/_components/useArticleActions.ts deleted file mode 100644 index 3609fbdd9..000000000 --- a/src/app/(common)/(board)/_components/useArticleActions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ArticleFormType } from "@/schemas/article"; -import { - addArticle, - deleteArticle, - modifyArticle, - toggleLike, - uploadArticleImage, -} from "@service/article"; - -export default function useArticleActions(articleId?: number) { - async function handleLike(flag: boolean) { - if (!articleId) return; - - return toggleLike(articleId, flag); - } - - async function handleArticleAdd(formData: ArticleFormType) { - try { - if (formData.image instanceof File) { - const { url } = await uploadArticleImage(formData.image); - formData.image = url; - } - - return await addArticle(formData); - } catch (err) { - throw err; - } - } - - async function handleArticleModify(formData: ArticleFormType) { - if (!articleId) return; - - try { - if (formData.image instanceof File) { - const { url } = await uploadArticleImage(formData.image); - formData.image = url; - } - - return await modifyArticle(articleId, formData); - } catch (err) { - throw err; - } - } - - async function handleArticleDelete() { - if (!articleId) return; - - return deleteArticle(articleId); - } - - return { - handleLike, - handleArticleAdd, - handleArticleModify, - handleArticleDelete, - }; -} diff --git a/src/app/(common)/(board)/addBoard/page.tsx b/src/app/(common)/(board)/addBoard/page.tsx index 3e2137c25..c9748b29d 100644 --- a/src/app/(common)/(board)/addBoard/page.tsx +++ b/src/app/(common)/(board)/addBoard/page.tsx @@ -1,10 +1,15 @@ +"use client"; + import { PageWrapper } from "@/components/Page"; -import ArticleAddForm from "../_components/ArticleAddForm"; +import ArticleForm from "@/components/board/ArticleForm"; +import { useArticleAdd } from "@/service/article.queries"; export default function AddBoardPage() { + const { mutateAsync: handleArticleAdd } = useArticleAdd(); + return ( - + ); } diff --git a/src/app/(common)/(board)/boards/(lists)/@all/loading.tsx b/src/app/(common)/(board)/boards/(lists)/@all/loading.tsx deleted file mode 100644 index 1c8669b09..000000000 --- a/src/app/(common)/(board)/boards/(lists)/@all/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 게시물 가져오는중...; -} diff --git a/src/app/(common)/(board)/boards/(lists)/@all/page.tsx b/src/app/(common)/(board)/boards/(lists)/@all/page.tsx deleted file mode 100644 index 66755ef92..000000000 --- a/src/app/(common)/(board)/boards/(lists)/@all/page.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { getArticles } from "@/service/article"; -import { Section } from "@/components/Section"; -import BoardList from "../_components/BoardList"; -import BoardFilter from "../_components/BoardFilter"; -import { Button } from "@/components/ui"; - -type ItemsPageQueryParams = { - page?: string; - orderBy?: string; - keyword?: string; -}; -export default async function AllListPage({ - searchParams, -}: { - searchParams: Promise; -}) { - const { page, orderBy, keyword } = await searchParams; - const data = await getArticles({ - page: Number(page) || 1, - orderBy: orderBy || "recent", - keyword: keyword || "", - }); - - return ( -
- - - - - - - -
- ); -} diff --git a/src/app/(common)/(board)/boards/(lists)/@best/loading.tsx b/src/app/(common)/(board)/boards/(lists)/@best/loading.tsx deleted file mode 100644 index c750861a2..000000000 --- a/src/app/(common)/(board)/boards/(lists)/@best/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 베스트 게시물 가져오는중...; -} diff --git a/src/app/(common)/(board)/boards/(lists)/@best/page.tsx b/src/app/(common)/(board)/boards/(lists)/@best/page.tsx deleted file mode 100644 index e6428ce99..000000000 --- a/src/app/(common)/(board)/boards/(lists)/@best/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { getArticles } from "@/service/article"; -import { Section } from "@/components/Section"; -import BestList from "../_components/BestList"; - -type BestItemsPageQueryParams = { - bestPageSize?: string; -}; - -export default async function BestListPage({ - searchParams, -}: { - searchParams: Promise; -}) { - const { bestPageSize } = await searchParams; - const data = await getArticles({ - orderBy: "like", - pageSize: Number(bestPageSize) || 3, - }); - - return ( -
- - - - -
- ); -} diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BestList.tsx b/src/app/(common)/(board)/boards/(lists)/_components/BestList.tsx deleted file mode 100644 index 2a50356f4..000000000 --- a/src/app/(common)/(board)/boards/(lists)/_components/BestList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import useResponsive from "@/hooks/useResponsive"; -import useParams from "@/hooks/useParams"; -import { PaginationResponse } from "@/types/common"; -import { Article } from "@/types/article"; -import BoardListWrapper from "./BoardListWrapper"; -import BestItem from "./BestItem"; - -interface BestListProps { - data: PaginationResponse
; -} - -export default function BestList({ data }: BestListProps) { - const { searchParams, handleParams } = useParams(); - const currentSize = Number(searchParams.get("bestPageSize")) || 3; - const pageSize = useResponsive({ - pc: 3, - tablet: 2, - mobile: 1, - }); - const { list } = data; - - useEffect(() => { - if (pageSize === currentSize) return; - - handleParams({ bestPageSize: pageSize }); - }, [pageSize, currentSize, handleParams]); - - return ( - <> - - {(item) => } - - - ); -} diff --git a/src/app/(common)/(board)/boards/(lists)/layout.tsx b/src/app/(common)/(board)/boards/(lists)/layout.tsx deleted file mode 100644 index c212166ae..000000000 --- a/src/app/(common)/(board)/boards/(lists)/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PageWrapper } from "@/components/Page"; -import { ReactNode } from "react"; - -export default function ListLayout({ - all, - best, - children, -}: { - all: ReactNode; - best: ReactNode; - children: ReactNode; -}) { - return ( - - {best} - {all} - {children} - - ); -} diff --git a/src/app/(common)/(board)/boards/(lists)/page.tsx b/src/app/(common)/(board)/boards/(lists)/page.tsx index c035d6f6a..d9e2386fb 100644 --- a/src/app/(common)/(board)/boards/(lists)/page.tsx +++ b/src/app/(common)/(board)/boards/(lists)/page.tsx @@ -1,3 +1,35 @@ +import { Suspense } from "react"; +import { PageWrapper } from "@/components/Page"; +import { Section } from "@/components/Section"; +import { Button, Loading } from "@/components/ui"; +import BestList from "@/components/board/BestList"; +import BoardFilter from "@/components/board/BoardFilter"; +import BoardList from "@/components/board/BoardList"; + export default function BoardsPage() { - return <>; + return ( + +
+ + + loading...}> + + + +
+
+ loading...}> + + + + + + + + +
+
+ ); } diff --git a/src/app/(common)/(board)/boards/[id]/@comments/loading.tsx b/src/app/(common)/(board)/boards/[id]/@comments/loading.tsx deleted file mode 100644 index 80dc79b76..000000000 --- a/src/app/(common)/(board)/boards/[id]/@comments/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 코멘트를 가져오는중입니다...; -} diff --git a/src/app/(common)/(board)/boards/[id]/@comments/page.tsx b/src/app/(common)/(board)/boards/[id]/@comments/page.tsx deleted file mode 100644 index 315b2dbd4..000000000 --- a/src/app/(common)/(board)/boards/[id]/@comments/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { CommentAdd, CommentList } from "@/components/Comment"; -import { getComments } from "@/service/comments"; - -export default async function ArticleCommentsPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const id = (await params).id; - const comments = await getComments("articles", { - id: Number(id), - limit: 5, - }); - - return ( - <> - - - - ); -} diff --git a/src/app/(common)/(board)/boards/[id]/@detail/loading.tsx b/src/app/(common)/(board)/boards/[id]/@detail/loading.tsx deleted file mode 100644 index 7c7ae290a..000000000 --- a/src/app/(common)/(board)/boards/[id]/@detail/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 게시물 정보를 불러오는중입니다...; -} diff --git a/src/app/(common)/(board)/boards/[id]/@detail/page.tsx b/src/app/(common)/(board)/boards/[id]/@detail/page.tsx deleted file mode 100644 index 7c3ac46ac..000000000 --- a/src/app/(common)/(board)/boards/[id]/@detail/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { getArticle } from "@/service/article"; -import { notFound } from "next/navigation"; -import BoardDetail from "../../../_components/BoardDetail"; -import { isAxiosError } from "axios"; - -export default async function BoardDetailPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const id = (await params).id; - - try { - const detail = await getArticle(Number(id)); - - return ; - } catch (error) { - if (isAxiosError(error)) { - if (error.status === 404) { - notFound(); - } - } - - throw new Error("페이지 정보를 가져오는데 문제가 생겼습니다."); - } -} diff --git a/src/app/(common)/(board)/boards/[id]/layout.tsx b/src/app/(common)/(board)/boards/[id]/layout.tsx deleted file mode 100644 index 0d4963dad..000000000 --- a/src/app/(common)/(board)/boards/[id]/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PageWrapper } from "@/components/Page"; -import { ReactNode } from "react"; - -export default function ArticleDetailLayout({ - detail, - comments, - children, -}: { - detail: ReactNode; - comments: ReactNode; - children: ReactNode; -}) { - return ( - - {children} - {detail} - {comments} - - ); -} diff --git a/src/app/(common)/(board)/boards/[id]/page.tsx b/src/app/(common)/(board)/boards/[id]/page.tsx index aa931ed73..733acbb2a 100644 --- a/src/app/(common)/(board)/boards/[id]/page.tsx +++ b/src/app/(common)/(board)/boards/[id]/page.tsx @@ -1,3 +1,13 @@ +import { PageWrapper } from "@/components/Page"; +import { CommentAdd, CommentList } from "@/components/Comment"; +import BoardDetail from "@/components/board/BoardDetail"; + export default function ArticleDetailPage() { - return <>; + return ( + + + + + + ); } diff --git a/src/app/(common)/(board)/modifyBoard/[id]/page.tsx b/src/app/(common)/(board)/modifyBoard/[id]/page.tsx index bdd644180..9d0dc4f70 100644 --- a/src/app/(common)/(board)/modifyBoard/[id]/page.tsx +++ b/src/app/(common)/(board)/modifyBoard/[id]/page.tsx @@ -1,52 +1,42 @@ +"use client"; + +import { notFound, redirect, useParams } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useArticleModify, useGetArticle } from "@/service/article.queries"; import { PageWrapper } from "@/components/Page"; -import { auth } from "@/auth"; -import { notFound, redirect } from "next/navigation"; -import { Suspense } from "react"; -import { Message } from "@/components/ui"; -import { getArticle } from "@/service/article"; -import { isAxiosError } from "axios"; -import { isRedirectError } from "next/dist/client/components/redirect-error"; -import ArticleModifyForm from "../../_components/ArticleModifyForm"; - -export default async function ModifyBoardPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const session = await auth(); - const id = (await params).id; - - try { - const detail = await getArticle(Number(id)); - const isOwner = detail.writer.id === Number(session?.user.id); - - if (!isOwner) { - redirect("/boards"); - } - - // 상세데이터에 이미지가 null로 오는경우 기본값을 undefined으로 변경시켜서 주입 - const filteredDetail = { ...detail, image: detail.image ?? undefined }; - - return ( - - 게시물정보를 가져오는 중입니다...} - > - - - - ); - } catch (error) { - if (isAxiosError(error)) { - if (error.status === 404) { - notFound(); - } - } - - if (isRedirectError(error)) { - throw error; - } - - throw new Error("페이지 정보를 가져오는데 문제가 생겼습니다."); +import { Loading } from "@/components/ui"; +import ArticleForm from "@/components/board/ArticleForm"; + +export default function ModifyBoardPage() { + const { data: session } = useSession(); + const { id } = useParams<{ id: string }>(); + const articleId = Number(id); + + const { data: detail, isPending } = useGetArticle(articleId); + const { mutateAsync: handleArticleModify } = useArticleModify(articleId); + + if (isPending) { + return 게시물 정보를 가져오는 중입니다.; + } + + if (!detail) { + notFound(); + } + + const isOwner = detail.writer.id === Number(session?.user.id); + if (!isOwner) { + redirect("/boards"); } + + const filteredDetail = { ...detail, image: detail.image ?? undefined }; + + return ( + + + + ); } diff --git a/src/app/(common)/(landing)/page.tsx b/src/app/(common)/(landing)/page.tsx index 0ba0771e4..b8f52c7c7 100644 --- a/src/app/(common)/(landing)/page.tsx +++ b/src/app/(common)/(landing)/page.tsx @@ -1,10 +1,10 @@ -import Banner from "./_components/Banner"; -import Feature from "./_components/Feature"; +import Banner from "@/components/landing/Banner"; +import Feature from "@/components/landing/Feature"; import { heroBannerData, footerBannerData, featureList, -} from "./_components/landingContents"; +} from "@/components/landing/landingContents"; export default function LandingPage() { return ( diff --git a/src/app/(common)/(market)/_components/ProductAddForm.tsx b/src/app/(common)/(market)/_components/ProductAddForm.tsx deleted file mode 100644 index 2243cbc47..000000000 --- a/src/app/(common)/(market)/_components/ProductAddForm.tsx +++ /dev/null @@ -1,10 +0,0 @@ -"use client"; - -import ProductForm from "./ProductForm"; -import useProductActions from "./useProductActions"; - -export default function ProductAddForm() { - const { handleProductAdd } = useProductActions(); - - return ; -} diff --git a/src/app/(common)/(market)/_components/ProductModifyForm.tsx b/src/app/(common)/(market)/_components/ProductModifyForm.tsx deleted file mode 100644 index bfd67b6a1..000000000 --- a/src/app/(common)/(market)/_components/ProductModifyForm.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { Product } from "@/types/product"; -import ProductForm from "./ProductForm"; -import useProductActions from "./useProductActions"; - -export default function ProductModifyForm({ - initialData, -}: { - initialData: Product; -}) { - const { handleProductModify } = useProductActions(initialData.id); - - return ( - - ); -} diff --git a/src/app/(common)/(market)/_components/useProductActions.ts b/src/app/(common)/(market)/_components/useProductActions.ts deleted file mode 100644 index b2d8d056e..000000000 --- a/src/app/(common)/(market)/_components/useProductActions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ProductFormType } from "@schemas/product"; -import { - addProduct, - deleteProduct, - modifyProduct, - toggleLike, - uploadProductImage, -} from "@service/product"; - -export default function useProductActions(productId?: number) { - async function handleLike(flag: boolean) { - if (!productId) return; - - return toggleLike(productId, flag); - } - - async function handleProductAdd(formData: ProductFormType) { - try { - if (formData.images[0] instanceof File) { - const { url } = await uploadProductImage(formData.images[0]); - formData.images = [url]; - } - - return await addProduct(formData); - } catch (err) { - throw err; - } - } - - async function handleProductModify(formData: ProductFormType) { - if (!productId) return; - - try { - if (formData.images[0] instanceof File) { - const { url } = await uploadProductImage(formData.images[0]); - formData.images = [url]; - } - - return await modifyProduct(productId, formData); - } catch (err) { - throw err; - } - } - - async function handleProductDelete() { - if (!productId) return; - - return deleteProduct(productId); - } - - return { - handleLike, - handleProductAdd, - handleProductModify, - handleProductDelete, - }; -} diff --git a/src/app/(common)/(market)/addItem/page.tsx b/src/app/(common)/(market)/addItem/page.tsx index c9bda2240..d7bc340c3 100644 --- a/src/app/(common)/(market)/addItem/page.tsx +++ b/src/app/(common)/(market)/addItem/page.tsx @@ -1,10 +1,15 @@ +"use client"; + import { PageWrapper } from "@/components/Page"; -import ProductAddForm from "../_components/ProductAddForm"; +import ProductForm from "@/components/market/ProductForm"; +import { useProductAdd } from "@/service/product.queries"; export default function AddItemPage() { + const { mutateAsync: handleProductAdd } = useProductAdd(); + return ( - + ; ); } diff --git a/src/app/(common)/(market)/items/(lists)/@all/loading.tsx b/src/app/(common)/(market)/items/(lists)/@all/loading.tsx deleted file mode 100644 index 1f368b102..000000000 --- a/src/app/(common)/(market)/items/(lists)/@all/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 전체 상품을 불러오는중입니다....; -} diff --git a/src/app/(common)/(market)/items/(lists)/@all/page.tsx b/src/app/(common)/(market)/items/(lists)/@all/page.tsx deleted file mode 100644 index 25664a825..000000000 --- a/src/app/(common)/(market)/items/(lists)/@all/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Section } from "@/components/Section"; -import { getProducts } from "@/service/product"; -import ProductFilter from "../_components/ProductFilter"; -import ProductList from "../_components/ProductList"; - -type ItemsPageQueryParams = { - page?: string; - orderBy?: string; - keyword?: string; - pageSize?: string; -}; - -export default async function AllListPage({ - searchParams, -}: { - searchParams: Promise; -}) { - const { page, orderBy, keyword, pageSize } = await searchParams; - const data = await getProducts({ - page: Number(page) || 1, - orderBy: orderBy || "recent", - keyword: keyword || "", - pageSize: Number(pageSize) || 10, - }); - - return ( -
- - - - - - -
- ); -} diff --git a/src/app/(common)/(market)/items/(lists)/@best/loading.tsx b/src/app/(common)/(market)/items/(lists)/@best/loading.tsx deleted file mode 100644 index 8fc5f651c..000000000 --- a/src/app/(common)/(market)/items/(lists)/@best/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 상품상제정보를 가져오는중입니다...; -} diff --git a/src/app/(common)/(market)/items/(lists)/@best/page.tsx b/src/app/(common)/(market)/items/(lists)/@best/page.tsx deleted file mode 100644 index b11ca5a10..000000000 --- a/src/app/(common)/(market)/items/(lists)/@best/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Section } from "@/components/Section"; -import { getProducts } from "@/service/product"; -import BestList from "../_components/BestList"; - -type BestItemsPageQueryParams = { - bestPageSize?: string; -}; - -export default async function BestListPage({ - searchParams, -}: { - searchParams: Promise; -}) { - const { bestPageSize } = await searchParams; - const data = await getProducts({ - orderBy: "favorite", - pageSize: Number(bestPageSize) || 4, - }); - - return ( -
- - - - -
- ); -} diff --git a/src/app/(common)/(market)/items/(lists)/_components/BestList.tsx b/src/app/(common)/(market)/items/(lists)/_components/BestList.tsx deleted file mode 100644 index dc69f3445..000000000 --- a/src/app/(common)/(market)/items/(lists)/_components/BestList.tsx +++ /dev/null @@ -1,38 +0,0 @@ -"use client"; - -import { useEffect } from "react"; -import useResponsive from "@/hooks/useResponsive"; -import useParams from "@/hooks/useParams"; -import { PaginationResponse } from "@/types/common"; -import { Product } from "@/types/product"; -import ProductItem from "./ProductItem"; -import ProductListWrapper from "./ProductListWrapper"; - -interface BestListProps { - data: PaginationResponse; -} - -export default function BestList({ data }: BestListProps) { - const { searchParams, handleParams } = useParams(); - const currentSize = Number(searchParams.get("bestPageSize")) || 4; - const pageSize = useResponsive({ - pc: 4, - tablet: 2, - mobile: 1, - }); - const { list } = data; - - useEffect(() => { - if (pageSize === currentSize) return; - - handleParams({ bestPageSize: pageSize }); - }, [pageSize, currentSize, handleParams]); - - return ( - <> - - {(item) => } - - - ); -} diff --git a/src/app/(common)/(market)/items/(lists)/layout.tsx b/src/app/(common)/(market)/items/(lists)/layout.tsx deleted file mode 100644 index c212166ae..000000000 --- a/src/app/(common)/(market)/items/(lists)/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PageWrapper } from "@/components/Page"; -import { ReactNode } from "react"; - -export default function ListLayout({ - all, - best, - children, -}: { - all: ReactNode; - best: ReactNode; - children: ReactNode; -}) { - return ( - - {best} - {all} - {children} - - ); -} diff --git a/src/app/(common)/(market)/items/(lists)/page.tsx b/src/app/(common)/(market)/items/(lists)/page.tsx index a70387363..26d4392a4 100644 --- a/src/app/(common)/(market)/items/(lists)/page.tsx +++ b/src/app/(common)/(market)/items/(lists)/page.tsx @@ -1,3 +1,32 @@ +import { Suspense } from "react"; +import { PageWrapper } from "@/components/Page"; +import { Section } from "@/components/Section"; +import { Loading } from "@/components/ui"; +import BestList from "@/components/market/BestList"; +import ProductFilter from "@/components/market/ProductFilter"; +import ProductList from "@/components/market/ProductList"; + export default function ItemsPage() { - return <>; + return ( + +
+ + + loading...}> + + + +
+
+ loading...}> + + + + + + + +
+
+ ); } diff --git a/src/app/(common)/(market)/items/[id]/@comments/loading.tsx b/src/app/(common)/(market)/items/[id]/@comments/loading.tsx deleted file mode 100644 index 80dc79b76..000000000 --- a/src/app/(common)/(market)/items/[id]/@comments/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 코멘트를 가져오는중입니다...; -} diff --git a/src/app/(common)/(market)/items/[id]/@comments/page.tsx b/src/app/(common)/(market)/items/[id]/@comments/page.tsx deleted file mode 100644 index c6e864681..000000000 --- a/src/app/(common)/(market)/items/[id]/@comments/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { CommentAdd, CommentList } from "@/components/Comment"; -import { getComments } from "@/service/comments"; - -export default async function ItemCommentsPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const id = (await params).id; - const comments = await getComments("products", { - id: Number(id), - limit: 5, - }); - - return ( - <> - - - - ); -} diff --git a/src/app/(common)/(market)/items/[id]/@detail/loading.tsx b/src/app/(common)/(market)/items/[id]/@detail/loading.tsx deleted file mode 100644 index 76cbb3b42..000000000 --- a/src/app/(common)/(market)/items/[id]/@detail/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { Message } from "@/components/ui"; - -export default function Loading() { - return 상품정보를 불러오는중입니다...; -} diff --git a/src/app/(common)/(market)/items/[id]/@detail/page.tsx b/src/app/(common)/(market)/items/[id]/@detail/page.tsx deleted file mode 100644 index a4e7bdf27..000000000 --- a/src/app/(common)/(market)/items/[id]/@detail/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { getProduct } from "@/service/product"; -import ProductDetail from "../../../_components/ProductDetail"; -import { notFound } from "next/navigation"; -import { isAxiosError } from "axios"; - -export default async function ItemDetailPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const id = (await params).id; - - try { - const detail = await getProduct(Number(id)); - - return ; - } catch (error) { - if (isAxiosError(error)) { - if (error.status === 404) { - notFound(); - } - } - - throw new Error("페이지 정보를 가져오는데 문제가 생겼습니다."); - } -} diff --git a/src/app/(common)/(market)/items/[id]/layout.tsx b/src/app/(common)/(market)/items/[id]/layout.tsx deleted file mode 100644 index d04e6f947..000000000 --- a/src/app/(common)/(market)/items/[id]/layout.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { PageWrapper } from "@/components/Page"; -import { ReactNode } from "react"; - -export default function ItemDetailLayout({ - detail, - comments, - children, -}: { - detail: ReactNode; - comments: ReactNode; - children: ReactNode; -}) { - return ( - - {children} - {detail} - {comments} - - ); -} diff --git a/src/app/(common)/(market)/items/[id]/page.tsx b/src/app/(common)/(market)/items/[id]/page.tsx index 936f12e0a..c950026d7 100644 --- a/src/app/(common)/(market)/items/[id]/page.tsx +++ b/src/app/(common)/(market)/items/[id]/page.tsx @@ -1,3 +1,13 @@ +import { PageWrapper } from "@/components/Page"; +import { CommentAdd, CommentList } from "@/components/Comment"; +import ProductDetail from "@/components/market/ProductDetail"; + export default function ItemDetailPage() { - return <>; + return ( + + + + + + ); } diff --git a/src/app/(common)/(market)/modifyItem/[id]/page.tsx b/src/app/(common)/(market)/modifyItem/[id]/page.tsx index fbecd068b..a0faff332 100644 --- a/src/app/(common)/(market)/modifyItem/[id]/page.tsx +++ b/src/app/(common)/(market)/modifyItem/[id]/page.tsx @@ -1,47 +1,40 @@ -import { PageWrapper } from "@/components/Page"; -import { getProduct } from "@/service/product"; -import { auth } from "@/auth"; -import { Suspense } from "react"; -import { Message } from "@/components/ui"; -import { notFound, redirect } from "next/navigation"; -import { isAxiosError } from "axios"; -import { isRedirectError } from "next/dist/client/components/redirect-error"; -import ProductModifyForm from "../../_components/ProductModifyForm"; +"use client"; -export default async function ModifyItemPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const session = await auth(); - const id = (await params).id; +import { notFound, redirect, useParams } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useGetProduct, useProductModify } from "@/service/product.queries"; +import { PageWrapper } from "@/components/Page"; +import { Loading } from "@/components/ui"; +import ProductForm from "@/components/market/ProductForm"; - try { - const detail = await getProduct(Number(id)); - const isOwner = detail.ownerId === Number(session?.user.id); +export default function ModifyItemPage() { + const { data: session } = useSession(); + const { id } = useParams<{ id: string }>(); + const productId = Number(id); - if (!isOwner) { - redirect("/items"); - } + const { data: detail, isPending } = useGetProduct(productId); + const { mutateAsync: handleProductModify } = useProductModify(productId); - return ( - - 상품정보를 가져오는 중입니다...}> - - - - ); - } catch (error) { - if (isAxiosError(error)) { - if (error.status === 404) { - notFound(); - } - } + if (isPending) { + return 상품 정보를 가져오는 중입니다.; + } - if (isRedirectError(error)) { - throw error; - } + if (!detail) { + notFound(); + } - throw new Error("페이지 정보를 가져오는데 문제가 생겼습니다."); + const isOwner = detail.ownerId === Number(session?.user.id); + if (!isOwner) { + redirect("/items"); } + + return ( + + + + ); } diff --git a/src/app/(common)/(user)/_components/Activity.tsx b/src/app/(common)/(user)/_components/Activity.tsx deleted file mode 100644 index fbfaba4a6..000000000 --- a/src/app/(common)/(user)/_components/Activity.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import styles from "./Activity.module.scss"; - -export default function Activity({ - productsCount, - favoritesCount, -}: { - productsCount: number; - favoritesCount: number; -}) { - return ( -
-
-
등록한 상품
-
{productsCount}
-
-
-
좋아요
-
{favoritesCount}
-
-
- ); -} diff --git a/src/app/(common)/(user)/changePassword/action.ts b/src/app/(common)/(user)/changePassword/action.ts deleted file mode 100644 index f5abc28f5..000000000 --- a/src/app/(common)/(user)/changePassword/action.ts +++ /dev/null @@ -1,51 +0,0 @@ -"use server"; - -import { - changePasswordFormSchema, - ChangePasswordFormType, -} from "@/schemas/user"; -import { changeUserPassword } from "@/service/user"; -import { isAxiosError } from "axios"; -import { revalidatePath } from "next/cache"; - -export default async function action(data: ChangePasswordFormType) { - const parsed = changePasswordFormSchema.safeParse(data); - - if (!parsed.success) { - return { - message: "제출양식에 문제가 있습니다. 확인해주세요", - success: false, - }; - } - const { password, newPassword, newPasswordConfirmation } = parsed.data; - - try { - await changeUserPassword({ - password, - newPassword, - newPasswordConfirmation, - }); - - revalidatePath("/mypage"); - - return { - message: "비밀번호 수정 성공", - success: true, - }; - } catch (error) { - if (isAxiosError(error)) { - console.log(error); - const message = - error.response?.data.message || "알 수 없는 에러가 발생했어요."; - return { - message: `비밀번호 수정 실패 : ${message}`, - success: false, - }; - } - - return { - message: "비밀번호 수정 실패", - success: false, - }; - } -} diff --git a/src/app/(common)/(user)/changePassword/page.tsx b/src/app/(common)/(user)/changePassword/page.tsx index 17e9a2244..3027de135 100644 --- a/src/app/(common)/(user)/changePassword/page.tsx +++ b/src/app/(common)/(user)/changePassword/page.tsx @@ -1,4 +1,4 @@ -import ChangePasswordForm from "../_components/ChangePasswordForm"; +import ChangePasswordForm from "@/components/user/ChangePasswordForm"; export default function ChangePasswordPage() { return ; diff --git a/src/app/(common)/(user)/editProfile/action.ts b/src/app/(common)/(user)/editProfile/action.ts deleted file mode 100644 index 0dbe0009f..000000000 --- a/src/app/(common)/(user)/editProfile/action.ts +++ /dev/null @@ -1,45 +0,0 @@ -"use server"; - -import { editProfileFormSchmea, EditProfileFormType } from "@/schemas/user"; -import { editProfileImage, uploadProfileImage } from "@/service/user"; -import { isAxiosError } from "axios"; - -export default async function action(data: EditProfileFormType) { - const parsed = editProfileFormSchmea.safeParse(data); - - if (!parsed.success) { - return { - message: "제출양식에 문제가 있습니다. 확인해주세요", - success: false, - }; - } - const { image } = parsed.data; - - if (image instanceof File) { - try { - const { url } = await uploadProfileImage(image); - await editProfileImage(url); - - return { - message: "프로필 이미지 업데이트 성공", - success: true, - }; - } catch (error) { - if (isAxiosError(error)) { - return { - message: `프로필 이미지 업데이트 실패 : ${error.message}`, - success: false, - }; - } - return { - message: "프로필 이미지 업데이트 실패", - success: false, - }; - } - } else { - return { - message: "이미지를 첨부해주세요", - success: false, - }; - } -} diff --git a/src/app/(common)/(user)/editProfile/page.tsx b/src/app/(common)/(user)/editProfile/page.tsx index 1fc5cffcf..ff779e3a7 100644 --- a/src/app/(common)/(user)/editProfile/page.tsx +++ b/src/app/(common)/(user)/editProfile/page.tsx @@ -1,15 +1,19 @@ -import { auth } from "@/auth"; -import EditProfileForm from "../_components/EditProfileForm"; -import { redirect } from "next/navigation"; -import { getUser } from "@/service/user"; +import EditProfileForm from "@/components/user/EditProfileForm"; +import { getQueryClient } from "@/util/getQueryClient"; +import { getUserOptions } from "@/service/user.queries"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; -export default async function EditProfilePage() { - const session = await auth(); - if (!session) { - redirect("/login"); - } +// build시에 ssg로 빌드되는것을 막기위해서 +export const dynamic = "force-dynamic"; - const { nickname, image } = await getUser(); +export default function EditProfilePage() { + const queryClient = getQueryClient(); - return ; + void queryClient.prefetchQuery(getUserOptions); + + return ( + + + + ); } diff --git a/src/app/(common)/(user)/layout.tsx b/src/app/(common)/(user)/layout.tsx index b53d76c29..d05465dc8 100644 --- a/src/app/(common)/(user)/layout.tsx +++ b/src/app/(common)/(user)/layout.tsx @@ -1,6 +1,6 @@ import { PageWrapper } from "@/components/Page"; import { ReactNode } from "react"; -import UserWrapper from "./_components/UserWrapper"; +import UserWrapper from "@/components/user/UserWrapper"; export default function UserLayout({ children }: { children: ReactNode }) { return ( diff --git a/src/app/(common)/(user)/mypage/page.tsx b/src/app/(common)/(user)/mypage/page.tsx index b6e199a22..4b5d392bc 100644 --- a/src/app/(common)/(user)/mypage/page.tsx +++ b/src/app/(common)/(user)/mypage/page.tsx @@ -1,25 +1,22 @@ -import { auth } from "@/auth"; -import { getUser, getUserActivity } from "@/service/user"; -import { redirect } from "next/navigation"; -import Profile from "../_components/Profile"; -import Activity from "../_components/Activity"; +import Profile from "@/components/user/Profile"; +import Activity from "@/components/user/Activity"; +import { getQueryClient } from "@/util/getQueryClient"; +import { dehydrate, HydrationBoundary } from "@tanstack/react-query"; +import { getUserActivityOptions, getUserOptions } from "@/service/user.queries"; + +// build시에 ssg로 빌드되는것을 막기위해서 +export const dynamic = "force-dynamic"; export default async function UserPage() { - const session = await auth(); - if (!session) { - redirect("/"); - } + const queryClient = getQueryClient(); - const { nickname, image, createdAt } = await getUser(); - const { products, favorites } = await getUserActivity(); + queryClient.prefetchQuery(getUserOptions); + queryClient.prefetchQuery(getUserActivityOptions); return ( - <> - - - + + + + ); } diff --git a/src/app/(common)/layout.tsx b/src/app/(common)/layout.tsx index a8fbbc170..1f6b1615f 100644 --- a/src/app/(common)/layout.tsx +++ b/src/app/(common)/layout.tsx @@ -1,6 +1,6 @@ import { PropsWithChildren } from "react"; -import { Header, Logo, Nav, Util } from "@components/Header"; -import { Footer } from "@components/Footer"; +import { Header, Logo, Nav, Util } from "@/components/layout/Header"; +import { Footer } from "@/components/layout/Footer"; import styles from "./layout.module.css"; export default function Layout({ children }: PropsWithChildren) { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index bb77904a1..6c734baba 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,7 +3,7 @@ import type { Metadata } from "next"; import { SessionProvider } from "next-auth/react"; import "@assets/scss/style.scss"; import QueryClientProvider from "@/context/QueryClientProvider"; -import { auth } from "@/auth"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; export const metadata: Metadata = { title: "판다마켓", @@ -16,15 +16,16 @@ export const metadata: Metadata = { }, }; -export default async function RootLayout({ children }: PropsWithChildren) { - const session = await auth(); - +export default function RootLayout({ children }: PropsWithChildren) { return (
- - {children} + + + {children} + +
diff --git a/src/auth.ts b/src/auth.ts index 1df59b6b8..fc8ce8f86 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,5 +1,5 @@ -import { signinFormSchmea } from "@/schemas/auth"; -import { login, refreshAccessToken } from "@/service/auth"; +import { signinFormSchmea } from "@/service/auth.schema"; +import { login, refreshAccessToken } from "@/service/auth.service"; import axios from "axios"; import { jwtDecode } from "jwt-decode"; import NextAuth, { CredentialsSignin } from "next-auth"; diff --git a/src/components/Comment/Comment.tsx b/src/components/Comment/Comment.tsx index 09ce4b432..7baf9172d 100644 --- a/src/components/Comment/Comment.tsx +++ b/src/components/Comment/Comment.tsx @@ -1,12 +1,13 @@ "use client"; import { useState } from "react"; -import useComment from "./useComment"; import { Author } from "@components/ui"; import { More } from "@components/Button"; import { CommentForm } from "."; import styles from "./Comment.module.scss"; -import { BoardName, Comment as CommentItem } from "@type/comment"; +import { BoardName, Comment as CommentItem } from "@/service/comment.type"; +import { useCommentDelete, useCommentModify } from "@/service/comment.queries"; +import { useSession } from "next-auth/react"; interface Comment { name: BoardName; @@ -15,14 +16,17 @@ interface Comment { export function Comment({ name, comment }: Comment) { const [isModify, setIsModify] = useState(false); + const { data: session } = useSession(); const { + id: commentId, content, updatedAt, - writer: { nickname, image }, + writer: { nickname, image, id: writerId }, } = comment; - - const { isOwner, handleUpdate, handleDelete } = useComment(name, comment); + const isOwner = writerId === Number(session?.user.id); + const { mutateAsync: handleUpdate } = useCommentModify(name, commentId); + const { mutate: handleDelete } = useCommentDelete(name, commentId); function handleModify() { if (!isOwner) { diff --git a/src/components/Comment/CommentAdd.tsx b/src/components/Comment/CommentAdd.tsx index 59f597177..a365a00b9 100644 --- a/src/components/Comment/CommentAdd.tsx +++ b/src/components/Comment/CommentAdd.tsx @@ -1,12 +1,14 @@ "use client"; import { CommentForm } from "."; -import useComment from "./useComment"; import styles from "./CommentAdd.module.scss"; -import { BoardName } from "@/types/comment"; +import { BoardName } from "@/service/comment.type"; +import { useCommentAdd } from "@/service/comment.queries"; +import { useParams } from "next/navigation"; export function CommentAdd({ name }: { name: BoardName }) { - const { handleSubmit } = useComment(name); + const { id } = useParams(); + const { mutateAsync: handleSubmit } = useCommentAdd(name, Number(id)); return (
diff --git a/src/components/Comment/CommentForm.tsx b/src/components/Comment/CommentForm.tsx index ca7aa7b28..eb06cfd54 100644 --- a/src/components/Comment/CommentForm.tsx +++ b/src/components/Comment/CommentForm.tsx @@ -3,17 +3,18 @@ import { FieldItem, Form, Textarea } from "@components/Field"; import { Author, Button } from "@components/ui"; import styles from "./CommentForm.module.scss"; -import { BoardName, Comment } from "@type/comment"; +import { BoardName, Comment } from "@/service/comment.type"; import useFormWithError from "@hooks/useFormWithError"; -import { CommentFormSchema, CommentFormType } from "@schemas/comment"; +import { CommentFormSchema, CommentFormType } from "@/service/comment.schema"; import { zodResolver } from "@hookform/resolvers/zod"; import { FieldAdapter } from "@components/adaptor/rhf"; import { COMMENT_PLACEHOLDER, COMMENT_TITLE } from "@/constants/message"; +import { isAxiosError } from "axios"; interface CommentForm { name: BoardName; initialData?: Comment; - onCommentSubmit: (data: CommentFormType) => Promise; + onCommentSubmit: (data: CommentFormType) => Promise; onClose?: () => void; isEdit?: boolean; } @@ -34,7 +35,9 @@ export function CommentForm({ } = useFormWithError({ mode: "onChange", resolver: zodResolver(CommentFormSchema), - defaultValues: initialData, + defaultValues: initialData || { + content: "", + }, }); function handleClose() { @@ -45,10 +48,16 @@ export function CommentForm({ async function onSubmit(data: CommentFormType) { try { await onCommentSubmit(data); - reset(); + reset({ + content: "", + }); onClose?.(); - } catch (err) { - throw err; + } catch (error) { + throw new Error( + isAxiosError(error) + ? error.response?.data.message + : "알 수 없는 에러가 발생했습니다." + ); } } diff --git a/src/components/Comment/CommentList.tsx b/src/components/Comment/CommentList.tsx index 9c151ff2a..8c06ae49f 100644 --- a/src/components/Comment/CommentList.tsx +++ b/src/components/Comment/CommentList.tsx @@ -9,19 +9,21 @@ import { COMMENT_LOADING, } from "@/constants/message"; import { BackToList } from "../Button"; -import useComments from "./useComments"; -import { BoardName, CommentList as CommentData } from "@/types/comment"; +import { BoardName } from "@/service/comment.type"; import { useParams } from "next/navigation"; +import { useGetComments } from "@/service/comment.queries"; interface CommentListType { name: BoardName; - data: CommentData; } -export function CommentList({ name, data }: CommentListType) { +export function CommentList({ name }: CommentListType) { const { id } = useParams(); - const { isLoading, error, comments, hasNextPage, fetchNextPage } = - useComments(Number(id), name, data); + const { isLoading, error, data, hasNextPage, fetchNextPage } = useGetComments( + name, + { id: Number(id), limit: 3 } + ); + const comments = data?.pages.flatMap((page) => page.list) || []; return ( <> diff --git a/src/components/Comment/useComment.ts b/src/components/Comment/useComment.ts deleted file mode 100644 index 63caac181..000000000 --- a/src/components/Comment/useComment.ts +++ /dev/null @@ -1,72 +0,0 @@ -"use client"; - -import { addComment, removeComment, updateComment } from "@service/comments"; -import { BoardName, Comment } from "@type/comment"; -import { CommentFormType } from "@schemas/comment"; -import { useParams } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { useQueryClient } from "@tanstack/react-query"; - -export default function useComment(name: BoardName, comment?: Comment) { - const { data: session } = useSession(); - const { id } = useParams(); - const productId = Number(id); - const isOwner = comment && Number(session?.user?.id) === comment.writer.id; - const queryClient = useQueryClient(); - - async function handleSubmit(data: CommentFormType) { - if (!session?.user) { - return alert("로그인이 필요합니다."); - } - - try { - await addComment(name, productId, data); - queryClient.invalidateQueries({ - queryKey: ["comments", name, Number(id)], - }); - alert("성공적으로 작성했습니다."); - } catch (err) { - throw err; - } - } - - async function handleUpdate(data: CommentFormType) { - if (!comment) return; - - if (!isOwner) { - return alert("작성자만 수정이 가능합니다."); - } - - try { - await updateComment(comment.id, data); - queryClient.invalidateQueries({ - queryKey: ["comments", name, Number(id)], - }); - alert("성공적으로 수정했습니다."); - } catch (err) { - throw err; - } - } - - async function handleDelete() { - if (!comment) return; - - if (!isOwner) { - return alert("작성자만 삭제가 가능합니다."); - } - - if (confirm("정말 삭제할까요?")) { - try { - await removeComment(comment.id); - alert("문의를 삭제했습니다."); - queryClient.invalidateQueries({ - queryKey: ["comments", name, Number(id)], - }); - } catch (err) { - console.log(err); - } - } - } - - return { isOwner, handleSubmit, handleUpdate, handleDelete }; -} diff --git a/src/components/Comment/useComments.ts b/src/components/Comment/useComments.ts deleted file mode 100644 index 8e542df63..000000000 --- a/src/components/Comment/useComments.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { getComments } from "@service/comments"; -import { BoardName, CommentList } from "@type/comment"; -import { useInfiniteQuery } from "@tanstack/react-query"; - -export default function useComments( - id: number, - name: BoardName, - initialComments: CommentList -) { - const { isLoading, error, data, fetchNextPage, hasNextPage } = - useInfiniteQuery({ - queryKey: ["comments", name, id], - queryFn: ({ pageParam = undefined }) => - getComments(name, { id, cursor: pageParam }), - getNextPageParam: (lastPage) => lastPage.nextCursor, - initialPageParam: 0, - initialData: { - pageParams: [0], - pages: [initialComments], - }, - }); - const comments = data?.pages.flatMap((page) => page.list) || []; - - return { - isLoading, - error, - comments, - hasNextPage, - fetchNextPage, - }; -} diff --git a/src/components/Field/TagsInput.tsx b/src/components/Field/TagsInput.tsx index 16700beed..461f96aa0 100644 --- a/src/components/Field/TagsInput.tsx +++ b/src/components/Field/TagsInput.tsx @@ -3,7 +3,7 @@ import clsx from "clsx"; import { Tags } from "@components/ui"; import { Error } from "@components/Field"; import styles from "./Input.module.scss"; -import { Tags as TagsType } from "@type/product"; +import { Tags as TagsType } from "@/service/product.type"; interface TagsInputProps { value: TagsType; diff --git a/src/components/Header/Util.tsx b/src/components/Header/Util.tsx deleted file mode 100644 index 92bc9248e..000000000 --- a/src/components/Header/Util.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Button } from "@components/ui"; -import { Profile } from "@components/Header"; -import styles from "./Util.module.scss"; -import { auth } from "@/auth"; -import { getUser } from "@/service/user"; - -export async function Util() { - const session = await auth(); - - if (!session) { - return ( -
- -
- ); - } - - const { nickname, image } = await getUser(); - - return ( -
- -
- ); -} diff --git a/src/app/(auth)/_components/AuthContainer.module.scss b/src/components/auth/AuthContainer.module.scss similarity index 100% rename from src/app/(auth)/_components/AuthContainer.module.scss rename to src/components/auth/AuthContainer.module.scss diff --git a/src/app/(auth)/_components/AuthContainer.tsx b/src/components/auth/AuthContainer.tsx similarity index 100% rename from src/app/(auth)/_components/AuthContainer.tsx rename to src/components/auth/AuthContainer.tsx diff --git a/src/app/(auth)/_components/LoginForm.tsx b/src/components/auth/LoginForm.tsx similarity index 69% rename from src/app/(auth)/_components/LoginForm.tsx rename to src/components/auth/LoginForm.tsx index b3b649cbe..9fd0416de 100644 --- a/src/app/(auth)/_components/LoginForm.tsx +++ b/src/components/auth/LoginForm.tsx @@ -1,13 +1,12 @@ "use client"; -import { useRouter } from "next/navigation"; import { FieldItem, Form, Input } from "@components/Field"; import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; import { zodResolver } from "@hookform/resolvers/zod"; -import { signinFormSchmea, SigninFormType } from "@schemas/auth"; +import { signinFormSchmea, SigninFormType } from "@/service/auth.schema"; import { FieldAdapter } from "@components/adaptor/rhf"; -import action from "../login/action"; +import { signIn } from "next-auth/react"; export default function LoginForm() { const { @@ -23,15 +22,23 @@ export default function LoginForm() { password: "", }, }); - const router = useRouter(); async function onSubmit(data: SigninFormType) { - const response = await action(data); - if (response.success) { - router.replace("/items"); - } else { - throw new Error(response.message); + const response = await signIn("credentials", { + ...data, + redirect: false, + }); + + // https://github.com/nextauthjs/next-auth/issues/9465 + // redirect false로 응답을 받아볼때, 로그인실패도 ok가 true 전달되고 있음 + // 임시로 message와 코드로 실패처리 + if (response?.error === "CredentialsSignin") { + throw new Error(response.code); } + + // 기본 signIn의 locatio href를 'redirect:false'로 잠시 막아놔서 + // 직접 reload를 통해 각종 캐시(리액트쿼리, 세션등을 초기) 초기화. + window.location.reload(); } return ( diff --git a/src/app/(auth)/_components/Oauth.module.scss b/src/components/auth/Oauth.module.scss similarity index 100% rename from src/app/(auth)/_components/Oauth.module.scss rename to src/components/auth/Oauth.module.scss diff --git a/src/app/(auth)/_components/Oauth.tsx b/src/components/auth/Oauth.tsx similarity index 100% rename from src/app/(auth)/_components/Oauth.tsx rename to src/components/auth/Oauth.tsx diff --git a/src/app/(auth)/_components/SignupForm.tsx b/src/components/auth/SignupForm.tsx similarity index 85% rename from src/app/(auth)/_components/SignupForm.tsx rename to src/components/auth/SignupForm.tsx index 8a7ebc0d7..1aa61ed03 100644 --- a/src/app/(auth)/_components/SignupForm.tsx +++ b/src/components/auth/SignupForm.tsx @@ -5,9 +5,10 @@ import { FieldItem, Form, Input } from "@components/Field"; import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; import { zodResolver } from "@hookform/resolvers/zod"; -import { signupFormSchema, SignupFormType } from "@schemas/auth"; +import { signupFormSchema, SignupFormType } from "@/service/auth.schema"; import { FieldAdapter } from "@components/adaptor/rhf"; -import action from "../signup/action"; +import { signUp } from "@/service/auth.service"; +import { isAxiosError } from "axios"; export default function SignupForm() { const { @@ -28,11 +29,16 @@ export default function SignupForm() { const router = useRouter(); async function onSubmit(data: SignupFormType) { - const response = await action(data); - if (response.success) { + try { + await signUp(data); + alert("가입에 성공했습니다. 로그인을 해주세요"); router.replace("/login"); - } else { - throw new Error(response.message); + } catch (error) { + const message = isAxiosError(error) + ? error.response?.data.message + : "알 수 없는 에러가 발생했어요."; + + throw new Error(message); } } diff --git a/src/app/(common)/(board)/_components/ArticleForm.tsx b/src/components/board/ArticleForm.tsx similarity index 90% rename from src/app/(common)/(board)/_components/ArticleForm.tsx rename to src/components/board/ArticleForm.tsx index 5b184d9a5..066c64684 100644 --- a/src/app/(common)/(board)/_components/ArticleForm.tsx +++ b/src/components/board/ArticleForm.tsx @@ -11,10 +11,11 @@ import { import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Article } from "@/types/article"; +import { Article } from "@/service/article.type"; import { FieldAdapter } from "@components/adaptor/rhf"; import { useRouter } from "next/navigation"; -import { ArticleFormSchema, ArticleFormType } from "@/schemas/article"; +import { ArticleFormSchema, ArticleFormType } from "@/service/article.schema"; +import { isAxiosError } from "axios"; interface ArticleAddFormProps { mode: "add"; @@ -56,8 +57,12 @@ export default function ArticleForm(props: ArticleFormProps) { mode === "add" ? "성공적으로 작성했습니다." : "성공적으로 수정했습니다." ); router.replace(id ? `/boards/${id}` : "boards"); - } catch (err) { - throw err; + } catch (error) { + throw new Error( + isAxiosError(error) + ? error.response?.data.message + : "알 수 없는 에러가 발생했습니다." + ); } } diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BestItem.module.scss b/src/components/board/BestItem.module.scss similarity index 100% rename from src/app/(common)/(board)/boards/(lists)/_components/BestItem.module.scss rename to src/components/board/BestItem.module.scss diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BestItem.tsx b/src/components/board/BestItem.tsx similarity index 96% rename from src/app/(common)/(board)/boards/(lists)/_components/BestItem.tsx rename to src/components/board/BestItem.tsx index 3d93ae55a..4bc09a78e 100644 --- a/src/app/(common)/(board)/boards/(lists)/_components/BestItem.tsx +++ b/src/components/board/BestItem.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import Image from "next/image"; -import { Article } from "@/types/article"; +import { Article } from "@/service/article.type"; import { Author, Badge, Date, Like, Thumbnail } from "@/components/ui"; import bestIcon from "@assets/img/icon/icon_best.svg"; import styles from "./BestItem.module.scss"; diff --git a/src/components/board/BestList.tsx b/src/components/board/BestList.tsx new file mode 100644 index 000000000..e8b0c9c0b --- /dev/null +++ b/src/components/board/BestList.tsx @@ -0,0 +1,31 @@ +"use client"; + +import BoardListWrapper from "./BoardListWrapper"; +import BestItem from "./BestItem"; +import { useGetArticles } from "@/service/article.queries"; +import { Loading } from "@/components/ui/Loading"; + +export default function BestList() { + const { data, isPending } = useGetArticles("best", { + pageSize: 3, + orderBy: "like", + }); + + if (isPending) { + return loading...; + } + + const list = data?.list ?? []; + + if (list.length === 0) { + return
게시물이 없습니다.
; + } + + return ( + <> + + {(item) => } + + + ); +} diff --git a/src/app/(common)/(board)/_components/BoardDetail.module.scss b/src/components/board/BoardDetail.module.scss similarity index 100% rename from src/app/(common)/(board)/_components/BoardDetail.module.scss rename to src/components/board/BoardDetail.module.scss diff --git a/src/app/(common)/(board)/_components/BoardDetail.tsx b/src/components/board/BoardDetail.tsx similarity index 72% rename from src/app/(common)/(board)/_components/BoardDetail.tsx rename to src/components/board/BoardDetail.tsx index eee083b7a..356a5dc5e 100644 --- a/src/app/(common)/(board)/_components/BoardDetail.tsx +++ b/src/components/board/BoardDetail.tsx @@ -1,40 +1,32 @@ "use client"; -import { Article } from "@/types/article"; import { useSession } from "next-auth/react"; -import { useRouter } from "next/navigation"; -import useArticleActions from "./useArticleActions"; +import { notFound, useParams, useRouter } from "next/navigation"; import { More } from "@/components/Button"; import { Author, Fullscreen, LikeButton, Thumbnail } from "@/components/ui"; import styles from "./BoardDetail.module.scss"; +import { + useArticleDelete, + useArticleToggleLike, + useGetArticle, +} from "@/service/article.queries"; +import { Loading } from "@/components/ui/Loading"; -interface BoardDetailProps { - detail: Article; -} - -export default function BoardDetail({ detail }: BoardDetailProps) { - const { - id, - image, - title, - content, - writer: { nickname, id: ownerId }, - updatedAt, - likeCount, - isLiked, - } = detail; - - const { data: session } = useSession(); +export default function BoardDetail() { const router = useRouter(); - const { handleLike, handleArticleDelete } = useArticleActions(id); - const isOwner = ownerId === Number(session?.user?.id); + const { data: session } = useSession(); + const { id } = useParams<{ id: string }>(); + const articleId = Number(id); + + const { data: detail, isPending } = useGetArticle(articleId); + const { mutate: toggleLike } = useArticleToggleLike(articleId); + const { mutateAsync: deleteArticle } = useArticleDelete(articleId); async function handleToggleLike() { if (!session?.user) { return alert("로그인이 필요합니다."); } - await handleLike(!isLiked); - router.refresh(); + toggleLike(!isLiked); } function handleModify() { @@ -52,15 +44,34 @@ export default function BoardDetail({ detail }: BoardDetailProps) { if (confirm("정말 삭제할까요?")) { try { - await handleArticleDelete(); - alert("상품을 삭제했습니다."); + await deleteArticle(); + alert("게시글을 삭제했습니다."); router.replace("/boards"); } catch (err) { - console.log(err); + console.error(err); } } } + if (isPending) { + return loading...; + } + + if (!detail) { + notFound(); + } + + const { + image, + title, + content, + writer: { nickname, id: ownerId }, + updatedAt, + likeCount, + isLiked, + } = detail; + const isOwner = ownerId === Number(session?.user?.id); + return (
diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardFilter.module.scss b/src/components/board/BoardFilter.module.scss similarity index 100% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardFilter.module.scss rename to src/components/board/BoardFilter.module.scss diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardFilter.tsx b/src/components/board/BoardFilter.tsx similarity index 100% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardFilter.tsx rename to src/components/board/BoardFilter.tsx diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardItem.module.scss b/src/components/board/BoardItem.module.scss similarity index 100% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardItem.module.scss rename to src/components/board/BoardItem.module.scss diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardItem.tsx b/src/components/board/BoardItem.tsx similarity index 95% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardItem.tsx rename to src/components/board/BoardItem.tsx index 3c9b79575..d339995c5 100644 --- a/src/app/(common)/(board)/boards/(lists)/_components/BoardItem.tsx +++ b/src/components/board/BoardItem.tsx @@ -1,5 +1,5 @@ import Link from "next/link"; -import { Article } from "@/types/article"; +import { Article } from "@/service/article.type"; import { Author, Like, Thumbnail } from "@/components/ui"; import styles from "./BoardItem.module.scss"; diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardList.tsx b/src/components/board/BoardList.tsx similarity index 68% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardList.tsx rename to src/components/board/BoardList.tsx index 69ffa085d..670e768ab 100644 --- a/src/app/(common)/(board)/boards/(lists)/_components/BoardList.tsx +++ b/src/components/board/BoardList.tsx @@ -2,34 +2,45 @@ import useParams from "@/hooks/useParams"; import usePagination from "@/hooks/usePagination"; -import { PaginationResponse } from "@/types/common"; -import { Article } from "@/types/article"; import { Pagination } from "@/components/Pagination"; import { Message } from "@/components/ui"; import BoardItem from "./BoardItem"; import BoardListWrapper from "./BoardListWrapper"; +import { useGetArticles } from "@/service/article.queries"; +import { Loading } from "@/components/ui/Loading"; -interface BoardListProps { - data: PaginationResponse
; -} -export default function BoardList({ data }: BoardListProps) { +export default function BoardList() { const { searchParams, handleParams } = useParams(); + const page = Number(searchParams.get("page")) || 1; - const pageSize = Number(searchParams.get("pageSize")) || 10; const keyword = searchParams.get("keyword") || ""; + const orderBy = searchParams.get("orderBy") || "recent"; + const pageSize = 10; const visibleCount = 5; - const { list, totalCount } = data; + + const { data, isPending } = useGetArticles("all", { + page, + pageSize, + keyword, + orderBy, + }); const pagination = usePagination({ page, pageSize, - totalCount, + totalCount: data?.totalCount || 0, visibleCount, onChange: (pageNumber) => { handleParams({ page: pageNumber.toString() }); }, }); + const list = data?.list ?? []; + + if (isPending) { + return loading...; + } + if (list.length === 0) { return ( diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardListWrapper.module.scss b/src/components/board/BoardListWrapper.module.scss similarity index 100% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardListWrapper.module.scss rename to src/components/board/BoardListWrapper.module.scss diff --git a/src/app/(common)/(board)/boards/(lists)/_components/BoardListWrapper.tsx b/src/components/board/BoardListWrapper.tsx similarity index 89% rename from src/app/(common)/(board)/boards/(lists)/_components/BoardListWrapper.tsx rename to src/components/board/BoardListWrapper.tsx index ac77cc503..1d12fdeba 100644 --- a/src/app/(common)/(board)/boards/(lists)/_components/BoardListWrapper.tsx +++ b/src/components/board/BoardListWrapper.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; import clsx from "clsx"; -import { Article, ListMode } from "@/types/article"; +import { Article, ListMode } from "@/service/article.type"; import styles from "./BoardListWrapper.module.scss"; interface BoardListWrapper { diff --git a/src/app/(common)/(landing)/_components/Banner.module.scss b/src/components/landing/Banner.module.scss similarity index 100% rename from src/app/(common)/(landing)/_components/Banner.module.scss rename to src/components/landing/Banner.module.scss diff --git a/src/app/(common)/(landing)/_components/Banner.tsx b/src/components/landing/Banner.tsx similarity index 100% rename from src/app/(common)/(landing)/_components/Banner.tsx rename to src/components/landing/Banner.tsx diff --git a/src/app/(common)/(landing)/_components/Feature.module.scss b/src/components/landing/Feature.module.scss similarity index 100% rename from src/app/(common)/(landing)/_components/Feature.module.scss rename to src/components/landing/Feature.module.scss diff --git a/src/app/(common)/(landing)/_components/Feature.tsx b/src/components/landing/Feature.tsx similarity index 100% rename from src/app/(common)/(landing)/_components/Feature.tsx rename to src/components/landing/Feature.tsx diff --git a/src/app/(common)/(landing)/_components/landingContents.ts b/src/components/landing/landingContents.ts similarity index 100% rename from src/app/(common)/(landing)/_components/landingContents.ts rename to src/components/landing/landingContents.ts diff --git a/src/components/Footer/Footer.module.scss b/src/components/layout/Footer/Footer.module.scss similarity index 100% rename from src/components/Footer/Footer.module.scss rename to src/components/layout/Footer/Footer.module.scss diff --git a/src/components/Footer/Footer.tsx b/src/components/layout/Footer/Footer.tsx similarity index 100% rename from src/components/Footer/Footer.tsx rename to src/components/layout/Footer/Footer.tsx diff --git a/src/components/Footer/index.ts b/src/components/layout/Footer/index.ts similarity index 100% rename from src/components/Footer/index.ts rename to src/components/layout/Footer/index.ts diff --git a/src/components/Header/Header.module.scss b/src/components/layout/Header/Header.module.scss similarity index 100% rename from src/components/Header/Header.module.scss rename to src/components/layout/Header/Header.module.scss diff --git a/src/components/Header/Header.tsx b/src/components/layout/Header/Header.tsx similarity index 100% rename from src/components/Header/Header.tsx rename to src/components/layout/Header/Header.tsx diff --git a/src/components/Header/Logo.module.scss b/src/components/layout/Header/Logo.module.scss similarity index 100% rename from src/components/Header/Logo.module.scss rename to src/components/layout/Header/Logo.module.scss diff --git a/src/components/Header/Logo.tsx b/src/components/layout/Header/Logo.tsx similarity index 100% rename from src/components/Header/Logo.tsx rename to src/components/layout/Header/Logo.tsx diff --git a/src/components/Header/Nav.module.scss b/src/components/layout/Header/Nav.module.scss similarity index 100% rename from src/components/Header/Nav.module.scss rename to src/components/layout/Header/Nav.module.scss diff --git a/src/components/Header/Nav.tsx b/src/components/layout/Header/Nav.tsx similarity index 100% rename from src/components/Header/Nav.tsx rename to src/components/layout/Header/Nav.tsx diff --git a/src/components/Header/Profile.tsx b/src/components/layout/Header/Profile.tsx similarity index 69% rename from src/components/Header/Profile.tsx rename to src/components/layout/Header/Profile.tsx index fda2d1d65..421eba763 100644 --- a/src/components/Header/Profile.tsx +++ b/src/components/layout/Header/Profile.tsx @@ -2,18 +2,17 @@ import { Dropdown, Avatar } from "@components/ui"; import { signOut } from "next-auth/react"; -import { useRouter } from "next/navigation"; - +import Link from "next/link"; interface ProfileProps { nickname: string; image: string; } export function Profile({ nickname, image }: ProfileProps) { - const router = useRouter(); - - function handleLogout() { + async function handleLogout() { if (confirm("정말로 로그아웃 하시겠습니까?")) { + // 기본 signOut의 locatio href의 행동을 통해 + // 각종 캐시(리액트쿼리, 세션등을 초기) 초기화. signOut(); } } @@ -24,8 +23,8 @@ export function Profile({ nickname, image }: ProfileProps) { - router.push("/mypage")}> - 내정보 + + 내정보 로그아웃 diff --git a/src/components/Header/Util.module.scss b/src/components/layout/Header/Util.module.scss similarity index 100% rename from src/components/Header/Util.module.scss rename to src/components/layout/Header/Util.module.scss diff --git a/src/components/layout/Header/Util.tsx b/src/components/layout/Header/Util.tsx new file mode 100644 index 000000000..67d6e6033 --- /dev/null +++ b/src/components/layout/Header/Util.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { Button } from "@components/ui"; +import { Profile } from "@/components/layout/Header"; +import styles from "./Util.module.scss"; +import { useSession } from "next-auth/react"; +import { useQuery } from "@tanstack/react-query"; +import { getUserOptions } from "@/service/user.queries"; + +export function Util() { + const { data: session } = useSession(); + const { data } = useQuery({ + ...getUserOptions, + enabled: !!session, + }); + + if (!session) { + return ( +
+ +
+ ); + } + + return ( +
+ {data && } +
+ ); +} diff --git a/src/components/Header/index.ts b/src/components/layout/Header/index.ts similarity index 100% rename from src/components/Header/index.ts rename to src/components/layout/Header/index.ts diff --git a/src/components/market/BestList.tsx b/src/components/market/BestList.tsx new file mode 100644 index 000000000..b8ea4cf3a --- /dev/null +++ b/src/components/market/BestList.tsx @@ -0,0 +1,31 @@ +"use client"; + +import ProductItem from "./ProductItem"; +import ProductListWrapper from "./ProductListWrapper"; +import { useGetProducts } from "@/service/product.queries"; +import { Loading } from "@/components/ui/Loading"; + +export default function BestList() { + const { data, isPending } = useGetProducts("best", { + pageSize: 4, + orderBy: "favorite", + }); + + if (isPending) { + return loading...; + } + + const list = data?.list ?? []; + + if (list.length === 0) { + return
상품이 없습니다.
; + } + + return ( + <> + + {(item) => } + + + ); +} diff --git a/src/app/(common)/(market)/_components/ProductDetail.module.scss b/src/components/market/ProductDetail.module.scss similarity index 93% rename from src/app/(common)/(market)/_components/ProductDetail.module.scss rename to src/components/market/ProductDetail.module.scss index ec5279547..bc1f3c049 100644 --- a/src/app/(common)/(market)/_components/ProductDetail.module.scss +++ b/src/components/market/ProductDetail.module.scss @@ -110,3 +110,11 @@ font-weight: 600; } } + +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 4em 2em; + opacity: 0.3; +} diff --git a/src/app/(common)/(market)/_components/ProductDetail.tsx b/src/components/market/ProductDetail.tsx similarity index 80% rename from src/app/(common)/(market)/_components/ProductDetail.tsx rename to src/components/market/ProductDetail.tsx index 0355323e8..b73051bc8 100644 --- a/src/app/(common)/(market)/_components/ProductDetail.tsx +++ b/src/components/market/ProductDetail.tsx @@ -11,41 +11,31 @@ import { import { More } from "@components/Button"; import { toWon } from "@util/formatter"; import styles from "./ProductDetail.module.scss"; -import { Product } from "@type/product"; import Link from "next/link"; -import { useRouter } from "next/navigation"; -import useProductActions from "./useProductActions"; +import { notFound, useParams, useRouter } from "next/navigation"; +import { + useGetProduct, + useProductDelete, + useProductToggleLike, +} from "@/service/product.queries"; import { useSession } from "next-auth/react"; +import { Loading } from "@/components/ui/Loading"; -interface ProductDetail { - detail: Product; -} - -export default function ProductDetail({ detail }: ProductDetail) { - const { - id, - images, - name, - price, - description, - tags, - ownerId, - ownerNickname, - updatedAt, - favoriteCount, - isFavorite, - } = detail; - const { data: session } = useSession(); +export default function ProductDetail() { const router = useRouter(); - const { handleLike, handleProductDelete } = useProductActions(id); - const isOwner = ownerId === Number(session?.user?.id); + const { data: session } = useSession(); + const { id } = useParams<{ id: string }>(); + const productId = Number(id); + + const { data: detail, isPending } = useGetProduct(productId); + const { mutate: toggleLike } = useProductToggleLike(productId); + const { mutateAsync: deleteProdcut } = useProductDelete(productId); async function handleToggleLike() { if (!session?.user) { return alert("로그인이 필요합니다."); } - await handleLike(!isFavorite); - router.refresh(); + toggleLike(!isFavorite); } function handleModify() { @@ -63,15 +53,37 @@ export default function ProductDetail({ detail }: ProductDetail) { if (confirm("정말 삭제할까요?")) { try { - await handleProductDelete(); + await deleteProdcut(); alert("상품을 삭제했습니다."); router.replace("/items"); } catch (err) { - console.log(err); + console.error(err); } } } + if (isPending) { + return loading...; + } + + if (!detail) { + notFound(); + } + + const { + images, + name, + price, + description, + tags, + ownerId, + ownerNickname, + updatedAt, + favoriteCount, + isFavorite, + } = detail; + const isOwner = ownerId === Number(session?.user?.id); + return (
diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductFilter.module.scss b/src/components/market/ProductFilter.module.scss similarity index 100% rename from src/app/(common)/(market)/items/(lists)/_components/ProductFilter.module.scss rename to src/components/market/ProductFilter.module.scss diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductFilter.tsx b/src/components/market/ProductFilter.tsx similarity index 100% rename from src/app/(common)/(market)/items/(lists)/_components/ProductFilter.tsx rename to src/components/market/ProductFilter.tsx diff --git a/src/app/(common)/(market)/_components/ProductForm.tsx b/src/components/market/ProductForm.tsx similarity index 92% rename from src/app/(common)/(market)/_components/ProductForm.tsx rename to src/components/market/ProductForm.tsx index 228c64ed8..d313be9c5 100644 --- a/src/app/(common)/(market)/_components/ProductForm.tsx +++ b/src/components/market/ProductForm.tsx @@ -12,11 +12,12 @@ import { } from "@components/Field"; import { Button } from "@components/ui"; import useFormWithError from "@hooks/useFormWithError"; -import { ProductFormSchema, ProductFormType } from "@schemas/product"; +import { ProductFormSchema, ProductFormType } from "@/service/product.schema"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Product } from "@type/product"; +import { Product } from "@/service/product.type"; import { FieldAdapter } from "@components/adaptor/rhf"; import { useRouter } from "next/navigation"; +import { isAxiosError } from "axios"; interface ProductAddFormProps { mode: "add"; @@ -62,8 +63,12 @@ export default function ProductForm(props: ProductFormProps) { mode === "add" ? "성공적으로 작성했습니다." : "성공적으로 수정했습니다." ); router.replace(id ? `/items/${id}` : "/items"); - } catch (err) { - throw err; + } catch (error) { + throw new Error( + isAxiosError(error) + ? error.response?.data.message + : "알 수 없는 에러가 발생했습니다." + ); } } diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductItem.module.scss b/src/components/market/ProductItem.module.scss similarity index 100% rename from src/app/(common)/(market)/items/(lists)/_components/ProductItem.module.scss rename to src/components/market/ProductItem.module.scss diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductItem.tsx b/src/components/market/ProductItem.tsx similarity index 96% rename from src/app/(common)/(market)/items/(lists)/_components/ProductItem.tsx rename to src/components/market/ProductItem.tsx index 0dbb3f603..24aafc3a0 100644 --- a/src/app/(common)/(market)/items/(lists)/_components/ProductItem.tsx +++ b/src/components/market/ProductItem.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { Like, Thumbnail } from "@components/ui"; import { toWon } from "@util/formatter"; import styles from "./ProductItem.module.scss"; -import { Product } from "@type/product"; +import { Product } from "@/service/product.type"; function HighLightWithKeyword({ content, diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductList.module.scss b/src/components/market/ProductList.module.scss similarity index 100% rename from src/app/(common)/(market)/items/(lists)/_components/ProductList.module.scss rename to src/components/market/ProductList.module.scss diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductList.tsx b/src/components/market/ProductList.tsx similarity index 76% rename from src/app/(common)/(market)/items/(lists)/_components/ProductList.tsx rename to src/components/market/ProductList.tsx index acbe48d52..2681a0e7b 100644 --- a/src/app/(common)/(market)/items/(lists)/_components/ProductList.tsx +++ b/src/components/market/ProductList.tsx @@ -4,46 +4,56 @@ import { useEffect } from "react"; import useParams from "@/hooks/useParams"; import usePagination from "@/hooks/usePagination"; import useResponsive from "@/hooks/useResponsive"; -import { Product } from "@type/product"; -import { PaginationResponse } from "@/types/common"; import { Message } from "@components/ui"; import { Pagination } from "@/components/Pagination"; import ProductListWrapper from "./ProductListWrapper"; import ProductItem from "./ProductItem"; +import { useGetProducts } from "@/service/product.queries"; +import { Loading } from "@/components/ui/Loading"; -interface ProductListProps { - data: PaginationResponse; -} - -export default function ProductList({ data }: ProductListProps) { +export default function ProductList() { const { searchParams, handleParams } = useParams(); + const page = Number(searchParams.get("page")) || 1; const currentPageSize = Number(searchParams.get("pageSize")) || 10; const keyword = searchParams.get("keyword") || ""; + const orderBy = searchParams.get("orderBy") || "recent"; + const visibleCount = 5; const pageSize = useResponsive({ pc: 10, tablet: 6, mobile: 4, }); - const visibleCount = 5; - const { list, totalCount } = data; - useEffect(() => { - if (pageSize === currentPageSize) return; - - handleParams({ pageSize }); - }, [pageSize, currentPageSize, handleParams]); + const { data, isPending } = useGetProducts("all", { + page, + pageSize, + keyword, + orderBy, + }); const pagination = usePagination({ page, pageSize, - totalCount, + totalCount: data?.totalCount || 0, visibleCount, onChange: (pageNumber) => { handleParams({ page: pageNumber.toString() }); }, }); + const list = data?.list ?? []; + + useEffect(() => { + if (pageSize === currentPageSize) return; + + handleParams({ pageSize }); + }, [pageSize, currentPageSize, handleParams]); + + if (isPending) { + return loading...; + } + if (list.length === 0) { return ( diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductListWrapper.module.scss b/src/components/market/ProductListWrapper.module.scss similarity index 100% rename from src/app/(common)/(market)/items/(lists)/_components/ProductListWrapper.module.scss rename to src/components/market/ProductListWrapper.module.scss diff --git a/src/app/(common)/(market)/items/(lists)/_components/ProductListWrapper.tsx b/src/components/market/ProductListWrapper.tsx similarity index 90% rename from src/app/(common)/(market)/items/(lists)/_components/ProductListWrapper.tsx rename to src/components/market/ProductListWrapper.tsx index 0a5ec7e4d..bb52e1745 100644 --- a/src/app/(common)/(market)/items/(lists)/_components/ProductListWrapper.tsx +++ b/src/components/market/ProductListWrapper.tsx @@ -1,6 +1,6 @@ import { ReactNode } from "react"; import clsx from "clsx"; -import { ListMode, Product } from "@/types/product"; +import { ListMode, Product } from "@/service/product.type"; import styles from "./ProductListWrapper.module.scss"; interface ProductListWrapper { diff --git a/src/components/ui/Dropdown.tsx b/src/components/ui/Dropdown.tsx index 27e5f4f0b..91139dbf0 100644 --- a/src/components/ui/Dropdown.tsx +++ b/src/components/ui/Dropdown.tsx @@ -3,6 +3,7 @@ import { cloneElement, createContext, + isValidElement, ReactElement, ReactNode, useContext, @@ -115,9 +116,11 @@ function Menu({ children }: { children: ReactNode }) { function Item({ onClick, children, + asChild = false, }: { onClick?: () => void; children: ReactNode; + asChild?: boolean; }) { const { setIsOpen } = useDropdownDispatch(); @@ -128,6 +131,20 @@ function Item({ } } + if ( + asChild && + isValidElement<{ onClick?: () => void; className?: string }>(children) + ) { + return ( +
  • + {cloneElement(children, { + onClick: handleClick, + className: styles.item, + })} +
  • + ); + } + return (