Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: #33 컴포넌트 분리 / types, constants 등 관심사 분리 #41

Merged
merged 4 commits into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/components/task/kanban/TaskItem.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Draggable draggableId={draggableId} index={index}>
{(dragProvided) => (
<div
className="m-5 flex h-30 items-center justify-start gap-5 rounded-sl bg-[#FEFEFE] p-5"
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
<div style={{ borderColor: color }} className="h-8 w-8 rounded-full border" />
<div className="select-none overflow-hidden text-ellipsis text-nowrap">{name}</div>
</div>
)}
</Draggable>
);
}
36 changes: 36 additions & 0 deletions src/components/task/kanban/TaskItemList.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Droppable droppableId={droppableId} type={DND_TYPE.TASK}>
{(taskDropProvided) => (
<article
style={{ borderColor: color }}
className="h-full w-full grow border-l-[3px] bg-scroll"
ref={taskDropProvided.innerRef}
{...taskDropProvided.droppableProps}
>
{tasks.map((task) => {
const { taskId, name, order } = task;
const draggableId = generatePrefixId(taskId, DND_DRAGGABLE_PREFIX.TASK);
const index = order - 1;
return <TaskItem key={taskId} draggableId={draggableId} color={color} index={index} name={name} />;
})}
{taskDropProvided.placeholder}
</article>
)}
</Droppable>
);
}
37 changes: 37 additions & 0 deletions src/components/task/kanban/TaskStatusContainer.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Draggable draggableId={draggableId} index={index}>
{(statusDragProvided) => (
<article
className="flex min-w-125 grow basis-1/3 flex-col"
ref={statusDragProvided.innerRef}
{...statusDragProvided.draggableProps}
>
<header className="flex items-center gap-4" {...statusDragProvided.dragHandleProps}>
<h2 className="select-none font-bold text-emphasis">{name}</h2>
<span>
<BsPencil className="cursor-pointer" />
</span>
</header>
<TaskItemList statusId={statusId} color={color} tasks={tasks} />
</article>
)}
</Draggable>
);
}
19 changes: 19 additions & 0 deletions src/constants/dnd.ts
Original file line number Diff line number Diff line change
@@ -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<DndDroppablePrefix> = 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 });
102 changes: 17 additions & 85 deletions src/pages/project/KanbanPage.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,24 @@
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];
}

function createChangedStatus(statusTasks: TaskWithStatus[], dropResult: DropResult) {
const { source, destination } = dropResult;

if (!destination) throw Error('Error: DnD destination is null');

const newStatusTasks = deepClone(statusTasks);
const stausTask = newStatusTasks[source.index];

newStatusTasks.splice(source.index, 1);
newStatusTasks.splice(destination.index, 0, stausTask);
newStatusTasks.forEach((status, index) => (status.order = index + 1));

return newStatusTasks;
}

Expand All @@ -40,15 +28,15 @@ 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;
const { tasks: destinationTasks } = isSameStatus
? { tasks: sourceTasks }
: (newStatusTasks.find((data) => data.statusId === destinationStatusId)! as TaskWithStatus);
const sourceTasks = newStatusTasks.find((data) => data.statusId === sourceStatusId)!.tasks;
const destinationTasks = isSameStatus
? sourceTasks
: newStatusTasks.find((data) => data.statusId === destinationStatusId)!.tasks;
const task = sourceTasks.find((data) => data.taskId === taskId)! as Task;

sourceTasks.splice(source.index, 1);
Expand All @@ -61,7 +49,6 @@ function createChangedTasks(statusTasks: TaskWithStatus[], dropResult: DropResul
}

// ToDo: DnD시 가시성을 위한 애니메이션 처리 추가할 것
// ToDo: 칸반보드 ItemList, Item 컴포넌트로 분리할 것
export default function KanbanPage() {
const [statusTasks, setStatusTasks] = useState<TaskWithStatus[]>(TASK_DUMMY);

Expand All @@ -85,71 +72,16 @@ export default function KanbanPage() {

return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="container" direction="horizontal" type={DND_TYPE.STATUS}>
<Droppable droppableId={DND_DROPPABLE_PREFIX.STATUS} type={DND_TYPE.STATUS} direction="horizontal">
{(statusDropProvided) => (
<section
className="flex grow gap-10 pt-10"
ref={statusDropProvided.innerRef}
{...statusDropProvided.droppableProps}
>
{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 (
<Draggable draggableId={draggableId} index={index} key={statusId}>
{(statusDragProvided) => (
<article
className="flex min-w-125 grow basis-1/3 flex-col"
ref={statusDragProvided.innerRef}
{...statusDragProvided.draggableProps}
>
<header className="flex items-center gap-4" {...statusDragProvided.dragHandleProps}>
<h2 className="font-bold text-emphasis">{name}</h2>
<span>
<BsPencil className="cursor-pointer" />
</span>
</header>
<Droppable droppableId={droppableId} type={DND_TYPE.TASK}>
{(taskDropProvided) => (
<article
style={{ borderColor: color }}
className="h-full w-full grow border-l-[3px] bg-scroll"
ref={taskDropProvided.innerRef}
{...taskDropProvided.droppableProps}
>
{tasks.map((task) => {
const { taskId, name, order } = task;
const draggableId = generatorPrefixId(taskId, 'task');
const index = order - 1;
return (
<Draggable key={taskId} draggableId={draggableId} index={index}>
{(dragProvided) => (
<div
className="m-5 flex h-30 items-center justify-start gap-5 rounded-sl bg-[#FEFEFE] p-5"
ref={dragProvided.innerRef}
{...dragProvided.draggableProps}
{...dragProvided.dragHandleProps}
>
<div style={{ borderColor: color }} className="h-8 w-8 rounded-full border" />
<div className="select-none overflow-hidden text-ellipsis text-nowrap">
{name}
</div>
</div>
)}
</Draggable>
);
})}
{taskDropProvided.placeholder}
</article>
)}
</Droppable>
</article>
)}
</Draggable>
);
})}
{statusTasks.map((statusTask) => (
<TaskStatusContainer key={statusTask.statusId} statusTask={statusTask} />
))}
{statusDropProvided.placeholder}
</section>
)}
Expand Down
1 change: 1 addition & 0 deletions src/types/TaskStatusType.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// ToDo: ERD에 맞춰 ProjectStatus로 변경할 것
// ToDo: API 설계 완료시 데이터 타입 변경할 것
export type TaskStatus = {
statusId: number;
Expand Down
File renamed without changes.
8 changes: 8 additions & 0 deletions src/utils/converter.ts
Original file line number Diff line number Diff line change
@@ -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];
}