feat: 게시글 댓글 등록 작업

This commit is contained in:
2025-05-09 17:25:59 +09:00
parent e675e4cff6
commit 697ac34f65
10 changed files with 314 additions and 42 deletions
+1
View File
@@ -19,6 +19,7 @@
"@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-slot": "^1.1.2",
"@tanstack/react-query": "^5.67.1",
"@tanstack/react-query-devtools": "^5.75.5",
"@tanstack/react-table": "^8.21.2",
"@tinymce/tinymce-react": "^6.1.0",
"axios": "^1.8.1",
@@ -1,6 +1,7 @@
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ConfirmProvider } from "@frontend-opensource/use-react-hooks";
import { AuthWrapper } from "@/entities/auth";
import { MenuWrapper } from "@/entities/menu";
@@ -25,6 +26,9 @@ const AppProvider = ({ children }: Readonly<{ children: React.ReactNode }>) => {
</AuthWrapper>
</ConfirmProvider>
</StoreProvider>
<div className="text-2xl">
<ReactQueryDevtools initialIsOpen={false} />
</div>
</QueryClientProvider>
);
};
@@ -0,0 +1,31 @@
import { client } from "@/shared/api";
import { ENDPOINT } from "@/shared/config";
import {
commentFormSchema,
type Comment,
type CommentService,
type CommentList,
type CommentParams,
type CommentFormData,
} from "../model/comment-interface";
export class CommentApiService implements CommentService {
public async getComments(params: CommentParams): Promise<CommentList> {
const response = await client.get<CommentList>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
{ params },
);
return response.data;
}
public async createComment(data: CommentFormData): Promise<Comment> {
const validatedData = commentFormSchema.parse(data);
const response = await client.post<Comment>(
`${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
validatedData,
);
return response.data;
}
}
export const CommentApi = new CommentApiService();
@@ -0,0 +1,9 @@
export { CommentApi } from "./api/comment-service";
export {
commentFormSchema,
commentUpdateFormSchema,
type Comment,
type CommentList,
type CommentParams,
type CommentFormData,
} from "./model/comment-interface";
@@ -0,0 +1,54 @@
import { z } from "zod";
import type { PageableList, PageableParams } from "@/shared/model";
export interface CommentParams extends PageableParams {
keywordType?: string;
keyword?: string;
pstNo: number;
delYn?: string;
}
export interface Comment {
cmntEvl: number;
pstNo: number;
cmntNo: number;
cmntCn: string;
wrtrNm: string;
regDt: string;
rgtr: string;
mdfcnDt: string;
mdfr: string;
}
export const commentFormSchema = z.object({
pstNo: z.number(),
cmntCn: z.string(),
wrtrNm: z.string().nullish(),
cmntPswd: z.string(),
cmntEvl: z.number().nullish(),
});
export const commentUpdateFormSchema = commentFormSchema.omit({
pstNo: true,
});
export type CommentList = PageableList<Comment>;
export type CommentFormData = z.infer<typeof commentFormSchema>;
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>;
}
@@ -1,5 +1,5 @@
import { z } from "zod";
import { type PageableList, type PageableParams } from "@/shared/model";
import type { PageableList, PageableParams } from "@/shared/model";
export interface PostParams extends PageableParams {
keywordType?: string;
@@ -38,6 +38,7 @@ export const ENDPOINT = {
BOARD_SERVICE: {
BBS: "/board-service/api/v1/bbs",
POSTS_USER: "/board-service/api/v1/posts/user",
POSTS_COMMENTS: "/board-service/api/v1/posts-cmnts",
},
};
@@ -0,0 +1,147 @@
import dayjs from "dayjs";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Button, Input, Textarea } from "@common/components";
import {
CommentApi,
commentFormSchema,
type CommentFormData,
} from "@/entities/comment";
import { Form, FormControl, FormField, FormItem } from "@/shared/ui/form";
import { useAlertStore, useUserStore } from "@/shared/lib";
import { ENDPOINT } from "@/shared/config";
function PostComment({ pstNo }: { pstNo: number }) {
const queryClient = useQueryClient();
const { setMessage: alert } = useAlertStore((state) => state);
const { userInfo } = useUserStore((state) => state);
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 form = useForm<CommentFormData>({
resolver: zodResolver(commentFormSchema),
values: {
pstNo,
cmntCn: "",
wrtrNm: userInfo?.userName ?? "",
cmntPswd: userInfo?.userUnqId ?? "",
},
});
const onSubmit = (data: CommentFormData) => {
createComment(data);
};
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) => (
<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>
</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>
);
}
export { PostComment };
@@ -8,6 +8,7 @@ import { AttachmentApi } from "@/entities/attachment";
import { PostApi } from "@/entities/post";
import { ENDPOINT } from "@/shared/config";
import { PostAction } from "./post-action";
import { PostComment } from "./post-comment";
function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
const [filename, setFilename] = useState("");
@@ -64,48 +65,51 @@ function Post({ bbsId, pstNo }: { bbsId: string; pstNo: string }) {
};
return (
<PostAction bbsId={bbsId} pstNo={pstNo}>
<div className="card card-border !p-8">
<h3 className="text-[2rem] font-bold">{postData?.pstTtl}</h3>
<div className="card !m-0 !mt-8 bg-accent">
<ul className="flex gap-8 p-4 text-xl text-label">
<li>
작성자: <b>{postData?.wrtrNm}</b>
</li>
<li>
등록일: <b>{dayjs(postData?.regDt).format("YYYY-MM-DD")}</b>
</li>
<li>
조회수: <b>{postData?.inqNocs}</b>
</li>
</ul>
<>
<PostAction bbsId={bbsId} pstNo={pstNo}>
<div className="card card-border !p-8">
<h3 className="text-[2rem] font-bold">{postData?.pstTtl}</h3>
<div className="card !m-0 !mt-8 bg-accent">
<ul className="flex gap-8 p-4 text-xl text-label">
<li>
작성자: <b>{postData?.wrtrNm}</b>
</li>
<li>
등록일: <b>{dayjs(postData?.regDt).format("YYYY-MM-DD")}</b>
</li>
<li>
조회수: <b>{postData?.inqNocs}</b>
</li>
</ul>
</div>
<hr className="mt-8" />
<div
className="min-h-[40rem] p-8"
dangerouslySetInnerHTML={{ __html: postData?.pstCn ?? "" }}
/>
<hr className="mt-8" />
<div className="flex items-center gap-4 p-8">
<span className="font-bold">첨부파일</span>
{isFetching ? (
<Skeleton className="h-[2.5rem] w-[20rem]" />
) : (
filename && (
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
)
)}
</div>
</div>
<hr className="mt-8" />
<div
className="min-h-[40rem] p-8"
dangerouslySetInnerHTML={{ __html: postData?.pstCn ?? "" }}
/>
<hr className="mt-8" />
<div className="flex items-center gap-4 p-8">
<span className="font-bold">첨부파일</span>
{isFetching ? (
<Skeleton className="h-[2.5rem] w-[20rem]" />
) : (
filename && (
<Button
size="sm"
color="white"
className="flex w-fit items-center gap-2"
onClick={handleDownload}
>
<i className="diveicon di-download" />
<span>{filename}</span>
</Button>
)
)}
</div>
</div>
</PostAction>
</PostAction>
<PostComment pstNo={parseInt(pstNo)} />
</>
);
}
+21
View File
@@ -69,6 +69,9 @@ importers:
'@tanstack/react-query':
specifier: ^5.67.1
version: 5.67.1(react@18.3.1)
'@tanstack/react-query-devtools':
specifier: ^5.75.5
version: 5.75.5(@tanstack/react-query@5.67.1)(react@18.3.1)
'@tanstack/react-table':
specifier: ^8.21.2
version: 8.21.2(react-dom@18.3.1)(react@18.3.1)
@@ -160,6 +163,9 @@ importers:
'@common/components':
specifier: workspace:*
version: link:../../packages/common/components
'@common/config':
specifier: workspace:*
version: link:../../packages/common/config
'@frontend-opensource/use-react-hooks':
specifier: ^1.1.4
version: 1.1.4
@@ -3929,6 +3935,21 @@ packages:
resolution: {integrity: sha512-AkFmuukVejyqVIjEQoFhLb3q+xHl7JG8G9cANWTMe3s8iKzD9j1VBSYXgCjy6vm6xM8cUCR9zP2yqWxY9pTWOA==}
dev: false
/@tanstack/query-devtools@5.74.7:
resolution: {integrity: sha512-nSNlfuGdnHf4yB0S+BoNYOE1o3oAH093weAYZolIHfS2stulyA/gWfSk/9H4ZFk5mAAHb5vNqAeJOmbdcGPEQw==}
dev: false
/@tanstack/react-query-devtools@5.75.5(@tanstack/react-query@5.67.1)(react@18.3.1):
resolution: {integrity: sha512-S31U00nJOQIbxydRH1kOwdLRaLBrda8O5QjzmgkRg60UZzPGdbI6+873Qa0YGUfPeILDbR2ukgWyg7CJQPy4iA==}
peerDependencies:
'@tanstack/react-query': ^5.75.5
react: ^18.3.1
dependencies:
'@tanstack/query-devtools': 5.74.7
'@tanstack/react-query': 5.67.1(react@18.3.1)
react: 18.3.1
dev: false
/@tanstack/react-query@5.67.1(react@18.3.1):
resolution: {integrity: sha512-fH5u4JLwB6A+wLFdi8wWBWAYoJV5deYif2OveJ26ktAWjU499uvVFS1wPWnyEyq5LvZX1MZInvv9QRaIZANRaQ==}
peerDependencies: