feat: 댓글 수정, 삭제 기능

This commit is contained in:
2025-05-12 14:25:38 +09:00
parent 1a8053910d
commit 8b265a7aae
5 changed files with 182 additions and 42 deletions
@@ -2,11 +2,13 @@ import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentService,
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "../model/comment-interface";
export class CommentApiService implements CommentService {
@@ -26,6 +28,32 @@ export class CommentApiService implements CommentService {
);
return response.data;
}
public async updateComment(
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;
}
public async deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment> {
const response = await client.delete<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}/${bbsId}/${pstNo}/${cmntNo}?cmntPswd=${cmntPswd}`,
);
return response.data;
}
}
export const CommentApi = new CommentApiService();
@@ -6,4 +6,5 @@ export {
type CommentList,
type CommentParams,
type CommentFormData,
type CommentUpdateFormData,
} from "./model/comment-interface";
@@ -18,6 +18,7 @@ export interface Comment {
rgtr: string;
mdfcnDt: string;
mdfr: string;
delYn: string;
}
export const commentFormSchema = z.object({
@@ -39,16 +40,16 @@ export type CommentUpdateFormData = z.infer<typeof commentUpdateFormSchema>;
export interface CommentService {
getComments(params: CommentParams): Promise<CommentList>;
createComment(data: CommentFormData): Promise<Comment>;
// updateComment(
// bbsId: number,
// pstNo: number,
// cmntNo: number,
// data: CommentUpdateFormData,
// ): Promise<Comment>;
// deleteComment(
// bbsId: number,
// pstNo: number,
// cmntNo: number,
// cmntPswd: string,
// ): Promise<void>;
updateComment(
bbsId: number,
pstNo: number,
cmntNo: number,
data: CommentUpdateFormData,
): Promise<Comment>;
deleteComment(
bbsId: number,
pstNo: number,
cmntNo: number,
cmntPswd: string,
): Promise<Comment>;
}
@@ -1,21 +1,28 @@
import dayjs from "dayjs";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useConfirm } from "@frontend-opensource/use-react-hooks";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Button, Input, Textarea } from "@common/components";
import {
CommentApi,
commentFormSchema,
type CommentFormData,
type CommentUpdateFormData,
} from "@/entities/comment";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
import { useAlertStore, usePrompt, useUserStore } from "@/shared/lib";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
function PostComment({ pstNo }: { pstNo: number }) {
function PostComment({ bbsId, pstNo }: { bbsId: number; pstNo: number }) {
const { prompt } = usePrompt();
const { confirm } = useConfirm();
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
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],
@@ -26,7 +33,7 @@ function PostComment({ pstNo }: { pstNo: number }) {
const { mutate: createComment } = useMutation({
mutationFn: (data: CommentFormData) => CommentApi.createComment(data),
onSuccess: () => {
alert("댓글이 등록되었습니다.");
alert("등록되었습니다.");
form.setValue("cmntCn", "");
queryClient.invalidateQueries({
queryKey: [ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS],
@@ -34,6 +41,35 @@ function PostComment({ pstNo }: { pstNo: number }) {
},
});
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: {
@@ -48,6 +84,35 @@ function PostComment({ pstNo }: { pstNo: number }) {
createComment(data);
};
const getCommentPassword = async () => {
let cmntPswd = "";
if (userInfo?.userUnqId) {
cmntPswd = userInfo?.userUnqId;
} else {
cmntPswd = await prompt("비밀번호를 입력해주세요.");
}
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 });
};
return (
<div className="card card-border !p-8">
<Form {...form}>
@@ -113,32 +178,77 @@ function PostComment({ pstNo }: { pstNo: number }) {
</form>
</Form>
<div className="mt-8 flex flex-col">
{comments?.content.map((comment) => (
<div
className="flex justify-between gap-2 border-t border-gray-200 py-8"
key={comment.cmntNo}
>
<div className="flex gap-10">
<div className="flex w-[10rem] shrink-0 flex-col">
<span className="text-[1.4rem] font-bold">
{comment.wrtrNm}
</span>
<span className="text-[1.2rem] text-gray-500">
{dayjs(comment.regDt).format("YYYY.MM.DD")}
</span>
{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">
{comment.wrtrNm}
</span>
<span className="text-[1.2rem] text-gray-500">
{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)}
maxLength={1000}
className="h-[10rem] bg-background p-4 !text-[1.3rem] !leading-6"
/>
<div className="flex gap-2">
<Button
size="sm"
onClick={() =>
handleUpdateComment(comment.cmntNo, comment.wrtrNm)
}
>
저장
</Button>
<Button
size="sm"
color="gray"
onClick={() => {
setEditingCmntNo(null);
setEditValue("");
}}
>
취소
</Button>
</div>
</div>
) : (
<p className="break-all text-[1.4rem]">{comment.cmntCn}</p>
)}
</div>
<div className="flex gap-2 whitespace-nowrap">
<Button
size="icon"
color="white"
onClick={() => {
setEditingCmntNo(comment.cmntNo);
setEditValue(comment.cmntCn);
}}
>
<i className="diveicon di-pencil" />
</Button>
<Button
size="icon"
color="white"
onClick={() => handleDeleteComment(comment.cmntNo)}
>
<i className="diveicon di-trash" />
</Button>
</div>
<p className="break-all text-[1.4rem]">{comment.cmntCn}</p>
</div>
<div className="flex gap-2 whitespace-nowrap">
<Button size="icon" color="white">
<i className="diveicon di-pencil" />
</Button>
<Button size="icon" color="white">
<i className="diveicon di-trash" />
</Button>
</div>
</div>
))}
);
})}
</div>
</div>
);
@@ -131,7 +131,7 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
</div>
</div>
</PostAction>
<PostComment pstNo={parseInt(pstNo)} />
<PostComment bbsId={parseInt(bbsId)} pstNo={parseInt(pstNo)} />
</>
);
}