diff --git a/frontend/src/components/dashboard/dashboard-edit/CardEditView.tsx b/frontend/src/components/dashboard/dashboard-edit/CardEditView.tsx index a4e8aaf0..119eba60 100644 --- a/frontend/src/components/dashboard/dashboard-edit/CardEditView.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/CardEditView.tsx @@ -1,7 +1,8 @@ import { useNavigate } from 'react-router-dom'; import { ButtonGroup } from '@/components/shared'; -import { useEditCard } from '@/hooks/dashboard'; +import { useDragAndDropCard, useEditCard } from '@/hooks/dashboard'; +import { useEditCardContext } from '@/hooks/dashboard/useEditCardContext'; import { CardEditViewTabs } from './CardEditViewTabs'; @@ -10,6 +11,11 @@ export const CardEditView = () => { const { isDirty } = useEditCard(); + const { isOverList } = useEditCardContext(); + + const { handleListDragEnter, handleListDragLeave, handleListDrop } = + useDragAndDropCard(); + const handleCancel = () => { navigate(-1); }; @@ -19,7 +25,13 @@ export const CardEditView = () => { }; return ( -
+
e.preventDefault()} + onDragLeave={handleListDragLeave} + onDrop={handleListDrop} + >

카드 편집

@@ -32,6 +44,14 @@ export const CardEditView = () => {
+ {isOverList && ( + <> +
+

+ 삭제하려면 여기에 놓으세요 +

+ + )}
); }; diff --git a/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx b/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx index b3c308be..3c81d649 100644 --- a/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/CardEditViewCard.tsx @@ -2,10 +2,11 @@ import { useCallback, useMemo } from 'react'; import { EditCardWrapper } from '@/components/shared'; import { + DASHBOARD_EDIT_AREA, DASHBOARD_METRIC_CARDS, type MetricCardCode, } from '@/constants/dashboard'; -import { useEditCard } from '@/hooks/dashboard'; +import { useDragAndDropCard, useEditCard } from '@/hooks/dashboard'; import { EditCardContent } from './EditCardContent'; @@ -15,6 +16,7 @@ interface CardEditViewCardProps { export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => { const { addCard, removeCard, isAdded } = useEditCard(); + const { handleDragStart, handleDragEnd } = useDragAndDropCard(); const card = useMemo(() => DASHBOARD_METRIC_CARDS[cardCode], [cardCode]); @@ -35,7 +37,20 @@ export const CardEditViewCard = ({ cardCode }: CardEditViewCardProps) => { const { period, sizeX, sizeY } = card; return ( -
  • +
  • + handleDragStart(e, DASHBOARD_EDIT_AREA.LIST, { + cardCode, + colNo: -1, + rowNo: -1, + }) + } + onDragEnd={handleDragEnd} + className="translate-x-0 cursor-grab active:cursor-grabbing" + onClick={handleAddCard} + > { const dashboardMetrics = useMemo(() => Object.values(DASHBOARD_METRICS), []); return ( - + {dashboardMetrics.map(({ tab }) => ( { return ( -
    +
    diff --git a/frontend/src/components/dashboard/dashboard-edit/EditCardProvider.tsx b/frontend/src/components/dashboard/dashboard-edit/EditCardProvider.tsx index 6b7ed297..5663b64d 100644 --- a/frontend/src/components/dashboard/dashboard-edit/EditCardProvider.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/EditCardProvider.tsx @@ -1,23 +1,41 @@ -import { type PropsWithChildren, useState } from 'react'; +import { type PropsWithChildren, useRef, useState } from 'react'; -import { - GRID_COL_SIZE, - GRID_ROW_SIZE, - type MetricCardCode, -} from '@/constants/dashboard'; import { EditCardContext } from '@/constants/dashboard/editCardContext'; - -const EMPTY_GRID: (MetricCardCode | null)[][] = Array.from( - { length: GRID_ROW_SIZE + 1 }, - () => Array.from({ length: GRID_COL_SIZE + 1 }, () => null), -); +import type { DashboardCard, DragState, GhostState } from '@/types/dashboard'; export const EditCardProvider = ({ children }: PropsWithChildren) => { - const initGrid = EMPTY_GRID; // TODO: 초기 그리드 상태 서버에서 받아오기 - const [grid, setGrid] = useState<(MetricCardCode | null)[][]>(EMPTY_GRID); + // TODO: 초기 그리드 상태 서버에서 받아오기 + const initPlacedCards: DashboardCard[] = []; + + // 카드 그리드 상태 + const [placedCards, setPlacedCards] = + useState(initPlacedCards); + + // 드래그앤드랍 관련 상태 + const [dragState, setDragState] = useState(null); + const [ghost, setGhost] = useState(null); + const [tempLayout, setTempLayout] = useState(null); + const [isOverList, setIsOverList] = useState(false); + + const gridRef = useRef(null); return ( - + {children} ); diff --git a/frontend/src/components/dashboard/dashboard-edit/MiniView.tsx b/frontend/src/components/dashboard/dashboard-edit/MiniView.tsx index 0e9f63f6..b6bc31f3 100644 --- a/frontend/src/components/dashboard/dashboard-edit/MiniView.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/MiniView.tsx @@ -1,34 +1,56 @@ import { useLocation } from 'react-router-dom'; -import { useEditCard } from '@/hooks/dashboard'; +import { GRID_COL_SIZE, GRID_ROW_SIZE } from '@/constants/dashboard'; +import { useDragAndDropCard, useEditCardContext } from '@/hooks/dashboard'; import { MiniViewActiveCard } from './MiniViewActiveCard'; import { MiniViewEmptyCard } from './MiniViewEmptyCard'; +import { MiniViewGhost } from './MiniViewGhost'; export const MiniView = () => { const searchParams = new URLSearchParams(useLocation().search); const title = searchParams.get('tab') || '알 수 없음'; - const { cards, emptyCellCount } = useEditCard(); + const { gridRef, dragState, tempLayout, placedCards } = useEditCardContext(); + const { handleGridDragOver, handleGridDragLeave, handleGridDrop } = + useDragAndDropCard(); return ( -
    +

    {title}

    -
    - {Array.from({ length: emptyCellCount }).map((_, index) => ( - - ))} +
    + {/* 그리드 셀 가이드라인 */} +
    + {Array.from({ length: GRID_ROW_SIZE * GRID_COL_SIZE }).map( + (_, index) => ( + + ), + )} +
    + {/* 활성 카드 */} - {cards.map((card) => ( - - ))} + {(tempLayout || placedCards).map((card) => { + const isDragging = dragState?.draggingCard.cardCode === card.cardCode; + return ( + + ); + })} + +
    ); diff --git a/frontend/src/components/dashboard/dashboard-edit/MiniViewActiveCard.tsx b/frontend/src/components/dashboard/dashboard-edit/MiniViewActiveCard.tsx index fe092432..7e516767 100644 --- a/frontend/src/components/dashboard/dashboard-edit/MiniViewActiveCard.tsx +++ b/frontend/src/components/dashboard/dashboard-edit/MiniViewActiveCard.tsx @@ -5,23 +5,34 @@ import { XIcon } from 'lucide-react'; import { PeriodTag } from '@/components/shared'; import { Button } from '@/components/shared/shadcn-ui'; import { + DASHBOARD_EDIT_AREA, DASHBOARD_METRIC_CARDS, type MetricCardCode, } from '@/constants/dashboard'; -import { CDN_BASE_URL } from '@/constants/shared/cdnBaseUrl'; -import { useEditCard } from '@/hooks/dashboard'; +import { CDN_BASE_URL } from '@/constants/shared'; +import { + useDragAndDropCard, + useEditCard, + useGridCellSize, +} from '@/hooks/dashboard'; +import { cn } from '@/utils/shared'; interface MiniViewActiveCardProps { cardCode: MetricCardCode; - posX: number; - posY: number; + colNo: number; + rowNo: number; + isDragging: boolean; } export const MiniViewActiveCard = ({ cardCode, - posX, - posY, + colNo, + rowNo, + isDragging, }: MiniViewActiveCardProps) => { const { removeCard } = useEditCard(); + const { handleDragStart, handleDragEnd } = useDragAndDropCard(); + const { getGridPosition, getGridCardSize } = useGridCellSize(); + const card = DASHBOARD_METRIC_CARDS[cardCode]; const handleRemove = useCallback( @@ -35,21 +46,39 @@ export const MiniViewActiveCard = ({ } const { label, type, period, sizeX, sizeY } = card; + const { topInPixel, leftInPixel } = getGridPosition(rowNo, colNo); + const { widthInPixel, heightInPixel } = getGridCardSize(sizeX, sizeY); + return (
    + handleDragStart(e, DASHBOARD_EDIT_AREA.GRID, { + cardCode, + colNo, + rowNo, + }) + } + onDragEnd={handleDragEnd} + className={cn( + 'rounded-300 bg-grey-0 absolute top-0 left-0 cursor-grab border-none transition-all duration-200 select-none active:cursor-grabbing', + isDragging ? 'opacity-0' : 'opacity-100', + )} style={{ - gridColumn: `${posX} / span ${sizeX}`, - gridRow: `${posY} / span ${sizeY}`, + translate: `${leftInPixel}px ${topInPixel}px`, + width: widthInPixel, + height: heightInPixel, }} > -
    +
    {`${label} -

    {label}

    +

    + {label} +

    {/* 상단 삭제 버튼 */} diff --git a/frontend/src/components/dashboard/dashboard-edit/MiniViewGhost.tsx b/frontend/src/components/dashboard/dashboard-edit/MiniViewGhost.tsx new file mode 100644 index 00000000..36f76e6e --- /dev/null +++ b/frontend/src/components/dashboard/dashboard-edit/MiniViewGhost.tsx @@ -0,0 +1,35 @@ +import { DASHBOARD_METRIC_CARDS } from '@/constants/dashboard'; +import { useEditCardContext, useGridCellSize } from '@/hooks/dashboard'; +import { cn } from '@/utils/shared'; + +export const MiniViewGhost = () => { + const { dragState, ghost, isOverList } = useEditCardContext(); + const { getGridPosition, getGridCardSize } = useGridCellSize(); + + if (!dragState || !ghost || isOverList) { + return null; + } + + const draggingCardDef = + DASHBOARD_METRIC_CARDS[dragState.draggingCard.cardCode]; + + const { topInPixel, leftInPixel } = getGridPosition(ghost.rowNo, ghost.colNo); + const { widthInPixel, heightInPixel } = getGridCardSize( + draggingCardDef.sizeX, + draggingCardDef.sizeY, + ); + + return ( +
    + ); +}; diff --git a/frontend/src/components/shared/edit-card-wrapper/PlusIconButton.tsx b/frontend/src/components/shared/edit-card-wrapper/PlusIconButton.tsx index bd993c78..5f7247f4 100644 --- a/frontend/src/components/shared/edit-card-wrapper/PlusIconButton.tsx +++ b/frontend/src/components/shared/edit-card-wrapper/PlusIconButton.tsx @@ -10,7 +10,10 @@ interface PlusIconButtonProps { export const PlusIconButton = ({ onClickAddButton }: PlusIconButtonProps) => { return (