dnd 기능
1) 날짜를 선택한다.
2) 입력창에 적고 버튼을 누르면 등록 된다.
3) dnd 기능을 사용해 todo에서 done 으로 옮길수 있음.
4) 등록하는 "할일"의 데이터에 상태값 정하기 (todo, done)
5) 다른 날짜 선택시 , 해당 날짜에 쓴 todo 의 데이터가 불러와져야 함.
6) 페이지를 reload 해도 , done으로 옮긴 할일이 그대로 남아있어야 함.
🎈 임의 데이터로 todolist 의 모습을 간략하게 구현해 보았다.
-> 대략 이런식으로 구현하려고 한다.
<div className="dateWrap">
<div className="dateBox">
<DatePicker
value={Todolist}
dateFormat="yyyy-MM-dd"
selected={Todolist.date}
onChange={(date) => setTodoList({
...date,
'date': date
})}
customInput={<DatePick/>}
/>
</div>
</div>
1) 처음 날짜를 선택하면 선택한 날짜로 바뀌게 된다.
date값을 복사하고 추가한 "date" 의 값을 setTodoList 에 저장.
const TodoListForm = () => {
const [ViewTodo, setViewTodo] = useState([]);
const [Todolist, setTodoList] = useState({
content:""
,date:""
,status: "todo"
});
const { content } = Todolist;
💛const getChangeTodo = (e) => {
const{name, value} = e.target;
console.log(name,value);
setTodoList({
...Todolist,
[name]: value
})
};
🧡const onClickTodo = () => {
axios.post(API_URL+'/todo', {
title: "test"
,content: Todolist.content
,date: Todolist.date
,status:"todo"
💚 }).then((response) => {
alert('등록 되었습니다.😊');
💙 setTodoList({
title: "test"
,content:""
,date:new Date()
,status:"todo"
})
Todo();
});
};
return(
<TodoInputWrap>
<p>Todo List</p>
<input
type="text"
placeholder="할 일을 추가해주세요"
onChange={getChangeTodo}
name='content'
value={content}
/>
<FontAwesomeIcon
className="faPlus"
icon={faPlus}
onClick={onClickTodo}
/>
</TodoInputWrap>
)
}
💛input 입력창에 입력 onChange={getChangeTodo} 함수를 만들어
새로운 입력 값을 받게 끔 구현 하였다.
🧡 + 아이콘의 버튼을 누르면 onClick 이벤트에 만든 {onClickTodo} 함수가 실행된다 .
서버에게 현재 Todolist에 입력된 데이터들을 post 방식으로 보내주었다.
그리고 "해야할"todo 와 "완료"된 done 을 drag-and-drop 으로 나눌수 있도록 status 상태값도 넣어 주었다. 등록후 기본값 "todo".
,content: Todolist.content
,date: Todolist.date
,status:"todo"
💚 서버에 성공적으로 보내지면, .then((response) => {
alert('등록 되었습니다.😊');
alert 창으로 알림이 뜬다.
💙 알림창이 뜬후 ,
setTodoList({
title: "test"
,content:""
,date:new Date()
,status:"todo"
})
Todo();
});
변경된 값을 setTodoList에 저장하고 Todo 함수를 호출 한다.
(Todo 는 아래에서 설명할 예정!)
위에 써진 코드를 응용해서 적용 시켜 보았다.
하나하나 쓰면서 이해한 내용을 바탕으로 작성한다.
<코드펜에 작성된 예시>
-> codepen에 작성된 코드는 임시로 데이터 값을 적고 ,
상태값을 적어두었다.
-> 임시 상태의 값을 State값 안에 저장해 두었다.
그리고, 임시 상태를 저장해둔 taskStatus 값을 useState 에다 넣어두고
새로운 state 값을 지정해 두었다.
const [taskStatus, ] = useState({
Todo: {
name: "todo",
items: []
},
Done: {
name: "done",
items: []
}
});
const [columns, setColumns] = useState(taskStatus);
-> < DragDropContext >로 감싸준후 이부분에 onDragEnd의 함수를 만들어 주었다.
const onDragEnd = (result, columns, setColumns) => {
💛if (!result.destination) return;
const { source, destination } = result;
🧡if (source.droppableId !== destination.droppableId) {
console.log(result);
💚const sourceColumn = columns[source.droppableId];
const destColumn = columns[destination.droppableId];
const sourceItems = [...sourceColumn.items];
const destItems = [...destColumn.items];
💙const [removed] = sourceItems.splice(source.index, 1);
destItems.splice(destination.index, 0, removed);
💜 setColumns({
...columns,
[source.droppableId]: {
...sourceColumn,
items: sourceItems
},
[destination.droppableId]: {
...destColumn,
items: destItems
},
});
} else { //순서만 변경되었을때
🤎 const column = columns[source.droppableId];
const copiedItems = [...column.items];
🖤const [removed] = copiedItems.splice(source.index, 1);
copiedItems.splice(destination.index, 0, removed);
setColumns({
...columns,
[source.droppableId]: {
...column,
items: copiedItems
}
});
}
return(
<DragDropContext
onDragEnd={(result) => onDragEnd(result, columns,setColumns)}>
</DragDropContext>
)
💛 onDragEnd 에 (result, columns, setColumns) 파라미터 값으로 전달되고 ,
만약 옮기려는 목적지가 일치 하지 않으면 return;
result라는 변수에 { source, destination } 두개의 값을 담는다.
🧡(source.droppableId !== destination.droppableId)
만약 source에 있는 Id 와, 옮길 Id 가 일치하지 않을시 아래의 로직이 실행되게 된다. (맨 처음에는 일치 하지 않음)
💚 임의로 구현한 UI를 확인해 보면 , todo 와 done 2개의
columns 와 , 안에 들어갈 todo , done 2개의 item 이 들어가게 된다. 따로 , 변수를 만들어 안에 이 부분들의 값을 넣는다.
<todo , done 의 colum>
/Todo/ const sourceColumn = columns[source.droppableId];
/Done/ const destColumn = columns[destination.droppableId];
<todo, done에 들어갈 content 내용>
/Todo/ const sourceItems = [ ...sourceColumn.items ];
/Done/ const destItems = [ ...destColumn.items ];
-> sourceItems 에 sourceColumn.items 을 ... 복사해서 담아두 었다.
💙 sourceItems.splice(source.index, 1);
-> todo에 등록된 items 에 대해 하나씩 새로운 배열을 만들고
배열 index부터 1개의 요소.
destItems.splice(destination.index, 0, removed);
-> done에 있는 items 값 에 대해 하나씩 새로운 배열을 만들고
0번째 위치(index) 에 remove 된 item 이 놓이게 된다.
💜 옮겨진후 setColumns 로 데이터들이 저장된다.
...columns,
...sourceColumn,
items: sourceItems
},
-> 복사된 columns 에 아래와 같은 추가 데이터 들을 넣는다.
...sourceColumn, 복사한 sourceColumn 에
추가된 items 를 [source.droppableId] 에 넣어두고,
...destColumn,
items: destItems
},
-> ...destColumn, 복사한 destColumn 에
추가된 items를 [destination.droppableId] 에 넣어 두었다.
🤎일치 하지 않고 droppableId 값이 같을시 else의 로직이 진행된다.
(같은 column 안에 item이 있을시)
const column = columns[source.droppableId];
-> columns의 source.droppableId 를 column 변수에 저장한다.
const copiedItems = [ ...column.items ];
-> 위에 저장해 두었던 column items 값을 복사하여 copiedItems 변수에 저장 한다.
🖤 위와 같이 [removed] 에
copiedItems.splice(source.index, 1);
-> copiedItems 값을 한개씩 새로운 배열로 만들어 배열 index부터 1개 의 요소 이다.
copiedItems.splice(destination.index, 0, removed);
-> copiedItems 값 에 대해 하나씩 새로운 배열을 만들고
0번째 위치(index) 에 remove 된 item 이 놓이게 된다.
1) id 값 일치하지 않을때
2) id 값 일치 할때
-> 위에 쓴 로직의 기능들을 가지고 return( ) 아래의 UI를 그려주는 로직
return(
<TodoListAllWrap>
<DragDropContext
onDragEnd={(result) => onDragEnd(result,columns,setColumns)}>
💛{Object.entries(columns).map(([columnId, column]) => {
return (
🧡<TodoWrap key={columnId}>
<H2>{columnId}</H2>
💚 <div className="ScorllBox" style={{ margin: 8}}>
<Droppable droppableId={columnId} key={columnId}>
{(provided, snapshot) => {
return (
<TodoListBox
{...provided.droppableProps}
ref={provided.innerRef}
style={{
background: snapshot.isDraggingOver
? "red"
: "yellow",
}}>
💙{column.items.map((item, index) => {
return(
<Draggable
key={item._id}
draggableId={item._id}
index={index}
>
{(provided, snapshot) => {
return (
<TodooContainer
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
backgroundColor: snapshot.isDragging
? "pink"
: "white",
...provided.draggableProps.style
}}
>
💜 <TodoInnerText>
{item.content}
<IconBox>
<FontAwesomeIcon className="XIcon" icon={faXmark}
onClick={(e) => RemovetodoList(item._id, e)}
/>
</IconBox>
</TodoInnerText>
</TodooContainer>
);
}}
</Draggable>
);
})}
{provided.placeholder}
</TodoListBox>
);
}}
</Droppable>
</div>
</TodoWrap>
);
})}
</DragDropContext>
</TodoListAllWrap>
)
💛Object.entries 는
주어진 개체 자체의 열거 가능한 문자열 키 속성 키-값 쌍의 배열을 반환
하는 자바스크립트 메서드 이다.
(columns)의 키-값 을 반환하고 map을 사용하여,
map 안에 columnId, column 배열을 순환하며 처리한다.
🧡 todo, done 전체의 UI 를 으로 정해 주었고 이 안에
key값으로 {columnId} 넣어 주었다.
그후 , columnId 값을 아래 와 같이 나타내 주었다.
< H2>{columnId}</ H2>
💚 className="ScorllBox" 박스 안에
Droppable 을 넣고 여기에 droppableId 와 , key값에 {columnId} 를 지정해주고 , drag and drop 의 에서 쓰이는
{(provided, snapshot) 의 의미는
provided -> provided.innerRef를 참조하여 동작을 실행하는 매개변수 이기에 반드시 들어가야하는 사항이며,
snapshot -> 동작시 dom 이벤트에 대하여 적용될 style 참조를 뜻한다.(선택적항목이다.)
{...provided.droppableProps} , provided 복사후
style 을 주었다.
드래그할 아이템이 위에 over될때 ? -> 되는중일때 의 색상
: -> 아닐때의 원래 색상을 넣어 주었다.
구현 예시)
💙{column.items.map((item, index)
column 에 있는 items 를 map 을 사용하여 (item, index) 로 반환하여,
에 key, draggableId, index 에 값을 넣어주 었다.
아이템 또한
(provided, snapshot) 을 사용하여, 동작 실행의 매개 변수랑
style 을 주었다.
? -> 되는중일때 의 색상
: -> 아닐때 의 색상
💜 안에
{item.content} -> content를 보이게 해놨고
안에 x아이콘 을 넣어 두었다.
설명: 데이데이터를 추가 , 등록후 바뀐 데이터의 값으로 보통 SetState 값을 호출한다. 하지만, 지금은 등록하는 데이터의 상태값을 저장해두고(todo, done) 저장된
값을 호출해야 한다. 따라 상태값이 todo인경우 todo의 column 에 내용이 보여야 하고 done 인경우 done의 column 에 내용이 보여야 한다.
따라, 상태값을 반영해서 출력하는 함수 Todo를 구현 하였다.
💛const Todo = (date) => {
let now = new Date();
if(date != null){
// 값이 전달받은 시간으로 조회
now = new Date(date);
};
🧡let year = now.getFullYear();
let month = now.getMonth()+1;
let day = now.getDate();
axios.get(API_URL+ '/todo?year='+year+'&month='+month+'&day='+day)
💚.then((response) => {
let todoInfo = [];
let doneInfo = [];
//상태 값에 따라서 데이터를 나눔
for(let i = 0; i<response.data.data.length;i++){
console.log(response.data.data[i].status);
if(response.data.data[i].status === "todo"){
todoInfo.push(response.data.data[i]);
}else{
doneInfo.push(response.data.data[i]);
}
}
💙setColumns({
...taskStatus,
Todo: {
name: "Todo",
items: todoInfo
},
Done: {
name: "Done",
items: doneInfo
}
});
})
};
💛 변수 now에 new Date(); 를 할당하고
파라미터 값으로 date 가 들어 오고 난후 ,
date가 null 값이 아닐시 now 변수에 new Date에 현재 (date)값을
넣어준다.
🧡 todo 에 (년도 , 월, 일) 3가지의 시간이 나타나야 되기 때문에
year에 변수에는 now.getFullYear(); -> 현지 시간 기준 연도
month 에는 now.getMonth()+1; -> 현지 시간 기준 월
day 에는 now.getDate(); -> 현지 시간 기준 일
그리고 받아온 data와 함께 + 년도, 월, 일 의 데이터를 가져왔다.
-> Month 에는 시간적 차이가 있어서 +1 달을 더 해주어 날짜를 맞추었다.
💚 서버에서 받아온 data에 서
todo와 done 상태값에 따라 데이터를 나눠주고 새로 그려줬다.
처음엔 어떻게 나눠야 할지 고민을 많이 했다.
제일 기본적인 if문으로 상태값을 나누고, 정해진 조건에 맞을시 push로 빈 배열에 넣어주는 쪽으로 구현 방법을 생각 하였다.
let todoInfo = [ ]; 두개의 변수에 비어 있는 배열의 값을 기본값으로 해주고,
let doneInfo = [ ];
let i=0 부터 , i보다 data.length 가 클때 아래의 로직을 순회하고 난후 i++값 증가. 만약, data[i].status === "todo" , data[i]의 상태 값이 === "todo"일때, todoInfo의 배열에 (response.data.data[i]) 값을 push 한다.
else 아닐시 , doneInfo 의 배열에 push 된다.
💙 상태값에 따라 data를 push 후 ,
setColumn에 아래 임시 상태값 items 에 위에 값을 저장해 주었다.
Todo: {
name: "Todo",
items: todoInfo
},
todo의 items 값에 todoInfo의 값 나머지는 doneInfo 를 넣어 주었다.
따라 Todo의 함수가 호출 될때마다.
서버에 저장된 데이터들을 가져오되, 저장해둔 상태 값에 나뉘어 UI가 보인다.
->x의 아이콘을 누르면 삭제 이벤트 함수가 실행된다
const RemovetodoList = (idx) => {
if(window.confirm("삭제하시겠습니까?")){
axios.delete(API_URL +'/todo/' + idx,{
}).then((response) => {
if(response.data.message === "successful"){
alert('삭제 되었습니다😉');
Todo()
} else {
console.error(response.data.message);
}
});
} else{
alert("취소 되었습니다.")
}
};
💛 삭제 아이콘의 클릭 이벤트 함수가 호출 되면,
선택한 내용의 idx 값을 파라미터로 받온다.
만약 confirm 창에서 삭제하시겠습니까의 알람이 뜬후, 확인을 누르면
해당 idx의 게시글이 서버로 넘어간후 삭제가 완료 되었다는 "successful" 의 응답이 오면, alet 창으로 삭제 되었습니다 가 뜬다.
-> todo에 있는 데이터, 현재 시간과 todo의 데이터를 useEffect을 통해 가져옴
useEffect(()=> {
setTodoList({
...Todolist,
date:new Date(),
});
Todo();
},[]);
import { useState, forwardRef, useEffect } from 'react';
import DatePicker from "react-datepicker";
import axios from 'axios';
import { API_URL } from '../../../../Common/Common';
import styled from "styled-components";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark, faPlus} from "@fortawesome/free-solid-svg-icons";
import { DragDropContext, Draggable, Droppable } from "react-beautiful-dnd";
import { DateStyle} from '../../../../styles/Common/CommonStyle';
import { BoxSize,InputWrapStyle, InnerTextStyle, IconStyle } from '../../../../styles/DetailStyle/ListStyle/common/common';
import {TodoListAllWrap, TodoWrap, TodoListBox, H2, TodooContainer} from '../../../../styles/DetailStyle/ListStyle/todo';
import Api from '../../../../apis/Api';
const TodoListForm = () => {
const [ ,setBtnStatus] = useState(false);
const [ViewTodo, setViewTodo] = useState([]);
const [Todolist, setTodoList] = useState({
content:""
,date:""
,status: "todo"
});
const { content } = Todolist;
useEffect(()=> {
setColumns({
...columns,
'Todo': {
items: ViewTodo
},
});
},[ViewTodo]);
useEffect(()=> {
setTodoList({
...Todolist,
date:new Date(),
});
Todo();
},[]);
const DatePick = forwardRef(({ value, onClick }, ref) => (
<DateButton className='custom-btn' onClick={onClick} ref={ref}> {value}
</DateButton>
));
const getChangeTodo = (e) => {
const{name, value} = e.target;
console.log(name,value);
setTodoList({
...Todolist,
[name]: value
})
};
const Todo = (date) => {
let now = new Date();
if(date != null){
// 값이 전달받은 시간으로 조회
now = new Date(date);
};
let year = now.getFullYear();
let month = now.getMonth()+1;
let day = now.getDate();
axios.get(API_URL+ '/todo?year='+year+'&month='+month+'&day='+day)
.then((response) => {
let todoInfo = [];
let doneInfo = [];
//상태 값에 따라서 데이터를 나눔
for(let i = 0; i<response.data.data.length;i++){
console.log(response.data.data[i].status);
if(response.data.data[i].status === "todo"){
todoInfo.push(response.data.data[i]);
}else{
doneInfo.push(response.data.data[i]);
}
}
setColumns({
...taskStatus,
Todo: {
name: "Todo",
items: todoInfo
},
Done: {
name: "Done",
items: doneInfo
}
});
})
};
const RemovetodoList = (idx) => {
if(window.confirm("삭제하시겠습니까?")){
axios.delete(API_URL +'/todo/' + idx,{
}).then((response) => {
console.log(response);
if(response.data.message === "successful"){
alert('삭제 되었습니다😉');
Todo();
// setBtnStatus(false);
} else {
console.error(response.data.message);
}
});
} else{
alert("취소 되었습니다.")
}
};
const DatePickChange = (date) => {
console.log(date);
setTodoList({
...date,
'date': date
})
Todo(date)
};
const ChangeStatus = (id, status) => {
axios.patch(API_URL+ '/todo/' + id,{
status : status
}).then((response) => {
Todo()
//Todo(Todolist.date)
});
};
const onClickTodo = () => {
axios.post(API_URL+'/todo', {
content: Todolist.content
,date: Todolist.date
,status:"todo"
}).then((response) => {
alert('등록 되었습니다.😊');
setTodoList({
content:""
,date:new Date()
,status:"todo"
})
Todo();
});
};
/**임시로 상태값 설정**/
const [taskStatus, ] = useState({
Todo: {
name: "todo",
items: []
},
Done: {
name: "done",
items: []
}
});
const [columns, setColumns] = useState(taskStatus);
const onDragEnd = (result, columns, setColumns) => {
if (!result.destination) return;
const { source, destination } = result;
//상태값이 변경되었을때
if (source.droppableId !== destination.droppableId) {
console.log(result);
const sourceColumn = columns[source.droppableId];
const destColumn = columns[destination.droppableId];
const sourceItems = [...sourceColumn.items];
const destItems = [...destColumn.items];
const [removed] =
sourceItems.splice(source.index, 1);
destItems.splice(destination.index, 0, removed);
setColumns({
...columns,
[source.droppableId]: {
...sourceColumn,
items: sourceItems
},
[destination.droppableId]: {
...destColumn,
items: destItems
},
});
ChangeStatus(result.draggableId, destination.droppableId.toLowerCase());
} else { //순서만 변경되었을때
const column = columns[source.droppableId];
const copiedItems = [...column.items];
const [removed] = copiedItems.splice(source.index, 1);
copiedItems.splice(destination.index, 0, removed);
setColumns({
...columns,
[source.droppableId]: {
...column,
items: copiedItems
}
});
}
}
return (
<>
<TodoAllContainer>
<div className="dateWrap">
<div className="dateBox">
<DatePicker
value={Todolist}
dateFormat="yyyy-MM-dd"
selected={Todolist.date}
onChange={(date) => DatePickChange(date)}
customInput={<DatePick/>}
/>
</div>
</div>
<TodoInputWrap>
<p>Todo List</p>
<input
type="text"
placeholder="할 일을 추가해주세요"
onChange={getChangeTodo}
name='content'
value={content}
/>
<FontAwesomeIcon
className="faPlus"
icon={faPlus}
onClick={onClickTodo}
/>
</TodoInputWrap>
<TodoListAllWrap>
<DragDropContext
onDragEnd={(result) => onDragEnd(result, columns, setColumns)}
>
{Object.entries(columns).map(([columnId, column]) => {
return (
<TodoWrap key={columnId}
>
<H2>{columnId}</H2>
<div className="ScorllBox" style={{ margin: 8}}>
<Droppable droppableId={columnId} key={columnId}>
{(provided, snapshot) => {
return (
<TodoListBox
{...provided.droppableProps}
ref={provided.innerRef}
style={{
background: snapshot.isDraggingOver
? "#B1AFFF"
: "#7A90E2",
}}>
{column.items.map((item, index) => {
return(
<Draggable
key={item._id}
draggableId={item._id}
index={index}
>
{(provided, snapshot) => {
return (
<TodooContainer
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
backgroundColor: snapshot.isDragging
? "pink"
: "white",
// color: "#7A90E2",
...provided.draggableProps.style
}}
>
<TodoInnerText>
{item.content}
<IconBox>
<FontAwesomeIcon className="XIcon" icon={faXmark}
onClick={(e) => RemovetodoList(item._id, e)}
/>
</IconBox>
</TodoInnerText>
</TodooContainer>
);
}}
</Draggable>
);
})}
{provided.placeholder}
</TodoListBox>
);
}}
</Droppable>
</div>
</TodoWrap>
);
})}
</DragDropContext>
</TodoListAllWrap>
</TodoAllContainer>
</>
)
};
export default TodoListForm;
const TodoAllContainer = styled.div`
width: 540px;
${BoxSize}
right: 10px;
& .dateWrap {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
}
`
const DateButton = styled.button`
/* ${DateStyle} */
background-color: #7A90E2;
width: 110px;
height: 40px;
border: none;
border-radius: 30px;
background-color: #7A90E2;
font-family: "Gaegu", serif;
color: white;
:hover{
background-color: #8D9EFF;
}
`
const TodoInputWrap = styled.div`
${ InputWrapStyle }
width: 403px;
top: 8px;
margin: 0 auto;
margin-bottom: 30px;
gap: 20px;
& .faPlus {
background-color: #7A90E2;
}
& p {
font-size: 18px;
color: #7A90E2;
font-family: "Gaegu", serif;
margin: 0;
padding-top: 3px;
}
`
const TodoInnerText = styled.div`
color: #7A90E2;
${InnerTextStyle }
`
const IconBox = styled.div`
margin-right: 20px;
color: #7A90E2;
${ IconStyle }
`
p.s 스타일을 추후에 바뀔수 있다.
-> 등록시 추가 잘됨
-> 선택한 날짜에 쓴 내용 잘 보여짐
-> todo완료시 done에 옮김. 옮겨진후 페이지가 리로드 되거나 다른 날짜에서 해당 날짜로 다시 돌아와도 그대로 잘 보임.
🔥문제점
-> 선택한 날짜에 등록후 그 날짜의 데이터가 호출되는게 아닌 현재 로컬 시간 기준으로 데이터가 호출됨.이 문제는 다음 글에 이어서 쓴다.
잘보고 갑니다. :)