From ed2065118c312912fc9b2768009a54a45fe5986e Mon Sep 17 00:00:00 2001 From: Suin Kim Date: Sun, 27 Apr 2025 16:44:11 +0900 Subject: [PATCH 01/32] =?UTF-8?q?chore:=20=EA=B8=B0=EB=8A=A5=EB=B3=84=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/PostsManagerPage.tsx | 199 ++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 88 deletions(-) diff --git a/src/pages/PostsManagerPage.tsx b/src/pages/PostsManagerPage.tsx index 9fa274db4..da7079508 100644 --- a/src/pages/PostsManagerPage.tsx +++ b/src/pages/PostsManagerPage.tsx @@ -31,41 +31,23 @@ const PostsManager = () => { const location = useLocation() const queryParams = new URLSearchParams(location.search) - // 상태 관리 + /** =============================================== */ + /** 애매(?) 공통(?) */ + /** =============================================== */ + const [loading, setLoading] = useState(false) + + /** =============================================== */ + /** 게시물 */ + /** =============================================== */ const [posts, setPosts] = useState([]) const [total, setTotal] = useState(0) const [skip, setSkip] = useState(parseInt(queryParams.get("skip") || "0")) const [limit, setLimit] = useState(parseInt(queryParams.get("limit") || "10")) - const [searchQuery, setSearchQuery] = useState(queryParams.get("search") || "") const [selectedPost, setSelectedPost] = useState(null) - const [sortBy, setSortBy] = useState(queryParams.get("sortBy") || "") - const [sortOrder, setSortOrder] = useState(queryParams.get("sortOrder") || "asc") const [showAddDialog, setShowAddDialog] = useState(false) const [showEditDialog, setShowEditDialog] = useState(false) const [newPost, setNewPost] = useState({ title: "", body: "", userId: 1 }) - const [loading, setLoading] = useState(false) - const [tags, setTags] = useState([]) - const [selectedTag, setSelectedTag] = useState(queryParams.get("tag") || "") - const [comments, setComments] = useState({}) - const [selectedComment, setSelectedComment] = useState(null) - const [newComment, setNewComment] = useState({ body: "", postId: null, userId: 1 }) - const [showAddCommentDialog, setShowAddCommentDialog] = useState(false) - const [showEditCommentDialog, setShowEditCommentDialog] = useState(false) const [showPostDetailDialog, setShowPostDetailDialog] = useState(false) - const [showUserModal, setShowUserModal] = useState(false) - const [selectedUser, setSelectedUser] = useState(null) - - // URL 업데이트 함수 - const updateURL = () => { - const params = new URLSearchParams() - if (skip) params.set("skip", skip.toString()) - if (limit) params.set("limit", limit.toString()) - if (searchQuery) params.set("search", searchQuery) - if (sortBy) params.set("sortBy", sortBy) - if (sortOrder) params.set("sortOrder", sortOrder) - if (selectedTag) params.set("tag", selectedTag) - navigate(`?${params.toString()}`) - } // 게시물 가져오기 const fetchPosts = () => { @@ -97,17 +79,6 @@ const PostsManager = () => { }) } - // 태그 가져오기 - const fetchTags = async () => { - try { - const response = await fetch("/api/posts/tags") - const data = await response.json() - setTags(data) - } catch (error) { - console.error("태그 가져오기 오류:", error) - } - } - // 게시물 검색 const searchPosts = async () => { if (!searchQuery) { @@ -126,34 +97,6 @@ const PostsManager = () => { setLoading(false) } - // 태그별 게시물 가져오기 - const fetchPostsByTag = async (tag) => { - if (!tag || tag === "all") { - fetchPosts() - return - } - setLoading(true) - try { - const [postsResponse, usersResponse] = await Promise.all([ - fetch(`/api/posts/tag/${tag}`), - fetch("/api/users?limit=0&select=username,image"), - ]) - const postsData = await postsResponse.json() - const usersData = await usersResponse.json() - - const postsWithUsers = postsData.posts.map((post) => ({ - ...post, - author: usersData.users.find((user) => user.id === post.userId), - })) - - setPosts(postsWithUsers) - setTotal(postsData.total) - } catch (error) { - console.error("태그별 게시물 가져오기 오류:", error) - } - setLoading(false) - } - // 게시물 추가 const addPost = async () => { try { @@ -199,6 +142,67 @@ const PostsManager = () => { } } + // 게시물 상세 보기 + const openPostDetail = (post) => { + setSelectedPost(post) + fetchComments(post.id) + setShowPostDetailDialog(true) + } + + /** =============================================== */ + /** 태그 */ + /** =============================================== */ + const [tags, setTags] = useState([]) + const [selectedTag, setSelectedTag] = useState(queryParams.get("tag") || "") + + // 태그 가져오기 + const fetchTags = async () => { + try { + const response = await fetch("/api/posts/tags") + const data = await response.json() + setTags(data) + } catch (error) { + console.error("태그 가져오기 오류:", error) + } + } + + // 태그별 게시물 가져오기 + const fetchPostsByTag = async (tag) => { + if (!tag || tag === "all") { + fetchPosts() + return + } + setLoading(true) + try { + const [postsResponse, usersResponse] = await Promise.all([ + fetch(`/api/posts/tag/${tag}`), + fetch("/api/users?limit=0&select=username,image"), + ]) + const postsData = await postsResponse.json() + const usersData = await usersResponse.json() + + const postsWithUsers = postsData.posts.map((post) => ({ + ...post, + author: usersData.users.find((user) => user.id === post.userId), + })) + + setPosts(postsWithUsers) + setTotal(postsData.total) + } catch (error) { + console.error("태그별 게시물 가져오기 오류:", error) + } + setLoading(false) + } + + /** =============================================== */ + /** 댓글 */ + /** =============================================== */ + const [comments, setComments] = useState({}) + const [selectedComment, setSelectedComment] = useState(null) + const [newComment, setNewComment] = useState({ body: "", postId: null, userId: 1 }) + const [showAddCommentDialog, setShowAddCommentDialog] = useState(false) + const [showEditCommentDialog, setShowEditCommentDialog] = useState(false) + // 댓글 가져오기 const fetchComments = async (postId) => { if (comments[postId]) return // 이미 불러온 댓글이 있으면 다시 불러오지 않음 @@ -268,7 +272,6 @@ const PostsManager = () => { // 댓글 좋아요 const likeComment = async (id, postId) => { try { - const response = await fetch(`/api/comments/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, @@ -277,19 +280,20 @@ const PostsManager = () => { const data = await response.json() setComments((prev) => ({ ...prev, - [postId]: prev[postId].map((comment) => (comment.id === data.id ? {...data, likes: comment.likes + 1} : comment)), + [postId]: prev[postId].map((comment) => + comment.id === data.id ? { ...data, likes: comment.likes + 1 } : comment, + ), })) } catch (error) { console.error("댓글 좋아요 오류:", error) } } - // 게시물 상세 보기 - const openPostDetail = (post) => { - setSelectedPost(post) - fetchComments(post.id) - setShowPostDetailDialog(true) - } + /** =============================================== */ + /** 사용자 정보(?) */ + /** =============================================== */ + const [showUserModal, setShowUserModal] = useState(false) + const [selectedUser, setSelectedUser] = useState(null) // 사용자 모달 열기 const openUserModal = async (user) => { @@ -303,6 +307,40 @@ const PostsManager = () => { } } + /** =============================================== */ + /** 검색 */ + /** =============================================== */ + const [searchQuery, setSearchQuery] = useState(queryParams.get("search") || "") + const [sortBy, setSortBy] = useState(queryParams.get("sortBy") || "") + const [sortOrder, setSortOrder] = useState(queryParams.get("sortOrder") || "asc") + + // 하이라이트 함수 추가 + const highlightText = (text: string, highlight: string) => { + if (!text) return null + if (!highlight.trim()) { + return {text} + } + const regex = new RegExp(`(${highlight})`, "gi") + const parts = text.split(regex) + return ( + + {parts.map((part, i) => (regex.test(part) ? {part} : {part}))} + + ) + } + + // URL 업데이트 함수 + const updateURL = () => { + const params = new URLSearchParams() + if (skip) params.set("skip", skip.toString()) + if (limit) params.set("limit", limit.toString()) + if (searchQuery) params.set("search", searchQuery) + if (sortBy) params.set("sortBy", sortBy) + if (sortOrder) params.set("sortOrder", sortOrder) + if (selectedTag) params.set("tag", selectedTag) + navigate(`?${params.toString()}`) + } + useEffect(() => { fetchTags() }, []) @@ -326,21 +364,6 @@ const PostsManager = () => { setSelectedTag(params.get("tag") || "") }, [location.search]) - // 하이라이트 함수 추가 - const highlightText = (text: string, highlight: string) => { - if (!text) return null - if (!highlight.trim()) { - return {text} - } - const regex = new RegExp(`(${highlight})`, "gi") - const parts = text.split(regex) - return ( - - {parts.map((part, i) => (regex.test(part) ? {part} : {part}))} - - ) - } - // 게시물 테이블 렌더링 const renderPostTable = () => ( From f127d27ddf6ce0430bcf5df4642a87e85637a4f0 Mon Sep 17 00:00:00 2001 From: Suin Kim Date: Sun, 27 Apr 2025 21:32:53 +0900 Subject: [PATCH 02/32] =?UTF-8?q?chore:=20App=20=EC=8B=9C=EC=9E=91?= =?UTF-8?q?=EC=A0=90=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 2 +- src/{ => app}/App.tsx | 6 +++--- src/{ => app}/index.css | 5 +++-- src/app/main.tsx | 10 ++++++++++ src/index.tsx | 12 ------------ src/main.tsx | 10 ---------- 6 files changed, 17 insertions(+), 28 deletions(-) rename src/{ => app}/App.tsx (70%) rename src/{ => app}/index.css (70%) create mode 100644 src/app/main.tsx delete mode 100644 src/index.tsx delete mode 100644 src/main.tsx diff --git a/index.html b/index.html index f89d6d351..04e761b48 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,6 @@
- + diff --git a/src/App.tsx b/src/app/App.tsx similarity index 70% rename from src/App.tsx rename to src/app/App.tsx index 82d35d55b..93dbc6a54 100644 --- a/src/App.tsx +++ b/src/app/App.tsx @@ -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/ui/Header.tsx" +import Footer from "../widgets/ui/Footer.tsx" +import PostsManagerPage from "../pages/PostsManagerPage.tsx" const App = () => { return ( diff --git a/src/index.css b/src/app/index.css similarity index 70% rename from src/index.css rename to src/app/index.css index 62d46a326..8e1047cca 100644 --- a/src/index.css +++ b/src/app/index.css @@ -1,4 +1,5 @@ -html, body { +html, +body { background: #fff; color: #000; -} \ No newline at end of file +} diff --git a/src/app/main.tsx b/src/app/main.tsx new file mode 100644 index 000000000..86e0ef88d --- /dev/null +++ b/src/app/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react" +import { createRoot } from "react-dom/client" +import "./index.css" +import App from "./App.tsx" + +createRoot(document.getElementById("root")!).render( + + + , +) diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 369e197bb..000000000 --- a/src/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import { BrowserRouter as Router } from "react-router-dom" -import App from "./App" - -ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - , -) diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index bef5202a3..000000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' - -createRoot(document.getElementById('root')!).render( - - - , -) From 239b946f87e7660c68f8ec086575986fd06f4469 Mon Sep 17 00:00:00 2001 From: Suin Kim Date: Mon, 28 Apr 2025 01:13:48 +0900 Subject: [PATCH 03/32] =?UTF-8?q?fix:=20shared/ui=20index.tsx=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EB=B0=8F=20=EA=B0=81=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=ED=83=80=EC=9E=85=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/shared/ui/Button/Button.tsx | 38 ++++ src/shared/ui/Card/Card.tsx | 11 ++ src/shared/ui/CardContent/CardContent.tsx | 10 + src/shared/ui/CardHeader/CardHeader.tsx | 11 ++ src/shared/ui/CardTitle/CardTitle.tsx | 10 + src/shared/ui/Dialog/Dialog.tsx | 57 ++++++ src/shared/ui/Input/Input.tsx | 17 ++ src/shared/ui/Select/Select.tsx | 68 +++++++ src/shared/ui/Table/Table.tsx | 13 ++ src/shared/ui/TableBody/TableBody.tsx | 11 ++ src/shared/ui/TableCell/TableCell.tsx | 11 ++ src/shared/ui/TableHead/TableHead.tsx | 15 ++ src/shared/ui/TableHeader/TableHeader.tsx | 11 ++ src/shared/ui/TableRow/TableRow.tsx | 15 ++ src/shared/ui/Textarea/Textarea.tsx | 17 ++ src/shared/ui/index.ts | 15 ++ src/shared/ui/index.tsx | 214 ---------------------- 17 files changed, 330 insertions(+), 214 deletions(-) create mode 100644 src/shared/ui/Button/Button.tsx create mode 100644 src/shared/ui/Card/Card.tsx create mode 100644 src/shared/ui/CardContent/CardContent.tsx create mode 100644 src/shared/ui/CardHeader/CardHeader.tsx create mode 100644 src/shared/ui/CardTitle/CardTitle.tsx create mode 100644 src/shared/ui/Dialog/Dialog.tsx create mode 100644 src/shared/ui/Input/Input.tsx create mode 100644 src/shared/ui/Select/Select.tsx create mode 100644 src/shared/ui/Table/Table.tsx create mode 100644 src/shared/ui/TableBody/TableBody.tsx create mode 100644 src/shared/ui/TableCell/TableCell.tsx create mode 100644 src/shared/ui/TableHead/TableHead.tsx create mode 100644 src/shared/ui/TableHeader/TableHeader.tsx create mode 100644 src/shared/ui/TableRow/TableRow.tsx create mode 100644 src/shared/ui/Textarea/Textarea.tsx create mode 100644 src/shared/ui/index.ts delete mode 100644 src/shared/ui/index.tsx diff --git a/src/shared/ui/Button/Button.tsx b/src/shared/ui/Button/Button.tsx new file mode 100644 index 000000000..76d828a8a --- /dev/null +++ b/src/shared/ui/Button/Button.tsx @@ -0,0 +1,38 @@ +import { forwardRef } from "react" +import { cva, VariantProps } from "class-variance-authority" + +interface IButtonProps extends React.ButtonHTMLAttributes, VariantProps { + className?: string +} + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", + { + variants: { + variant: { + default: "bg-blue-500 text-white hover:bg-blue-600", + destructive: "bg-red-500 text-white hover:bg-red-600", + outline: "border border-gray-300 bg-transparent text-gray-700 hover:bg-gray-100", + secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300", + ghost: "bg-transparent text-gray-700 hover:bg-gray-100", + link: "underline-offset-4 hover:underline text-blue-500", + }, + size: { + default: "h-10 py-2 px-4", + sm: "h-8 px-3 rounded-md text-xs", + lg: "h-11 px-8 rounded-md", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) + +export const Button = forwardRef(({ className, variant, size, ...props }, ref) => { + return
+ +)) + +Table.displayName = "Table" diff --git a/src/shared/ui/TableBody/TableBody.tsx b/src/shared/ui/TableBody/TableBody.tsx new file mode 100644 index 000000000..3b2bde116 --- /dev/null +++ b/src/shared/ui/TableBody/TableBody.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from "react" + +interface ITableBodyProps extends React.HTMLAttributes { + className?: string +} + +export const TableBody = forwardRef(({ className, ...props }, ref) => ( + +)) + +TableBody.displayName = "TableBody" diff --git a/src/shared/ui/TableCell/TableCell.tsx b/src/shared/ui/TableCell/TableCell.tsx new file mode 100644 index 000000000..4a1d13992 --- /dev/null +++ b/src/shared/ui/TableCell/TableCell.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from "react" + +interface ITableCellProps extends React.HTMLAttributes { + className?: string +} + +export const TableCell = forwardRef(({ className, ...props }, ref) => ( + +)) + +TableHeader.displayName = "TableHeader" diff --git a/src/shared/ui/TableRow/TableRow.tsx b/src/shared/ui/TableRow/TableRow.tsx new file mode 100644 index 000000000..9b44586cf --- /dev/null +++ b/src/shared/ui/TableRow/TableRow.tsx @@ -0,0 +1,15 @@ +import { forwardRef } from "react" + +interface ITableRowProps extends React.HTMLAttributes { + className?: string +} + +export const TableRow = forwardRef(({ className, ...props }, ref) => ( + +)) + +TableRow.displayName = "TableRow" diff --git a/src/shared/ui/Textarea/Textarea.tsx b/src/shared/ui/Textarea/Textarea.tsx new file mode 100644 index 000000000..b2ffbb13d --- /dev/null +++ b/src/shared/ui/Textarea/Textarea.tsx @@ -0,0 +1,17 @@ +import { forwardRef } from "react" + +interface ITextareaProps extends React.TextareaHTMLAttributes { + className?: string +} + +export const Textarea = forwardRef(({ className, ...props }, ref) => { + return ( +
+)) + +TableCell.displayName = "TableCell" diff --git a/src/shared/ui/TableHead/TableHead.tsx b/src/shared/ui/TableHead/TableHead.tsx new file mode 100644 index 000000000..f50d7f8e3 --- /dev/null +++ b/src/shared/ui/TableHead/TableHead.tsx @@ -0,0 +1,15 @@ +import { forwardRef } from "react" + +interface ITableHeadProps extends React.HTMLAttributes { + className?: string +} + +export const TableHead = forwardRef(({ className, ...props }, ref) => ( + +)) + +TableHead.displayName = "TableHead" diff --git a/src/shared/ui/TableHeader/TableHeader.tsx b/src/shared/ui/TableHeader/TableHeader.tsx new file mode 100644 index 000000000..78cdd6b83 --- /dev/null +++ b/src/shared/ui/TableHeader/TableHeader.tsx @@ -0,0 +1,11 @@ +import { forwardRef } from "react" + +interface ITableHeaderProps extends React.HTMLAttributes { + className?: string +} + +export const TableHeader = forwardRef(({ className, ...props }, ref) => ( +