diff --git a/config/metadata.js b/config/metadata.js index d2b25a10..dc28ec67 100644 --- a/config/metadata.js +++ b/config/metadata.js @@ -17,3 +17,13 @@ export const signupMetadata = { title: '판다마켓 회원가입', description: '판다마켓 회원가입 페이지', }; + +export const articleMetadata = { + title: '판다마켓 게시글', + description: '판다마켓 게시글 페이지', +}; + +export const itemMetadata = { + title: '판다마켓 상품', + description: '판다마켓 상품 페이지', +}; diff --git a/config/paths.js b/config/paths.js index 025d82b7..b481cb1d 100644 --- a/config/paths.js +++ b/config/paths.js @@ -10,6 +10,9 @@ export const paths = { registration: { getHref: () => '/articles/registration', }, + items: { + getHref: () => '/items', + }, }, auth: { diff --git a/package.json b/package.json index 78bee5a1..ee5ca50a 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "dev": "next dev", "build": "prisma generate && next build", "start": "next start", - "seed": "node prisma/seed.js", + "seed": "node ./prisma/seed.js", "prepare": "husky", "postinstall": "prisma generate --no-engine --schema=./prisma/schema", "migrate:dev": "prisma -- migrate dev --schema=./prisma/schema", diff --git a/prisma/seed.js b/prisma/seed.js index 440ad4d4..5eb001d8 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -4,10 +4,8 @@ import { randAvatar, randCatchPhrase, randEmail, - randFirstName, randFullName, randImg, - randLastName, randNumber, randParagraph, randPassword, @@ -15,9 +13,8 @@ import { randSentence, randText, } from '@ngneat/falso'; -import { PrismaClient } from '@prisma/client'; -const prisma = new PrismaClient(); +import prisma from '../src/libs/prisma.js'; async function main() { console.log('기존 데이터 삭제 시작...'); diff --git a/src/app/api/login/route.js b/src/app/api/(auth)/login/route.js similarity index 100% rename from src/app/api/login/route.js rename to src/app/api/(auth)/login/route.js diff --git a/src/app/api/(auth)/logout/route.js b/src/app/api/(auth)/logout/route.js new file mode 100644 index 00000000..408a2576 --- /dev/null +++ b/src/app/api/(auth)/logout/route.js @@ -0,0 +1,32 @@ +import { cookies } from 'next/headers'; +import { NextResponse } from 'next/server'; + +import prisma from '@/libs/prisma'; + +export async function POST() { + try { + const cookieStore = cookies(); + const refreshToken = cookieStore.get('refreshToken')?.value; + + if (refreshToken) { + await prisma.refreshToken.deleteMany({ + where: { token: refreshToken }, + }); + } + + cookies().set('refreshToken', '', { + httpOnly: true, + secure: process.env.NODE_ENV !== 'development', + sameSite: 'strict', + path: '/', + expires: new Date(0), + }); + + return NextResponse.json({ message: 'Logged out successfully' }); + } catch (error) { + return NextResponse.json( + { message: 'An error occurred during logout', error: error.message }, + { status: 500 }, + ); + } +} diff --git a/src/app/api/refresh-token/route.js b/src/app/api/(auth)/refresh-token/route.js similarity index 100% rename from src/app/api/refresh-token/route.js rename to src/app/api/(auth)/refresh-token/route.js diff --git a/src/app/api/signup/route.js b/src/app/api/(auth)/signup/route.js similarity index 100% rename from src/app/api/signup/route.js rename to src/app/api/(auth)/signup/route.js diff --git a/src/app/api/user/route.jsx b/src/app/api/(auth)/user/route.jsx similarity index 100% rename from src/app/api/user/route.jsx rename to src/app/api/(auth)/user/route.jsx diff --git a/src/app/api/articles/[id]/route.js b/src/app/api/articles/[id]/route.js index ef8fbd63..49306d26 100644 --- a/src/app/api/articles/[id]/route.js +++ b/src/app/api/articles/[id]/route.js @@ -14,7 +14,7 @@ export const GET = async (request, { params }) => { async (id) => { const article = await prisma.article.findUnique({ where: { - id: parseInt(id), + id: id, }, include: { Comment: { diff --git a/src/app/api/items/detail/[id]/route.js b/src/app/api/items/detail/[id]/route.js new file mode 100644 index 00000000..9d1417b2 --- /dev/null +++ b/src/app/api/items/detail/[id]/route.js @@ -0,0 +1,55 @@ +import { unstable_cache } from 'next/cache'; + +import prisma from '@/libs/prisma'; +import { apiResponse } from '@/libs/utils/api-helper'; + +export const GET = async (request, { params }) => { + const { id } = await params; + if (!id) { + return apiResponse(false, 'Not Found Article', null, 400); + } + /** @see https://nextjs.org/docs/app/api-reference/functions/unstable_cache */ + const itemCached = unstable_cache( + async (itemId) => { + const item = await prisma.item.findUnique({ + where: { + id: itemId, + }, + include: { + user: { + include: { + userProfile: true, + }, + include: { + comments: true, + }, + }, + }, + }); + return item; + }, + [`article-detail-${id}`], + { + tags: ['item', 'item-comment', `article-${id}`], + revalidate: 3, + }, + ); + + try { + const item = await itemCached(id); + + if (!item) { + return apiResponse(false, `ID ${id} not found`, null, 404); + } + return apiResponse(true, 'Success Get Article', item, 200); + } catch (error) { + console.error('API Error', error); + return apiResponse( + false, + 'Internal Server Error', + null, + 500, + error.message, + ); + } +}; diff --git a/src/app/api/items/route.js b/src/app/api/items/route.js new file mode 100644 index 00000000..9df4eb12 --- /dev/null +++ b/src/app/api/items/route.js @@ -0,0 +1,52 @@ +import prisma from '@/libs/prisma'; +import { apiResponse } from '@/libs/utils/api-helper'; + +export const GET = async (request) => { + try { + const { searchParams } = new URL(request.url); + + const pageStr = searchParams.get('page') ?? '1'; + const limitStr = searchParams.get('limit') ?? '10'; + const keyword = searchParams.get('keyword') ?? ''; + const orderBy = searchParams.get('keyword') ?? 'recent'; + + const page = parseInt(pageStr); + const limit = parseInt(limitStr); + const total = await prisma.item.count(); + const totalPage = Math.ceil(total / limit); + + const SORT_MAP = { + recent: { createdAt: 'desc' }, + favorite: { createdAt: 'asc' }, + }; + + const sortOptions = SORT_MAP[orderBy] ?? { createdAt: 'desc' }; + + const items = await prisma.item.findMany({ + where: { + OR: [ + { name: { contains: keyword, mode: 'insensitive' } }, + { description: { contains: keyword, mode: 'insensitive' } }, + ], + }, + include: { + user: { + include: { + userProfile: true, + }, + }, + }, + orderBy: sortOptions, + skip: limit * (page - 1), + take: limit, + }); + + return apiResponse(true, '성공적으로 아이템을 가져왔습니다.', { + items, + pagination: { page, limit, total, totalPage }, + }); + } catch (error) { + console.error('API Error:', error); + return apiResponse(false, 'Internal Server Error', null, 500); + } +}; diff --git a/src/app/articles/layout.jsx b/src/app/articles/layout.jsx index 6e60f6a7..9482c9e2 100644 --- a/src/app/articles/layout.jsx +++ b/src/app/articles/layout.jsx @@ -1,9 +1,6 @@ -/* + #은 jsconfig설정에 의해 최상위 루트를 가리키게된다. */ -import '../globals.css' - import { Footer } from '@/components/layouts/Footer' import { Navigation } from '@/components/ui/navigation' -import { rootMetadata } from '#/config/metadata' +import { articleMetadata } from '#/config/metadata' export default function ArticleLayout({ children }) { return ( @@ -15,4 +12,4 @@ export default function ArticleLayout({ children }) { ) } -export const metadata = { ...rootMetadata } +export const metadata = { ...articleMetadata } diff --git a/src/app/articles/page.jsx b/src/app/articles/page.jsx index 97b7a954..3f24b0c5 100644 --- a/src/app/articles/page.jsx +++ b/src/app/articles/page.jsx @@ -18,7 +18,7 @@ export default async function ArticlePage(props) { ]); return ( -
+
{/* 베스트 게시글 영역 */} diff --git a/src/app/items/detail/[id]/_components/back-to-articles.jsx b/src/app/items/detail/[id]/_components/back-to-articles.jsx new file mode 100644 index 00000000..51b3b561 --- /dev/null +++ b/src/app/items/detail/[id]/_components/back-to-articles.jsx @@ -0,0 +1,24 @@ +import Image from "next/image" +import Link from "next/link" + +import Undo from '@/assets/icons/ic_undo.svg' +import { paths } from '#/config/paths' + +export function BackToItems() { + return ( + +

+ 목록으로 돌아가기 +

+ undo-img + + ) +} diff --git a/src/app/items/detail/[id]/_components/comment/item-comment-form.jsx b/src/app/items/detail/[id]/_components/comment/item-comment-form.jsx new file mode 100644 index 00000000..93832725 --- /dev/null +++ b/src/app/items/detail/[id]/_components/comment/item-comment-form.jsx @@ -0,0 +1,25 @@ + +export function ItemCommentForm({ item, action }) { + + return ( +
+

+ 문의하기 +

+
+ + +