Skip to content

Commit 271f230

Browse files
authored
Merge pull request #105 from LimSB-dev/canary
canary to master (v2.0.0)
2 parents 676bd96 + 911948c commit 271f230

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3357
-278
lines changed

auth.config.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const authConfig = {
66
},
77
callbacks: {
88
authorized({ auth, request: { nextUrl } }) {
9-
console.log('authorized', auth, nextUrl);
109
const isLoggedIn = !!auth?.user;
1110
const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
1211

auth.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,21 @@ export const {
1818
callbacks: {
1919
async signIn({ user }) {
2020
try {
21-
console.log(user);
21+
// 사용자 로그인 처리
2222
return true;
2323
} catch (error) {
2424
console.error("Error handling signIn:", error);
25-
return false; // 에러가 발생해도 세션을 반환합니다.
25+
return false;
2626
}
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { GET, POST } from "../../../../../auth";
1+
export { GET, POST } from "@/auth";
22

33
export const runtime = "edge";
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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
* @param limit 가져올 댓글 개수 (기본값: 20)
12+
* @param offset 건너뛸 댓글 개수 (기본값: 0)
13+
* @returns 댓글 목록
14+
*/
15+
export async function getComments(
16+
postIndex: number,
17+
limit: number = 5,
18+
offset: number = 0
19+
): Promise<IComment[]> {
20+
noStore();
21+
22+
try {
23+
// 테이블이 없으면 생성
24+
await ensureCommentsTable();
25+
26+
// 먼저 게시글 ID를 가져옵니다
27+
const { rows: postRows } = await sql`
28+
SELECT id FROM posts WHERE index = ${postIndex} AND status = 'published'
29+
`;
30+
31+
if (!postRows[0]) {
32+
return [];
33+
}
34+
35+
const postId = postRows[0].id;
36+
37+
// 댓글을 가져오면서 사용자 정보도 함께 조인 (최신순 정렬)
38+
const { rows } = await sql`
39+
SELECT
40+
c.id,
41+
c.post_id as "postId",
42+
p.index as "postIndex",
43+
c.user_id as "userId",
44+
u.email as "userEmail",
45+
u.name as "userName",
46+
u.image as "userImage",
47+
c.content,
48+
c.created_at as "createdAt",
49+
c.updated_at as "updatedAt",
50+
c.deleted_at as "deletedAt"
51+
FROM comments c
52+
INNER JOIN posts p ON c.post_id = p.id
53+
LEFT JOIN users u ON c.user_id = u.id
54+
WHERE c.post_id = ${postId} AND c.deleted_at IS NULL
55+
ORDER BY c.created_at DESC
56+
LIMIT ${limit}
57+
OFFSET ${offset}
58+
`;
59+
60+
return camelcaseKeys(rows, { deep: true }) as IComment[];
61+
} catch (error) {
62+
console.error("Database Error:", error);
63+
// 테이블이 없거나 에러가 발생하면 빈 배열 반환
64+
return [];
65+
}
66+
}

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+
}

0 commit comments

Comments
 (0)