v1.12.24 멤버 작성 이력 통합 표 및 종류 필터 추가

This commit is contained in:
2026-10-21 17:03:07 +09:00
parent 7c95797fd2
commit 93dc6c04bd
13 changed files with 229 additions and 73 deletions

View File

@@ -0,0 +1,55 @@
import { getPostgresClient } from './postgres-client'
import { createPostSummary } from '../../composables/createPostSummary.js'
/**
* 회원의 게시글·댓글을 합쳐 최신순으로 검색하고 페이지 조회한다.
* @param {{ memberId: string, type: 'all'|'post'|'comment', q: string, page: number, pageSize: number }} options - 조회 조건
* @returns {Promise<Object>} 통합 작성 이력과 페이지 정보
*/
export const listMemberActivityForAdmin = async ({ memberId, type, q, page, pageSize }) => {
const sql = getPostgresClient()
if (!sql) return { items: [], pagination: { page: 1, pageSize, total: 0, pageCount: 1 } }
return sql.begin(async (transaction) => {
await transaction`SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY`
/**
* 집계와 목록에 동일한 작성자·종류·검색 조건을 적용한다.
* @returns {Object} SQL 조회 조각
*/
const activityQuery = () => transaction`
SELECT 'post'::text AS kind, posts.id, posts.id AS "postId", posts.title AS "postTitle",
posts.slug AS "postSlug", posts.status AS "postStatus", posts.published_at AS "publishedAt",
posts.created_at AS "createdAt", users.username,
LEFT(COALESCE(NULLIF(posts.excerpt, ''), posts.content, ''), 1000) AS body
FROM posts INNER JOIN users ON users.id = posts.author_id
WHERE posts.author_id = ${memberId} AND ${type} <> 'comment'
AND (${q} = '' OR strpos(lower(concat_ws(' ', posts.title, posts.slug, posts.excerpt, posts.content)), lower(${q})) > 0)
UNION ALL
SELECT 'comment'::text AS kind, comments.id, posts.id AS "postId", posts.title AS "postTitle",
posts.slug AS "postSlug", posts.status AS "postStatus", posts.published_at AS "publishedAt",
comments.created_at AS "createdAt", users.username, comments.body
FROM comments
INNER JOIN posts ON posts.id = comments.post_id
INNER JOIN users ON users.id = comments.user_id
WHERE comments.user_id = ${memberId} AND ${type} <> 'post'
AND (${q} = '' OR strpos(lower(concat_ws(' ', comments.body, posts.title)), lower(${q})) > 0)
`
const [count] = await transaction`SELECT COUNT(*)::int AS total FROM (${activityQuery()}) AS activity`
const total = Number(count.total)
const pageCount = Math.max(1, Math.ceil(total / pageSize))
const currentPage = Math.min(page, pageCount)
const rows = await transaction`
SELECT * FROM (${activityQuery()}) AS activity
ORDER BY "createdAt" DESC, kind ASC, id DESC
LIMIT ${pageSize} OFFSET ${(currentPage - 1) * pageSize}
`
const items = rows.map((row) => ({
...row,
body: row.kind === 'post' ? createPostSummary('', row.body, { maxLength: 320 }) : row.body,
publicUrl: row.postStatus === 'members' || (row.postStatus === 'published' && (!row.publishedAt || row.publishedAt <= new Date()))
? `/post/${encodeURIComponent(row.postSlug)}${row.kind === 'comment' ? '#comments' : ''}`
: null
}))
return { items, pagination: { page: currentPage, pageSize, total, pageCount } }
})
}

View File

@@ -0,0 +1,26 @@
import { createError, getQuery, getRouterParam } from 'h3'
import { z } from 'zod'
import { requireAdminSession } from '../../../../../utils/admin-auth'
import { listMemberActivityForAdmin } from '../../../../../repositories/member-activity-repository'
const querySchema = z.object({
memberId: z.string().uuid(),
type: z.enum(['all', 'post', 'comment']).default('all'),
q: z.string().trim().max(200).default(''),
page: z.coerce.number().int().min(1).max(1000000).default(1),
pageSize: z.coerce.number().int().min(1).max(100).default(10)
})
/**
* 관리자 회원 작성 이력을 게시글·댓글 통합 페이지로 조회한다.
* @param {import('h3').H3Event} event - 요청 이벤트
* @returns {Promise<Object>} 통합 작성 이력
*/
export default defineEventHandler((event) => {
requireAdminSession(event)
const result = querySchema.safeParse({ ...getQuery(event), memberId: getRouterParam(event, 'id') })
if (!result.success) {
throw createError({ statusCode: 400, message: '작성 이력 조회 조건이 올바르지 않습니다.' })
}
return listMemberActivityForAdmin(result.data)
})