Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,13 @@ export const signupMetadata = {
title: '판다마켓 회원가입',
description: '판다마켓 회원가입 페이지',
};

export const articleMetadata = {
title: '판다마켓 게시글',
description: '판다마켓 게시글 페이지',
};

export const itemMetadata = {
title: '판다마켓 상품',
description: '판다마켓 상품 페이지',
};
3 changes: 3 additions & 0 deletions config/paths.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export const paths = {
registration: {
getHref: () => '/articles/registration',
},
items: {
getHref: () => '/items',
},
},

auth: {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 1 addition & 4 deletions prisma/seed.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,17 @@ import {
randAvatar,
randCatchPhrase,
randEmail,
randFirstName,
randFullName,
randImg,
randLastName,
randNumber,
randParagraph,
randPassword,
randProduct,
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('기존 데이터 삭제 시작...');
Expand Down
File renamed without changes.
32 changes: 32 additions & 0 deletions src/app/api/(auth)/logout/route.js
Original file line number Diff line number Diff line change
@@ -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 },
);
}
}
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/app/api/articles/[id]/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
55 changes: 55 additions & 0 deletions src/app/api/items/detail/[id]/route.js
Original file line number Diff line number Diff line change
@@ -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,
);
}
};
52 changes: 52 additions & 0 deletions src/app/api/items/route.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
7 changes: 2 additions & 5 deletions src/app/articles/layout.jsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -15,4 +12,4 @@ export default function ArticleLayout({ children }) {
)
}

export const metadata = { ...rootMetadata }
export const metadata = { ...articleMetadata }
2 changes: 1 addition & 1 deletion src/app/articles/page.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export default async function ArticlePage(props) {
]);

return (
<main className="max-w-full min-h-screen flex-1 x-[21.4375rem] md:x-7xl my-0 mx-auto p-5">
<main className="container max-w-480 min-h-screen flex-1 x-[21.4375rem] md:x-7xl my-0 mx-auto p-5">
{/* 베스트 게시글 영역 */}
<ArticleBestSection articles={bestArticles} />

Expand Down
24 changes: 24 additions & 0 deletions src/app/items/detail/[id]/_components/back-to-articles.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
className="flex self-center items-center justify-center w-62 h-12 py-3 px-16 mt-16 mb-48.5 rounded-[2.5rem] bg-primary-100 gap-2 no-underline active:bg-primary-100"
href={paths.app.items.getHref()}
>
<p className="font-pretendard text-base font-semibold leading-6.5 text-white text-nowrap">
목록으로 돌아가기
</p>
<Image
src={Undo}
alt="undo-img"
width={24} height={24}
unoptimized
/>
</Link>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

export function ItemCommentForm({ item, action }) {

return (
<section className="container w-full mb-8 md:max-w-7xl">
<h3 className="font-pretendard font-semibold mb-2 mt-6 leading-6.5">
문의하기
</h3>
<form action={action}>
<input type="hidden" name="userId" value={item.userId} />
<input type="hidden" name="itemId" value={item.id} />
<textarea
name="context"
className="max-w-85.75 md:max-w-7xl w-full h-32.25 md:h-26 p-2.5 border-0 rounded-md resize-none mb-2 bg-gray-100 placeholder:text-gray-400 placeholder:text-sm placeholder:leading-6"
placeholder="개인정보를 공유 및 요청하거나, 명예 훼손, 무단 광고, 불법 정보 유포시 모니터링 후 삭제될 수 있으며, 이에 대한 민형사상 책임은 게시자에게 있습니다."
/>
<div className="flex justify-end">
<button className="flex justify-center items-center py-3 px-5.75 grow-0 bg-gray-400 border-none rounded-md text-white whitespace-nowrap cursor-pointer hover:bg-primary-100">
등록
</button>
</div>
</form>
</section>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Image from 'next/image'
import React from 'react'

import CommentEmptyImg from '@/assets/article/Img_reply_empty.svg'
import DefaultImg from '@/assets/logo.svg'
import { formatDate } from "@/libs/utils/format";

import { DropdownContent } from '../dropdown-content'

export function ItemCommentSection({ item, action }) {
return (
<section className="flex flex-col gap-6 no-underline list-none">
{item.Comment && item.Comment.length > 0 ? (
article.Comment.map((comment) => (
<li key={comment.id} className="flex flex-col border-2.5 border-t-0 border-r-0 border-l-0 border border-solid border-gray-300 py-3 px-0 gap-6 bg-gray-50">
<div className="flex justify-between">
<DropdownContent comment={comment} action={action} />
</div>
<div className="flex items-center gap-2 mb-1.5">
<Image
src={
comment.author?.userProfile?.photoUrl || { DefaultImg }
}
alt="avatar"
width={32}
height={32}
className="shrink-0 w-8 h-8 rounded-full object-cover"
/>
<div>
<span className="font-pretendard text-xs leading-4.5 text-gray-600">
{comment.author?.name}
</span>
<p className="text-gray-400 text-xs leading-4.5">
{formatDate(comment.createdAt)}
</p>
</div>
</div>
</li>
))
) : (
<>
<Image
className="self-center mb-4"
width={140}
height={140}
src={CommentEmptyImg}
alt="comment-empty-img"
/>
<p className="text-gray-400 text-center font-pretendard leading-6.5">
아직 댓글이 없어요,
<br />
지금 댓글을 달아보세요!
</p>
</>
)}
</section>
)
}
Loading