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

Feat: #33 특정 프로젝트 칸반 보드 UI 작성 & 할일 목록 DnD 기능 추가 #36

Merged
merged 6 commits into from
Jul 3, 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
3 changes: 3 additions & 0 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ module.exports = {
'consistent-return': 'off',
'object-curly-newline': 'off',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-shadow': 'warn',
'no-param-reassign': 'warn',
'no-return-assign': 'warn',
'no-unused-vars': 'warn',
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"prepare": "husky"
},
"dependencies": {
"@hello-pangea/dnd": "^16.6.0",
"@tanstack/react-query": "^5.36.2",
"axios": "^1.6.8",
"react": "^18.2.0",
Expand Down
3 changes: 2 additions & 1 deletion src/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
/* ========= Scrollbar Custom ========= */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
border: 1px solid var(--border-scroll);
Expand All @@ -93,6 +94,6 @@

@layer components {
.selected::before {
@apply absolute left-0 top-0 block h-30 w-4 bg-main content-[''];
@apply absolute left-0 top-0 block h-30 w-3 bg-main content-[''];
}
}
10 changes: 5 additions & 5 deletions src/layouts/page/ProjectLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ export default function ProjectLayout() {
<RiSettings5Fill /> Project Setting
</div>
</header>
<div className="grow p-10">
<div className="flex justify-between">
<ul className="flex border-b *:mr-15">
<li>
<div className="flex grow flex-col overflow-auto p-10">
<div className="flex items-center justify-between border-b">
<ul className="*:mr-15">
<li className="inline">
<NavLink to="calendar" className={({ isActive }) => (isActive ? 'text-main' : 'text-emphasis')}>
Calendar
</NavLink>
</li>
<li>
<li className="inline">
<NavLink to="kanban" className={({ isActive }) => (isActive ? 'text-main' : 'text-emphasis')}>
Kanban
</NavLink>
Expand Down
143 changes: 143 additions & 0 deletions src/mocks/mockData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import type { TodoStatus } from '@/types/TodoStatusType';
import type { TodoWithStatus } from '@/types/TodoType';

export const USER_DUMMY = [
{
userId: 1,
email: '[email protected]',
nickname: '꾸르',
bio: '풀스택 개발자를 목표중',
},
{
userId: 2,
email: '[email protected]',
nickname: '무드메이커',
bio: '디자이너 + 프론트엔드 육각형 인재',
},
{
userId: 3,
email: '[email protected]',
nickname: 'SOL천사',
bio: '프론트엔드 취준생',
},
];

export const STATUS_DUMMY: TodoStatus[] = [
{
statusId: 1,
name: 'To Do',
color: '#c83c00',
order: 1,
},
{
statusId: 2,
name: 'In Progress',
color: '#dab700',
order: 2,
},
{
statusId: 3,
name: 'Done',
color: '#237700',
order: 3,
},
];

export const TODO_DUMMY: TodoWithStatus[] = [
{
statusId: 1,
name: 'To Do',
color: '#c83c00',
order: 1,
tasks: [
{
taskId: 7,
name: '할일 추가 모달 구현하기',
order: 1,
userId: 3,
files: [],
startDate: '2024-06-26',
endDate: '2024-07-02',
},
{
taskId: 8,
name: 'ID 찾기 페이지 작성하기',
order: 2,
userId: 3,
files: [],
startDate: '2024-07-03',
endDate: '2024-07-05',
},
{
taskId: 9,
name: 'DnD 구현하기',
order: 3,
userId: 1,
files: [],
startDate: '2024-06-30',
endDate: '2024-07-02',
},
],
},
{
statusId: 2,
name: 'In Progress',
color: '#dab700',
order: 2,
tasks: [
{
taskId: 5,
name: 'DnD 기술 조사하기',
order: 1,
userId: 1,
files: [],
startDate: '2024-06-27',
endDate: '2024-06-29',
},
{
taskId: 4,
name: 'API 명세서 작성하기',
order: 2,
userId: 2,
files: [],
startDate: '2024-06-27',
endDate: '2024-06-29',
},
],
},
{
statusId: 3,
name: 'Done',
color: '#237700',
order: 3,
tasks: [
{
taskId: 1,
name: 'todo 상태 추가 모달 작업하기',
order: 1,
userId: 2,
files: [],
startDate: '2024-06-22',
endDate: '2024-06-26',
},
{
taskId: 2,
name: 'project layout 작성하기',
order: 2,
userId: 1,
files: [],
startDate: '2024-06-18',
endDate: '2024-06-21',
},
{
taskId: 3,
name: 'tailwindcss 설정하기',
order: 3,
userId: 3,
files: [],
startDate: '2024-06-14',
endDate: '2024-06-18',
},
],
},
];
142 changes: 141 additions & 1 deletion src/pages/project/KanbanPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,143 @@
import { useState } from 'react';
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import { BsPencil } from 'react-icons/bs';
import deepClone from '@utils/deepClone';

import { TODO_DUMMY } from '@mocks/mockData';
import type { DropResult } from '@hello-pangea/dnd';
import type { Todo, TodoWithStatus } from '@/types/TodoType';

// ToDo: 유틸리티로 분리할지 고려하기
function generatorPrefixId(id: number | string, prefix: string, delimiter: string = '-') {
const result = prefix + delimiter + id;
return result;
}
function parserPrefixId(prefixId: string, delimiter: string = '-') {
const result = prefixId.split(delimiter);
return result[result.length - 1];
}

function createChangedTodoForSameStatus(todo: TodoWithStatus[], dropResult: DropResult) {
const { source, draggableId } = dropResult;

const newTodo = deepClone(todo);
const sourceStatusId = Number(parserPrefixId(source.droppableId));
const taskId = Number(parserPrefixId(draggableId));

const { tasks: sourceTasks } = newTodo.find((data) => data.statusId === sourceStatusId)! as TodoWithStatus;
const task = sourceTasks.find((data) => data.taskId === taskId)! as Todo;

sourceTasks.splice(source.index, 1);
sourceTasks.splice(source.index, 0, task);
sourceTasks.forEach((task, index) => (task.order = index + 1));

return newTodo;
}

function createChangedTodoForOtherStatus(todo: TodoWithStatus[], dropResult: DropResult) {
const { source, destination, draggableId } = dropResult;

// ToDo: 메세지 포맷 정하고 수정하기
if (!destination) throw Error('Error: DnD destination is null');

const newTodo = deepClone(todo);
const sourceStatusId = Number(parserPrefixId(source.droppableId));
const destinationStatusId = Number(parserPrefixId(destination.droppableId));
const taskId = Number(parserPrefixId(draggableId));

const { tasks: sourceTasks } = newTodo.find((data) => data.statusId === sourceStatusId)! as TodoWithStatus;
const { tasks: destinationTasks } = newTodo.find((data) => data.statusId === destinationStatusId)! as TodoWithStatus;
const task = sourceTasks.find((data) => data.taskId === taskId)! as Todo;

sourceTasks.splice(source.index, 1);
destinationTasks.splice(destination.index, 0, task);

sourceTasks.forEach((task, index) => {
task.order = index + 1;
});
destinationTasks.forEach((task, index) => {
task.order = index + 1;
});

return newTodo;
}

// ToDo: 할일 상태 Vertical DnD 추가할 것
// ToDo: DnD시 가시성을 위한 애니메이션 처리 추가할 것
// ToDo: 칸반보드 ItemList, Item 컴포넌트로 분리할 것
export default function KanbanPage() {
return <div>KanbanPage</div>;
const [todo, setTodo] = useState<TodoWithStatus[]>(TODO_DUMMY);

const handleDragEnd = (dropResult: DropResult) => {
const { source, destination } = dropResult;

if (!destination) return;
if (source.droppableId === destination.droppableId && source.index === destination.index) return;

const newTodo =
source.droppableId !== destination.droppableId
? createChangedTodoForOtherStatus(todo, dropResult)
: createChangedTodoForSameStatus(todo, dropResult);

setTodo(newTodo);
};

return (
<section className="flex grow gap-10 pt-10">
<DragDropContext onDragEnd={handleDragEnd}>
{todo.map((data) => {
const { statusId, name, color, tasks } = data;
const droppableId = generatorPrefixId(statusId, 'status');
return (
<article className="flex min-w-125 grow basis-1/3 flex-col" key={statusId}>
<header className="flex items-center gap-4">
<h2 className="font-bold text-emphasis">{name}</h2>
<span>
<BsPencil className="cursor-pointer" />
</span>
</header>
<div className="grow">
<Droppable droppableId={droppableId} type="TODO">
{(dropProvided) => {
return (
<article
style={{ borderColor: color }}
className="inline-block min-h-full w-full border-l-[3px] bg-scroll"
ref={dropProvided.innerRef}
{...dropProvided.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) => {
return (
<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>
);
})}
{dropProvided.placeholder}
</article>
);
}}
</Droppable>
</div>
</article>
);
})}
</DragDropContext>
</section>
);
}
1 change: 1 addition & 0 deletions src/types/TodoStatusType.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type TodoStatus = {
statusId: number;
name: string;
color: string;
order: number;
};

export type TodoStatusForm = {
Expand Down
14 changes: 14 additions & 0 deletions src/types/TodoType.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { TodoStatus } from './TodoStatusType';

// ToDo: API 설계 완료시 데이터 타입 변경할 것
export type Todo = {
taskId: number;
name: string;
order: number;
userId: number;
files: string[];
startDate: string;
endDate: string;
};

export type TodoWithStatus = TodoStatus & { tasks: Todo[] };
22 changes: 22 additions & 0 deletions src/utils/deepClone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default function deepClone<T>(data: T): T {
if (data === undefined || data === null) return data;

if (typeof data !== 'object') return data;

if (data instanceof Date) return new Date(data.getTime()) as T;

if (Array.isArray(data)) {
const arrCopy = [] as unknown[];
data.forEach((item) => arrCopy.push(deepClone(item)));
return arrCopy as T;
}

const objCopy = {} as { [key: string]: unknown };
Object.keys(data).forEach((key) => {
if (Object.prototype.hasOwnProperty.call(data, key)) {
objCopy[key] = deepClone((data as { [key: string]: unknown })[key]);
}
});

return objCopy as T;
}
Loading