Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"coverage": "vitest run --coverage"
},
"dependencies": {
"pnpm": "^10.14.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
Expand Down Expand Up @@ -40,6 +41,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
11 changes: 11 additions & 0 deletions src/entities/comment/model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface Comment {
id: number
body: string
postId: number
userId: number
user: {
id: number
username: string
}
likes: number
}
22 changes: 22 additions & 0 deletions src/entities/post/api/api.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
}
}
42 changes: 42 additions & 0 deletions src/features/comment/ui/CommentForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Textarea } from "../../../shared/ui"
import React from "react"

interface CommentFormProps {
open: boolean
onChangeOpen: (open: boolean) => void
// 새 댓글 추가 or 댓글 수정
formTitle: string
bodyValue: string
onChangeBody: (value: string) => void
// 댓글 추가 or 댓글 업데이트
submitActionLabel: string
onSubmit: () => void
}

export const CommentForm: React.FC<CommentFormProps> = ({
open,
onChangeOpen,
formTitle,
bodyValue,
onChangeBody,
submitActionLabel,
onSubmit,
}) => {
return (
<Dialog open={open} onOpenChange={onChangeOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{formTitle}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Textarea
placeholder="댓글 내용"
value={bodyValue}
onChange={(e) => onChangeBody(e.target.value)}
/>
<Button onClick={onSubmit}>{submitActionLabel}</Button>
</div>
</DialogContent>
</Dialog>
)
}
6 changes: 6 additions & 0 deletions src/features/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { PostsTable } from "./post/ui/PostsTable"
export { PostForm } from "./post/ui/PostForm"
export { Pagination } from "./post/ui/Pagination"
export { PostsFilter } from "./post/ui/PostsFilter"
export { UserModal } from "./user/ui/UserModal"
export { CommentForm } from "./comment/ui/CommentForm"
2 changes: 2 additions & 0 deletions src/features/post/config/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// 페이지 개수 옵션
export const LIMIT_OPTIONS = [10, 20, 30]
49 changes: 49 additions & 0 deletions src/features/post/ui/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../shared/ui"
import { LIMIT_OPTIONS } from "../config/constants.ts"
import React from "react"

interface PaginationProps {
total: number
skip: number
limit: number
onClickPrev: () => void
onClickNext: () => void
onChangeLimit: (value: string) => void
}

export const Pagination: React.FC<PaginationProps> = ({ total, skip, limit, onClickPrev, onClickNext, onChangeLimit }) => {

const hasPrev = skip === 0
const hasNext = skip + limit >= total

return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<span>표시</span>
<Select value={limit.toString()} onValueChange={(value) => onChangeLimit(value)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="10" />
</SelectTrigger>
<SelectContent>
{
LIMIT_OPTIONS.map((option) => (
<SelectItem key={option} value={String(option)}>
{option}
</SelectItem>
))
}
</SelectContent>
</Select>
<span>항목</span>
</div>
<div className="flex gap-2">
<Button disabled={hasPrev} onClick={onClickPrev}>
이전
</Button>
<Button disabled={hasNext} onClick={onClickNext}>
다음
</Button>
</div>
</div>
)
}
68 changes: 68 additions & 0 deletions src/features/post/ui/PostForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, Textarea } from "../../../shared/ui"
import React from "react"

interface PostFormProps {
open: boolean
isNewPost: boolean
onChangeOpen: (open: boolean) => void
// 게시물 추가 or 게시물 수정
formTitle: string
userIdValue?: number
titleValue: string
contentValue: string
onChangeTitle: (value: string) => void
onChangeContent: (value: string) => void
onChangeUserId?: (value: number) => void
//게시물 추가 or 게시물 업데이트
submitActionLabel: string
onSubmit: () => void

}
export const PostForm: React.FC<PostFormProps> = ({
open,
isNewPost,
onChangeOpen,
formTitle,
userIdValue,
titleValue,
contentValue,
onChangeTitle,
onChangeContent,
onChangeUserId,
submitActionLabel,
onSubmit,

}) => {

return (
<Dialog open={open} onOpenChange={onChangeOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{formTitle}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<Input
placeholder="제목"
value={titleValue}
onChange={(e) => onChangeTitle(e.target.value)}
/>
<Textarea
rows={30}
placeholder="내용"
value={contentValue}
onChange={(e) => onChangeContent(e.target.value)}
/>
{isNewPost && (
<Input
type="number"
placeholder="사용자 ID"
value={userIdValue}
onChange={(e) => onChangeUserId?.(Number(e.target.value))}
/>
)}
<Button onClick={onSubmit}>{submitActionLabel}</Button>
</div>
</DialogContent>
</Dialog>
)
}
84 changes: 84 additions & 0 deletions src/features/post/ui/PostsFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Search } from "lucide-react"
import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../../shared/ui"
import React from "react"
import { Tag } from "../../../entities/tag/model/types.ts"

interface PostsFilterProps {
searchQuery: string
onChangeSearchQuery: (query: string) => void
onSearchPosts: () => void
selectedTag: string
sortBy: string
onChangeSortBy: (sortBy: string) => void
sortOrder: string
onChangeSortOrder: (sortOrder: string) => void
tags: Tag[]
onChangeTag: (value: string) => void
}

export const PostsFilter: React.FC<PostsFilterProps> = ({
searchQuery,
onChangeSearchQuery,
onSearchPosts,
selectedTag,
sortBy,
onChangeSortBy,
sortOrder,
onChangeSortOrder,
tags,
onChangeTag,
}) => {

return (
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="게시물 검색..."
className="pl-8"
value={searchQuery}
onChange={(e) => onChangeSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && onSearchPosts()}
/>
</div>
</div>
<Select
value={selectedTag}
onValueChange={onChangeTag}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="태그 선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">모든 태그</SelectItem>
{tags.map((tag) => (
<SelectItem key={tag.url} value={tag.slug}>
{tag.slug}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={sortBy} onValueChange={onChangeSortBy}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="정렬 기준" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">없음</SelectItem>
<SelectItem value="id">ID</SelectItem>
<SelectItem value="title">제목</SelectItem>
<SelectItem value="reactions">반응</SelectItem>
</SelectContent>
</Select>
<Select value={sortOrder} onValueChange={onChangeSortOrder}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="정렬 순서" />
</SelectTrigger>
<SelectContent>
<SelectItem value="asc">오름차순</SelectItem>
<SelectItem value="desc">내림차순</SelectItem>
</SelectContent>
</Select>
</div>
)
}
Loading