댓글 시스템 복구

This commit is contained in:
2026-04-07 12:44:24 +09:00
parent 2c0b5268fa
commit 6bac13006a
16 changed files with 1403 additions and 15 deletions

View File

@@ -0,0 +1,49 @@
const express = require('express')
const { z } = require('zod')
const {
listCommentNotifications,
countUnreadCommentNotifications,
markCommentNotificationsRead,
} = require('../db')
const { requireAuth } = require('../middleware/auth')
const router = express.Router()
router.get('/inbox', requireAuth, async (req, res) => {
const schema = z.object({
unreadOnly: z
.union([z.literal('1'), z.literal('0'), z.literal('true'), z.literal('false')])
.optional()
.default('0'),
})
const parsed = schema.safeParse(req.query)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const unreadOnly = ['1', 'true'].includes(parsed.data.unreadOnly)
const notifications = await listCommentNotifications(req.session.userId, { unreadOnly })
res.json({ notifications })
})
router.get('/inbox/unread-count', requireAuth, async (req, res) => {
const unreadCount = await countUnreadCommentNotifications(req.session.userId)
res.json({ unreadCount })
})
router.post('/inbox/read', requireAuth, async (req, res) => {
const schema = z.object({
all: z.boolean().optional().default(false),
notificationIds: z.array(z.string().min(1).max(64)).max(100).optional().default([]),
})
const parsed = schema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
await markCommentNotificationsRead(req.session.userId, {
all: parsed.data.all,
notificationIds: parsed.data.notificationIds,
})
const unreadCount = await countUnreadCommentNotifications(req.session.userId)
res.json({ ok: true, unreadCount })
})
module.exports = router

View File

@@ -16,6 +16,11 @@ const {
favoriteTierList,
unfavoriteTierList,
duplicateTierListForUser,
listTierListComments,
findTierListCommentById,
createTierListComment,
deleteTierListComment,
createCommentNotificationsForComment,
} = require('../db')
const { requireAuth } = require('../middleware/auth')
const { createMemoryUpload, writeOptimizedImage } = require('../lib/image-storage')
@@ -122,6 +127,22 @@ const tierListUpsertSchema = z.object({
}
})
const tierListCommentSchema = z.object({
content: z.string().trim().min(1).max(2000),
parentCommentId: z.string().trim().max(64).optional().default(''),
})
async function getTierListAccessContext(req, tierListId) {
const tierList = await findTierListById(tierListId, req.session?.userId || '')
if (!tierList) return { error: 'not_found' }
if (tierList.isPublic) return { tierList, canRead: true, canEdit: req.session?.userId === tierList.authorId }
if (!req.session?.userId) return { error: 'forbidden' }
if (req.session.userId === tierList.authorId) return { tierList, canRead: true, canEdit: true }
const currentUser = await findUserById(req.session.userId)
if (!currentUser?.isAdmin) return { error: 'forbidden' }
return { tierList, canRead: true, canEdit: true, isAdmin: true }
}
router.get('/public', async (req, res) => {
const topicId = typeof req.query.topicId === 'string' ? req.query.topicId : ''
const queryText = typeof req.query.q === 'string' ? req.query.q : ''
@@ -142,14 +163,10 @@ router.get('/favorites/me', requireAuth, async (req, res) => {
})
router.get('/:id', async (req, res) => {
const t = await findTierListById(req.params.id, req.session?.userId || '')
if (!t) return res.status(404).json({ error: 'not_found' })
if (!t.isPublic) {
if (!req.session?.userId) return res.status(403).json({ error: 'forbidden' })
const currentUser = req.session.userId === t.authorId ? { isAdmin: false } : await findUserById(req.session.userId)
if (req.session.userId !== t.authorId && !currentUser?.isAdmin) return res.status(403).json({ error: 'forbidden' })
}
res.json({ tierList: normalizeTierList(t) })
const access = await getTierListAccessContext(req, req.params.id)
if (access.error === 'not_found') return res.status(404).json({ error: 'not_found' })
if (access.error === 'forbidden') return res.status(403).json({ error: 'forbidden' })
res.json({ tierList: normalizeTierList(access.tierList) })
})
router.post('/:id/duplicate', requireAuth, async (req, res) => {
@@ -189,6 +206,62 @@ router.delete('/:id/favorite', requireAuth, async (req, res) => {
res.json({ tierList: normalizeTierList(updated) })
})
router.get('/:id/comments', async (req, res) => {
const access = await getTierListAccessContext(req, req.params.id)
if (access.error === 'not_found') return res.status(404).json({ error: 'not_found' })
if (access.error === 'forbidden') return res.status(403).json({ error: 'forbidden' })
const comments = await listTierListComments(access.tierList.id)
res.json({ comments })
})
router.post('/:id/comments', requireAuth, async (req, res) => {
const parsed = tierListCommentSchema.safeParse(req.body)
if (!parsed.success) return res.status(400).json({ error: 'bad_request' })
const access = await getTierListAccessContext(req, req.params.id)
if (access.error === 'not_found') return res.status(404).json({ error: 'not_found' })
if (access.error === 'forbidden') return res.status(403).json({ error: 'forbidden' })
try {
const commentId = await createTierListComment({
tierListId: access.tierList.id,
authorId: req.session.userId,
parentCommentId: parsed.data.parentCommentId,
content: parsed.data.content,
})
await createCommentNotificationsForComment(commentId)
const comments = await listTierListComments(access.tierList.id)
res.json({ comments, createdCommentId: commentId })
} catch (error) {
if (error?.code === 'COMMENT_PARENT_INVALID') {
return res.status(400).json({ error: 'comment_parent_invalid' })
}
if (error?.code === 'COMMENT_REPLY_DEPTH_INVALID') {
return res.status(400).json({ error: 'comment_reply_depth_invalid' })
}
throw error
}
})
router.delete('/:id/comments/:commentId', requireAuth, async (req, res) => {
const access = await getTierListAccessContext(req, req.params.id)
if (access.error === 'not_found') return res.status(404).json({ error: 'not_found' })
if (access.error === 'forbidden') return res.status(403).json({ error: 'forbidden' })
const comment = await findTierListCommentById(req.params.commentId)
if (!comment || comment.tierListId !== access.tierList.id) return res.status(404).json({ error: 'not_found' })
const currentUser = req.session.userId === comment.authorId ? { isAdmin: false } : await findUserById(req.session.userId)
if (req.session.userId !== comment.authorId && !currentUser?.isAdmin) {
return res.status(403).json({ error: 'forbidden' })
}
await deleteTierListComment(comment.id)
const comments = await listTierListComments(access.tierList.id)
res.json({ comments })
})
router.post('/custom-items', requireAuth, upload.single('image'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'file_required' })