From 6746725e8f8eea06ec0ee87a516133b9aae30051 Mon Sep 17 00:00:00 2001 From: typhoon0678 Date: Sun, 9 Mar 2025 16:09:24 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B8=94=EB=A1=9C=EA=B7=B8=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=20api=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/apis/blog.tsx | 3 + src/common/apis/post.tsx | 18 +++- src/common/router/blogRouter.tsx | 6 +- src/common/router/page.tsx | 1 + src/common/types/post.tsx | 7 ++ src/common/util/string.tsx | 2 +- src/common/util/url.tsx | 6 +- src/components/blog/BlogSideBar.tsx | 32 +++---- src/components/blog/BlogTagCard.tsx | 10 ++- src/pages/blog/BlogPage.tsx | 41 ++++++--- src/pages/blog/BlogTagPage.tsx | 101 +++++++++++++++++++++++ src/pages/post/WritePage.tsx | 30 +++---- src/pages/setting/PostSettingPage.tsx | 25 +++++- src/pages/setting/ProfileSettingPage.tsx | 11 +-- 14 files changed, 233 insertions(+), 60 deletions(-) create mode 100644 src/pages/blog/BlogTagPage.tsx diff --git a/src/common/apis/blog.tsx b/src/common/apis/blog.tsx index c10a69f..417ef83 100644 --- a/src/common/apis/blog.tsx +++ b/src/common/apis/blog.tsx @@ -8,5 +8,8 @@ export const getMemberPosts = (postListMemberRequest: PostListMemberRequest) => export const getMemberFolders = (username: string) => axiosApi.get(`/folders/${username}`); +export const getMemberTags = (username: string) => + axiosApi.get(`/tag/${username}`); + export const saveAndUpdateFolder = (folderRequestList: FolderRequest[]) => axiosApi.put("/folders", folderRequestList); \ No newline at end of file diff --git a/src/common/apis/post.tsx b/src/common/apis/post.tsx index ff6a14c..83bc9c7 100644 --- a/src/common/apis/post.tsx +++ b/src/common/apis/post.tsx @@ -1,6 +1,17 @@ import axiosApi from "./AxiosApi.tsx"; -import {PostFolderRequest, PostListRequest, PostRequest, PostSearchRequest, PostUpdateRequest} from "../types/post.tsx"; -import {postListRequestToParameter, postListSearchRequestToParameter} from "../util/url.tsx"; +import { + PostFolderRequest, + PostListRequest, + PostListTagRequest, + PostRequest, + PostSearchRequest, + PostUpdateRequest +} from "../types/post.tsx"; +import { + postListRequestToParameter, + postListSearchRequestToParameter, + postListTagRequestToParameter +} from "../util/url.tsx"; export const createPost = async (postRequest: PostRequest) => await axiosApi.post("/post", postRequest); @@ -20,5 +31,8 @@ export const getPost = async (id: string) => export const getPosts = async (postListRequest: PostListRequest) => await axiosApi.get(`/post${postListRequestToParameter(postListRequest)}`); +export const getMemberTagPosts = async (postListTagRequest: PostListTagRequest) => + await axiosApi.get(`/post/member/tag${postListTagRequestToParameter(postListTagRequest)}`); + export const searchPost = async (postSearchRequest: PostSearchRequest) => await axiosApi.get(`/post/search${postListSearchRequestToParameter(postSearchRequest)}`); \ No newline at end of file diff --git a/src/common/router/blogRouter.tsx b/src/common/router/blogRouter.tsx index c5b6a3c..dd6f259 100644 --- a/src/common/router/blogRouter.tsx +++ b/src/common/router/blogRouter.tsx @@ -1,5 +1,5 @@ import {Suspense} from "react"; -import {Blog, Loading} from "./page.tsx"; +import {Blog, BlogTag, Loading} from "./page.tsx"; const blogRouter = () => { @@ -8,6 +8,10 @@ const blogRouter = () => { path: ':username', element: }, + { + path: ':username/tag', + element: + }, ]; } diff --git a/src/common/router/page.tsx b/src/common/router/page.tsx index 63bcab4..6b29228 100644 --- a/src/common/router/page.tsx +++ b/src/common/router/page.tsx @@ -28,3 +28,4 @@ export const Post = lazy(() => import('../../pages/post/PostPage.tsx')); export const Write = lazy(() => import('../../pages/post/WritePage.tsx')); export const Blog = lazy(() => import('../../pages/blog/BlogPage.tsx')); +export const BlogTag = lazy(() => import('../../pages/blog/BlogTagPage.tsx')); diff --git a/src/common/types/post.tsx b/src/common/types/post.tsx index c13a32a..3398dc3 100644 --- a/src/common/types/post.tsx +++ b/src/common/types/post.tsx @@ -33,6 +33,13 @@ export interface PostSearchRequest { isDescending: boolean, } +export interface PostListTagRequest { + username: string, + tagId: string, + page: number, + size: number, +} + export interface PostListResponse { content: PostResponse[]; page: PageResponse; diff --git a/src/common/util/string.tsx b/src/common/util/string.tsx index 6592c79..157db19 100644 --- a/src/common/util/string.tsx +++ b/src/common/util/string.tsx @@ -3,7 +3,7 @@ export const getFolderTitle = (title: string, depth: number) => { if (depth === 1) { prefix = "↳"; } else if (depth === 2) { - prefix = "-"; + prefix = "↳"; } return `${prefix} ${title}`; } \ No newline at end of file diff --git a/src/common/util/url.tsx b/src/common/util/url.tsx index 3eec18c..0078556 100644 --- a/src/common/util/url.tsx +++ b/src/common/util/url.tsx @@ -1,4 +1,4 @@ -import {PostListRequest, PostSearchRequest} from "../types/post.tsx"; +import {PostListRequest, PostListTagRequest, PostSearchRequest} from "../types/post.tsx"; import {CommentListRequest} from "../types/comment.tsx"; import {PostListMemberRequest} from "../types/blog.tsx"; import {MemberProfileSearchRequest} from "../types/member.tsx"; @@ -39,6 +39,10 @@ export const postListSearchRequestToParameter = (postSearchRequest: PostSearchRe return `?${sorts}&keyword=${postSearchRequest.keyword}&option=${postSearchRequest.option}&page=${postSearchRequest.page}&size=${postSearchRequest.size}&isDescending=${postSearchRequest.isDescending}`; } +export const postListTagRequestToParameter = (postListTagRequest: PostListTagRequest) => { + return `?username=${postListTagRequest.username}&tagId=${postListTagRequest.tagId}&page=${postListTagRequest.page}&size=${postListTagRequest.size}`; +} + export const memberProfileSearchRequestToParameter = (memberProfileSearchRequest: MemberProfileSearchRequest) => { return `?username=${memberProfileSearchRequest.username}&page=${memberProfileSearchRequest.page}&size=${memberProfileSearchRequest.size}`; } \ No newline at end of file diff --git a/src/components/blog/BlogSideBar.tsx b/src/components/blog/BlogSideBar.tsx index 8286839..4aebc2a 100644 --- a/src/components/blog/BlogSideBar.tsx +++ b/src/components/blog/BlogSideBar.tsx @@ -1,17 +1,18 @@ import {FolderType} from "../../common/types/blog.tsx"; import {useSelector} from "react-redux"; import {RootState} from "../../store.tsx"; -import {faker} from "@faker-js/faker/locale/ko"; import {OutlineLink} from "../common/OutlineButton.tsx"; import BlogTagCard from "./BlogTagCard.tsx"; import {getFolderTitle} from "../../common/util/string.tsx"; import ProfileImageCircle from "../common/ProfileImageCircle.tsx"; +import {TagResponse} from "../../common/types/post.tsx"; -function BlogSideBar({folders, username, profileUrl, addTag, setSelectedFolder, bgColor, side}: { +function BlogSideBar({folders, tags, username, profileUrl, selectedFolder, setSelectedFolder, bgColor, side}: { folders: FolderType[], + tags: TagResponse[], username: string | undefined, profileUrl: string | null, - addTag: (tagName: string) => void, + selectedFolder: FolderType | null, setSelectedFolder: (folder: FolderType) => void, bgColor?: string, side?: boolean, @@ -38,28 +39,28 @@ function BlogSideBar({folders, username, profileUrl, addTag, setSelectedFolder,
폴더
태그
- {[Array.from({length: 24}).map(() => ( - - ))]} + {tags.map((tag) => ( + + ))}
); } -function BlogFolderList({depth = 0, folders, setSelectedFolder}: { +function BlogFolderList({depth = 0, folders, selectedFolder, setSelectedFolder}: { depth?: number, folders: FolderType[], + selectedFolder: FolderType | null, setSelectedFolder: (folder: FolderType) => void, }) { @@ -83,16 +84,19 @@ function BlogFolderList({depth = 0, folders, setSelectedFolder}: { {depth === 0 &&
} )} diff --git a/src/components/blog/BlogTagCard.tsx b/src/components/blog/BlogTagCard.tsx index ac9c8e1..99f187e 100644 --- a/src/components/blog/BlogTagCard.tsx +++ b/src/components/blog/BlogTagCard.tsx @@ -1,14 +1,18 @@ import {TagResponse} from "../../common/types/post.tsx"; import {TextButton} from "../common/TextButton.tsx"; +import {useNavigate} from "react-router-dom"; -function BlogTagCard({tag, addTag}: { +function BlogTagCard({tag, username}: { tag: TagResponse, - addTag: (tagName: string) => void, + username: string, }) { + + const navigate = useNavigate(); + return ( addTag(tag.name)} + onClick={() => navigate(`/blog/${username}/tag`, {state: {tagId: tag.id}})} addStyle={"!px-1 !py-0 hover:text-lime-700"}/> ); } diff --git a/src/pages/blog/BlogPage.tsx b/src/pages/blog/BlogPage.tsx index 1224efc..6cb2885 100644 --- a/src/pages/blog/BlogPage.tsx +++ b/src/pages/blog/BlogPage.tsx @@ -8,10 +8,11 @@ import {FolderType, toFolderTypeList} from "../../common/types/blog.tsx"; import BlogSideBar from "../../components/blog/BlogSideBar.tsx"; import IconButton from "../../components/common/IconButton.tsx"; import {PageResponse} from "../../common/types/common.tsx"; -import {getMemberFolders, getMemberPosts} from "../../common/apis/blog.tsx"; -import {PostResponse} from "../../common/types/post.tsx"; +import {getMemberFolders, getMemberPosts, getMemberTags} from "../../common/apis/blog.tsx"; +import {PostResponse, TagResponse} from "../../common/types/post.tsx"; import {MemberProfileResponse} from "../../common/types/member.tsx"; import {getProfileByUsername} from "../../common/apis/member.tsx"; +import {TextLink} from "../../components/common/TextButton.tsx"; function BlogPage() { @@ -28,10 +29,10 @@ function BlogPage() { const [posts, setPosts] = useState([]); const [member, setMember] = useState({username: username || "", profileUrl: null}); const [folders, setFolders] = useState([]); + const [tags, setTags] = useState([]); const [isOpen, setIsOpen] = useState(false); const [selectedFolder, setSelectedFolder] = useState(null); - const [selectedTagList, setSelectedTagList] = useState([]); const sideBarRef = useRef(null); @@ -44,10 +45,6 @@ function BlogPage() { } }; - const addTag = (selectTag: string) => { - setSelectedTagList([...selectedTagList, selectTag]); - } - const handlePage = (page: number) => { setFolderParam({...folderParam, "page": page.toString()}); setPage(page); @@ -128,6 +125,15 @@ function BlogPage() { .then(res => { const folders = toFolderTypeList(res.data); setFolders(folders); + setFolders(prev => [ + { + id: "", + title: "전체", + postCount: 0, + subFolders: [] + }, + ...prev, + ]) setPage(Number(folderParam.get("page")) || 0); @@ -140,6 +146,12 @@ function BlogPage() { setSelectedFolder(initialSelectedFolder); } }); + + getMemberTags(username) + .then(res => { + setTags(res.data); + }) + .catch(error => alert(error.response.data.message)); }, [username]); useEffect(() => { @@ -164,15 +176,18 @@ function BlogPage() {
-
-
{username}의 블로그
+
+ } onClick={handleMenuOpen} addStyle="lg:hidden"/>
-
+
{posts.map((post) => (
@@ -204,9 +220,10 @@ function BlogPage() { diff --git a/src/pages/blog/BlogTagPage.tsx b/src/pages/blog/BlogTagPage.tsx new file mode 100644 index 0000000..164345f --- /dev/null +++ b/src/pages/blog/BlogTagPage.tsx @@ -0,0 +1,101 @@ +import BasicLayout from "../../layout/BasicLayout.tsx"; +import {useLocation, useNavigate, useParams} from "react-router-dom"; +import {useEffect, useState} from "react"; +import {PostResponse, TagResponse} from "../../common/types/post.tsx"; +import {PageResponse} from "../../common/types/common.tsx"; +import {getMemberTags} from "../../common/apis/blog.tsx"; +import TagCard from "../../components/post/TagCard.tsx"; +import PostCard from "../../components/post/PostCard.tsx"; +import {getMemberTagPosts} from "../../common/apis/post.tsx"; +import PaginationButton from "../../components/common/PaginationButton.tsx"; +import {TextLink} from "../../components/common/TextButton.tsx"; + +function BlogTagPage() { + + const {username} = useParams(); + const navigate = useNavigate(); + const {state} = useLocation(); + const {tagId} = state; + + + const [tags, setTags] = useState([]); + const [posts, setPosts] = useState([]); + const [selectedTag, setSelectedTag] = useState({ + id: tagId || "", + name: "", + }); + + const [page, setPage] = useState(0); + const [pageInfo, setPageInfo] = useState({ + number: 0, size: 3, totalPages: 0, totalElements: 0, + }); + + const handleSelectedTag = (tag: TagResponse) => { + setSelectedTag(tag); + setPage(0); + } + + useEffect(() => { + if (!username) { + alert("잘못된 url 경로입니다."); + navigate(-1); + return; + } + + getMemberTags(username) + .then(res => { + const tags: TagResponse[] = res.data; + setTags(tags); + setSelectedTag(tags.find(tag => tag.id === tagId) || null); + }) + .catch(error => alert(error.response.data.message)); + }, []); + + useEffect(() => { + if (!selectedTag) { + return; + } + + if (typeof username !== "string") { + return; + } + + getMemberTagPosts({ + username: username, + tagId: selectedTag.id, + page: page, + size: pageInfo.size, + }) + .then(res => { + setPosts(res.data.content); + setPageInfo(res.data.page); + }) + .catch(error => alert(error.response.data.message)); + }, [selectedTag, page]); + + return ( + +
+ +
+ {tags.map(tag => + handleSelectedTag(tag)}/>)} +
+

+ {selectedTag && "{selectedTag.name}"} + {pageInfo.totalElements}개의 게시글 +

+
+ {posts.map(post => )} +
+ +
+
+ ); +} + +export default BlogTagPage; \ No newline at end of file diff --git a/src/pages/post/WritePage.tsx b/src/pages/post/WritePage.tsx index 2bd4787..f31ede4 100644 --- a/src/pages/post/WritePage.tsx +++ b/src/pages/post/WritePage.tsx @@ -169,6 +169,20 @@ function WritePage() { }, []); useEffect(() => { + getMemberFolders(loginState.username) + .then(res => { + setFolders(toFolderTypeList(res.data)); + setFolders(prev => [ + { + id: "empty", + title: "폴더 없음", + postCount: 0, + subFolders: [], + }, + ...prev + ]); + }); + if (!path.endsWith("edit")) { return; } @@ -195,20 +209,6 @@ function WritePage() { }); }) .catch((error) => alert(error.response.data.message)); - - getMemberFolders(loginState.username) - .then(res => { - setFolders(toFolderTypeList(res.data)); - setFolders(prev => [ - { - id: "empty", - title: "폴더 없음", - postCount: 0, - subFolders: [], - }, - ...prev - ]); - }); }, []); useEffect(() => { @@ -220,7 +220,7 @@ function WritePage() { return ( -
+
-1; } - const getTargetFolderId = (targetFolder : FolderType | null) => { + const getTargetFolderId = (targetFolder: FolderType | null) => { if (!targetFolder || targetFolder.id === "empty") { return null; } @@ -65,7 +66,12 @@ function PostSettingPage() { return targetFolder.id; } const submitPostFolderUpdate = () => { - if (!confirm("변경사항을 저장하시겠습니까?")) { + if (!targetFolder) { + alert("폴더를 선택해주세요."); + return; + } + + if (!confirm(`선택한 게시글을 ${targetFolder.title}로 이동하시겠습니까?`)) { return; } @@ -75,6 +81,17 @@ function PostSettingPage() { }) .then(() => { alert("폴더가 수정되었습니다."); + getMemberPosts({ + username: loginState.username, + folderIds: [], + page: page, + size: pageInfo.size, + }) + .then((res) => { + setPosts([...res.data.content]); + setPageInfo(res.data.page); + }) + .catch((error) => alert(error.response.data.message)); setIsFolderEdit(false); }) .catch((error) => alert(error.response.data.message)); @@ -137,6 +154,10 @@ function PostSettingPage() {

{post.title}

{fullDateToKorean(post.createdAt)}

+
+ +

{post.folder ? post.folder.title : "폴더 없음"}

+
{!isFolderEdit &&
diff --git a/src/pages/setting/ProfileSettingPage.tsx b/src/pages/setting/ProfileSettingPage.tsx index 7209f2f..253072b 100644 --- a/src/pages/setting/ProfileSettingPage.tsx +++ b/src/pages/setting/ProfileSettingPage.tsx @@ -1,7 +1,7 @@ import {useDispatch, useSelector} from "react-redux"; import {RootState} from "../../store.tsx"; import {MdOutlineEdit} from "react-icons/md"; -import {ChangeEvent, useEffect, useRef, useState} from "react"; +import {ChangeEvent, useRef, useState} from "react"; import {FillButton} from "../../components/common/FillButton.tsx"; import {setUsername} from "../../common/slices/loginSlice.tsx"; import {TextButton} from "../../components/common/TextButton.tsx"; @@ -75,20 +75,13 @@ function ProfileSettingPage() { .catch((error) => alert(error.response.data.message)); } - useEffect(() => { - if (!loginState.isLogin) { - alert("로그인이 필요한 페이지입니다."); - navigate("/login"); - } - }, []); - return (

프로필 관리

-
+
{isImageEdit && image ?