Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"jotai": "^2.13.1",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
Expand Down
42 changes: 30 additions & 12 deletions pnpm-lock.yaml

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

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/ui/Header.tsx"
import Footer from "./widgets/ui/Footer.tsx"
import PostsManagerPage from "./pages/PostsManagerPage.tsx"

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

// 특정 게시물의 댓글을 가져오는 API 함수
export const getCommentsByPostIdApi = async (postId: number) => {
const response = await fetch(`/api/comments/post/${postId}`)
if (!response.ok) {
throw new Error("댓글을 가져오는데 실패했습니다.")
}
return response
}

// 댓글 추가 API 함수
export const addCommentApi = async (newComment: Comment) => {
const response = await fetch("/api/comments/add", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newComment),
})
if (!response.ok) {
throw new Error("댓글 추가에 실패했습니다.")
}
return response
}

// 댓글 수정 API 함수
export const updateCommentApi = async (comment: Comment) => {
const response = await fetch(`/api/comments/${comment.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: comment.body }),
})
if (!response.ok) {
throw new Error("댓글 수정에 실패했습니다.")
}
return response
}

// 댓글 삭제 API 함수
export const deleteCommentApi = async (id: string) => {
const response = await fetch(`/api/comments/${id}`, {
method: "DELETE",
})
if (!response.ok) {
throw new Error("댓글 삭제에 실패했습니다.")
}
return response
}

// 댓글 좋아요 API 함수
export const likeCommentApi = async (id: number, likes: number) => {
const response = await fetch(`/api/comments/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ likes }),
})
if (!response.ok) {
throw new Error("댓글 좋아요에 실패했습니다.")
}
return response
}
127 changes: 127 additions & 0 deletions src/entities/comment/hooks/useComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { useState } from "react"
import {
addCommentApi,
deleteCommentApi,
getCommentsByPostIdApi,
likeCommentApi,
updateCommentApi,
} from "../api/comment-api"
import { loadingAtom } from "../../../shared/model/store"
import {
commentsAtom,
selectedCommentAtom,
newCommentAtom,
showAddCommentDialogAtom,
showEditCommentDialogAtom,
} from "../model/store"
import { useAtom } from "jotai"

export const useComment = () => {
const [loading, setLoading] = useAtom(loadingAtom)

const [comments, setComments] = useAtom(commentsAtom)
const [selectedComment, setSelectedComment] = useAtom(selectedCommentAtom)
const [newComment, setNewComment] = useAtom(newCommentAtom)
const [showAddCommentDialog, setShowAddCommentDialog] = useAtom(showAddCommentDialogAtom)
const [showEditCommentDialog, setShowEditCommentDialog] = useAtom(showEditCommentDialogAtom)

// 댓글 가져오기
const fetchComments = async (postId) => {
if (comments[postId]) return
setLoading(true)
try {
const response = await getCommentsByPostIdApi(postId)
const data = await response.json()
setComments((prev) => ({ ...prev, [postId]: data.comments }))
} catch (error) {
console.error("댓글 가져오기 오류:", error)
}
setLoading(false)
}

// 댓글 추가
const addComment = async () => {
setLoading(true)
try {
const response = await addCommentApi(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)
}
setLoading(false)
}

// 댓글 수정
const updateComment = async () => {
setLoading(true)
try {
const response = await updateCommentApi(selectedComment)
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)
}
setLoading(false)
}

// 댓글 삭제
const deleteComment = async (id, postId) => {
setLoading(true)
try {
await deleteCommentApi(id)
setComments((prev) => ({
...prev,
[postId]: prev[postId].filter((comment) => comment.id !== id),
}))
} catch (error) {
console.error("댓글 삭제 오류:", error)
}
setLoading(false)
}

// 댓글 좋아요
const likeComment = async (id, postId) => {
setLoading(true)
try {
const currentLikes = Array.isArray(comments[postId]) ? (comments[postId].find((c) => c.id === id)?.likes ?? 0) : 0
const response = await likeCommentApi(id, currentLikes + 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)
}
setLoading(false)
}

return {
comments,
selectedComment,
setSelectedComment,
newComment,
setNewComment,
showAddCommentDialog,
setShowAddCommentDialog,
showEditCommentDialog,
setShowEditCommentDialog,
fetchComments,
addComment,
updateComment,
deleteComment,
likeComment,
}
}
16 changes: 16 additions & 0 deletions src/entities/comment/model/store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { atom } from "jotai"

// 댓글 목록을 저장하는 atom
export const commentsAtom = atom({})

// 선택된 댓글을 저장하는 atom
export const selectedCommentAtom = atom(null)

// 새 댓글 정보를 저장하는 atom
export const newCommentAtom = atom({ body: "", postId: null, userId: 1 })

// 댓글 추가 다이얼로그 표시 여부 atom
export const showAddCommentDialogAtom = atom(false)

// 댓글 수정 다이얼로그 표시 여부 atom
export const showEditCommentDialogAtom = atom(false)
17 changes: 17 additions & 0 deletions src/entities/comment/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// 댓글(Comment) 타입 정의

export interface Comment {
id: number
postId: number
userId: number
body: string
likes?: number
// author 필드는 API에서 join해서 넘길 때만 사용 (옵션)
author?: {
id: number
username: string
image?: string
}
createdAt?: string
updatedAt?: string
}
25 changes: 25 additions & 0 deletions src/entities/comment/ui/CommentAddDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Textarea } from "../../../widgets/ui"
import { useComment } from "../hooks/useComment"

// 게시물 테이블 렌더링
export const CommentAddDialog = () => {
const { newComment, setNewComment, showAddCommentDialog, setShowAddCommentDialog, addComment } = useComment()

return (
<Dialog open={showAddCommentDialog} onOpenChange={setShowAddCommentDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>새 댓글 추가</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="댓글 내용"
value={newComment.body}
onChange={(e) => setNewComment({ ...newComment, body: e.target.value })}
/>
<Button onClick={addComment}>댓글 추가</Button>
</div>
</DialogContent>
</Dialog>
)
}
26 changes: 26 additions & 0 deletions src/entities/comment/ui/CommentEditDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Textarea } from "../../../widgets/ui"
import { useComment } from "../hooks/useComment"

// 게시물 테이블 렌더링
export const CommentEditDialog = () => {
const { selectedComment, setSelectedComment, showEditCommentDialog, setShowEditCommentDialog, updateComment } =
useComment()

return (
<Dialog open={showEditCommentDialog} onOpenChange={setShowEditCommentDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>댓글 수정</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="댓글 내용"
value={selectedComment?.body || ""}
onChange={(e) => setSelectedComment({ ...selectedComment, body: e.target.value })}
/>
<Button onClick={updateComment}>댓글 업데이트</Button>
</div>
</DialogContent>
</Dialog>
)
}
Loading