Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
04c08c3
refactor: shared/ui폴더 내에 Card관련 컴포넌트로 분리함
Apr 28, 2025
b735c6a
refactor(shared/ui):Button컴포넌트 따로 분리
Apr 28, 2025
9b2cc14
refactor(shared/ui):Input컴포넌트 따로 분리
Apr 28, 2025
1130ee1
refactor(shared/ui):TextArea컴포넌트 따로 분리
Apr 28, 2025
133547f
refactor(shared/ui):table관련 컴포넌트 분리
Apr 28, 2025
f391c72
refactor(shared/ui):Select관련 컴포넌트 분리
Apr 28, 2025
d868ebe
refactor(shared/ui):Dialog관련 컴포넌트 분리
Apr 28, 2025
58ce50b
refactor: import하는 부분 수정
Apr 28, 2025
3601f6f
refactor(features/ui):게시물 추가 헤더 따로 컴포넌트로 분리
Apr 28, 2025
67a0866
refactor(features/ui):comment 댓글 수정 컴포넌트로 분리
Apr 28, 2025
5a4e049
refactor(features/ui): 게시물 상세보기 대화상자 컴포넌트로 분리
Apr 28, 2025
0c8aca2
refactor(features/ui):게시물 수정 대화상자 컴포넌트로 분리
Apr 28, 2025
21e1890
refactor(features/ui):댓글 추가 대화상자 컴포넌트로 분리
Apr 28, 2025
b433b44
refactor(features/pagination/ui): 페이지네이션 컴포넌트로 분리
Apr 28, 2025
24327e5
refactor(features/post/ui):게시물 추가 대화상자 컴포넌트로 분리
Apr 28, 2025
9b05545
refactor(features/post/ui):검색필터로 컴포넌트 분리
Apr 28, 2025
9c166c7
refactor(entities/user/ui):사용자 모달 컴포넌트로 분리
Apr 28, 2025
4e123e8
refactor(features/post/ui):게시물 테이블 컴포넌트로 분리
Apr 28, 2025
5528738
refactor(features/comment/ui):댓글렌더링부분 컴포넌트로 분리
Apr 29, 2025
9dada74
refactor(shared/lib):hightlightText함수 따로 분리
Apr 29, 2025
5d0985d
refactor:index.ts추가하여 깔끔하게 import할 수 있게 수정
Apr 29, 2025
3ec5fe0
refactor(entities/post/model): zustand로 store만들어서 상태관리할 수 있게 하기
Apr 29, 2025
e309e43
refactor(entities/user/model):userStore만들어서 상태관련 코드 분리
Apr 29, 2025
884b645
refactor(entities/comment/model):useCommentStore만들어서 관련 상태코드 분리
Apr 29, 2025
f63e586
refactor:store에 있는 타입 따로 파일 만들어서 분리
Apr 29, 2025
9df2f41
refactor(feature/post/model):usePostUpdateURL훅 만들어서 사용할 수 있게 하기
Apr 29, 2025
a91cb6a
refactor:postmanagerPage에 있던 함수들 컴포넌트안에서 호출되게 분리
Apr 29, 2025
bc5faf8
refactor(shared/ui):로딩컴포넌트로 분리
Apr 29, 2025
25f06fb
refactor: 각 컴포넌트에 있는 상태, store에서 가져올 수 있게 하기
Apr 29, 2025
d02e461
refactor: fetchTags를 entity 레이어로 이동 후, Tanstack Query 기반 useTagsQuery…
Apr 29, 2025
4564407
refactor: 게시물 추가 TanstackQuery기반 userAddPostMutation으로 호출
Apr 29, 2025
126b113
refactor: 게시물 업데이트 Tanstackquery기반 usePostUpdateMutation으로 호출되게 코드 변경
Apr 29, 2025
4892922
refactor:게시물 검색 tanstack query기반 usePostSearchQuery로 호출되게 코드 변경
Apr 30, 2025
d34d882
refactor:게시물, 태그별 게시물 가져오기 tanstack query로 호출해서 가져오도록 코드 변경
Apr 30, 2025
0a2bd23
refactor:paginationStore에서 setStateFromParams를 하나 만들어서 여러번 set되지 않게 하기
Apr 30, 2025
1d252a2
refactor:댓글추가tanstack query로 호출되게 코드 변경
Apr 30, 2025
f00998c
refactor: 댓글 업데이트 tanstack query로 호출되게 코드 변경
Apr 30, 2025
7895384
refactor: 댓글 삭제 tanstack query로 호출되게 코드 변경
Apr 30, 2025
ed3a2db
refactor:댓글 좋아요 부분 tanstackquery로 호출되게 코드 변경
Apr 30, 2025
4399acd
refactor:fetchComment하는 부분 재 수정
Apr 30, 2025
a64c806
refactor:게시물 불러오는 부분 코드 수정 at PostTable
Apr 30, 2025
6800588
fix: import에러나는 부분 해결하기 위해 코드 수정
Apr 30, 2025
a749bc5
refactor:CommentList 함수가 아니라 컴포넌트로 넘기도록 수정
Apr 30, 2025
e7a1184
fix: 댓글 추가 안되는 이슈 해결
Apr 30, 2025
da73e8c
fix: post 삭제 안되는 이슈 해결
Apr 30, 2025
8d49d8a
fix: 게시물 검색 테스트 통과 안되는 이슈 해결
May 1, 2025
1469b5d
fix: 게시물 추가 잘 안되는 이슈 해결
May 1, 2025
aaa144e
fix:댓글 좋아요 클릭 안되는 이슈 해결
May 1, 2025
6e75176
chore:배포 설정
May 1, 2025
93605ad
chore: 배포 시 api 요청 주소 변경
May 1, 2025
27bb40e
fix: 사용자 정보 제대로 안들어오고 있는 부분 해결
May 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"deploy": "gh-pages -d dist",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage"
},
"dependencies": {
"@tanstack/react-query": "^5.74.7",
"@tanstack/react-query-devtools": "^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",
Expand All @@ -22,6 +26,7 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/axios": "^0.14.4",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
Expand All @@ -30,6 +35,7 @@
"eslint": "^9.25.1",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"gh-pages": "^6.3.0",
"globals": "^16.0.0",
"jsdom": "^26.1.0",
"lucide-react": "^0.503.0",
Expand Down
290 changes: 289 additions & 1 deletion pnpm-lock.yaml

Large diffs are not rendered by default.

27 changes: 16 additions & 11 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
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"
import Footer from "./widgets/ui/Footer.tsx"
import Header from "./widgets/ui/Header.tsx"

const App = () => {
const queryClient = new QueryClient()

return (
<Router>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
<PostsManagerPage />
</main>
<Footer />
</div>
</Router>
<QueryClientProvider client={queryClient}>
<Router>
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
<PostsManagerPage />
</main>
<Footer />
</div>
</Router>
</QueryClientProvider>
)
}

Expand Down
36 changes: 36 additions & 0 deletions src/entities/comment/api/CommentAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
Comment,
CommentsResponse,
DeleteCommentParams,
NewComment,
UpdateCommentParams,
} from "@/entities/comment/model/CommentType"
import { axiosInstance } from "@/shared/api/axiosInstance"
import axios from "axios"

export const addComment = async (newComment: NewComment): Promise<Comment> => {
const response = await axiosInstance.post("/api/comments/add", newComment)
return response.data
}

export const updateComment = async ({ id, body }: UpdateCommentParams) => {
const response = await axiosInstance.put(`/api/comments/${id}`, { body })
return response.data
}

export const deleteComment = async ({ id }: DeleteCommentParams) => {
const response = await axiosInstance.delete(`/api/comments/${id}`)
return response.data
}

export const likeComment = async ({ id, likes, postId }: { id: number; likes: number; postId: number }) => {
await axiosInstance.patch(`/api/comments/${id}`, {
likes: likes + 1,
})
return { id, postId, likes: likes + 1 }
}

export const fetchComments = async (postId: number): Promise<CommentsResponse> => {
const response = await axios.get(`/api/comments/post/${postId}`)
return response.data
}
39 changes: 39 additions & 0 deletions src/entities/comment/model/CommentStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { create } from "zustand"
import { Comment } from "./CommentType"

interface CommentStore {
newComment: {
body: string
postId: number | null
likes: number
userId: number
}
selectedComment: Comment | null
showAddCommentDialog: boolean
showEditCommentDialog: boolean

setNewComment: (comment: Partial<CommentStore["newComment"]>) => void
setSelectedComment: (comment: Comment | null) => void
setShowAddCommentDialog: (open: boolean) => void
setShowEditCommentDialog: (open: boolean) => void
}

export const useCommentStore = create<CommentStore>((set) => ({
newComment: {
body: "",
likes: 0,
postId: null,
userId: 1,
},
selectedComment: null,
showAddCommentDialog: false,
showEditCommentDialog: false,

setNewComment: (comment) =>
set((state) => ({
newComment: { ...state.newComment, ...comment },
})),
setSelectedComment: (comment) => set({ selectedComment: comment }),
setShowAddCommentDialog: (open) => set({ showAddCommentDialog: open }),
setShowEditCommentDialog: (open) => set({ showEditCommentDialog: open }),
}))
36 changes: 36 additions & 0 deletions src/entities/comment/model/CommentType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export interface Comment {
id: number
body: string
postId: number
userId: number
likes: number
user: {
id: number
username: string
fullName: string
}
}

export type NewComment = {
body: string
postId: number | null
likes: number
userId: number
}

export type UpdateCommentParams = {
id: number
body: string
}

export type DeleteCommentParams = {
id: number
postId: number
}

export interface CommentsResponse {
comments: Comment[]
total: number
skip: number
limit: number
}
92 changes: 92 additions & 0 deletions src/entities/post/api/PostAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Post, PostWithAuthor } from "@/entities/post/model/PostType"
import { getUsers } from "@/entities/user/api/UserAPI"
import { axiosInstance } from "@/shared/api/axiosInstance"
import axios from "axios"

export const fetchTags = async () => {
try {
const response = await axios.get("/api/posts/tags")
return response.data
} catch (error) {
console.error("태그 가져오기 오류:", error)
}
}

export const createPost = async (post: { title: string; body: string; userId: number }) => {
const response = await axiosInstance.post("/api/posts/add", post)
return response.data
}

export const updatePost = async (post: Post) => {
const response = await axiosInstance.put(`/api/posts/${post.id}`, post)
return response.data
}

export const searchPosts = async (query: string) => {
const response = await axios.get(`/api/posts/search?q=${query}`)
return response.data
}

//post 전체 조회
export const getPosts = async ({ limit, skip }: { limit: number; skip: number }) => {
const response = await axios.get<{
total: number
posts: Post[]
}>("/api/posts", { params: { limit, skip } })
return response.data
}

export const deletePost = async (id: number): Promise<void> => {
const response = await axiosInstance.delete(`/api/posts/${id}`)
return response.data
}

//태그별 조회
export const getPostsByTag = async (tag: string) => {
const response = await axios.get<{
total: number
posts: Post[]
}>(`/api/posts/tag/${tag}`)
return response.data
}

// 게시물 가져오기
const fetchPosts = async ({ limit, skip }: { limit: number; skip: number }) => {
const [postsRes, user] = await Promise.all([getPosts({ limit, skip }), getUsers()])

const postsWithUsers = postsRes.posts.map((post) => ({
...post,
author: user.find((user) => user.id === post.userId),
}))

return {
posts: postsWithUsers,
total: postsRes.total,
}
}
// 태그별 게시물 가져오기 - 통합
export const fetchPostsByTag = async ({
tag,
limit,
skip,
}: {
tag: string
limit: number
skip: number
}): Promise<{ posts: PostWithAuthor[]; total: number }> => {
if (!tag || tag === "all") {
return fetchPosts({ limit, skip })
}

const [postsRes, users] = await Promise.all([getPostsByTag(tag), getUsers()])

const postsWithUsers = postsRes.posts.map((post: Post) => ({
...post,
author: users.find((user) => user.id === post.userId),
}))

return {
posts: postsWithUsers,
total: postsRes.total,
}
}
48 changes: 48 additions & 0 deletions src/entities/post/model/PostPaginationStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { create } from "zustand"

interface PostPaginationStore {
skip: number
limit: number
total: number
searchQuery: string
sortBy: string
sortOrder: "asc" | "desc"
selectedTag: string

setSkip: (value: number) => void
setLimit: (value: number) => void
setTotal: (value: number) => void
setSearchQuery: (value: string) => void
setSortBy: (value: string) => void
setSortOrder: (value: "asc" | "desc") => void
setSelectedTag: (tag: string) => void
setStateFromParams: (params: URLSearchParams) => void
}

export const usePostPaginationStore = create<PostPaginationStore>((set) => ({
skip: 0,
limit: 10,
total: 0,
searchQuery: "",
sortBy: "",
sortOrder: "asc",
selectedTag: "",

setSkip: (value) => set({ skip: value }),
setLimit: (value) => set({ limit: value }),
setTotal: (value) => set({ total: value }),
setSearchQuery: (value) => set({ searchQuery: value }),
setSortBy: (value) => set({ sortBy: value }),
setSortOrder: (value) => set({ sortOrder: value }),
setSelectedTag: (tag) => set({ selectedTag: tag }),
setStateFromParams: (params: URLSearchParams) => {
set({
skip: parseInt(params.get("skip") || "0"),
limit: parseInt(params.get("limit") || "10"),
searchQuery: params.get("search") || "",
sortBy: params.get("sortBy") || "",
sortOrder: (params.get("sortOrder") as "asc" | "desc") || "asc",
selectedTag: params.get("tag") || "",
})
},
}))
30 changes: 30 additions & 0 deletions src/entities/post/model/PostStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { create } from "zustand"
import { Post, PostWithAuthor } from "./PostType"

interface PostStore {
newPost: PostWithAuthor
selectedPost: Post | null
showAddDialog: boolean
showEditDialog: boolean
showPostDetailDialog: boolean

setNewPost: (post: PostWithAuthor) => void
setSelectedPost: (post: Post | null) => void
setShowAddDialog: (visible: boolean) => void
setShowEditDialog: (visible: boolean) => void
setShowPostDetailDialog: (visible: boolean) => void
}

export const usePostStore = create<PostStore>((set) => ({
newPost: { title: "", body: "", userId: 1 },
selectedPost: null,
showAddDialog: false,
showEditDialog: false,
showPostDetailDialog: false,

setNewPost: (post) => set({ newPost: post }),
setSelectedPost: (post) => set({ selectedPost: post }),
setShowAddDialog: (visible) => set({ showAddDialog: visible }),
setShowEditDialog: (visible) => set({ showEditDialog: visible }),
setShowPostDetailDialog: (visible) => set({ showPostDetailDialog: visible }),
}))
26 changes: 26 additions & 0 deletions src/entities/post/model/PostType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { User } from "@/entities/user/model/UserType"

export interface Post {
id?: number
title: string
body: string
userId: number
reactions?: {
likes: number
dislikes: number
}
tags?: string[] //TODO 타입 수정 필요
}

export type PostWithAuthor = Post & { author?: User }
export interface Tag {
url: string
slug: string
}

export interface PostResponse {
posts: Post[]
total: number
skip: number
limit: number
}
Loading