diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..42e62935 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "prisma.pinToPrisma6": true +} \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index d973aeea..9708020c 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,58 @@ import type { NextConfig } from "next"; +import bundleAnalyzer from "@next/bundle-analyzer"; + +// Bundle Analyzer 설정 +const withBundleAnalyzer = bundleAnalyzer({ + enabled: process.env.ANALYZE === "true", + openAnalyzer: true, +}); const nextConfig: NextConfig = { + serverExternalPackages: ["jsdom"], + webpack: (config, { dev }) => { + if (!dev) { + config.resolve.alias["@hono/swagger-ui"] = false; + + config.resolve.alias["swagger-ui-dist"] = false; + + config.resolve.alias["swagger-ui-react"] = false; + + config.optimization = { + ...config.optimization, + splitChunks: { + ...config.optimization?.splitChunks, + cacheGroups: { + ...config.optimization?.splitChunks?.cacheGroups, + swiper: { + test: /[\\/]node_modules[\\/]swiper[\\/]/, + name: "swiper", + priority: 20, + reuseExistingChunk: true, + }, + ckeditor: { + test: /[\\/]node_modules[\\/](@?ckeditor|ckeditor5)[\\/]/, + name: "ckeditor", + priority: 20, + reuseExistingChunk: true, + }, + }, + }, + }; + } + return config; + }, + modularizeImports: { + "lucide-react": { + transform: "lucide-react/dist/esm/icons/{{member}}", + }, + }, images: { + deviceSizes: [640, 750, 828, 1080, 1200], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 340, 384, 680], + formats: ["image/webp"], + minimumCacheTTL: 31536000, + dangerouslyAllowSVG: true, + contentDispositionType: "inline", remotePatterns: [ { protocol: "https", @@ -34,6 +85,80 @@ const nextConfig: NextConfig = { }, ], }, + compiler: { + removeConsole: process.env.NODE_ENV === "production", + }, + experimental: { + optimizePackageImports: [ + "react-hook-form", + "zustand", + "swiper", + "chart.js", + "react-chartjs-2", + "ckeditor5", + "@ckeditor/ckeditor5-react", + ], + }, + async headers() { + return [ + { + source: "/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=3600, must-revalidate", + }, + { + key: "X-Content-Type-Options", + value: "nosniff", + }, + { + key: "X-Frame-Options", + value: "DENY", + }, + { + key: "X-XSS-Protection", + value: "1; mode=block", + }, + { + key: "Referrer-Policy", + value: "strict-origin-when-cross-origin", + }, + { + key: "Permissions-Policy", + value: "camera=(), microphone=(), geolocation=()", + }, + ], + }, + { + source: "/api/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=300, stale-while-revalidate=299", + }, + ], + }, + { + source: "/_next/static/:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, + { + source: "/_next/image:path*", + headers: [ + { + key: "Cache-Control", + value: "public, max-age=31536000, immutable", + }, + ], + }, + ]; + }, }; -export default nextConfig; +export default withBundleAnalyzer(nextConfig); diff --git a/package.json b/package.json index 155bc2a9..6e8c4da1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "next dev", "build": "prisma generate && next build", + "build:analyze": "cross-env ANALYZE=true prisma generate && cross-env ANALYZE=true next build", "start": "next start", "lint": "next lint", "test": "vitest", @@ -36,9 +37,10 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "dayjs": "^1.11.13", - "dompurify": "^3.2.6", + "dompurify": "^3.3.1", "hono": "^4.8.3", "hono-openapi": "^0.4.8", + "jsdom": "^28.1.0", "lucide-react": "^0.462.0", "next": "15.0.5", "next-auth": "^5.0.0-beta.25", @@ -61,6 +63,7 @@ }, "devDependencies": { "@chromatic-com/storybook": "^3.2.4", + "@next/bundle-analyzer": "15.0.5", "@storybook/addon-essentials": "^8.5.8", "@storybook/addon-interactions": "^8.5.8", "@storybook/addon-onboarding": "^8.5.8", @@ -68,7 +71,9 @@ "@storybook/nextjs": "^8.5.8", "@storybook/react": "^8.5.8", "@storybook/test": "^8.5.8", + "@svgr/webpack": "^8.1.0", "@types/dompurify": "^3.2.0", + "@types/jsdom": "^27.0.0", "@types/navermaps": "^3.9.1", "@types/node": "^20", "@types/react": "^18", @@ -76,6 +81,7 @@ "@types/swagger-ui-react": "^5.18.0", "@vitejs/plugin-react": "^5.1.1", "@vitest/ui": "^4.0.9", + "cross-env": "^10.1.0", "eslint": "^8", "eslint-config-next": "15.0.3", "eslint-plugin-storybook": "^0.11.3", @@ -90,7 +96,14 @@ "zod-prisma-types": "^3.2.4" }, "resolutions": { - "sharp": "0.33.5" + "sharp": "0.33.5", + "jsdom": "25.0.1" }, + "browserslist": [ + "chrome >= 90", + "firefox >= 90", + "safari >= 15", + "edge >= 90" + ], "packageManager": "yarn@4.10.3" } diff --git a/public/fonts/Pretendard-Bold.subset.woff2 b/public/fonts/Pretendard-Bold.subset.woff2 new file mode 100644 index 00000000..f80bdb72 Binary files /dev/null and b/public/fonts/Pretendard-Bold.subset.woff2 differ diff --git a/public/fonts/Pretendard-Medium.subset.woff2 b/public/fonts/Pretendard-Medium.subset.woff2 new file mode 100644 index 00000000..94199805 Binary files /dev/null and b/public/fonts/Pretendard-Medium.subset.woff2 differ diff --git a/public/fonts/Pretendard-Regular.subset.woff2 b/public/fonts/Pretendard-Regular.subset.woff2 new file mode 100644 index 00000000..6fc8ec42 Binary files /dev/null and b/public/fonts/Pretendard-Regular.subset.woff2 differ diff --git a/public/fonts/Pretendard-SemiBold.subset.woff2 b/public/fonts/Pretendard-SemiBold.subset.woff2 new file mode 100644 index 00000000..38175ff0 Binary files /dev/null and b/public/fonts/Pretendard-SemiBold.subset.woff2 differ diff --git a/public/fonts/PretendardVariable.woff2 b/public/fonts/PretendardVariable.woff2 deleted file mode 100644 index 49c54b51..00000000 Binary files a/public/fonts/PretendardVariable.woff2 and /dev/null differ diff --git a/public/icons/bottomNavBar/index.ts b/public/icons/bottomNavBar/index.ts new file mode 100644 index 00000000..21bf6df1 --- /dev/null +++ b/public/icons/bottomNavBar/index.ts @@ -0,0 +1,19 @@ +import Home from "./Home.svg"; +import HomeActive from "./HomeActive.svg"; +import Search from "./Search.svg"; +import SearchActive from "./SearchActive.svg"; +import Writing from "./Writing.svg"; +import WritingActive from "./WritingActive.svg"; +import Mypage from "./Mypage.svg"; +import MypageActive from "./MypageActive.svg"; + +export { + Home, + HomeActive, + Search, + SearchActive, + Writing, + WritingActive, + Mypage, + MypageActive, +}; diff --git a/public/icons/index.ts b/public/icons/index.ts index 14d39401..50b4d9f2 100644 --- a/public/icons/index.ts +++ b/public/icons/index.ts @@ -45,11 +45,6 @@ import RadioPrimary from "./review/RadioPrimary.svg"; import SwiperLeftButton from "./SwiperLeftButton.svg"; import SwiperRightButton from "./SwiperRightButton.svg"; -import BottmNavBarHome from "./bottomNavBar/Home.svg"; -import BottmNavBarSearch from "./bottomNavBar/Search.svg"; -import BottmNavBarWriting from "./bottomNavBar/Writing.svg"; -import BottmNavBarMypage from "./bottomNavBar/Mypage.svg"; - export { ArrowDownGray, ArrowDownPrimary, @@ -89,8 +84,4 @@ export { RadioPrimary, SwiperLeftButton, SwiperRightButton, - BottmNavBarHome, - BottmNavBarSearch, - BottmNavBarWriting, - BottmNavBarMypage, }; diff --git a/public/images/Community.webp b/public/images/Community.webp new file mode 100644 index 00000000..01caeeed Binary files /dev/null and b/public/images/Community.webp differ diff --git a/public/images/Logo.webp b/public/images/Logo.webp new file mode 100644 index 00000000..168d10b2 Binary files /dev/null and b/public/images/Logo.webp differ diff --git a/public/images/MainButton_1.png b/public/images/MainButton_1.png deleted file mode 100644 index 3ddb5b1b..00000000 Binary files a/public/images/MainButton_1.png and /dev/null differ diff --git a/public/images/MainButton_2.png b/public/images/MainButton_2.png deleted file mode 100644 index d8f00e23..00000000 Binary files a/public/images/MainButton_2.png and /dev/null differ diff --git a/public/images/PerfumeSearch.webp b/public/images/PerfumeSearch.webp new file mode 100644 index 00000000..9e41cc4b Binary files /dev/null and b/public/images/PerfumeSearch.webp differ diff --git a/public/images/Placeholder.png b/public/images/Placeholder.png deleted file mode 100644 index e413ed9f..00000000 Binary files a/public/images/Placeholder.png and /dev/null differ diff --git a/public/images/Placeholder.webp b/public/images/Placeholder.webp new file mode 100644 index 00000000..3c2f1533 Binary files /dev/null and b/public/images/Placeholder.webp differ diff --git a/public/images/images.ts b/public/images/images.ts deleted file mode 100644 index 98e82ed0..00000000 --- a/public/images/images.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Logo from "./Logo.svg"; - -import Google from "./Google.svg"; -import Naver from "./Naver.svg"; -import Kakao from "./Kakao.svg"; - -import Profile from "./Profile.svg"; - -import MainButton1 from "./MainButton_1.png"; -import MainButton2 from "./MainButton_2.png"; - -export { Logo, Google, Naver, Kakao, Profile, MainButton1, MainButton2 }; diff --git a/public/images/index.ts b/public/images/index.ts new file mode 100644 index 00000000..f8a4848c --- /dev/null +++ b/public/images/index.ts @@ -0,0 +1,22 @@ +import Logo from "./Logo.svg"; + +import Google from "./Google.svg"; +import Naver from "./Naver.svg"; +import Kakao from "./Kakao.svg"; + +import Profile from "./Profile.svg"; + +import Community from "./Community.webp"; +import PerfumeSearch from "./PerfumeSearch.webp"; +import PlaceHolder from "./Placeholder.webp"; + +export { + Logo, + Google, + Naver, + Kakao, + Profile, + Community, + PerfumeSearch, + PlaceHolder, +}; diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index 5233b4af..f48b929c 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -3,11 +3,10 @@ import { MainContent } from "@/components/domains/main/mainContent/MainContent"; import { MainLogo } from "@/components/domains/main/MainLogo"; import { MainSearchBar } from "@/components/domains/main/MainSearchBar"; -export const dynamic = "force-dynamic"; - export default function Home() { return ( <> +

ChickChick - 나만의 향수 컬렉션

diff --git a/src/app/api-doc/layout.tsx b/src/app/api-doc/layout.tsx index ebc6609e..dd030a7d 100644 --- a/src/app/api-doc/layout.tsx +++ b/src/app/api-doc/layout.tsx @@ -1,4 +1,3 @@ -import { notFound } from "next/navigation"; import React from "react"; export default function ApiDocLayout({ @@ -7,8 +6,7 @@ export default function ApiDocLayout({ children: React.ReactNode; }) { if (process.env.NODE_ENV === "production") { - notFound(); + return null; } - return <>{children}; } diff --git a/src/app/api-doc/page.tsx b/src/app/api-doc/page.tsx index 5b7460a7..bd56fdda 100644 --- a/src/app/api-doc/page.tsx +++ b/src/app/api-doc/page.tsx @@ -1,20 +1,13 @@ -"use client"; +import SwaggerUI from "swagger-ui-react"; -import { lazy, Suspense } from "react"; -import "swagger-ui-react/swagger-ui.css"; - -const SwaggerUI = lazy(() => import("swagger-ui-react")); - -function SwaggerDocs() { +export default function Page() { return ( -
- Loading API Documentation...}> - - -
+
+ +
); } - -export default function Page() { - return ; -} diff --git a/src/app/api/[[...route]]/route.ts b/src/app/api/[[...route]]/route.ts index 4e5c8dc2..020c8650 100644 --- a/src/app/api/[[...route]]/route.ts +++ b/src/app/api/[[...route]]/route.ts @@ -3,6 +3,18 @@ import { handle } from "hono/vercel"; // export const runtime = "edge"; +if (process.env.NODE_ENV !== "production") { + try { + const { swaggerUI } = await import("@hono/swagger-ui"); + app.get("/swagger-ui", swaggerUI({ url: "/api/doc" })); + } catch (_) { + console.warn( + "Swagger UI dependencies not found in production environment.", + _, + ); + } +} + export const GET = handle(app); export const POST = handle(app); export const PUT = handle(app); diff --git a/src/app/brand/[name]/page.tsx b/src/app/brand/[name]/page.tsx index 2a651a39..0b9fdf17 100644 --- a/src/app/brand/[name]/page.tsx +++ b/src/app/brand/[name]/page.tsx @@ -1,3 +1,5 @@ +import { cache } from "react"; +import type { Metadata } from "next"; import { HydrationBoundary, dehydrate } from "@tanstack/react-query"; import getQueryClient from "@/lib/utils/getQueryClient"; import { brandApi } from "@/lib/utils/api/brands.api"; @@ -8,6 +10,47 @@ import { PageClient } from "@/components/domains/brandDetail/perfumes/PageClient import { BrandDetailSearchBar } from "@/components/domains/brandDetail/searchBar"; import { notFound } from "next/navigation"; import { BrandMap } from "@/components/domains/brandDetail/map"; +import { generateSeo } from "@/lib/utils/generateSeo"; + +type Props = { + params: Promise<{ name: string }>; +}; + +const getBrandByName = cache(async (brandName: string) => { + return brandApi.getByName(brandName); +}); + +export async function generateMetadata({ params }: Props): Promise { + const resolvedParams = await params; + const brandName = decodeURIComponent(resolvedParams.name); + + try { + const brandResponse = await getBrandByName(brandName); + + if (!brandResponse || !brandResponse.data) { + return generateSeo({ + title: "브랜드 없음", + description: "존재하지 않거나 삭제된 브랜드입니다.", + }); + } + + const brand = brandResponse.data; + const displayName = `${brand.nameKo} ${brand.nameEn}`.trim(); + + return generateSeo({ + title: `${displayName} 향수`, + description: + brand.description || + `${displayName}의 다양한 향수를 만나보세요. ChickChick에서 브랜드별 향수 정보와 리뷰를 확인하세요.`, + }); + } catch (error) { + console.error("메타데이터 생성 중 오류 발생:", error); + return generateSeo({ + title: "브랜드", + description: "ChickChick 브랜드 페이지입니다.", + }); + } +} export default async function BrandDetailPage({ params, @@ -21,7 +64,7 @@ export default async function BrandDetailPage({ try { const brandResponse = await queryClient.fetchQuery({ queryKey: ["brand", "detail", brandName], - queryFn: () => brandApi.getByName(brandName), + queryFn: () => getBrandByName(brandName), }); if (!brandResponse) { diff --git a/src/app/community/page.tsx b/src/app/community/page.tsx index 576ce2b6..0c7b1b4b 100644 --- a/src/app/community/page.tsx +++ b/src/app/community/page.tsx @@ -1,5 +1,18 @@ import PageClient from "@/components/domains/community/PageClient"; import { Suspense } from "react"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "커뮤니티 | ChickChick", + description: + "향수 애호가들과 소통하고, 다양한 향수 정보와 경험을 공유하세요. ChickChick 커뮤니티에서 나만의 향수 이야기를 나눠보세요.", + openGraph: { + title: "커뮤니티", + description: + "향수 애호가들과 소통하고, 다양한 향수 정보와 경험을 공유하세요. ChickChick 커뮤니티에서 나만의 향수 이야기를 나눠보세요.", + type: "website", + }, +}; export default function Page() { const ClientFallback = () =>
로딩 중...
; diff --git a/src/app/community/post/(write)/layout.tsx b/src/app/community/post/(write)/layout.tsx new file mode 100644 index 00000000..ab4a17d5 --- /dev/null +++ b/src/app/community/post/(write)/layout.tsx @@ -0,0 +1,6 @@ +import "ckeditor5/ckeditor5.css"; +import "@/components/commons/ckeditor5/ckeditor5.css"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/community/post/(write)/page.tsx b/src/app/community/post/(write)/page.tsx new file mode 100644 index 00000000..70863065 --- /dev/null +++ b/src/app/community/post/(write)/page.tsx @@ -0,0 +1,8 @@ +import PageClient from "@/components/domains/post/PageClient"; +export default async function Page() { + return ( + <> + + + ); +} diff --git a/src/app/community/post/[id]/edit/layout.tsx b/src/app/community/post/[id]/edit/layout.tsx new file mode 100644 index 00000000..ab4a17d5 --- /dev/null +++ b/src/app/community/post/[id]/edit/layout.tsx @@ -0,0 +1,6 @@ +import "ckeditor5/ckeditor5.css"; +import "@/components/commons/ckeditor5/ckeditor5.css"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/community/post/[id]/layout.tsx b/src/app/community/post/[id]/layout.tsx new file mode 100644 index 00000000..790272de --- /dev/null +++ b/src/app/community/post/[id]/layout.tsx @@ -0,0 +1,3 @@ +export default function Layout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/src/app/community/post/layout.tsx b/src/app/community/post/layout.tsx index 1f1994bf..a23bd933 100644 --- a/src/app/community/post/layout.tsx +++ b/src/app/community/post/layout.tsx @@ -1,6 +1,3 @@ -import "ckeditor5/ckeditor5.css"; -import "@/components/commons/ckeditor5/ckeditor5.css"; - export default function Layout({ children }: { children: React.ReactNode }) { return (
diff --git a/src/app/community/post/page.tsx b/src/app/community/post/page.tsx deleted file mode 100644 index cc781bc7..00000000 --- a/src/app/community/post/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import PageClient from "@/components/domains/post/PageClient"; -// import { getSession } from "@/lib/database/getSession"; -export default async function Page() { - // const session = await getSession(); - // if (!session) { - // return
로그인이 필요한 서비스 입니다.
; - // } - - return ( - <> - - - ); -} diff --git a/src/app/font.css b/src/app/font.css new file mode 100644 index 00000000..81a58611 --- /dev/null +++ b/src/app/font.css @@ -0,0 +1,29 @@ +@media (min-width: 768px) { + @font-face { + font-family: "Pretendard"; + font-weight: 400; + font-display: swap; + src: url("/fonts/Pretendard-Regular.subset.woff2") format("woff2"); + } + + @font-face { + font-family: "Pretendard"; + font-weight: 500; + font-display: swap; + src: url("/fonts/Pretendard-Medium.subset.woff2") format("woff2"); + } + + @font-face { + font-family: "Pretendard"; + font-weight: 600; + font-display: swap; + src: url("/fonts/Pretendard-SemiBold.subset.woff2") format("woff2"); + } + + @font-face { + font-family: "Pretendard"; + font-weight: 700; + font-display: swap; + src: url("/fonts/Pretendard-Bold.subset.woff2") format("woff2"); + } +} diff --git a/src/app/globals.css b/src/app/globals.css index 606b224b..9f375754 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,6 +2,31 @@ @tailwind components; @tailwind utilities; +@import "./font.css"; + +@layer base { + body { + font-family: + -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Malgun Gothic", + "맑은 고딕", system-ui, helvetica, sans-serif; + } + + @media (min-width: 768px) { + body { + font-family: + "Pretendard", + -apple-system, + BlinkMacSystemFont, + "Apple SD Gothic Neo", + "Malgun Gothic", + "맑은 고딕", + system-ui, + helvetica, + sans-serif; + } + } +} + @layer utilities { .shadow-card { @apply shadow-[0_4px_20px_rgba(0,0,0,0.08)]; @@ -19,4 +44,28 @@ .divider-horizontal-thick { @apply bg-gray-300 h-[8px] w-full; } + + .system-font { + font-family: + -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Pretendard", + "Malgun Gothic", "맑은 고딕", system-ui, helvetica, sans-serif; + } + + .system-font * { + font-family: inherit; + } +} + +.swiper-pagination-bullet { + width: 4px; + height: 4px; + background: rgba(255, 255, 255, 0.5); + opacity: 1; +} + +.swiper-pagination-bullet-active { + background: #dbc0b0; + width: 8px; + height: 4px; + border-radius: 10px; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e541f21d..c2a0ffe1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,20 +1,84 @@ -import NavBarWrapper from "@/components/commons/navBar/Wrapper"; -import "./globals.css"; -import localFont from "next/font/local"; +import type { Metadata, Viewport } from "next"; +import { auth } from "@/auth"; +import Providers from "@/components/commons/Provider/TanstackProvider"; import { BodyWrapper } from "@/components/commons/wrapper/BodyWrapper"; +import { NavBarWrapper } from "@/components/commons/navBar/Wrapper"; import LoginModalProvider from "@/components/modal/LoginModalProvider"; -import Providers from "@/components/commons/Provider/TanstackProvider"; import GlobalStateSync from "@/components/commons/Provider/GlobalStateSync"; import RecentSyncManager from "@/components/commons/Provider/RecentSyncManager"; -import KakaoScript from "@/lib/script/KakaoScript"; -import { auth } from "@/auth"; +import "./globals.css"; + +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, +}; -const pretendard = localFont({ - src: "../../public/fonts/PretendardVariable.woff2", - display: "swap", - weight: "45 920", - variable: "--font-pretendard", -}); +export const metadata: Metadata = { + metadataBase: new URL( + process.env.NEXT_PUBLIC_SITE_URL || "https://chickchick-kohl.vercel.app/", + ), + title: { + default: "ChickChick - 나만의 향수 컬렉션", + template: "%s | ChickChick", + }, + description: + "당신의 향수 취향을 발견하고, 공유하세요. ChickChick에서 향수 리뷰, 추천, 커뮤니티를 경험하세요.", + keywords: [ + "향수", + "퍼퓸", + "향수 추천", + "향수 리뷰", + "향수 커뮤니티", + "fragrance", + "perfume", + "scent", + "chickchick", + ], + authors: [{ name: "ChickChick Team" }], + creator: "ChickChick", + publisher: "ChickChick", + robots: { + index: true, + follow: true, + googleBot: { + index: true, + follow: true, + "max-video-preview": -1, + "max-image-preview": "large", + "max-snippet": -1, + }, + }, + openGraph: { + type: "website", + locale: "ko_KR", + url: "https://chickchick-kohl.vercel.app/", + title: "ChickChick - 나만의 향수 컬렉션", + description: + "당신의 향수 취향을 발견하고, 공유하세요. ChickChick에서 향수 리뷰, 추천, 커뮤니티를 경험하세요.", + siteName: "ChickChick", + images: [ + { + url: "/images/LogoForShare.png", + width: 1200, + height: 630, + alt: "ChickChick Logo", + }, + ], + }, + twitter: { + card: "summary_large_image", + title: "ChickChick - 나만의 향수 컬렉션", + description: + "당신의 향수 취향을 발견하고, 공유하세요. ChickChick에서 향수 리뷰, 추천, 커뮤니티를 경험하세요.", + images: ["/images/LogoForShare.png"], + }, + verification: { + google: "google-site-verification-code", + }, + alternates: { + canonical: "https://chickchick-kohl.vercel.app/", + }, +}; export default async function RootLayout({ children, @@ -25,8 +89,31 @@ export default async function RootLayout({ const isAuthenticated = !!session?.user; return ( - - + + + + + + + @@ -37,7 +124,6 @@ export default async function RootLayout({ - ); diff --git a/src/app/perfumes/page.tsx b/src/app/perfumes/page.tsx index f46a7d25..ca4b695d 100644 --- a/src/app/perfumes/page.tsx +++ b/src/app/perfumes/page.tsx @@ -4,24 +4,44 @@ import getQueryClient from "@/lib/utils/getQueryClient"; import { brandApi } from "@/lib/utils/api/brands.api"; import { perfumeApi } from "@/lib/utils/api/perfumes.api"; import PageClient from "@/components/domains/perfumes/PageClient"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "향수 탐색 | ChickChick", + description: + "다양한 브랜드의 향수를 탐색하고, 노트와 어코드로 필터링하여 나에게 맞는 향수를 찾아보세요. ChickChick에서 수천 개의 향수 정보를 확인하세요.", + openGraph: { + title: "향수 탐색", + description: + "다양한 브랜드의 향수를 탐색하고, 노트와 어코드로 필터링하여 나에게 맞는 향수를 찾아보세요.", + type: "website", + }, +}; + +// 동적 렌더링으로 전환 (빌드 시 타임아웃 방지) +export const dynamic = "force-dynamic"; export default async function Page() { const queryClient = getQueryClient(); - await Promise.all([ - queryClient.prefetchQuery({ - queryKey: ["brand", "list"], - queryFn: () => brandApi.list(), - }), - queryClient.prefetchQuery({ - queryKey: ["perfume", "notes"], - queryFn: () => perfumeApi.notes(), - }), - queryClient.prefetchQuery({ - queryKey: ["perfume", "accords"], - queryFn: () => perfumeApi.accords(), - }), - ]); + try { + await Promise.all([ + queryClient.prefetchQuery({ + queryKey: ["brand", "list"], + queryFn: () => brandApi.list(), + }), + queryClient.prefetchQuery({ + queryKey: ["perfume", "notes"], + queryFn: () => perfumeApi.notes(), + }), + queryClient.prefetchQuery({ + queryKey: ["perfume", "accords"], + queryFn: () => perfumeApi.accords(), + }), + ]); + } catch (error) { + console.error("Failed to prefetch perfume data:", error); + } return ( diff --git a/src/app/user/[id]/layout.tsx b/src/app/user/[id]/layout.tsx index 72444726..a5aaf5b9 100644 --- a/src/app/user/[id]/layout.tsx +++ b/src/app/user/[id]/layout.tsx @@ -4,12 +4,48 @@ import { userApi } from "@/lib/utils/api/users.api"; import UserLayoutClient from "@/components/domains/user/UserLayoutClient"; import { getUserSessionInfo } from "@/lib/utils/getUserSessionInfo"; import { queryKeys } from "@/lib/utils/queryKeys"; +import { generateSeo } from "@/lib/utils/generateSeo"; +import type { Metadata } from "next"; interface LayoutProps { children: React.ReactNode; params: Promise<{ id: string }>; } +type Props = { + params: Promise<{ id: string }>; +}; + +export async function generateMetadata({ params }: Props): Promise { + const { id: pageOwnerId } = await params; + + try { + const userResponse = await userApi.getById(pageOwnerId); + + if (!userResponse || !userResponse.data) { + return generateSeo({ + title: "사용자 없음", + description: "존재하지 않는 사용자입니다.", + }); + } + + const user = userResponse.data; + const userName = user.nickname || "사용자"; + + return generateSeo({ + title: `${userName}의 프로필`, + description: `${userName}님의 향수 컬렉션과 리뷰를 확인하세요. ChickChick에서 다양한 향수 정보를 공유합니다.`, + image: user.imageUrl || undefined, + }); + } catch (error) { + console.error("메타데이터 생성 중 오류 발생:", error); + return generateSeo({ + title: "사용자 프로필", + description: "ChickChick 사용자 프로필 페이지입니다.", + }); + } +} + export default async function UserLayout({ children, params }: LayoutProps) { const { id: pageOwnerId } = await params; const queryClient = getQueryClient(); diff --git a/src/components/commons/Provider/TanstackProvider.tsx b/src/components/commons/Provider/TanstackProvider.tsx index 8ed9a081..d2945d61 100644 --- a/src/components/commons/Provider/TanstackProvider.tsx +++ b/src/components/commons/Provider/TanstackProvider.tsx @@ -1,9 +1,20 @@ "use client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import dynamic from "next/dynamic"; import { useState } from "react"; +const ReactQueryDevtools = + process.env.NODE_ENV === "development" + ? dynamic( + () => + import("@tanstack/react-query-devtools").then( + (mod) => mod.ReactQueryDevtools, + ), + { ssr: false }, + ) + : () => null; + export default function Providers({ children }: { children: React.ReactNode }) { const [queryClient] = useState( () => @@ -23,7 +34,7 @@ export default function Providers({ children }: { children: React.ReactNode }) { refetchOnWindowFocus: true, // 윈도우 포커스 시 재요청 }, }, - }) + }), ); return ( diff --git a/src/components/commons/actions/index.tsx b/src/components/commons/actions/index.tsx index 3eee309d..f2f93563 100644 --- a/src/components/commons/actions/index.tsx +++ b/src/components/commons/actions/index.tsx @@ -47,9 +47,10 @@ export const Actions = ({ actions, size = "default" }: ActionsProps) => {
{index !== 0 &&
} diff --git a/src/components/commons/author/AuthorInfo.tsx b/src/components/commons/author/AuthorInfo.tsx index 89fee408..e4da149f 100644 --- a/src/components/commons/author/AuthorInfo.tsx +++ b/src/components/commons/author/AuthorInfo.tsx @@ -60,7 +60,7 @@ const AuthorInfo: React.FC = ({ size === SIZE_STATUSES.DEFAULT ? "text-label-4 tablet:text-label-2" : "text-body-1" - } font-medium text-black-300`} + } font-medium text-black-100`} >
diff --git a/src/components/commons/author/AuthorProfile.tsx b/src/components/commons/author/AuthorProfile.tsx index b82353fd..97e1522b 100644 --- a/src/components/commons/author/AuthorProfile.tsx +++ b/src/components/commons/author/AuthorProfile.tsx @@ -19,13 +19,20 @@ export default function AuthorProfile({ : DEFAULT_PROFILE_IMAGE; return (
- {`${name}의 +
+ {`${name}의 +
{name}
diff --git a/src/components/commons/author/IconBadge.tsx b/src/components/commons/author/IconBadge.tsx index 0e14d5bf..9369fd62 100644 --- a/src/components/commons/author/IconBadge.tsx +++ b/src/components/commons/author/IconBadge.tsx @@ -13,7 +13,7 @@ export default function IconBadge({ iconSrc, altText, count }: IconBadgeProps) { const formattedCount = count > MAX_COUNT ? `${MAX_COUNT}+` : count; return ( - + {altText} {formattedCount} diff --git a/src/components/commons/author/PostTime.tsx b/src/components/commons/author/PostTime.tsx index 299aff88..a7d235be 100644 --- a/src/components/commons/author/PostTime.tsx +++ b/src/components/commons/author/PostTime.tsx @@ -40,7 +40,7 @@ export default function PostTime({ time, type, size }: PostTimeProps) { return ( {formattedTime} diff --git a/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx b/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx index 4f9d2d42..5e190439 100644 --- a/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx +++ b/src/components/commons/bottomNavBar/BottomNavBar.constants.tsx @@ -1,39 +1,41 @@ +import { NAV_BAR_ICONS } from "@/lib/constants/icons/navBar"; + export const BOTTOM_NAV_BAR_ICONS = (userId: string) => { return [ { key: "home", src: { - inactive: "/icons/bottomNavBar/home.svg", - active: "/icons/bottomNavBar/homeActive.svg", + inactive: NAV_BAR_ICONS.home.inactive, + active: NAV_BAR_ICONS.home.active, }, - alt: "홈", + alt: NAV_BAR_ICONS.home.alt, href: "/", }, { key: "search", src: { - inactive: "/icons/bottomNavBar/search.svg", - active: "/icons/bottomNavBar/searchActive.svg", + inactive: NAV_BAR_ICONS.search.inactive, + active: NAV_BAR_ICONS.search.active, }, - alt: "검색", + alt: NAV_BAR_ICONS.search.alt, href: "/perfumes", }, { key: "writing", src: { - inactive: "/icons/bottomNavBar/writing.svg", - active: "/icons/bottomNavBar/writingActive.svg", + inactive: NAV_BAR_ICONS.writing.inactive, + active: NAV_BAR_ICONS.writing.active, }, - alt: "글쓰기", + alt: NAV_BAR_ICONS.writing.alt, href: "/community/post", }, { key: "mypage", src: { - inactive: "/icons/bottomNavBar/mypage.svg", - active: "/icons/bottomNavBar/mypageActive.svg", + inactive: NAV_BAR_ICONS.mypage.inactive, + active: NAV_BAR_ICONS.mypage.active, }, - alt: "마이페이지", + alt: NAV_BAR_ICONS.mypage.alt, href: `${userId ? `/user/${userId}/collection` : "/"}`, }, ]; diff --git a/src/components/commons/bottomNavBar/BottomNavBarIcon.tsx b/src/components/commons/bottomNavBar/BottomNavBarIcon.tsx index 9c6d801b..e85d587b 100644 --- a/src/components/commons/bottomNavBar/BottomNavBarIcon.tsx +++ b/src/components/commons/bottomNavBar/BottomNavBarIcon.tsx @@ -24,7 +24,7 @@ export const BottomNavBarIcon = ({ image, isActive }: IBottomNavBarIcon) => { />
{image.alt} diff --git a/src/components/commons/button/ButtonBase.tsx b/src/components/commons/button/ButtonBase.tsx index 5b9f1ca8..9adeded2 100644 --- a/src/components/commons/button/ButtonBase.tsx +++ b/src/components/commons/button/ButtonBase.tsx @@ -38,7 +38,7 @@ export default function ButtonBase(props: IButtonBaseProps) { return (
) : ( -
- User - {isDropdownOpen && navBarRef.current && ( diff --git a/src/components/commons/navBar/Wrapper.tsx b/src/components/commons/navBar/Wrapper.tsx index 2713a469..a819068d 100644 --- a/src/components/commons/navBar/Wrapper.tsx +++ b/src/components/commons/navBar/Wrapper.tsx @@ -3,12 +3,8 @@ import { usePathname } from "next/navigation"; import NavBar from "."; -export default function NavBarWrapper() { +export const NavBarWrapper = () => { const currentPath = usePathname(); - return ( - <> - - - ); -} + return ; +}; diff --git a/src/components/commons/navBar/index.tsx b/src/components/commons/navBar/index.tsx index d5520e1c..dc76a908 100644 --- a/src/components/commons/navBar/index.tsx +++ b/src/components/commons/navBar/index.tsx @@ -2,7 +2,6 @@ import React from "react"; import Image from "next/image"; import Link from "next/link"; import { NAV_LABELS, NAV_PATHS } from "./navBar.constants"; -import IMAGES from "@/lib/constants/images"; import UserProfileSection from "./UserProfileSection"; export interface NavBarProps { @@ -15,54 +14,50 @@ export default function NavBar({ currentPath }: NavBarProps) { return ( <> -
+
{/* 로고 */} -
- +
+ logo
{/* 네비게이션 */} -
- - {/* 드롭다운을 NavBar의 바로 아래에 배치 */} ); } diff --git a/src/components/commons/perfumeList/filter/index.tsx b/src/components/commons/perfumeList/filter/index.tsx index 31788d04..fa7ecbcb 100644 --- a/src/components/commons/perfumeList/filter/index.tsx +++ b/src/components/commons/perfumeList/filter/index.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import FilterItem from "./FilterItem"; -import { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; +import type { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; import { ApiPerfumeNoteResponse, ApiPerfumeAccordResponse, diff --git a/src/components/commons/perfumeList/perfumes.helpers.ts b/src/components/commons/perfumeList/perfumes.helpers.ts index 55fd4b79..015e0b53 100644 --- a/src/components/commons/perfumeList/perfumes.helpers.ts +++ b/src/components/commons/perfumeList/perfumes.helpers.ts @@ -1,4 +1,4 @@ -import { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; import { FILTER_LABELS } from "./filter/filter.constants"; /** diff --git a/src/components/commons/perfumeList/search/index.tsx b/src/components/commons/perfumeList/search/index.tsx index 1ba32d45..e86ccc4f 100644 --- a/src/components/commons/perfumeList/search/index.tsx +++ b/src/components/commons/perfumeList/search/index.tsx @@ -3,7 +3,7 @@ import { ApiPerfumeAccordResponse, ApiPerfumeNoteResponse, } from "@/lib/hono/schemas/perfume.schema"; -import { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; +import type { ApiBrandSimpleResponse } from "@/lib/hono/schemas/brand.schema"; import { SearchBar } from "@/components/commons/search/SearchBar"; import PerFumeFilter from "../filter"; diff --git a/src/components/commons/perfumeList/section/PerfumeSection.tsx b/src/components/commons/perfumeList/section/PerfumeSection.tsx index cadb9f56..e24ebceb 100644 --- a/src/components/commons/perfumeList/section/PerfumeSection.tsx +++ b/src/components/commons/perfumeList/section/PerfumeSection.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { Spinner } from "@/components/commons/loading/Spinner"; import PerfumeCard from "@/components/commons/card/perfumeCard"; import { PERFUME_CARD_TYPES } from "@/components/commons/card/perfumeCard/perfumeCard.constants"; -import { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; +import type { ApiPerfumeSimpleResponse } from "@/lib/hono/schemas/perfume.schema"; interface PerfumeSectionProps { perfumes: ApiPerfumeSimpleResponse[]; diff --git a/src/components/commons/search/SearchBar.tsx b/src/components/commons/search/SearchBar.tsx index 962f218d..a91401b5 100644 --- a/src/components/commons/search/SearchBar.tsx +++ b/src/components/commons/search/SearchBar.tsx @@ -52,15 +52,24 @@ export function SearchBar({ onChange={onChange} {...rest} /> -