(10) TodoList 드래그앤드롭 적용하기

2023. 6. 11. 17:06Github 프로젝트/todolist반응형웹앱

728x90
반응형
SMALL

지난 시간에는 클린 코딩을 작성하는 것으로 마치였습니다.

지금 부터 드래그앤 드롭으로 TodoList의 순서를 변경하도록 적용하겠습니다.

이전시간에 이어서 진행하도록 하겠습니다.

(9) Todo List 클린 코드로 리팩토리 및 고도화 작업 : https://berkley.tistory.com/13

 

(9) Todo List 클린 코드로 리팩토링 및 고도화 작업 (코드줄 줄이기, 분할, 필요없는 주석 제거 등)

이전 시간 (8) Todolist 메인 부분 이외의 상세부분의 대해 CRUD 작성 : https://berkley.tistory.com/12 (8) Todolist 메인 부분 이외의 상세부분의 대해 CRUD 작성 지금까지 TodoList CRUD 및 UI 반응형 웹앱, Redux까지

berkley.tistory.com

 

 

1. 사전 작업

먼저 react-dnd와 react-dnd-html5-backend 설치합니다. 이전에 immutability-helper 설치 하셨기 때문에 생략합니다.

 

 

 

2. 드래그 앤 드롭 셋팅

먼저 드래그앤 드롭 사용 범위를 조정하기 위해 DndProvider 를 사용합니다.

 

App.js에서 아래와 같이 변경합니다.

(1) 렌더링 실행 할 위치

  todoList?.map((todo, index) => (
    <DndProvider key={index} backend={HTML5Backend}>
      <TodoMain index={index} todo={todo} dndMoveTodoList={dndMoveTodoList}/>
    </DndProvider>
  ))

 

(2) 드래그 앤 드롭 실행 후 state 값 변경하기 위해 state와 드래그앤 드롭 기능에 대한 필요한 코드 작성

  const todoList = useSelector((state) => state.todoList.array);
  const dispatch = useDispatch();

  // 이벤트 변환이 안될 시 이 걸 사용
  const [isStateUpdate, setIsStateUpdate] = useState(false);

  // 드래그앤 드롭 기능 추가
  const dndMoveTodoList = useCallback(
    (dragIndex, hoverIndex) => {
      const dragCard = todoList[dragIndex];
      let array = todoList;
      console.log(dragCard);
      console.log(array);
      dispatch({
        type: "setTodoList",
        array: update(todoList, {
          $splice: [
            [dragIndex, 1], // delete
            [hoverIndex, 0, dragCard], // Add
          ],
        }),
      });

      // 여기서 전체 리스트 update API 삽입
      setIsStateUpdate(!isStateUpdate);

      // 삽입 끝
    },
    [todoList]
  );

 

이처럼 자식 컴포넌트 TodoMain에서 드래그인 드롭을 하면 state 값을 변환 시킬 수 있습니다.

다음 작업으로는 자식 컴포넌트에 드래그기능과 드롭 기능을 셋팅하여 구현합니다.

 

 

 

3. TodoMain에서 드래그, 드롭 기능 실행 

다음은 드래그 이후, 드롭으로 떨어질 시 순서 변경 가능하게 구현합니다.

이 경우, drag와 drop을 동시 작업을 하기 위해 useRef()를 먼저 작성합니다.

 

App.js의 TodoMain 자식 컴포넌트 위치 : ./component/main/index.js

 

(1) 먼저 useRef를 작성합니다.

  const ref = useRef(null); // (*)

 

 

(2) 렌더링 부분의 다음과 같이 변경

return(
  <div ref={ref} key={index} className="todoContainer">
  	{ /* ...... */ }
  </div>
 )

 

(3) 드래그 부분 구현

  const [{ isDragging }, drag] = useDrag({
    // (*)
    item: { type: ItemTypes.CARD, todo, index },
    type: ItemTypes.CARD,
    collect: (monitor) => ({
      isDragging: !!monitor.isDragging(),
    }),
  });

  drag(drop(ref)); // (*)
  // drag and drop 끝

 

(4) 드롭 부분 구현

  const [, drop] = useDrop({
    // (*)
    accept: ItemTypes.CARD,
    hover(item, monitor) {
      if (item.index === index)
        return
      if (!ref.current) {
        return;
      }

      const dragIndex = item.index;
      const hoverIndex = index;

      if (dragIndex === hoverIndex) {
        return;
      }

      const hoverBoundingRect = ref.current?.getBoundingClientRect();
      const hoverMiddleY =
        (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;

      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }

      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return;
      }

      dndMoveTodoList(dragIndex, hoverIndex);

      item.index = hoverIndex;
    },
  });

 

 

여기까지 하셨으면 드래그앤 드롭 기능을 이용하여 순서 조정 하실 수 있습니다.

 

 

 

 

4. 완성 코드

(1)  App.js

import { Button, Card, CardBody, CardHeader, CardTitle } from "reactstrap";
import "./App.css";
import { useDispatch, useSelector } from "react-redux";

import CreateTitle from "./component/create/index";
import TodoMain from "./component/main/index";
import { useCallback, useState } from "react";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import update from "immutability-helper";

function App() {
  const todoList = useSelector((state) => state.todoList.array);
  const dispatch = useDispatch();

  // 이벤트 변환이 안될 시 이 걸 사용
  const [isStateUpdate, setIsStateUpdate] = useState(false);

  // 드래그앤 드롭 기능 추가
  const dndMoveTodoList = useCallback(
    (dragIndex, hoverIndex) => {
      const dragCard = todoList[dragIndex];
      let array = todoList;
      console.log(dragCard);
      console.log(array);
      dispatch({
        type: "setTodoList",
        array: update(todoList, {
          $splice: [
            [dragIndex, 1], // delete
            [hoverIndex, 0, dragCard], // Add
          ],
        }),
      });

      // 여기서 전체 리스트 update API 삽입
      setIsStateUpdate(!isStateUpdate);

      // 삽입 끝
    },
    [todoList]
  );

  return (
    <div className="my-2 background-container">
      <Card className="my-2 container">
        <CardHeader className="header-container">
          To-do list 반응형 웹 개발
        </CardHeader>
        <CardBody>
          <CardTitle tag="h2">To-do list</CardTitle>
          <div className="todo">
            {
              // 다음은 각각 데이터를 불려올 때 map을 주로 사용합니다.
              // todoList에 저장된 state 값을
              // 유사 forEach문처럼 todo를 이용하여 출력하고,
              // 상위 div에 key값을 설정하기 위해 index 값을 집어 넣어야 합니다.
              todoList?.map((todo, index) => (
                <DndProvider key={index} backend={HTML5Backend}>
                  <TodoMain
                    index={index}
                    todo={todo}
                    dndMoveTodoList={dndMoveTodoList}
                  />
                </DndProvider>
              ))
            }
          </div>
          <CreateTitle />
        </CardBody>
      </Card>
    </div>
  );
}

export default App;

 

 

(2) ./component/main/index.js

import React, { useCallback, useRef, useState } from "react";

import BigList from "./category/big/index";
import SmallList from "./category/small/index";
import { useDispatch, useSelector } from "react-redux";
import update from "immutability-helper";
import { Button, Input } from "reactstrap";
import { useDrag, useDrop } from "react-dnd";

const ItemTypes = {
  CARD: 'card'
}

const TodoMain = ({ index, todo, dndMoveTodoList }) => {
  const todoList = useSelector((state) => state.todoList.array);
  const dispatch = useDispatch();

    // todolist 소분류 추가 입력 모드 활성화 state
  // 여기는 소분류 입력 모드 활성화 및 비활성화 작업을 진행함
  const [isContentsAdd, setIsContentsAdd] = useState(false);

  // 다음은 생성시 바로 state문에 이벤트 표시되도록 설정
  const [isBehaviorChange, setIsBehaviorChange] = useState(false);

  const [createInputContents, setCreateInputContents] = useState();

  /**
   * todolist 소분류 추가 작업 입력
   */
  const addContentsOnChange = (e, index) => {
    const { name, value } = e.target;
    setCreateInputContents({ [index]: { [name]: value } });
    console.log(createInputContents);
  };
  /**
   *  Todolist 소분류 추가하는 기능
   */
  const createContentsButton = (todo, index) => {
    let tmp = todo;
    tmp?.contents.push(createInputContents[index].contents);
    dispatch({
      type: "setTodoList",
      array: update(todoList, {
        $merge: {
          [index]: tmp,
        },
      }),
    });
    // 생성 후 바로 입력모드 취소
    setIsContentsAdd(!isContentsAdd)
  };

  // drag and drop 관련
  // 참조 : https://velog.io/@suyeonme/React-DragDrop-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
  // 문서 참조 : https://react-dnd.github.io/react-dnd/docs/api/use-drag
  const ref = useRef(null); // (*)

  const [, drop] = useDrop({
    // (*)
    accept: ItemTypes.CARD,
    hover(item, monitor) {
      if (item.index === index)
        return
      if (!ref.current) {
        return;
      }

      const dragIndex = item.index;
      const hoverIndex = index;

      if (dragIndex === hoverIndex) {
        return;
      }

      const hoverBoundingRect = ref.current?.getBoundingClientRect();
      const hoverMiddleY =
        (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
      const clientOffset = monitor.getClientOffset();
      const hoverClientY = clientOffset.y - hoverBoundingRect.top;

      if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
        return;
      }

      if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
        return;
      }

      dndMoveTodoList(dragIndex, hoverIndex);

      item.index = hoverIndex;
    },
  });

  const [{ isDragging }, drag] = useDrag({
    // (*)
    item: { type: ItemTypes.CARD, todo, index },
    type: ItemTypes.CARD,
    collect: (monitor) => ({
      isDragging: !!monitor.isDragging(),
    }),
  });

  drag(drop(ref)); // (*)
  // drag and drop 끝

  return (
    <div ref={ref} key={index} className="todoContainer">
      <BigList
        todo={todo}
        todoList={todoList}
        index={index}
        isContentsAdd={isContentsAdd}
        setIsContentsAdd={setIsContentsAdd}
      />

      {isContentsAdd && (
        <>
          {"소분류 추가용 Input : "}
          <Input
            name="contents"
            onChange={(e) => addContentsOnChange(e, index)}
          />
          <Button onClick={() => createContentsButton(todo, index)}>
            추가
          </Button>
        </>
      )}

      {todo?.contents?.map((tc, tcIndex) => (
        <div key={tcIndex} className="todoContents">
          <SmallList
            tc={tc}
            index={index}
            tcIndex={tcIndex}
            todoList={todoList}
            isBehaviorChange={isBehaviorChange} setIsBehaviorChange={setIsBehaviorChange}
          />
        </div>
      ))}

{/* <Button onClick={() => dndTodoList(dragIndex, hoverIndex)} /> */}
    </div>
  );
};

export default TodoMain;

 

5. 결과물 - 600px~1200px 기준

 

<초기화면>

 

<JWT 인증 처리를 Redux 학습하기 이동한 화면 결과>

 

 

 

마치며,

여기까지 하셨으면 TodoList의 대부분의 기능이 완성 되었습니다.

아직까지는 UI 면에서는 부실한 점이 있습니다.

현재 저는 React를 연습을 목적으로 두었기 때문에 UI에 신경을 미쳐 쓰지 못했지만, 

차근차근 UI를 개선하여 실시간 꾸준히 업데이트를 할 예정입니다.

실시간 UI 업데이트 참조 : https://berkley.tistory.com/14

 

(별첨) 실시간 css 리팩토링 (23년 6월 10일 기준)

1. 600px ~ 1200px /* 1200px 이하 일 경우 */ @media (max-width:1200px) { .background-container { margin:2% 0 0 0 } .container { background-color: yellow; margin : 0 auto; text-align: center; max-width:960px; vertical-align: center; } .header-container

berkley.tistory.com

 

 

 

728x90
반응형
LIST