diff --git a/src/components/task/kanban/TaskItem.tsx b/src/components/task/kanban/TaskItem.tsx new file mode 100644 index 00000000..5aebda7a --- /dev/null +++ b/src/components/task/kanban/TaskItem.tsx @@ -0,0 +1,26 @@ +import { Draggable, DraggableId } from '@hello-pangea/dnd'; + +type TaskItemProps = { + draggableId: DraggableId; + name: string; + color: string; + index: number; +}; + +export default function TaskItem({ draggableId, name, color, index }: TaskItemProps) { + return ( + + {(dragProvided) => ( +
+
+
{name}
+
+ )} + + ); +} diff --git a/src/components/task/kanban/TaskItemList.tsx b/src/components/task/kanban/TaskItemList.tsx new file mode 100644 index 00000000..d9d87070 --- /dev/null +++ b/src/components/task/kanban/TaskItemList.tsx @@ -0,0 +1,36 @@ +import { useMemo } from 'react'; +import { Droppable } from '@hello-pangea/dnd'; +import TaskItem from '@components/task/kanban/TaskItem'; +import { generatePrefixId } from '@utils/converter'; +import { DND_DRAGGABLE_PREFIX, DND_DROPPABLE_PREFIX, DND_TYPE } from '@constants/dnd'; +import type { Task } from '@/types/TaskType'; + +type TaskItemListProps = { + statusId: number; + color: string; + tasks: Task[]; +}; + +export default function TaskItemList({ statusId, color, tasks }: TaskItemListProps) { + const droppableId = useMemo(() => generatePrefixId(statusId, DND_DROPPABLE_PREFIX.TASK), [statusId]); + return ( + + {(taskDropProvided) => ( +
+ {tasks.map((task) => { + const { taskId, name, order } = task; + const draggableId = generatePrefixId(taskId, DND_DRAGGABLE_PREFIX.TASK); + const index = order - 1; + return ; + })} + {taskDropProvided.placeholder} +
+ )} +
+ ); +} diff --git a/src/components/task/kanban/TaskStatusContainer.tsx b/src/components/task/kanban/TaskStatusContainer.tsx new file mode 100644 index 00000000..c4e85849 --- /dev/null +++ b/src/components/task/kanban/TaskStatusContainer.tsx @@ -0,0 +1,37 @@ +import { useMemo } from 'react'; +import { Draggable } from '@hello-pangea/dnd'; +import TaskItemList from '@components/task/kanban/TaskItemList'; +import { generatePrefixId } from '@utils/converter'; +import { DND_DRAGGABLE_PREFIX } from '@constants/dnd'; +import { BsPencil } from 'react-icons/bs'; +import { TaskWithStatus } from '@/types/TaskType'; + +type TaskStatusContainerProps = { + statusTask: TaskWithStatus; +}; + +export default function TaskStatusContainer({ statusTask }: TaskStatusContainerProps) { + const { statusId, name, color, order, tasks } = statusTask; + const draggableId = useMemo(() => generatePrefixId(statusId, DND_DRAGGABLE_PREFIX.STATUS), [statusId]); + const index = useMemo(() => order - 1, [order]); + + return ( + + {(statusDragProvided) => ( +
+
+

{name}

+ + + +
+ +
+ )} +
+ ); +} diff --git a/src/constants/dnd.ts b/src/constants/dnd.ts new file mode 100644 index 00000000..757beb8b --- /dev/null +++ b/src/constants/dnd.ts @@ -0,0 +1,19 @@ +type DndTypeKeys = keyof typeof DND_TYPE; +type DndTypeValues = (typeof DND_TYPE)[DndTypeKeys]; +type DndDroppablePrefix = { + [K in DndTypeKeys]: `${DndTypeValues}-CONTAINER`; +}; + +export const DND_TYPE = Object.freeze({ + STATUS: 'STATUS', + TASK: 'TASK', +}); + +export const DND_DROPPABLE_PREFIX: Readonly = Object.freeze( + Object.entries(DND_TYPE).reduce((obj, [key, value]) => { + obj[key as DndTypeKeys] = `${value}-CONTAINER`; + return obj; + }, {} as DndDroppablePrefix), +); + +export const DND_DRAGGABLE_PREFIX = Object.freeze({ ...DND_TYPE }); diff --git a/src/pages/project/KanbanPage.tsx b/src/pages/project/KanbanPage.tsx index 0bd318d6..6cee0869 100644 --- a/src/pages/project/KanbanPage.tsx +++ b/src/pages/project/KanbanPage.tsx @@ -1,26 +1,13 @@ import { useState } from 'react'; -import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'; -import { BsPencil } from 'react-icons/bs'; +import { DragDropContext, Droppable, DropResult } from '@hello-pangea/dnd'; +import TaskStatusContainer from '@components/task/kanban/TaskStatusContainer'; +import { DND_DROPPABLE_PREFIX, DND_TYPE } from '@constants/dnd'; import deepClone from '@utils/deepClone'; - +import { parsePrefixId } from '@utils/converter'; import { TASK_DUMMY } from '@mocks/mockData'; -import type { DropResult } from '@hello-pangea/dnd'; import type { Task, TaskWithStatus } from '@/types/TaskType'; -const DND_TYPE = { - STATUS: 'STATUS', - TASK: 'TASK', -}; - -// ToDo: 유틸리티로 분리할지 고려하기 -function generatorPrefixId(id: number | string, prefix: string, delimiter: string = '-') { - return `${prefix}${delimiter}${id}`; -} -function parserPrefixId(prefixId: string, delimiter: string = '-') { - const result = prefixId.split(delimiter); - return result[result.length - 1]; -} - +// ToDo: 상태 DnD와 할일 DnD 로직 통합 가능한가 생각해보기 function createChangedStatus(statusTasks: TaskWithStatus[], dropResult: DropResult) { const { source, destination } = dropResult; @@ -40,9 +27,9 @@ function createChangedTasks(statusTasks: TaskWithStatus[], dropResult: DropResul // ToDo: 메세지 포맷 정하고 수정하기 if (!destination) throw Error('Error: DnD destination is null'); - const sourceStatusId = Number(parserPrefixId(source.droppableId)); - const destinationStatusId = Number(parserPrefixId(destination.droppableId)); - const taskId = Number(parserPrefixId(draggableId)); + const sourceStatusId = Number(parsePrefixId(source.droppableId)); + const destinationStatusId = Number(parsePrefixId(destination.droppableId)); + const taskId = Number(parsePrefixId(draggableId)); const newStatusTasks = deepClone(statusTasks); const { tasks: sourceTasks } = newStatusTasks.find((data) => data.statusId === sourceStatusId)! as TaskWithStatus; @@ -61,7 +48,6 @@ function createChangedTasks(statusTasks: TaskWithStatus[], dropResult: DropResul } // ToDo: DnD시 가시성을 위한 애니메이션 처리 추가할 것 -// ToDo: 칸반보드 ItemList, Item 컴포넌트로 분리할 것 export default function KanbanPage() { const [statusTasks, setStatusTasks] = useState(TASK_DUMMY); @@ -85,71 +71,16 @@ export default function KanbanPage() { return ( - + {(statusDropProvided) => (
- {statusTasks.map((data) => { - const { statusId, name, color, order, tasks } = data; - const draggableId = generatorPrefixId(statusId, 'status-drag'); - const droppableId = generatorPrefixId(statusId, 'status'); - const index = order - 1; - return ( - - {(statusDragProvided) => ( -
-
-

{name}

- - - -
- - {(taskDropProvided) => ( -
- {tasks.map((task) => { - const { taskId, name, order } = task; - const draggableId = generatorPrefixId(taskId, 'task'); - const index = order - 1; - return ( - - {(dragProvided) => ( -
-
-
- {name} -
-
- )} - - ); - })} - {taskDropProvided.placeholder} -
- )} -
-
- )} -
- ); - })} + {statusTasks.map((statusTask) => ( + + ))} {statusDropProvided.placeholder}
)} diff --git a/src/types/TaskStatusType.tsx b/src/types/TaskStatusType.tsx index e8bc76e3..8a4b3ea8 100644 --- a/src/types/TaskStatusType.tsx +++ b/src/types/TaskStatusType.tsx @@ -1,3 +1,4 @@ +// ToDo: ERD에 맞춰 ProjectStatus로 변경할 것 // ToDo: API 설계 완료시 데이터 타입 변경할 것 export type TaskStatus = { statusId: number; diff --git a/src/utils/converter.ts b/src/utils/converter.ts new file mode 100644 index 00000000..37847405 --- /dev/null +++ b/src/utils/converter.ts @@ -0,0 +1,8 @@ +export function generatePrefixId(id: number | string, prefix: string, delimiter: string = '-') { + return `${prefix}${delimiter}${id}`; +} + +export function parsePrefixId(prefixId: string, delimiter: string = '-') { + const result = prefixId.split(delimiter); + return result[result.length - 1]; +}