feat: 게시글 댓글 등록 작업
This commit is contained in:
@@ -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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Generated
+21
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user