본문 바로가기
개발

사내 칸반보드 개발 프로젝트 -2

by 싼쵸 2024. 5. 16.
반응형

2024.05.15 - [개발] - 사내 칸반보드 개발 프로젝트 -1

포부를 밝혔던 1편에이어 이제 진짜 코드가 들어가는 2편이 시작된다.

빠르게 설명부터 들어가자

 

칸반보드의 핵심의 첫번째는 바로 드래그앤 드롭 기능 구현이다.

구글링 해보니 대부분 라이브러리 react-beautiful-dnd 를 사용해서 구현하고 있었다.

나는 라이브러리를 사용하기 보다 직접 구현하고 싶어 사용하지 않기로 했다.

나에게는 chat gpt가 있으니 말이다.

코드를 보여주기전에 나의 스택을 나열하면

 

1. react

2. tailwind

3. style-components

를 사용했다. 

css 라이브러리를 특이하게 두개나 사용하는데 taillwind 는 보통 엘레멘트 css 용도로 사용하고 스타일 컴포넌트는 컴포넌트를 만들때 주로 사용한다. tailwind 를 사용하다보면 가독성이 않좋아져 이렇게 작업하고 있다.

 

먼저 칸반보드의 첫번째 페이지 todo,inProgress,done 영역을 만들었다.

import React from "react";

const Column = ({ header, children, onDrop, onDragOver }) => {
  return (
    <div
      className="flex min-h-lvh w-full flex-col gap-2 rounded-lg bg-slate-200 p-4"
      onDrop={onDrop}
      onDragOver={onDragOver}
    >
      <div className="text-20 mb-4 border-b-4 border-indigo-500">{header}</div>
      {children}
    </div>
  );
};

export default Column;

 

그다음은 영역 안에 들어갈 card 코드다.

import React from "react";
import styled from "styled-components";

const Card = ({ id, status = "to-do", onDragStart }) => {
  return (
    <Wrap draggable="true" onDragStart={onDragStart} id={id} data-task-id={id}>
      <div>
        <StatusChip>{status}</StatusChip>
      </div>
    </Wrap>
  );
};

export default Card;
const Wrap = styled.div`
  // 이 부분을 button에서 div로 변경
  width: 90%;
  background: #fff;
  height: 100px;
  border-radius: 8px;
  padding: 8px;
  cursor: pointer; // 드래그 표시를 위해 cursor 추가
`;

const StatusChip = styled.div`
  width: fit-content;
  background: gray;
  padding: 4px;
`;

 

App.js

 

import styled from "styled-components";
import "./App.css";
import Column from "./components/Column";
import Card from "./components/Card";
import { useState } from "react";
import AddModal from "./components/AddModal";

function App() {
  const [columns, setColumns] = useState({
    todo: ["Task 1", "Task 2", "Task 3"],
    inProgress: [],
    done: [],
  });
  console.log("columns", columns);

  const handleDrop = (e, col) => {
    e.preventDefault();
    const cardId = e.dataTransfer.getData("text");
    const card = document.getElementById(cardId);
    const taskId = card.getAttribute("data-task-id");

    // Find and remove the card from its current column
    const startCol = Object.keys(columns).find((key) =>
      columns[key].includes(taskId),
    );
    // console.log("startCol", startCol);
    const updatedStartCol = columns[startCol]?.filter(
      (task) => task !== taskId,
    );

    // Add card to the new column
    const updatedCol = [...columns[col], taskId];

    setColumns({
      ...columns,
      [startCol]: updatedStartCol,
      [col]: updatedCol,
    });
  };

  return (
    <div className="App">
      <div className="relative flex h-full justify-around gap-3">
        <AddModal />
        {Object.entries(columns)?.map(([columnName, tasks]) => (
          <Column
            header={columnName}
            key={columnName}
            onDrop={(e) => handleDrop(e, columnName)}
            onDragOver={(e) => e.preventDefault()}
          >
            {tasks?.map((task) => (
              <Card
                key={task}
                id={task}
                status={columnName}
                onDragStart={(e) => e.dataTransfer.setData("text", e.target.id)}
              />
            ))}
          </Column>
        ))}
      </div>
    </div>
  );
}

export default App;

 

위에 코드를 보면 궁금증이 생기는 코드들이 있다.

 

onDrop: 잡은 item을 적절한 곳에 놓았을 때 발생

onDragOver: 잡은 item을 다른 item과 겹쳐졌을 때 milli sec마다 발생함 --> 그래서 이벤트를 막았다.

onDragStart: item을 잡기 시작했을 때 발생

 

1. e.dataTransfer

이벤트 객체의 dataTransfer 속성은 드래그 앤 드롭 작업 중 데이터를 관리하는 데 사용됩니다. 이 객체를 사용하여 드래그할 데이터를 설정하거나, 드래그 중인 데이터를 다른 대상에 드롭할 때 그 데이터를 검색할 수 있습니다.

 

2.setData

메소드는 두 개의 인자를 받습니다. 첫 번째 인자는 데이터의 형식,두 번째 인자는 실제로 전달하고자 하는 데이터

즉, 드래그하는 요소의 고유 식별자를 문자열 형태로 전달하고 있습니다.

 

이 코드의 목적

사용자가 특정 요소를 드래그 시작할 때, 그 요소의 id를 "text" 형태로 데이터 전송 객체에 저장합니다. 이렇게 저장된 데이터는 나중에 드롭 이벤트가 발생했을 때 사용될 수 있으며, 드롭 이벤트 핸들러 내에서 getData 메소드를 사용하여 이 데이터를 검색하고 사용할 수 있습니다.

 

그리고 또 의문점이 생기는 분이 함수 handleDrop 이다.

친절하게 나눠서 설명하는게 이해가 빠를 것 같다.

 

1. 이벤트와 데이터 전송처리

e.preventDefault();
const cardId = e.dataTransfer.getData("text");
const card = document.getElementById(cardId);
const taskId = card.getAttribute("data-task-id");

 

  • e.preventDefault();: 기본 이벤트를 막고 드롭 이벤트의 경우, 기본적으로 일부 데이터가 다른 위치로 드롭되는 것을 방지할 수 있습니다.
  • e.dataTransfer.getData("text");: 드래그 이벤트에서 설정된 데이터(여기서는 요소의 id)를 검색합니다. 이 id는 드래그된 요소를 DOM에서 식별하기 위해 사용됩니다.
  • document.getElementById(cardId);: id를 사용하여 드래그된 요소(DOM 요소)를 찾습니다.
  • card.getAttribute("data-task-id");: 드래그된 요소에서 data-task-id 속성을 읽어와서, taskId로 사용합니다. 이는 내부 로직에서 해당 태스크를 식별하는 데 사용됩니다.

2.카드 이동 로직

const startCol = Object.keys(columns).find((key) =>
  columns[key].includes(taskId),
);
const updatedStartCol = columns[startCol]?.filter(
  (task) => task !== taskId,
);
const updatedCol = [...columns[col], taskId];

 

 

  • Object.keys(columns).find(...);: 현재 columns 객체에서 taskId를 포함하는 키(열의 이름)를 찾습니다. 이는 카드가 현재 어느 열에 있는지를 식별합니다.
  • columns[startCol]?.filter(...);: 카드가 현재 위치한 열에서 해당 taskId를 제거합니다. 즉, 카드를 현재 위치에서 제거하고 업데이트된 열의 데이터를 반환합니다.
  • [...columns[col], taskId];: 새로운 열(드롭된 위치)에 taskId를 추가합니다. 이는 드롭 이벤트가 발생한 열에 해당 카드의 id를 추가함으로써 카드를 해당 위치로 "이동"시키는 것입니다.

3.상태 업데이트

setColumns({
  ...columns,
  [startCol]: updatedStartCol,
  [col]: updatedCol,
});
  • setColumns(...);: React의 상태 업데이트 함수를 사용하여 columns 상태를 업데이트합니다. 여기서는 스프레드 연산자(...)를 사용하여 기존의 columns 상태를 복사한 다음, startCol과 col 키의 값을 각각 updatedStartCol과 updatedCol로 업데이트합니다. 이 과정은 카드가 이전 위치에서 제거되고 새 위치에 추가되는 것을 상태에 반영합니다.

결과물

 

구현 영상

 

 

 

지금은 결과물이 볼품없지만 일단 1차 완성을 하면 디자인을 개선할 예정이니 너무 걱정 안하셔도 됩니다.

디자인을 개선하는 내용도 포스팅 할 것이다.

반응형

댓글