Skip to content

Commit 59b53ab

Browse files
committed
댓글 작성 기능 추가
This reverts commit 3fe217d.
1 parent 3fe217d commit 59b53ab

File tree

19 files changed

+1234
-80
lines changed

19 files changed

+1234
-80
lines changed

auth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export const {
2727
},
2828
async session({ session }) {
2929
try {
30+
// 이메일이 없으면 세션을 반환하지 않음
31+
if (!session?.user?.email) {
32+
console.error("Session callback - No email in session:", session);
33+
return session;
34+
}
35+
3036
const userEmail = session.user.email;
3137
const userName = session.user.name || "Unknown";
3238
const userImage = session.user.image || null;

scripts/seed.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,43 @@ async function seedPosts(client) {
9696
}
9797
}
9898

99+
async function seedComments(client) {
100+
try {
101+
await client.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
102+
103+
// Create the "comments" table if it doesn't exist
104+
await client.query(`
105+
CREATE TABLE IF NOT EXISTS comments (
106+
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
107+
post_id UUID NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
108+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
109+
content TEXT NOT NULL,
110+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
111+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
112+
deleted_at TIMESTAMP DEFAULT NULL
113+
)
114+
`);
115+
116+
// 인덱스 생성
117+
await client.query(`
118+
CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments(post_id);
119+
CREATE INDEX IF NOT EXISTS idx_comments_user_id ON comments(user_id);
120+
CREATE INDEX IF NOT EXISTS idx_comments_created_at ON comments(created_at);
121+
`);
122+
123+
console.log(`Created "comments" table`);
124+
} catch (error) {
125+
console.error("Error seeding comments:", error);
126+
throw error;
127+
}
128+
}
129+
99130
async function main() {
100131
const client = await db.connect();
101132
console.log("Connected to the database");
102133
await seedUsers(client);
103134
await seedPosts(client);
135+
await seedComments(client);
104136

105137
await client.end();
106138
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use server";
2+
3+
import { sql } from "@vercel/postgres";
4+
import { unstable_noStore as noStore, revalidatePath } from "next/cache";
5+
import { auth } from "@/auth";
6+
import { ensureCommentsTable } from "@/utils/ensureCommentsTable";
7+
8+
/**
9+
* 댓글을 삭제합니다 (soft delete).
10+
* @param commentId 댓글 ID
11+
*/
12+
export async function deleteComment(commentId: string): Promise<void> {
13+
noStore();
14+
15+
// 인증 체크
16+
const session = await auth();
17+
console.log("session", session);
18+
if (!session?.user) {
19+
throw new Error("Unauthorized: 로그인이 필요합니다.");
20+
}
21+
22+
const userEmail = session.user.email;
23+
if (!userEmail) {
24+
throw new Error("Unauthorized: 사용자 이메일 정보를 찾을 수 없습니다.");
25+
}
26+
27+
try {
28+
// 테이블이 없으면 생성
29+
await ensureCommentsTable();
30+
31+
// 사용자 ID 가져오기
32+
const { rows: userRows } = await sql`
33+
SELECT id FROM users WHERE email = ${userEmail}
34+
`;
35+
36+
if (!userRows[0]) {
37+
throw new Error("사용자 정보를 찾을 수 없습니다.");
38+
}
39+
40+
const userId = userRows[0].id;
41+
42+
// 댓글 소유권 확인 및 삭제
43+
const { rows: commentRows } = await sql`
44+
UPDATE comments
45+
SET deleted_at = CURRENT_TIMESTAMP
46+
WHERE id = ${commentId}
47+
AND user_id = ${userId}
48+
AND deleted_at IS NULL
49+
RETURNING post_id
50+
`;
51+
52+
if (!commentRows[0]) {
53+
throw new Error("댓글을 찾을 수 없거나 삭제 권한이 없습니다.");
54+
}
55+
56+
// 게시글 index 가져오기
57+
const { rows: postRows } = await sql`
58+
SELECT index FROM posts WHERE id = ${commentRows[0].post_id}
59+
`;
60+
61+
if (postRows[0]) {
62+
revalidatePath(`/posts/${postRows[0].index}`);
63+
}
64+
} catch (error) {
65+
console.error("Database Error:", error);
66+
throw new Error(
67+
error instanceof Error
68+
? error.message
69+
: "댓글 삭제 중 오류가 발생했습니다."
70+
);
71+
}
72+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use server";
2+
3+
import { sql } from "@vercel/postgres";
4+
import { unstable_noStore as noStore } from "next/cache";
5+
import camelcaseKeys from "camelcase-keys";
6+
import { ensureCommentsTable } from "@/utils/ensureCommentsTable";
7+
8+
/**
9+
* 특정 게시글의 댓글 목록을 가져옵니다.
10+
* @param postIndex 게시글의 index
11+
* @returns 댓글 목록
12+
*/
13+
export async function getComments(postIndex: number): Promise<IComment[]> {
14+
noStore();
15+
16+
try {
17+
// 테이블이 없으면 생성
18+
await ensureCommentsTable();
19+
20+
// 먼저 게시글 ID를 가져옵니다
21+
const { rows: postRows } = await sql`
22+
SELECT id FROM posts WHERE index = ${postIndex} AND status = 'published'
23+
`;
24+
25+
if (!postRows[0]) {
26+
return [];
27+
}
28+
29+
const postId = postRows[0].id;
30+
31+
// 댓글을 가져오면서 사용자 정보도 함께 조인
32+
const { rows } = await sql`
33+
SELECT
34+
c.id,
35+
c.post_id as "postId",
36+
p.index as "postIndex",
37+
c.user_id as "userId",
38+
u.email as "userEmail",
39+
u.name as "userName",
40+
u.image as "userImage",
41+
c.content,
42+
c.created_at as "createdAt",
43+
c.updated_at as "updatedAt",
44+
c.deleted_at as "deletedAt"
45+
FROM comments c
46+
INNER JOIN posts p ON c.post_id = p.id
47+
LEFT JOIN users u ON c.user_id = u.id
48+
WHERE c.post_id = ${postId} AND c.deleted_at IS NULL
49+
ORDER BY c.created_at ASC
50+
`;
51+
52+
return camelcaseKeys(rows, { deep: true }) as IComment[];
53+
} catch (error) {
54+
console.error("Database Error:", error);
55+
// 테이블이 없거나 에러가 발생하면 빈 배열 반환
56+
return [];
57+
}
58+
}

src/app/api/comments/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { getComments } from "./getComments";
2+
export { postComment } from "./postComment";
3+
export { putComment } from "./putComment";
4+
export { deleteComment } from "./deleteComment";
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"use server";
2+
3+
import { sql } from "@vercel/postgres";
4+
import { unstable_noStore as noStore, revalidatePath } from "next/cache";
5+
import { auth } from "@/auth";
6+
import camelcaseKeys from "camelcase-keys";
7+
import { ensureCommentsTable } from "@/utils/ensureCommentsTable";
8+
9+
/**
10+
* 댓글을 생성합니다.
11+
* @param postIndex 게시글의 index
12+
* @param content 댓글 내용
13+
*/
14+
export async function postComment({
15+
postIndex,
16+
content,
17+
}: {
18+
postIndex: number;
19+
content: string;
20+
}): Promise<IComment> {
21+
noStore();
22+
23+
// 인증 체크
24+
const session = await auth();
25+
console.log("session", session);
26+
if (!session?.user) {
27+
throw new Error("Unauthorized: 로그인이 필요합니다.");
28+
}
29+
30+
// 이메일이 없으면 에러
31+
const userEmail = session.user.email;
32+
if (!userEmail) {
33+
throw new Error("Unauthorized: 사용자 이메일 정보를 찾을 수 없습니다.");
34+
}
35+
36+
// 입력 검증
37+
if (!content || !content.trim()) {
38+
throw new Error("댓글 내용을 입력해주세요.");
39+
}
40+
41+
try {
42+
// 테이블이 없으면 생성
43+
await ensureCommentsTable();
44+
45+
// 게시글 ID 가져오기
46+
const { rows: postRows } = await sql`
47+
SELECT id FROM posts WHERE index = ${postIndex} AND status = 'published'
48+
`;
49+
50+
if (!postRows[0]) {
51+
throw new Error("게시글을 찾을 수 없습니다.");
52+
}
53+
54+
const postId = postRows[0].id;
55+
56+
// 사용자 ID 가져오기
57+
const { rows: userRows } = await sql`
58+
SELECT id FROM users WHERE email = ${userEmail}
59+
`;
60+
61+
if (!userRows[0]) {
62+
throw new Error("사용자 정보를 찾을 수 없습니다.");
63+
}
64+
65+
const userId = userRows[0].id;
66+
67+
// 댓글 생성
68+
const { rows } = await sql`
69+
INSERT INTO comments (post_id, user_id, content)
70+
VALUES (${postId}, ${userId}, ${content.trim()})
71+
RETURNING
72+
id,
73+
post_id as "postId",
74+
user_id as "userId",
75+
content,
76+
created_at as "createdAt",
77+
updated_at as "updatedAt",
78+
deleted_at as "deletedAt"
79+
`;
80+
81+
// 사용자 정보 조인하여 반환
82+
const { rows: commentWithUser } = await sql`
83+
SELECT
84+
c.id,
85+
c.post_id as "postId",
86+
p.index as "postIndex",
87+
c.user_id as "userId",
88+
u.email as "userEmail",
89+
u.name as "userName",
90+
u.image as "userImage",
91+
c.content,
92+
c.created_at as "createdAt",
93+
c.updated_at as "updatedAt",
94+
c.deleted_at as "deletedAt"
95+
FROM comments c
96+
INNER JOIN posts p ON c.post_id = p.id
97+
LEFT JOIN users u ON c.user_id = u.id
98+
WHERE c.id = ${rows[0].id}
99+
`;
100+
101+
revalidatePath(`/posts/${postIndex}`);
102+
103+
return camelcaseKeys(commentWithUser[0], { deep: true }) as IComment;
104+
} catch (error) {
105+
console.error("Database Error:", error);
106+
throw new Error(
107+
error instanceof Error
108+
? error.message
109+
: "댓글 작성 중 오류가 발생했습니다."
110+
);
111+
}
112+
}

src/app/api/comments/putComment.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"use server";
2+
3+
import { sql } from "@vercel/postgres";
4+
import { unstable_noStore as noStore, revalidatePath } from "next/cache";
5+
import { auth } from "@/auth";
6+
import { ensureCommentsTable } from "@/utils/ensureCommentsTable";
7+
8+
/**
9+
* 댓글을 수정합니다.
10+
* @param commentId 댓글 ID
11+
* @param content 댓글 내용
12+
*/
13+
export async function putComment({
14+
commentId,
15+
content,
16+
}: {
17+
commentId: string;
18+
content: string;
19+
}): Promise<void> {
20+
noStore();
21+
22+
// 인증 체크
23+
const session = await auth();
24+
if (!session?.user) {
25+
throw new Error("Unauthorized: 로그인이 필요합니다.");
26+
}
27+
28+
const userEmail = session.user.email;
29+
if (!userEmail) {
30+
throw new Error("Unauthorized: 사용자 이메일 정보를 찾을 수 없습니다.");
31+
}
32+
33+
// 입력 검증
34+
if (!content || !content.trim()) {
35+
throw new Error("댓글 내용을 입력해주세요.");
36+
}
37+
38+
try {
39+
// 테이블이 없으면 생성
40+
await ensureCommentsTable();
41+
42+
// 사용자 ID 가져오기
43+
const { rows: userRows } = await sql`
44+
SELECT id FROM users WHERE email = ${userEmail}
45+
`;
46+
47+
if (!userRows[0]) {
48+
throw new Error("사용자 정보를 찾을 수 없습니다.");
49+
}
50+
51+
const userId = userRows[0].id;
52+
53+
// 댓글 소유권 확인 및 수정
54+
const { rows: commentRows } = await sql`
55+
UPDATE comments
56+
SET content = ${content.trim()}, updated_at = CURRENT_TIMESTAMP
57+
WHERE id = ${commentId}
58+
AND user_id = ${userId}
59+
AND deleted_at IS NULL
60+
RETURNING post_id
61+
`;
62+
63+
if (!commentRows[0]) {
64+
throw new Error("댓글을 찾을 수 없거나 수정 권한이 없습니다.");
65+
}
66+
67+
// 게시글 index 가져오기
68+
const { rows: postRows } = await sql`
69+
SELECT index FROM posts WHERE id = ${commentRows[0].post_id}
70+
`;
71+
72+
if (postRows[0]) {
73+
revalidatePath(`/posts/${postRows[0].index}`);
74+
}
75+
} catch (error) {
76+
console.error("Database Error:", error);
77+
throw new Error(
78+
error instanceof Error
79+
? error.message
80+
: "댓글 수정 중 오류가 발생했습니다."
81+
);
82+
}
83+
}

0 commit comments

Comments
 (0)