diff --git a/src/components/folder/FolderCardListTest.tsx b/src/components/folder/FolderCardListTest.tsx new file mode 100644 index 0000000..425effb --- /dev/null +++ b/src/components/folder/FolderCardListTest.tsx @@ -0,0 +1,178 @@ +import {SortableContext, useSortable, verticalListSortingStrategy} from "@dnd-kit/sortable"; +import {FolderType} from "../../common/types/blog.tsx"; +import {DndContext, DragEndEvent} from "@dnd-kit/core"; +import {CSS} from "@dnd-kit/utilities"; +import {FillButton} from "../common/FillButton.tsx"; +import {MdOutlineMenu} from "react-icons/md"; +import {TextButton} from "../common/TextButton.tsx"; +import {restrictToVerticalAxis} from "@dnd-kit/modifiers"; + +function FolderCardList({ + folders, + folderEditType, + depth = 0, + editFolderId, + editFolderTitle, + setEditFolderId, + setEditFolderTitle, + handleEdit, + addFolder, + handleOnDragEnd, + handleDelete, + setSelectedFolder, + setOpenMoveModal + }: { + folders: FolderType[], + folderEditType: string, + depth?: number, + editFolderId: string, + editFolderTitle: string, + setEditFolderId: (editFolderId: string) => void, + setEditFolderTitle: (editFolderTitle: string) => void, + handleEdit: (editFolder: FolderType) => void, + addFolder: (targetFolder: FolderType) => void, + handleOnDragEnd: (event: DragEndEvent) => void, + handleDelete: (deleteFolder: FolderType) => void, + setOpenMoveModal: (openMoveModal: boolean) => void, + setSelectedFolder: (selectedFolder: FolderType) => void, +}) { + + return ( + + + {folders.map((folder: FolderType) => ( + + ))} + + + ); +} + +function FolderCard({ + folder, + folderEditType, + depth, + editFolderId, + editFolderTitle, + setEditFolderId, + setEditFolderTitle, + handleEdit, + addFolder, + handleOnDragEnd, + handleDelete, + setSelectedFolder, + setOpenMoveModal + }: { + folder: FolderType, + folderEditType: string, + depth: number, + editFolderId: string, + editFolderTitle: string, + setEditFolderId: (editFolderId: string) => void, + setEditFolderTitle: (editFolderTitle: string) => void, + handleEdit: (editFolder: FolderType) => void, + addFolder: (targetFolder: FolderType) => void, + handleOnDragEnd: (event: DragEndEvent) => void, + handleDelete: (deleteFolder: FolderType) => void, + setOpenMoveModal: (openMoveModal: boolean) => void, + setSelectedFolder: (selectedFolder: FolderType) => void, +}) { + + const {attributes, listeners, setNodeRef, transform, transition} = useSortable({id: folder.id}); + + const style = { + transform: CSS.Translate.toString(transform), + transition, + }; + + return ( +
+
+ {folder.id === editFolderId + ?
+ setEditFolderTitle(e.target.value)} + placeholder="폴더 이름"/> +
+ setEditFolderId("")} + addStyle={"!bg-gray-400 hover:brightness-110"}/> + handleEdit({...folder, title: editFolderTitle})} + addStyle={`${folder.title === editFolderTitle && "opacity-50 hover:!cursor-auto"}`} + disabled={folder.title === editFolderTitle}/> +
+
+ :
+
+ {folderEditType === "create" && + } +

{folder.title}

+

({folder.postCount})

+
+
+ {folderEditType === "create" && depth !== 2 && + addFolder(folder)} + addStyle={"text-xs hover:text-lime-600"}/> + } + {folderEditType === "create" && + { + setEditFolderId(folder.id); + setEditFolderTitle(folder.title); + }} addStyle={"text-xs hover:text-lime-600"}/>} + {folderEditType === "delete" && + { + handleDelete(folder); + }} addStyle={"text-xs hover:text-lime-600"}/>} + {folderEditType === "create" && + { + setOpenMoveModal(true); + setSelectedFolder(folder); + }} addStyle={"text-xs hover:text-lime-600"}/>} +
+
} +
+ {folder.subFolders.length > 0 && +
+ +
} +
+ ); +} + + +export default FolderCardList; \ No newline at end of file diff --git a/src/components/setting/SettingSideBar.tsx b/src/components/setting/SettingSideBar.tsx index fcebca3..ee0774f 100644 --- a/src/components/setting/SettingSideBar.tsx +++ b/src/components/setting/SettingSideBar.tsx @@ -11,6 +11,7 @@ function SettingSideBar({setSelectedSection}: { const tabList: TabType[] = [ {section: "profile", title: "프로필"}, {section: "folder", title: "폴더"}, + {section: "folder-test", title: "폴더 테스트"}, {section: "post", title: "게시글"}, ]; diff --git a/src/pages/setting/FolderSettingPageTest.tsx b/src/pages/setting/FolderSettingPageTest.tsx new file mode 100644 index 0000000..b4ae09f --- /dev/null +++ b/src/pages/setting/FolderSettingPageTest.tsx @@ -0,0 +1,400 @@ +import {useEffect, useRef, useState} from "react"; +import {FolderType, toFolderRequestList, toFolderTypeList} from "../../common/types/blog.tsx"; +import {FillButton} from "../../components/common/FillButton.tsx"; +import ModalLayout from "../../layout/ModalLayout.tsx"; +import {DragEndEvent} from "@dnd-kit/core"; +import {arrayMove} from "@dnd-kit/sortable"; +import FolderSelectBox from "../../components/folder/FolderSelectBox.tsx"; +import {getMemberFolders, saveAndUpdateFolder} from "../../common/apis/blog.tsx"; +import {useSelector} from "react-redux"; +import {RootState} from "../../store.tsx"; +import FolderCardListTest from "../../components/folder/FolderCardListTest.tsx"; + +function FolderSettingPage() { + + const loginState = useSelector((state: RootState) => state.loginSlice); + + const [trigger, setTrigger] = useState(false); + const [tempId, setTempId] = useState(0); + const [folders, setFolders] = useState([]); + const [folderEditType, setFolderEditType] = useState(""); // create, delete + + const handleFolderEditType = (type: string) => { + if (folderEditType === "create" && type === "delete" + && !confirm("생성/수정한 폴더 변경사항이 저장됩니다. 계속하시겠습니까?")) { + return; + } + + if (folderEditType === "delete" && type === "create" + && !confirm("삭제한 폴더 변경사항이 저장됩니다. 계속하시겠습니까?")) { + return; + } + + setFolderEditType(type); + } + + const getTempId = () => { + setTempId(prev => prev + 1); + return `temp${tempId}`; + } + + const addFolder = (parentFolder: FolderType | null) => { + const tempTitle = `폴더_${crypto.randomUUID().substring(0, 4)}`; + + if (parentFolder === null) { + const tempId = getTempId(); + setFolders([...folders, {id: tempId, title: tempTitle, postCount: 0, subFolders: []}]); + setEditFolderTitle(""); + setEditFolderId(tempId); + return; + } + + setFolders(prevFolders => getAddFolderList(prevFolders, parentFolder.id, tempTitle)); + } + const getAddFolderList = (folders: FolderType[], id: string, title: string) => { + return folders.map((folder: FolderType): FolderType => { + if (folder.id === id) { + const tempId = getTempId(); + setEditFolderTitle(""); + setEditFolderId(tempId); + return { + ...folder, + subFolders: [...folder.subFolders, {id: tempId, title: title, postCount: 0, subFolders: []}] + }; + } else if (folder.subFolders.length > 0) { + return {...folder, subFolders: getAddFolderList(folder.subFolders, id, title)}; + } + return folder; + }); + } + + const [openMoveModal, setOpenMoveModal] = useState(false); + const [selectedFolder, setSelectedFolder] = useState(null); + const [editFolderId, setEditFolderId] = useState(""); + const [editFolderTitle, setEditFolderTitle] = useState(""); + const [targetFolder, setTargetFolder] = useState({ + id: crypto.randomUUID(), + title: "", + postCount: 0, + subFolders: [], + }); + const [folderMoveType, setFolderMoveType] = useState(0); + const folderMoveTypes = ["폴더 위로 옮깁니다.", "폴더 아래로 옮깁니다.", "폴더 내부로 옮깁니다."]; + + const handleMoveFolder = () => { + if (handleDisabled(folderMoveType) || !selectedFolder) { + alert("활성화된 동작 중에서 선택해주세요."); + return; + } + + if (folderMoveType === 2) { + setFolders(prevFolders => { + const deleteFolderList = getDeleteFolderList(prevFolders, selectedFolder.id); + return getAddFolderModalList(deleteFolderList); + }); + } else { + setFolders(prevFolders => { + const deleteFolderList = getDeleteFolderList(prevFolders, selectedFolder.id); + return getModalMoveFolderList(deleteFolderList); + }); + } + + setOpenMoveModal(false); + } + const getModalMoveFolderList = (folders: FolderType[]) => { + if (!selectedFolder) { + return []; + } + + const targetIndex = folders.findIndex((folder) => folder.id === targetFolder.id); + + if (targetIndex !== -1) { + const moveIndex = (folderMoveType === 0) ? targetIndex : targetIndex + 1; + return folders.slice(0, moveIndex).concat(selectedFolder, folders.slice(moveIndex)); + } + return folders.map((folder: FolderType): FolderType => { + if (folder.subFolders.length > 0) { + return {...folder, subFolders: getModalMoveFolderList(folder.subFolders)}; + } + return folder; + }); + } + const getAddFolderModalList = (folders: FolderType[]) => { + if (!selectedFolder) { + return []; + } + + return folders.map((folder: FolderType): FolderType => { + if (folder.id === targetFolder.id) { + return {...folder, subFolders: [...folder.subFolders, selectedFolder]}; + } else if (folder.subFolders.length > 0) { + return {...folder, subFolders: getAddFolderModalList(folder.subFolders)}; + } + return folder; + }); + } + + const handleOnDragEnd = ({active, over}: DragEndEvent) => { + if (!over || active.id === over.id) { + return; + } + + setFolders(prevFolders => getMoveFolderList(prevFolders, active.id as string, over.id as string)); + } + const getMoveFolderList = (folders: FolderType[], activeId: string, overId: string) => { + const activeIndex = folders.findIndex(folder => folder.id === activeId); + const overIndex = folders.findIndex(folder => folder.id === overId); + + if (activeIndex !== -1 && overIndex !== -1) { + return arrayMove(folders, activeIndex, overIndex); + } + return folders.map((folder: FolderType): FolderType => { + if (folder.subFolders.length > 0) { + return {...folder, subFolders: getMoveFolderList(folder.subFolders, activeId, overId)}; + } + return folder; + }); + } + + const handleDisabled = (moveType: number) => { + if (targetFolder.title === "" || !selectedFolder) { + return true; + } + + const maxDepth = 3; + + const selectedFolderMaxDepth = getMaxFolderDepth(folders, selectedFolder.id) || 1; + const selectedFolderDepth = getFolderDepth(folders, selectedFolder.id) || 1; + const targetFolderDepth = getFolderDepth(folders, targetFolder.id) || 1; + if (moveType === 2) { + return selectedFolderMaxDepth + targetFolderDepth > maxDepth; + } else { + return selectedFolderMaxDepth - selectedFolderDepth + targetFolderDepth > maxDepth; + } + } + + // 하위 폴더의 최대 깊이 + const getMaxFolderDepth = (folders: FolderType[], folderId: string): number | null => { + for (const folder of folders) { + if (folder.id === folderId) { + return calculateMaxDepth(folder.subFolders); + } + + const depth = getMaxFolderDepth(folder.subFolders, folderId); + if (depth !== null) { + return depth; + } + } + + return null; + } + const calculateMaxDepth = (folders: FolderType[]): number => { + if (folders.length === 0) return 1; + + const depths = folders.map(folder => 1 + calculateMaxDepth(folder.subFolders)); + return Math.max(...depths); + }; + // 폴더의 현재 깊이 + const getFolderDepth = (folders: FolderType[], targetId: string, currentDepth: number = 1): number | null => { + for (const folder of folders) { + if (folder.id === targetId) { + return currentDepth; + } + + const depth = getFolderDepth(folder.subFolders, targetId, currentDepth + 1); + if (depth !== null) { + return depth; + } + } + + return null; + }; + + const handleEdit = (editFolder: FolderType) => { + if (editFolder.title.trim() === "") { + alert("폴더 이름을 입력해주세요."); + return; + } + + const handleFolders = getHandleFolders(folders, editFolder.id); + if (handleFolders.findIndex(folder => folder.title === editFolderTitle.trim()) !== -1) { + alert("중복된 폴더 이름입니다."); + return; + } + + setFolders(prevFolders => getEditFolderList(prevFolders, editFolder.id, editFolder.title)); + setEditFolderId(""); + } + const getHandleFolders = (folders: FolderType[], targetId: string): FolderType[] => { + for (const folder of folders) { + if (folder.id === targetId) { + return folders; + } + + if (folder.subFolders) { + const subResult = getHandleFolders(folder.subFolders, targetId); + if (subResult.length > 0) { + return subResult; + } + } + } + + return []; + } + const getEditFolderList = (folders: FolderType[], id: string, title: string) => { + return folders.map((folder: FolderType): FolderType => { + if (folder.id === id) { + return {...folder, title: title}; + } else if (folder.subFolders.length > 0) { + return {...folder, subFolders: getEditFolderList(folder.subFolders, id, title)}; + } + return folder; + }) + } + + const handleDelete = (deleteFolder: FolderType) => { + if (deleteFolder.postCount > 0) { + alert("게시글을 모두 이동한 후에 제거할 수 있습니다."); + return; + } + + if (deleteFolder.subFolders.length > 0) { + alert("하위 폴더를 모두 제거한 후에 제거할 수 있습니다."); + return; + } + + setFolders(prevFolders => getDeleteFolderList(prevFolders, deleteFolder.id)); + } + const getDeleteFolderList = (folders: FolderType[], id: string) => { + if (folders.findIndex(folder => folder.id === id) !== -1) { + return folders.filter(folder => folder.id !== id); + } + + return folders.map((folder: FolderType): FolderType => { + if (folder.subFolders.length > 0) { + return {...folder, subFolders: getDeleteFolderList(folder.subFolders, id)}; + } + return folder; + }) + } + + const handleSubmit = () => { + if (!confirm("변경사항을 저장하시겠습니까?")) { + return; + } + + saveAndUpdateFolder(toFolderRequestList(folders)) + .then(() => { + alert("변경사항이 저장되었습니다."); + setTrigger(prev => !prev); + setFolderEditType(""); + }) + .catch(error => alert(error.response.data.message)); + } + + const modalRef = useRef(null); + const handleClickOutside = (event: MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node)) { + setOpenMoveModal(false); + } + }; + + useEffect(() => { + getMemberFolders(loginState.username) + .then(res => { + setFolders(toFolderTypeList(res.data)); + }) + .catch(error => alert(error.response.data.message)); + }, [trigger]); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + useEffect(() => { + setTargetFolder({...targetFolder, title: ""}); + }, [openMoveModal]); + + return ( +
+

폴더 관리 테스트

+ + {folderEditType === "create" && + + } +
+ {folderEditType !== "create" && + handleFolderEditType("create")} + addStyle={"text-sm bg-lime-700"}/>} + {folderEditType !== "delete" && + handleFolderEditType("delete")} + addStyle={"text-sm bg-red-500"}/>} + {folderEditType !== "" && } +
+ {openMoveModal && ( + +
+
+

+ {selectedFolder?.title} 폴더를 +

+ +
    + {folderMoveTypes.map((moveType, index) => +
  • + setFolderMoveType(index)} + disabled={handleDisabled(index)}/> + +
  • + )} +
+
+
+ { + setOpenMoveModal(false); + }}/> + +
+
+
+ )} +
+ ); +} + +export default FolderSettingPage; \ No newline at end of file diff --git a/src/pages/setting/SettingPage.tsx b/src/pages/setting/SettingPage.tsx index 099c9a2..95d1900 100644 --- a/src/pages/setting/SettingPage.tsx +++ b/src/pages/setting/SettingPage.tsx @@ -5,6 +5,7 @@ import ProfileSettingPage from "./ProfileSettingPage.tsx"; import FolderSettingPage from "./FolderSettingPage.tsx"; import {useNavigate, useParams} from "react-router-dom"; import SettingSideBar from "../../components/setting/SettingSideBar.tsx"; +import FolderSettingPageTest from "./FolderSettingPageTest.tsx"; function SettingPage() { @@ -28,6 +29,10 @@ function SettingPage() {
} + {(selectedSection === "folder-test") && +
+ +
} {(selectedSection === "post") &&