Kanban Board
Drag & Drop
kanban card 추가, 수정, 삭제
kanban 기본틀
kanban의 기본구성은 'To do' 'In progress' 'Done' 'Notes & Reference' 4개의 list로 구성한다.
Add task 버튼으로 card를 생성하고, card는 수정과 삭제가 가능하며 list간 drag,drop으로 이동이 가능하게 구현한다.
Recoil 기본setting
https://recoiljs.org/ko/docs/basic-tutorial/atoms
Recoil 공식사이트에 간단한 todo list를 만들면서 기본적인 기능들을 써볼 수 있도록 코드가 상세하게 나와있다.
todo list에 기능을 더하여 kanban board를 구현하였다.
redux에 비하면 recoil은 말도안되게 간단하게 상태관리를 할 수 있었다.
key값과 default값만 설정해주면 간편하게 전역state 생성이 가능하며,
useState를 사용하는 것 처럼 useRecoilState로 state사용이 가능하다.
//recoil/index.tsx
import { atom } from 'recoil';
export const kanbanListState = atom<cardtype[]>({
key: 'kanbanState',
default: [],
});
const [list, setList] = useRecoilState(kanbanListState);
kanban 기본틀 구현
kanban에 입력한 데이터를 map으로 돌려서 list title name ('To do'등 4개의 category)과 category가 일치하는
데이터를 card component에 넣어서 UI에 출력되게 한다.
//App.tsx
const kanbanList = useRecoilValue(kanbanListState);
const titleName = [
{ id: 1, title: 'To do' },
{ id: 2, title: 'In progress' },
{ id: 3, title: 'Done' },
{ id: 4, title: 'Notes & Reference' },
];
const cardDataHandler = (cardTitle: string) => {
return kanbanList
.filter((data) => data.category === cardTitle)
.map((item, index) => <Card key={item.id} item={item} />);
};
return (
<>
<header>
<span className="title">KANBAN BOARD</span>
</header>
<section className="kanbanListContainer">
{titleName.map((data: any) => (
<KanbanList title={`${data.title}`}>
{cardDataHandler(data.title)}
</KanbanList>
))}
</section>
</>
);
}
KanbanList
: 총4개의 list 생성
export default function KanbanList({ title, children }) {
return (
<>
<div className="kanbanListWrap" ref={drop}>
<div className="kanbanTitle">{title}</div>
{children}
<KanbanCreator title={title} />
</div>
</>
);
}
KanbanCreator component
: Add card button을 클릭해서 새로운 카드를 생성
export default function KanbanCreator({ title }: { title: string }) {
const [kanbanList, setKanbanList] = useRecoilState(kanbanListState);
const getId: number =
kanbanList.length > 0 ? kanbanList[kanbanList.length - 1].id + 1 : 0;
const addCard = useCallback(
(e) => {
setKanbanList((prev) => [
...prev,
{
id: getId,
title: '',
content: '',
category: title,
},
]);
},
[getId, setKanbanList, title]
);
return (
<div className="addBtnWrap">
<button className="cardAddBtn" onClick={addCard}>
+ Add task
</button>
</div>
);
}
Card component
: edit(title, content) , delete 기능
content의 textarea에 text양에따라 height 동적으로 커지게 handleResizeHeight 함수적용
function Card({ item }: { item: cardtype }) {
const [list, setList] = useRecoilState(kanbanListState);
const index = list.findIndex((data) => data === item);
const ref = useRef<HTMLTextAreaElement>(null);
const replaceIndex = (list: cardtype[], index: number, data: cardtype) => {
return [...list.slice(0, index), data, ...list.slice(index + 1)];
};
const editTitle = (e: React.ChangeEvent<HTMLInputElement>) => {
const newList = replaceIndex(list, index, {
...item,
title: e.target.value,
});
setList(newList);
};
const editText = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newList = replaceIndex(list, index, {
...item,
content: e.target.value,
});
setList(newList);
};
const handleResizeHeight = useCallback(() => {
if (ref === null || ref.current === null) {
return;
}
ref.current.style.height = '70px';
ref.current.style.height = ref.current.scrollHeight + 'px';
}, []);
const deleteItem = () => {
setList([...list.slice(0, index), ...list.slice(index + 1)]);
};
return (
<div
className="cardWrap"
>
<div className="cardHeaderWrap">
<span
className="cardTitleBadge"
style={{ backgroundColor: badgeColor }}
>
{item.category}
</span>
<img
className="deleteimg"
src="images/cancel.png"
alt="delete"
onClick={deleteItem}
/>
</div>
<input
className="cardTitle"
type="text"
value={item.title}
onChange={editTitle}
placeholder="제목을 입력하세요"
/>
<textarea
className="cardContent"
value={item.content}
onChange={editText}
onInput={handleResizeHeight}
ref={ref}
placeholder="내용을 입력하세요"
spellCheck="false"
/>
</div>
);
}
React DnD - Drag
yarn add react-dnd react-dnd-html5-backend
// OR
npm install react-dnd react-dnd-html5-backend
Drag하고 Drop할 컴포넌트 상위를
<DndProvider backend={HTML5Backend}> 로 감싸준다.
function App() {
return (
<>
...
<DndProvider backend={HTML5Backend}>
{titleName.map((data: any) => (
<KanbanList title={`${data.title}`}>
{cardDataHandler(data.title)}
</KanbanList>
))}
</DndProvider>
...
</>
);
}
Drag할 Card component에 useDrag를 추가한다.
Ref로 카드를 지정하고 drag가 완료되었을 때 drop된 list의 카테고리로 drag한 카드의 category state를 변경시켜준다.
//드래그가 완료되면 drag된 카드가 drop한 card의 카테고리로 변경되는 함수.
const changeItemCategory = (selectedItem: cardtype, title: string) => {
setList((prev) => {
return prev.map((e) => {
return {
...e,
category: e.id === selectedItem.id ? title : e.category,
};
});
});
};
const [{ isDragging }, dragRef] = useDrag(() => ({
//isDragging : card가 드래깅중일때 true, 아닐때 false값을 리턴한다.
//dragRef : 드래그될 부분에 ref 적용해준다
type: 'card',
item: item,
//드래그될 card에 넣어질 정보
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
// 현재 드래깅중인지 리턴
end: (item: cardtype, monitor) => {
//드래그가 끝났을때 작동할 코드
const dropResult: drop | null = monitor.getDropResult();
if (dropResult) {
switch (dropResult.name) {
case TO_DO:
changeItemCategory(item, TO_DO);
break;
case IN_PROGRESS:
changeItemCategory(item, IN_PROGRESS);
break;
case DONE:
changeItemCategory(item, DONE);
break;
case NOTE:
changeItemCategory(item, NOTE);
break;
}
}
},
}));
return (
<div
className="cardWrap"
//Ref지정
ref={dragRef}
//드래그중일 때 card의 투명도를 30%로 한다.
style={{ opacity: isDragging ? '0.3' : '1' }}
>
React DnD - Drop
Drop되는 kanbanList에 useDrop을 추가한다.
위에 Card component에 추가해준 useDrag의 type과 useDrop의 accept가 같아야한다.
const [{ canDrop, isOver }, drop] = useDrop({
accept: 'card',
// useDrag의 type과 같아야한다.
drop: () => ({ name: title }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
//drag진행동안 canDrop : true
//drop할 영역 접근시 isOver : true
}),
});
return (
<>
<div className="kanbanListWrap" ref={drop}>
<div className="kanbanTitle">{title}</div>
{children}
<KanbanCreator title={title} />
</div>
</>
);
}
코드 & 참고사이트
https://github.com/manonkim/kanban-Board
https://ichi.pro/ko/yeje-ui-react-dnd-226845595632541
'Archive' 카테고리의 다른 글
[TIL220407] Tailwind CSS (0) | 2022.04.07 |
---|---|
[Next.JS] NextJS 기초 (0) | 2022.04.02 |
[Frontend] 프론트엔드 주니어 개발자 면접 질문 (기술/인성) (0) | 2022.03.18 |
[React] Kakao Map - 폴리곤 (카카오맵) (0) | 2022.03.14 |
[TIL220301] CORS (0) | 2022.03.02 |