Skip to content

Commit 9fa0f2f

Browse files
committed
feat: implement comment feature with authentication and authorization
1 parent d3b8afe commit 9fa0f2f

File tree

13 files changed

+343
-80
lines changed

13 files changed

+343
-80
lines changed

src/app.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import { specs, swaggerUiOptions } from './swaggerOptions.js';
1212
// 라우터 import
1313
import productRouter from './routes/productRouter.js';
1414
import articleRouter from './routes/articleRouter.js';
15-
import commentRouter from './routes/commentRouter.js';
15+
import productCommentRouter from './routes/productCommentRouter.js';
16+
import articleCommentRouter from './routes/articleCommentRouter.js';
1617
import authRouter from './routes/authRouter.js';
1718

1819
const app = express();
@@ -32,7 +33,8 @@ app.use(morgan('combined'));
3233
// 라우터 설정
3334
app.use('/products', productRouter);
3435
app.use('/articles', articleRouter);
35-
app.use('/comments', commentRouter);
36+
app.use('/product-comments', productCommentRouter);
37+
app.use('/article-comments', articleCommentRouter);
3638
app.use('/auth', authRouter);
3739

3840
// Swagger API Docs Setting
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as commentRepository from '../repositories/commentRepository.js';
2+
3+
export const createArticleComment = async (req, res, next) => {
4+
try {
5+
const { articleId } = req.params;
6+
const { content } = req.body;
7+
const ownerId = req.user.id;
8+
9+
const comment = await commentRepository.createCommentByArticle(articleId, content, ownerId);
10+
res.status(201).json({ success: true, data: comment });
11+
} catch (error) {
12+
next(error);
13+
}
14+
};
15+
16+
export const updateArticleComment = async (req, res, next) => {
17+
try {
18+
const { commentId } = req.params;
19+
const { content } = req.body;
20+
const ownerId = req.user.id;
21+
22+
const comment = await commentRepository.updateCommentByArticle(commentId, content, ownerId);
23+
res.status(200).json({ success: true, data: comment });
24+
} catch (error) {
25+
next(error);
26+
}
27+
};
28+
29+
export const deleteArticleComment = async (req, res, next) => {
30+
try {
31+
const { commentId } = req.params;
32+
const ownerId = req.user.id;
33+
34+
await commentRepository.deleteCommentByArticle(commentId);
35+
res.status(200).json({ success: true, message: 'Comment deleted successfully' });
36+
} catch (error) {
37+
next(error);
38+
}
39+
};

src/controllers/commentControllers.js

Lines changed: 57 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,94 @@
11
import prisma from '../middlewares/prisma.js';
22

3-
export const createComment = async (req, res, next) => {
3+
export const getCommentByArticle = async (req, res, next) => {
44
try {
5-
const { content, articleId } = req.body;
6-
7-
if (!content) {
8-
return res.status(400).json({ success: false, message: 'Content is required' });
9-
}
5+
const { id } = req.params;
6+
const cursor = req.query.cursor;
7+
const limit = parseInt(req.query.limit) || 10;
8+
const direction = req.query.direction || 'next'; // 'next' or 'prev'
109

11-
const comment = await prisma.comment.create({ data: { content, articleId } });
12-
res.status(201).json({ success: true, data: comment });
13-
} catch (error) {
14-
next(error);
15-
}
16-
};
10+
const validLimit = Math.min(Math.max(1, limit), 50); // 1-50 사이로 제한
1711

18-
export const updateComment = async (req, res, next) => {
19-
try {
20-
const { id } = req.params;
21-
const { content } = req.body;
12+
let whereCondition = { articleId: id };
13+
let orderBy = { createdAt: 'desc' };
2214

23-
if (!id) {
24-
return res.status(400).json({ success: false, message: 'Id is required' });
15+
// cursor가 있는 경우
16+
if (cursor) {
17+
const cursorDate = new Date(cursor);
18+
if (direction === 'prev') {
19+
// 이전 페이지: cursor보다 이전 데이터
20+
whereCondition.createdAt = { lt: cursorDate };
21+
orderBy = { createdAt: 'desc' };
22+
} else {
23+
// 다음 페이지: cursor보다 이후 데이터
24+
whereCondition.createdAt = { gt: cursorDate };
25+
orderBy = { createdAt: 'asc' };
26+
}
2527
}
2628

27-
const comment = await prisma.comment.update({ where: { id }, data: { content } });
28-
res.status(200).json({ success: true, data: comment });
29-
} catch (error) {
30-
next(error);
31-
}
32-
};
29+
const comments = await prisma.comment.findMany({
30+
where: whereCondition,
31+
take: validLimit + 1, // 한 개 더 가져와서 hasNextPage 확인
32+
orderBy,
33+
select: {
34+
id: true,
35+
content: true,
36+
createdAt: true,
37+
updatedAt: true,
38+
},
39+
});
3340

34-
export const deleteComment = async (req, res, next) => {
35-
try {
36-
const { id } = req.params;
41+
// hasNextPage 확인
42+
const hasNextPage = comments.length > validLimit;
43+
if (hasNextPage) {
44+
comments.pop(); // 마지막 요소 제거
45+
}
3746

38-
if (!id) {
39-
return res.status(400).json({ success: false, message: 'Id is required' });
47+
// direction이 'prev'인 경우 결과를 다시 역순으로 정렬
48+
if (direction === 'prev') {
49+
comments.reverse();
4050
}
4151

42-
await prisma.comment.delete({ where: { id } });
43-
res.status(200).json({ success: true, data: null });
44-
} catch (error) {
45-
next(error);
46-
}
47-
};
52+
// cursor 정보 계산
53+
const nextCursor =
54+
comments.length > 0 ? comments[comments.length - 1].createdAt.toISOString() : null;
55+
const prevCursor = comments.length > 0 ? comments[0].createdAt.toISOString() : null;
4856

49-
export const getComment = async (req, res, next) => {
50-
try {
51-
const comment = await prisma.comment.findMany();
52-
res.status(200).json({ success: true, data: comment });
57+
res.status(200).json({
58+
success: true,
59+
data: comments,
60+
pagination: {
61+
hasNextPage,
62+
hasPrevPage: !!cursor,
63+
nextCursor,
64+
prevCursor,
65+
limit: validLimit,
66+
direction,
67+
},
68+
});
5369
} catch (error) {
5470
next(error);
5571
}
5672
};
5773

58-
export const getCommentByArticle = async (req, res, next) => {
74+
export const getCommentByProduct = async (req, res, next) => {
5975
try {
6076
const { id } = req.params;
6177
const cursor = req.query.cursor;
6278
const limit = parseInt(req.query.limit) || 10;
6379
const direction = req.query.direction || 'next'; // 'next' or 'prev'
6480

65-
if (!id) {
66-
return res.status(400).json({ success: false, message: 'Id is required' });
67-
}
68-
6981
const validLimit = Math.min(Math.max(1, limit), 50); // 1-50 사이로 제한
7082

71-
let whereCondition = { articleId: id };
83+
let whereCondition = { productId: id };
7284
let orderBy = { createdAt: 'desc' };
7385

74-
// cursor가 있는 경우
7586
if (cursor) {
7687
const cursorDate = new Date(cursor);
7788
if (direction === 'prev') {
78-
// 이전 페이지: cursor보다 이전 데이터
7989
whereCondition.createdAt = { lt: cursorDate };
8090
orderBy = { createdAt: 'desc' };
8191
} else {
82-
// 다음 페이지: cursor보다 이후 데이터
8392
whereCondition.createdAt = { gt: cursorDate };
8493
orderBy = { createdAt: 'asc' };
8594
}
@@ -97,18 +106,15 @@ export const getCommentByArticle = async (req, res, next) => {
97106
},
98107
});
99108

100-
// hasNextPage 확인
101109
const hasNextPage = comments.length > validLimit;
102110
if (hasNextPage) {
103111
comments.pop(); // 마지막 요소 제거
104112
}
105113

106-
// direction이 'prev'인 경우 결과를 다시 역순으로 정렬
107114
if (direction === 'prev') {
108115
comments.reverse();
109116
}
110117

111-
// cursor 정보 계산
112118
const nextCursor =
113119
comments.length > 0 ? comments[comments.length - 1].createdAt.toISOString() : null;
114120
const prevCursor = comments.length > 0 ? comments[0].createdAt.toISOString() : null;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as commentRepository from '../repositories/commentRepository.js';
2+
3+
export const createProductComment = async (req, res, next) => {
4+
try {
5+
const { productId } = req.params;
6+
const { content } = req.body;
7+
const ownerId = req.user.id;
8+
9+
const comment = await commentRepository.createCommentByProduct(productId, content, ownerId);
10+
res.status(201).json({ success: true, data: comment });
11+
} catch (error) {
12+
next(error);
13+
}
14+
};
15+
16+
export const updateProductComment = async (req, res, next) => {
17+
try {
18+
const { commentId } = req.params;
19+
const { content } = req.body;
20+
const ownerId = req.user.id;
21+
22+
const comment = await commentRepository.updateCommentByProduct(commentId, content, ownerId);
23+
res.status(200).json({ success: true, data: comment });
24+
} catch (error) {
25+
next(error);
26+
}
27+
};
28+
29+
export const deleteProductComment = async (req, res, next) => {
30+
try {
31+
const { commentId } = req.params;
32+
const ownerId = req.user.id;
33+
34+
await commentRepository.deleteCommentByProduct(commentId);
35+
res.status(200).json({ success: true, message: 'Comment deleted successfully' });
36+
} catch (error) {
37+
next(error);
38+
}
39+
};

src/middlewares/ownership.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as productRepository from '../repositories/productRepository.js';
22
import * as articleRepository from '../repositories/articleRepository.js';
3+
import * as commentRepository from '../repositories/commentRepository.js';
34

45
export const verifyProductOwner = async (req, res, next) => {
56
try {
@@ -42,3 +43,45 @@ export const verifyArticleOwner = async (req, res, next) => {
4243
next(error);
4344
}
4445
};
46+
47+
export const verifyProductCommentOwner = async (req, res, next) => {
48+
try {
49+
const { commentId } = req.params;
50+
const comment = await commentRepository.findProductByIdForOwner(commentId);
51+
52+
if (!comment) {
53+
return res.status(404).json({ success: false, message: 'Comment not found' });
54+
}
55+
56+
if (comment.ownerId !== req.user.id) {
57+
return res
58+
.status(403)
59+
.json({ success: false, message: 'You are not the owner of this comment' });
60+
}
61+
62+
next();
63+
} catch (error) {
64+
next(error);
65+
}
66+
};
67+
68+
export const verifyArticleCommentOwner = async (req, res, next) => {
69+
try {
70+
const { commentId } = req.params;
71+
const comment = await commentRepository.findArticleByIdForOwner(commentId);
72+
73+
if (!comment) {
74+
return res.status(404).json({ success: false, message: 'Comment not found' });
75+
}
76+
77+
if (comment.ownerId !== req.user.id) {
78+
return res
79+
.status(403)
80+
.json({ success: false, message: 'You are not the owner of this comment' });
81+
}
82+
83+
next();
84+
} catch (error) {
85+
next(error);
86+
}
87+
};
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { body, param } from 'express-validator';
2+
3+
export const createCommentValidator = [
4+
body('content').notEmpty().withMessage('Content is required'),
5+
];
6+
7+
export const updateCommentValidator = [
8+
body('content').notEmpty().withMessage('Content is required'),
9+
];
10+
11+
export const commentIdValidator = [param('commentId').isUUID().withMessage('Invalid comment id')];
12+
13+
export const articleIdValidator = [param('articleId').isUUID().withMessage('Invalid article id')];

src/middlewares/validate/productValidator.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ export const updateProductValidator = [
1414
body('tags').optional().isArray().withMessage('Tags must be an array'),
1515
];
1616

17-
export const productIdValidator = [param('id').isUUID().withMessage('Invalid product ID')];
17+
export const productIdValidator = [param('productId').isUUID().withMessage('Invalid product ID')];
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import prisma from '../middlewares/prisma.js';
2+
3+
export const createCommentByProduct = async (productId, content, ownerId) => {
4+
return await prisma.productComment.create({
5+
data: { productId, content, ownerId },
6+
});
7+
};
8+
9+
export const createCommentByArticle = async (articleId, content, ownerId) => {
10+
return await prisma.articleComment.create({
11+
data: { articleId, content, ownerId },
12+
});
13+
};
14+
15+
export const updateCommentByProduct = async (commentId, content, ownerId) => {
16+
return await prisma.productComment.update({
17+
where: { id: commentId },
18+
data: { content, ownerId },
19+
});
20+
};
21+
22+
export const updateCommentByArticle = async (commentId, content, ownerId) => {
23+
return await prisma.articleComment.update({
24+
where: { id: commentId },
25+
data: { content, ownerId },
26+
});
27+
};
28+
29+
export const deleteCommentByProduct = async (commentId) => {
30+
return await prisma.productComment.delete({
31+
where: { id: commentId },
32+
});
33+
};
34+
35+
export const deleteCommentByArticle = async (commentId) => {
36+
return await prisma.articleComment.delete({
37+
where: { id: commentId },
38+
});
39+
};
40+
41+
export const findProductByIdForOwner = async (commentId) => {
42+
return await prisma.productComment.findUnique({
43+
where: { id: commentId },
44+
select: { id: true, ownerId: true },
45+
});
46+
};
47+
48+
export const findArticleByIdForOwner = async (commentId) => {
49+
return await prisma.articleComment.findUnique({
50+
where: { id: commentId },
51+
select: { id: true, ownerId: true },
52+
});
53+
};

0 commit comments

Comments
 (0)