Skip to content

Commit

Permalink
[FEAT] pick dnd (#272)
Browse files Browse the repository at this point in the history
* feat: nodeApi로 변환 로직 추가

* fix: 임시 디바운싱 적용

* fix: api 교체

* fix: 간헐적으로 unclassified 안보이는 문제 수정

* feat: pick 휴지통, 복원, 삭제 api 연결완료

* fix: 이름 길어지면 ...으로 표시

* fix: 가상 미분류 폴더 렌더링 방지 처리

* fix: 버그유발 기능 비활성화

* fix: fix

* fix: fix2
  • Loading branch information
enigsuss authored Oct 18, 2024
1 parent c9d334b commit 5effe9b
Show file tree
Hide file tree
Showing 24 changed files with 475 additions and 229 deletions.
2 changes: 2 additions & 0 deletions frontend/techpick/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 15 additions & 5 deletions frontend/techpick/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 [[], []];
Expand Down Expand Up @@ -60,11 +63,18 @@ export default function MainPage() {
return (
<div className={rootLayout}>
<div className={viewWrapper}>
<div className={viewContainer}>
<DirectoryTreeSection />
<LinkEditorSection />
<FeaturedSection />
</div>
{!isLoading && (
<div
className={viewContainer}
onContextMenu={(event) => {
event.preventDefault();
}}
>
<DirectoryTreeSection defaultFolderIdData={defaultFolderData!} />
<LinkEditorSection />
<FeaturedSection />
</div>
)}
</div>
</div>
);
Expand Down
12 changes: 9 additions & 3 deletions frontend/techpick/src/entities/pick/ui/PickCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<PickCardProps>) {
const { data: pickData, isLoading, isError } = useGetPickQuery(pickId);
const { data: pickData, isLoading, isError } = useGetPickQuery(node.data.pickId);
const ref = useDragHook(node);

if (isLoading) {
return (
Expand All @@ -40,7 +43,9 @@ export function PickCard({

return (
<Link href={url} target="_blank" className={linkStyle}>
<div className={pickCardLayout}>
<div className={pickCardLayout}
ref={ref as unknown as React.LegacyRef<HTMLDivElement>}
>
<div className={cardImageSectionStyle}>
{imageUrl ? (
<Image
Expand Down Expand Up @@ -68,4 +73,5 @@ export function PickCard({
}
interface PickCardProps {
pickId: number;
node: NodeApi;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,51 @@ export const postPick = async ({
})
.json();
};

export const putPickMove = async ({
pickId,
structure,
}: {
pickId: string;
structure: object;
}): Promise<void> => {
return await apiClient
.put(`structures/picks/${pickId}`, {
json: structure,
})
.json();
};

export const getPicksByParentId = async (
parentId: string
): Promise<ApiPickData[]> => {
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<void> => {
return await apiClient
.delete(`structures/picks/${pickId}`, {
json: structure,
})
.json();
};

export const getPickDetail = async (pickId: string): Promise<ApiPickData> => {
try {
return await apiClient.get(`/api/picks/${pickId}`).json();
} catch (error) {
console.error('Failed to fetch pick details:', error);
throw error;
}
};
Original file line number Diff line number Diff line change
@@ -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,
});
};
Original file line number Diff line number Diff line change
@@ -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<ApiPickData[], Error>({
queryKey: ['picksByParentId', parentId],
queryFn: async () => await getPicksByParentId(parentId),
enabled: !!parentId,
});
};
Original file line number Diff line number Diff line change
@@ -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,
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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<DragItem, DropResult, void>(
() => ({
canDrag: () => node.isDraggable,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand All @@ -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);
Expand All @@ -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'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 } =
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand All @@ -98,6 +98,7 @@ export const useTreeHandlers = () => {
parentNode,
index
);

// 서버에 업데이트된 트리 전송
const serverData = {
parentFolderId: parentNode ? parentNode.data.folderId : currentRootId,
Expand All @@ -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();
};

Expand All @@ -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();
}
Expand All @@ -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);
Expand All @@ -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);
};
Expand All @@ -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),
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 5effe9b

Please sign in to comment.