From 740ac82f3ff793c0695534623ea8b975239f2b34 Mon Sep 17 00:00:00 2001 From: typhoon0678 Date: Wed, 12 Feb 2025 17:00:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1,=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20id=EB=A1=9C?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=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/image.tsx | 8 ++++++ src/common/apis/post.tsx | 8 ++++++ src/common/types/post.tsx | 7 +++++ src/common/util/html.tsx | 6 +++++ src/common/util/regex.tsx | 5 ++++ src/components/common/Header.tsx | 19 ++++++++++--- src/pages/MainPage.tsx | 29 +++----------------- src/pages/member/LoginPage.tsx | 4 +-- src/pages/post/PostPage.tsx | 46 +++++++++++++++++++++++--------- src/pages/post/WritePage.tsx | 43 ++++++++++++++++++++++------- 10 files changed, 122 insertions(+), 53 deletions(-) create mode 100644 src/common/apis/image.tsx create mode 100644 src/common/apis/post.tsx diff --git a/src/common/apis/image.tsx b/src/common/apis/image.tsx new file mode 100644 index 0000000..0d68d64 --- /dev/null +++ b/src/common/apis/image.tsx @@ -0,0 +1,8 @@ +import axiosApi from "./AxiosApi.tsx"; + +export const uploadImage = async (blogInfo: any) => { + const formData = new FormData(); + formData.append("file", blogInfo.blob()); + + return await axiosApi.post("/image", formData); +} diff --git a/src/common/apis/post.tsx b/src/common/apis/post.tsx new file mode 100644 index 0000000..1034c77 --- /dev/null +++ b/src/common/apis/post.tsx @@ -0,0 +1,8 @@ +import axiosApi from "./AxiosApi.tsx"; +import {PostRequest} from "../types/post.tsx"; + +export const createPost = async (postRequest: PostRequest) => + await axiosApi.post("/post", postRequest); + +export const getPost = async (id: string) => + await axiosApi.get(`/post/${id}`); diff --git a/src/common/types/post.tsx b/src/common/types/post.tsx index b77f423..8746448 100644 --- a/src/common/types/post.tsx +++ b/src/common/types/post.tsx @@ -1,5 +1,12 @@ import {PageResponse} from "./common.tsx"; +export interface PostRequest { + title: string, + content: string, + tagNames: string[], + urls: string[], +} + export interface PostListResponse { content: PostResponse[]; page: PageResponse; diff --git a/src/common/util/html.tsx b/src/common/util/html.tsx index 2cec434..7819968 100644 --- a/src/common/util/html.tsx +++ b/src/common/util/html.tsx @@ -9,4 +9,10 @@ export const getImgSrc = (content: string) => { const doc = new DOMParser().parseFromString(content, 'text/html'); const imgTag = doc.querySelector('img'); return imgTag ? imgTag.src : null; +} + +export const getImgUrls = (content: string) => { + const doc = new DOMParser().parseFromString(content, 'text/html'); + const images = doc.querySelectorAll('img'); + return Array.from(images).map(img => img.src); } \ No newline at end of file diff --git a/src/common/util/regex.tsx b/src/common/util/regex.tsx index a66be20..1d29f55 100644 --- a/src/common/util/regex.tsx +++ b/src/common/util/regex.tsx @@ -8,4 +8,9 @@ export const checkPassword = (password: string) => { const regex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\W_]{8,16}$/; return !!(password !== "" && password.match(regex)); +} + +export const checkUUID = (id: string) => { + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(id); } \ No newline at end of file diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx index b9d9617..0c9f3f4 100644 --- a/src/components/common/Header.tsx +++ b/src/components/common/Header.tsx @@ -1,22 +1,35 @@ import {Link, useNavigate} from "react-router-dom"; -import {useSelector} from "react-redux"; +import {useDispatch, useSelector} from "react-redux"; import {RootState} from "../../store.tsx"; import {faker} from "@faker-js/faker/locale/ko"; import {useEffect, useRef, useState} from "react"; import {TextLink} from "./TextButton.tsx"; import {MdOutlineSearch} from "react-icons/md"; import IconButton from "./IconButton.tsx"; -import {logoutApi} from "../../common/apis/member.tsx"; -import {logout} from "../../common/slices/loginSlice.tsx"; +import {getProfile, logoutApi} from "../../common/apis/member.tsx"; +import {login, logout} from "../../common/slices/loginSlice.tsx"; function Header() { + + const dispatch = useDispatch(); const loginState = useSelector((state: RootState) => state.loginSlice); const navigate = useNavigate(); const dashboardRef = useRef(null); const [isOpen, setIsOpen] = useState(false); + useEffect(() => { + getProfile() + .then(res => { + dispatch(login({ + ...loginState, + email: res.data.email, + username: res.data.username, + })); + }) + }, []); + const handleDropDown = () => { setIsOpen(cur => !cur); } diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 25406f4..ba39afe 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,33 +1,9 @@ import BasicLayout from "../layout/BasicLayout.tsx"; -import {useEffect} from "react"; -import {getProfile} from "../common/apis/member.tsx"; -import {login} from "../common/slices/loginSlice.tsx"; -import {useDispatch, useSelector} from "react-redux"; -import {RootState} from "../store.tsx"; import PostCard from "../components/post/PostCard.tsx"; import {faker} from "@faker-js/faker/locale/ko"; function MainPage() { - const dispatch = useDispatch(); - const loginState = useSelector((state: RootState) => state.loginSlice); - - useEffect(() => { - getProfile() - .then(res => { - dispatch(login({ - ...loginState, - email: res.data.email, - username: res.data.username, - })); - } - ) - .catch(err => { - console.log(err); - }); - }, []); - - return (
@@ -40,7 +16,10 @@ function MainPage() { key={faker.number.int().toString()} id={faker.number.int().toString()} title={faker.lorem.sentence()} - content={`${faker.lorem.paragraphs()}`} + content={`${faker.lorem.paragraphs()}`} username={faker.animal.cat()} tags={[{ id: faker.number.int().toString(), diff --git a/src/pages/member/LoginPage.tsx b/src/pages/member/LoginPage.tsx index 0d00df2..564b4ca 100644 --- a/src/pages/member/LoginPage.tsx +++ b/src/pages/member/LoginPage.tsx @@ -28,9 +28,7 @@ function LoginPage() { })); navigate("/"); }) - .catch(err => { - alert(err); - }) + .catch(error => alert(error.response.data.message)); } const handlePasswordEnter = (event: React.KeyboardEvent) => { diff --git a/src/pages/post/PostPage.tsx b/src/pages/post/PostPage.tsx index 4dfcc50..6b9f6c0 100644 --- a/src/pages/post/PostPage.tsx +++ b/src/pages/post/PostPage.tsx @@ -1,38 +1,58 @@ import {Link, useNavigate, useParams} from "react-router-dom"; import BasicLayout from "../../layout/BasicLayout.tsx"; -import {PostResponse} from "../../common/types/post.tsx"; -import {faker} from "@faker-js/faker/locale/ko"; import DOMPurify from "dompurify"; import {fullDateToKorean} from "../../common/util/date.tsx"; import TagCard from "../../components/post/TagCard.tsx"; +import {getPost} from "../../common/apis/post.tsx"; +import {useEffect, useState} from "react"; +import {PostResponse} from "../../common/types/post.tsx"; +import {checkUUID} from "../../common/util/regex.tsx"; function PostPage() { const {id} = useParams(); - console.log(id); const navigate = useNavigate(); - const post: PostResponse = { - id: faker.number.int().toString(), - title: faker.lorem.sentence(), - content: `
${faker.lorem.paragraphs()}
`, - username: faker.animal.cat(), - tags: [{id: "1", name: faker.lorem.word()}, {id: "2", name: faker.lorem.word()}], + const [post, setPost] = useState({ + id: "", + title: "", + content: "", + username: "", + tags: [], createdAt: new Date(), - }; + }); + + useEffect(() => { + if (id === null || id === undefined || !checkUUID(id)) { + alert("올바르지 않은 주소입니다."); + navigate(-1); + return; + } + + getPost(id) + .then((res) => { + console.log(res.data); + setPost({ + ...res.data, + id: id, + createdAt: new Date(), + }); + }) + .catch((error) => alert(error.response.data.message)); + }, []); const safeContent = DOMPurify.sanitize(post.content); return ( -
+
Home
{` > `}
카테고리
{` > `}
-
{post.title}
+
{post.title}
-
+
{post.title}
diff --git a/src/pages/post/WritePage.tsx b/src/pages/post/WritePage.tsx index 919a164..8fd36b8 100644 --- a/src/pages/post/WritePage.tsx +++ b/src/pages/post/WritePage.tsx @@ -7,6 +7,10 @@ import {useSelector} from "react-redux"; import {RootState} from "../../store.tsx"; import {faker} from "@faker-js/faker/locale/ko"; import {MdOutlineArrowDropDown, MdOutlineClear} from "react-icons/md"; +import {createPost} from "../../common/apis/post.tsx"; +import LoadingLayout from "../../layout/LoadingLayout.tsx"; +import {getImgUrls} from "../../common/util/html.tsx"; +import {uploadImage} from "../../common/apis/image.tsx"; interface WritePostType { inputTag: string; @@ -26,6 +30,7 @@ function WritePage() { const navigate = useNavigate(); const categoryRef = useRef(null); + const [loading, setLoading] = useState(false); const [post, setPost] = useState({ inputTag: "", tags: [], @@ -63,9 +68,23 @@ function WritePage() { return; } - alert("작성되었습니다."); - setExitPage(true); - navigate(`/blog/${loginState.username}username`); + setLoading(true); + + const urls: string[] = getImgUrls(post.content); + + createPost({ + title: post.title, + content: post.content, + tagNames: post.tags, + urls: urls, + }) + .then(() => { + alert("작성되었습니다."); + setExitPage(true); + navigate(`/blog/${loginState.username}username`); + }) + .catch((error) => alert(error.response.data.message)) + .finally(() => setLoading(false)); } const handleClickOutside = (event: MouseEvent) => { @@ -118,6 +137,13 @@ function WritePage() { }, ]; + useEffect(() => { + if (!loginState.isLogin) { + alert("해당 페이지 이용에는 로그인이 필요합니다."); + navigate("/login"); + } + }, []); + return (
@@ -212,12 +238,10 @@ function WritePage() { placeholder: "내용을 작성해주세요.", file_picker_types: "image", images_upload_handler: async (blobInfo) => { - setUploadCount((prev) => prev + 1); - // const res = await uploadImage(blobInfo); - // return res.data.url; - console.log("Image Upload...... ", blobInfo); - setUploadCount((prev) => prev - 1); - return faker.image.url(); + setUploadCount(prev => prev + 1); + const res = await uploadImage(blobInfo); + setUploadCount(prev => prev - 1); + return res.data.url; } }} value={post.content} @@ -227,6 +251,7 @@ function WritePage() {
+ ); } From cfee84cdd31878d7bf5852dadc95471128b7e218 Mon Sep 17 00:00:00 2001 From: typhoon0678 Date: Wed, 12 Feb 2025 17:32:28 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?api=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/post.tsx | 6 +++- src/common/types/post.tsx | 7 ++++ src/common/util/html.tsx | 12 +++---- src/common/util/url.tsx | 7 ++++ src/components/post/PostCard.tsx | 6 ++-- src/pages/MainPage.tsx | 59 ++++++++++++++++---------------- 6 files changed, 57 insertions(+), 40 deletions(-) create mode 100644 src/common/util/url.tsx diff --git a/src/common/apis/post.tsx b/src/common/apis/post.tsx index 1034c77..7963419 100644 --- a/src/common/apis/post.tsx +++ b/src/common/apis/post.tsx @@ -1,8 +1,12 @@ import axiosApi from "./AxiosApi.tsx"; -import {PostRequest} from "../types/post.tsx"; +import {PostListRequest, PostRequest} from "../types/post.tsx"; +import {postListRequestToParameter} from "../util/url.tsx"; export const createPost = async (postRequest: PostRequest) => await axiosApi.post("/post", postRequest); export const getPost = async (id: string) => await axiosApi.get(`/post/${id}`); + +export const getPosts = async (postListRequest: PostListRequest) => + await axiosApi.get(`/post${postListRequestToParameter(postListRequest)}`); diff --git a/src/common/types/post.tsx b/src/common/types/post.tsx index 8746448..9bebd5e 100644 --- a/src/common/types/post.tsx +++ b/src/common/types/post.tsx @@ -7,6 +7,13 @@ export interface PostRequest { urls: string[], } +export interface PostListRequest { + sorts: string[], + page: number, + size: number, + isDescending: boolean, +} + export interface PostListResponse { content: PostResponse[]; page: PageResponse; diff --git a/src/common/util/html.tsx b/src/common/util/html.tsx index 7819968..da4df5d 100644 --- a/src/common/util/html.tsx +++ b/src/common/util/html.tsx @@ -1,10 +1,3 @@ -export const removeImgTags = (content: string) => { - const doc = new DOMParser().parseFromString(content, 'text/html'); - const imgTags = doc.querySelectorAll('img'); - imgTags.forEach((img) => img.remove()); - return doc.body.innerHTML; -} - export const getImgSrc = (content: string) => { const doc = new DOMParser().parseFromString(content, 'text/html'); const imgTag = doc.querySelector('img'); @@ -15,4 +8,9 @@ export const getImgUrls = (content: string) => { const doc = new DOMParser().parseFromString(content, 'text/html'); const images = doc.querySelectorAll('img'); return Array.from(images).map(img => img.src); +} + +export const removeHtmlTags = (content: string) => { + const regex = /(<([^>]+)>)/gi; + return content.replace(regex, ""); } \ No newline at end of file diff --git a/src/common/util/url.tsx b/src/common/util/url.tsx new file mode 100644 index 0000000..4b90032 --- /dev/null +++ b/src/common/util/url.tsx @@ -0,0 +1,7 @@ +import {PostListRequest} from "../types/post.tsx"; + +export const postListRequestToParameter = (postListRequest: PostListRequest)=> { + const sorts = postListRequest.sorts.map(sort => `sorts=${sort}`).join("&"); + + return `?${sorts}&page=${postListRequest.page}&size=${postListRequest.size}&isDescending=${postListRequest.isDescending}`; +} \ No newline at end of file diff --git a/src/components/post/PostCard.tsx b/src/components/post/PostCard.tsx index 31f8c8a..32c0464 100644 --- a/src/components/post/PostCard.tsx +++ b/src/components/post/PostCard.tsx @@ -1,5 +1,5 @@ import {PostResponse} from "../../common/types/post.tsx"; -import {getImgSrc, removeImgTags} from "../../common/util/html.tsx"; +import {getImgSrc, removeHtmlTags} from "../../common/util/html.tsx"; import DOMPurify from "dompurify"; import {MdImage} from "react-icons/md"; import {dateToKorean} from "../../common/util/date.tsx"; @@ -17,7 +17,7 @@ function PostCard(post: PostResponse) { ? post_image - :
+ :
} @@ -37,7 +37,7 @@ function PostCard(post: PostResponse) {
- {removeImgTags(safeContent)} + {removeHtmlTags(safeContent)}
diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index ba39afe..03e8552 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,43 +1,44 @@ import BasicLayout from "../layout/BasicLayout.tsx"; import PostCard from "../components/post/PostCard.tsx"; -import {faker} from "@faker-js/faker/locale/ko"; +import {useEffect, useState} from "react"; +import {getPosts} from "../common/apis/post.tsx"; +import {PostResponse} from "../common/types/post.tsx"; function MainPage() { + const [page, setPage] = useState(0); + const [posts, setPosts] = useState([]); + + useEffect(() => { + getPosts({ + sorts: ["createdAt"], + page: page, + size: 12, + isDescending: true, + }) + .then((res) => { + setPosts(res.data.content); + }) + .catch(() => alert("데이터 로드 중 문제가 발생했습니다. 다시 시도해주세요.")); + }, [page]); + return ( -
+
인기 있는 게시글
-
- {[Array.from({length: 6}).map(() => ( +
+ {posts.map((post) => ( `} - username={faker.animal.cat()} - tags={[{ - id: faker.number.int().toString(), - name: faker.word.sample() - }, {id: faker.number.int().toString(), name: faker.word.sample()}]} - createdAt={new Date()}/> - ))]} - + key={post.id} + id={post.id} + title={post.title} + content={post.content} + username={post.username} + tags={post.tags} + createdAt={post.createdAt}/> + ))}
From e4364f3ef9225effa90d7b364f7aa6ad6494bbe4 Mon Sep 17 00:00:00 2001 From: typhoon0678 Date: Wed, 12 Feb 2025 20:13:13 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=AC=B4=ED=95=9C=EC=8A=A4=ED=81=AC?= =?UTF-8?q?=EB=A1=A4=20refactor:=20category=20->=20folder=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=EB=B0=8D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/util/infiniteScroll.tsx | 71 +++++++++ src/components/common/FillButton.tsx | 6 +- src/components/common/OutlineButton.tsx | 6 +- src/components/setting/CategoryMoveModal.tsx | 53 ------- ...ategoryAddModal.tsx => FolderAddModal.tsx} | 72 ++++----- .../{CategoryCard.tsx => FolderCard.tsx} | 42 ++--- src/components/setting/FolderMoveModal.tsx | 53 +++++++ src/pages/MainPage.tsx | 19 ++- src/pages/blog/BlogPage.tsx | 44 ++++-- src/pages/post/PostPage.tsx | 2 +- src/pages/post/WritePage.tsx | 62 ++++---- src/pages/setting/CategorySettingPage.tsx | 58 ------- src/pages/setting/FolderSettingPage.tsx | 58 +++++++ src/pages/setting/SettingPage.tsx | 148 +++++++++--------- 14 files changed, 393 insertions(+), 301 deletions(-) create mode 100644 src/common/util/infiniteScroll.tsx delete mode 100644 src/components/setting/CategoryMoveModal.tsx rename src/components/setting/{CategoryAddModal.tsx => FolderAddModal.tsx} (52%) rename src/components/setting/{CategoryCard.tsx => FolderCard.tsx} (72%) create mode 100644 src/components/setting/FolderMoveModal.tsx delete mode 100644 src/pages/setting/CategorySettingPage.tsx create mode 100644 src/pages/setting/FolderSettingPage.tsx diff --git a/src/common/util/infiniteScroll.tsx b/src/common/util/infiniteScroll.tsx new file mode 100644 index 0000000..d47e1ce --- /dev/null +++ b/src/common/util/infiniteScroll.tsx @@ -0,0 +1,71 @@ +import {useEffect, useState, useMemo, useRef, MutableRefObject} from 'react'; +import {PostResponse} from "../types/post.tsx"; + +export interface InfiniteScrollProps { + root?: Element | null; + rootMargin?: string; + target: MutableRefObject; + threshold?: number; + targetArray: Array; + endPoint?: number; +} + +const useInfiniteScroll = ({ + root = null, + target, + threshold = 1, + rootMargin = '0px', + targetArray, + endPoint = 1 + }: InfiniteScrollProps) => { + + const [page, setPage] = useState(0); + const currentChild = useRef(null); + + // IntersectionObserver 생성자 등록 및 callback 함수 등록 + const observer = useMemo(() => { + return new IntersectionObserver( + (entries, observer) => { + if (target?.current === null) { + return; + } + if (entries[0].isIntersecting) { + setPage((v) => v + 1); + // setPage 가 무한으로 올라가는 것을 방지하기 위한 연결 끊음 + observer.disconnect(); + } + }, + { + root, + rootMargin, + threshold, + }, + ); + }, [target, root, rootMargin, threshold]); + + useEffect(() => { + if (target?.current === null) { + return; + } + + // 관측하는 Element 가 달라졌을 때, 다시 관측을 시작 + const observeChild = target.current.children[target.current.children.length - endPoint]; + if (observeChild && currentChild.current !== observeChild) { + currentChild.current = observeChild; + observer.observe(observeChild); + } + + return () => { + if (target.current !== null && observer) { + observer.unobserve(target.current); + } + }; + }, [page, targetArray, target, endPoint, observer]); + + return { + page, + setPage + }; +}; + +export default useInfiniteScroll; \ No newline at end of file diff --git a/src/components/common/FillButton.tsx b/src/components/common/FillButton.tsx index 46a3652..bbeb89e 100644 --- a/src/components/common/FillButton.tsx +++ b/src/components/common/FillButton.tsx @@ -10,9 +10,9 @@ export function FillButton({text, onClick, addStyle}: { text: string, onClick: ( ); } -export function FillLink({text, to}: { text: string, to: string }) { +export function FillLink({text, to, addStyle}: { text: string, to: string, addStyle?: string }) { return ( - + {text} ); @@ -20,6 +20,6 @@ export function FillLink({text, to}: { text: string, to: string }) { export function LoadMoreButton({onClick, addStyle}: { onClick: () => void, addStyle?: string }) { return ( - + ); } \ No newline at end of file diff --git a/src/components/common/OutlineButton.tsx b/src/components/common/OutlineButton.tsx index f2735e0..3b59dd3 100644 --- a/src/components/common/OutlineButton.tsx +++ b/src/components/common/OutlineButton.tsx @@ -1,6 +1,6 @@ import {Link} from "react-router-dom"; -const className = "bg-transparent hover:bg-lime-300 text-lime-700 font-semibold hover:text-white py-2 px-4 border border-lime-500 hover:border-transparent rounded hover:cursor-pointer"; +const className = " bg-transparent hover:bg-lime-300 text-lime-700 font-semibold hover:text-white py-2 px-4 border border-lime-500 hover:border-transparent rounded hover:cursor-pointer"; export function OutlineButton({text, onClick, addStyle}: { text: string, onClick?: () => void, addStyle?: string }) { return ( @@ -10,9 +10,9 @@ export function OutlineButton({text, onClick, addStyle}: { text: string, onClick ); } -export function OutlineLink({text, to}: { text: string, to: string }) { +export function OutlineLink({text, to, addStyle}: { text: string, to: string, addStyle?: string }) { return ( - + {text} ); diff --git a/src/components/setting/CategoryMoveModal.tsx b/src/components/setting/CategoryMoveModal.tsx deleted file mode 100644 index a7b62d6..0000000 --- a/src/components/setting/CategoryMoveModal.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import {CategoryType} from "../../pages/setting/SettingPage.tsx"; -import {FillButton} from "../common/FillButton.tsx"; -import {useState} from "react"; -import {TextButton} from "../common/TextButton.tsx"; -import {useSelector} from "react-redux"; -import {RootState} from "../../store.tsx"; -import ModalLayout from "../../layout/ModalLayout.tsx"; - -function CategoryMoveModal({selectedCategory, categories, handleCategoryMove, setShowModal}: { - selectedCategory: CategoryType | null, - categories: CategoryType[], - handleCategoryMove: (categoryId: string) => void - setShowModal: (showModal: boolean) => void, -}) { - - const loginState = useSelector((state: RootState) => state.loginSlice); - - const [selectedCategoryId, setSelectedCategoryId] = useState(""); - - const handleCategoryChange = () => { - handleCategoryMove(selectedCategoryId); - - setShowModal(false); - } - - return ( - -
-

- "{selectedCategory?.name}" 카테고리를 이동할 곳을 골라주세요. -

- - {categories.map(category => ( - - ))} -
- setShowModal(false)}/> - -
-
-
- ); -} - -export default CategoryMoveModal; \ No newline at end of file diff --git a/src/components/setting/CategoryAddModal.tsx b/src/components/setting/FolderAddModal.tsx similarity index 52% rename from src/components/setting/CategoryAddModal.tsx rename to src/components/setting/FolderAddModal.tsx index d539b3d..e54b4cc 100644 --- a/src/components/setting/CategoryAddModal.tsx +++ b/src/components/setting/FolderAddModal.tsx @@ -1,73 +1,73 @@ import ModalLayout from "../../layout/ModalLayout.tsx"; import {useState} from "react"; import {FillButton} from "../common/FillButton.tsx"; -import {CategoryType} from "../../pages/setting/SettingPage.tsx"; +import {FolderType} from "../../pages/setting/SettingPage.tsx"; import {TextButton} from "../common/TextButton.tsx"; import {MdOutlineArrowDropDown} from "react-icons/md"; -function CategoryAddModal({categories, setShowCategoryAddModal}: { - categories: CategoryType[], - setShowCategoryAddModal: (modal: boolean) => void +function FolderAddModal({folders, setShowFolderAddModal}: { + folders: FolderType[], + setShowFolderAddModal: (modal: boolean) => void }) { - const [inputCategory, setInputCategory] = useState(""); - const [categoryOpen, setCategoryOpen] = useState(false); - const [selectedCategory, setSelectedCategory] = useState("블로그"); + const [inputFolder, setInputFolder] = useState(""); + const [folderOpen, setFolderOpen] = useState(false); + const [selectedFolder, setSelectedFolder] = useState("블로그"); return (
-

카테고리 추가

+

폴더 추가

+ className={`${folderOpen ? "" : "hidden"} absolute z-50 top-12 left-0 bg-white divide-y divide-gray-500 rounded-lg shadow-sm`}>
- {categories.map((category) => { - if (!category.subCategories) { - return
+ {folders.map((folder) => { + if (!folder.subFolders) { + return
} else { - return
- {category.subCategories.map((subCategory: CategoryType) => + {folder.subFolders.map((subFolder: FolderType) => )}
; @@ -75,15 +75,15 @@ function CategoryAddModal({categories, setShowCategoryAddModal}: { })}
setInputCategory(e.target.value)} - placeholder="카테고리 이름을 입력해주세요." + value={inputFolder} + onChange={(e) => setInputFolder(e.target.value)} + placeholder="폴더 이름을 입력해주세요." className="border border-gray-400 p-4 font-bold"/>
- setShowCategoryAddModal(false)}/> - { - alert("카테고리가 추가되었습니다."); - setShowCategoryAddModal(false); + setShowFolderAddModal(false)}/> + { + alert("폴더가 추가되었습니다."); + setShowFolderAddModal(false); }}/>
@@ -91,4 +91,4 @@ function CategoryAddModal({categories, setShowCategoryAddModal}: { ); } -export default CategoryAddModal; \ No newline at end of file +export default FolderAddModal; \ No newline at end of file diff --git a/src/components/setting/CategoryCard.tsx b/src/components/setting/FolderCard.tsx similarity index 72% rename from src/components/setting/CategoryCard.tsx rename to src/components/setting/FolderCard.tsx index ef83fc8..b35cacd 100644 --- a/src/components/setting/CategoryCard.tsx +++ b/src/components/setting/FolderCard.tsx @@ -1,4 +1,4 @@ -import {CategoryType} from "../../pages/setting/SettingPage.tsx"; +import {FolderType} from "../../pages/setting/SettingPage.tsx"; import {DndContext, DragEndEvent} from "@dnd-kit/core"; import {SortableContext, useSortable} from "@dnd-kit/sortable"; import {CSS} from "@dnd-kit/utilities"; @@ -8,9 +8,9 @@ import {restrictToVerticalAxis} from "@dnd-kit/modifiers"; import {useState} from "react"; import {FillButton} from "../common/FillButton.tsx"; -function CategoryCard({setSelectedCategory, category, setShowModal, handleDrag, isHover, handleHover, isSub}: { - setSelectedCategory: (category: CategoryType) => void, - category: CategoryType, +function FolderCard({setSelectedFolder, folder, setShowModal, handleDrag, isHover, handleHover, isSub}: { + setSelectedFolder: (folder: FolderType) => void, + folder: FolderType, setShowModal: (modal: boolean) => void, handleDrag?: (event: DragEndEvent) => void, isHover: boolean, @@ -19,17 +19,17 @@ function CategoryCard({setSelectedCategory, category, setShowModal, handleDrag, }) { const [isEdit, setIsEdit] = useState(false); - const [categoryNameInput, setCategoryNameInput] = useState(category.name); + const [folderNameInput, setFolderNameInput] = useState(folder.name); - const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id: category.id}); + const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id: folder.id}); const style = { transform: CSS.Translate.toString(transform), transition, }; - const handleCategoryNameChange = () => { - category.name = categoryNameInput; + const handleFolderNameChange = () => { + folder.name = folderNameInput; setIsEdit(false); } @@ -55,24 +55,24 @@ function CategoryCard({setSelectedCategory, category, setShowModal, handleDrag, ? { - setCategoryNameInput(e.target.value) + setFolderNameInput(e.target.value) }}/> - :

{category.name}

} + :

{folder.name}

}
{(isSub && !isEdit) && { - setSelectedCategory(category); + setSelectedFolder(folder); setShowModal(true); }}/>} {(isEdit) ? : setIsEdit(true)}/>}
- {category.subCategories && ( + {folder.subFolders && ( - - {category.subCategories.map((subCategory) => ( - + {folder.subFolders.map((subFolder) => ( + @@ -99,4 +99,4 @@ function CategoryCard({setSelectedCategory, category, setShowModal, handleDrag, ); } -export default CategoryCard; \ No newline at end of file +export default FolderCard; \ No newline at end of file diff --git a/src/components/setting/FolderMoveModal.tsx b/src/components/setting/FolderMoveModal.tsx new file mode 100644 index 0000000..af0fde7 --- /dev/null +++ b/src/components/setting/FolderMoveModal.tsx @@ -0,0 +1,53 @@ +import {FolderType} from "../../pages/setting/SettingPage.tsx"; +import {FillButton} from "../common/FillButton.tsx"; +import {useState} from "react"; +import {TextButton} from "../common/TextButton.tsx"; +import {useSelector} from "react-redux"; +import {RootState} from "../../store.tsx"; +import ModalLayout from "../../layout/ModalLayout.tsx"; + +function FolderMoveModal({selectedFolder, folders, handleFolderMove, setShowModal}: { + selectedFolder: FolderType | null, + folders: FolderType[], + handleFolderMove: (folderId: string) => void + setShowModal: (showModal: boolean) => void, +}) { + + const loginState = useSelector((state: RootState) => state.loginSlice); + + const [selectedFolderId, setSelectedFolderId] = useState(""); + + const handleFolderChange = () => { + handleFolderMove(selectedFolderId); + + setShowModal(false); + } + + return ( + +
+

+ "{selectedFolder?.name}" 폴더를 이동할 곳을 골라주세요. +

+ + {folders.map(folder => ( + + ))} +
+ setShowModal(false)}/> + +
+
+
+ ); +} + +export default FolderMoveModal; \ No newline at end of file diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx index 03e8552..192d70a 100644 --- a/src/pages/MainPage.tsx +++ b/src/pages/MainPage.tsx @@ -1,23 +1,31 @@ import BasicLayout from "../layout/BasicLayout.tsx"; import PostCard from "../components/post/PostCard.tsx"; -import {useEffect, useState} from "react"; +import {useEffect, useRef, useState} from "react"; import {getPosts} from "../common/apis/post.tsx"; import {PostResponse} from "../common/types/post.tsx"; +import useInfiniteScroll from "../common/util/infiniteScroll.tsx"; function MainPage() { - const [page, setPage] = useState(0); + const pageRef = useRef(null); const [posts, setPosts] = useState([]); + const {page} = useInfiniteScroll({ + target: pageRef, + targetArray: posts, + threshold: 0.5, + endPoint: 4, + }); + const pageSize = 12; useEffect(() => { getPosts({ sorts: ["createdAt"], page: page, - size: 12, + size: pageSize, isDescending: true, }) .then((res) => { - setPosts(res.data.content); + setPosts([...posts, ...res.data.content]); }) .catch(() => alert("데이터 로드 중 문제가 발생했습니다. 다시 시도해주세요.")); }, [page]); @@ -28,7 +36,8 @@ function MainPage() {
인기 있는 게시글
-
+
{posts.map((post) => ( ([]); const mainRef = useRef(null); @@ -38,9 +41,9 @@ function BlogPage() { setSelectedTagList(prevState => prevState.filter(tag => tag !== selectTag)); } - const removeCategory = (selectCategory: string) => { - console.log(selectCategory); - setSelectedCategory(""); + const removeFolder = (selectFolder: string) => { + console.log(selectFolder); + setSelectedFolder(""); } useEffect(() => { @@ -62,9 +65,9 @@ function BlogPage() { useEffect(() => { setSearchParams({ - "category": selectedCategory + "folder": selectedFolder }) - }, [selectedCategory]); + }, [selectedFolder]); useEffect(() => { if (isOpen) { @@ -90,7 +93,7 @@ function BlogPage() {
- 카테고리 {selectedCategory !== "" && } + 폴더 {selectedFolder !== "" && }
태그 {selectedTagList.map((tag) => )} @@ -107,7 +110,7 @@ function BlogPage() { width: 320, height: 320 })}/>`} - username={faker.animal.cat()} + username={username || faker.animal.cat()} tags={[{ id: faker.number.int().toString(), name: faker.word.sample() @@ -122,7 +125,7 @@ function BlogPage() { + setSelectedFolder={setSelectedFolder}/>
@@ -136,20 +139,22 @@ function BlogPage() {
); } -function SideBar({username, addTag, setSelectedCategory, bgColor}: { +function SideBar({username, addTag, setSelectedFolder, bgColor}: { username: string | undefined, addTag: (tagName: string) => void, - setSelectedCategory: (category: string) => void, + setSelectedFolder: (folder: string) => void, bgColor?: string }) { + const loginState = useSelector((state: RootState) => state.loginSlice); + return (
@@ -158,21 +163,28 @@ function SideBar({username, addTag, setSelectedCategory, bgColor}: {
{username}
+ {username === loginState.username && ( +
+ + +
+ )}
-
카테고리
+
폴더
{[Array.from({length: 10}).map((_, i) => (
{Array.from({length: 3}).map((_, i) => ( ))} diff --git a/src/pages/post/PostPage.tsx b/src/pages/post/PostPage.tsx index 6b9f6c0..7cef8b7 100644 --- a/src/pages/post/PostPage.tsx +++ b/src/pages/post/PostPage.tsx @@ -50,7 +50,7 @@ function PostPage() {
Home
{` > `}
- 카테고리 + 폴더
{` > `}
{post.title}
diff --git a/src/pages/post/WritePage.tsx b/src/pages/post/WritePage.tsx index 8fd36b8..139f7aa 100644 --- a/src/pages/post/WritePage.tsx +++ b/src/pages/post/WritePage.tsx @@ -19,16 +19,16 @@ interface WritePostType { content: string; } -interface CategoryType { +interface FolderType { name: string; - subCategories?: CategoryType[]; + subFolders?: FolderType[]; } function WritePage() { const loginState = useSelector((state: RootState) => state.loginSlice); const navigate = useNavigate(); - const categoryRef = useRef(null); + const folderRef = useRef(null); const [loading, setLoading] = useState(false); const [post, setPost] = useState({ @@ -39,12 +39,12 @@ function WritePage() { }); const [showTag, setShowTag] = useState(false); - const [categoryOpen, setCategoryOpen] = useState(false); - const [selectedCategory, setSelectedCategory] = useState("카테고리 선택"); + const [folderOpen, setFolderOpen] = useState(false); + const [selectedFolder, setSelectedFolder] = useState("폴더 선택"); const [uploadCount, setUploadCount] = useState(0); const [exitPage, setExitPage] = useState(false); - const handleCategoryOpen = () => { - setCategoryOpen(prev => !prev); + const handleFolderOpen = () => { + setFolderOpen(prev => !prev); } const removeTag = (tag: string | null) => { @@ -88,8 +88,8 @@ function WritePage() { } const handleClickOutside = (event: MouseEvent) => { - if (categoryRef.current && !categoryRef.current.contains(event.target as Node)) { - setCategoryOpen(false); + if (folderRef.current && !folderRef.current.contains(event.target as Node)) { + setFolderOpen(false); } }; @@ -116,10 +116,10 @@ function WritePage() { }; }, []); - const categoryData: CategoryType[] = [ + const folderData: FolderType[] = [ { name: faker.lorem.words(), - subCategories: [ + subFolders: [ {name: faker.lorem.words()}, {name: faker.lorem.words()} ] @@ -129,7 +129,7 @@ function WritePage() { }, { name: faker.lorem.words(), - subCategories: [ + subFolders: [ {name: faker.lorem.words()}, {name: faker.lorem.words()}, {name: faker.lorem.words()} @@ -148,48 +148,48 @@ function WritePage() {
-
- {categoryData.map((category) => { - if (!category.subCategories) { - return
+ className={`${folderOpen ? "" : "hidden"} absolute z-50 top-12 left-0 bg-white divide-y divide-gray-500 rounded-lg shadow-sm`}> + {folderData.map((folder) => { + if (!folder.subFolders) { + return
} else { - return
- {category.subCategories.map((subCategory: CategoryType) => + {folder.subFolders.map((subFolder: FolderType) => )}
; diff --git a/src/pages/setting/CategorySettingPage.tsx b/src/pages/setting/CategorySettingPage.tsx deleted file mode 100644 index 53bd333..0000000 --- a/src/pages/setting/CategorySettingPage.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import {DndContext, DragEndEvent} from "@dnd-kit/core"; -import {restrictToVerticalAxis} from "@dnd-kit/modifiers"; -import {SortableContext} from "@dnd-kit/sortable"; -import {FillButton} from "../../components/common/FillButton.tsx"; -import {CategoryType} from "./SettingPage.tsx"; -import CategoryCard from "../../components/setting/CategoryCard.tsx"; -import {OutlineButton} from "../../components/common/OutlineButton.tsx"; - -function CategorySettingPage({ - setSelectedCategory, - categories, - setShowModal, - setShowCategoryAddModal, - handleDragEnd, - isHover, - handleHover, - submitCategoryChange - }: { - setSelectedCategory: (category: CategoryType) => void, - categories: CategoryType[], - setShowModal: (modal: boolean) => void, - setShowCategoryAddModal: (modal: boolean) => void, - handleDragEnd: (event: DragEndEvent) => void, - isHover: boolean, - handleHover: (hover: boolean) => void, - submitCategoryChange: () => void, -}) { - - return ( -
-

카테고리 관리

-
- - - {categories.map((category) => ( -
- -
- ))} -
-
-
-
- setShowCategoryAddModal(true)} addStyle={"font-normal"}/> - -
-
- ); -} - -export default CategorySettingPage; \ No newline at end of file diff --git a/src/pages/setting/FolderSettingPage.tsx b/src/pages/setting/FolderSettingPage.tsx new file mode 100644 index 0000000..6991b24 --- /dev/null +++ b/src/pages/setting/FolderSettingPage.tsx @@ -0,0 +1,58 @@ +import {DndContext, DragEndEvent} from "@dnd-kit/core"; +import {restrictToVerticalAxis} from "@dnd-kit/modifiers"; +import {SortableContext} from "@dnd-kit/sortable"; +import {FillButton} from "../../components/common/FillButton.tsx"; +import {FolderType} from "./SettingPage.tsx"; +import FolderCard from "../../components/setting/FolderCard.tsx"; +import {OutlineButton} from "../../components/common/OutlineButton.tsx"; + +function FolderSettingPage({ + setSelectedFolder, + folders, + setShowModal, + setShowFolderAddModal, + handleDragEnd, + isHover, + handleHover, + submitFolderChange + }: { + setSelectedFolder: (folder: FolderType) => void, + folders: FolderType[], + setShowModal: (modal: boolean) => void, + setShowFolderAddModal: (modal: boolean) => void, + handleDragEnd: (event: DragEndEvent) => void, + isHover: boolean, + handleHover: (hover: boolean) => void, + submitFolderChange: () => void, +}) { + + return ( +
+

폴더 관리

+
+ + + {folders.map((folder) => ( +
+ +
+ ))} +
+
+
+
+ setShowFolderAddModal(true)} addStyle={"font-normal"}/> + +
+
+ ); +} + +export default FolderSettingPage; \ No newline at end of file diff --git a/src/pages/setting/SettingPage.tsx b/src/pages/setting/SettingPage.tsx index 87f39e9..1c592b7 100644 --- a/src/pages/setting/SettingPage.tsx +++ b/src/pages/setting/SettingPage.tsx @@ -4,31 +4,31 @@ import {faker} from "@faker-js/faker/locale/ko"; import {DragEndEvent} from "@dnd-kit/core"; import {arrayMove} from "@dnd-kit/sortable"; import {useNavigate} from "react-router-dom"; -import CategorySettingPage from "./CategorySettingPage.tsx"; +import FolderSettingPage from "./FolderSettingPage.tsx"; import PostSettingPage from "./PostSettingPage.tsx"; import ProfileSettingPage from "./ProfileSettingPage.tsx"; -import CategoryMoveModal from "../../components/setting/CategoryMoveModal.tsx"; -import CategoryAddModal from "../../components/setting/CategoryAddModal.tsx"; +import FolderMoveModal from "../../components/setting/FolderMoveModal.tsx"; +import FolderAddModal from "../../components/setting/FolderAddModal.tsx"; -export interface CategoryType { +export interface FolderType { id: string; name: string; - subCategories?: CategoryType[]; + subFolders?: FolderType[]; } function SettingPage() { const navigate = useNavigate(); - const [showCategoryModal, setShowCategoryModal] = useState(false); - const [showCategoryAddModal, setShowCategoryAddModal] = useState(false); + const [showFolderModal, setShowFolderModal] = useState(false); + const [showFolderAddModal, setShowFolderAddModal] = useState(false); - const tabList = ["프로필", "카테고리", "게시글"]; - const categoryData: CategoryType[] = [ + const tabList = ["프로필", "폴더", "게시글"]; + const folderData: FolderType[] = [ { id: "1", name: faker.lorem.words(), - subCategories: [ + subFolders: [ {id: "4", name: faker.lorem.words()}, {id: "5", name: faker.lorem.words()} ] @@ -40,7 +40,7 @@ function SettingPage() { { id: "3", name: faker.lorem.words(), - subCategories: [ + subFolders: [ {id: "6", name: faker.lorem.words()}, {id: "7", name: faker.lorem.words()}, {id: "8", name: faker.lorem.words()} @@ -49,8 +49,8 @@ function SettingPage() { ]; const [selectedTab, setSelectedTab] = useState("프로필"); - const [categories, setCategories] = useState(categoryData); - const [selectedCategory, setSelectedCategory] = useState(null); + const [folders, setFolders] = useState(folderData); + const [selectedFolder, setSelectedFolder] = useState(null); const [isHover, setIsHover] = useState(false); const handleHover = (hover: boolean) => { @@ -64,93 +64,93 @@ function SettingPage() { return; } - let categoryIndex = -1; + let folderIndex = -1; let oldIndex = -1; let newIndex = -1; - oldIndex = categories.findIndex(category => category.id === active.id); - newIndex = categories.findIndex(category => category.id === over.id); + oldIndex = folders.findIndex(folder => folder.id === active.id); + newIndex = folders.findIndex(folder => folder.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - setCategories(category => { - return arrayMove(category, oldIndex, newIndex); + setFolders(folder => { + return arrayMove(folder, oldIndex, newIndex); }); return; } - for (const category of categories) { - if (category.subCategories) { - oldIndex = category.subCategories.findIndex(category => category.id === active.id); - newIndex = category.subCategories.findIndex(category => category.id === over.id); + for (const folder of folders) { + if (folder.subFolders) { + oldIndex = folder.subFolders.findIndex(folder => folder.id === active.id); + newIndex = folder.subFolders.findIndex(folder => folder.id === over.id); if (oldIndex !== -1 && newIndex !== -1) { - categoryIndex = categories.indexOf(category); + folderIndex = folders.indexOf(folder); break; } } } - if (categoryIndex !== -1 && oldIndex !== -1 && newIndex !== -1) { - setCategories(currentCategories => { - const updatedSubCategories = [...currentCategories[categoryIndex].subCategories!]; - const [movedCategory] = updatedSubCategories.splice(oldIndex, 1); - updatedSubCategories.splice(newIndex, 0, movedCategory); + if (folderIndex !== -1 && oldIndex !== -1 && newIndex !== -1) { + setFolders(currentFolders => { + const updatedSubFolders = [...currentFolders[folderIndex].subFolders!]; + const [movedFolder] = updatedSubFolders.splice(oldIndex, 1); + updatedSubFolders.splice(newIndex, 0, movedFolder); - const updatedCategories = [...currentCategories]; - updatedCategories[categoryIndex] = { - ...updatedCategories[categoryIndex], - subCategories: updatedSubCategories + const updatedFolders = [...currentFolders]; + updatedFolders[folderIndex] = { + ...updatedFolders[folderIndex], + subFolders: updatedSubFolders }; - return updatedCategories; + return updatedFolders; }); } } - const handleCategoryMove = (categoryId: string) => { - setCategories(prevCategories => { - const newCategories = [...prevCategories]; - let categoryToMove; - let targetCategory: CategoryType | undefined; - - newCategories.forEach(category => { - if (category.subCategories) { - const subCategoryIndex = category.subCategories.findIndex(sub => sub.id === selectedCategory?.id); - if (subCategoryIndex !== -1) { - categoryToMove = category.subCategories[subCategoryIndex]; - category.subCategories.splice(subCategoryIndex, 1); + const handleFolderMove = (folderId: string) => { + setFolders(prevFolders => { + const newFolders = [...prevFolders]; + let folderToMove; + let targetFolder: FolderType | undefined; + + newFolders.forEach(folder => { + if (folder.subFolders) { + const subFolderIndex = folder.subFolders.findIndex(sub => sub.id === selectedFolder?.id); + if (subFolderIndex !== -1) { + folderToMove = folder.subFolders[subFolderIndex]; + folder.subFolders.splice(subFolderIndex, 1); } } - if (category.id === categoryId) { - targetCategory = category; + if (folder.id === folderId) { + targetFolder = folder; } }); - if (!categoryToMove) { - return prevCategories; + if (!folderToMove) { + return prevFolders; } - if (categoryId === "top") { - newCategories.push(categoryToMove); - return newCategories; + if (folderId === "top") { + newFolders.push(folderToMove); + return newFolders; } - if (!targetCategory) { - return prevCategories; + if (!targetFolder) { + return prevFolders; } - if (!targetCategory.subCategories) { - targetCategory.subCategories = []; + if (!targetFolder.subFolders) { + targetFolder.subFolders = []; } - targetCategory.subCategories.push(categoryToMove); + targetFolder.subFolders.push(folderToMove); - return newCategories; + return newFolders; }); } - const submitCategoryChange = () => { + const submitFolderChange = () => { if (confirm("변경사항을 저장하시겠습니까?")) { alert("저장되었습니다."); navigate(0); @@ -173,17 +173,17 @@ function SettingPage() { )}
- {(selectedTab === "카테고리") && + {(selectedTab === "폴더") &&
- + submitFolderChange={submitFolderChange}/>
} {(selectedTab === "게시글") &&
@@ -194,14 +194,14 @@ function SettingPage() {
}
- {showCategoryModal && } - {showCategoryAddModal && } + {showFolderModal && } + {showFolderAddModal && } ); } From f6c6f7e8750cb982009b595a15344c63ca8bfa1a Mon Sep 17 00:00:00 2001 From: typhoon0678 Date: Wed, 12 Feb 2025 21:13:38 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20=EA=B2=8C=EC=8B=9C=EA=B8=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20api=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common/apis/post.tsx | 8 ++- src/common/router/postRouter.tsx | 6 +- src/common/types/post.tsx | 8 +++ src/common/util/sort.tsx | 19 +++++++ src/pages/blog/BlogPage.tsx | 2 +- src/pages/post/PostPage.tsx | 28 +++++++--- src/pages/post/WritePage.tsx | 95 +++++++++++++++++++++++++++++--- 7 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 src/common/util/sort.tsx diff --git a/src/common/apis/post.tsx b/src/common/apis/post.tsx index 7963419..c7e5473 100644 --- a/src/common/apis/post.tsx +++ b/src/common/apis/post.tsx @@ -1,10 +1,16 @@ import axiosApi from "./AxiosApi.tsx"; -import {PostListRequest, PostRequest} from "../types/post.tsx"; +import {PostListRequest, PostRequest, PostUpdateRequest} from "../types/post.tsx"; import {postListRequestToParameter} from "../util/url.tsx"; export const createPost = async (postRequest: PostRequest) => await axiosApi.post("/post", postRequest); +export const updatePost = async (postUpdateRequest: PostUpdateRequest) => + await axiosApi.patch(`/post`, postUpdateRequest); + +export const deletePost = async (id: string) => + await axiosApi.patch(`/post/delete/${id}`); + export const getPost = async (id: string) => await axiosApi.get(`/post/${id}`); diff --git a/src/common/router/postRouter.tsx b/src/common/router/postRouter.tsx index 1188aa8..f27b41a 100644 --- a/src/common/router/postRouter.tsx +++ b/src/common/router/postRouter.tsx @@ -1,9 +1,13 @@ import {Suspense} from "react"; -import {Loading, Post} from "./page.tsx"; +import {Loading, Post, Write} from "./page.tsx"; const postRouter = () => { return [ + { + path: 'edit/:id', + element: + }, { path: ':id', element: diff --git a/src/common/types/post.tsx b/src/common/types/post.tsx index 9bebd5e..c3a8a7e 100644 --- a/src/common/types/post.tsx +++ b/src/common/types/post.tsx @@ -7,6 +7,14 @@ export interface PostRequest { urls: string[], } +export interface PostUpdateRequest { + id: string, + title: string, + content: string, + tagNames: string[], + urls: string[], +} + export interface PostListRequest { sorts: string[], page: number, diff --git a/src/common/util/sort.tsx b/src/common/util/sort.tsx new file mode 100644 index 0000000..d3253f0 --- /dev/null +++ b/src/common/util/sort.tsx @@ -0,0 +1,19 @@ +import {TagResponse} from "../types/post.tsx"; + +export const sortByName = (names: string[]) => { + names.sort((a: string, b: string) => { + if (a < b) return -1; + else if (a > b) return 1; + return 0; + }); + return names; +} + +export const sortTagByName = (tags: TagResponse[]) => { + tags.sort((a: TagResponse, b: TagResponse) => { + if (a.name < b.name) return -1; + else if (a.name > b.name) return 1; + return 0; + }); + return tags; +} \ No newline at end of file diff --git a/src/pages/blog/BlogPage.tsx b/src/pages/blog/BlogPage.tsx index 6eb0e1f..113b51a 100644 --- a/src/pages/blog/BlogPage.tsx +++ b/src/pages/blog/BlogPage.tsx @@ -166,7 +166,7 @@ function SideBar({username, addTag, setSelectedFolder, bgColor}: { {username === loginState.username && (
-
)} diff --git a/src/pages/post/PostPage.tsx b/src/pages/post/PostPage.tsx index 7cef8b7..42dab10 100644 --- a/src/pages/post/PostPage.tsx +++ b/src/pages/post/PostPage.tsx @@ -7,11 +7,16 @@ import {getPost} from "../../common/apis/post.tsx"; import {useEffect, useState} from "react"; import {PostResponse} from "../../common/types/post.tsx"; import {checkUUID} from "../../common/util/regex.tsx"; +import {sortTagByName} from "../../common/util/sort.tsx"; +import {useSelector} from "react-redux"; +import {RootState} from "../../store.tsx"; +import {FillLink} from "../../components/common/FillButton.tsx"; function PostPage() { const {id} = useParams(); const navigate = useNavigate(); + const loginState = useSelector((state: RootState) => state.loginSlice); const [post, setPost] = useState({ id: "", @@ -31,10 +36,10 @@ function PostPage() { getPost(id) .then((res) => { - console.log(res.data); setPost({ ...res.data, id: id, + tags: sortTagByName(res.data.tags), createdAt: new Date(), }); }) @@ -47,12 +52,18 @@ function PostPage() {
-
- Home -
{` > `}
- 폴더 -
{` > `}
-
{post.title}
+
+ {(loginState.username === post.username) + &&
} +
+ Home +
{` > `}
+ 폴더 +
{` > `}
+
{post.title}
+
+ {(loginState.username === post.username) + && }
- ); + ) + ; } export default PostPage; \ No newline at end of file diff --git a/src/pages/post/WritePage.tsx b/src/pages/post/WritePage.tsx index 139f7aa..a101ef3 100644 --- a/src/pages/post/WritePage.tsx +++ b/src/pages/post/WritePage.tsx @@ -1,16 +1,19 @@ import BasicLayout from "../../layout/BasicLayout.tsx"; import {useEffect, useRef, useState} from "react"; -import {useBlocker, useNavigate} from "react-router-dom"; +import {useBlocker, useLocation, useNavigate, useParams} from "react-router-dom"; import {Editor} from "@tinymce/tinymce-react"; import {FillButton} from "../../components/common/FillButton.tsx"; import {useSelector} from "react-redux"; import {RootState} from "../../store.tsx"; import {faker} from "@faker-js/faker/locale/ko"; import {MdOutlineArrowDropDown, MdOutlineClear} from "react-icons/md"; -import {createPost} from "../../common/apis/post.tsx"; +import {createPost, deletePost, getPost, updatePost} from "../../common/apis/post.tsx"; import LoadingLayout from "../../layout/LoadingLayout.tsx"; import {getImgUrls} from "../../common/util/html.tsx"; import {uploadImage} from "../../common/apis/image.tsx"; +import {checkUUID} from "../../common/util/regex.tsx"; +import {TagResponse} from "../../common/types/post.tsx"; +import {sortByName} from "../../common/util/sort.tsx"; interface WritePostType { inputTag: string; @@ -29,6 +32,11 @@ function WritePage() { const loginState = useSelector((state: RootState) => state.loginSlice); const navigate = useNavigate(); const folderRef = useRef(null); + const path = useLocation().pathname.substring(0, location.pathname.lastIndexOf("/")); + const {id} = useParams(); + + console.log(path); + console.log(id); const [loading, setLoading] = useState(false); const [post, setPost] = useState({ @@ -87,6 +95,46 @@ function WritePage() { .finally(() => setLoading(false)); } + const handleEdit = () => { + if (uploadCount > 0) { + alert("업로드 중인 이미지가 있습니다. 잠시만 기다려주세요."); + return; + } + + if (!id) return; + setLoading(true); + + const urls: string[] = getImgUrls(post.content); + + updatePost({ + id: id, + title: post.title, + content: post.content, + tagNames: post.tags, + urls: urls, + }) + .then(() => { + alert("수정되었습니다."); + setExitPage(true); + navigate(`/blog/${loginState.username}username`); + }) + .catch((error) => alert(error.response.data.message)) + .finally(() => setLoading(false)); + } + + const handleDelete = (id: string | undefined) => { + if (!id || !confirm("정말 삭제하시겠습니까?")) { + return; + } + + deletePost(id) + .then(() => { + alert("삭제되었습니다."); + navigate(`/blog/${loginState.username}`); + }) + .catch((error) => alert(error.response.data.message)); + } + const handleClickOutside = (event: MouseEvent) => { if (folderRef.current && !folderRef.current.contains(event.target as Node)) { setFolderOpen(false); @@ -138,12 +186,41 @@ function WritePage() { ]; useEffect(() => { - if (!loginState.isLogin) { - alert("해당 페이지 이용에는 로그인이 필요합니다."); - navigate("/login"); + if (!path.endsWith("edit")) { + return; } + + if (!id || !checkUUID(id)) { + alert("올바르지 않은 url입니다."); + navigate(-1); + return; + } + + getPost(id) + .then((res) => { + setPost({ + ...post, + title: res.data.title, + content: res.data.content, + tags: sortByName(res.data.tags.map((tag: TagResponse) => tag.name)), + }); + }) + .catch((error) => alert(error.response.data.message)); + + // 폴더 불러오기 api + // if (loginState.isLogin) { + // alert("해당 페이지 이용에는 로그인이 필요합니다."); + // navigate("/login"); + // } }, []); + useEffect(() => { + if (uploadCount > 0) { + setLoading(true); + } + setLoading(false) + }, [uploadCount]); + return (
@@ -247,8 +324,12 @@ function WritePage() { value={post.content} onEditorChange={(content) => setPost({...post, content: content})} /> -
- +
+ {path.endsWith("/edit") + && handleDelete(id)} addStyle={"bg-red-400 hover:bg-red-700"}/>} + {path.endsWith("/edit") + ? + : }