diff --git a/src/App.tsx b/src/App.tsx index b91763b..01e2be0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { ThemeProvider } from 'styled-components'; @@ -30,22 +30,14 @@ import { ToastList } from './components/common'; // Store import { userTokenState } from './stores/user'; import { useEffect } from 'react'; -import { categoryState } from './stores/category'; -import { getCategories } from './apis/category'; -import handleCategory from './utils/handleCategory'; +import useUpdateCategories from './hooks/useUpdateCategories'; const App = () => { - const setCategories = useSetRecoilState(categoryState); const userToken = useRecoilValue(userTokenState); - const { initializeCategory } = handleCategory(); + const { updateCategories } = useUpdateCategories(); useEffect(() => { - userToken && - getCategories() - .then((res) => { - setCategories(initializeCategory(res)); - }) - .catch((err) => console.log(err)); - }, [userToken]); + userToken && updateCategories(); + }, [updateCategories, userToken]); return ( diff --git a/src/apis/category.ts b/src/apis/category.ts index 1de5bbe..be348b5 100644 --- a/src/apis/category.ts +++ b/src/apis/category.ts @@ -1,9 +1,11 @@ +import { APIResponse } from '@/models/config/axios'; import axiosInstance from './config/instance'; +import { ICreateCategoryResponse } from '@/models/category'; // 모든 카테고리 가져오는 API export const getCategories = async () => { const response = await axiosInstance.get('/category'); - return response.data.result; + return response.data; }; // 카테고리 이동1 API @@ -14,13 +16,13 @@ export const putSubToOtherTop = async ( const response = await axiosInstance.put( `/category/${categoryId}/${topCategoryId}`, ); - return response.data.result; + return response.data; }; // 카테고리 이동2 API export const putSubToTop = async (categoryId: number) => { const response = await axiosInstance.put(`/category/up/${categoryId}`); - return response.data.result; + return response.data; }; // 카테고리 이동3 API @@ -31,17 +33,36 @@ export const putTopToOtherTop = async ( const response = await axiosInstance.put( `/category/down/${categoryId}/${topCategoryId}`, ); - return response.data.result; + return response.data; }; // 상위 카테고리 추가 API -export const postTopCategroy = async () => { - const response = await axiosInstance.post('/category'); - return response.data.result; +export const postTopCategroy = async ( + name: string, +): Promise> => { + const response = await axiosInstance.post('/category', { name }); + return response.data; }; -// 상위 카테고리 추가 API -export const postSubCategroy = async (topCategoryId: number) => { - const response = await axiosInstance.post(`/category/${topCategoryId}`); - return response.data.result; +// 하위 카테고리 추가 API +export const postSubCategroy = async ( + name: string, + topCategoryId: number, +): Promise> => { + const response = await axiosInstance.post(`/category/${topCategoryId}`, { + name, + }); + return response.data; +}; + +// 카테고리 삭제 API +export const deleteCategory = async (category_id: number) => { + const response = await axiosInstance.delete(`/category/${category_id}`); + return response.data; +}; + +// 카테고리 이름 수정 API +export const updateCategoryName = async (name: string, categoryId: number) => { + const response = await axiosInstance.put(`/category/${categoryId}`, { name }); + return response.data; }; diff --git a/src/components/layout/sideBar/SubCategory.tsx b/src/components/layout/sideBar/SubCategory.tsx index e3fd88f..fbb3c44 100644 --- a/src/components/layout/sideBar/SubCategory.tsx +++ b/src/components/layout/sideBar/SubCategory.tsx @@ -15,6 +15,7 @@ interface ISubCategoryProps { setIsDeleteModalOpen: React.Dispatch>; grabedCategory: React.MutableRefObject; putCategoryFolder: () => void; + setCategoryId: React.Dispatch>; } const SubCategory = ({ @@ -25,14 +26,26 @@ const SubCategory = ({ setIsDeleteModalOpen, grabedCategory, putCategoryFolder, + setCategoryId, }: ISubCategoryProps) => { const [subFolderOptionModalOpen, setSubFolderOptionModalOpen] = useState(false); const [isEditing, setIsEditing] = useState(false); const [edit, setEdit] = useState(name); + const [beforeEdit, setBeforeEdit] = useState(edit); + const { editText, finishEdit } = handleEdit(); const [editNameRef] = useOutsideClick(() => - setIsEditing(false), + finishEdit( + edit, + setEdit, + beforeEdit, + setIsEditing, + nameRegex, + setNameRegex, + categoryId, + ), ); + const [nameRegex, setNameRegex] = useState(true); const [subFolderOptionModalRef] = useOutsideClick(() => setSubFolderOptionModalOpen(false), @@ -44,7 +57,9 @@ const SubCategory = ({ e.stopPropagation(); if (option === '수정') { setIsEditing(true); + setBeforeEdit(edit); } else if (option === '삭제') { + setCategoryId(categoryId); setIsDeleteModalOpen(true); } setSubFolderOptionModalOpen(false); @@ -57,7 +72,7 @@ const SubCategory = ({ }; const handleInput = (e: React.ChangeEvent) => - handleEdit(e, setEdit); + editText(e, setEdit, setNameRegex); const handleDragStart = () => (grabedCategory.current = { @@ -76,7 +91,10 @@ const SubCategory = ({ onDragEnd={putCategoryFolder} > {isEditing ? ( - + ; setIsSubCategoryModalOpen: React.Dispatch>; setIsDeleteModalOpen: React.Dispatch>; + setCategoryId: React.Dispatch>; putCategoryFolder: () => void; } @@ -36,6 +37,7 @@ const TopCategory = ({ setIsSubCategoryModalOpen, setIsDeleteModalOpen, putCategoryFolder, + setCategoryId, }: ITopCategoryProps) => { const [folderOptionModalOpen, setFolderOptionModalOpen] = useState(false); const [folderOptionModalRef] = useOutsideClick(() => @@ -45,18 +47,22 @@ const TopCategory = ({ const [isEditing, setIsEditing] = useState(false); const [edit, setEdit] = useState(name); const [beforeEdit, setBeforeEdit] = useState(edit); + const { editText, finishEdit } = handleEdit(); - const categoryNameRegex = /^[a-zA-Z0-9가-힣\s]*$/; const [nameRegex, setNameRegex] = useState(true); const options = ['추가', '수정', '삭제', '이동']; - const finishEdit = () => { - if (!edit.length) { - setEdit(beforeEdit); - } - setIsEditing(false); - }; - const [editNameRef] = useOutsideClick(finishEdit); + const [editNameRef] = useOutsideClick(() => + finishEdit( + edit, + setEdit, + beforeEdit, + setIsEditing, + nameRegex, + setNameRegex, + categoryId, + ), + ); const handleOptionClick = (e: React.MouseEvent, option: string) => { e.stopPropagation(); @@ -66,6 +72,7 @@ const TopCategory = ({ setIsEditing(true); setBeforeEdit(edit); } else if (option === '삭제') { + setCategoryId(categoryId); setIsDeleteModalOpen(true); } setFolderOptionModalOpen(false); @@ -76,14 +83,8 @@ const TopCategory = ({ setFolderOptionModalOpen(true); }; - const handleInput = (e: React.ChangeEvent) => { - if (!categoryNameRegex.test(e.target.value)) { - setNameRegex(false); - return; - } - setNameRegex(true); - handleEdit(e, setEdit); - }; + const handleInput = (e: React.ChangeEvent) => + editText(e, setEdit, setNameRegex); const handleDragStart = () => (grabedCategory.current = { categoryId: categoryId, @@ -176,6 +177,7 @@ const TopCategory = ({ setIsDeleteModalOpen={setIsDeleteModalOpen} grabedCategory={grabedCategory} putCategoryFolder={putCategoryFolder} + setCategoryId={setCategoryId} key={`${subFolder.name}-${subFolder.categoryId}`} /> ))} diff --git a/src/components/layout/sideBar/UserMode.tsx b/src/components/layout/sideBar/UserMode.tsx index be82a49..9b5fe42 100644 --- a/src/components/layout/sideBar/UserMode.tsx +++ b/src/components/layout/sideBar/UserMode.tsx @@ -1,30 +1,33 @@ import LookSvg from '@/assets/icons/look.svg?react'; import * as UserModeStyle from '@/styles/layout/sideBar/UserMode.style'; -import { useLocation } from 'react-router-dom'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { topCategoryModalState } from '@/stores/modal'; import AddCategoryModal from '@/components/modals/AddCategoryModal'; import SuccessAddCategoryModal from '@/components/modals/SuccessAddCategoryModal'; import { useRef, useState } from 'react'; import TopCategory from './TopCategory'; import DeleteCategory from './DeleteCategory'; -import handleCategory from '@/utils/handleCategory'; import { categoryState } from '@/stores/category'; import { IFolderProps, ISubFolderProps } from 'types/category'; import useMoveCategory from '@/hooks/useMoveCategory'; +import { deleteCategory } from '@/apis/category'; +import useUpdateCategories from '@/hooks/useUpdateCategories'; const UserMode = () => { const isTopCategoryModalOpen = useRecoilValue(topCategoryModalState); const [isSuccessAddCategoryModalOpen, setIsSuccessAddCategoryModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isSubAdded, setIsSubAdded] = useState(false); const [categoryName, setCategoryName] = useState(''); - const [categories, setCategories] = useRecoilState(categoryState); + const [categoryId, setCategoryId] = useState(null); + const categories = useRecoilValue(categoryState); const [isSubCategoryModalOpen, setIsSubCategoryModalOpen] = useState(false); const grabedCategory = useRef(undefined); const dropedCategory = useRef(undefined); - const { deleteSubCategory } = handleCategory(); + const navigate = useNavigate(); + + const { updateCategories } = useUpdateCategories(); const { subToOtherTop, subToTop, topToOtherTop } = useMoveCategory(); @@ -34,14 +37,11 @@ const UserMode = () => { const topId = Number(href[0]); const subId = Number(href[1]); - const handleDeleteCategory = () => { - if (!isNaN(subId)) { - setCategories([...deleteSubCategory(categories, topId, subId)]); - } else { - const newData = categories.filter( - (myFolder) => myFolder.categoryId !== topId, - ); - setCategories([...newData]); + const handleDeleteCategory = async () => { + const response = await deleteCategory(categoryId!); + if (response.isSuccess) { + updateCategories(); + navigate('/category/recent'); } setIsDeleteModalOpen(false); }; @@ -85,6 +85,7 @@ const UserMode = () => { setIsSubCategoryModalOpen={setIsSubCategoryModalOpen} setIsDeleteModalOpen={setIsDeleteModalOpen} putCategoryFolder={putCategoryFolder} + setCategoryId={setCategoryId} key={`${category.name}-${category.categoryId}`} /> ))} @@ -96,8 +97,8 @@ const UserMode = () => { categoryName={categoryName} setCategoryName={setCategoryName} setIsSuccessAddCategoryModalOpen={setIsSuccessAddCategoryModalOpen} - setIsSubAdded={setIsSubAdded} topCategoryId={topId} + setCategoryId={setCategoryId} /> )} {isDeleteModalOpen && ( @@ -111,9 +112,7 @@ const UserMode = () => { categoryName={categoryName} setCategoryName={setCategoryName} setIsSuccessAddCategoryModalOpen={setIsSuccessAddCategoryModalOpen} - isSubAdded={isSubAdded} - setIsSubAdded={setIsSubAdded} - topId={topId} + categoryId={categoryId} /> )} diff --git a/src/components/modals/AddCategoryModal.tsx b/src/components/modals/AddCategoryModal.tsx index 4e934e0..3efb0fa 100644 --- a/src/components/modals/AddCategoryModal.tsx +++ b/src/components/modals/AddCategoryModal.tsx @@ -13,12 +13,13 @@ import { import { ICommonModalProps } from 'types/modal'; import handleEdit from '@/utils/handleEdit'; import { postSubCategroy, postTopCategroy } from '@/apis/category'; +import useUpdateCategories from '@/hooks/useUpdateCategories'; interface IAddTopCategoryModalProps extends ICommonModalProps { isTopCategoryModalOpen: boolean; setIsSubCategoryModalOpen: React.Dispatch>; - setIsSubAdded: React.Dispatch>; topCategoryId: number; + setCategoryId: React.Dispatch>; } const AddCategoryModal = ({ @@ -27,10 +28,12 @@ const AddCategoryModal = ({ categoryName, setCategoryName, setIsSuccessAddCategoryModalOpen, - setIsSubAdded, topCategoryId, + setCategoryId, }: IAddTopCategoryModalProps) => { const setIsTopCategoryModalOpen = useSetRecoilState(topCategoryModalState); + const { updateCategories } = useUpdateCategories(); + const { editText } = handleEdit(); const [isFocused, setIsFocused] = useState(false); @@ -42,19 +45,28 @@ const AddCategoryModal = ({ isTopCategoryModalOpen ? setIsTopCategoryModalOpen(false) : setIsSubCategoryModalOpen(false); - !isTopCategoryModalOpen && setIsSubAdded(true); }; const [topCategoryModalRef] = useOutsideClick(onCloseModal); const handleInputCategoryName = (e: React.ChangeEvent) => - handleEdit(e, setCategoryName); + editText(e, setCategoryName); - const addCategory = (e: React.MouseEvent) => { - isTopCategoryModalOpen ? postTopCategroy() : postSubCategroy(topCategoryId); + const addCategory = async (e: React.MouseEvent) => { + const response = isTopCategoryModalOpen + ? await postTopCategroy(categoryName) + : await postSubCategroy(categoryName, topCategoryId); + if (response.isSuccess) { + updateCategories(); + setCategoryId( + isTopCategoryModalOpen + ? response.result.categoryId + : response.result.topCategoryId, + ); + setIsSuccessAddCategoryModalOpen(true); + } e.stopPropagation(); onCloseModal(); - setIsSuccessAddCategoryModalOpen(true); }; return ( diff --git a/src/components/modals/SuccessAddCategoryModal.tsx b/src/components/modals/SuccessAddCategoryModal.tsx index 02c1f7c..b3d12c2 100644 --- a/src/components/modals/SuccessAddCategoryModal.tsx +++ b/src/components/modals/SuccessAddCategoryModal.tsx @@ -7,58 +7,25 @@ import { import CloseSvg from '@/assets/icons/close.svg?react'; import * as SuccessAddCategoryStyles from '@/styles/modals/SuccessAddCategoryModal.style'; import { ICommonModalProps } from 'types/modal'; -import { useRecoilState } from 'recoil'; -import { categoryState } from '@/stores/category'; - import FileImage from '@/assets/file.png'; interface ISuccessAddCategory extends ICommonModalProps { - isSubAdded: boolean; - setIsSubAdded: React.Dispatch>; - topId: number; + categoryId: number | null; } const SuccessAddCategoryModal = ({ categoryName, setCategoryName, setIsSuccessAddCategoryModalOpen, - isSubAdded, - setIsSubAdded, - topId, + categoryId, }: ISuccessAddCategory) => { const onCloseModal = () => { setIsSuccessAddCategoryModalOpen(false); setCategoryName(''); }; - const [categories, setCategories] = useRecoilState(categoryState); const [successAddCategoryModalRef] = useOutsideClick(onCloseModal); - - const handleGoToCategory = () => { - if (isSubAdded) { - const index = categories.findIndex( - (folder) => folder.categoryId === topId, - ); - categories[index].subFolders.push({ - name: categoryName, - categoryId: categories[index].categoryId, - topCategoryId: topId, - }); - } else { - setCategories([ - ...categories, - { - categoryId: categories.length + 1, - name: categoryName, - topCategoryId: null, - subFolders: [], - }, - ]); - } - setIsSubAdded(false); - onCloseModal(); - }; return ( @@ -73,12 +40,8 @@ const SuccessAddCategoryModal = ({ 생성 완료! 보러가기 diff --git a/src/hooks/useUpdateCategories.ts b/src/hooks/useUpdateCategories.ts new file mode 100644 index 0000000..50e8fd9 --- /dev/null +++ b/src/hooks/useUpdateCategories.ts @@ -0,0 +1,18 @@ +import { getCategories } from '@/apis/category'; +import { categoryState } from '@/stores/category'; +import handleCategory from '@/utils/handleCategory'; +import { useSetRecoilState } from 'recoil'; + +const useUpdateCategories = () => { + const setCategories = useSetRecoilState(categoryState); + const { initializeCategory } = handleCategory(); + const updateCategories = async () => { + await getCategories() + .then((res) => setCategories(initializeCategory(res.result))) + .catch((err) => console.log(err)); + }; + + return { updateCategories }; +}; + +export default useUpdateCategories; diff --git a/src/models/category.ts b/src/models/category.ts new file mode 100644 index 0000000..802adf4 --- /dev/null +++ b/src/models/category.ts @@ -0,0 +1,5 @@ +export interface ICreateCategoryResponse { + topCategoryId: number | null; + categoryId: number; + name: string; +} diff --git a/src/styles/layout/footer/index.ts b/src/styles/layout/footer/index.ts index e2a6ea3..52b87e6 100644 --- a/src/styles/layout/footer/index.ts +++ b/src/styles/layout/footer/index.ts @@ -6,7 +6,7 @@ export const Container = styled.footer` background-color: ${theme.color.gray100}; ${theme.typography.Body1}; position: relative; - z-index: 1; + z-index: -1; `; export const SendEmailWrap = styled.div` @@ -38,7 +38,6 @@ export const SendEmailInput = styled.input` `; export const SendEmailButton = styled.button` - /* padding: 7px 28px; */ width: 98px; height: 40px; border-radius: 8px; diff --git a/src/styles/layout/sideBar/Option.style.ts b/src/styles/layout/sideBar/Option.style.ts index 0b48db9..2adfb59 100644 --- a/src/styles/layout/sideBar/Option.style.ts +++ b/src/styles/layout/sideBar/Option.style.ts @@ -21,7 +21,8 @@ export const OptionsWrap = styled.div` export const OptionButton = styled.button` cursor: pointer; background-color: ${theme.color.white}; - padding: 12px 71.25px; + padding: 12px 0; + width: 167.5px; border: 0; color: ${theme.color.gray400}; ${theme.typography.Body3} diff --git a/src/styles/layout/sideBar/SubCategory.style.ts b/src/styles/layout/sideBar/SubCategory.style.ts index 2358ef7..75ee617 100644 --- a/src/styles/layout/sideBar/SubCategory.style.ts +++ b/src/styles/layout/sideBar/SubCategory.style.ts @@ -13,6 +13,9 @@ export const EditNameInputWrap = styled.div` border: 1px solid ${theme.color.gray200}; width: 100%; border-radius: 100px; + &.warning { + border: 1px solid ${theme.color.red}; + } `; export const EditNameInput = styled.input` diff --git a/src/utils/handleEdit.ts b/src/utils/handleEdit.ts index a3fed68..70ad682 100644 --- a/src/utils/handleEdit.ts +++ b/src/utils/handleEdit.ts @@ -1,9 +1,41 @@ -const handleEdit = ( - e: React.ChangeEvent, - setValue: React.Dispatch>, -) => { - if (e.target.value.length > 10) return; - setValue(e.target.value); -}; +import { updateCategoryName } from '@/apis/category'; + +const handleEdit = () => { + const editText = ( + e: React.ChangeEvent, + setValue: React.Dispatch>, + setNameRegex?: React.Dispatch>, + ) => { + const categoryNameRegex = /^[a-zA-Z0-9가-힣\s]*$/; + if (e.target.value.length > 10) return; + if (!categoryNameRegex.test(e.target.value)) { + setNameRegex && setNameRegex(false); + } else { + setNameRegex && setNameRegex(true); + } + setValue(e.target.value); + }; + const finishEdit = ( + edit: string, + setEdit: React.Dispatch>, + beforeEdit: string, + setIsEditing: React.Dispatch>, + nameRegex: boolean, + setNameRegex: React.Dispatch>, + categoryId: number, + ) => { + if (!edit.length || !nameRegex) { + setEdit(beforeEdit); + setIsEditing(false); + setNameRegex(true); + setIsEditing(false); + return; + } + if (edit !== beforeEdit) updateCategoryName(edit, categoryId); + setIsEditing(false); + }; + + return { editText, finishEdit }; +}; export default handleEdit;