diff --git a/next.config.mjs b/next.config.mjs index cd1aa1a7..f9c48da9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -20,17 +20,13 @@ const nextConfig = { }, webpack(config) { - // Grab the existing rule that handles SVG imports const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); - config.module.rules.push( - // Reapply the existing rule, but only for svg imports ending in ?url { ...fileLoaderRule, test: /\.svg$/i, - resourceQuery: /url/, // *.svg?url + resourceQuery: /url/, }, - // Convert all other *.svg imports to React components { test: /\.svg$/i, issuer: fileLoaderRule.issuer, @@ -56,10 +52,7 @@ const nextConfig = { ], }, ); - - // Modify the file loader rule to ignore *.svg, since we have it handled now. fileLoaderRule.exclude = /\.svg$/i; - config.module.rules.push({ test: /\.(woff|woff2|eot|ttf|otf)$/i, type: 'asset/resource', @@ -67,16 +60,244 @@ const nextConfig = { filename: 'static/fonts/[name][ext]', }, }); - return config; }, - sassOptions: { includePaths: ['styles'], additionalData: `@import "src/styles/_globals.scss";`, }, - transpilePackages: ['three'], + + async redirects() { + return [ + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*MJ12bot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*Amazonbot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*ClaudeBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*DotBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*Linkbot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*Iframely.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*AhrefsBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*PetalBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*BLEXBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*woorankreview.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*Barkrowler.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*Neevabot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*SeoSiteCheckup.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*SemrushBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*RSiteAuditor.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*YandexBot.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*GrapeshotCrawler.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/(.*)', + has: [ + { + type: 'header', + key: 'User-Agent', + value: '(.*proximic.*)', + }, + ], + destination: '/401', + permanent: false, + }, + { + source: '/wordpress', + destination: '/401', + permanent: true, + }, + { + source: '/wp-login.php', + destination: '/401', + permanent: true, + }, + ]; + }, }; export default nextConfig; diff --git a/src/app/shop/[category]/[productId]/_utils/metadata.ts b/src/app/shop/[category]/[productId]/_utils/metadata.ts new file mode 100644 index 00000000..bf820848 --- /dev/null +++ b/src/app/shop/[category]/[productId]/_utils/metadata.ts @@ -0,0 +1,19 @@ +import { ProductType } from '@/types/productType'; +import { Metadata } from 'next'; + +interface ProductMetadataParams { + product: ProductType; +} + +export const createProductMetadata = ({ product }: ProductMetadataParams): Metadata => { + const { name, detailsImg, categoryName, reviewscount, price, scope, views } = product; + + return { + title: `${name} - 상품 정보`, + description: `${name}은(는) ${categoryName} 카테고리의 제품으로, 가격은 ${price}원입니다. 총 ${reviewscount}개의 리뷰에서 평균 평점 ${scope}점을 기록했으며, ${views}회의 조회수를 기록한 인기 상품입니다.`, + openGraph: { + title: `${name} - 상품 정보`, + images: detailsImg, + }, + }; +}; diff --git a/src/app/shop/[category]/[productId]/page.tsx b/src/app/shop/[category]/[productId]/page.tsx index 658a18ba..033a4864 100644 --- a/src/app/shop/[category]/[productId]/page.tsx +++ b/src/app/shop/[category]/[productId]/page.tsx @@ -1,5 +1,9 @@ +import { getQueryClient } from '@/libs/client'; +import { Metadata } from 'next'; +import { fetchProductQuery } from '@/libs/prefetchers'; import ProductDetail from './_components/Product/ProductDetail'; import DetailTab from './_components/TabContents/DetailTab'; +import { createProductMetadata } from './_utils/metadata'; interface ProductDetailParams { params: { @@ -7,6 +11,20 @@ interface ProductDetailParams { }; } +export const generateMetadata = async ({ params }: ProductDetailParams): Promise => { + const { productId } = params; + const queryClient = getQueryClient(); + const product = await fetchProductQuery(queryClient, productId); + + if (!product) { + return { + title: '상품 정보를 불러올 수 없습니다.', + }; + } + + return createProductMetadata({ product }); +}; + export default async function ProductDetailPage({ params }: ProductDetailParams) { const { productId } = params; const localProductId = Number(productId); diff --git a/src/app/shop/layout.tsx b/src/app/shop/layout.tsx index 1e5525e9..91bb07d3 100644 --- a/src/app/shop/layout.tsx +++ b/src/app/shop/layout.tsx @@ -1,22 +1,18 @@ -// import { ReactNode } from 'react'; +import { ReactNode } from 'react'; -// import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; +import Breadcrumb from '@/components/Breadcrumb/Breadcrumb'; -// interface ShopLayoutProps { -// children: ReactNode; -// } - -// export default function ShopLayout({ children }: ShopLayoutProps) { -// return ( -//
-//
-// -// {children} -//
-//
-// ); -// } +interface ShopLayoutProps { + children: ReactNode; +} -export default function ShopLayout() { - return null; +export default function ShopLayout({ children }: ShopLayoutProps) { + return ( +
+
+ + {children} +
+
+ ); } diff --git a/src/app/shop/page.tsx b/src/app/shop/page.tsx index 43eec198..0c31071f 100644 --- a/src/app/shop/page.tsx +++ b/src/app/shop/page.tsx @@ -1,46 +1,42 @@ -// import { getAllProductList } from '@/api/productAPI'; -// import Pagination from '@/components/Pagination/Pagination'; -// import type { ProductParams } from '@/types/productItemType'; -// import classNames from 'classnames/bind'; -// import CategoryMenu from './_components/Category/CategoryMenu'; -// import CategoryTitle from './_components/Category/CategoryTitle'; -// import ProductList from './_components/Product/ProductList'; -// import Sort from './_components/Sort/Sort'; -// import styles from './page.module.scss'; +import { getAllProductList } from '@/api/productAPI'; +import Pagination from '@/components/Pagination/Pagination'; +import type { ProductParams } from '@/types/productItemType'; +import classNames from 'classnames/bind'; +import CategoryMenu from './_components/Category/CategoryMenu'; +import CategoryTitle from './_components/Category/CategoryTitle'; +import ProductList from './_components/Product/ProductList'; +import Sort from './_components/Sort/Sort'; +import styles from './page.module.scss'; -// const cn = classNames.bind(styles); +const cn = classNames.bind(styles); -// interface ShopAllPageProps { -// searchParams: { [key: string]: string | string[] | undefined }; -// } - -// export default async function ShopAllPage({ searchParams }: ShopAllPageProps) { -// const sortParam = Array.isArray(searchParams.sort) ? searchParams.sort[0] : searchParams.sort || 'createdAt_desc'; -// const pageParam = Array.isArray(searchParams.page) ? searchParams.page[0] : searchParams.page || '0'; +interface ShopAllPageProps { + searchParams: { [key: string]: string | string[] | undefined }; +} -// const getAllProductParams: ProductParams = { -// sort: sortParam as string, -// page: pageParam as string, -// size: '16', -// }; +export default async function ShopAllPage({ searchParams }: ShopAllPageProps) { + const sortParam = Array.isArray(searchParams.sort) ? searchParams.sort[0] : searchParams.sort || 'createdAt_desc'; + const pageParam = Array.isArray(searchParams.page) ? searchParams.page[0] : searchParams.page || '0'; -// const { data } = await getAllProductList(getAllProductParams); -// const { content, ...rest } = data; -// return ( -// <> -//
-// 전체상품 -// -//
-// -//
-// -// -//
-// -// ); -// } + const getAllProductParams: ProductParams = { + sort: sortParam as string, + page: pageParam as string, + size: '16', + }; -export default async function ShopAllPage() { - return null; + const { data } = await getAllProductList(getAllProductParams); + const { content, ...rest } = data; + return ( + <> +
+ 전체상품 + +
+ +
+ + +
+ + ); } diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index a1f685f8..f4423c26 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -15,7 +15,7 @@ import { deleteCookie } from '@/utils/manageCookie'; import { useUser } from '@/hooks/useUser'; import { LogoIcon, UserIcon } from '@/public/index'; import type { CartAPIDataType } from '@/types/cartType'; -import { CartButton, LoginButton, LogoutButton, NotificationButton, SearchButton } from './HeaderParts'; +import { CartButton, LoginButton, LogoutButton, NotificationButton, SearchButton, ShopButton } from './HeaderParts'; import styles from './Header.module.scss'; @@ -103,7 +103,7 @@ export default function Header() { > 커스텀 키보드 만들기 - {/* */} + 커뮤니티 diff --git a/src/libs/prefetchers.ts b/src/libs/prefetchers.ts index 4d6c6b74..332c9aee 100644 --- a/src/libs/prefetchers.ts +++ b/src/libs/prefetchers.ts @@ -4,6 +4,7 @@ import { getMyPosts } from '@/api/communityAPI'; import { getCoupon } from '@/api/couponAPI'; import { getProductLikes } from '@/api/likesAPI'; import { getOrder, getOrdersData, getPayment } from '@/api/orderAPI'; +import { getProductDetail } from '@/api/productAPI'; import { getUserProductReviews } from '@/api/productReviewAPI'; import { getAddresses } from '@/api/shippingAPI'; import { getUserData } from '@/api/usersAPI'; @@ -72,3 +73,12 @@ export const prefetchProductReviewsQuery = async (queryClient: QueryClient) => { export const prefetchPaymentQuery = async (queryClient: QueryClient, orderId: string) => { await queryClient.prefetchQuery({ queryKey: QUERY_KEYS.PAYMENT.DETAIL(orderId), queryFn: () => getPayment(orderId) }); }; + +export const fetchProductQuery = async (queryClient: QueryClient, productId: string | number) => { + const product = await queryClient.fetchQuery({ + queryKey: QUERY_KEYS.PRODUCT.DETAIL(Number(productId)), + queryFn: () => getProductDetail(Number(productId)), + }); + + return product; +};