diff --git a/apps/common/features/package.json b/apps/common/features/package.json
index 8423560be..b9acdc511 100644
--- a/apps/common/features/package.json
+++ b/apps/common/features/package.json
@@ -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",
diff --git a/apps/common/features/src/app/providers/app-provider.tsx b/apps/common/features/src/app/providers/app-provider.tsx
index afe4ee651..6d7cb4480 100644
--- a/apps/common/features/src/app/providers/app-provider.tsx
+++ b/apps/common/features/src/app/providers/app-provider.tsx
@@ -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 }>) => {
+
+
+
);
};
diff --git a/apps/common/features/src/entities/comment/api/comment-service.ts b/apps/common/features/src/entities/comment/api/comment-service.ts
new file mode 100644
index 000000000..7fb0b5655
--- /dev/null
+++ b/apps/common/features/src/entities/comment/api/comment-service.ts
@@ -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 {
+ const response = await client.get(
+ `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
+ { params },
+ );
+ return response.data;
+ }
+
+ public async createComment(data: CommentFormData): Promise {
+ const validatedData = commentFormSchema.parse(data);
+ const response = await client.post(
+ `${ENDPOINT.BOARD_SERVICE.POSTS_COMMENTS}`,
+ validatedData,
+ );
+ return response.data;
+ }
+}
+
+export const CommentApi = new CommentApiService();
diff --git a/apps/common/features/src/entities/comment/index.ts b/apps/common/features/src/entities/comment/index.ts
new file mode 100644
index 000000000..376ec62e4
--- /dev/null
+++ b/apps/common/features/src/entities/comment/index.ts
@@ -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";
diff --git a/apps/common/features/src/entities/comment/model/comment-interface.ts b/apps/common/features/src/entities/comment/model/comment-interface.ts
new file mode 100644
index 000000000..ea22af65b
--- /dev/null
+++ b/apps/common/features/src/entities/comment/model/comment-interface.ts
@@ -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;
+export type CommentFormData = z.infer;
+export type CommentUpdateFormData = z.infer;
+
+export interface CommentService {
+ getComments(params: CommentParams): Promise;
+ createComment(data: CommentFormData): Promise;
+ // updateComment(
+ // bbsId: number,
+ // pstNo: number,
+ // cmntNo: number,
+ // data: CommentUpdateFormData,
+ // ): Promise;
+ // deleteComment(
+ // bbsId: number,
+ // pstNo: number,
+ // cmntNo: number,
+ // cmntPswd: string,
+ // ): Promise;
+}
diff --git a/apps/common/features/src/entities/post/model/post-interface.ts b/apps/common/features/src/entities/post/model/post-interface.ts
index 16a2f4de2..fb77b2c43 100644
--- a/apps/common/features/src/entities/post/model/post-interface.ts
+++ b/apps/common/features/src/entities/post/model/post-interface.ts
@@ -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;
diff --git a/apps/common/features/src/shared/config/api.ts b/apps/common/features/src/shared/config/api.ts
index 1ab27e341..69bd31b2c 100644
--- a/apps/common/features/src/shared/config/api.ts
+++ b/apps/common/features/src/shared/config/api.ts
@@ -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",
},
};
diff --git a/apps/common/features/src/widgets/post/ui/post-comment.tsx b/apps/common/features/src/widgets/post/ui/post-comment.tsx
new file mode 100644
index 000000000..b7a55a6ca
--- /dev/null
+++ b/apps/common/features/src/widgets/post/ui/post-comment.tsx
@@ -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({
+ resolver: zodResolver(commentFormSchema),
+ values: {
+ pstNo,
+ cmntCn: "",
+ wrtrNm: userInfo?.userName ?? "",
+ cmntPswd: userInfo?.userUnqId ?? "",
+ },
+ });
+
+ const onSubmit = (data: CommentFormData) => {
+ createComment(data);
+ };
+
+ return (
+
+
+
+
+ {comments?.content.map((comment) => (
+
+
+
+
+ {comment.wrtrNm}
+
+
+ {dayjs(comment.regDt).format("YYYY.MM.DD")}
+
+
+
{comment.cmntCn}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}
+
+export { PostComment };
diff --git a/apps/common/features/src/widgets/post/ui/post.tsx b/apps/common/features/src/widgets/post/ui/post.tsx
index 920eab753..6ec857b7b 100644
--- a/apps/common/features/src/widgets/post/ui/post.tsx
+++ b/apps/common/features/src/widgets/post/ui/post.tsx
@@ -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 (
-
-
-
{postData?.pstTtl}
-
-
- -
- 작성자: {postData?.wrtrNm}
-
- -
- 등록일: {dayjs(postData?.regDt).format("YYYY-MM-DD")}
-
- -
- 조회수: {postData?.inqNocs}
-
-
+ <>
+
+
+
{postData?.pstTtl}
+
+
+ -
+ 작성자: {postData?.wrtrNm}
+
+ -
+ 등록일: {dayjs(postData?.regDt).format("YYYY-MM-DD")}
+
+ -
+ 조회수: {postData?.inqNocs}
+
+
+
+
+
+
+
+ 첨부파일
+ {isFetching ? (
+
+ ) : (
+ filename && (
+
+ )
+ )}
+
-
-
-
-
- 첨부파일
- {isFetching ? (
-
- ) : (
- filename && (
-
- )
- )}
-
-
-
+
+
+ >
);
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a56c9aeb2..4988f297b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -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: