From c6e5ae2647e1b70f5f592b1adf492f2d6c83ac6c Mon Sep 17 00:00:00 2001 From: dmdgpdi <33450285+dmdgpdi@users.noreply.github.com> Date: Tue, 19 Nov 2024 20:04:46 +0900 Subject: [PATCH] =?UTF-8?q?[FEAT]=20=ED=94=BD=20=EB=A0=8C=EB=8D=94?= =?UTF-8?q?=EB=A7=81=20=EC=B9=B4=EB=93=9C=EC=97=90=EC=84=9C=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=95=84=EC=9D=B4=ED=85=9C=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B5=90=EC=B2=B4=20(#501)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: PickRenderModeState 구현 * feat: PickRecordListLayout 컴포넌트 구현 * feat: PickCard대신 PickRecord 렌더링으로 변경 * feat: usePickRenderModeStore 의존성 제거 --- .../(signed)/folders/unclassified/page.tsx | 1 + .../techpick/src/components/DragOverlay.tsx | 12 +- .../DraggablePickListViewer.tsx | 26 +++-- .../PickListViewer/PickDnDCardListLayout.tsx | 3 +- .../PickListViewer/PickDndRecord.tsx | 103 ++++++++++++++++++ .../PickDndRecordListLayout.tsx | 17 +++ .../PickListSortableContext.tsx | 17 ++- .../PickListViewerInfiniteScroll.tsx | 4 +- .../PickListViewer/PickRecordListLayout.tsx | 9 ++ .../pickRecordListLayout.css.ts | 15 ++- frontend/techpick/src/stores/index.ts | 1 + .../src/stores/pickRenderModeStore.ts | 24 ++++ .../src/stores/pickRenderModeStore.type.ts | 9 ++ .../techpick/src/types/PickRenderModeType.ts | 4 + frontend/techpick/src/types/index.ts | 1 + 15 files changed, 226 insertions(+), 20 deletions(-) create mode 100644 frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx create mode 100644 frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx create mode 100644 frontend/techpick/src/components/PickListViewer/PickRecordListLayout.tsx create mode 100644 frontend/techpick/src/stores/pickRenderModeStore.ts create mode 100644 frontend/techpick/src/stores/pickRenderModeStore.type.ts create mode 100644 frontend/techpick/src/types/PickRenderModeType.ts diff --git a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx index af4420b68..fde9619f9 100644 --- a/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx +++ b/frontend/techpick/src/app/(signed)/folders/unclassified/page.tsx @@ -41,6 +41,7 @@ export default function UnclassifiedFolderPage() { ); } diff --git a/frontend/techpick/src/components/DragOverlay.tsx b/frontend/techpick/src/components/DragOverlay.tsx index 060ab8af0..404adc649 100644 --- a/frontend/techpick/src/components/DragOverlay.tsx +++ b/frontend/techpick/src/components/DragOverlay.tsx @@ -3,13 +3,15 @@ import { useEffect, useState } from 'react'; import type { CSSProperties } from 'react'; import { DragOverlay as DndKitDragOverlay } from '@dnd-kit/core'; -import { usePickStore, useTreeStore } from '@/stores'; +import { usePickRenderModeStore, usePickStore, useTreeStore } from '@/stores'; import { pickDragOverlayStyle } from './dragOverlay.css'; import { PickCard } from './PickListViewer/PickCard'; +import { PickRecord } from './PickListViewer/PickRecord'; export function DargOverlay({ elementClickPosition }: DargOverlayProps) { const { isDragging: isFolderDragging, draggingFolderInfo } = useTreeStore(); const { isDragging: isPickDragging, draggingPickInfo } = usePickStore(); + const { pickRenderMode } = usePickRenderModeStore(); const [mousePosition, setMousePosition] = useState({ x: -1, y: -1 }); const overlayStyle: CSSProperties = { top: `${mousePosition.y}px`, @@ -72,6 +74,14 @@ export function DargOverlay({ elementClickPosition }: DargOverlayProps) { } if (isPickDragging && draggingPickInfo) { + if (pickRenderMode === 'list') { + return ( + + + + ); + } + return (
diff --git a/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx index e850c965e..81c2b66fb 100644 --- a/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx +++ b/frontend/techpick/src/components/PickListViewer/DraggablePickListViewer.tsx @@ -1,22 +1,24 @@ import type { ReactNode } from 'react'; import { PickDnDCard } from './PickDnDCard'; import { PickDnDCardListLayout } from './PickDnDCardListLayout'; +import { PickDndRecord } from './PickDndRecord'; +import { PickDndRecordListLayout } from './PickDndRecordListLayout'; import type { PickViewItemComponentProps, PickViewItemListLayoutComponentProps, } from './PickListViewer'; -import type { PickInfoType } from '@/types'; +import type { PickInfoType, PickRenderModeType } from '@/types'; export function DraggablePickListViewer({ pickList, - viewType = 'card', + viewType = 'list', folderId, }: PickListViewerProps) { const { PickViewItemComponent, PickViewItemListLayoutComponent } = DND_PICK_LIST_VIEW_TEMPLATES[viewType]; return ( - + {pickList.map((pickInfo) => ( ))} @@ -27,24 +29,23 @@ export function DraggablePickListViewer({ interface PickListViewerProps { pickList: PickInfoType[]; folderId: number; - viewType?: DnDViewTemplateType; + viewType?: PickRenderModeType; } const DND_PICK_LIST_VIEW_TEMPLATES: Record< - DnDViewTemplateType, + PickRenderModeType, DnDViewTemplateValueType > = { card: { PickViewItemComponent: PickDnDCard, PickViewItemListLayoutComponent: PickDnDCardListLayout, }, + list: { + PickViewItemComponent: PickDndRecord, + PickViewItemListLayoutComponent: PickDndRecordListLayout, + }, }; -/** - * @description DnDViewTemplateType은 Drag&Drop이 가능한 UI 중 무엇을 보여줄지 나타냅니다. ex) card, list - */ -type DnDViewTemplateType = 'card'; - interface DnDViewTemplateValueType { PickViewItemListLayoutComponent: ( props: PickViewDnDItemListLayoutComponentProps @@ -53,6 +54,9 @@ interface DnDViewTemplateValueType { } export type PickViewDnDItemListLayoutComponentProps = - PickViewItemListLayoutComponentProps<{ folderId: number }>; + PickViewItemListLayoutComponentProps<{ + folderId: number; + viewType: PickRenderModeType; + }>; export type PickViewDnDItemComponentProps = PickViewItemComponentProps; diff --git a/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx index 49d77d37a..a1cf7b8e3 100644 --- a/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickDnDCardListLayout.tsx @@ -6,11 +6,12 @@ import { PickListSortableContext } from './PickListSortableContext'; export function PickDnDCardListLayout({ children, + viewType, folderId, }: PickViewDnDItemListLayoutComponentProps) { return (
- + {children}
diff --git a/frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx b/frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx new file mode 100644 index 000000000..59f85f174 --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickDndRecord.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { MouseEvent, useCallback, type CSSProperties } from 'react'; +import { useSortable } from '@dnd-kit/sortable'; +import { CSS } from '@dnd-kit/utilities'; +import { usePickStore } from '@/stores'; +import { isSelectionActive } from '@/utils'; +import { + isActiveDraggingItemStyle, + selectedDragItemStyle, +} from './pickDnDCard.css'; +import { getSelectedPickRange } from './pickDnDCard.util'; +import { PickRecord } from './PickRecord'; +import type { PickViewDnDItemComponentProps } from './PickListViewer'; + +export function PickDndRecord({ pickInfo }: PickViewDnDItemComponentProps) { + const { + selectedPickIdList, + selectSinglePick, + getOrderedPickIdListByFolderId, + focusPickId, + setSelectedPickIdList, + isDragging, + } = usePickStore(); + const { linkInfo, id: pickId, parentFolderId } = pickInfo; + const { url } = linkInfo; + const isSelected = selectedPickIdList.includes(pickId); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging: isActiveDragging, + } = useSortable({ + id: pickId, + data: { + id: pickId, + type: 'pick', + parentFolderId: parentFolderId, + }, + }); + const pickElementId = `pickId-${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)} + id={pickElementId} + > + +
+
+ + ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx b/frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx new file mode 100644 index 000000000..c0f814f8d --- /dev/null +++ b/frontend/techpick/src/components/PickListViewer/PickDndRecordListLayout.tsx @@ -0,0 +1,17 @@ +import { PickViewDnDItemListLayoutComponentProps } from './DraggablePickListViewer'; +import { PickListSortableContext } from './PickListSortableContext'; +import { PickRecordListLayout } from './PickRecordListLayout'; + +export function PickDndRecordListLayout({ + children, + folderId, + viewType, +}: PickViewDnDItemListLayoutComponentProps) { + return ( + + + {children} + + + ); +} diff --git a/frontend/techpick/src/components/PickListViewer/PickListSortableContext.tsx b/frontend/techpick/src/components/PickListViewer/PickListSortableContext.tsx index 4330a7171..de8ca13a1 100644 --- a/frontend/techpick/src/components/PickListViewer/PickListSortableContext.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickListSortableContext.tsx @@ -1,12 +1,17 @@ 'use client'; -import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'; -import { usePickStore } from '@/stores/pickStore/pickStore'; +import { + SortableContext, + rectSortingStrategy, + verticalListSortingStrategy, +} from '@dnd-kit/sortable'; +import { usePickStore } from '@/stores'; import { PickViewDnDItemListLayoutComponentProps } from './DraggablePickListViewer'; export function PickListSortableContext({ folderId, children, + viewType, }: PickViewDnDItemListLayoutComponentProps) { const { getOrderedPickIdListByFolderId, @@ -23,11 +28,17 @@ export function PickListSortableContext({ ) : orderedPickIdList; + /** + * @description card일때와 vertical일 때 렌더링이 다릅니다. + */ + const strategy = + viewType === 'card' ? rectSortingStrategy : verticalListSortingStrategy; + return ( {children} diff --git a/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx b/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx index ee28b3ac7..88fb7cff2 100644 --- a/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx +++ b/frontend/techpick/src/components/PickListViewer/PickListViewerInfiniteScroll.tsx @@ -6,7 +6,7 @@ import InfiniteLoader from 'react-window-infinite-loader'; import { colorVars } from 'techpick-shared'; import { PickRecord } from '@/components/PickListViewer/PickRecord'; import { - pickRecordListLayoutStyle, + pickRecordListLayoutInlineStyle, RECORD_HEIGHT, } from '@/components/PickListViewer/pickRecordListLayout.css'; import { usePickStore } from '@/stores'; @@ -62,7 +62,7 @@ export function PickListViewerInfiniteScroll( > {({ onItemsRendered, ref }) => ( {children}
; +} diff --git a/frontend/techpick/src/components/PickListViewer/pickRecordListLayout.css.ts b/frontend/techpick/src/components/PickListViewer/pickRecordListLayout.css.ts index 8c6b262e6..68681eccf 100644 --- a/frontend/techpick/src/components/PickListViewer/pickRecordListLayout.css.ts +++ b/frontend/techpick/src/components/PickListViewer/pickRecordListLayout.css.ts @@ -1,5 +1,16 @@ -import { CSSProperties } from 'react'; +import type { CSSProperties } from 'react'; +import { style } from '@vanilla-extract/css'; +import { sizes, space } from 'techpick-shared'; export const RECORD_HEIGHT = 100; -export const pickRecordListLayoutStyle: CSSProperties = {}; +export const pickRecordListLayoutInlineStyle: CSSProperties = {}; + +export const pickRecordListLayoutStyle = style({ + display: 'flex', + flexDirection: 'column', + gap: space['12'], + width: sizes['full'], + height: sizes['full'], + overflowY: 'scroll', +}); diff --git a/frontend/techpick/src/stores/index.ts b/frontend/techpick/src/stores/index.ts index 3b6f750dc..18c84e6a8 100644 --- a/frontend/techpick/src/stores/index.ts +++ b/frontend/techpick/src/stores/index.ts @@ -1,3 +1,4 @@ export { useTagStore } from './tagStore'; export { usePickStore } from './pickStore/pickStore'; export { useTreeStore } from './dndTreeStore/dndTreeStore'; +export { usePickRenderModeStore } from './pickRenderModeStore'; diff --git a/frontend/techpick/src/stores/pickRenderModeStore.ts b/frontend/techpick/src/stores/pickRenderModeStore.ts new file mode 100644 index 000000000..7d2bff2c0 --- /dev/null +++ b/frontend/techpick/src/stores/pickRenderModeStore.ts @@ -0,0 +1,24 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import type { + PickRenderModeAction, + PickRenderModeState, +} from './pickRenderModeStore.type'; + +const initialState: PickRenderModeState = { + pickRenderMode: 'list', +}; + +export const usePickRenderModeStore = create< + PickRenderModeState & PickRenderModeAction +>()( + immer((set) => ({ + ...initialState, + + setPickRenderMode: (newPickRenderMode) => { + set((state) => { + state.pickRenderMode = newPickRenderMode; + }); + }, + })) +); diff --git a/frontend/techpick/src/stores/pickRenderModeStore.type.ts b/frontend/techpick/src/stores/pickRenderModeStore.type.ts new file mode 100644 index 000000000..32d55bdcd --- /dev/null +++ b/frontend/techpick/src/stores/pickRenderModeStore.type.ts @@ -0,0 +1,9 @@ +import type { PickRenderModeType } from '@/types'; + +export type PickRenderModeState = { + pickRenderMode: PickRenderModeType; +}; + +export type PickRenderModeAction = { + setPickRenderMode: (newPickRenderMode: PickRenderModeType) => void; +}; diff --git a/frontend/techpick/src/types/PickRenderModeType.ts b/frontend/techpick/src/types/PickRenderModeType.ts new file mode 100644 index 000000000..a50072175 --- /dev/null +++ b/frontend/techpick/src/types/PickRenderModeType.ts @@ -0,0 +1,4 @@ +/** + * @description PickRenderModeType은 Pick의 렌더링 UI 중 무엇을 보여줄지 나타냅니다. ex) card, list + */ +export type PickRenderModeType = 'card' | 'list'; diff --git a/frontend/techpick/src/types/index.ts b/frontend/techpick/src/types/index.ts index 099c111bd..5dac9653f 100644 --- a/frontend/techpick/src/types/index.ts +++ b/frontend/techpick/src/types/index.ts @@ -6,3 +6,4 @@ export type * from './dnd.type'; export type { PickDraggableObjectType } from './PickDraggableObjectType'; export type { FolderDraggableObjectType } from './FolderDraggableObjectType'; export type { PickToFolderDroppableObjectType } from './PickToFolderDroppableObjectType'; +export type { PickRenderModeType } from './PickRenderModeType';