From 5effe9bb81d2f3d4896027a6211eabde9efb1e33 Mon Sep 17 00:00:00 2001 From: Jason <69420498+enigsuss@users.noreply.github.com> Date: Fri, 18 Oct 2024 13:23:17 +0900 Subject: [PATCH] [FEAT] pick dnd (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: nodeApi로 변환 로직 추가 * fix: 임시 디바운싱 적용 * fix: api 교체 * fix: 간헐적으로 unclassified 안보이는 문제 수정 * feat: pick 휴지통, 복원, 삭제 api 연결완료 * fix: 이름 길어지면 ...으로 표시 * fix: 가상 미분류 폴더 렌더링 방지 처리 * fix: 버그유발 기능 비활성화 * fix: fix * fix: fix2 --- frontend/techpick/package.json | 2 + frontend/techpick/src/app/page.tsx | 20 ++- .../src/entities/pick/ui/PickCard.tsx | 12 +- .../api/pick/pickQueryFunctions.ts | 48 ++++++ .../nodeManagement/api/pick/useDeletePick.ts | 8 + .../api/pick/useGetPicksByParentId.ts | 11 ++ .../nodeManagement/api/pick/useMovePick.ts | 8 + .../nodeManagement/hooks/useDragHook.ts | 5 +- .../nodeManagement/hooks/useRestoreNode.ts | 24 ++- .../nodeManagement/hooks/useTreeHandlers.ts | 72 ++++++--- .../src/features/nodeManagement/ui/Folder.tsx | 47 +++--- .../utils/convertPickDataToNodeData.ts | 26 +++ .../utils/getCurrentTreeTypeByNode.ts | 4 +- .../utils/getNewIdFromStructure.ts | 9 +- .../techpick/src/shared/stores/treeStore.ts | 19 ++- .../techpick/src/shared/types/ApiTypes.ts | 3 +- .../techpick/src/shared/types/NodeData.ts | 1 + .../widgets/ContextMenu/EditorContextMenu.tsx | 16 +- .../widgets/ContextMenu/TreeContextMenu.tsx | 79 ++++----- .../DirectoryNode/DirectoryNode.css.ts | 6 + .../widgets/DirectoryNode/DirectoryNode.tsx | 90 ++++++----- .../DirectoryTreeSection.tsx | 152 +++++++++++++----- .../widgets/LinkEditorSection/LinkEditor.tsx | 38 ++--- frontend/yarn.lock | 4 +- 24 files changed, 475 insertions(+), 229 deletions(-) create mode 100644 frontend/techpick/src/features/nodeManagement/api/pick/useDeletePick.ts create mode 100644 frontend/techpick/src/features/nodeManagement/api/pick/useGetPicksByParentId.ts create mode 100644 frontend/techpick/src/features/nodeManagement/api/pick/useMovePick.ts create mode 100644 frontend/techpick/src/features/nodeManagement/utils/convertPickDataToNodeData.ts diff --git a/frontend/techpick/package.json b/frontend/techpick/package.json index 81bdfb75..9857bb8b 100644 --- a/frontend/techpick/package.json +++ b/frontend/techpick/package.json @@ -29,6 +29,7 @@ "dompurify": "^3.1.7", "immer": "^10.1.1", "ky": "^1.7.2", + "lodash": "^4.17.21", "lucide-react": "^0.447.0", "next": "14.2.9", "randomcolor": "^0.6.2", @@ -60,6 +61,7 @@ "@testing-library/jest-dom": "^6.5.0", "@testing-library/react": "^16.0.1", "@types/dompurify": "^3", + "@types/lodash": "^4", "@types/node": "^22.5.4", "@types/randomcolor": "^0.5.9", "@types/react": "^18", diff --git a/frontend/techpick/src/app/page.tsx b/frontend/techpick/src/app/page.tsx index 205f8ae5..bd80f118 100644 --- a/frontend/techpick/src/app/page.tsx +++ b/frontend/techpick/src/app/page.tsx @@ -12,11 +12,14 @@ import { NodeApi } from 'react-arborist'; import { useTreeStore } from '@/shared/stores/treeStore'; import { useRouter } from 'next/navigation'; import { getClientCookie } from '@/features/userManagement/utils/getClientCookie'; +import { useGetDefaultFolderData } from '@/features/nodeManagement/api/folder/useGetDefaultFolderData'; export default function MainPage() { const router = useRouter(); const { focusedNode, setFocusedFolderNodeList, setFocusedLinkNodeList } = useTreeStore(); + const { data: defaultFolderData, isLoading } = useGetDefaultFolderData(); + const [tempFocusedFolderList, tempFocusedPickList] = useMemo(() => { if (!focusedNode || !focusedNode.children?.length) { return [[], []]; @@ -60,11 +63,18 @@ export default function MainPage() { return (
-
- - - -
+ {!isLoading && ( +
{ + event.preventDefault(); + }} + > + + + +
+ )}
); diff --git a/frontend/techpick/src/entities/pick/ui/PickCard.tsx b/frontend/techpick/src/entities/pick/ui/PickCard.tsx index 2169839b..f9e4752e 100644 --- a/frontend/techpick/src/entities/pick/ui/PickCard.tsx +++ b/frontend/techpick/src/entities/pick/ui/PickCard.tsx @@ -14,12 +14,15 @@ import { skeleton, linkStyle, } from './pickCard.css'; +import { useDragHook } from '@/features/nodeManagement/hooks/useDragHook'; +import { NodeApi } from 'react-arborist'; export function PickCard({ children, - pickId, + node, }: PropsWithChildren) { - const { data: pickData, isLoading, isError } = useGetPickQuery(pickId); + const { data: pickData, isLoading, isError } = useGetPickQuery(node.data.pickId); + const ref = useDragHook(node); if (isLoading) { return ( @@ -40,7 +43,9 @@ export function PickCard({ return ( -
+
} + >
{imageUrl ? ( => { + return await apiClient + .put(`structures/picks/${pickId}`, { + json: structure, + }) + .json(); +}; + +export const getPicksByParentId = async ( + parentId: string +): Promise => { + try { + return await apiClient.get(`picks?parentId=${parentId}`).json(); + } catch (error) { + console.error('Error fetching picks by parent ID:', error); + throw error; + } +}; + +export const deletePick = async ({ + pickId, + structure, +}: { + pickId: string; + structure: object; +}): Promise => { + return await apiClient + .delete(`structures/picks/${pickId}`, { + json: structure, + }) + .json(); +}; + +export const getPickDetail = async (pickId: string): Promise => { + try { + return await apiClient.get(`/api/picks/${pickId}`).json(); + } catch (error) { + console.error('Failed to fetch pick details:', error); + throw error; + } +}; diff --git a/frontend/techpick/src/features/nodeManagement/api/pick/useDeletePick.ts b/frontend/techpick/src/features/nodeManagement/api/pick/useDeletePick.ts new file mode 100644 index 00000000..b8dfa3f1 --- /dev/null +++ b/frontend/techpick/src/features/nodeManagement/api/pick/useDeletePick.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { deletePick } from '@/features/nodeManagement/api/pick/pickQueryFunctions'; + +export const useDeletePick = () => { + return useMutation({ + mutationFn: deletePick, + }); +}; diff --git a/frontend/techpick/src/features/nodeManagement/api/pick/useGetPicksByParentId.ts b/frontend/techpick/src/features/nodeManagement/api/pick/useGetPicksByParentId.ts new file mode 100644 index 00000000..5cb9891b --- /dev/null +++ b/frontend/techpick/src/features/nodeManagement/api/pick/useGetPicksByParentId.ts @@ -0,0 +1,11 @@ +import { getPicksByParentId } from '@/features/nodeManagement/api/pick/pickQueryFunctions'; +import { useQuery } from '@tanstack/react-query'; +import { ApiPickData } from '@/shared/types/ApiTypes'; + +export const useGetPicksByParentId = (parentId: string) => { + return useQuery({ + queryKey: ['picksByParentId', parentId], + queryFn: async () => await getPicksByParentId(parentId), + enabled: !!parentId, + }); +}; diff --git a/frontend/techpick/src/features/nodeManagement/api/pick/useMovePick.ts b/frontend/techpick/src/features/nodeManagement/api/pick/useMovePick.ts new file mode 100644 index 00000000..5dc0a285 --- /dev/null +++ b/frontend/techpick/src/features/nodeManagement/api/pick/useMovePick.ts @@ -0,0 +1,8 @@ +import { useMutation } from '@tanstack/react-query'; +import { putPickMove } from '@/features/nodeManagement/api/pick/pickQueryFunctions'; + +export const useMovePick = () => { + return useMutation({ + mutationFn: putPickMove, + }); +}; diff --git a/frontend/techpick/src/features/nodeManagement/hooks/useDragHook.ts b/frontend/techpick/src/features/nodeManagement/hooks/useDragHook.ts index 5b24c6b9..84cc0ca2 100644 --- a/frontend/techpick/src/features/nodeManagement/hooks/useDragHook.ts +++ b/frontend/techpick/src/features/nodeManagement/hooks/useDragHook.ts @@ -7,9 +7,12 @@ import { safeRun } from 'react-arborist/dist/main/utils'; import { ROOT_ID } from 'react-arborist/dist/main/data/create-root'; import { useEffect } from 'react'; import { getEmptyImage } from 'react-dnd-html5-backend'; +import { useTreeStore } from '@/shared/stores/treeStore'; export function useDragHook(node: NodeApi): ConnectDragSource { - const tree = node.tree; + const { treeRef } = useTreeStore(); + const tree = treeRef.rootRef.current!; + const [_, ref, preview] = useDrag( () => ({ canDrag: () => node.isDraggable, diff --git a/frontend/techpick/src/features/nodeManagement/hooks/useRestoreNode.ts b/frontend/techpick/src/features/nodeManagement/hooks/useRestoreNode.ts index 60c78f15..3d22d73b 100644 --- a/frontend/techpick/src/features/nodeManagement/hooks/useRestoreNode.ts +++ b/frontend/techpick/src/features/nodeManagement/hooks/useRestoreNode.ts @@ -4,9 +4,12 @@ import { useMoveFolder } from '@/features/nodeManagement/api/folder/useMoveFolde import { useQueryClient } from '@tanstack/react-query'; import { ApiStructureData } from '@/shared/types/ApiTypes'; import { useGetDefaultFolderData } from '@/features/nodeManagement/api/folder/useGetDefaultFolderData'; +import { useMovePick } from '@/features/nodeManagement/api/pick/useMovePick'; export const useRestoreNode = () => { const { mutateAsync: moveFolder } = useMoveFolder(); + const { mutateAsync: movePick } = useMovePick(); + const { data: defaultFolderIdData } = useGetDefaultFolderData(); const queryClient = useQueryClient(); const structureData: ApiStructureData | undefined = queryClient.getQueryData([ @@ -20,10 +23,8 @@ export const useRestoreNode = () => { ids: string[]; nodes: NodeApi[]; }) => { - const realNodeId = - nodes[0].data.type === 'folder' - ? nodes[0].data.folderId - : nodes[0].data.pickId; + const isFolder = nodes[0].data.type === 'folder'; + const realNodeId = isFolder ? nodes[0].data.folderId : nodes[0].data.pickId; const updatedRoot = structuredClone(structureData!.root); updatedRoot.splice(0, 0, nodes[0].data); @@ -41,10 +42,17 @@ export const useRestoreNode = () => { }, }; - await moveFolder({ - folderId: realNodeId.toString(), - structure: serverData, - }); + if (isFolder) { + await moveFolder({ + folderId: realNodeId.toString(), + structure: serverData, + }); + } else { + await movePick({ + pickId: realNodeId.toString(), + structure: serverData, + }); + } await queryClient.invalidateQueries({ queryKey: ['rootAndRecycleBinData'], diff --git a/frontend/techpick/src/features/nodeManagement/hooks/useTreeHandlers.ts b/frontend/techpick/src/features/nodeManagement/hooks/useTreeHandlers.ts index a0902b32..f196990f 100644 --- a/frontend/techpick/src/features/nodeManagement/hooks/useTreeHandlers.ts +++ b/frontend/techpick/src/features/nodeManagement/hooks/useTreeHandlers.ts @@ -14,6 +14,8 @@ import { ApiStructureData } from '@/shared/types/ApiTypes'; import toast from 'react-hot-toast'; import { getCurrentTreeTypeByNode } from '@/features/nodeManagement/utils/getCurrentTreeTypeByNode'; import { deleteFolder } from '@/features/nodeManagement/api/folder/folderQueryFunctions'; +import { useMovePick } from '@/features/nodeManagement/api/pick/useMovePick'; +import { deletePick } from '@/features/nodeManagement/api/pick/pickQueryFunctions'; export const useTreeHandlers = () => { const { data: structureData, refetch: refetchStructure } = @@ -30,6 +32,8 @@ export const useTreeHandlers = () => { const { data: defaultFolderIdData } = useGetDefaultFolderData(); const { mutateAsync: moveFolder } = useMoveFolder(); const { mutateAsync: renameFolder } = useRenameFolder(); + + const { mutateAsync: movePick } = useMovePick(); const queryClient = useQueryClient(); const recycleBinId = defaultFolderIdData?.RECYCLE_BIN; @@ -72,12 +76,8 @@ export const useTreeHandlers = () => { parentNode, index, }) => { - console.log('dragIds', dragIds); - console.log('dragNodes', dragNodes); - console.log('parentId', parentId); - console.log('parentNode', parentNode); - console.log('index', index); const isRoot = getCurrentTreeTypeByNode(dragNodes[0], treeRef) === 'root'; + const isPick = dragNodes[0].data.type === 'pick'; const currentStructureData = isRoot ? structuredClone(structureData!.root) : structuredClone(structureData!.recycleBin); @@ -98,6 +98,7 @@ export const useTreeHandlers = () => { parentNode, index ); + // 서버에 업데이트된 트리 전송 const serverData = { parentFolderId: parentNode ? parentNode.data.folderId : currentRootId, @@ -107,10 +108,17 @@ export const useTreeHandlers = () => { }, }; - await moveFolder({ - folderId: dragNodes[0].data.folderId!.toString(), - structure: serverData, - }); + if (isPick) { + await movePick({ + pickId: dragNodes[0].data.pickId!.toString(), + structure: serverData, + }); + } else { + await moveFolder({ + folderId: dragNodes[0].data.folderId!.toString(), + structure: serverData, + }); + } await refetchStructure(); }; @@ -129,7 +137,7 @@ export const useTreeHandlers = () => { await refetchStructure(); } catch (error) { console.error('Folder rename failed:', error); - toast.error('동일한 이름을 가진 폴더가 존재합니다.'); + toast.error('이름이 중복됩니다.\n 다른 이름을 입력해주세요.'); treeRef.rootRef.current?.root?.reset(); await refetchStructure(); } @@ -142,10 +150,8 @@ export const useTreeHandlers = () => { ids: string[]; nodes: NodeApi[]; }) => { - const realNodeId = - nodes[0].data.type === 'folder' - ? nodes[0].data.folderId - : nodes[0].data.pickId; + const isFolder = nodes[0].data.type === 'folder'; + const realNodeId = isFolder ? nodes[0].data.folderId : nodes[0].data.pickId; const updatedRecycleBin = structuredClone(structureData!.recycleBin); updatedRecycleBin.splice(0, 0, nodes[0].data); @@ -163,10 +169,18 @@ export const useTreeHandlers = () => { recycleBin: updatedRecycleBin, }, }; - await moveFolder({ - folderId: realNodeId.toString(), - structure: serverData, - }); + if (isFolder) { + await moveFolder({ + folderId: realNodeId.toString(), + structure: serverData, + }); + } else { + await movePick({ + pickId: realNodeId.toString(), + structure: serverData, + }); + } + await refetchStructure(); setFocusedNode(null); }; @@ -178,10 +192,8 @@ export const useTreeHandlers = () => { ids: string[]; nodes: NodeApi[]; }) => { - const realNodeId = - nodes[0].data.type === 'folder' - ? nodes[0].data.folderId - : nodes[0].data.pickId; + const isFolder = nodes[0].data.type === 'folder'; + const realNodeId = isFolder ? nodes[0].data.folderId : nodes[0].data.pickId; const updatedRecycleBin = removeByIdFromStructure( structuredClone(structureData!.recycleBin), @@ -195,10 +207,18 @@ export const useTreeHandlers = () => { recycleBin: updatedRecycleBin, }, }; - await deleteFolder({ - folderId: realNodeId.toString(), - structure: serverData, - }); + if (isFolder) { + await deleteFolder({ + folderId: realNodeId.toString(), + structure: serverData, + }); + } else { + await deletePick({ + pickId: realNodeId.toString(), + structure: serverData, + }); + } + await refetchStructure(); setFocusedNode(null); diff --git a/frontend/techpick/src/features/nodeManagement/ui/Folder.tsx b/frontend/techpick/src/features/nodeManagement/ui/Folder.tsx index 486787a0..90966fe6 100644 --- a/frontend/techpick/src/features/nodeManagement/ui/Folder.tsx +++ b/frontend/techpick/src/features/nodeManagement/ui/Folder.tsx @@ -7,7 +7,6 @@ import { import { NodeApi } from 'react-arborist'; import { useDragHook } from '@/features/nodeManagement/hooks/useDragHook'; import { useTreeStore } from '@/shared/stores/treeStore'; -import { EditorContextMenu } from '@/widgets/ContextMenu/EditorContextMenu'; export function Folder({ node }: { node: NodeApi }) { const ref = useDragHook(node); @@ -19,28 +18,28 @@ export function Folder({ node }: { node: NodeApi }) { const isFocused = focusedNodeInEditorSection?.id === node.id; return ( - -
} - className={isFocused ? folderWrapperFocused : folderWrapper} - onClick={() => { - setFocusedNodeInEditorSection(node); - }} - onDoubleClick={() => { - setFocusedNode(node); - }} - onContextMenu={() => { - setFocusedNodeInEditorSection(node); - }} - > - {`${node.data.name}'s - {node.data.name} -
-
+ // +
} + className={isFocused ? folderWrapperFocused : folderWrapper} + onClick={() => { + setFocusedNodeInEditorSection(node); + }} + onDoubleClick={() => { + setFocusedNode(node); + }} + onContextMenu={() => { + setFocusedNodeInEditorSection(node); + }} + > + {`${node.data.name}'s + {node.data.name} +
+ //
); } diff --git a/frontend/techpick/src/features/nodeManagement/utils/convertPickDataToNodeData.ts b/frontend/techpick/src/features/nodeManagement/utils/convertPickDataToNodeData.ts new file mode 100644 index 00000000..cdab6a3c --- /dev/null +++ b/frontend/techpick/src/features/nodeManagement/utils/convertPickDataToNodeData.ts @@ -0,0 +1,26 @@ +import { ApiPickData, ApiStructureData } from '@/shared/types/ApiTypes'; +import { getNewIdFromStructure } from '@/features/nodeManagement/utils/getNewIdFromStructure'; +import { NodeData } from '@/shared/types'; + +export function convertPickDataToNodeData( + unClassifiedPickDataList: ApiPickData[], + structure: ApiStructureData +): NodeData { + let newId = Number(getNewIdFromStructure(structure)); + const children: NodeData[] = unClassifiedPickDataList.map((pickData) => { + return { + id: String(newId++), + name: pickData.title, + type: 'pick', + pickId: pickData.id, + url: pickData.linkUrlResponse.url, + }; + }); + return { + id: '-2', + name: '미분류', + type: 'folder', + folderId: -2, + children, + }; +} diff --git a/frontend/techpick/src/features/nodeManagement/utils/getCurrentTreeTypeByNode.ts b/frontend/techpick/src/features/nodeManagement/utils/getCurrentTreeTypeByNode.ts index 897f502c..dbdba200 100644 --- a/frontend/techpick/src/features/nodeManagement/utils/getCurrentTreeTypeByNode.ts +++ b/frontend/techpick/src/features/nodeManagement/utils/getCurrentTreeTypeByNode.ts @@ -8,5 +8,7 @@ export const getCurrentTreeTypeByNode = ( recycleBinRef: React.RefObject | undefined>; } ) => { - return treeRef.rootRef.current?.get(currentNode.id) ? 'root' : 'recycleBin'; + return treeRef.recycleBinRef.current?.get(currentNode.id) + ? 'recycleBin' + : 'root'; }; diff --git a/frontend/techpick/src/features/nodeManagement/utils/getNewIdFromStructure.ts b/frontend/techpick/src/features/nodeManagement/utils/getNewIdFromStructure.ts index 344f54bd..a81e8741 100644 --- a/frontend/techpick/src/features/nodeManagement/utils/getNewIdFromStructure.ts +++ b/frontend/techpick/src/features/nodeManagement/utils/getNewIdFromStructure.ts @@ -22,7 +22,12 @@ function getMaxIdFromNodes(nodes: NodeData[]): number { export function getNewIdFromStructure(structure: ApiStructureData): string { const maxIdInRoot = getMaxIdFromNodes(structure.root); const maxIdInRecycleBin = getMaxIdFromNodes(structure.recycleBin); - const maxId = Math.max(maxIdInRoot, maxIdInRecycleBin); + if (structure.unclassified) { + const maxIdInUnclassified = getMaxIdFromNodes(structure.unclassified); + return String( + Math.max(maxIdInRoot, maxIdInRecycleBin, maxIdInUnclassified) + 1 + ); + } - return String(maxId + 1); + return String(Math.max(maxIdInRoot, maxIdInRecycleBin) + 1); } diff --git a/frontend/techpick/src/shared/stores/treeStore.ts b/frontend/techpick/src/shared/stores/treeStore.ts index 524dc58b..fec3beb0 100644 --- a/frontend/techpick/src/shared/stores/treeStore.ts +++ b/frontend/techpick/src/shared/stores/treeStore.ts @@ -2,7 +2,7 @@ import { create } from 'zustand'; import { NodeData } from '@/shared/types'; import { NodeApi, TreeApi } from 'react-arborist'; import React, { createRef } from 'react'; -import { ApiPickData } from '@/shared/types/ApiTypes'; +import { ApiDefaultFolderIdData, ApiPickData } from '@/shared/types/ApiTypes'; interface TreeState { treeData: NodeData[]; @@ -14,9 +14,13 @@ interface TreeState { focusedNodeInEditorSection: NodeApi | null; focusedFolderNodeList: NodeApi[]; focusedLinkNodeList: NodeApi[]; - unClassifiedPicks: ApiPickData[]; + unClassifiedPickDataList: ApiPickData[]; + unClassifiedNodeRoot: NodeApi | null; + defaultFolderIdData: ApiDefaultFolderIdData | null; - setUnClassifiedPicks: (data: ApiPickData[]) => void; + setDeFaultFolderIdData: (data: ApiDefaultFolderIdData) => void; + setUnclassifiedNodeRoot: (data: NodeApi | null) => void; + setUnClassifiedPickDataList: (data: ApiPickData[]) => void; setTreeData: (data: NodeData[]) => void; setTreeRef: ( rootRef: React.RefObject | undefined>, @@ -38,9 +42,14 @@ export const useTreeStore = create((set) => ({ focusedNodeInEditorSection: null, focusedFolderNodeList: [], focusedLinkNodeList: [], - unClassifiedPicks: [], + unClassifiedPickDataList: [], + unClassifiedNodeRoot: null, + defaultFolderIdData: null, - setUnClassifiedPicks: (data) => set({ unClassifiedPicks: data }), + setDeFaultFolderIdData: (data) => set({ defaultFolderIdData: data }), + setUnclassifiedNodeRoot: (data) => set({ unClassifiedNodeRoot: data }), + setUnClassifiedPickDataList: (data) => + set({ unClassifiedPickDataList: data }), setTreeData: (data) => set({ treeData: data }), setTreeRef: (rootRef, recycleBinRef) => set({ treeRef: { rootRef, recycleBinRef } }), diff --git a/frontend/techpick/src/shared/types/ApiTypes.ts b/frontend/techpick/src/shared/types/ApiTypes.ts index e2b018bd..b6304d3c 100644 --- a/frontend/techpick/src/shared/types/ApiTypes.ts +++ b/frontend/techpick/src/shared/types/ApiTypes.ts @@ -10,6 +10,7 @@ export interface ApiDefaultFolderIdData { export interface ApiStructureData { root: NodeData[]; recycleBin: NodeData[]; + unclassified?: NodeData[]; } export interface ApiFolderData { @@ -35,7 +36,7 @@ export interface ApiLinkUrlData { export interface ApiPickData { id: number; - name: string; + title: string; memo: string; folderId: number; userId: number; diff --git a/frontend/techpick/src/shared/types/NodeData.ts b/frontend/techpick/src/shared/types/NodeData.ts index 513d4351..abef8593 100644 --- a/frontend/techpick/src/shared/types/NodeData.ts +++ b/frontend/techpick/src/shared/types/NodeData.ts @@ -8,6 +8,7 @@ export interface NodeData { name: string; folderId?: number; // folder에만 적용 pickId?: number; // pick에만 적용 + url?: string; // pick에만 적용 } export interface ArboristCreateProps { diff --git a/frontend/techpick/src/widgets/ContextMenu/EditorContextMenu.tsx b/frontend/techpick/src/widgets/ContextMenu/EditorContextMenu.tsx index ee333529..fd06d109 100644 --- a/frontend/techpick/src/widgets/ContextMenu/EditorContextMenu.tsx +++ b/frontend/techpick/src/widgets/ContextMenu/EditorContextMenu.tsx @@ -63,14 +63,14 @@ export function EditorContextMenu({ children }: ContextMenuWrapperProps) { Folder
- { - treeRef.rootRef.current!.createLeaf(); - }} - > - Pick - + {/* {*/} + {/* treeRef.rootRef.current!.createLeaf();*/} + {/* }}*/} + {/*>*/} + {/* Pick*/} + {/**/} diff --git a/frontend/techpick/src/widgets/ContextMenu/TreeContextMenu.tsx b/frontend/techpick/src/widgets/ContextMenu/TreeContextMenu.tsx index 49419f79..1e9ed4ee 100644 --- a/frontend/techpick/src/widgets/ContextMenu/TreeContextMenu.tsx +++ b/frontend/techpick/src/widgets/ContextMenu/TreeContextMenu.tsx @@ -1,14 +1,11 @@ import React from 'react'; import * as ContextMenu from '@radix-ui/react-context-menu'; -import { ChevronRightIcon } from '@radix-ui/react-icons'; import { ContextMenuTrigger, ContextMenuContent, ContextMenuItem, RightSlot, - ContextMenuSubContent, - ContextMenuSubTrigger, } from './ContextMenu.css'; import { useTreeStore } from '@/shared/stores/treeStore'; import { getCurrentTreeTypeByNode } from '@/features/nodeManagement/utils/getCurrentTreeTypeByNode'; @@ -35,39 +32,47 @@ export function TreeContextMenu({ children }: ContextMenuWrapperProps) { {focusedNode?.data.type === 'folder' && currentTree === 'root' && ( - - - 새로 만들기 -
- -
-
- - - { - treeRef.rootRef.current!.createInternal(); - }} - > - Folder
-
- - { - treeRef.rootRef.current!.createLeaf(); - }} - > - Pick - -
-
-
+ { + treeRef.rootRef.current!.createInternal(); + }} + > + 새 폴더
+
+ // + // + // 새로 만들기 + //
+ // + //
+ //
+ // + // + // { + // treeRef.rootRef.current!.createInternal(); + // }} + // > + // Folder
+ //
+ // + // { + // treeRef.rootRef.current!.createLeaf(); + // }} + // > + // Pick + // + //
+ //
+ //
)} {currentTree === 'root' && ( - 이름 변경
+ 이름 바꾸기
)} {currentTree === 'recycleBin' && ( diff --git a/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.css.ts b/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.css.ts index 45e643c6..afd4d86a 100644 --- a/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.css.ts +++ b/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.css.ts @@ -33,6 +33,12 @@ export const dirIcFolder = style({ marginRight: '8px', }); +export const dirName = style({ + textOverflow: 'ellipsis', + overflow: 'hidden', + whiteSpace: 'nowrap', +}); + export const nodeNameInput = style({ fontSize: '14px', fontWeight: 300, diff --git a/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.tsx b/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.tsx index 501ecfb3..6c849350 100644 --- a/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.tsx +++ b/frontend/techpick/src/widgets/DirectoryNode/DirectoryNode.tsx @@ -1,6 +1,7 @@ import { DirectoryNodeProps } from '@/shared/types/NodeData'; import { dirIcFolder, + dirName, dirNode, dirNodeWrapper, dirNodeWrapperFocused, @@ -56,7 +57,7 @@ export const DirectoryNode = ({ if (event.key === 'Enter') { if (event.currentTarget.value === '') { - toast.error('폴더 이름을 입력해주세요.'); + toast.error('이름을 입력해주세요.'); return; } @@ -99,7 +100,7 @@ export const DirectoryNode = ({ exact: true, }); - toast.error('동일한 이름을 가진 폴더가 존재합니다.'); + toast.error('이름이 중복됩니다.\n 다른 이름을 입력해주세요. '); node.reset(); } } else node.submit(event.currentTarget.value); @@ -115,6 +116,13 @@ export const DirectoryNode = ({ setFocusedNode(node); if (currentTree === 'root') { treeRef.recycleBinRef.current!.deselectAll(); + if (node.isLeaf) { + console.log('node', node); + // const pickData = getPickDetail(node.data.pickId!.toString()); + // pickData.then((data) => { + // alert(data.linkUrlResponse.url); + // }); + } } else { treeRef.rootRef.current!.deselectAll(); } @@ -130,44 +138,46 @@ export const DirectoryNode = ({ } }} > - -
- {node.isOpen ? ( - - ) : node.isLeaf ? ( -
- ) : ( - - )} - {node.data.type === 'folder' ? ( - {`${node.data.name}'s - ) : ( - {`${node.data.name}'s - )} - {node.isEditing ? ( - - ) : ( - node.data.name - )} -
-
+ {node.id !== '-2' && ( + +
+ {node.isOpen ? ( + + ) : node.isLeaf ? ( +
+ ) : ( + + )} + {node.data.type === 'folder' ? ( + {`${node.data.name}'s + ) : ( + {`${node.data.name}'s + )} + {node.isEditing ? ( + + ) : ( +
{node.data.name}
+ )} +
+
+ )}
); }; diff --git a/frontend/techpick/src/widgets/DirectoryTreeSection/DirectoryTreeSection.tsx b/frontend/techpick/src/widgets/DirectoryTreeSection/DirectoryTreeSection.tsx index 041a5c31..3327562e 100644 --- a/frontend/techpick/src/widgets/DirectoryTreeSection/DirectoryTreeSection.tsx +++ b/frontend/techpick/src/widgets/DirectoryTreeSection/DirectoryTreeSection.tsx @@ -6,20 +6,20 @@ import { directoryTreeContainer, directoryTreeSectionFooter, directoryTreeWrapper, - recycleBinTreeWrapperClosed, + directoryTreeWrapperFullSize, leftSidebarSection, logo, + logout, + plusButton, profileContainer, profileSection, + recycleBinContainerClosed, + recycleBinContainerOpen, recycleBinLabelContainer, recycleBinTreeWrapper, - recycleBinContainerOpen, - recycleBinContainerClosed, - logout, - directoryTreeWrapperFullSize, - plusButton, - unClassifiedLabelContainer, + recycleBinTreeWrapperClosed, rightIcon, + unClassifiedLabelContainer, } from './DirectoryTreeSection.css'; import Image from 'next/image'; import { NodeData } from '@/shared/types/NodeData'; @@ -41,14 +41,33 @@ import { DirectoryNode } from '@/widgets/DirectoryNode/DirectoryNode'; import { useLogout } from '@/features/userManagement/api/useLogout'; import { useRouter } from 'next/navigation'; import toast from 'react-hot-toast'; -import { useGetUnclassifiedPicks } from '@/features/nodeManagement/api/pick/useGetUnclassifiedPicks'; +import { convertPickDataToNodeData } from '@/features/nodeManagement/utils/convertPickDataToNodeData'; +import { addNodeToStructure } from '@/features/nodeManagement/utils/addNodeToStructure'; +import { useQueryClient } from '@tanstack/react-query'; +import { + ApiDefaultFolderIdData, + ApiStructureData, +} from '@/shared/types/ApiTypes'; +import { debounce } from 'lodash'; +import { useGetPicksByParentId } from '@/features/nodeManagement/api/pick/useGetPicksByParentId'; -export function DirectoryTreeSection() { +export function DirectoryTreeSection({ + defaultFolderIdData, +}: { + defaultFolderIdData: ApiDefaultFolderIdData; +}) { + const queryClient = useQueryClient(); const { ref, width, height } = useResizeObserver(); const rootTreeRef = useRef | undefined>(undefined); const recycleBinTreeRef = useRef | undefined>(undefined); const dragDropManager = useDragDropManager(); - const { setTreeRef, setFocusedNode, setUnClassifiedPicks } = useTreeStore(); + const { + treeRef, + setTreeRef, + setFocusedNode, + setUnClassifiedPickDataList, + setUnclassifiedNodeRoot, + } = useTreeStore(); const { handleCreate, handleDrag, @@ -96,9 +115,48 @@ export function DirectoryTreeSection() { data: rootAndRecycleBinData, error: structureError, isLoading: isStructureLoading, + refetch: refetchStructure, } = useGetRootAndRecycleBinData(); - const { data: unClassifiedPicks } = useGetUnclassifiedPicks(); + const { + data: unClassifiedPickDataList, + isLoading: isUnClassifiedPickDataLoading, + refetch: refetchUnclassifiedPickDataList, + } = useGetPicksByParentId(defaultFolderIdData.UNCLASSIFIED.toString()); + + function convertUnClassifiedPickDataToNodeApi() { + if (unClassifiedPickDataList && rootAndRecycleBinData) { + // NodeData 타입으로 변환해야함 + const unClassifiedNodeData = convertPickDataToNodeData( + unClassifiedPickDataList, + rootAndRecycleBinData + ); + // Tree에 추가해야 함 + const newRootData = addNodeToStructure( + rootAndRecycleBinData.root, + null, + 0, + unClassifiedNodeData + ); + queryClient.setQueryData( + ['rootAndRecycleBinData'], + (oldData: ApiStructureData) => ({ + root: newRootData, + recycleBin: oldData.recycleBin, + }) + ); + // NodeApi 타입으로 변환된 데이터를 Store 에 저장, focusedNode 설정 + setTimeout(() => { + const unClassifiedNodeRoot = rootTreeRef.current?.get('-2'); + if (unClassifiedNodeRoot) { + setUnclassifiedNodeRoot(unClassifiedNodeRoot); + setFocusedNode(unClassifiedNodeRoot); + } + }, 0); + // Tree에서 삭제 + refetchStructure(); + } + } return (
@@ -117,16 +175,17 @@ export function DirectoryTreeSection() {
{ - if (!unClassifiedPicks) { + onClick={debounce(async () => { + await refetchUnclassifiedPickDataList(); + if (!unClassifiedPickDataList) { return; } - setFocusedNode(null); - console.log('clicked'); - console.log('unClassifiedPicks :', unClassifiedPicks); - setUnClassifiedPicks(unClassifiedPicks); - }} - onDoubleClick={() => {}} + treeRef.rootRef.current?.deselectAll(); + treeRef.recycleBinRef.current?.deselectAll(); + setUnClassifiedPickDataList(unClassifiedPickDataList); + + convertUnClassifiedPickDataToNodeApi(); + }, 300)} >
Unclassified
@@ -159,32 +218,37 @@ export function DirectoryTreeSection() { } ref={ref} > - {isStructureLoading &&
Loading...
} - {structureError &&
Error: {structureError.message}
} - {!isStructureLoading && !structureError && ( - - ref={handleTreeRef('root')} - className={directoryTree} - data={rootAndRecycleBinData?.root} - disableMultiSelection={true} - onFocus={(node: NodeApi) => { - setFocusedNode(node); - }} - onMove={handleDrag} - onCreate={handleCreate} - onRename={handleRename} - onDelete={handleMoveToTrash} - openByDefault={false} - width={width} - height={height} - rowHeight={32} - indent={24} - overscanCount={1} - dndManager={dragDropManager} - > - {DirectoryNode} - + {isStructureLoading && isUnClassifiedPickDataLoading && ( +
Loading...
)} + + {structureError &&
Error: {structureError.message}
} + {!isStructureLoading && + !isUnClassifiedPickDataLoading && + !structureError && ( + + ref={handleTreeRef('root')} + className={directoryTree} + data={rootAndRecycleBinData?.root} + disableMultiSelection={true} + onFocus={(node: NodeApi) => { + setFocusedNode(node); + }} + onMove={handleDrag} + onCreate={handleCreate} + onRename={handleRename} + onDelete={handleMoveToTrash} + openByDefault={false} + width={width} + height={height} + rowHeight={32} + indent={24} + overscanCount={1} + dndManager={dragDropManager} + > + {DirectoryNode} + + )}
{ - const { - treeRef, - focusedNode, - focusedFolderNodeList, - focusedLinkNodeList, - unClassifiedPicks, - } = useTreeStore(); + const { treeRef, focusedNode, focusedFolderNodeList, focusedLinkNodeList } = + useTreeStore(); const el = useRef(null); const dropRef = useDropHook(el, focusedNode || treeRef.rootRef.current!.root); @@ -29,6 +20,7 @@ export const LinkEditor = () => { }, [dropRef] ); + return (
{!!focusedFolderNodeList?.length && ( @@ -38,20 +30,20 @@ export const LinkEditor = () => { ))}
)} + {/*{!!focusedLinkNodeList?.length && (*/} + {/*
*/} + {/* {focusedLinkNodeList?.map((node, index) => (*/} + {/* */} + {/* ))}*/} + {/*
*/} + {/*)}*/} {!!focusedLinkNodeList?.length && ( -
- {/*todo: pick id 넘져줘야 함*/} - {focusedLinkNodeList?.map((node, index) => ( - - ))} -
- )} - {!!unClassifiedPicks?.length && !focusedNode && ( - {unClassifiedPicks.map((node, index) => { + {focusedLinkNodeList.map((node, index) => { + console.log(node.data); return ( - - + + ); })} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 3d523719..cd2e5afd 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -4766,7 +4766,7 @@ __metadata: languageName: node linkType: hard -"@types/lodash@npm:^4.14.167": +"@types/lodash@npm:^4, @types/lodash@npm:^4.14.167": version: 4.17.10 resolution: "@types/lodash@npm:4.17.10" checksum: 10c0/149b2b9fcc277204393423ed14df28894980c2322ec522fc23f2c6f7edef6ee8d876ee09ed4520f45d128adc0a7a6e618bb0017668349716cd99c6ef54a21621 @@ -14535,6 +14535,7 @@ __metadata: "@testing-library/jest-dom": "npm:^6.5.0" "@testing-library/react": "npm:^16.0.1" "@types/dompurify": "npm:^3" + "@types/lodash": "npm:^4" "@types/node": "npm:^22.5.4" "@types/randomcolor": "npm:^0.5.9" "@types/react": "npm:^18" @@ -14557,6 +14558,7 @@ __metadata: jest: "npm:^29.7.0" jest-environment-jsdom: "npm:^29.7.0" ky: "npm:^1.7.2" + lodash: "npm:^4.17.21" lucide-react: "npm:^0.447.0" mini-css-extract-plugin: "npm:^2.9.1" next: "npm:14.2.9"