diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-07-02 00:45:49 +0000 |
| commit | 2acf5f8966a40c1c9a97680c8dc263ee3f1ad3d1 (patch) | |
| tree | f406b5c86f563347c7fd088a85fd1a82284dc5ff /lib/qna/service.ts | |
| parent | 6a9ca20deddcdcbe8495cf5a73ec7ea5f53f9b55 (diff) | |
(대표님/최겸) 20250702 변경사항 업데이트
Diffstat (limited to 'lib/qna/service.ts')
| -rw-r--r-- | lib/qna/service.ts | 1006 |
1 files changed, 1006 insertions, 0 deletions
diff --git a/lib/qna/service.ts b/lib/qna/service.ts new file mode 100644 index 00000000..d9c877c6 --- /dev/null +++ b/lib/qna/service.ts @@ -0,0 +1,1006 @@ +"use server"; + +import { revalidateTag, unstable_noStore } from "next/cache"; +import { getServerSession } from "next-auth/next"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import db from "@/db/db"; +import { qna, qnaAnswer, qnaComments } from "@/db/schema/qna"; +import { qnaView, qnaAnswerView, qnaCommentView } from "@/db/schema"; +import { + eq, + desc, + asc, + and, + or, + ilike, + inArray, + gte, + lte, + isNull, + isNotNull, + count, + sql +} from "drizzle-orm"; +import { unstable_cache } from "@/lib/unstable-cache"; +import { filterColumns } from "@/lib/filter-columns"; +import type { + GetQnaSchema, + CreateQnaSchema, + UpdateQnaSchema, + CreateAnswerSchema, + UpdateAnswerSchema, + CreateCommentSchema, + UpdateCommentSchema, +} from "./validation"; + +/* ================================================================ + Helper Functions +================================================================ */ + +/** + * 인증된 사용자 정보 가져오기 + */ +async function getAuthenticatedUser() { + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + throw new Error("인증이 필요합니다."); + } + return parseInt(session.user.id); +} + +/** + * 현재 사용자 ID 가져오기 (비로그인 허용) + */ +async function getCurrentUserId() { + try { + const session = await getServerSession(authOptions); + return session?.user?.id ? parseInt(session.user.id) : null; + } catch { + return null; + } +} + +/* ================================================================ + Repository Functions (트랜잭션 지원) +================================================================ */ + +/** + * Q&A 목록 조회 Repository + */ +async function selectQnaList( + tx: any, + options: { + where?: any; + orderBy?: any[]; + offset: number; + limit: number; + } +) { + const { where, orderBy = [desc(qnaView.createdAt)], offset, limit } = options; + + return await tx + .select() + .from(qnaView) + .where(where) + .orderBy(...orderBy) + .offset(offset) + .limit(limit); +} + +/** + * Q&A 총 개수 조회 Repository + */ +async function countQnaList(tx: any, where?: any) { + const [result] = await tx + .select({ count: count() }) + .from(qnaView) + .where(where); + return result.count; +} + +/** + * Q&A 상세 조회 Repository + */ +async function selectQnaDetail(tx: any, id: number) { + const [question] = await tx + .select() + .from(qnaView) + .where(eq(qnaView.id, id)); + + return question || null; +} + +/** + * 답변 목록 조회 Repository + */ +async function selectAnswersByQnaId(tx: any, qnaId: number) { + return await tx + .select() + .from(qnaAnswerView) + .where(eq(qnaAnswerView.qnaId, qnaId)) + .orderBy(desc(qnaAnswerView.createdAt)); +} + +/** + * 댓글 목록 조회 Repository (계층형) + */ +async function selectCommentsByAnswerId(tx: any, answerId: number) { + return await tx + .select() + .from(qnaCommentView) + .where(eq(qnaCommentView.answerId, answerId)) + .orderBy(asc(qnaCommentView.createdAt)); // 댓글은 오래된 순으로 +} + +/** + * 내가 답변한 질문 ID 목록 조회 + */ +async function getMyAnsweredQnaIds(tx: any, userId: number, qnaIds: number[]) { + if (qnaIds.length === 0) return []; + + const results = await tx + .select({ qnaId: qnaAnswer.qnaId }) + .from(qnaAnswer) + .where( + and( + eq(qnaAnswer.author, userId), + inArray(qnaAnswer.qnaId, qnaIds), + eq(qnaAnswer.isDeleted, false) + ) + ); + + return results.map(r => r.qnaId); +} + +/* ================================================================ + 1) Q&A 목록 조회 (고급 필터링/정렬 지원) +================================================================ */ + +export async function getQnaList(input: GetQnaSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + const advancedTable = input.flags.includes("advancedTable") || true; + + // 현재 사용자 ID (내 답변 여부 확인용) + const currentUserId = await getCurrentUserId(); + + // 고급 필터링 WHERE 절 구성 + const advancedWhere = advancedTable + ? filterColumns({ + table: qnaView, + filters: input.filters, + joinOperator: input.joinOperator, + }) + : undefined; + + // 전역 검색 WHERE 절 + let globalWhere; + if (input.search) { + const searchPattern = `%${input.search}%`; + globalWhere = or( + ilike(qnaView.title, searchPattern), + ilike(qnaView.content, searchPattern), + ilike(qnaView.authorName, searchPattern), + ilike(qnaView.companyName, searchPattern) + ); + } + + // Q&A 특화 필터링 + const qnaSpecificWhere = and( + // 도메인 필터 + input.authorDomain.length > 0 + ? inArray(qnaView.authorDomain, input.authorDomain) + : undefined, + + // 벤더 타입 필터 + input.vendorType.length > 0 + ? inArray(qnaView.vendorType, input.vendorType) + : undefined, + + // 답변 유무 필터 (뷰에서 이미 계산된 값 사용) + input.hasAnswers === "answered" + ? eq(qnaView.hasAnswers, true) + : input.hasAnswers === "unanswered" + ? eq(qnaView.hasAnswers, false) + : undefined, + + // 내 질문만 보기 + input.myQuestions === "true" && currentUserId + ? eq(qnaView.author, currentUserId) + : undefined, + + // 날짜 범위 필터 + ); + + // 최종 WHERE 절 결합 + const finalWhere = and( + advancedWhere, + globalWhere, + qnaSpecificWhere + ); + + // 정렬 설정 + const orderBy = input.sort.length > 0 + ? input.sort.map((item) => + item.desc + ? desc(qnaView[item.id as keyof typeof qnaView]) + : asc(qnaView[item.id as keyof typeof qnaView]) + ) + : [desc(qnaView.lastActivityAt), desc(qnaView.createdAt)]; // 최근 활동순으로 기본 정렬 + + // 트랜잭션 내에서 데이터 조회 + const { data, total } = await db.transaction(async (tx) => { + const data = await selectQnaList(tx, { + where: finalWhere, + orderBy, + offset, + limit: input.perPage, + }); + + const total = await countQnaList(tx, finalWhere); + + // 내가 답변한 질문들 표시 (로그인한 경우만) + let dataWithMyAnswers = data; + if (currentUserId && data.length > 0) { + const qnaIds = data.map(q => q.id); + const myAnsweredIds = await getMyAnsweredQnaIds(tx, currentUserId, qnaIds); + + dataWithMyAnswers = data.map(q => ({ + ...q, + hasMyAnswer: myAnsweredIds.includes(q.id), + isMyQuestion: currentUserId === q.author, + })); + } else { + dataWithMyAnswers = data.map(q => ({ + ...q, + hasMyAnswer: false, + isMyQuestion: false, + })); + } + + return { data: dataWithMyAnswers, total }; + }); + + const pageCount = Math.ceil(total / input.perPage); + + return { + data, + pageCount, + total, + // 메타 정보 추가 + meta: { + currentPage: input.page, + perPage: input.perPage, + hasNextPage: input.page < pageCount, + hasPrevPage: input.page > 1, + } + }; + } catch (err) { + console.error("Q&A 목록 조회 실패:", err); + return { + data: [], + pageCount: 0, + total: 0, + meta: { + currentPage: 1, + perPage: input.perPage, + hasNextPage: false, + hasPrevPage: false, + } + }; + } + }, + [JSON.stringify(input)], // 캐싱 키 + { + revalidate: 1800, // 30분 캐싱 + tags: ["qna", "qna-list"], + } + )(); +} + +/* ================================================================ + 2) Q&A 상세 조회 +================================================================ */ + +export async function getQnaById(id: number) { + return unstable_cache( + async () => { + try { + const currentUserId = await getCurrentUserId(); + + return await db.transaction(async (tx) => { + // 질문 정보 조회 (뷰 사용으로 단순화) + const question = await selectQnaDetail(tx, id); + if (!question) return null; + + // 답변 목록 조회 (뷰 사용으로 단순화) + const answers = await selectAnswersByQnaId(tx, id); + + // 각 답변의 댓글들 조회 및 계층 구조 생성 + const answersWithComments = await Promise.all( + answers.map(async (answer) => { + const comments = await selectCommentsByAnswerId(tx, answer.id); + + // 댓글 계층 구조 생성 (뷰에서 이미 계층 정보 제공) + const commentMap = new Map(); + const rootComments: any[] = []; + + // 먼저 모든 댓글을 Map에 저장 + comments.forEach(comment => { + commentMap.set(comment.id, { + ...comment, + children: [], + isMyComment: currentUserId === comment.author, + }); + }); + + // 계층 구조 생성 + comments.forEach(comment => { + const commentWithChildren = commentMap.get(comment.id); + if (comment.parentCommentId) { + const parent = commentMap.get(comment.parentCommentId); + if (parent) { + parent.children.push(commentWithChildren); + } + } else { + rootComments.push(commentWithChildren); + } + }); + + return { + ...answer, + comments: rootComments, + isMyAnswer: currentUserId === answer.author, + }; + }) + ); + + return { + ...question, + answers: answersWithComments, + isMyQuestion: currentUserId === question.author, + // 뷰에서 이미 계산된 통계 정보 활용 + totalInteractions: question.totalAnswers + question.totalComments, + }; + }); + } catch (err) { + console.error("Q&A 상세 조회 실패:", err); + return null; + } + }, + [`qna-detail-${id}`], + { + revalidate: 1800, // 30분 캐싱 + tags: ["qna", `qna-${id}`], + } + )(); +} + +/* ================================================================ + 3) Q&A 생성/수정/삭제 (CRUD 액션들) +================================================================ */ + +/** + * 새로운 Q&A 질문 생성 + */ +export async function createQna(input: CreateQnaSchema) { + try { + const userId = await getAuthenticatedUser(); + + const [newQna] = await db.insert(qna).values({ + title: input.title, + content: input.content, + category: input.category, + author: userId, + }).returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + + return { + success: true, + data: newQna, + message: "질문이 성공적으로 등록되었습니다." + }; + } catch (err) { + console.error("질문 생성 실패:", err); + return { + success: false, + error: err instanceof Error ? err.message : "질문 등록에 실패했습니다.", + data: null + }; + } +} + +/** + * Q&A 질문 수정 + */ +export async function updateQna(id: number, input: UpdateQnaSchema) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 + const [existing] = await db + .select({ + author: qna.author, + title: qna.title + }) + .from(qna) + .where(eq(qna.id, id)); + + if (!existing) { + return { success: false, error: "질문을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "수정 권한이 없습니다." }; + } + + const [updated] = await db + .update(qna) + .set({ + ...input, + updatedAt: new Date() + }) + .where(eq(qna.id, id)) + .returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${id}`); + + return { + success: true, + data: updated, + message: "질문이 성공적으로 수정되었습니다." + }; + } catch (err) { + console.error("질문 수정 실패:", err); + return { + success: false, + error: err instanceof Error ? err.message : "질문 수정에 실패했습니다." + }; + } +} + +/** + * Q&A 질문 삭제 (소프트 삭제) + */ +export async function deleteQna(id: number) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 + const [existing] = await db + .select({ + author: qna.author, + title: qna.title + }) + .from(qna) + .where(eq(qna.id, id)); + + if (!existing) { + return { success: false, error: "질문을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "삭제 권한이 없습니다." }; + } + + await db + .update(qna) + .set({ + isDeleted: true, + deletedAt: new Date() + }) + .where(eq(qna.id, id)); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${id}`); + + return { + success: true, + message: "질문이 성공적으로 삭제되었습니다." + }; + } catch (err) { + console.error("질문 삭제 실패:", err); + return { + success: false, + error: err instanceof Error ? err.message : "질문 삭제에 실패했습니다." + }; + } +} + +/* ================================================================ + 4) 답변 관련 CRUD 액션들 +================================================================ */ + +/** + * 답변 생성 + */ +export async function createAnswer(input: CreateAnswerSchema) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 질문 존재 여부 확인 + const [questionExists] = await db + .select({ id: qna.id }) + .from(qna) + .where(and( + eq(qna.id, input.qnaId), + eq(qna.isDeleted, false) + )); + + if (!questionExists) { + return { success: false, error: "존재하지 않는 질문입니다." }; + } + + const [newAnswer] = await db.insert(qnaAnswer).values({ + qnaId: input.qnaId, + content: input.content, + author: userId, + }).returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${input.qnaId}`); + + return { + success: true, + data: newAnswer, + message: "답변이 성공적으로 등록되었습니다." + }; + } catch (error) { + console.error("답변 생성 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "답변 등록에 실패했습니다." + }; + } +} + +/** + * 답변 수정 + */ +export async function updateAnswer(id: number, input: UpdateAnswerSchema) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 + const [existing] = await db + .select({ + author: qnaAnswer.author, + qnaId: qnaAnswer.qnaId + }) + .from(qnaAnswer) + .where(eq(qnaAnswer.id, id)); + + if (!existing) { + return { success: false, error: "답변을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "수정 권한이 없습니다." }; + } + + const [updated] = await db + .update(qnaAnswer) + .set({ + content: input.content, + updatedAt: new Date() + }) + .where(eq(qnaAnswer.id, id)) + .returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${existing.qnaId}`); + + return { + success: true, + data: updated, + message: "답변이 성공적으로 수정되었습니다." + }; + } catch (err) { + console.error("답변 수정 실패:", err); + return { + success: false, + error: err instanceof Error ? err.message : "답변 수정에 실패했습니다." + }; + } +} + +/** + * 답변 삭제 + */ +export async function deleteAnswer(id: number) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 + const [existing] = await db + .select({ + author: qnaAnswer.author, + qnaId: qnaAnswer.qnaId + }) + .from(qnaAnswer) + .where(eq(qnaAnswer.id, id)); + + if (!existing) { + return { success: false, error: "답변을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "삭제 권한이 없습니다." }; + } + + // 하드 삭제 (답변은 CASCADE로 댓글도 함께 삭제) + await db.delete(qnaAnswer).where(eq(qnaAnswer.id, id)); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${existing.qnaId}`); + + return { + success: true, + message: "답변이 성공적으로 삭제되었습니다." + }; + } catch (err) { + console.error("답변 삭제 실패:", err); + return { + success: false, + error: err instanceof Error ? err.message : "답변 삭제에 실패했습니다." + }; + } +} + +/* ================================================================ + 5) 댓글 관련 CRUD 액션들 +================================================================ */ + +/** + * 댓글 생성 + */ +export async function createComment(input: CreateCommentSchema) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 답변 존재 여부 확인 + const [answerExists] = await db + .select({ + id: qnaAnswer.id, + qnaId: qnaAnswer.qnaId + }) + .from(qnaAnswer) + .where(and( + eq(qnaAnswer.id, input.answerId), + eq(qnaAnswer.isDeleted, false) + )); + + if (!answerExists) { + return { success: false, error: "존재하지 않는 답변입니다." }; + } + + // 부모 댓글 존재 여부 확인 (대댓글인 경우) + if (input.parentCommentId) { + const [parentExists] = await db + .select({ id: qnaComments.id }) + .from(qnaComments) + .where(and( + eq(qnaComments.id, input.parentCommentId), + eq(qnaComments.answerId, input.answerId), + eq(qnaComments.isDeleted, false) + )); + + if (!parentExists) { + return { success: false, error: "존재하지 않는 부모 댓글입니다." }; + } + } + + const [newComment] = await db.insert(qnaComments).values({ + ...input, + author: userId, + }).returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${answerExists.qnaId}`); + + return { + success: true, + data: newComment, + message: "댓글이 성공적으로 등록되었습니다." + }; + } catch (error) { + console.error("댓글 작성 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "댓글 등록에 실패했습니다." + }; + } +} + +/** + * 댓글 수정 + */ +export async function updateComment(id: number, input: UpdateCommentSchema) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 및 질문 ID 가져오기 + const [existing] = await db + .select({ + author: qnaComments.author, + answerId: qnaComments.answerId, + qnaId: qnaAnswer.qnaId + }) + .from(qnaComments) + .leftJoin(qnaAnswer, eq(qnaComments.answerId, qnaAnswer.id)) + .where(eq(qnaComments.id, id)); + + if (!existing) { + return { success: false, error: "댓글을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "수정 권한이 없습니다." }; + } + + const [updated] = await db + .update(qnaComments) + .set({ + content: input.content, + updatedAt: new Date() + }) + .where(eq(qnaComments.id, id)) + .returning(); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${existing.qnaId}`); + + return { + success: true, + data: updated, + message: "댓글이 성공적으로 수정되었습니다." + }; + } catch (error) { + console.error("댓글 수정 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "댓글 수정에 실패했습니다." + }; + } +} + +/** + * 댓글 삭제 + */ +export async function deleteComment(id: number) { + unstable_noStore(); + try { + const userId = await getAuthenticatedUser(); + + // 권한 확인 및 질문 ID 가져오기 + const [existing] = await db + .select({ + author: qnaComments.author, + answerId: qnaComments.answerId, + qnaId: qnaAnswer.qnaId + }) + .from(qnaComments) + .leftJoin(qnaAnswer, eq(qnaComments.answerId, qnaAnswer.id)) + .where(eq(qnaComments.id, id)); + + if (!existing) { + return { success: false, error: "댓글을 찾을 수 없습니다." }; + } + + if (existing.author !== userId) { + return { success: false, error: "삭제 권한이 없습니다." }; + } + + // 하드 삭제 (대댓글도 CASCADE로 함께 삭제) + await db.delete(qnaComments).where(eq(qnaComments.id, id)); + + revalidateTag("qna"); + revalidateTag("qna-list"); + revalidateTag(`qna-${existing.qnaId}`); + + return { + success: true, + message: "댓글이 성공적으로 삭제되었습니다." + }; + } catch (error) { + console.error("댓글 삭제 실패:", error); + return { + success: false, + error: error instanceof Error ? error.message : "댓글 삭제에 실패했습니다." + }; + } +} + +/* ================================================================ + 6) 답변별 댓글 목록 조회 +================================================================ */ + +export async function getCommentsByAnswerId(answerId: number) { + return unstable_cache( + async () => { + try { + const currentUserId = await getCurrentUserId(); + + return await db.transaction(async (tx) => { + const comments = await selectCommentsByAnswerId(tx, answerId); + + // 댓글 계층 구조 생성 + const commentMap = new Map(); + const rootComments: any[] = []; + + // 모든 댓글을 Map에 저장 + comments.forEach(comment => { + commentMap.set(comment.id, { + ...comment, + children: [], + isMyComment: currentUserId === comment.author, + }); + }); + + // 계층 구조 생성 + comments.forEach(comment => { + const commentWithChildren = commentMap.get(comment.id); + if (comment.parentCommentId) { + const parent = commentMap.get(comment.parentCommentId); + if (parent) { + parent.children.push(commentWithChildren); + } + } else { + rootComments.push(commentWithChildren); + } + }); + + return rootComments; + }); + } catch (error) { + console.error("댓글 조회 실패:", error); + return []; + } + }, + [`comments-${answerId}`], + { + revalidate: 1800, // 30분 캐싱 + tags: ["qna", `comments-${answerId}`], + } + )(); +} + +/* ================================================================ + 7) 통계 및 메타 정보 조회 +================================================================ */ + +/** + * Q&A 대시보드 통계 조회 + */ +export async function getQnaStats() { + return unstable_cache( + async () => { + try { + // 뷰를 사용하여 간단하게 통계 조회 + const [stats] = await db + .select({ + totalQuestions: count(), + answeredQuestions: sql<number>`COUNT(CASE WHEN ${qnaView.hasAnswers} = true THEN 1 END)`, + unansweredQuestions: sql<number>`COUNT(CASE WHEN ${qnaView.hasAnswers} = false THEN 1 END)`, + popularQuestions: sql<number>`COUNT(CASE WHEN ${qnaView.isPopular} = true THEN 1 END)`, + totalAnswers: sql<number>`SUM(${qnaView.totalAnswers})`, + totalComments: sql<number>`SUM(${qnaView.totalComments})`, + }) + .from(qnaView); + + // 최근 활동 통계 + const [recentStats] = await db + .select({ + questionsThisWeek: sql<number>`COUNT(CASE WHEN ${qnaView.createdAt} >= NOW() - INTERVAL '7 days' THEN 1 END)`, + questionsThisMonth: sql<number>`COUNT(CASE WHEN ${qnaView.createdAt} >= NOW() - INTERVAL '30 days' THEN 1 END)`, + activeQuestionsThisWeek: sql<number>`COUNT(CASE WHEN ${qnaView.lastActivityAt} >= NOW() - INTERVAL '7 days' THEN 1 END)`, + }) + .from(qnaView); + + return { + ...stats, + ...recentStats, + // 추가 계산 통계 + answerRate: stats.totalQuestions > 0 + ? Math.round((stats.answeredQuestions / stats.totalQuestions) * 100) + : 0, + avgAnswersPerQuestion: stats.totalQuestions > 0 + ? Math.round((stats.totalAnswers || 0) / stats.totalQuestions * 100) / 100 + : 0, + }; + } catch (err) { + console.error("Q&A 통계 조회 실패:", err); + return { + totalQuestions: 0, + answeredQuestions: 0, + unansweredQuestions: 0, + popularQuestions: 0, + totalAnswers: 0, + totalComments: 0, + questionsThisWeek: 0, + questionsThisMonth: 0, + activeQuestionsThisWeek: 0, + answerRate: 0, + avgAnswersPerQuestion: 0, + }; + } + }, + ["qna-stats"], + { + revalidate: 3600, // 1시간 캐싱 + tags: ["qna", "qna-stats"], + } + )(); +} + +/** + * 사용자별 Q&A 활동 통계 + */ +export async function getMyQnaActivity() { + try { + const userId = await getAuthenticatedUser(); + + return unstable_cache( + async () => { + const [myStats] = await db + .select({ + myQuestions: count(), + myAnsweredQuestions: sql<number>`COUNT(CASE WHEN ${qnaView.hasAnswers} = true THEN 1 END)`, + }) + .from(qnaView) + .where(eq(qnaView.author, userId)); + + const [myAnswers] = await db + .select({ + totalAnswers: count(), + }) + .from(qnaAnswerView) + .where(eq(qnaAnswerView.author, userId)); + + const [myComments] = await db + .select({ + totalComments: count(), + }) + .from(qnaCommentView) + .where(eq(qnaCommentView.author, userId)); + + return { + ...myStats, + ...myAnswers, + ...myComments, + }; + }, + [`my-qna-activity-${userId}`], + { + revalidate: 1800, // 30분 캐싱 + tags: ["qna", `user-activity-${userId}`], + } + )(); + } catch (err) { + return { + myQuestions: 0, + myAnsweredQuestions: 0, + totalAnswers: 0, + totalComments: 0, + }; + } +}
\ No newline at end of file |
