Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2ea075b
Refactor : 기존 폴더 구조 변경
LEEYEONSEONG Apr 28, 2025
3542519
Add : shered ui components
LEEYEONSEONG Apr 30, 2025
c1a55fe
Add : zustand global state lib
LEEYEONSEONG Apr 30, 2025
781cd67
Add : entities post type, api, store
LEEYEONSEONG Apr 30, 2025
5891e1f
Add : entities comment type, api, store
LEEYEONSEONG Apr 30, 2025
a2da74e
Add : entities user type, api
LEEYEONSEONG Apr 30, 2025
4f88da5
Add : post filters pannel feature component
LEEYEONSEONG Apr 30, 2025
0b510a3
Add : pagination feature component
LEEYEONSEONG Apr 30, 2025
3675719
Add : comment editor form feature component
LEEYEONSEONG Apr 30, 2025
eab044b
Add : comment list widget
LEEYEONSEONG Apr 30, 2025
e7ecfcc
Add : post detail dialog widget
LEEYEONSEONG Apr 30, 2025
845207d
Add : post table widget
LEEYEONSEONG Apr 30, 2025
fb1d073
Add : post manager page 수정
LEEYEONSEONG Apr 30, 2025
b9875bb
Add : tanstack react query lib
LEEYEONSEONG Apr 30, 2025
54cde1e
Add : axios client
LEEYEONSEONG Apr 30, 2025
14ddc2d
Add : queryClient provider
LEEYEONSEONG Apr 30, 2025
c1e1d2f
Fix : high light text
LEEYEONSEONG Apr 30, 2025
6292a52
Fix : add post default option
LEEYEONSEONG Apr 30, 2025
477c77b
Add : tanstack query setting
LEEYEONSEONG May 1, 2025
74fc906
Add : user detail feature component
LEEYEONSEONG May 1, 2025
cb7ba56
Fix : type
LEEYEONSEONG May 1, 2025
3cf3f13
Fix : add comment onSuccess option
LEEYEONSEONG May 1, 2025
a27049c
Add : post store total state
LEEYEONSEONG May 1, 2025
ebbd9df
Add : vercel.json
LEEYEONSEONG 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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"@tanstack/react-query": "^5.74.11",
"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 Down
46 changes: 45 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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 Header from "./widgets/layout/Header/Header.tsx"
import Footer from "./widgets/layout/Footer/Footer.tsx"
import PostsManagerPage from "./pages/PostManagerPage/PostsManagerPage.tsx"

const App = () => {
return (
Expand Down
32 changes: 32 additions & 0 deletions src/entities/comment/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { axiosClient } from "../../../shared/api/client"

import { Comment } from "../types"

export async function fetchComments(postId: number): Promise<Comment[]> {
const { data } = await axiosClient.get(`/api/comments/post/${postId}`)
return data.comments
}

export async function addComment(comment: {
body: string
postId: number
userId: number
likes: number
}): Promise<Comment> {
const { data } = await axiosClient.post("/api/comments/add", comment)
return data
}

export async function updateComment(id: number, body: string): Promise<Comment> {
const { data } = await axiosClient.put(`/api/comments/${id}`, { body })
return data
}

export async function deleteComment(id: number): Promise<void> {
await axiosClient.delete(`/api/comments/${id}`)
}

export async function likeComment(id: number, likes: number): Promise<Comment> {
const { data } = await axiosClient.patch(`/api/comments/${id}`, { likes })
return data
}
28 changes: 28 additions & 0 deletions src/entities/comment/model/commentStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { create } from "zustand"
import { Comment } from "../types"

interface CommentStoreState {
newComment: {
body: string
postId: number
userId: number
likes: number
}
selectedComment: Comment | null

setNewComment: (v: { body: string; postId: number; userId: number; likes: number }) => void
setSelectedComment: (c: Comment | null) => void
}

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

setNewComment: (v) => set({ newComment: v }),
setSelectedComment: (c) => set({ selectedComment: c }),
}))
70 changes: 70 additions & 0 deletions src/entities/comment/queries/useCommentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"

import { addComment, updateComment, deleteComment, likeComment } from "../api/api"
import { Comment } from "../types"

export const useAddCommentMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: addComment,
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["comments", data.postId] })
},
})
}

export const useUpdateCommentMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, body }: { id: number; body: string }) => updateComment(id, body),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["comments", data.postId] })
},
})
}

export const useDeleteCommentMutation = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: ({ id }: { id: number; postId: number }) => deleteComment(id),
onMutate: async ({ id, postId }) => {
await queryClient.cancelQueries({ queryKey: ["comments", postId] })

const previousComments = queryClient.getQueryData<Comment[]>(["comments", postId])

queryClient.setQueryData<Comment[]>(["comments", postId], (old = []) => old.filter((c) => c.id !== id))

return { previousComments }
},
})
}

export const useLikeCommentMutation = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: ({ id, likes }: { id: number; likes: number }) => likeComment(id, likes),
onMutate: async ({ id }) => {
// 옵티미스틱 UI
const matchingQuery = queryClient
.getQueryCache()
.findAll({ queryKey: ["comments"] })
.find((query) => {
const data = queryClient.getQueryData<Comment[]>(query.queryKey as [string, number])
return data?.some((comment) => comment.id === id)
})

const postId = (matchingQuery?.queryKey as [string, number])?.[1]

if (postId === undefined) return

const previousComments = queryClient.getQueryData<Comment[]>(["comments", postId])

queryClient.setQueryData<Comment[]>(["comments", postId], (old = []) =>
old.map((comment) => (comment.id === id ? { ...comment, likes: comment.likes + 1 } : comment)),
)

return { previousComments, postId }
},
})
}
10 changes: 10 additions & 0 deletions src/entities/comment/queries/useCommentQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useQuery } from "@tanstack/react-query"
import { fetchComments } from "../api/api"

export const useCommentsQuery = (postId: number) => {
return useQuery({
queryKey: ["comments", postId],
queryFn: () => fetchComments(postId),
enabled: !!postId,
})
}
10 changes: 10 additions & 0 deletions src/entities/comment/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface Comment {
id: number
body: string
postId: number
user: {
id: number
username: string
}
likes: number
}
56 changes: 56 additions & 0 deletions src/entities/post/api/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { axiosClient } from "../../../shared/api/client"

import { User } from "../../user/types"
import { Post, Tag } from "../types"

export async function fetchPosts(skip: number, limit: number): Promise<{ posts: Post[]; total: number }> {
const { data: postData } = await axiosClient.get(`/api/posts?limit=${limit}&skip=${skip}`)
const { data: userData } = await axiosClient.get("/api/users?limit=0&select=username,image")

const postsWithAuthor = postData.posts.map((post: Post) => ({
...post,
author: userData.users.find((u: User) => u.id === post.userId),
}))

return { posts: postsWithAuthor, total: postData.total }
}

export async function fetchPostsByTag(tag: string): Promise<{ posts: Post[]; total: number }> {
const { data: postData } = await axiosClient.get(`/api/posts/tag/${tag}`)
const { data: userData } = await axiosClient.get("/api/users?limit=0&select=username,image")

const postsWithAuthor = postData.posts.map((post: Post) => ({
...post,
author: userData.users.find((u: User) => u.id === post.userId),
}))

return { posts: postsWithAuthor, total: postData.total }
}

export async function fetchPostsBySearch(query: string): Promise<{ posts: Post[]; total: number }> {
const { data } = await axiosClient.get(`/api/posts/search?q=${query}`)
return data
}

export async function fetchTags(): Promise<Tag[]> {
const { data } = await axiosClient.get("/api/posts/tags")
return data
}

export async function addPost(post: Omit<Post, "id">): Promise<Post> {
const payload = {
...post,
userId: 1, // 기본 유저 설정
}
const { data } = await axiosClient.post("/api/posts/add", payload)
return data
}

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

export async function deletePost(id: number): Promise<void> {
await axiosClient.delete(`/api/posts/${id}`)
}
45 changes: 45 additions & 0 deletions src/entities/post/model/postStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// src/entities/post/model/postStore.ts
import { create } from "zustand"
import { Post } from "../types"

interface PostStoreState {
selectedPost: Post | null
total: number
newPost: Omit<Post, "id">
selectedTag: string
searchQuery: string
sortBy: string
sortOrder: string
skip: number
limit: number

setSelectedPost: (post: Post | null) => void
setNewPost: (post: Omit<Post, "id">) => void
setSelectedTag: (tag: string) => void
setSearchQuery: (query: string) => void
setSortBy: (value: string) => void
setSortOrder: (value: string) => void
setSkip: (v: number) => void
setLimit: (v: number) => void
}

export const usePostStore = create<PostStoreState>((set) => ({
selectedPost: null,
total: 0,
newPost: { title: "", body: "", userId: 1 },
selectedTag: "",
searchQuery: "",
sortBy: "",
sortOrder: "asc",
skip: 0,
limit: 10,

setSelectedPost: (post) => set({ selectedPost: post }),
setNewPost: (post) => set({ newPost: post }),
setSelectedTag: (tag) => set({ selectedTag: tag }),
setSearchQuery: (query) => set({ searchQuery: query }),
setSortBy: (value) => set({ sortBy: value }),
setSortOrder: (value) => set({ sortOrder: value }),
setSkip: (v) => set({ skip: v }),
setLimit: (v) => set({ limit: v }),
}))
Loading