[NEXT] 7. 게시판 만들기, 기본 CRUD
드디어 마지막 관문인 게시판 만들기!
전 회사에서도 CRUD 정도는 해봤지만,, 백엔드 분이 만들어 주신 api를 받아서 프론트 쪽 작업만 했었기에,, 항상 api 만드는 과정이 궁금했었다 😳 다음 개인프로젝트에서 무조건 게시판을 만들어봐야겠다라고 결심한지 어언 몇 개월,, 드디어 실행에 옮겼다!
1️⃣ 글 작성페이지 만들기 (CREATE)
우선 퍼블리싱은 다 해놓은 상태니,, 데이터만 붙이이면 다 되니까 빨리 끝나겠지 ㅎㅎ 라고 생각했다면 경기도 오산 😧
1. schema.prisma 에서 필드 정의하기
model Post {
id String @default(cuid()) @id
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime?
}
2. 게시글 작성 페이지
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { SyntheticEvent, useEffect, useState } from "react";
const PostForm = () => {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [authorId, setAuthorId] = useState<Number | null>(null);
const { data: session } = useSession();
const author = session?.user?.name;
const router = useRouter();
useEffect(() => {
// 세션 데이터가 로드되면 사용자 식별자를 설정
if (session?.user?.id) {
setAuthorId(session.user.id);
}
}, [session]);
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
};
const handleSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
// authorId가 null인 경우 처리
if (authorId === null) {
console.error("사용자 식별자가 없습니다. 사용자를 다시 로그인하세요.");
return;
}
try {
await fetch("/api/addpost", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, content, authorId }),
});
router.refresh();
} catch (error) {
console.log(error);
}
setTitle("");
setContent("");
};
return (
<>
{/* <h1>{initialData.title ? "글 수정하기" : "글 작성하기"}</h1> */}
<form onSubmit={handleSubmit}>
<div>
<div>
<label htmlFor="title">글 제목</label>
<input
type="text"
id="title"
name="title"
value={title}
onChange={handleTitleChange}
required
/>
</div>
</div>
<div>
<label htmlFor="content">내용</label>
<textarea
name="content"
id="content"
value={content}
onChange={handleContentChange}
required
/>
</div>
<div>
<button type="submit">저장하기</button>
<button>취소하기</button>
</div>
</form>
</>
);
};
export default PostForm;
처음에는 작성자 이름을 보내려고 했으나,, db에서 author가 user와 관련되어 있어서 type 지정할 때 계속 오류가 나는 것이었다,,
여기서 한 시간은 헤맨 듯 ㅠㅠㅠ
그래서 그냥 authorId를 받아서 서버로 보내주기,, 생각해보니까 이름은 동명이인이 있을 수 있으니 id를 받아서 보내는 게 더 좋을 것 같다는 생각이 들었다!
3. addPost api 만들기
import prisma from "@/app/lib/prisma";
import { NextResponse } from "next/server";
type PostCreateInput = {
title: string;
content: string;
published: boolean;
authorId: number;
};
export async function POST(request: Request) {
const res: PostCreateInput = await request.json();
const { title, content, authorId } = res;
const result = await prisma.post.create({
data: {
title,
content,
published: true,
authorId,
updatedAt: new Date(),
},
});
return NextResponse.json({ result });
}
여기에서도 계속 오류가 났었는데... 저 updatedAt 때문이었다
DB에서 updatedAt이 필수로 지정해져 있어서 optional 하게 바꾸었는데도,,, 그래서 저렇게 new Date()로 지정해놓으니까 드디어 해결!
이게 맞는 방법인지 모르겠지만,, 차근차근 고쳐나가도록 해야겠다 🥹
4. postForm 컴포넌트에 붙이기
"use client";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { SyntheticEvent, useEffect, useState } from "react";
import * as styles from "./index.css";
const PostForm = () => {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [authorId, setAuthorId] = useState<Number | null>(null);
// 로그인한 유저 데이터
const { data: session } = useSession();
const router = useRouter();
useEffect(() => {
// 세션 데이터가 로드되면 사용자 식별자를 설정
if (session?.user?.id) {
setAuthorId(session.user.id);
}
}, [session]);
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
};
const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setContent(e.target.value);
};
const handleSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
// authorId가 null인 경우 처리
if (authorId === null) {
console.error("사용자 식별자가 없습니다. 사용자를 다시 로그인하세요.");
return;
}
try {
await fetch("/api/addpost", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, content, authorId }),
});
alert("저장하시겠습니까?");
router.push("/board");
router.refresh();
} catch (error) {
console.log(error);
}
setTitle("");
setContent("");
};
return (
<div className={styles.formBox}>
{/* <h1>{initialData.title ? "글 수정하기" : "글 작성하기"}</h1> */}
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.inputBox}>
<label htmlFor="title" className={styles.label}>
제목
</label>
<input
type="text"
id="title"
name="title"
value={title}
onChange={handleTitleChange}
required
className={styles.input}
/>
</div>
<div className={styles.textAreaBox}>
<label htmlFor="content" className={styles.label}>
내용
</label>
<textarea
name="content"
id="content"
value={content}
onChange={handleContentChange}
required
className={styles.textArea}
/>
</div>
<div className={styles.buttonBox}>
<button type="submit" className={styles.saveBtn}>
저장하기
</button>
</div>
<div className={styles.buttonBox}>
<p
onClick={() => {
alert(
"작성 중인 내용은 저장되지 않습니다. 그래도 취소하시겠습니까?"
);
router.push("/board");
}}
className={styles.cancelTxt}
>
취소하기
</p>
</div>
</form>
</div>
);
};
export default PostForm;
5. DB에서 확인
다행히 잘 들어오는 것이 확인된다,,
2️⃣ 게시글 데이터 불러오기 (READE)
1. 전체 게시글 리스트
// 전체 게시글 가져오는 함수
async function getPosts() {
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true }, // 작성자의 이름만을 선택
},
},
});
// 각 게시물의 작성자 이름만을 추출하여 반환
return posts.map((post) => ({
...post,
authorName: post.author?.name || "Unknown", // 작성자 이름 또는 'Unknown'으로 설정
}));
}
export default async function BoardListPage() {
const posts = await getPosts();
return (
<div className={styles.root}>
{/* <div className={styles.hotPost}>
<h2>인기 게시글</h2>
<div className={styles.hotPostItemBox}>
<BoardItem id={dummy.id} />
</div>
</div> */}
<div className={styles.newPost}>
<div className={styles.topBox}>
<h2>최근 게시글</h2>
<Link href="/board/write" className={styles.goWrite}>
게시글 작성 >
</Link>
</div>
<div className={styles.newPostTable1}>
<span className={`${styles.th} ${styles.th1}`}>No</span>
<span className={`${styles.th} ${styles.th2}`}>제목</span>
<span className={`${styles.th} ${styles.th3}`}>작성자</span>
<span className={`${styles.th} ${styles.th4}`}>날짜</span>
<span className={`${styles.th} ${styles.th5}`}>추천수</span>
</div>
<div className={styles.newPostTable2}>
{posts.map((post, index) => {
return (
<BoardNewItem
num={index + 1}
key={post.id}
id={post.id}
title={post.title}
content={post.content}
authorName={post.authorName}
createdAt={post.createdAt}
/>
);
})}
</div>
</div>
</div>
);
}
BoardNewItem 컴포넌트를 만들어서 데이터를 보내도록 하였다.
처음에는 grid로 스타일링 하여 한 줄에 4개씩 보이게 만들었는데 아무래도 게시판은 기본 게시판 스타일로 보여주는 게 좋을 것 같아서
2. 개별 게시글 페이지
- 개별 게시글 페이지 데이터 가져오는 api 작성 (api/post/[id]/route.ts)
import prisma from "@/app/lib/prisma";
import { NextResponse } from "next/server";
export async function GET(request: Request, { params }: any) {
const id = params.id;
const post = await prisma.post.findUnique({
where: { id },
include: {
author: true,
},
});
const authorName = post?.author?.name || "Unknown";
// 필요한 데이터만 추출하여 반환
const postData = {
id: post?.id,
title: post?.title,
content: post?.content,
published: post?.published,
authorId: post?.authorId,
createdAt: post?.createdAt,
updatedAt: post?.updatedAt,
authorName: authorName, // 작성자 이름 추가
};
return NextResponse.json(postData);
}
- 상세페이지 features에 적용
"use client";
const BoardDetailPage = ({ postId }: any) => {
// 로그인한 유저 정보
const { data: session } = useSession();
const userId = session?.user?.id;
// 좋아요
const [like, setLike] = useState(false);
const OnHeartClick = () => {
setLike(!like);
};
// 게시글
const [postState, setPostState] = useState<{
title: string;
authorName: string;
createdAt: string;
updatedAt: string | null;
content: string | null;
authorId: number | null;
}>({
title: "",
authorName: "",
createdAt: "",
updatedAt: null,
content: null,
authorId: null,
});
// 게시글 데이터 가져오기
useEffect(() => {
async function fetchPost() {
try {
const response = await fetch(`/api/post/${postId}`, {
method: "GET",
});
const postData = await response.json();
setPostState(postData);
} catch (error) {
console.error("Error fetching post:", error);
}
}
fetchPost();
}, [postId]);
const newFormat = dayjs(postState?.createdAt).format("YYYY-MM-DD H:mm:ss");
return (
<div className={styles.root}>
<div className={styles.titleBox}>
<div className={styles.titleTop}>
<p className={styles.title}>{postState?.title}</p>
{/* 로그인한 유저와 글쓴 유저가 같을 때만 나타남 */}
{userId === postState?.authorId && (
<div className={styles.subTxt}>
<UpdatePostButton postId={postId} />
<span className={styles.bar}>|</span>
<DeletePostButton postId={postId} />
</div>
)}
</div>
<div className={styles.info}>
<p>작성자: {postState?.authorName}</p>
<span>{newFormat}</span>
</div>
</div>
<div className={styles.top}>
{/* <span className={styles.mainImgBox}>
<Image src={example1} alt="예시사진" className={styles.mainImg} />
</span> */}
<span className={styles.heartBox}>
<Image
src={like ? heart_fill : heart}
alt="빈 하트"
className={styles.heartImg}
onClick={OnHeartClick}
/>
</span>
</div>
<div className={styles.bottom}>
<div className={styles.nameWrap}>{postState?.content}</div>
</div>
</div>
);
};
export default BoardDetailPage;
- fetch 함수를 통해서 만들어놓은 api를 불러오고
- 로그인 한 유저의 아이디와 작성자 아이디가 일치하면 수정, 삭제 버튼이 보이도록 하였다.
- 하트를 누르면 검은색 하트로 바뀌면서 그 정보가 db에 저장되도록 할 예정
- 로그인한 유저와 작성자가 같지 않을 때
- 로그인한 유저와 작성자가 같을 때
3️⃣ 게시글 수정하기 (UPDATE)
1. update api 작성하기
export async function POST(request: Request, { params }: any) {
const id = params.id;
const { title, content, authorId } = await request.json(); // JSON 형식으로 파싱
// 글 수정 로직 구현
const updatedPost = await prisma.post.update({
where: { id },
data: {
title,
content,
authorId,
},
});
return NextResponse.json(updatedPost);
2. postForm 컴포넌트 수정
"use client";
const PostForm = ({ postId }: any) => {
...
useEffect(() => {
// postId가 존재하면 해당 글 데이터를 불러옴
if (postId) {
// postId를 사용하여 글 데이터를 불러오는 API 호출
fetch(`/api/post/${postId}`)
.then((response) => response.json())
.then((postData) => {
setTitle(postData.title);
setContent(postData.content);
// 작성자 식별자를 설정
setAuthorId(postData.authorId);
})
.catch((error) => console.error("Error fetching post:", error));
}
}, [postId]);
// 새 글 작성 및 수정 로직
const handleSubmit = async (e: SyntheticEvent) => {
e.preventDefault();
// authorId가 null인 경우 처리
if (authorId === null) {
console.error("사용자 식별자가 없습니다. 사용자를 다시 로그인하세요.");
return;
}
try {
// postId가 있으면 글 수정, 없으면 새 글 작성
const url = postId ? `/api/post/${postId}` : "/api/addpost";
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title, content, authorId }),
});
alert("저장하시겠습니까?");
router.push("/board");
router.refresh();
} catch (error) {
console.log(error);
}
setTitle("");
setContent("");
};
return (
<div className={styles.formBox}>
<h1 className={styles.formTitle}>
{title ? "글 수정하기" : "글 작성하기"}
</h1>
<form onSubmit={handleSubmit} className={styles.form}>
<div className={styles.inputBox}>
<label htmlFor="title" className={styles.label}>
제목
</label>
<input
type="text"
id="title"
name="title"
value={title}
onChange={handleTitleChange}
required
className={styles.input}
/>
</div>
<div className={styles.textAreaBox}>
<label htmlFor="content" className={styles.label}>
내용
</label>
<textarea
name="content"
id="content"
value={content}
onChange={handleContentChange}
required
className={styles.textArea}
/>
</div>
<div className={styles.buttonBox}>
<button type="submit" className={styles.saveBtn}>
저장하기
</button>
</div>
<div className={styles.buttonBox}>
<p
onClick={() => {
alert(
"작성 중인 내용은 저장되지 않습니다. 그래도 취소하시겠습니까?"
);
router.push("/board");
}}
className={styles.cancelTxt}
>
취소하기
</p>
</div>
</form>
</div>
);
};
export default PostForm;
수정 잘 되는 것 확인!
4️⃣ 게시글 삭제하기 (DELETE)
1. delete api 작성하기
import prisma from "@/app/lib/prisma";
import { NextResponse } from "next/server";
export async function DELETE(request: Request, { params }: any) {
const id = params.id;
const post = await prisma.post.delete({
where: { id },
});
return NextResponse.json(post);
}
2. 삭제 버튼 컴포넌트 작성
"use client";
import { useRouter } from "next/navigation";
import * as styles from "./index.css";
const DeletePostButton = ({ postId }: any) => {
const router = useRouter();
async function handleClick() {
try {
await fetch(`/api/post/${postId}`, {
method: "DELETE",
});
router.refresh;
} catch (e) {
console.error(e);
}
}
return (
<span onClick={handleClick} className={styles.deleteBtn}>
삭제
</span>
);
};
export default DeletePostButton;
- asdfasdf 게시글이 삭제된 것을 볼 수 있다
이렇게 기본적인 CRUD도 끝!
이렇게 api까지 만들어 db 연결하고,, 처음이라 많이 헤맸지만 (typescript 오류까지 겹쳐서 더더,,) 풀로 다 만들어보니 전체적인 과정도 알 수 있어서 좋은 경험이었다!