From c093072ef6637998d7ae6f803a35c2d4aae43c74 Mon Sep 17 00:00:00 2001 From: Lee MinGi Date: Sun, 27 Apr 2025 19:47:31 +0900 Subject: [PATCH 01/21] =?UTF-8?q?=F0=9F=9A=80=20Add=20deploy=20yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 55 ++++++++++++++++++++++++++++++++++++ vite.config.ts | 3 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/deploy.yml diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..2558d65ff --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,55 @@ +name: Deploy to GitHub Pages +on: + push: + branches: + - main + +permissions: + contents: read + pages: write + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: latest + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "20" + cache: "pnpm" + + - name: Install dependencies + run: pnpm install + + - name: Build with Vite + run: pnpm run build + + - name: Create 404 page + run: cp dist/index.html dist/404.html + + - name: Disable Jekyll + run: touch dist/.nojekyll + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: "./dist" + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/vite.config.ts b/vite.config.ts index be7b7a3d4..1eb7a6c3c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,8 +1,9 @@ -import { defineConfig } from "vite" import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" // https://vite.dev/config/ export default defineConfig({ + base: process.env.NODE_ENV === "production" ? "/front_5th_chapter2-2/" : "", plugins: [react()], server: { proxy: { From e59907bcedd87a280515a7f1722da8f7c8312778 Mon Sep 17 00:00:00 2001 From: Lee MinGi Date: Thu, 1 May 2025 23:59:35 +0900 Subject: [PATCH 02/21] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactoring=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- pnpm-lock.yaml | 46 +- src/App.tsx | 20 - src/app/App.tsx | 25 + src/app/assets/react.svg | 1 - src/app/provider/tanstack-query/index.ts | 1 + .../tanstack-query-provider.tsx | 7 + src/{ => app/styles}/index.css | 0 .../comment/api/comment-query-key.api.ts | 3 + src/entities/comment/api/comment.api.ts | 52 ++ src/entities/comment/api/index.ts | 2 + src/entities/comment/dto/comment.dto.ts | 6 + src/entities/comment/stores/comment.store.ts | 25 + .../comment/stores/comments.stores.ts | 22 + src/entities/comment/stores/index.ts | 1 + src/entities/comment/types/comment.types.ts | 9 + src/entities/comment/types/index.ts | 1 + .../comment/ui/comment-view/CommentView.tsx | 18 + src/entities/comment/ui/index.ts | 1 + src/entities/post/api/index.ts | 2 + src/entities/post/api/post-query-key.api.ts | 7 + src/entities/post/api/post.api.ts | 70 ++ src/entities/post/dto/post.dto.ts | 6 + src/entities/post/stores/index.ts | 3 + src/entities/post/stores/post-total.stores.ts | 11 + src/entities/post/stores/post.stores.ts | 12 + src/entities/post/stores/posts.stores.ts | 21 + src/entities/post/types/index.ts | 1 + src/entities/post/types/post.types.ts | 26 + src/entities/post/ui/index.ts | 1 + src/entities/post/ui/tag-view/TagView.tsx | 18 + src/entities/user/api/index.ts | 1 + src/entities/user/api/user-query-key.api.ts | 3 + src/entities/user/api/user.api.ts | 27 + src/entities/user/dto/user.dto.ts | 11 + src/entities/user/stores/index.ts | 1 + src/entities/user/stores/user.stores.ts | 9 + src/entities/user/types/index.ts | 1 + src/entities/user/types/user.types.ts | 19 + src/entities/user/ui/index.ts | 1 + src/entities/user/ui/user-view/UserView.tsx | 36 + src/features/comment/hooks/index.ts | 5 + src/features/comment/hooks/useAddComment.ts | 31 + .../comment/hooks/useDeleteComment.ts | 26 + .../comment/hooks/useGetCommentsByPostId.ts | 14 + src/features/comment/hooks/useLikeComment.ts | 29 + .../comment/hooks/useUpdateComment.ts | 33 + .../comment/services/comment.service.ts | 53 ++ src/features/comment/services/index.ts | 1 + src/features/post/hooks/index.ts | 5 + src/features/post/hooks/useAddPost.ts | 54 ++ src/features/post/hooks/useDeletePost.ts | 39 + src/features/post/hooks/useGetPosts.ts | 75 ++ src/features/post/hooks/useUpdatePost.ts | 54 ++ src/features/post/services/index.ts | 1 + src/features/post/services/post.service.ts | 103 +++ src/features/post/types/index.ts | 1 + src/features/post/types/post.types.ts | 6 + src/features/user/services/index.ts | 1 + src/features/user/services/user.service.ts | 14 + src/index.tsx | 2 +- src/main.tsx | 9 +- src/pages/PostsManagerPage.tsx | 708 ------------------ src/pages/posts/manager/PostsManagerPage.tsx | 45 ++ src/pages/posts/manager/index.ts | 1 + src/shared/api/api.ts | 100 +++ src/shared/api/index.ts | 2 + src/shared/api/query-client.ts | 13 + src/shared/hooks/useDebounce.ts | 17 + src/shared/stores/modal/index.ts | 1 + src/shared/stores/modal/modal.stores.ts | 17 + src/shared/stores/query-params/index.ts | 1 + .../stores/query-params/query-params.store.ts | 78 ++ src/shared/types/api.types.ts | 7 + src/shared/types/index.ts | 2 + src/shared/types/pagination.types.ts | 5 + src/shared/ui/button/Button.tsx | 37 + src/shared/ui/card/Card.tsx | 6 + src/shared/ui/card/CardContent.tsx | 6 + src/shared/ui/card/CardHeader.tsx | 8 + src/shared/ui/card/CardTitle.tsx | 8 + src/shared/ui/dialog/Dialog.tsx | 2 + src/shared/ui/dialog/DialogContent.tsx | 27 + src/shared/ui/dialog/DialogHeader.tsx | 8 + src/shared/ui/dialog/DialogPortal.tsx | 2 + src/shared/ui/dialog/DialogTitle.tsx | 14 + .../ui/highlight-text/HighlightText.tsx | 29 + src/shared/ui/index.ts | 29 + src/shared/ui/index.tsx | 214 ------ src/shared/ui/input/Input.tsx | 15 + src/shared/ui/loading/Loading.tsx | 3 + src/shared/ui/select/Select.tsx | 2 + src/shared/ui/select/SelectContent.tsx | 20 + src/shared/ui/select/SelectGroup.tsx | 2 + src/shared/ui/select/SelectItem.tsx | 21 + src/shared/ui/select/SelectTrigger.tsx | 18 + src/shared/ui/select/SelectValue.tsx | 2 + src/shared/ui/table/Table.tsx | 10 + src/shared/ui/table/TableBody.tsx | 8 + src/shared/ui/table/TableCell.tsx | 8 + src/shared/ui/table/TableHead.tsx | 12 + src/shared/ui/table/TableHeader.tsx | 6 + src/shared/ui/table/TableRow.tsx | 12 + src/shared/ui/textarea/Textarea.tsx | 14 + .../hooks/useAddCommentModal.ts | 3 + .../comment/add-comment-modal/index.ts | 2 + .../add-comment-modal/ui/AddCommentModal.tsx | 39 + .../comment/comment-detail-container/index.ts | 1 + .../ui/CommentDetailContainer.tsx | 67 ++ .../ui/CommentDetailItem.tsx | 40 + .../hooks/useEditCommentModal.ts | 3 + .../comment/edit-comment-modal/index.ts | 2 + .../ui/EditCommentModal.tsx | 28 + src/widgets/common/footer/index.ts | 1 + src/widgets/common/footer/ui/Footer.tsx | 11 + .../header/constants/header.constants.ts | 14 + src/widgets/common/header/index.ts | 1 + src/widgets/common/header/ui/Header.tsx | 27 + src/widgets/common/pagination/index.ts | 1 + .../common/pagination/ui/Pagination.tsx | 45 ++ .../add-post-modal/hooks/useAddPostModal.ts | 3 + src/widgets/post/add-post-modal/index.ts | 2 + .../post/add-post-modal/ui/AddPostModal.tsx | 57 ++ .../edit-post-modal/hooks/useEditPostModal.ts | 3 + src/widgets/post/edit-post-modal/index.ts | 2 + .../post/edit-post-modal/ui/EditPostModal.tsx | 51 ++ src/widgets/post/filter-controller/index.ts | 1 + .../filter-controller/ui/FilterController.tsx | 84 +++ .../hooks/usePostDetailModal.ts | 3 + src/widgets/post/post-detail-modal/index.ts | 2 + .../post-detail-modal/ui/PostDetailModal.tsx | 30 + src/widgets/post/post-table/index.ts | 1 + .../post/post-table/ui/PostListItem.tsx | 96 +++ .../post/post-table/ui/PostListTable.tsx | 35 + src/widgets/ui/Footer.tsx | 13 - src/widgets/ui/Header.tsx | 25 - .../hooks/useUserProfileModal.ts | 3 + src/widgets/user/profile-modal/index.ts | 2 + .../user/profile-modal/ui/UserModal.tsx | 33 + tests/basic.test.tsx | 37 +- tsconfig.app.json | 6 + vite.config.ts | 6 + vitest.config.ts | 8 +- 143 files changed, 2417 insertions(+), 998 deletions(-) delete mode 100644 src/App.tsx create mode 100644 src/app/App.tsx delete mode 100644 src/app/assets/react.svg create mode 100644 src/app/provider/tanstack-query/index.ts create mode 100644 src/app/provider/tanstack-query/tanstack-query-provider.tsx rename src/{ => app/styles}/index.css (100%) create mode 100644 src/entities/comment/api/comment-query-key.api.ts create mode 100644 src/entities/comment/api/comment.api.ts create mode 100644 src/entities/comment/api/index.ts create mode 100644 src/entities/comment/dto/comment.dto.ts create mode 100644 src/entities/comment/stores/comment.store.ts create mode 100644 src/entities/comment/stores/comments.stores.ts create mode 100644 src/entities/comment/stores/index.ts create mode 100644 src/entities/comment/types/comment.types.ts create mode 100644 src/entities/comment/types/index.ts create mode 100644 src/entities/comment/ui/comment-view/CommentView.tsx create mode 100644 src/entities/comment/ui/index.ts create mode 100644 src/entities/post/api/index.ts create mode 100644 src/entities/post/api/post-query-key.api.ts create mode 100644 src/entities/post/api/post.api.ts create mode 100644 src/entities/post/dto/post.dto.ts create mode 100644 src/entities/post/stores/index.ts create mode 100644 src/entities/post/stores/post-total.stores.ts create mode 100644 src/entities/post/stores/post.stores.ts create mode 100644 src/entities/post/stores/posts.stores.ts create mode 100644 src/entities/post/types/index.ts create mode 100644 src/entities/post/types/post.types.ts create mode 100644 src/entities/post/ui/index.ts create mode 100644 src/entities/post/ui/tag-view/TagView.tsx create mode 100644 src/entities/user/api/index.ts create mode 100644 src/entities/user/api/user-query-key.api.ts create mode 100644 src/entities/user/api/user.api.ts create mode 100644 src/entities/user/dto/user.dto.ts create mode 100644 src/entities/user/stores/index.ts create mode 100644 src/entities/user/stores/user.stores.ts create mode 100644 src/entities/user/types/index.ts create mode 100644 src/entities/user/types/user.types.ts create mode 100644 src/entities/user/ui/index.ts create mode 100644 src/entities/user/ui/user-view/UserView.tsx create mode 100644 src/features/comment/hooks/index.ts create mode 100644 src/features/comment/hooks/useAddComment.ts create mode 100644 src/features/comment/hooks/useDeleteComment.ts create mode 100644 src/features/comment/hooks/useGetCommentsByPostId.ts create mode 100644 src/features/comment/hooks/useLikeComment.ts create mode 100644 src/features/comment/hooks/useUpdateComment.ts create mode 100644 src/features/comment/services/comment.service.ts create mode 100644 src/features/comment/services/index.ts create mode 100644 src/features/post/hooks/index.ts create mode 100644 src/features/post/hooks/useAddPost.ts create mode 100644 src/features/post/hooks/useDeletePost.ts create mode 100644 src/features/post/hooks/useGetPosts.ts create mode 100644 src/features/post/hooks/useUpdatePost.ts create mode 100644 src/features/post/services/index.ts create mode 100644 src/features/post/services/post.service.ts create mode 100644 src/features/post/types/index.ts create mode 100644 src/features/post/types/post.types.ts create mode 100644 src/features/user/services/index.ts create mode 100644 src/features/user/services/user.service.ts delete mode 100644 src/pages/PostsManagerPage.tsx create mode 100644 src/pages/posts/manager/PostsManagerPage.tsx create mode 100644 src/pages/posts/manager/index.ts create mode 100644 src/shared/api/api.ts create mode 100644 src/shared/api/index.ts create mode 100644 src/shared/api/query-client.ts create mode 100644 src/shared/hooks/useDebounce.ts create mode 100644 src/shared/stores/modal/index.ts create mode 100644 src/shared/stores/modal/modal.stores.ts create mode 100644 src/shared/stores/query-params/index.ts create mode 100644 src/shared/stores/query-params/query-params.store.ts create mode 100644 src/shared/types/api.types.ts create mode 100644 src/shared/types/index.ts create mode 100644 src/shared/types/pagination.types.ts create mode 100644 src/shared/ui/button/Button.tsx create mode 100644 src/shared/ui/card/Card.tsx create mode 100644 src/shared/ui/card/CardContent.tsx create mode 100644 src/shared/ui/card/CardHeader.tsx create mode 100644 src/shared/ui/card/CardTitle.tsx create mode 100644 src/shared/ui/dialog/Dialog.tsx create mode 100644 src/shared/ui/dialog/DialogContent.tsx create mode 100644 src/shared/ui/dialog/DialogHeader.tsx create mode 100644 src/shared/ui/dialog/DialogPortal.tsx create mode 100644 src/shared/ui/dialog/DialogTitle.tsx create mode 100644 src/shared/ui/highlight-text/HighlightText.tsx create mode 100644 src/shared/ui/index.ts delete mode 100644 src/shared/ui/index.tsx create mode 100644 src/shared/ui/input/Input.tsx create mode 100644 src/shared/ui/loading/Loading.tsx create mode 100644 src/shared/ui/select/Select.tsx create mode 100644 src/shared/ui/select/SelectContent.tsx create mode 100644 src/shared/ui/select/SelectGroup.tsx create mode 100644 src/shared/ui/select/SelectItem.tsx create mode 100644 src/shared/ui/select/SelectTrigger.tsx create mode 100644 src/shared/ui/select/SelectValue.tsx create mode 100644 src/shared/ui/table/Table.tsx create mode 100644 src/shared/ui/table/TableBody.tsx create mode 100644 src/shared/ui/table/TableCell.tsx create mode 100644 src/shared/ui/table/TableHead.tsx create mode 100644 src/shared/ui/table/TableHeader.tsx create mode 100644 src/shared/ui/table/TableRow.tsx create mode 100644 src/shared/ui/textarea/Textarea.tsx create mode 100644 src/widgets/comment/add-comment-modal/hooks/useAddCommentModal.ts create mode 100644 src/widgets/comment/add-comment-modal/index.ts create mode 100644 src/widgets/comment/add-comment-modal/ui/AddCommentModal.tsx create mode 100644 src/widgets/comment/comment-detail-container/index.ts create mode 100644 src/widgets/comment/comment-detail-container/ui/CommentDetailContainer.tsx create mode 100644 src/widgets/comment/comment-detail-container/ui/CommentDetailItem.tsx create mode 100644 src/widgets/comment/edit-comment-modal/hooks/useEditCommentModal.ts create mode 100644 src/widgets/comment/edit-comment-modal/index.ts create mode 100644 src/widgets/comment/edit-comment-modal/ui/EditCommentModal.tsx create mode 100644 src/widgets/common/footer/index.ts create mode 100644 src/widgets/common/footer/ui/Footer.tsx create mode 100644 src/widgets/common/header/constants/header.constants.ts create mode 100644 src/widgets/common/header/index.ts create mode 100644 src/widgets/common/header/ui/Header.tsx create mode 100644 src/widgets/common/pagination/index.ts create mode 100644 src/widgets/common/pagination/ui/Pagination.tsx create mode 100644 src/widgets/post/add-post-modal/hooks/useAddPostModal.ts create mode 100644 src/widgets/post/add-post-modal/index.ts create mode 100644 src/widgets/post/add-post-modal/ui/AddPostModal.tsx create mode 100644 src/widgets/post/edit-post-modal/hooks/useEditPostModal.ts create mode 100644 src/widgets/post/edit-post-modal/index.ts create mode 100644 src/widgets/post/edit-post-modal/ui/EditPostModal.tsx create mode 100644 src/widgets/post/filter-controller/index.ts create mode 100644 src/widgets/post/filter-controller/ui/FilterController.tsx create mode 100644 src/widgets/post/post-detail-modal/hooks/usePostDetailModal.ts create mode 100644 src/widgets/post/post-detail-modal/index.ts create mode 100644 src/widgets/post/post-detail-modal/ui/PostDetailModal.tsx create mode 100644 src/widgets/post/post-table/index.ts create mode 100644 src/widgets/post/post-table/ui/PostListItem.tsx create mode 100644 src/widgets/post/post-table/ui/PostListTable.tsx delete mode 100644 src/widgets/ui/Footer.tsx delete mode 100644 src/widgets/ui/Header.tsx create mode 100644 src/widgets/user/profile-modal/hooks/useUserProfileModal.ts create mode 100644 src/widgets/user/profile-modal/index.ts create mode 100644 src/widgets/user/profile-modal/ui/UserModal.tsx diff --git a/package.json b/package.json index fcb2757eb..0f2b19c82 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ "coverage": "vitest run --coverage" }, "dependencies": { + "@tanstack/react-query": "^5.74.7", "react": "^19.1.0", - "react-dom": "^19.1.0" + "react-dom": "^19.1.0", + "zustand": "^5.0.3" }, "devDependencies": { "@eslint/js": "^9.25.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1c5650db..8bc4d5686 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: dependencies: + '@tanstack/react-query': + specifier: ^5.74.7 + version: 5.74.7(react@19.1.0) react: specifier: ^19.1.0 version: 19.1.0 react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + zustand: + specifier: ^5.0.3 + version: 5.0.3(@types/react@19.1.2)(react@19.1.0) devDependencies: '@eslint/js': specifier: ^9.25.1 @@ -899,6 +905,14 @@ packages: cpu: [x64] os: [win32] + '@tanstack/query-core@5.74.7': + resolution: {integrity: sha512-X3StkN/Y6KGHndTjJf8H8th7AX4bKfbRpiVhVqevf0QWlxl6DhyJ0TYG3R0LARa/+xqDwzU9mA4pbJxzPCI29A==} + + '@tanstack/react-query@5.74.7': + resolution: {integrity: sha512-u4o/RIWnnrq26orGZu2NDPwmVof1vtAiiV6KYUXd49GuK+8HX+gyxoAYqIaZogvCE1cqOuZAhQKcrKGYGkrLxg==} + peerDependencies: + react: ^18 || ^19 + '@testing-library/dom@10.4.0': resolution: {integrity: sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==} engines: {node: '>=18'} @@ -2229,6 +2243,24 @@ packages: resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==} engines: {node: '>=18'} + zustand@5.0.3: + resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@adobe/css-tools@4.4.0': {} @@ -2936,6 +2968,13 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.40.0': optional: true + '@tanstack/query-core@5.74.7': {} + + '@tanstack/react-query@5.74.7(react@19.1.0)': + dependencies: + '@tanstack/query-core': 5.74.7 + react: 19.1.0 + '@testing-library/dom@10.4.0': dependencies: '@babel/code-frame': 7.26.2 @@ -3177,7 +3216,7 @@ snapshots: '@vitest/utils@2.1.3': dependencies: '@vitest/pretty-format': 2.1.3 - loupe: 3.1.2 + loupe: 3.1.3 tinyrainbow: 1.2.0 '@vitest/utils@3.1.2': @@ -4234,3 +4273,8 @@ snapshots: yocto-queue@0.1.0: {} yoctocolors-cjs@2.1.2: {} + + zustand@5.0.3(@types/react@19.1.2)(react@19.1.0): + optionalDependencies: + '@types/react': 19.1.2 + react: 19.1.0 diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 82d35d55b..000000000 --- a/src/App.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { BrowserRouter as Router } from "react-router-dom" -import Header from "./widgets/ui/Header.tsx" -import Footer from "./widgets/ui/Footer.tsx" -import PostsManagerPage from "./pages/PostsManagerPage.tsx" - -const App = () => { - return ( - -
-
-
- -
-
-
-
- ) -} - -export default App diff --git a/src/app/App.tsx b/src/app/App.tsx new file mode 100644 index 000000000..884065521 --- /dev/null +++ b/src/app/App.tsx @@ -0,0 +1,25 @@ +import { PostsManagerPage } from "@/pages/posts/manager" + +import { Footer } from "@/widgets/common/footer" +import { Header } from "@/widgets/common/header" +import { BrowserRouter as Router } from "react-router-dom" +import { TanstackQueryProvider } from "./provider/tanstack-query" +import "./styles/index.css" + +const App = () => { + return ( + + +
+
+
+ +
+
+
+
+
+ ) +} + +export default App diff --git a/src/app/assets/react.svg b/src/app/assets/react.svg deleted file mode 100644 index 6c87de9bb..000000000 --- a/src/app/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/provider/tanstack-query/index.ts b/src/app/provider/tanstack-query/index.ts new file mode 100644 index 000000000..548b0980f --- /dev/null +++ b/src/app/provider/tanstack-query/index.ts @@ -0,0 +1 @@ +export { TanstackQueryProvider } from "./tanstack-query-provider"; diff --git a/src/app/provider/tanstack-query/tanstack-query-provider.tsx b/src/app/provider/tanstack-query/tanstack-query-provider.tsx new file mode 100644 index 000000000..8d01c3a9f --- /dev/null +++ b/src/app/provider/tanstack-query/tanstack-query-provider.tsx @@ -0,0 +1,7 @@ +import { queryClient } from "@/shared/api" +import { QueryClientProvider } from "@tanstack/react-query" +import * as React from "react" + +export const TanstackQueryProvider = ({ children }: { children: React.ReactNode }) => { + return {children} +} diff --git a/src/index.css b/src/app/styles/index.css similarity index 100% rename from src/index.css rename to src/app/styles/index.css diff --git a/src/entities/comment/api/comment-query-key.api.ts b/src/entities/comment/api/comment-query-key.api.ts new file mode 100644 index 000000000..eb87c72c8 --- /dev/null +++ b/src/entities/comment/api/comment-query-key.api.ts @@ -0,0 +1,3 @@ +export const COMMENT_QUERY_KEY = { + byPostId: (postId: number) => ["comments", postId] as const, +} diff --git a/src/entities/comment/api/comment.api.ts b/src/entities/comment/api/comment.api.ts new file mode 100644 index 000000000..58eb9a329 --- /dev/null +++ b/src/entities/comment/api/comment.api.ts @@ -0,0 +1,52 @@ +import { ApiClient } from "@/shared/api/api" +import { ApiResponse } from "@/shared/types" +import { CommentResponseDto } from "../dto/comment.dto" +import { Comment } from "../types" + +export const commentApi = (apiClient: ApiClient) => ({ + fetchAllComments: async (postId: number): Promise => { + return await apiClient + .get>(`/api/comments/post/${postId}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchAllComments Error: ", error) + return error + }) + }, + fetchAddComment: async (body: string, postId: number, userId: number): Promise => { + return await apiClient + .post>(`/api/comments/add`, { body, postId, userId }) + .then((response) => response.data) + .catch((error) => { + console.error("FetchAddComment Error: ", error) + return error + }) + }, + fetchUpdateComment: async (id: number, body: string): Promise => { + return await apiClient + .put>(`/api/comments/${id}`, { body }) + .then((response) => response.data) + .catch((error) => { + console.error("FetchUpdateComment Error: ", error) + return error + }) + }, + fetchDeleteComment: async (id: number): Promise => { + return await apiClient + .delete>(`/api/comments/${id}`) + .then((response) => response.ok) + .catch((error) => { + console.error("FetchDeleteComment Error: ", error) + return error + }) + }, + fetchLikeComment: async (id: number): Promise => { + return await apiClient + .patch>(`/api/comments/${id}/like`) + .then((response) => response.ok) + .catch((error) => { + console.error("FetchLikeComment Error: ", error) + return error + }) + }, +}) diff --git a/src/entities/comment/api/index.ts b/src/entities/comment/api/index.ts new file mode 100644 index 000000000..001679e23 --- /dev/null +++ b/src/entities/comment/api/index.ts @@ -0,0 +1,2 @@ +export { COMMENT_QUERY_KEY } from "./comment-query-key.api" +export { commentApi } from "./comment.api" diff --git a/src/entities/comment/dto/comment.dto.ts b/src/entities/comment/dto/comment.dto.ts new file mode 100644 index 000000000..2660b4233 --- /dev/null +++ b/src/entities/comment/dto/comment.dto.ts @@ -0,0 +1,6 @@ +import { Pagination } from "@/shared/types" +import { Comment } from "../types/comment.types" + +export interface CommentResponseDto extends Pagination { + comments: Comment[] +} diff --git a/src/entities/comment/stores/comment.store.ts b/src/entities/comment/stores/comment.store.ts new file mode 100644 index 000000000..ac2ac8b4c --- /dev/null +++ b/src/entities/comment/stores/comment.store.ts @@ -0,0 +1,25 @@ +import { create } from "zustand" +import { Comment } from "../types" + +type CommentStoreState = Pick & { + userId: number + + setBody: (body: string) => void + setPostId: (postId: number) => void + setUserId: (userId: number) => void + setSelectedComment: (postId: number, commentId: number, body: string) => void + reset: () => void +} + +export const useCommentStore = create((set) => ({ + body: "", + postId: 0, + userId: 0, + id: 0, + + setBody: (body) => set({ body }), + setPostId: (postId) => set({ postId }), + setUserId: (userId) => set({ userId }), + setSelectedComment: (postId: number, commentId: number, body: string) => set({ postId, id: commentId, body }), + reset: () => set({ body: "", postId: 0 }), +})) diff --git a/src/entities/comment/stores/comments.stores.ts b/src/entities/comment/stores/comments.stores.ts new file mode 100644 index 000000000..485a15185 --- /dev/null +++ b/src/entities/comment/stores/comments.stores.ts @@ -0,0 +1,22 @@ +import { create } from "zustand" +import { Comment } from "../types" + +interface CommentsState { + comments: Array + setComments: (comments: Comment[]) => void + addComment: (comment: Comment) => void + removeComment: (id: number) => void + updateComment: (comment: Comment) => void +} + +export const commentsStore = () => { + return create((set) => ({ + comments: [], + setComments: (comments: Comment[]) => set({ comments }), + addComment: (comment: Comment) => set((state) => ({ comments: [...state.comments, comment] })), + removeComment: (id: number) => + set((state) => ({ comments: state.comments.filter((comment) => comment.id !== id) })), + updateComment: (comment: Comment) => + set((state) => ({ comments: state.comments.map((c) => (c.id === comment.id ? comment : c)) })), + })) +} diff --git a/src/entities/comment/stores/index.ts b/src/entities/comment/stores/index.ts new file mode 100644 index 000000000..63e91d6d1 --- /dev/null +++ b/src/entities/comment/stores/index.ts @@ -0,0 +1 @@ +export { useCommentStore } from "./comment.store" diff --git a/src/entities/comment/types/comment.types.ts b/src/entities/comment/types/comment.types.ts new file mode 100644 index 000000000..de1c20c5f --- /dev/null +++ b/src/entities/comment/types/comment.types.ts @@ -0,0 +1,9 @@ +import { User } from "@/entities/user/types/user.types" + +export interface Comment { + body: string + id: number + likes: number + postId: number + user: User +} diff --git a/src/entities/comment/types/index.ts b/src/entities/comment/types/index.ts new file mode 100644 index 000000000..b9c473999 --- /dev/null +++ b/src/entities/comment/types/index.ts @@ -0,0 +1 @@ +export { type Comment } from "./comment.types" diff --git a/src/entities/comment/ui/comment-view/CommentView.tsx b/src/entities/comment/ui/comment-view/CommentView.tsx new file mode 100644 index 000000000..ee59905d0 --- /dev/null +++ b/src/entities/comment/ui/comment-view/CommentView.tsx @@ -0,0 +1,18 @@ +import { HighlightText } from "@/shared/ui" +import { Comment } from "../../types" + +interface CommentViewProps extends React.HTMLAttributes { + comment: Comment + searchQuery: string +} + +export const CommentView: React.FC = ({ comment, searchQuery, ...props }) => { + return ( +
+ {comment.user.username}: + + + +
+ ) +} diff --git a/src/entities/comment/ui/index.ts b/src/entities/comment/ui/index.ts new file mode 100644 index 000000000..7e59b615e --- /dev/null +++ b/src/entities/comment/ui/index.ts @@ -0,0 +1 @@ +export { CommentView } from "./comment-view/CommentView" diff --git a/src/entities/post/api/index.ts b/src/entities/post/api/index.ts new file mode 100644 index 000000000..37e7b2bb7 --- /dev/null +++ b/src/entities/post/api/index.ts @@ -0,0 +1,2 @@ +export { POST_QUERY_KEY } from "./post-query-key.api" +export { postApi } from "./post.api" diff --git a/src/entities/post/api/post-query-key.api.ts b/src/entities/post/api/post-query-key.api.ts new file mode 100644 index 000000000..0bdfd5963 --- /dev/null +++ b/src/entities/post/api/post-query-key.api.ts @@ -0,0 +1,7 @@ +export const POST_QUERY_KEY = { + all: ["posts"] as const, + list: (params: { limit?: number; skip?: number }) => ["posts", "list", params] as const, + search: (query: string) => ["posts", "search", query] as const, + tag: (tag: string) => ["posts", "tag", tag] as const, + detail: (id: number) => ["post", id] as const, +} diff --git a/src/entities/post/api/post.api.ts b/src/entities/post/api/post.api.ts new file mode 100644 index 000000000..6fdd672dc --- /dev/null +++ b/src/entities/post/api/post.api.ts @@ -0,0 +1,70 @@ +import { ApiClient } from "@/shared/api/api" +import { ApiResponse } from "@/shared/types" +import { PostsResponseDto } from "../dto/post.dto" +import { Post, Tag } from "../types/post.types" + +export const postApi = (apiClient: ApiClient) => ({ + fetchAllPosts: async (limit: number, skip: number): Promise => { + return await apiClient + .get>(`/api/posts?limit=${limit}&skip=${skip}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchAllPosts Error: ", error) + return error + }) + }, + fetchPostsByTag: async (tag: string): Promise => { + return await apiClient + .get>(`/api/posts/tag/${tag}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchPostsByTag Error: ", error) + return error + }) + }, + fetchAllTags: async (): Promise => { + return await apiClient + .get>(`/api/posts/tags`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchAllTags Error: ", error) + return error + }) + }, + fetchSearchPosts: async (searchQuery: string): Promise => { + return await apiClient + .get>(`/api/posts/search?q=${searchQuery}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchSearchPosts Error: ", error) + return error + }) + }, + fetchAddPost: async (title: string, body: string, userId: number): Promise => { + return await apiClient + .post>(`/api/posts/add`, { title, body, userId }) + .then((response) => response.data) + .catch((error) => { + console.error("FetchAddPost Error: ", error) + return error + }) + }, + fetchUpdatePost: async (post: Post): Promise => { + return await apiClient + .put>(`/api/posts/${post.id}`, post) + .then((response) => response.data) + .catch((error) => { + console.error("FetchUpdatePost Error: ", error) + return error + }) + }, + fetchDeletePost: async (id: number): Promise => { + return await apiClient + .delete>(`/api/posts/${id}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchDeletePost Error: ", error) + return error + }) + }, +}) diff --git a/src/entities/post/dto/post.dto.ts b/src/entities/post/dto/post.dto.ts new file mode 100644 index 000000000..4d3ee5e79 --- /dev/null +++ b/src/entities/post/dto/post.dto.ts @@ -0,0 +1,6 @@ +import { Pagination } from "@/shared/types" +import { Post } from "../types/post.types" + +export interface PostsResponseDto extends Pagination { + posts: Post[] +} diff --git a/src/entities/post/stores/index.ts b/src/entities/post/stores/index.ts new file mode 100644 index 000000000..26e5cc0c1 --- /dev/null +++ b/src/entities/post/stores/index.ts @@ -0,0 +1,3 @@ +export { usePostTotalStore } from "./post-total.stores" +export { usePostStore } from "./post.stores" +export { postsStore } from "./posts.stores" diff --git a/src/entities/post/stores/post-total.stores.ts b/src/entities/post/stores/post-total.stores.ts new file mode 100644 index 000000000..dfbe8acee --- /dev/null +++ b/src/entities/post/stores/post-total.stores.ts @@ -0,0 +1,11 @@ +import { create } from "zustand" + +interface PostTotalState { + total: number + setTotal: (total: number) => void +} + +export const usePostTotalStore = create((set) => ({ + total: 0, + setTotal: (total: number) => set({ total }), +})) diff --git a/src/entities/post/stores/post.stores.ts b/src/entities/post/stores/post.stores.ts new file mode 100644 index 000000000..b95fd4277 --- /dev/null +++ b/src/entities/post/stores/post.stores.ts @@ -0,0 +1,12 @@ +import { create } from "zustand" +import { PostWithAuthor } from "../types" + +type PostStoreState = { + selectedPost: PostWithAuthor | null + setSelectedPost: (post: PostWithAuthor | null) => void +} + +export const usePostStore = create((set) => ({ + selectedPost: null, + setSelectedPost: (post) => set({ selectedPost: post }), +})) diff --git a/src/entities/post/stores/posts.stores.ts b/src/entities/post/stores/posts.stores.ts new file mode 100644 index 000000000..8f2268029 --- /dev/null +++ b/src/entities/post/stores/posts.stores.ts @@ -0,0 +1,21 @@ +import { create } from "zustand" +import { PostWithAuthor } from "../types" + +interface PostsState { + posts: Array + setPosts: (posts: PostWithAuthor[]) => void + addPosts: (post: PostWithAuthor) => void + removePosts: (id: number) => void + updatePosts: (post: PostWithAuthor) => void +} + +export const postsStore = () => { + return create((set) => ({ + posts: [], + setPosts: (posts: PostWithAuthor[]) => set({ posts }), + addPosts: (post: PostWithAuthor) => set((state) => ({ posts: [...state.posts, post] })), + removePosts: (id: number) => set((state) => ({ posts: state.posts.filter((post) => post.id !== id) })), + updatePosts: (post: PostWithAuthor) => + set((state) => ({ posts: state.posts.map((p) => (p.id === post.id ? post : p)) })), + })) +} diff --git a/src/entities/post/types/index.ts b/src/entities/post/types/index.ts new file mode 100644 index 000000000..9e10e4b42 --- /dev/null +++ b/src/entities/post/types/index.ts @@ -0,0 +1 @@ +export type { Post, PostWithAuthor, Tag } from "./post.types" diff --git a/src/entities/post/types/post.types.ts b/src/entities/post/types/post.types.ts new file mode 100644 index 000000000..c29bf2365 --- /dev/null +++ b/src/entities/post/types/post.types.ts @@ -0,0 +1,26 @@ +import { User } from "@/entities/user/types/user.types" + +export interface Post { + id: number + title: string + body: string + userId?: number + tags: string[] + reactions?: Reactions + views?: number +} + +export interface Tag { + name: string + slug: string + url: string +} + +interface Reactions { + likes: number + dislikes: number +} + +export interface PostWithAuthor extends Post { + author?: User +} diff --git a/src/entities/post/ui/index.ts b/src/entities/post/ui/index.ts new file mode 100644 index 000000000..464bcc35b --- /dev/null +++ b/src/entities/post/ui/index.ts @@ -0,0 +1 @@ +export { TagView } from "./tag-view/TagView" diff --git a/src/entities/post/ui/tag-view/TagView.tsx b/src/entities/post/ui/tag-view/TagView.tsx new file mode 100644 index 000000000..0cbc864b3 --- /dev/null +++ b/src/entities/post/ui/tag-view/TagView.tsx @@ -0,0 +1,18 @@ +interface TagViewProps extends React.HTMLAttributes { + tag: string +} + +export const TagView: React.FC = ({ tag, ...props }) => { + return ( + + {tag} + + ) +} diff --git a/src/entities/user/api/index.ts b/src/entities/user/api/index.ts new file mode 100644 index 000000000..3a1dd0ab6 --- /dev/null +++ b/src/entities/user/api/index.ts @@ -0,0 +1 @@ +export { userApi } from "./user.api" diff --git a/src/entities/user/api/user-query-key.api.ts b/src/entities/user/api/user-query-key.api.ts new file mode 100644 index 000000000..3fa4e57fe --- /dev/null +++ b/src/entities/user/api/user-query-key.api.ts @@ -0,0 +1,3 @@ +export const USER_QUERY_KEY = { + profile: (id: number) => ["user", id] as const, +} diff --git a/src/entities/user/api/user.api.ts b/src/entities/user/api/user.api.ts new file mode 100644 index 000000000..f6cdb2804 --- /dev/null +++ b/src/entities/user/api/user.api.ts @@ -0,0 +1,27 @@ +import { ApiClient } from "@/shared/api/api" +import { ApiResponse } from "@/shared/types" +import { AllUserProfilesResponseDto } from "../dto/user.dto" +import { User } from "../types" + +export const userApi = (apiClient: ApiClient) => ({ + fetchAllUserProfiles: async (): Promise => { + return await apiClient + .get>(`/api/users?limit=0&select=username,image`) + .then((response) => { + return response.data + }) + .catch((error) => { + console.error("FetchAllUserProfiles Error: ", error) + return error + }) + }, + fetchGetUserProfile: async (userId: number): Promise => { + return await apiClient + .get>(`/api/users/${userId}`) + .then((response) => response.data) + .catch((error) => { + console.error("FetchGetUserProfile Error: ", error) + return error + }) + }, +}) diff --git a/src/entities/user/dto/user.dto.ts b/src/entities/user/dto/user.dto.ts new file mode 100644 index 000000000..d5fa917c1 --- /dev/null +++ b/src/entities/user/dto/user.dto.ts @@ -0,0 +1,11 @@ +import { Pagination } from "@/shared/types" + +export interface UserProfileResponseDto { + id: number + image: string + username: string +} + +export interface AllUserProfilesResponseDto extends Pagination { + users: UserProfileResponseDto[] +} diff --git a/src/entities/user/stores/index.ts b/src/entities/user/stores/index.ts new file mode 100644 index 000000000..bf592c019 --- /dev/null +++ b/src/entities/user/stores/index.ts @@ -0,0 +1 @@ +export { useUserStore } from "./user.stores" diff --git a/src/entities/user/stores/user.stores.ts b/src/entities/user/stores/user.stores.ts new file mode 100644 index 000000000..87bfe4947 --- /dev/null +++ b/src/entities/user/stores/user.stores.ts @@ -0,0 +1,9 @@ +import { create } from "zustand" +import { User } from "../types" + +type UserStoreState = Pick + +export const useUserStore = create((set) => ({ + id: 1, + setSelectedUserId: (id: number) => set({ id }), +})) diff --git a/src/entities/user/types/index.ts b/src/entities/user/types/index.ts new file mode 100644 index 000000000..0cae2f6b8 --- /dev/null +++ b/src/entities/user/types/index.ts @@ -0,0 +1 @@ +export { type User } from "./user.types" diff --git a/src/entities/user/types/user.types.ts b/src/entities/user/types/user.types.ts new file mode 100644 index 000000000..3acaafc75 --- /dev/null +++ b/src/entities/user/types/user.types.ts @@ -0,0 +1,19 @@ +export interface User { + id: number; + username: string; + image: string; + firstName?: string; + lastName?: string; + age?: number; + email?: string; + phone?: string; + address?: { + address: string; + city: string; + state: string; + }; + company?: { + name: string; + title: string; + }; +} diff --git a/src/entities/user/ui/index.ts b/src/entities/user/ui/index.ts new file mode 100644 index 000000000..621c9c841 --- /dev/null +++ b/src/entities/user/ui/index.ts @@ -0,0 +1 @@ +export { UserView } from "./user-view/UserView" diff --git a/src/entities/user/ui/user-view/UserView.tsx b/src/entities/user/ui/user-view/UserView.tsx new file mode 100644 index 000000000..4d581afc3 --- /dev/null +++ b/src/entities/user/ui/user-view/UserView.tsx @@ -0,0 +1,36 @@ +import React from "react" +import { User } from "../../types" + +interface UserViewProps extends React.HTMLAttributes { + userProfile: User +} + +export const UserView: React.FC = ({ userProfile, ...props }) => { + return ( +
+ {userProfile?.username} +

{userProfile?.username}

+
+

+ 이름: {userProfile?.firstName} {userProfile?.lastName} +

+

+ 나이: {userProfile?.age} +

+

+ 이메일: {userProfile?.email} +

+

+ 전화번호: {userProfile?.phone} +

+

+ 주소: {userProfile?.address?.address}, {userProfile?.address?.city},{" "} + {userProfile?.address?.state} +

+

+ 직장: {userProfile?.company?.name} - {userProfile?.company?.title} +

+
+
+ ) +} diff --git a/src/features/comment/hooks/index.ts b/src/features/comment/hooks/index.ts new file mode 100644 index 000000000..5de735b38 --- /dev/null +++ b/src/features/comment/hooks/index.ts @@ -0,0 +1,5 @@ +export { useAddComment } from "./useAddComment" +export { useDeleteComment } from "./useDeleteComment" +export { useGetCommentsByPostId } from "./useGetCommentsByPostId" +export { useLikeComment } from "./useLikeComment" +export { useUpdateComment } from "./useUpdateComment" diff --git a/src/features/comment/hooks/useAddComment.ts b/src/features/comment/hooks/useAddComment.ts new file mode 100644 index 000000000..589b2af8c --- /dev/null +++ b/src/features/comment/hooks/useAddComment.ts @@ -0,0 +1,31 @@ +import { COMMENT_QUERY_KEY, commentApi } from "@/entities/comment/api" +import { Comment } from "@/entities/comment/types" +import { CommentService } from "@/features/comment/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type AddCommentParams = { + body: string + postId: number + userId: number +} + +export const useAddComment = () => { + return useMutation({ + mutationFn: async ({ body, postId, userId }) => { + const result = await CommentService(commentApi(apiClient)).addComment(body, postId, userId) + return result + }, + + // * 성공 시 캐시 직접 업데이트 + onSuccess: (newComment) => { + // * 캐시에 있는 댓글 데이터 직접 업데이트 + queryClient.setQueryData(COMMENT_QUERY_KEY.byPostId(newComment.postId), (oldComments: Comment[] | undefined) => { + if (!oldComments) + return [newComment] // * 기존 댓글이 없으면 새 배열 생성 + // * 기존 배열에 새 댓글 추가 + else return [newComment, ...oldComments] + }) + }, + }) +} diff --git a/src/features/comment/hooks/useDeleteComment.ts b/src/features/comment/hooks/useDeleteComment.ts new file mode 100644 index 000000000..30c510537 --- /dev/null +++ b/src/features/comment/hooks/useDeleteComment.ts @@ -0,0 +1,26 @@ +import { COMMENT_QUERY_KEY, commentApi } from "@/entities/comment/api" +import { Comment } from "@/entities/comment/types" +import { CommentService } from "@/features/comment/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type DeleteCommentParams = { + id: number + postId: number +} + +export const useDeleteComment = () => { + return useMutation<{ result: boolean; postId: number }, Error, DeleteCommentParams>({ + mutationFn: async ({ id, postId }) => { + const result = await CommentService(commentApi(apiClient)).deleteComment(id) + return { result, postId } + }, + onSuccess: ({ postId }, { id }) => { + queryClient.setQueryData(COMMENT_QUERY_KEY.byPostId(postId), (oldComments: Comment[] | undefined) => { + if (!oldComments) return oldComments + + return oldComments.filter((comment) => comment.id !== id) + }) + }, + }) +} diff --git a/src/features/comment/hooks/useGetCommentsByPostId.ts b/src/features/comment/hooks/useGetCommentsByPostId.ts new file mode 100644 index 000000000..7c01a36dc --- /dev/null +++ b/src/features/comment/hooks/useGetCommentsByPostId.ts @@ -0,0 +1,14 @@ +import { COMMENT_QUERY_KEY, commentApi } from "@/entities/comment/api" +import { CommentService } from "@/features/comment/services" +import { apiClient } from "@/shared/api" +import { useQuery } from "@tanstack/react-query" + +export const useGetCommentsByPostId = (postId: number) => { + return useQuery({ + queryKey: COMMENT_QUERY_KEY.byPostId(postId), + queryFn: () => CommentService(commentApi(apiClient)).getAllComments(postId), + staleTime: 0, + refetchOnWindowFocus: false, + enabled: !!postId, + }) +} diff --git a/src/features/comment/hooks/useLikeComment.ts b/src/features/comment/hooks/useLikeComment.ts new file mode 100644 index 000000000..1b1c288d8 --- /dev/null +++ b/src/features/comment/hooks/useLikeComment.ts @@ -0,0 +1,29 @@ +import { COMMENT_QUERY_KEY, commentApi } from "@/entities/comment/api" +import { Comment } from "@/entities/comment/types" +import { CommentService } from "@/features/comment/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type LikeCommentParams = { + id: number + postId: number +} + +export const useLikeComment = () => { + return useMutation<{ result: boolean; id: number }, Error, LikeCommentParams>({ + mutationFn: async ({ id }) => { + const result = await CommentService(commentApi(apiClient)).likeComment(id) + + return { result, id } + }, + + onSuccess: ({ result, id }, { postId }) => { + if (!result) return + queryClient.setQueryData(COMMENT_QUERY_KEY.byPostId(postId), (oldComments: Comment[] | undefined) => { + if (!oldComments) return oldComments + + return oldComments.map((comment) => (comment.id === id ? { ...comment, likes: comment.likes + 1 } : comment)) + }) + }, + }) +} diff --git a/src/features/comment/hooks/useUpdateComment.ts b/src/features/comment/hooks/useUpdateComment.ts new file mode 100644 index 000000000..e9dea0d2e --- /dev/null +++ b/src/features/comment/hooks/useUpdateComment.ts @@ -0,0 +1,33 @@ +import { COMMENT_QUERY_KEY, commentApi } from "@/entities/comment/api" +import { Comment } from "@/entities/comment/types" +import { CommentService } from "@/features/comment/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type UpdateCommentParams = { + id: number + body: string +} + +export const useUpdateComment = () => { + return useMutation({ + mutationFn: async ({ id, body }) => { + const result = await CommentService(commentApi(apiClient)).updateComment(id, body) + + return result + }, + + onSuccess: (updatedComment) => { + queryClient.setQueryData( + COMMENT_QUERY_KEY.byPostId(updatedComment.postId), + (oldComments: Comment[] | undefined) => { + if (!oldComments) return oldComments + + return oldComments.map((comment) => + comment.id === updatedComment.id ? { ...comment, body: updatedComment.body } : comment, + ) + }, + ) + }, + }) +} diff --git a/src/features/comment/services/comment.service.ts b/src/features/comment/services/comment.service.ts new file mode 100644 index 000000000..dde3d9062 --- /dev/null +++ b/src/features/comment/services/comment.service.ts @@ -0,0 +1,53 @@ +import { commentApi } from "@/entities/comment/api" +import { Comment } from "@/entities/comment/types/comment.types" + +export const CommentService = (commentApiClient: ReturnType) => ({ + getAllComments: async (postId: number): Promise => { + try { + const { comments } = await commentApiClient.fetchAllComments(postId) + if (!comments) return [] + return comments + } catch (error) { + console.error("GetAllComments Error:", error) + throw error + } + }, + addComment: async (body: string, postId: number, userId: number): Promise => { + try { + const result = await commentApiClient.fetchAddComment(body, postId, userId) + return result + } catch (error) { + console.error("AddComment Error:", error) + throw error + } + }, + updateComment: async (id: number, body: string): Promise => { + try { + const result = await commentApiClient.fetchUpdateComment(id, body) + return result + } catch (error) { + console.error("UpdateComment Error:", error) + throw error + } + }, + deleteComment: async (id: number): Promise => { + try { + const result = await commentApiClient.fetchDeleteComment(id) + if (result) return false + return true + } catch (error) { + console.error("DeleteComment Error:", error) + throw error + } + }, + likeComment: async (id: number): Promise => { + try { + const result = await commentApiClient.fetchLikeComment(id) + if (!result) return false + return true + } catch (error) { + console.error("LikeComment Error:", error) + throw error + } + }, +}) diff --git a/src/features/comment/services/index.ts b/src/features/comment/services/index.ts new file mode 100644 index 000000000..b5cd1f5ef --- /dev/null +++ b/src/features/comment/services/index.ts @@ -0,0 +1 @@ +export { CommentService } from "./comment.service" diff --git a/src/features/post/hooks/index.ts b/src/features/post/hooks/index.ts new file mode 100644 index 000000000..624709c85 --- /dev/null +++ b/src/features/post/hooks/index.ts @@ -0,0 +1,5 @@ +export { useGetPostById, useGetPosts } from "./useGetPosts" + +export { useAddPost } from "./useAddPost" +export { useDeletePost } from "./useDeletePost" +export { useUpdatePost } from "./useUpdatePost" diff --git a/src/features/post/hooks/useAddPost.ts b/src/features/post/hooks/useAddPost.ts new file mode 100644 index 000000000..35da09254 --- /dev/null +++ b/src/features/post/hooks/useAddPost.ts @@ -0,0 +1,54 @@ +import { POST_QUERY_KEY, postApi } from "@/entities/post/api" +import { PostWithAuthor } from "@/entities/post/types" +import { userApi } from "@/entities/user/api" +import { PostService, PostsWithResult } from "@/features/post/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type AddPostParams = { + title: string + body: string + userId: number + tags: string[] +} + +export const useAddPost = () => { + return useMutation({ + mutationFn: async ({ title, body, userId, tags }) => { + const service = PostService(postApi(apiClient), userApi(apiClient)) + const newPost = await service.addPost(title, body, userId) + if (!newPost) throw new Error("Error: Fail to add post") + + const { users } = await userApi(apiClient).fetchAllUserProfiles() + const author = users.find((user) => user.id === userId) + + return { + ...newPost, + id: newPost.id || Math.floor(Math.random() * 1000) + 100, + title, + body, + userId, + author, + tags, + reactions: { likes: 0, dislikes: 0 }, + } + }, + + onSuccess: (newPost) => { + queryClient.setQueryData(POST_QUERY_KEY.list({ limit: 10, skip: 0 }), (oldData: PostsWithResult | undefined) => { + if (!oldData) return { posts: [newPost], total: 1 } + return { + ...oldData, + posts: [newPost, ...oldData.posts], + total: oldData.total + 1, + } + }) + + // * 검색 쿼리 무효화 + queryClient.invalidateQueries({ + queryKey: POST_QUERY_KEY.search(""), + refetchType: "none", + }) + }, + }) +} diff --git a/src/features/post/hooks/useDeletePost.ts b/src/features/post/hooks/useDeletePost.ts new file mode 100644 index 000000000..aa50ce037 --- /dev/null +++ b/src/features/post/hooks/useDeletePost.ts @@ -0,0 +1,39 @@ +import { POST_QUERY_KEY, postApi } from "@/entities/post/api" +import { userApi } from "@/entities/user/api" +import { PostService, PostsWithResult } from "@/features/post/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +type DeletePostParams = { + id: number +} + +export const useDeletePost = () => { + return useMutation<{ result: boolean; id: number }, Error, DeletePostParams>({ + mutationFn: async ({ id }) => { + const deletedPost = await PostService(postApi(apiClient), userApi(apiClient)).deletePost(id) + return { result: !!deletedPost, id } + }, + + onSuccess: ({ result, id }) => { + if (!result) return + + queryClient + .getQueryCache() + .findAll({ queryKey: POST_QUERY_KEY.all }) + .forEach((query) => { + queryClient.setQueryData(query.queryKey, (oldData: PostsWithResult | undefined) => { + if (!oldData) return oldData + + return { + ...oldData, + posts: oldData.posts.filter((post: { id: number }) => post.id !== id), + total: oldData.total - 1, + } + }) + }) + + queryClient.removeQueries({ queryKey: POST_QUERY_KEY.detail(id) }) + }, + }) +} diff --git a/src/features/post/hooks/useGetPosts.ts b/src/features/post/hooks/useGetPosts.ts new file mode 100644 index 000000000..3b7b20e6d --- /dev/null +++ b/src/features/post/hooks/useGetPosts.ts @@ -0,0 +1,75 @@ +import { POST_QUERY_KEY, postApi } from "@/entities/post/api" +import { usePostTotalStore } from "@/entities/post/stores/post-total.stores" +import { PostWithAuthor } from "@/entities/post/types" +import { userApi } from "@/entities/user/api" +import { PostService } from "@/features/post/services" +import { apiClient } from "@/shared/api" +import { useQuery } from "@tanstack/react-query" + +type GetPostsOptions = { + limit?: number + skip?: number + tag?: string + searchQuery?: string + enabled?: boolean +} + +export const useGetPosts = (options: GetPostsOptions = {}) => { + const { limit = 10, skip = 0, tag, searchQuery, enabled = true } = options + const { setTotal } = usePostTotalStore() + + // * 검색/태그 상태 확인 + const isSearchQuery = !!searchQuery && searchQuery.length >= 2 + const isTagQuery = !!tag && tag !== "all" && !isSearchQuery + + // * 쿼리 활성화 여부 + const isEnabled = isSearchQuery + ? enabled && searchQuery.length >= 2 + : isTagQuery + ? enabled && !!tag && tag !== "all" + : enabled + + // * 쿼리 키 결정 + const queryKey = isSearchQuery + ? POST_QUERY_KEY.search(searchQuery) + : isTagQuery + ? POST_QUERY_KEY.tag(tag) + : POST_QUERY_KEY.list({ limit, skip }) + + return useQuery({ + queryKey, + queryFn: async () => { + const service = PostService(postApi(apiClient), userApi(apiClient)) + let result + + if (isSearchQuery) { + result = await service.searchPosts(searchQuery) + } else if (isTagQuery && tag) { + result = await service.getPostsByTag(tag) + } else { + result = await service.getAllPosts(limit, skip) + } + + setTotal(result.total) + return result + }, + enabled: isEnabled, + staleTime: 60000, + }) +} + +export const useGetPostById = (id: number, enabled: boolean = !!id) => { + return useQuery({ + queryKey: POST_QUERY_KEY.detail(id), + queryFn: async () => { + const service = PostService(postApi(apiClient), userApi(apiClient)) + const result = await service.getAllPosts(1, 0) + const post = result.posts.find((post) => post.id === id) + + if (!post) throw new Error(`ID ${id}에 해당하는 게시물을 찾을 수 없습니다`) + return post + }, + enabled, + staleTime: 60000, + }) +} diff --git a/src/features/post/hooks/useUpdatePost.ts b/src/features/post/hooks/useUpdatePost.ts new file mode 100644 index 000000000..456870649 --- /dev/null +++ b/src/features/post/hooks/useUpdatePost.ts @@ -0,0 +1,54 @@ +import { POST_QUERY_KEY, postApi } from "@/entities/post/api" +import { PostWithAuthor } from "@/entities/post/types" +import { userApi } from "@/entities/user/api" +import { PostService, PostsWithResult } from "@/features/post/services" +import { apiClient, queryClient } from "@/shared/api" +import { useMutation } from "@tanstack/react-query" + +interface UpdatePostParams { + id: number + title: string + body: string + tags: string[] +} + +export const useUpdatePost = () => { + return useMutation({ + mutationFn: async ({ id, title, body, tags }) => { + const postService = PostService(postApi(apiClient), userApi(apiClient)) + + const updatedPost = await postService.updatePost({ id, title, body, tags }) + if (!updatedPost) throw new Error("게시물 업데이트 실패") + + const { users } = await userApi(apiClient).fetchAllUserProfiles() + const author = users.find((user) => user.id === updatedPost.userId) + + return { + ...updatedPost, + id, + title, + body, + author, + tags, + reactions: updatedPost.reactions || { likes: 0, dislikes: 0 }, + } + }, + + onSuccess: (updatedPost) => { + // * 메인 페이지 목록 업데이트 + queryClient.setQueryData(POST_QUERY_KEY.list({ limit: 10, skip: 0 }), (oldData: PostsWithResult | undefined) => { + if (!oldData) return oldData + return { + ...oldData, + posts: oldData.posts.map((post: PostWithAuthor) => (post.id === updatedPost.id ? updatedPost : post)), + } + }) + + // * 기타 쿼리 무효화 + queryClient.invalidateQueries({ + queryKey: POST_QUERY_KEY.all, + refetchType: "none", + }) + }, + }) +} diff --git a/src/features/post/services/index.ts b/src/features/post/services/index.ts new file mode 100644 index 000000000..5db64ab4b --- /dev/null +++ b/src/features/post/services/index.ts @@ -0,0 +1 @@ +export { PostService, type PostsWithResult } from "./post.service" diff --git a/src/features/post/services/post.service.ts b/src/features/post/services/post.service.ts new file mode 100644 index 000000000..7595d6ba0 --- /dev/null +++ b/src/features/post/services/post.service.ts @@ -0,0 +1,103 @@ +import { postApi } from "@/entities/post/api" +import { Post } from "@/entities/post/types" +import { userApi } from "@/entities/user/api" +import { User } from "@/entities/user/types" +import { PostsWithResult } from "../types" + +export const PostService = (postApiClient: ReturnType, userApiClient: ReturnType) => ({ + getAllPosts: async (limit: number, skip: number): Promise => { + try { + const postsData = await postApiClient.fetchAllPosts(limit, skip) + if (!postsData) return { posts: [], total: 0 } + + const usersResponse = await userApiClient.fetchAllUserProfiles() + const usersData = usersResponse.users + + const postsWithUsers = postsData.posts.map((post: Post) => ({ + ...post, + author: usersData.find((user: User) => user.id === post.userId), + })) + + return { + posts: postsWithUsers, + total: postsData.total, + } + } catch (error) { + console.error("PostService getAllPosts Error:", error) + return { posts: [], total: 0 } + } + }, + getPostsByTag: async (tag: string): Promise => { + try { + const [postsResponse, usersResponse] = await Promise.all([ + postApiClient.fetchPostsByTag(tag), + userApiClient.fetchAllUserProfiles(), + ]) + const postsWithUsers = postsResponse.posts.map((post: Post) => ({ + ...post, + author: usersResponse.users.find((user) => user.id === post.userId), + })) + return { + posts: postsWithUsers, + total: postsResponse.total, + } + } catch (error) { + console.error("PostService getPostsByTag Error:", error) + throw error + } + }, + getAllTags: async () => { + try { + const result = await postApiClient.fetchAllTags() + if (!result) return [] + return result + } catch (error) { + console.error("PostService getAllTags Error:", error) + throw error + } + }, + searchPosts: async (searchQuery: string): Promise => { + try { + const result = await postApiClient.fetchSearchPosts(searchQuery) + if (!result) return { posts: [], total: 0 } + return { + posts: result.posts, + total: result.total, + } + } catch (error) { + console.error("PostService searchPosts Error:", error) + throw error + } + }, + addPost: async (title: string, body: string, userId: number) => { + try { + const result = await postApiClient.fetchAddPost(title, body, userId) + console.log(result) + if (!result) return null + return result + } catch (error) { + console.error("PostService addPost Error:", error) + throw error + } + }, + updatePost: async (post: Post) => { + try { + const result = await postApiClient.fetchUpdatePost(post) + if (!result) return null + return result + } catch (error) { + console.error("PostService updatePost Error:", error) + throw error + } + }, + deletePost: async (id: number) => { + try { + const result = await postApiClient.fetchDeletePost(id) + if (!result) return null + return result + } catch (error) { + console.error("PostService deletePost Error:", error) + throw error + } + }, +}) diff --git a/src/features/post/types/index.ts b/src/features/post/types/index.ts new file mode 100644 index 000000000..3635c97fc --- /dev/null +++ b/src/features/post/types/index.ts @@ -0,0 +1 @@ +export { type PostsWithResult } from "./post.types" diff --git a/src/features/post/types/post.types.ts b/src/features/post/types/post.types.ts new file mode 100644 index 000000000..71d52d437 --- /dev/null +++ b/src/features/post/types/post.types.ts @@ -0,0 +1,6 @@ +import { PostWithAuthor } from "@/entities/post/types" + +export interface PostsWithResult { + posts: Array + total: number +} diff --git a/src/features/user/services/index.ts b/src/features/user/services/index.ts new file mode 100644 index 000000000..a1aef1322 --- /dev/null +++ b/src/features/user/services/index.ts @@ -0,0 +1 @@ +export { UserService } from "./user.service" diff --git a/src/features/user/services/user.service.ts b/src/features/user/services/user.service.ts new file mode 100644 index 000000000..6523a31f9 --- /dev/null +++ b/src/features/user/services/user.service.ts @@ -0,0 +1,14 @@ +import { userApi } from "@/entities/user/api" +import { User } from "@/entities/user/types" + +export const UserService = (userApiClient: ReturnType) => ({ + getUserProfile: async (userId: number): Promise => { + try { + const result = await userApiClient.fetchGetUserProfile(userId) + return result + } catch (error) { + console.error("UserService getUserProfile Error:", error) + throw error + } + }, +}) diff --git a/src/index.tsx b/src/index.tsx index 369e197bb..48326ff9b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import React from "react" import ReactDOM from "react-dom/client" import { BrowserRouter as Router } from "react-router-dom" -import App from "./App" +import App from "./app/App" ReactDOM.createRoot(document.getElementById("root")!).render( diff --git a/src/main.tsx b/src/main.tsx index bef5202a3..1d0b09bc6 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,9 +1,8 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import App from "./app/App.tsx" -createRoot(document.getElementById('root')!).render( +createRoot(document.getElementById("root")!).render( , diff --git a/src/pages/PostsManagerPage.tsx b/src/pages/PostsManagerPage.tsx deleted file mode 100644 index 9fa274db4..000000000 --- a/src/pages/PostsManagerPage.tsx +++ /dev/null @@ -1,708 +0,0 @@ -import { useEffect, useState } from "react" -import { Edit2, MessageSquare, Plus, Search, ThumbsDown, ThumbsUp, Trash2 } from "lucide-react" -import { useLocation, useNavigate } from "react-router-dom" -import { - Button, - Card, - CardContent, - CardHeader, - CardTitle, - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - Input, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - Textarea, -} from "../shared/ui" - -const PostsManager = () => { - const navigate = useNavigate() - const location = useLocation() - const queryParams = new URLSearchParams(location.search) - - // 상태 관리 - const [posts, setPosts] = useState([]) - const [total, setTotal] = useState(0) - const [skip, setSkip] = useState(parseInt(queryParams.get("skip") || "0")) - const [limit, setLimit] = useState(parseInt(queryParams.get("limit") || "10")) - const [searchQuery, setSearchQuery] = useState(queryParams.get("search") || "") - const [selectedPost, setSelectedPost] = useState(null) - const [sortBy, setSortBy] = useState(queryParams.get("sortBy") || "") - const [sortOrder, setSortOrder] = useState(queryParams.get("sortOrder") || "asc") - const [showAddDialog, setShowAddDialog] = useState(false) - const [showEditDialog, setShowEditDialog] = useState(false) - const [newPost, setNewPost] = useState({ title: "", body: "", userId: 1 }) - const [loading, setLoading] = useState(false) - const [tags, setTags] = useState([]) - const [selectedTag, setSelectedTag] = useState(queryParams.get("tag") || "") - const [comments, setComments] = useState({}) - const [selectedComment, setSelectedComment] = useState(null) - const [newComment, setNewComment] = useState({ body: "", postId: null, userId: 1 }) - const [showAddCommentDialog, setShowAddCommentDialog] = useState(false) - const [showEditCommentDialog, setShowEditCommentDialog] = useState(false) - const [showPostDetailDialog, setShowPostDetailDialog] = useState(false) - const [showUserModal, setShowUserModal] = useState(false) - const [selectedUser, setSelectedUser] = useState(null) - - // URL 업데이트 함수 - const updateURL = () => { - const params = new URLSearchParams() - if (skip) params.set("skip", skip.toString()) - if (limit) params.set("limit", limit.toString()) - if (searchQuery) params.set("search", searchQuery) - if (sortBy) params.set("sortBy", sortBy) - if (sortOrder) params.set("sortOrder", sortOrder) - if (selectedTag) params.set("tag", selectedTag) - navigate(`?${params.toString()}`) - } - - // 게시물 가져오기 - const fetchPosts = () => { - setLoading(true) - let postsData - let usersData - - fetch(`/api/posts?limit=${limit}&skip=${skip}`) - .then((response) => response.json()) - .then((data) => { - postsData = data - return fetch("/api/users?limit=0&select=username,image") - }) - .then((response) => response.json()) - .then((users) => { - usersData = users.users - const postsWithUsers = postsData.posts.map((post) => ({ - ...post, - author: usersData.find((user) => user.id === post.userId), - })) - setPosts(postsWithUsers) - setTotal(postsData.total) - }) - .catch((error) => { - console.error("게시물 가져오기 오류:", error) - }) - .finally(() => { - setLoading(false) - }) - } - - // 태그 가져오기 - const fetchTags = async () => { - try { - const response = await fetch("/api/posts/tags") - const data = await response.json() - setTags(data) - } catch (error) { - console.error("태그 가져오기 오류:", error) - } - } - - // 게시물 검색 - const searchPosts = async () => { - if (!searchQuery) { - fetchPosts() - return - } - setLoading(true) - try { - const response = await fetch(`/api/posts/search?q=${searchQuery}`) - const data = await response.json() - setPosts(data.posts) - setTotal(data.total) - } catch (error) { - console.error("게시물 검색 오류:", error) - } - setLoading(false) - } - - // 태그별 게시물 가져오기 - const fetchPostsByTag = async (tag) => { - if (!tag || tag === "all") { - fetchPosts() - return - } - setLoading(true) - try { - const [postsResponse, usersResponse] = await Promise.all([ - fetch(`/api/posts/tag/${tag}`), - fetch("/api/users?limit=0&select=username,image"), - ]) - const postsData = await postsResponse.json() - const usersData = await usersResponse.json() - - const postsWithUsers = postsData.posts.map((post) => ({ - ...post, - author: usersData.users.find((user) => user.id === post.userId), - })) - - setPosts(postsWithUsers) - setTotal(postsData.total) - } catch (error) { - console.error("태그별 게시물 가져오기 오류:", error) - } - setLoading(false) - } - - // 게시물 추가 - const addPost = async () => { - try { - const response = await fetch("/api/posts/add", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newPost), - }) - const data = await response.json() - setPosts([data, ...posts]) - setShowAddDialog(false) - setNewPost({ title: "", body: "", userId: 1 }) - } catch (error) { - console.error("게시물 추가 오류:", error) - } - } - - // 게시물 업데이트 - const updatePost = async () => { - try { - const response = await fetch(`/api/posts/${selectedPost.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(selectedPost), - }) - const data = await response.json() - setPosts(posts.map((post) => (post.id === data.id ? data : post))) - setShowEditDialog(false) - } catch (error) { - console.error("게시물 업데이트 오류:", error) - } - } - - // 게시물 삭제 - const deletePost = async (id) => { - try { - await fetch(`/api/posts/${id}`, { - method: "DELETE", - }) - setPosts(posts.filter((post) => post.id !== id)) - } catch (error) { - console.error("게시물 삭제 오류:", error) - } - } - - // 댓글 가져오기 - const fetchComments = async (postId) => { - if (comments[postId]) return // 이미 불러온 댓글이 있으면 다시 불러오지 않음 - try { - const response = await fetch(`/api/comments/post/${postId}`) - const data = await response.json() - setComments((prev) => ({ ...prev, [postId]: data.comments })) - } catch (error) { - console.error("댓글 가져오기 오류:", error) - } - } - - // 댓글 추가 - const addComment = async () => { - try { - const response = await fetch("/api/comments/add", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newComment), - }) - const data = await response.json() - setComments((prev) => ({ - ...prev, - [data.postId]: [...(prev[data.postId] || []), data], - })) - setShowAddCommentDialog(false) - setNewComment({ body: "", postId: null, userId: 1 }) - } catch (error) { - console.error("댓글 추가 오류:", error) - } - } - - // 댓글 업데이트 - const updateComment = async () => { - try { - const response = await fetch(`/api/comments/${selectedComment.id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ body: selectedComment.body }), - }) - const data = await response.json() - setComments((prev) => ({ - ...prev, - [data.postId]: prev[data.postId].map((comment) => (comment.id === data.id ? data : comment)), - })) - setShowEditCommentDialog(false) - } catch (error) { - console.error("댓글 업데이트 오류:", error) - } - } - - // 댓글 삭제 - const deleteComment = async (id, postId) => { - try { - await fetch(`/api/comments/${id}`, { - method: "DELETE", - }) - setComments((prev) => ({ - ...prev, - [postId]: prev[postId].filter((comment) => comment.id !== id), - })) - } catch (error) { - console.error("댓글 삭제 오류:", error) - } - } - - // 댓글 좋아요 - const likeComment = async (id, postId) => { - try { - - const response = await fetch(`/api/comments/${id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ likes: comments[postId].find((c) => c.id === id).likes + 1 }), - }) - const data = await response.json() - setComments((prev) => ({ - ...prev, - [postId]: prev[postId].map((comment) => (comment.id === data.id ? {...data, likes: comment.likes + 1} : comment)), - })) - } catch (error) { - console.error("댓글 좋아요 오류:", error) - } - } - - // 게시물 상세 보기 - const openPostDetail = (post) => { - setSelectedPost(post) - fetchComments(post.id) - setShowPostDetailDialog(true) - } - - // 사용자 모달 열기 - const openUserModal = async (user) => { - try { - const response = await fetch(`/api/users/${user.id}`) - const userData = await response.json() - setSelectedUser(userData) - setShowUserModal(true) - } catch (error) { - console.error("사용자 정보 가져오기 오류:", error) - } - } - - useEffect(() => { - fetchTags() - }, []) - - useEffect(() => { - if (selectedTag) { - fetchPostsByTag(selectedTag) - } else { - fetchPosts() - } - updateURL() - }, [skip, limit, sortBy, sortOrder, selectedTag]) - - useEffect(() => { - const params = new URLSearchParams(location.search) - setSkip(parseInt(params.get("skip") || "0")) - setLimit(parseInt(params.get("limit") || "10")) - setSearchQuery(params.get("search") || "") - setSortBy(params.get("sortBy") || "") - setSortOrder(params.get("sortOrder") || "asc") - setSelectedTag(params.get("tag") || "") - }, [location.search]) - - // 하이라이트 함수 추가 - const highlightText = (text: string, highlight: string) => { - if (!text) return null - if (!highlight.trim()) { - return {text} - } - const regex = new RegExp(`(${highlight})`, "gi") - const parts = text.split(regex) - return ( - - {parts.map((part, i) => (regex.test(part) ? {part} : {part}))} - - ) - } - - // 게시물 테이블 렌더링 - const renderPostTable = () => ( - - - - ID - 제목 - 작성자 - 반응 - 작업 - - - - {posts.map((post) => ( - - {post.id} - -
-
{highlightText(post.title, searchQuery)}
- -
- {post.tags?.map((tag) => ( - { - setSelectedTag(tag) - updateURL() - }} - > - {tag} - - ))} -
-
-
- -
openUserModal(post.author)}> - {post.author?.username} - {post.author?.username} -
-
- -
- - {post.reactions?.likes || 0} - - {post.reactions?.dislikes || 0} -
-
- -
- - - -
-
-
- ))} -
-
- ) - - // 댓글 렌더링 - const renderComments = (postId) => ( -
-
-

댓글

- -
-
- {comments[postId]?.map((comment) => ( -
-
- {comment.user.username}: - {highlightText(comment.body, searchQuery)} -
-
- - - -
-
- ))} -
-
- ) - - return ( - - - - 게시물 관리자 - - - - -
- {/* 검색 및 필터 컨트롤 */} -
-
-
- - setSearchQuery(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && searchPosts()} - /> -
-
- - - -
- - {/* 게시물 테이블 */} - {loading ?
로딩 중...
: renderPostTable()} - - {/* 페이지네이션 */} -
-
- 표시 - - 항목 -
-
- - -
-
-
-
- - {/* 게시물 추가 대화상자 */} - - - - 새 게시물 추가 - -
- setNewPost({ ...newPost, title: e.target.value })} - /> -