-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
* Config: #33 react-beautiful-dnd를 계승한 @hello-pangea/dnd 설치 * Chore: #33 mock 데이터 추가 및 관련 Types 추가 정의 * Config: #33 ESLint 설정 완화 * Feat: #33 칸반 보드 UI 작성 & 칸반 보드 할일 드래그 앤 드롭 기능 추가 * UI: #33 ProjectLayout 수정 * Formmating: #33 deepClone의 매개변수명 변경
- Loading branch information
Showing
10 changed files
with
424 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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', | ||
}, | ||
], | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[] }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.