Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0ad612e
feat: shared/ui 폴더 생성 및 ui 관련 컴포넌트 생성
q1Lim Aug 11, 2025
a18efdb
refactor: Header, Footer widget으로 위치 변경
q1Lim Aug 12, 2025
22fffda
feat: Type (Post, Tag, User) 분리를 위한 필수 요소만 추가
q1Lim Aug 12, 2025
0809ca6
feat: highlightText 함수 분리
q1Lim Aug 12, 2025
ad3ea0c
feat: PostsTable 분리
q1Lim Aug 12, 2025
cf80dc3
chore: zustand 설치
q1Lim Aug 13, 2025
a46452b
feat: axios client 추가
q1Lim Aug 13, 2025
cd8a000
feat: post api 추가
q1Lim Aug 13, 2025
61f0b9b
feat: pagination 분리
q1Lim Aug 14, 2025
ef75f2b
feat: Post Filter 분리
q1Lim Aug 14, 2025
bec6c87
chore: import 배럴 정리
q1Lim Aug 14, 2025
7b67515
feat: PostForm Dialog 분리
q1Lim Aug 14, 2025
e6f0ab7
feat: UserModal 분리
q1Lim Aug 14, 2025
0a4bc9c
fix: 오탈자 및 export 수정
q1Lim Aug 14, 2025
06bbb59
feat: Type Comment 추가
q1Lim Aug 14, 2025
302938a
feat: CommentForm 추가
q1Lim Aug 14, 2025
2e43d7e
fix: widgets -> features 디렉토리로 이동
q1Lim Aug 14, 2025
4cce607
fix: PostsManagerPage에서 생성한 타입 반영
q1Lim Aug 14, 2025
64662b0
feat: PostDetail 분리
q1Lim Aug 14, 2025
6994e28
feat: Comment API 추가
q1Lim Aug 15, 2025
d9f686b
chore: tanstack/react-query 설치
q1Lim Aug 15, 2025
40587fc
feat: Provider 설정
q1Lim Aug 15, 2025
e97d5c2
feat: comment query 추가
q1Lim Aug 15, 2025
0b3f326
fix: bug fix
q1Lim Aug 15, 2025
14345e4
chore: 배럴 및 파일명 정리
q1Lim Aug 15, 2025
8eaa5e8
feat: 분리한 useComment로 댓글 CRUD 기능 동작하도록 반영
q1Lim Aug 15, 2025
40302a8
feat: useLikeCommentMutation에 낙관적 업데이트 적용
q1Lim Aug 16, 2025
17c44ea
fix: useLikeCommentMutation에 낙관적 업데이트 시 1이 올라가지 않는 현상 수정
q1Lim Aug 16, 2025
feb232d
chore: comment mutations 분리
q1Lim Aug 16, 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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"pnpm": "^10.14.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-select": "^2.2.5",
"@tanstack/react-query": "^5.85.3",
"@tanstack/react-query-devtools": "^5.85.3",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
Expand All @@ -40,6 +43,7 @@
"typescript-eslint": "^8.39.0",
"vite": "^7.1.1",
"vitest": "^3.2.4",
"vitest-browser-react": "^1.0.1"
"vitest-browser-react": "^1.0.1",
"zustand": "^5.0.7"
}
}
4 changes: 2 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BrowserRouter as Router } from "react-router-dom"
import Header from "./components/Header.tsx"
import Footer from "./components/Footer.tsx"
import Header from "./widgets/Header.tsx"
import Footer from "./widgets/Footer.tsx"
import PostsManagerPage from "./pages/PostsManagerPage.tsx"

const App = () => {
Expand Down
22 changes: 22 additions & 0 deletions src/entities/comment/api/commentApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Comment, CommentsResponse, CreateCommentRequest, UpdateCommentRequest } from "../model/types.ts"
import { api } from "../../../shared/api/axios"

export const getComments = async (postId: number): Promise<CommentsResponse> => {
return api.get(`/comments/post/${postId}`)
}

export const createComment = async (newComment: CreateCommentRequest): Promise<Comment> => {
return api.post("/comments/add", newComment)
}

export const updateComment = async (id: number, body: Pick<UpdateCommentRequest, "body">): Promise<Comment> => {
return api.put(`/comments/${id}`, body)
}

export const deleteComment = async (id: number): Promise<void> => {
return api.delete(`/comments/${id}`)
}

export const likeComment = async (id: number, currentLikes: number): Promise<Comment> => {
return api.patch(`/comments/${id}`, {likes: currentLikes + 1})
}
1 change: 1 addition & 0 deletions src/entities/comment/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./commentApi"
4 changes: 4 additions & 0 deletions src/entities/comment/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./queries"
export * from "./mutations"
export * from "./queryKeys"
export * from "./types"
95 changes: 95 additions & 0 deletions src/entities/comment/model/mutations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { createComment, deleteComment, likeComment, updateComment } from "../api"
import { Comments, CommentsResponse, CreateCommentRequest, LikeCommentRequest, UpdateCommentRequest } from "./types"
import { useMutation, useQueryClient } from "@tanstack/react-query"
import { commentQueryKeys } from "./queryKeys.ts"

// mutation
// TODO 낙관적 업데이트
export const useCreateCommentMutation = () => {
const queryClient = useQueryClient()

return useMutation({
mutationFn: (payload: CreateCommentRequest) => createComment(payload),
onSuccess: (response) => {
if(!response.postId) return
queryClient.setQueryData<Comments>(commentQueryKeys.list(response.postId), (old) =>
old ? { ...old, comments: [response, ...old.comments], total: (old.total ?? 0) + 1 } : old)
}
})
}

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

return useMutation({
mutationFn: (payload: UpdateCommentRequest ) => updateComment(payload.id, { body: payload.body }),

onSuccess: (response, { id }) => {
queryClient.setQueryData<Comments>(commentQueryKeys.list(id), (old) => {

if (!old) return old;
return {
...old,
comments: old.comments.map((comment) => (comment.id === response.id ? response : comment)),
}
})
},
})
}

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

return useMutation({
mutationFn: ({ id }: { id: number; postId: number }) => deleteComment(id),
onSuccess: (_response, { postId, id }) => {
queryClient.setQueryData<CommentsResponse>(commentQueryKeys.list(postId), (old) => {
if (!old) return old
return {
...old,
comments: old.comments.filter((comment) => comment.id !== id),
total: Math.max(0, (old.total ?? 0) - 1),
}
})
},
})
}

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

return useMutation({
mutationFn: async (payload: LikeCommentRequest) => likeComment(payload.id, payload.currentLikes),

// 낙관적 업데이트 적용
onMutate: async (payload) => {
const key = commentQueryKeys.list(payload.postId);

// 1) 진행 중 쿼리 중단
await queryClient.cancelQueries({ queryKey: key });

// 2) 스냅샷 저장(롤백용)
const previous = queryClient.getQueryData<CommentsResponse>(key);

// 3) 즉시 캐시 반영(+1)
queryClient.setQueryData<CommentsResponse>(key, (old) => {
if (!old) return old;
return {
...old,
comments: old.comments.map((comment) =>
comment.id === payload.id ? { ...comment, likes: (comment.likes ?? 0) + 1 } : comment
),
};
});
// 4) 롤백 컨텍스트 반환
return { key, previous };
},
onError: (error, variables, context) => {
if (context?.previous) {
queryClient.setQueryData(context.key, context.previous)
}
},
onSuccess: () => {
},
})
}
14 changes: 14 additions & 0 deletions src/entities/comment/model/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { Comments, CommentsResponse, CreateCommentRequest, LikeCommentRequest, UpdateCommentRequest } from "./types"
import { commentQueryKeys } from "./queryKeys.ts"
import { createComment, deleteComment, getComments, likeComment, updateComment } from "../api"

// query
export const useGetCommentsQuery = (postId: number) =>{

return useQuery<CommentsResponse>({
queryKey: commentQueryKeys.list(postId),
queryFn: () => getComments(postId),
enabled: !!postId
})
}
5 changes: 5 additions & 0 deletions src/entities/comment/model/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const commentQueryKeys = {
all: ["comments"] as const,
list: (postId: number) => [...commentQueryKeys.all, "list", {postId}] as const,
detail: (id: number) => [...commentQueryKeys.all, "detail", id] as const,
}
49 changes: 49 additions & 0 deletions src/entities/comment/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export interface Comment {
id: number
body: string
postId: number
userId: number
user: {
id: number
username: string
}
likes: number
}

export interface Comments {
comments: Comment[]
limit: number
skip: number
total: number
}

// api 호출용 타입
export interface NewCommentDraft {
body: string
postId: number | null
userId: number
}

export interface CreateCommentRequest {
body: string
postId: number
userId: number
}

export interface UpdateCommentRequest {
id: number
body: string
}

export interface LikeCommentRequest {
id: number
postId: number
currentLikes: number
}

export interface CommentsResponse {
comments: Comment[]
limit: number
skip: number
total: number
}
1 change: 1 addition & 0 deletions src/entities/post/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./postApi"
22 changes: 22 additions & 0 deletions src/entities/post/api/postApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Post, PostsResponse, UpdatePostRequest, CreatePostRequest } from "../model/types"
import { api } from "../../../shared/api/axios"

export const getPosts = async (limit: number, skip: number): Promise<PostsResponse> => {
return api.get(`/posts?limit=${limit}&skip=${skip}`)
}

export const getSearchPosts = async (searchQuery: string): Promise<PostsResponse> => {
return api.get(`/posts/search?q=${searchQuery}`)
}

export const getPostByTag = async (tag: string):Promise<PostsResponse> => {
return api.get(`/posts/tag/${tag}`)
}

export const createPost = async (newPost: CreatePostRequest): Promise<Post> => {
return api.post("/posts", newPost)
}

export const updatePost = async (id: number, updatePost: UpdatePostRequest): Promise<Post> => {
return api.put(`/posts/${id}`, updatePost)
}
34 changes: 34 additions & 0 deletions src/entities/post/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { User } from "../../user/model/types"

export interface Post {
id: number
title: string
body: string
userId: number
createdAt?: string
updatedAt?: string
reactions: {
likes: number
dislikes: number
}
author?: User
tags?: string[]
}

// api 호출 관련

export interface PostsResponse {
posts: Post[]
total?: number
}

export interface CreatePostRequest {
title: string
body: string
userId: number
}

export interface UpdatePostRequest {
title: string
body: string
}
7 changes: 7 additions & 0 deletions src/entities/tag/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Tag {
id: string
name: string
slug: string
url?: string
color?: string
}
23 changes: 23 additions & 0 deletions src/entities/user/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@

export interface User {
id: number
username: string
image?: string | null
}
// TODO UserModal 부분
export interface UserWithInfo extends User {
firstName: string
lastName: string
age: number
email: string
phone: string
address: {
address: string
city: string
state: string
}
company: {
name: string
title: string
}
}
1 change: 1 addition & 0 deletions src/features/comment/model/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useComment'
Loading