# 게시물 댓글(Comment) 관리 프로그램 아키텍처 문서 ## 개요 이 문서는 게시물 댓글(Comment) 관리 프로그램의 아키텍처와 소스 흐름에 대한 상세 설명입니다. 댓글 관리 기능은 게시물에 대한 댓글을 등록, 조회, 수정, 삭제하는 기능을 제공하며, 로그인 및 비로그인 사용자 모두 이용할 수 있습니다. ## 기술 스택 - **프레임워크**: Next.js (App Router) - **상태 관리**: React Query, Zustand(useUserStore, useAlertStore) - **UI 컴포넌트**: @common/components - **폼 관리**: React Hook Form, Zod - **API 통신**: Axios - **알림 처리**: useConfirm, usePrompt 커스텀 훅 ## 디렉토리 구조 ``` frontend/ ├── apps/ │ └── cs/ │ ├── src/ │ │ ├── entities/ │ │ │ └── comment/ │ │ │ ├── api/ │ │ │ │ └── comment-service.ts # 댓글 API 호출 함수 │ │ │ └── model/ │ │ │ └── comment-interface.ts # 댓글 인터페이스 정의 │ │ └── widgets/ │ │ └── post/ │ │ └── ui/ │ │ └── post-comment.tsx # 댓글 UI 컴포넌트 ``` ## 데이터 모델 ### 댓글(Comment) 인터페이스 ```typescript export interface Comment { cmntEvl: number; // 댓글 평가 점수 pstNo: number; // 게시물 번호 cmntNo: number; // 댓글 번호 cmntCn: string; // 댓글 내용 wrtrNm: string; // 작성자명 regDt: string; // 등록일시 rgtr: string; // 등록자 mdfcnDt: string; // 수정일시 mdfr: string; // 수정자 delYn: string; // 삭제 여부 } ``` ### 댓글 검색 파라미터(CommentParams) 인터페이스 ```typescript export interface CommentParams extends PageableParams { keywordType?: string; // 검색 키워드 유형 keyword?: string; // 검색 키워드 pstNo: number; // 게시물 번호 delYn?: string; // 삭제 여부 } ``` ### 댓글 폼 데이터(CommentFormData) 인터페이스 Zod 스키마를 사용하여 폼 데이터의 유효성 검증을 수행합니다. ```typescript export const commentFormSchema = z.object({ pstNo: z.number(), // 게시물 번호 cmntCn: z.string(), // 댓글 내용 wrtrNm: z.string().nullish(), // 작성자명 (선택적) cmntPswd: z.string(), // 댓글 비밀번호 cmntEvl: z.number().nullish(), // 댓글 평가 점수 (선택적) }); export type CommentFormData = z.infer; ``` ### 댓글 수정 폼 데이터(CommentUpdateFormData) 인터페이스 ```typescript export const commentUpdateFormSchema = commentFormSchema.omit({ pstNo: true, // 수정 시에는 게시물 번호가 필요 없음 }); export type CommentUpdateFormData = z.infer; ``` ## API 기능 댓글 관리 API는 다음과 같은 기능을 제공합니다: ### 1. 댓글 목록 조회 ```typescript getComments: async (params: CommentParams): Promise => { const response = await client.get( `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`, { params }, ); return response.data; } ``` - **설명**: 특정 게시물의 댓글 목록을 페이지네이션과 함께 조회합니다. - **파라미터**: `CommentParams` (게시물 번호, 검색 조건) - **반환값**: 페이지네이션된 댓글 목록 (`CommentList`) ### 2. 댓글 등록 ```typescript createComment: async (data: CommentFormData): Promise => { const validatedData = commentFormSchema.parse(data); const response = await client.post( `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`, validatedData, ); return response.data; } ``` - **설명**: 새로운 댓글을 등록합니다. - **파라미터**: `CommentFormData` (댓글 등록 데이터) - **반환값**: 등록된 댓글 정보 (`Comment`) ### 3. 댓글 수정 ```typescript updateComment: async ( bbsId: number, pstNo: number, cmntNo: number, data: CommentUpdateFormData, ): Promise => { const validatedData = commentUpdateFormSchema.parse(data); const response = await client.put( `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`, validatedData, ); return response.data; } ``` - **설명**: 기존 댓글 정보를 수정합니다. - **파라미터**: `bbsId` (게시판 ID), `pstNo` (게시물 번호), `cmntNo` (댓글 번호), `CommentUpdateFormData` (수정할 댓글 데이터) - **반환값**: 수정된 댓글 정보 (`Comment`) ### 4. 댓글 삭제 ```typescript deleteComment: async ( bbsId: number, pstNo: number, cmntNo: number, cmntPswd: string, ): Promise => { const response = await client.delete( `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}`, { params: { cmntPswd } }, ); return response.data; } ``` - **설명**: 댓글을 삭제합니다. - **파라미터**: `bbsId` (게시판 ID), `pstNo` (게시물 번호), `cmntNo` (댓글 번호), `cmntPswd` (댓글 비밀번호) - **반환값**: 삭제된 댓글 정보 (`Comment`) ## 프로그램 흐름 ### 1. 댓글 목록 조회 흐름 1. 게시물 상세 페이지에서 `PostComment` 컴포넌트 로드 2. React Query를 사용하여 `getComments` API 호출 ```typescript const { data: comments } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS, pstNo], queryFn: () => CommentApi.getComments({ pstNo }), enabled: !!pstNo, }); ``` 3. 응답 데이터를 UI에 표시 ```tsx {comments?.content.map((comment) => { if (comment.delYn === "Y") return null; return (
{/* 댓글 내용 표시 */}
); })} ``` ### 2. 댓글 등록 흐름 1. 사용자가 댓글 내용 입력 및 등록 버튼 클릭 2. React Hook Form을 통해 폼 데이터 유효성 검증 ```typescript const form = useForm({ resolver: zodResolver(commentFormSchema), values: { pstNo, cmntCn: "", wrtrNm: userInfo?.userName ?? "", cmntPswd: userInfo?.userUnqId ?? "", }, }); ``` 3. 유효성 검증 통과 시 `onSubmit` 함수 호출 ```typescript const onSubmit = (data: CommentFormData) => { createComment(data); }; ``` 4. React Query의 `useMutation` 훅을 통해 `createComment` API 호출 ```typescript const { mutate: createComment } = useMutation({ mutationFn: (data: CommentFormData) => CommentApi.createComment(data), onSuccess: () => { alert("등록되었습니다."); form.setValue("cmntCn", ""); queryClient.invalidateQueries({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS], }); }, }); ``` 5. 성공 시 알림 표시 및 댓글 목록 새로고침 ### 3. 댓글 수정 흐름 1. 사용자가 댓글 수정 버튼 클릭 2. 수정 모드 활성화 및 기존 댓글 내용 로드 ```typescript const handleEdit = (cmntNo: number, cmntCn: string) => { setEditingCmntNo(cmntNo); setEditValue(cmntCn); }; ``` 3. 사용자가 댓글 내용 수정 후 저장 버튼 클릭 4. 확인 다이얼로그 표시 및 비밀번호 확인 ```typescript const handleUpdateComment = async (cmntNo: number, wrtrNm: string) => { if (!(await confirm("수정하시겠습니까?"))) return; const cmntPswd = await getCommentPassword(); updateComment({ cmntNo, data: { cmntCn: editValue, wrtrNm, cmntPswd, }, }); }; ``` 5. React Query의 `useMutation` 훅을 통해 `updateComment` API 호출 6. 성공 시 알림 표시 및 댓글 목록 새로고침 ### 4. 댓글 삭제 흐름 1. 사용자가 댓글 삭제 버튼 클릭 2. 확인 다이얼로그 표시 및 비밀번호 확인 ```typescript const handleDeleteComment = async (cmntNo: number) => { if (!(await confirm("삭제하시겠습니까?"))) return; const cmntPswd = await getCommentPassword(); deleteComment({ cmntNo, cmntPswd }); }; ``` 3. React Query의 `useMutation` 훅을 통해 `deleteComment` API 호출 4. 성공 시 알림 표시 및 댓글 목록 새로고침 ## 주요 컴포넌트 ### PostComment 컴포넌트 게시물 댓글을 표시하고 관리하는 컴포넌트입니다. ```tsx function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) { // 상태 및 훅 설정 const { prompt } = usePrompt(); const { confirm } = useConfirm(); const queryClient = useQueryClient(); const { userInfo } = useUserStore((state) => state); const { setMessage: alert } = useAlertStore((state) => state); const [editingCmntNo, setEditingCmntNo] = useState(null); const [editValue, setEditValue] = useState(""); // 댓글 목록 조회 const { data: comments } = useQuery({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS, pstNo], queryFn: () => CommentApi.getComments({ pstNo }), enabled: !!pstNo, }); // 댓글 등록 뮤테이션 const { mutate: createComment } = useMutation({ mutationFn: (data: CommentFormData) => CommentApi.createComment(data), onSuccess: () => { alert("등록되었습니다."); form.setValue("cmntCn", ""); queryClient.invalidateQueries({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS], }); }, }); // 댓글 수정 뮤테이션 const { mutate: updateComment } = useMutation({ mutationFn: ({ cmntNo, data, }: { cmntNo: number; data: CommentUpdateFormData; }) => CommentApi.updateComment(bbsId, pstNo, cmntNo, data), onSuccess: () => { alert("수정되었습니다."); setEditingCmntNo(null); setEditValue(""); queryClient.invalidateQueries({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS], }); }, }); // 댓글 삭제 뮤테이션 const { mutate: deleteComment } = useMutation({ mutationFn: ({ cmntNo, cmntPswd }: { cmntNo: number; cmntPswd: string }) => CommentApi.deleteComment(bbsId, pstNo, cmntNo, cmntPswd), onSuccess: () => { alert("삭제되었습니다."); queryClient.invalidateQueries({ queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS], }); }, }); // 폼 설정 const form = useForm({ resolver: zodResolver(commentFormSchema), values: { pstNo, cmntCn: "", wrtrNm: userInfo?.userName ?? "", cmntPswd: userInfo?.userUnqId ?? "", }, }); // 폼 제출 핸들러 const onSubmit = (data: CommentFormData) => { createComment(data); }; // 비밀번호 획득 함수 const getCommentPassword = async () => { let cmntPswd = ""; if (userInfo?.userUnqId) { cmntPswd = userInfo?.userUnqId; } else { const { resolved, value } = await prompt("비밀번호를 입력해주세요."); if (resolved) cmntPswd = value ?? ""; } return cmntPswd; }; // 댓글 수정 핸들러 const handleUpdateComment = async (cmntNo: number, wrtrNm: string) => { if (!(await confirm("수정하시겠습니까?"))) return; const cmntPswd = await getCommentPassword(); updateComment({ cmntNo, data: { cmntCn: editValue, wrtrNm, cmntPswd, }, }); }; // 댓글 삭제 핸들러 const handleDeleteComment = async (cmntNo: number) => { if (!(await confirm("삭제하시겠습니까?"))) return; const cmntPswd = await getCommentPassword(); deleteComment({ cmntNo, cmntPswd }); }; // UI 렌더링 return (
{/* 댓글 등록 폼 */}
{/* 댓글 내용 입력 필드 */} (