diff --git a/frontend/schema/schema.d.ts b/frontend/schema/schema.d.ts index c944b76db..eae663f59 100644 --- a/frontend/schema/schema.d.ts +++ b/frontend/schema/schema.d.ts @@ -320,7 +320,10 @@ export interface components { /** Format: int64 */ id?: number; name?: string; - /** @enum {string} */ + /** + * @example GENERAL + * @enum {string} + */ folderType?: "UNCLASSIFIED" | "RECYCLE_BIN" | "ROOT" | "GENERAL"; /** Format: int64 */ parentFolderId?: number; @@ -740,7 +743,7 @@ export interface operations { [name: string]: unknown; }; content: { - "*/*": components["schemas"]["techpick.api.application.folder.dto.FolderApiResponse"][]; + "application/json": unknown; }; }; /** @description 본인 폴더만 조회할 수 있습니다. */ diff --git a/frontend/techpick-shared/themes/commonTheme.css.ts b/frontend/techpick-shared/themes/commonTheme.css.ts index bbe082590..82d98f7fa 100644 --- a/frontend/techpick-shared/themes/commonTheme.css.ts +++ b/frontend/techpick-shared/themes/commonTheme.css.ts @@ -112,6 +112,8 @@ export const [commonThemeClass, commonTheme] = createTheme({ max: 'max-content', /** @type {string} Minimum size, set to `min-content`. */ min: 'min-content', + /** @type {string} fit size, set to `fit-content`. */ + fit: 'fit-content', /** @type {string} Full size, set to `100%`. */ full: '100%', /** @type {string} Size for ultra extra extra extra extra small screens (2rem, 32px). */ diff --git a/frontend/techpick/src/apis/apiConstants.ts b/frontend/techpick/src/apis/apiConstants.ts index a6f876233..445e223e4 100644 --- a/frontend/techpick/src/apis/apiConstants.ts +++ b/frontend/techpick/src/apis/apiConstants.ts @@ -2,6 +2,7 @@ const API_ENDPOINTS = { FOLDERS: 'folders', LOCATION: 'location', BASIC: 'basic', + PICKS: 'picks', }; export const API_URLS = { @@ -11,4 +12,7 @@ export const API_URLS = { UPDATE_FOLDER: API_ENDPOINTS.FOLDERS, MOVE_FOLDER: `${API_ENDPOINTS.FOLDERS}/${API_ENDPOINTS.LOCATION}`, GET_BASIC_FOLDERS: `${API_ENDPOINTS.FOLDERS}/${API_ENDPOINTS.BASIC}`, + GET_PICKS_BY_FOLDER_ID: (folderId: number) => + `${API_ENDPOINTS.PICKS}?folderIdList=${folderId}`, + MOVE_PICKS: `${API_ENDPOINTS.PICKS}/${API_ENDPOINTS.LOCATION}`, }; diff --git a/frontend/techpick/src/apis/pick/getPicksByFolderId.ts b/frontend/techpick/src/apis/pick/getPicksByFolderId.ts new file mode 100644 index 000000000..521581b1a --- /dev/null +++ b/frontend/techpick/src/apis/pick/getPicksByFolderId.ts @@ -0,0 +1,46 @@ +import { HTTPError } from 'ky'; +import { apiClient, returnErrorFromHTTPError } from '@/apis'; +import { API_URLS } from '../apiConstants'; +import type { + GetPicksByFolderIdResponseType, + PickIdOrderedListType, + PickInfoRecordType, + PickListType, +} from '@/types'; + +export const getPicksByFolderId = async (folderId: number) => { + const data = await getPickListByFolderId(folderId); + + const pickRecordData = generatePickRecordData(data[0]['pickList']); + + return pickRecordData; +}; + +const getPickListByFolderId = async (folderId: number) => { + try { + const response = await apiClient.get( + API_URLS.GET_PICKS_BY_FOLDER_ID(folderId) + ); + const data = await response.json(); + + return data; + } catch (httpError) { + if (httpError instanceof HTTPError) { + const error = returnErrorFromHTTPError(httpError); + throw error; + } + throw httpError; + } +}; + +const generatePickRecordData = (pickList: PickListType) => { + const pickIdOrderedList: PickIdOrderedListType = []; + const pickInfoRecord = {} as PickInfoRecordType; + + for (const pickInfo of pickList) { + pickIdOrderedList.push(pickInfo.id); + pickInfoRecord[`${pickInfo.id}`] = pickInfo; + } + + return { pickIdOrderedList, pickInfoRecord }; +}; diff --git a/frontend/techpick/src/apis/pick/index.ts b/frontend/techpick/src/apis/pick/index.ts index a25035844..b0f3faae8 100644 --- a/frontend/techpick/src/apis/pick/index.ts +++ b/frontend/techpick/src/apis/pick/index.ts @@ -1,2 +1,4 @@ export { useGetPickQuery } from './getPick/useGetPickQuery'; export { useUpdatePickMutation } from './updatePick/useUpdatePickMutation'; +export { getPicksByFolderId } from './getPicksByFolderId'; +export { movePicks } from './movePicks'; diff --git a/frontend/techpick/src/apis/pick/movePicks.ts b/frontend/techpick/src/apis/pick/movePicks.ts new file mode 100644 index 000000000..dae6b4e7f --- /dev/null +++ b/frontend/techpick/src/apis/pick/movePicks.ts @@ -0,0 +1,16 @@ +import { HTTPError } from 'ky'; +import { apiClient, returnErrorFromHTTPError } from '@/apis'; +import { API_URLS } from '../apiConstants'; +import type { MovePicksRequestType } from '@/types'; + +export const movePicks = async (movePicksInfo: MovePicksRequestType) => { + try { + await apiClient.patch(API_URLS.MOVE_PICKS, { json: movePicksInfo }); + } catch (httpError) { + if (httpError instanceof HTTPError) { + const error = returnErrorFromHTTPError(httpError); + throw error; + } + throw httpError; + } +}; diff --git a/frontend/techpick/src/apis/pick/pickApi.type.ts b/frontend/techpick/src/apis/pick/pickApi.type.ts index 77902c4e5..d6187bc72 100644 --- a/frontend/techpick/src/apis/pick/pickApi.type.ts +++ b/frontend/techpick/src/apis/pick/pickApi.type.ts @@ -1,4 +1,4 @@ -import type { Concrete } from '@/types/uitl.type'; +import type { Concrete } from '@/types/util.type'; import type { components } from '@/schema'; export type GetPickResponseType = Concrete< diff --git a/frontend/techpick/src/app/(signed)/folders/layout.css.ts b/frontend/techpick/src/app/(signed)/folders/layout.css.ts index dd817df44..8717f0bfc 100644 --- a/frontend/techpick/src/app/(signed)/folders/layout.css.ts +++ b/frontend/techpick/src/app/(signed)/folders/layout.css.ts @@ -3,4 +3,5 @@ import { style } from '@vanilla-extract/css'; export const pageContainerLayout = style({ display: 'flex', flexDirection: 'row', + height: '100vh', }); diff --git a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx index cd1719928..af4420b68 100644 --- a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx +++ b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx @@ -1,10 +1,13 @@ 'use client'; import { useEffect } from 'react'; -import { PickListViewerPanel } from '@/components/PickListViewerPanel/PickListViewerPanel'; +import { DraggablePickListViewer } from '@/components'; import { useTreeStore } from '@/stores/dndTreeStore/dndTreeStore'; +import { usePickStore } from '@/stores/pickStore/pickStore'; export default function UnclassifiedFolderPage() { + const { fetchPickDataByFolderId, getOrderedPickListByFolderId } = + usePickStore(); const selectSingleFolder = useTreeStore((state) => state.selectSingleFolder); const basicFolderMap = useTreeStore((state) => state.basicFolderMap); @@ -19,5 +22,25 @@ export default function UnclassifiedFolderPage() { [basicFolderMap, selectSingleFolder] ); - return ; + useEffect( + function loadPickDataFromRemote() { + if (!basicFolderMap) { + return; + } + + fetchPickDataByFolderId(basicFolderMap['UNCLASSIFIED'].id); + }, + [basicFolderMap, fetchPickDataByFolderId] + ); + + if (!basicFolderMap) { + return
loading...
; + } + + return ( + + ); } diff --git a/frontend/techpick/src/components/FolderTree/FolderDropZone.tsx b/frontend/techpick/src/components/FolderTree/FolderDropZone.tsx index 8a2b2dae6..dacbac6f4 100644 --- a/frontend/techpick/src/components/FolderTree/FolderDropZone.tsx +++ b/frontend/techpick/src/components/FolderTree/FolderDropZone.tsx @@ -8,7 +8,7 @@ import { useSensors, } from '@dnd-kit/core'; import { useTreeStore } from '@/stores/dndTreeStore/dndTreeStore'; -import { isDnDCurrentData } from '@/stores/dndTreeStore/utils/isDnDCurrentData'; +import { isDnDCurrentData } from '@/utils'; import type { DragEndEvent, DragOverEvent, diff --git a/frontend/techpick/src/components/FolderTree/FolderListItem.tsx b/frontend/techpick/src/components/FolderTree/FolderListItem.tsx index df33bd343..60a37b5c2 100644 --- a/frontend/techpick/src/components/FolderTree/FolderListItem.tsx +++ b/frontend/techpick/src/components/FolderTree/FolderListItem.tsx @@ -2,13 +2,13 @@ import { useState } from 'react'; import type { MouseEvent } from 'react'; import { ROUTES } from '@/constants'; import { useTreeStore } from '@/stores/dndTreeStore/dndTreeStore'; +import { isSelectionActive } from '@/utils'; import { FolderContextMenu } from './FolderContextMenu'; import { FolderInput } from './FolderInput'; import { FolderLinkItem } from './FolderLinkItem'; import { getSelectedFolderRange, isSameParentFolder, - isSelectionActive, } from './folderListItem.util'; import type { FolderMapType } from '@/types'; diff --git a/frontend/techpick/src/components/FolderTree/folderListItem.util.ts b/frontend/techpick/src/components/FolderTree/folderListItem.util.ts index 8c134903f..78451b179 100644 --- a/frontend/techpick/src/components/FolderTree/folderListItem.util.ts +++ b/frontend/techpick/src/components/FolderTree/folderListItem.util.ts @@ -1,8 +1,6 @@ import { hasIndex } from '@/utils'; import type { FolderMapType } from '@/types'; -export const isSelectionActive = (length: number) => 0 < length; - export const isSameParentFolder = ( id: number, selectedId: number, diff --git a/frontend/techpick/src/components/FolderTree/index.tsx b/frontend/techpick/src/components/FolderTree/index.tsx index d173ebff0..4e4c1b46a 100644 --- a/frontend/techpick/src/components/FolderTree/index.tsx +++ b/frontend/techpick/src/components/FolderTree/index.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useEffect } from 'react'; import { DragOverlay } from '@dnd-kit/core'; import { useCreateFolderInputStore } from '@/stores/createFolderInputStore'; import { useTreeStore } from '@/stores/dndTreeStore/dndTreeStore'; @@ -12,12 +13,14 @@ import { TreeNode } from './TreeNode'; export function FolderTree() { const { newFolderParentId } = useCreateFolderInputStore(); - const { getFolders, getBasicFolders } = useTreeStore.getState(); + const { getFolders, getBasicFolders } = useTreeStore(); const rootFolderId = useTreeStore((state) => state.rootFolderId); const isShow = newFolderParentId !== rootFolderId; - getFolders(); - getBasicFolders(); + useEffect(() => { + getFolders(); + getBasicFolders(); + }, [getBasicFolders, getFolders]); return ( diff --git a/frontend/techpick/src/components/PickCard/PickCard.tsx b/frontend/techpick/src/components/PickCard/PickCard.tsx deleted file mode 100644 index fb64941b9..000000000 --- a/frontend/techpick/src/components/PickCard/PickCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { PropsWithChildren } from 'react'; - -export function PickCard({ children }: PropsWithChildren) { - return { children }; - - // 나중에 픽 리스트를 조회할 때 다시 사용할 예정입니다. - - // const { - // data: pickData, - // isLoading, - // isError, - // } = useGetPickQuery(node.data.pickId); - // const ref = useDragHook(node); - - // if (isLoading) { - // return ( - //
- //
- //
- //
- //
- // ); - // } - - // if (isError || !pickData) { - // return

oops! something is wrong

; - // } - - // const { memo, title, linkInfo } = pickData; - // const { imageUrl, url } = linkInfo; - - // return ( - // - //
} - // > - //
- // {imageUrl ? ( - // - // ) : ( - //
- // )} - //
- - //
- //

{title}

- //
- //
- //

{memo}

- //
- //
{children}
- //
- // - // ); -} -interface PickCardProps { - pickId: number; -} diff --git a/frontend/techpick/src/components/PickCardGridLayout/PickCardGridLayout.tsx b/frontend/techpick/src/components/PickCardGridLayout/PickCardGridLayout.tsx deleted file mode 100644 index 4d77d2650..000000000 --- a/frontend/techpick/src/components/PickCardGridLayout/PickCardGridLayout.tsx +++ /dev/null @@ -1,8 +0,0 @@ -'use client'; - -import { PropsWithChildren } from 'react'; -import { pickCardGridLayoutStyle } from './pickCardGridLayout.css'; - -export function PickCardGridLayout({ children }: PropsWithChildren) { - return
{children}
; -} diff --git a/frontend/techpick/src/components/PickCardGridLayout/pickCardGridLayout.css.ts b/frontend/techpick/src/components/PickCardGridLayout/pickCardGridLayout.css.ts deleted file mode 100644 index 864cfbfe9..000000000 --- a/frontend/techpick/src/components/PickCardGridLayout/pickCardGridLayout.css.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { style } from '@vanilla-extract/css'; -import { space } from 'techpick-shared'; - -export const pickCardGridLayoutStyle = style({ - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', // 280px는 PickCard의 width - padding: space[16], - rowGap: space[16], - columnGap: space[16], -}); diff --git a/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx new file mode 100644 index 000000000..e850c965e --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx @@ -0,0 +1,58 @@ +import type { ReactNode } from 'react'; +import { PickDnDCard } from './PickDnDCard'; +import { PickDnDCardListLayout } from './PickDnDCardListLayout'; +import type { + PickViewItemComponentProps, + PickViewItemListLayoutComponentProps, +} from './PickListViewer'; +import type { PickInfoType } from '@/types'; + +export function DraggablePickListViewer({ + pickList, + viewType = 'card', + folderId, +}: PickListViewerProps) { + const { PickViewItemComponent, PickViewItemListLayoutComponent } = + DND_PICK_LIST_VIEW_TEMPLATES[viewType]; + + return ( + + {pickList.map((pickInfo) => ( + + ))} + + ); +} + +interface PickListViewerProps { + pickList: PickInfoType[]; + folderId: number; + viewType?: DnDViewTemplateType; +} + +const DND_PICK_LIST_VIEW_TEMPLATES: Record< + DnDViewTemplateType, + DnDViewTemplateValueType +> = { + card: { + PickViewItemComponent: PickDnDCard, + PickViewItemListLayoutComponent: PickDnDCardListLayout, + }, +}; + +/** + * @description DnDViewTemplateType은 Drag&Drop이 가능한 UI 중 무엇을 보여줄지 나타냅니다. ex) card, list + */ +type DnDViewTemplateType = 'card'; + +interface DnDViewTemplateValueType { + PickViewItemListLayoutComponent: ( + props: PickViewDnDItemListLayoutComponentProps + ) => ReactNode; + PickViewItemComponent: (props: PickViewDnDItemComponentProps) => ReactNode; +} + +export type PickViewDnDItemListLayoutComponentProps = + PickViewItemListLayoutComponentProps<{ folderId: number }>; + +export type PickViewDnDItemComponentProps = PickViewItemComponentProps; diff --git a/frontend/techpick/src/components/PickListViewer/PickCard.tsx b/frontend/techpick/src/components/PickListViewer/PickCard.tsx new file mode 100644 index 000000000..cc1ea0761 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickCard.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useCallback } from 'react'; +import Image from 'next/image'; +import { + cardDescriptionSectionStyle, + cardImageSectionStyle, + cardImageStyle, + cardTitleSectionStyle, + defaultCardImageSectionStyle, + pickCardLayout, +} from './pickCard.css'; +import { PickViewItemComponentProps } from './PickListViewer'; + +export function PickCard({ pickInfo }: PickViewItemComponentProps) { + const { memo, title, linkInfo } = pickInfo; + const { imageUrl, url } = linkInfo; + + const openUrl = useCallback(() => { + window.open(url, '_blank'); + }, [url]); + + return ( +
+
+ {imageUrl ? ( + + ) : ( +
+ )} +
+ +
+

{title}

+
+
+

{memo}

+
+
+ ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickCardDropZone.tsx b/frontend/techpick/src/components/PickListViewer/PickCardDropZone.tsx new file mode 100644 index 000000000..8a793cc9c --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickCardDropZone.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { + closestCenter, + DndContext, + DragOverlay, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from '@dnd-kit/core'; +import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'; +import { usePickStore } from '@/stores/pickStore/pickStore'; +import { PickViewDnDItemListLayoutComponentProps } from './DraggablePickListViewer'; +import { PickCard } from './PickCard'; +import type { DragEndEvent, DragStartEvent } from '@dnd-kit/core'; + +export function PickCardDropZone({ + folderId, + children, +}: PickViewDnDItemListLayoutComponentProps) { + const { + getOrderedPickIdListByFolderId, + getPickInfoByFolderIdAndPickId, + movePicks, + selectedPickIdList, + selectSinglePick, + setIsDragging, + setFocusedPickId, + isDragging, + focusPickId, + } = usePickStore(); + const orderedPickIdList = getOrderedPickIdListByFolderId(folderId); + const orderedPickIdListWithoutSelectedIdList = isDragging + ? orderedPickIdList.filter( + (orderedPickId) => + !selectedPickIdList.includes(orderedPickId) || + orderedPickId === focusPickId + ) + : orderedPickIdList; + const draggingPickInfo = focusPickId + ? getPickInfoByFolderIdAndPickId(folderId, focusPickId) + : null; + + const mouseSensor = useSensor(MouseSensor, { + activationConstraint: { + distance: 10, // MouseSensor: 10px 이동해야 드래그 시작 + }, + }); + const touchSensor = useSensor(TouchSensor, { + activationConstraint: { + delay: 250, + tolerance: 5, + }, + }); + const sensors = useSensors(mouseSensor, touchSensor); + + const onDragStart = (event: DragStartEvent) => { + setIsDragging(true); + const { active } = event; + const pickId = Number(active.id); + + if (!selectedPickIdList.includes(pickId)) { + selectSinglePick(pickId); + return; + } + + setFocusedPickId(pickId); + }; + + const onDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + + if (!over) return; // 드래그 중 놓은 위치가 없을 때 종료 + + movePicks({ folderId, from: active, to: over }); + setIsDragging(false); + }; + + return ( + + + {children} + + {isDragging && draggingPickInfo && ( + + + + )} + + ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickCardListLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickCardListLayout.tsx new file mode 100644 index 000000000..627249e22 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickCardListLayout.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { pickCardGridLayoutStyle } from './pickCardGridLayout.css'; +import { PickViewItemListLayoutComponentProps } from './PickListViewer'; + +export function PickCardListLayout({ + children, +}: PickViewItemListLayoutComponentProps) { + return
{children}
; +} diff --git a/frontend/techpick/src/components/PickListViewer/PickDnDCard.tsx b/frontend/techpick/src/components/PickListViewer/PickDnDCard.tsx new file mode 100644 index 000000000..f685eeb47 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickDnDCard.tsx @@ -0,0 +1,125 @@ +'use client'; + +import { useCallback } from 'react'; +import type { CSSProperties, MouseEvent } from 'react'; +import Image from 'next/image'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { usePickStore } from '@/stores/pickStore/pickStore'; +import { isSelectionActive } from '@/utils'; +import { + cardDescriptionSectionStyle, + cardImageSectionStyle, + cardImageStyle, + cardTitleSectionStyle, + defaultCardImageSectionStyle, + pickCardLayout, +} from './pickCard.css'; +import { + selectedDragItemStyle, + isActiveDraggingItemStyle, +} from './pickDnDCard.css'; +import { getSelectedPickRange } from './pickDnDCard.util'; +import { PickViewDnDItemComponentProps } from './PickListViewer'; + +export function PickDnDCard({ pickInfo }: PickViewDnDItemComponentProps) { + const { + selectedPickIdList, + selectSinglePick, + getOrderedPickIdListByFolderId, + focusPickId, + setSelectedPickIdList, + isDragging, + } = usePickStore(); + const { memo, title, linkInfo, id: pickId, parentFolderId } = pickInfo; + const { imageUrl, url } = linkInfo; + const isSelected = selectedPickIdList.includes(pickId); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging: isActiveDragging, + } = useSortable({ + id: pickId, + data: { + id: `pick ${pickId}`, + }, + }); + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + opacity: 1, + }; + + const openUrl = useCallback(() => { + window.open(url, '_blank'); + }, [url]); + + const handleShiftSelect = (parentFolderId: number, pickId: number) => { + if (!focusPickId) { + return; + } + + // 새로운 선택된 배열 만들기. + const orderedPickIdList = getOrderedPickIdListByFolderId(parentFolderId); + + const newSelectedPickIdList = getSelectedPickRange({ + orderedPickIdList, + startPickId: focusPickId, + endPickId: pickId, + }); + + setSelectedPickIdList(newSelectedPickIdList); + }; + + const handleClick = ( + pickId: number, + event: MouseEvent + ) => { + if (event.shiftKey && isSelectionActive(selectedPickIdList.length)) { + event.preventDefault(); + handleShiftSelect(parentFolderId, pickId); + return; + } + + selectSinglePick(pickId); + }; + + if (isDragging && isSelected && !isActiveDragging) { + return null; + } + + return ( +
+
handleClick(pickId, event)} + > +
+ {imageUrl ? ( + + ) : ( +
+ )} +
+ +
+

{title}

+
+
+

{memo}

+
+
+
+ ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx new file mode 100644 index 000000000..33dd3895d --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { PickViewDnDItemListLayoutComponentProps } from './DraggablePickListViewer'; +import { PickCardDropZone } from './PickCardDropZone'; +import { pickCardGridLayoutStyle } from './pickCardGridLayout.css'; + +export function PickDnDCardListLayout({ + children, + folderId, +}: PickViewDnDItemListLayoutComponentProps) { + return ( +
+ {children} +
+ ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx b/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx new file mode 100644 index 000000000..ac9e2eb71 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickListViewer.tsx @@ -0,0 +1,56 @@ +import type { PropsWithChildren, ReactNode } from 'react'; +import { PickCard } from './PickCard'; +import { PickCardListLayout } from './PickCardListLayout'; +import type { PickInfoType } from '@/types'; + +export function PickListViewer({ + pickList, + viewType = 'card', +}: PickListViewerProps) { + const { PickViewItemComponent, PickViewItemListLayoutComponent } = + NORMAL_PICK_LIST_VIEW_TEMPLATES[viewType]; + + return ( + + {pickList.map((pickInfo) => ( + + ))} + + ); +} + +interface PickListViewerProps { + pickList: PickInfoType[]; + viewType?: ViewTemplateType; +} + +const NORMAL_PICK_LIST_VIEW_TEMPLATES: Record< + ViewTemplateType, + ViewTemplateValueType +> = { + card: { + PickViewItemListLayoutComponent: PickCardListLayout, + PickViewItemComponent: PickCard, + }, +}; + +/** + * @description ViewTemplateType은 pickInfo를 어떤 UI로 보여줄지 나타냅니다. ex) card, list + */ +type ViewTemplateType = 'card'; + +interface ViewTemplateValueType { + PickViewItemListLayoutComponent: ( + props: PickViewItemListLayoutComponentProps + ) => ReactNode; + PickViewItemComponent: (props: PickViewItemComponentProps) => ReactNode; +} + +export type PickViewItemListLayoutComponentProps = + PropsWithChildren; + +export type PickViewItemComponentProps = { + pickInfo: PickInfoType; +} & ExtraProps; + +export type PickViewDnDItemComponentProps = PickViewItemComponentProps; diff --git a/frontend/techpick/src/components/PickListViewer/index.tsx b/frontend/techpick/src/components/PickListViewer/index.tsx new file mode 100644 index 000000000..d2269feb7 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/index.tsx @@ -0,0 +1,2 @@ +export { PickListViewer } from './PickListViewer'; +export { DraggablePickListViewer } from './DraggablePickListViewer'; diff --git a/frontend/techpick/src/components/PickCard/pickCard.css.ts b/frontend/techpick/src/components/PickListViewer/pickCard.css.ts similarity index 77% rename from frontend/techpick/src/components/PickCard/pickCard.css.ts rename to frontend/techpick/src/components/PickListViewer/pickCard.css.ts index f35e59946..18e2bc7ab 100644 --- a/frontend/techpick/src/components/PickCard/pickCard.css.ts +++ b/frontend/techpick/src/components/PickListViewer/pickCard.css.ts @@ -1,25 +1,6 @@ import { keyframes, style } from '@vanilla-extract/css'; import { space, color } from 'techpick-shared'; -export const linkStyle = style({ - color: 'inherit', // 부모의 텍스트 색상 따르기 - textDecoration: 'none', // 밑줄 제거 - selectors: { - '&:hover': { - color: 'inherit', - textDecoration: 'none', - }, - '&:active': { - color: 'inherit', - textDecoration: 'none', - }, - '&:visited': { - color: 'inherit', - textDecoration: 'none', - }, - }, -}); - export const pickCardLayout = style({ display: 'flex', flexDirection: 'column', @@ -30,6 +11,7 @@ export const pickCardLayout = style({ border: `1px solid ${color.border}`, borderRadius: '4px', backgroundColor: color.background, + cursor: 'pointer', }); export const cardImageSectionStyle = style({ diff --git a/frontend/techpick/src/components/PickListViewer/pickCardGridLayout.css.ts b/frontend/techpick/src/components/PickListViewer/pickCardGridLayout.css.ts new file mode 100644 index 000000000..cbab9037e --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickCardGridLayout.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; +import { space, sizes } from 'techpick-shared'; + +export const pickCardGridLayoutStyle = style({ + display: 'grid', + // 280px는 PickCard의 width + gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', + padding: space[8], + rowGap: space[8], + columnGap: space[8], + width: sizes['full'], + height: sizes['full'], + overflowY: 'scroll', +}); diff --git a/frontend/techpick/src/components/PickListViewer/pickDnDCard.css.ts b/frontend/techpick/src/components/PickListViewer/pickDnDCard.css.ts new file mode 100644 index 000000000..8c135f5b7 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickDnDCard.css.ts @@ -0,0 +1,11 @@ +import { style } from '@vanilla-extract/css'; +import { colorThemeContract } from 'techpick-shared'; + +export const selectedDragItemStyle = style({ + backgroundColor: colorThemeContract.primary, + userSelect: 'none', +}); + +export const isActiveDraggingItemStyle = style({ + opacity: 0, +}); diff --git a/frontend/techpick/src/components/PickListViewer/pickDnDCard.util.ts b/frontend/techpick/src/components/PickListViewer/pickDnDCard.util.ts new file mode 100644 index 000000000..d253870aa --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/pickDnDCard.util.ts @@ -0,0 +1,32 @@ +import { hasIndex } from '@/utils'; +import type { OrderedPickIdListType } from '@/types'; + +export const getSelectedPickRange = ({ + orderedPickIdList, + startPickId, + endPickId, +}: GetSelectedPickRangePayload) => { + const firstSelectedIndex = orderedPickIdList.findIndex( + (orderedPickId) => orderedPickId === startPickId + ); + const lastSelectedIndex = orderedPickIdList.findIndex( + (orderedPickId) => orderedPickId === endPickId + ); + + if (!hasIndex(firstSelectedIndex) || !hasIndex(lastSelectedIndex)) return []; + + const startIndex = Math.min(firstSelectedIndex, lastSelectedIndex); + const endIndex = Math.max(firstSelectedIndex, lastSelectedIndex); + const newSelectedPickIdList = orderedPickIdList.slice( + startIndex, + endIndex + 1 + ); + + return newSelectedPickIdList; +}; + +interface GetSelectedPickRangePayload { + orderedPickIdList: OrderedPickIdListType; + startPickId: number; + endPickId: number; +} diff --git a/frontend/techpick/src/components/PickListViewerPanel/template/view/ViewTemplate.ts b/frontend/techpick/src/components/PickListViewerPanel/template/view/ViewTemplate.ts index c593cc48a..3661fd29a 100644 --- a/frontend/techpick/src/components/PickListViewerPanel/template/view/ViewTemplate.ts +++ b/frontend/techpick/src/components/PickListViewerPanel/template/view/ViewTemplate.ts @@ -1,17 +1,20 @@ +import type { ReactNode } from 'react'; import { ListBulletIcon, ViewGridIcon } from '@radix-ui/react-icons'; import { SimpleCard } from './component/Card/SimpleCard'; import { SimpleRecord } from './component/Record/SimpleRecord'; import { gridLayout } from './layout/GridLayout.css'; import { listLayout } from './layout/ListLayout.css'; -import { - Pick, - UiIcon, - UiLabel, - UiListComponent, -} from '../../types/common.type'; +import { UiIcon, UiLabel, UiListComponent } from '../../types/common.type'; -export type ViewTemplate = UiIcon & UiLabel & UiListComponent; +export interface ListProps { + pickId: number; + children: ReactNode; +} +// 여기 +export type ViewTemplate = UiIcon & UiLabel & UiListComponent; + +// ViewTemplateType을 가져간다. export type ViewTemplateType = 'GRID' | 'LIST'; /** diff --git a/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Card/SimpleCard.tsx b/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Card/SimpleCard.tsx index 1f80aee1a..76d9b0502 100644 --- a/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Card/SimpleCard.tsx +++ b/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Card/SimpleCard.tsx @@ -1,22 +1,10 @@ import { ReactElement } from 'react'; -import { - Pick, - UiProps, -} from '@/components/PickListViewerPanel/types/common.type'; -import { ChipItemList } from '@/components/PickListViewerPanel/ui/SelectedTagItem'; +import { UiProps } from '@/components/PickListViewerPanel/types/common.type'; import { cardLayout } from './SimpleCard.css'; +import { ListProps } from '../../ViewTemplate'; -interface CardProps extends UiProps {} +export function SimpleCard(props: UiProps): ReactElement { + console.log('props', props); -export function SimpleCard({ uiData: pick }: CardProps): ReactElement { - return ( -
- {pick.title} - - {/*{pick.tagOrderList.map((tag, idx) => (*/} - {/* */} - {/*))}*/} - -
- ); + return
{}
; } diff --git a/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Record/SimpleRecord.tsx b/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Record/SimpleRecord.tsx index 094c38ea6..4cc9ac0df 100644 --- a/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Record/SimpleRecord.tsx +++ b/frontend/techpick/src/components/PickListViewerPanel/template/view/component/Record/SimpleRecord.tsx @@ -1,27 +1,10 @@ import { ReactElement } from 'react'; -import { - Pick, - UiProps, -} from '@/components/PickListViewerPanel/types/common.type'; -import { - // ChipItem, - ChipItemList, -} from '@/components/PickListViewerPanel/ui/SelectedTagItem'; +import { UiProps } from '@/components/PickListViewerPanel/types/common.type'; import { recordLayout } from './SimpleRecord.css'; +import { ListProps } from '../../ViewTemplate'; -export interface SimpleRecordProps extends UiProps {} +export function SimpleRecord(props: UiProps): ReactElement { + console.log(props); -export function SimpleRecord({ - uiData: pick, -}: SimpleRecordProps): ReactElement { - return ( -
- {pick.title} - - {/*{pick.tagList.map((tag, idx) => (*/} - {/* */} - {/*))}*/} - -
- ); + return
; } diff --git a/frontend/techpick/src/components/PickListViewerPanel/types/common.type.ts b/frontend/techpick/src/components/PickListViewerPanel/types/common.type.ts index a1e0b1052..792b21854 100644 --- a/frontend/techpick/src/components/PickListViewerPanel/types/common.type.ts +++ b/frontend/techpick/src/components/PickListViewerPanel/types/common.type.ts @@ -41,11 +41,11 @@ export interface UiLabel { export interface UiListComponent { listLayoutStyle: string; - renderComponent: >(props: Props) => ReactElement; + renderComponent: (props: UiProps) => ReactElement; } export interface UiProps { - uiData: T; + props: T; } export type RadixUiIconElement = React.ForwardRefExoticComponent< diff --git a/frontend/techpick/src/components/index.ts b/frontend/techpick/src/components/index.ts index ceb5fa687..dea5d7850 100644 --- a/frontend/techpick/src/components/index.ts +++ b/frontend/techpick/src/components/index.ts @@ -2,8 +2,6 @@ export * from './Text'; export * from './Button'; export * from './Gap'; export { DeferredComponent } from './DeferredComponent'; -export { PickCard } from './PickCard/PickCard'; -export { PickCardGridLayout } from './PickCardGridLayout/PickCardGridLayout'; export { SelectedTagItem } from './SelectedTagItem'; export { SelectedTagListLayout } from './SelectedTagListLayout/SelectedTagListLayout'; export { DeleteTagDialog } from './DeleteTagDialog'; @@ -13,3 +11,4 @@ export { ToggleThemeButton } from './ToggleThemeButton'; export { FeaturedSection } from './FeaturedSection/FeaturedSection'; export { TagPicker } from './TagPicker'; export { FolderTree } from './FolderTree'; +export { PickListViewer, DraggablePickListViewer } from './PickListViewer'; diff --git a/frontend/techpick/src/stores/dndTreeStore/dndTreeStore.ts b/frontend/techpick/src/stores/dndTreeStore/dndTreeStore.ts index 7c12f7f58..dc92bf1ee 100644 --- a/frontend/techpick/src/stores/dndTreeStore/dndTreeStore.ts +++ b/frontend/techpick/src/stores/dndTreeStore/dndTreeStore.ts @@ -10,10 +10,9 @@ import { } from '@/apis/folder'; import { getEntries } from '@/components/PickListViewerPanel/types/common.type'; import { UNKNOWN_FOLDER_ID } from '@/constants'; +import { isDnDCurrentData, reorderSortableIdList } from '@/utils'; import { changeParentFolderId } from './utils/changeParentFolderId'; -import { isDnDCurrentData } from './utils/isDnDCurrentData'; import { moveFolderToDifferentParent } from './utils/moveFolderToDifferentParent'; -import { reorderFolderInSameParent } from './utils/reorderFoldersInSameParent'; import type { Active, Over, UniqueIdentifier } from '@dnd-kit/core'; import type { FolderType, @@ -205,8 +204,8 @@ export const useTreeStore = create()( prevChildFolderList = [...childFolderList]; state.treeDataMap[parentId].childFolderIdOrderedList = - reorderFolderInSameParent({ - childFolderList, + reorderSortableIdList({ + sortableIdList: childFolderList, fromId, toId, selectedFolderList, diff --git a/frontend/techpick/src/stores/dndTreeStore/utils/reorderFoldersInSameParent.ts b/frontend/techpick/src/stores/dndTreeStore/utils/reorderFoldersInSameParent.ts deleted file mode 100644 index 4e7870c6c..000000000 --- a/frontend/techpick/src/stores/dndTreeStore/utils/reorderFoldersInSameParent.ts +++ /dev/null @@ -1,44 +0,0 @@ -// utils/reorderFolder.ts -import { hasIndex } from '@/utils'; -import type { UniqueIdentifier } from '@dnd-kit/core'; -import type { ChildFolderListType, SelectedFolderListType } from '@/types'; - -export function reorderFolderInSameParent({ - childFolderList, - fromId, - toId, - selectedFolderList, -}: ReorderFolderPayload): ChildFolderListType { - const curIndex = childFolderList.findIndex((item) => item === fromId); - const targetIndex = childFolderList.findIndex((item) => item === toId); - - const nextIndex = - curIndex < targetIndex - ? Math.min(targetIndex + 1, childFolderList.length) - : targetIndex; - - if (!hasIndex(curIndex) || !hasIndex(nextIndex)) return childFolderList; - - const beforeNextIndexList = childFolderList - .slice(0, nextIndex) - .filter((index) => !selectedFolderList.includes(index)); - const afterNextIndexList = childFolderList - .slice(nextIndex) - .filter((index) => !selectedFolderList.includes(index)); - - // 새로운 리스트를 생성하여 반환합니다. - childFolderList = [ - ...beforeNextIndexList, - ...selectedFolderList, - ...afterNextIndexList, - ]; - - return childFolderList; -} - -interface ReorderFolderPayload { - childFolderList: ChildFolderListType; - fromId: UniqueIdentifier; - toId: UniqueIdentifier; - selectedFolderList: SelectedFolderListType; -} diff --git a/frontend/techpick/src/stores/pickStore/pickStore.ts b/frontend/techpick/src/stores/pickStore/pickStore.ts new file mode 100644 index 000000000..6483f3213 --- /dev/null +++ b/frontend/techpick/src/stores/pickStore/pickStore.ts @@ -0,0 +1,202 @@ +import { create } from 'zustand'; +import { subscribeWithSelector } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import { getPicksByFolderId, movePicks } from '@/apis/pick'; +import { isDnDCurrentData, reorderSortableIdList } from '@/utils'; +import type { Active, Over } from '@dnd-kit/core'; +import type { + PickRecordType, + PickInfoType, + PickRecordValueType, + SelectedPickIdListType, +} from '@/types'; + +type PickState = { + pickRecord: PickRecordType; + focusPickId: number | null; + selectedPickIdList: SelectedPickIdListType; + isDragging: boolean; +}; + +type PickAction = { + fetchPickDataByFolderId: (folderId: number) => Promise; + getOrderedPickIdListByFolderId: (folderId: number) => number[]; + getOrderedPickListByFolderId: (folderId: number) => PickInfoType[]; + getPickInfoByFolderIdAndPickId: ( + folderId: number, + pickId: number + ) => PickInfoType | null | undefined; + hasPickRecordValue: ( + pickRecordValue: PickRecordValueType | undefined + ) => pickRecordValue is PickRecordValueType; + movePicks: (movePickPayload: MovePickPayload) => Promise; + setSelectedPickIdList: ( + newSelectedPickIdList: SelectedPickIdListType + ) => void; + selectSinglePick: (pickId: number) => void; + setIsDragging: (isDragging: boolean) => void; + setFocusedPickId: (focusedPickId: number) => void; +}; + +const initialState: PickState = { + pickRecord: {}, + focusPickId: null, + selectedPickIdList: [], + isDragging: false, +}; + +export const usePickStore = create()( + subscribeWithSelector( + immer((set, get) => ({ + ...initialState, + fetchPickDataByFolderId: async (folderId) => { + try { + const { pickInfoRecord, pickIdOrderedList } = + await getPicksByFolderId(folderId); + + set((state) => { + state.pickRecord[folderId] = { + pickIdOrderedList, + pickInfoRecord, + }; + }); + } catch (error) { + console.log('fetchPickDataByFolderId error', error); + } + }, + getOrderedPickIdListByFolderId: (folderId) => { + const pickRecordValue = get().pickRecord[`${folderId}`]; + + if (!get().hasPickRecordValue(pickRecordValue)) { + return []; + } + const { pickIdOrderedList } = pickRecordValue; + + return pickIdOrderedList; + }, + getOrderedPickListByFolderId: (folderId: number) => { + const pickRecordValue = get().pickRecord[`${folderId}`]; + + if (!get().hasPickRecordValue(pickRecordValue)) { + return []; + } + + const { pickIdOrderedList, pickInfoRecord } = pickRecordValue; + const pickOrderedList: PickInfoType[] = []; + + for (const pickId of pickIdOrderedList) { + const pickInfo = pickInfoRecord[`${pickId}`]; + + if (pickInfo) { + pickOrderedList.push(pickInfo); + } + } + + return pickOrderedList; + }, + getPickInfoByFolderIdAndPickId: (folderId, pickId) => { + const pickRecordValue = get().pickRecord[`${folderId}`]; + + if (!get().hasPickRecordValue(pickRecordValue)) { + return null; + } + + const { pickIdOrderedList, pickInfoRecord } = pickRecordValue; + + if (!pickIdOrderedList.includes(pickId)) { + return null; + } + + return pickInfoRecord[`${pickId}`]; + }, + hasPickRecordValue: ( + pickRecordValue + ): pickRecordValue is PickRecordValueType => { + if (!pickRecordValue) { + return false; + } + + return true; + }, + movePicks: async ({ from, to }) => { + const fromData = from.data.current; + const toData = to.data.current; + + if (!isDnDCurrentData(fromData) || !isDnDCurrentData(toData)) return; + // SortableContext에 id가 없으면 종료 + if (!fromData.sortable.containerId || !toData.sortable.containerId) + return; + + const folderId = fromData.sortable.containerId; + const pickRecordValue = get().pickRecord[folderId]; + + if (!get().hasPickRecordValue(pickRecordValue)) { + return; + } + + const prevPickIdOrderedList = pickRecordValue.pickIdOrderedList; + const fromId = from.id; + const toId = to.id; + + set((state) => { + if (!state.pickRecord[folderId]) { + return; + } + + state.pickRecord[folderId].pickIdOrderedList = reorderSortableIdList({ + sortableIdList: prevPickIdOrderedList, + fromId, + toId, + selectedFolderList: state.selectedPickIdList, + }); + }); + + try { + await movePicks({ + idList: get().selectedPickIdList, + orderIdx: toData.sortable.index, + destinationFolderId: Number(folderId), + }); + } catch { + set((state) => { + const curPickRecordValue = state.pickRecord[`${folderId}`]; + + if (!get().hasPickRecordValue(curPickRecordValue)) { + return; + } + + curPickRecordValue.pickIdOrderedList = prevPickIdOrderedList; + state.pickRecord[`${folderId}`] = curPickRecordValue; + }); + } + }, + setSelectedPickIdList: (newSelectedPickIdList) => { + set((state) => { + state.selectedPickIdList = newSelectedPickIdList; + }); + }, + selectSinglePick: (pickId) => { + set((state) => { + state.focusPickId = pickId; + state.selectedPickIdList = [pickId]; + }); + }, + setIsDragging: (isDragging) => { + set((state) => { + state.isDragging = isDragging; + }); + }, + setFocusedPickId: (focusedPickId) => { + set((state) => { + state.focusPickId = focusedPickId; + }); + }, + })) + ) +); + +type MovePickPayload = { + folderId: number; + from: Active; + to: Over; +}; diff --git a/frontend/techpick/src/types/dnd.type.ts b/frontend/techpick/src/types/dnd.type.ts new file mode 100644 index 000000000..edc2c1cba --- /dev/null +++ b/frontend/techpick/src/types/dnd.type.ts @@ -0,0 +1,10 @@ +import type { UniqueIdentifier } from '@dnd-kit/core'; + +export type DnDCurrentType = { + id: UniqueIdentifier; + sortable: { + containerId: string | null; + items: UniqueIdentifier[]; + index: number; + }; +}; diff --git a/frontend/techpick/src/types/folder.type.ts b/frontend/techpick/src/types/folder.type.ts index ba5676d6a..d52cf5b50 100644 --- a/frontend/techpick/src/types/folder.type.ts +++ b/frontend/techpick/src/types/folder.type.ts @@ -1,20 +1,10 @@ -import type { Concrete } from './uitl.type'; -import type { UniqueIdentifier } from '@dnd-kit/core'; +import type { Concrete } from './util.type'; import type { components } from '@/schema'; export type SelectedFolderListType = number[]; export type ChildFolderListType = number[]; -export type DnDCurrentType = { - id: UniqueIdentifier; - sortable: { - containerId: string | null; - items: UniqueIdentifier[]; - index: number; - }; -}; - export type GetFolderListResponseType = Concrete< components['schemas']['techpick.api.application.folder.dto.FolderApiResponse'] >[]; diff --git a/frontend/techpick/src/types/index.ts b/frontend/techpick/src/types/index.ts index f891fa9f9..6569da7d6 100644 --- a/frontend/techpick/src/types/index.ts +++ b/frontend/techpick/src/types/index.ts @@ -1,3 +1,5 @@ export type { NodeData, DirectoryNodeProps } from './NodeData'; export type * from './tag.type'; export type * from './folder.type'; +export type * from './pick.type'; +export type * from './dnd.type'; diff --git a/frontend/techpick/src/types/pick.type.ts b/frontend/techpick/src/types/pick.type.ts new file mode 100644 index 000000000..e4245316d --- /dev/null +++ b/frontend/techpick/src/types/pick.type.ts @@ -0,0 +1,36 @@ +import type { Concrete } from './util.type'; +import type { components } from '@/schema'; + +export type PickInfoType = Concrete< + components['schemas']['techpick.api.domain.pick.dto.PickResult$Pick'] +>; + +export type PickInfoRecordType = { + [pickId: string]: PickInfoType | undefined; +}; + +export type PickIdOrderedListType = number[]; + +export type PickRecordValueType = { + pickIdOrderedList: PickIdOrderedListType; + pickInfoRecord: PickInfoRecordType; +}; + +export type PickRecordType = { + [folderId: string]: PickRecordValueType | undefined; +}; + +export type PickListType = Concrete< + components['schemas']['techpick.api.domain.pick.dto.PickResult$Pick'] +>[]; + +export type GetPicksByFolderIdResponseType = { + folderId: number; + pickList: PickListType; +}[]; + +export type OrderedPickIdListType = number[]; +export type SelectedPickIdListType = number[]; + +export type MovePicksRequestType = + components['schemas']['techpick.api.application.pick.dto.PickApiRequest$Move']; diff --git a/frontend/techpick/src/types/tag.type.ts b/frontend/techpick/src/types/tag.type.ts index b4371368d..e1dcd0a75 100644 --- a/frontend/techpick/src/types/tag.type.ts +++ b/frontend/techpick/src/types/tag.type.ts @@ -1,4 +1,4 @@ -import type { Concrete } from './uitl.type'; +import type { Concrete } from './util.type'; import type { components } from '@/schema'; export type TagType = Concrete< diff --git a/frontend/techpick/src/types/uitl.type.ts b/frontend/techpick/src/types/util.type.ts similarity index 100% rename from frontend/techpick/src/types/uitl.type.ts rename to frontend/techpick/src/types/util.type.ts diff --git a/frontend/techpick/src/stores/dndTreeStore/utils/isDnDCurrentData.ts b/frontend/techpick/src/utils/dnd/isDnDCurrentData.ts similarity index 100% rename from frontend/techpick/src/stores/dndTreeStore/utils/isDnDCurrentData.ts rename to frontend/techpick/src/utils/dnd/isDnDCurrentData.ts diff --git a/frontend/techpick/src/utils/dnd/isSelectionActive.ts b/frontend/techpick/src/utils/dnd/isSelectionActive.ts new file mode 100644 index 000000000..9d76aeca2 --- /dev/null +++ b/frontend/techpick/src/utils/dnd/isSelectionActive.ts @@ -0,0 +1 @@ +export const isSelectionActive = (length: number) => 0 < length; diff --git a/frontend/techpick/src/utils/dnd/reorderSortableList.ts b/frontend/techpick/src/utils/dnd/reorderSortableList.ts new file mode 100644 index 000000000..42dffdd1d --- /dev/null +++ b/frontend/techpick/src/utils/dnd/reorderSortableList.ts @@ -0,0 +1,47 @@ +import { hasIndex } from '@/utils'; +import type { UniqueIdentifier } from '@dnd-kit/core'; +import type { + ChildFolderListType, + SelectedFolderListType, + SelectedPickIdListType, +} from '@/types'; + +export const reorderSortableIdList = ({ + sortableIdList, + fromId, + toId, + selectedFolderList, +}: ReorderSortableIdListPayload) => { + const curIndex = sortableIdList.findIndex((item) => item === fromId); + const targetIndex = sortableIdList.findIndex((item) => item === toId); + + const nextIndex = + curIndex < targetIndex + ? Math.min(targetIndex + 1, sortableIdList.length) + : targetIndex; + + if (!hasIndex(curIndex) || !hasIndex(nextIndex)) return sortableIdList; + + const beforeNextIndexList = sortableIdList + .slice(0, nextIndex) + .filter((index) => !selectedFolderList.includes(index)); + const afterNextIndexList = sortableIdList + .slice(nextIndex) + .filter((index) => !selectedFolderList.includes(index)); + + // 새로운 리스트를 생성하여 반환합니다. + const newSortableIdList = [ + ...beforeNextIndexList, + ...selectedFolderList, + ...afterNextIndexList, + ]; + + return newSortableIdList; +}; + +interface ReorderSortableIdListPayload { + sortableIdList: ChildFolderListType | number[]; + fromId: UniqueIdentifier; + toId: UniqueIdentifier; + selectedFolderList: SelectedFolderListType | SelectedPickIdListType; +} diff --git a/frontend/techpick/src/utils/index.ts b/frontend/techpick/src/utils/index.ts index 0e2303e26..e42d6bb1e 100644 --- a/frontend/techpick/src/utils/index.ts +++ b/frontend/techpick/src/utils/index.ts @@ -4,3 +4,6 @@ export { getClientCookie } from './getClientCookie'; export { hasIndex } from './array'; export { isEmptyString } from './string'; export { getPortalContainer } from './portal'; +export { isDnDCurrentData } from './dnd/isDnDCurrentData'; +export { reorderSortableIdList } from './dnd/reorderSortableList'; +export { isSelectionActive } from './dnd/isSelectionActive';