651 lines
20 KiB
Markdown
651 lines
20 KiB
Markdown
# 게시물 댓글(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<typeof commentFormSchema>;
|
|
```
|
|
|
|
### 댓글 수정 폼 데이터(CommentUpdateFormData) 인터페이스
|
|
|
|
```typescript
|
|
export const commentUpdateFormSchema = commentFormSchema.omit({
|
|
pstNo: true, // 수정 시에는 게시물 번호가 필요 없음
|
|
});
|
|
|
|
export type CommentUpdateFormData = z.infer<typeof commentUpdateFormSchema>;
|
|
```
|
|
|
|
## API 기능
|
|
|
|
댓글 관리 API는 다음과 같은 기능을 제공합니다:
|
|
|
|
### 1. 댓글 목록 조회
|
|
|
|
```typescript
|
|
getComments: async (params: CommentParams): Promise<CommentList> => {
|
|
const response = await client.get<CommentList>(
|
|
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
|
|
{ params },
|
|
);
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
- **설명**: 특정 게시물의 댓글 목록을 페이지네이션과 함께 조회합니다.
|
|
- **파라미터**: `CommentParams` (게시물 번호, 검색 조건)
|
|
- **반환값**: 페이지네이션된 댓글 목록 (`CommentList`)
|
|
|
|
### 2. 댓글 등록
|
|
|
|
```typescript
|
|
createComment: async (data: CommentFormData): Promise<Comment> => {
|
|
const validatedData = commentFormSchema.parse(data);
|
|
const response = await client.post<Comment>(
|
|
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
|
|
validatedData,
|
|
);
|
|
return response.data;
|
|
}
|
|
```
|
|
|
|
- **설명**: 새로운 댓글을 등록합니다.
|
|
- **파라미터**: `CommentFormData` (댓글 등록 데이터)
|
|
- **반환값**: 등록된 댓글 정보 (`Comment`)
|
|
|
|
### 3. 댓글 수정
|
|
|
|
```typescript
|
|
updateComment: async (
|
|
bbsId: number,
|
|
pstNo: number,
|
|
cmntNo: number,
|
|
data: CommentUpdateFormData,
|
|
): Promise<Comment> => {
|
|
const validatedData = commentUpdateFormSchema.parse(data);
|
|
const response = await client.put<Comment>(
|
|
`${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<Comment> => {
|
|
const response = await client.delete<Comment>(
|
|
`${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 (
|
|
<div className="flex justify-between gap-2 border-t border-gray-200 py-8" key={comment.cmntNo}>
|
|
{/* 댓글 내용 표시 */}
|
|
</div>
|
|
);
|
|
})}
|
|
```
|
|
|
|
### 2. 댓글 등록 흐름
|
|
|
|
1. 사용자가 댓글 내용 입력 및 등록 버튼 클릭
|
|
2. React Hook Form을 통해 폼 데이터 유효성 검증
|
|
```typescript
|
|
const form = useForm<CommentFormData>({
|
|
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<number | null>(null);
|
|
const [editValue, setEditValue] = useState<string>("");
|
|
|
|
// 댓글 목록 조회
|
|
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<CommentFormData>({
|
|
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 (
|
|
<div className="card card-border !p-8">
|
|
{/* 댓글 등록 폼 */}
|
|
<Form {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
{/* 댓글 내용 입력 필드 */}
|
|
<FormField
|
|
control={form.control}
|
|
name="cmntCn"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Textarea
|
|
maxLength={1000}
|
|
className="h-[10rem] bg-background p-4 !text-[1.3rem] !leading-6"
|
|
placeholder="댓글을 입력해주세요."
|
|
{...field}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<div className="mt-4 flex items-center justify-between">
|
|
<div className="flex gap-4">
|
|
{/* 비로그인 사용자를 위한 작성자명 입력 필드 */}
|
|
{!userInfo?.userName && (
|
|
<FormField
|
|
control={form.control}
|
|
name="wrtrNm"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[16rem] bg-background !text-[1.3rem]"
|
|
placeholder="작성자"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
{/* 비로그인 사용자를 위한 비밀번호 입력 필드 */}
|
|
{!userInfo?.userUnqId && (
|
|
<FormField
|
|
control={form.control}
|
|
name="cmntPswd"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
type="password"
|
|
value={field.value ?? ""}
|
|
className="w-[12rem] bg-background !text-[1.3rem]"
|
|
placeholder="비밀번호"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
</div>
|
|
<Button type="submit">등록</Button>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
|
|
{/* 댓글 목록 */}
|
|
<div className="mt-8 flex flex-col">
|
|
{comments?.content.map((comment) => {
|
|
if (comment.delYn === "Y") return null;
|
|
return (
|
|
<div
|
|
className="flex justify-between gap-2 border-t border-gray-200 py-8"
|
|
key={comment.cmntNo}
|
|
>
|
|
{/* 댓글 정보 표시 */}
|
|
<div className="flex w-full gap-10">
|
|
<div className="flex w-[10rem] shrink-0 flex-col">
|
|
<span className="text-[1.4rem] font-bold text-label">
|
|
{comment.wrtrNm}
|
|
</span>
|
|
<span className="text-[1.2rem] text-label">
|
|
{dayjs(comment.regDt).format("YYYY.MM.DD")}
|
|
</span>
|
|
</div>
|
|
|
|
{/* 수정 모드일 때 */}
|
|
{editingCmntNo === comment.cmntNo ? (
|
|
<div className="flex flex-1 flex-col gap-2">
|
|
<Textarea
|
|
value={editValue}
|
|
onChange={(e) => setEditValue(e.target.value)}
|
|
className="h-[10rem] bg-background p-4 !text-[1.3rem] !leading-6"
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<Button
|
|
onClick={() => {
|
|
setEditingCmntNo(null);
|
|
setEditValue("");
|
|
}}
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={() =>
|
|
handleUpdateComment(comment.cmntNo, comment.wrtrNm)
|
|
}
|
|
>
|
|
저장
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
/* 일반 모드일 때 */
|
|
<div className="flex flex-1 flex-col">
|
|
<p className="whitespace-pre-wrap text-[1.4rem] text-label">
|
|
{comment.cmntCn}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 수정/삭제 버튼 */}
|
|
{editingCmntNo !== comment.cmntNo && (
|
|
<div className="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => handleEdit(comment.cmntNo, comment.cmntCn)}
|
|
>
|
|
수정
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
color="red"
|
|
onClick={() => handleDeleteComment(comment.cmntNo)}
|
|
>
|
|
삭제
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
## 로그인/비로그인 사용자 처리
|
|
|
|
### 로그인 사용자 처리
|
|
|
|
로그인 사용자의 경우 `useUserStore`에서 사용자 정보를 가져와 자동으로 설정합니다.
|
|
|
|
```typescript
|
|
const { userInfo } = useUserStore((state) => state);
|
|
|
|
const form = useForm<CommentFormData>({
|
|
resolver: zodResolver(commentFormSchema),
|
|
values: {
|
|
pstNo,
|
|
cmntCn: "",
|
|
wrtrNm: userInfo?.userName ?? "",
|
|
cmntPswd: userInfo?.userUnqId ?? "",
|
|
},
|
|
});
|
|
```
|
|
|
|
### 비로그인 사용자 처리
|
|
|
|
비로그인 사용자의 경우 작성자명과 비밀번호를 직접 입력받습니다.
|
|
|
|
```tsx
|
|
{!userInfo?.userName && (
|
|
<FormField
|
|
control={form.control}
|
|
name="wrtrNm"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
value={field.value ?? ""}
|
|
className="w-[16rem] bg-background !text-[1.3rem]"
|
|
placeholder="작성자"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{!userInfo?.userUnqId && (
|
|
<FormField
|
|
control={form.control}
|
|
name="cmntPswd"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
type="password"
|
|
value={field.value ?? ""}
|
|
className="w-[12rem] bg-background !text-[1.3rem]"
|
|
placeholder="비밀번호"
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
)}
|
|
```
|
|
|
|
## 비밀번호 검증 로직
|
|
|
|
댓글 수정/삭제 시 비밀번호 검증을 위한 로직이 구현되어 있습니다.
|
|
|
|
```typescript
|
|
const getCommentPassword = async () => {
|
|
let cmntPswd = "";
|
|
if (userInfo?.userUnqId) {
|
|
// 로그인 사용자의 경우 사용자 ID를 비밀번호로 사용
|
|
cmntPswd = userInfo?.userUnqId;
|
|
} else {
|
|
// 비로그인 사용자의 경우 프롬프트를 통해 비밀번호 입력 요청
|
|
const { resolved, value } = await prompt("비밀번호를 입력해주세요.");
|
|
if (resolved) cmntPswd = value ?? "";
|
|
}
|
|
return cmntPswd;
|
|
};
|
|
```
|
|
|
|
## 아키텍처 특징
|
|
|
|
1. **계층 분리**: API, 모델, UI 컴포넌트가 명확히 분리되어 있습니다.
|
|
2. **타입 안전성**: TypeScript 인터페이스와 Zod 스키마를 통한 타입 검증을 제공합니다.
|
|
3. **상태 관리**: React Query를 통한 서버 상태 관리와 Zustand를 통한 클라이언트 상태 관리를 제공합니다.
|
|
4. **유효성 검증**: Zod 스키마를 사용하여 폼 데이터의 유효성을 검증합니다.
|
|
5. **사용자 경험**: 로그인/비로그인 사용자에 대한 차별화된 UI 제공합니다.
|
|
6. **비동기 처리**: React Query의 `useMutation`을 통한 효율적인 비동기 작업 처리를 제공합니다.
|
|
|
|
## 확장 가능성
|
|
|
|
1. **댓글 계층 구조**: 대댓글 기능 추가
|
|
2. **댓글 좋아요/싫어요**: 댓글 평가 기능 강화
|
|
3. **댓글 신고**: 부적절한 댓글 신고 기능 추가
|
|
4. **댓글 알림**: 댓글 등록 시 알림 기능 추가
|
|
5. **댓글 정렬**: 최신순, 인기순 등 다양한 정렬 옵션 제공
|
|
6. **댓글 검색**: 댓글 내용 검색 기능 추가
|
|
|
|
## 주의사항
|
|
|
|
1. 비로그인 사용자의 댓글 비밀번호는 클라이언트 측에서만 검증되므로 보안에 주의가 필요합니다.
|
|
2. 댓글 삭제 시 실제로 데이터베이스에서 삭제하지 않고 `delYn` 필드를 'Y'로 설정하는 소프트 삭제 방식을 사용합니다.
|
|
3. 댓글 내용에 HTML 태그나 스크립트가 포함될 경우 XSS 공격 가능성이 있으므로 서버 측에서 적절한 필터링이 필요합니다.
|