[KonvaJS] 데이터 라벨링 툴 만들기 챕터 - 2

emit·2024년 7월 4일
2

☠️ 다시 시작..

먼저 죄송합니다! 블로그를 방치하다 못해 유기했습니다. 사죄의 말씀을 드립니다.. ㅠ

다시 글을 쓴 이유는 글을 적긴 해야겠다고 생각은 했는데 어영부영~ 넘어가다가
테오 프론트엔드방의 어떤 분께서 제 블로그 글을 봤는데 다음 챕터가 언제 나오나 기다렸다고 하셨고 다른 분도 요청하셨기에 마음을 다시 잡고 작성해보려고 합니다.

시작합니다. 화이팅 :)

🧊 챕터-2 시작하기

오늘의 챕터를 간략하게 설명하자면, 라벨 이동, 라벨 점 위치 변경, 캔버스 범위 밖 나가는 것 막기 및 라벨리스트 표출입니다.

먼저, 코드입니다. (전체 코드는 깃허브를 확인해주세요.)

// src/App.jsx

function App() {
  // ...
  const [labelList, setLabelList] = useState([]); // state 변수 이름 바꿨습니다. labelObjList -> labelList
  
  // ...
  
  // 라벨 리스트 업데이트
  const handleUpdateLabelList = (id, attrs) => {
    const updateLabelList = labelList.map((obj) => {
      if (obj.id !== id) {
        return obj;
      }
      return {
        ...obj,
        ...attrs,
      };
    });
    setLabelList(updateLabelList);
  };
  
  const handleMouseDown = (e) => {
    const stage = e.target.getStage();
    const mousePos = getMousePos(stage);

    // 라벨 생성 전 이벤트 막기
    if (isFinished) {
      return;
    }

    if (isMouseOverStartPoint && points.length >= 3) {
      setIsFinished(true);

      const target = {
        id: uuidv4(),
        points: points,
      };

      setLabelList([...labelList, target]); // 변수 변경부분
      setPoints([]);
  	} else {
      setPoints([...points, mousePos]);
    }
  }
  
  // ...
  
  return (
    		  // ...
    		  // 변수 변경 부분
              {labelList.length > 0 &&
                labelList.map((label) => (
                  // target -> label 로 변경
                  <Label // LabelTarget 컴포넌트명 변경 기존 -> Label
                    key={label.id}
                    label={label}
                    handleUpdateLabelList={handleUpdateLabelList} // 업데이트 함수 전달
                  />
                ))}
               // ...
  )
}
  • 전체 변수 및 컴포넌트 이름을 변경했습니다.

    • labelObjList -> labelList, LabelTarget 컴포넌트 -> Label 컴포넌트, target -> label
    • 이유는 e.target과 혼동할 수 있고, 변수가 무의미하게 길어지는 느낌이 들었습니다.
  • handleUpdateLabelList 함수는 라벨리스트를 업데이트하는 함수입니다. Label 컴포넌트에 props로 전달해줍니다.

// src/components/Label.jsx        LabelTarget -> Label

const Label = ({ handleUpdateLabelList, label }) => {
  const labelRef = useRef(); // targetRef -> labelRef
  
  // [1]
  // 캔버스 밖으로 나가는 것 막기 (임시 기능 완성x)
  const preventCanvasOutside = (shape) => {
    const box = shape.getClientRect(); // 현재 객체의 경계 정보
    const absPos = shape.getAbsolutePosition(); // 현재 객체의 절대 위치

    // 경계 정보와 절대 위치 간의 차이를 계산하여 오프셋 값
    const offsetX = box.x - absPos.x;
    const offsetY = box.y - absPos.y;

    const newAbsPos = { ...absPos };

    // 객체가 캔버스의 왼쪽 경계를 넘는 경우, 객체의 x 위치를 조정
    if (box.x < 0) {
      newAbsPos.x = -offsetX;
    }

    // 객체의 위쪽 y 위치 조정
    if (box.y < 0) {
      newAbsPos.y = -offsetY;
    }

    // 객체의 오른쪽 x 위치 조정
    if (box.x + box.width > shape.getStage().width()) {
      newAbsPos.x = shape.getStage().width() - box.width - offsetX;
    }

    // 객체의 아래 y 위치 조정
    if (box.y + box.height > shape.getStage().height()) {
      newAbsPos.y = shape.getStage().height() - box.height - offsetY;
    }

    // 객체의 절대 위치를 다시 조정
    shape.setAbsolutePosition(newAbsPos);
  };

  // [2] 그룹 드래그
  const handleDragMove = (e) => {
    if (!labelRef.current) {
      return;
    }
    // 그룹 드래그시 캔버스 경계 나가는 것 막기
    preventCanvasOutside(e.target);
  };
  
  // ...
  
  // [3]
  const handleDragMoveCirclePoint = (e) => {
    // 원 포인트 드래그시 캔버스 경계 나가는 것 막기
    preventCanvasOutside(e.target);

    // 클릭한 원 인덱스
    const index = e.target.index - 1;

    // 클릭한 원 위치
    const pos = [e.target.attrs.x, e.target.attrs.y];

    // props로 받은 업데이트 함수 
    // 라벨 포인트 위치 업데이트
    handleUpdateLabelList(label.id, {
      points: [
        ...label.points.slice(0, index),
        pos,
        ...label.points.slice(index + 1),
      ],
    });
  };

  const flattedendPoints = label.points
    ? label.points.reduce((a, b) => a.concat(b), [])
    : [];
  
  return (
    <>
      <Group
        ref={labelRef}
        draggable={true} // 드래그 가능 false -> true
        onDragMove={handleDragMove} // [2] 와 연동
        onMouseUp={handleMouseUp}
      >
        <Line
          points={flattedendPoints}
          stroke="black"
          strokeWidth={1}
          lineJoin="round"
          fill="red"
          closed={true}
          opacity={0.5}
        />
        {label.points &&
          label.points.map((point, index) => {
            return (
              <Circle
                key={index}
                x={point[0]}
                y={point[1]}
                radius={5}
                fill="yellow"
                stroke="yellow"
                strokeWidth={0.1}
                onDragMove={handleDragMoveCirclePoint} // [3] 과 연동
                draggable={true} // 드래그 가능 false -> true
              />
            );
          })}
      </Group>
    </>
  );
};
  • [1]부터 설명 드리자면, 라벨 이동시 객체가 캔버스 보이는 영역을 벗어나는 것을 막는 함수입니다. 내부 객체(shape)는 Group 컴포넌트 즉 라벨이 될 수 있고, 라벨의 Point가 될 수 있습니다. 객체의 경계정보(드래그하는 그룹 객체)와 절대 위치를 구해 오프셋값을 구합니다.
    좌우상하 영역을 벗어나면 위 값에 맞게 x, y 넣어주고 객체의 위치를 조정합니다.

  • [2]그룹 드래그 이벤트에 현재 이동하는 객체 값을 preventCanvasOutside 함수 안에 파라미터로 넘겨줍니다. (위 함수를 적용한 것과 적용하지 않은 것을 비교해보시고, 위 변수들을 콘솔로 찍어보세요)

  • [3]포인트 포인트 이동했을 때도 마찬가지로 영역밖으로 나가지 못하게 preventCanvasOutside 함수 안에 현재 드래그하는 Circle 객체를 넣어줍니다.

    • 클릭한 포인트의 인덱스를 구해, 위치를 handleUpdateLabelList 함수로 업데이트 진행합니다.
  • Group 컴포넌트에서 draggable 값을 true로 활성화시켜야 라벨 이동이 가능합니다. 드래그 함수를 연결시켜줍니다.

  • 마찬가지로 Circle 컴포넌트도 draggable 활성화시켜주고, 드래그 함수를 연결시켜 라벨을 이동시키면서 영역을 못 벗어나는 것을 볼 수 있습니다.

// src/components/LabelSection.jsx

import LabelCard from "./LabelCard";

// [1]
const LabelSection = ({ handleStartDraw, labelList }) => {
  return (
    <div className="w-1/5 p-5 flex flex-col">
      <button
        className="w-full px-4 py-2 text-white transition duration-500 bg-indigo-500 border border-indigo-500 rounded-md select-none ease hover:bg-indigo-600 focus:outline-none focus:shadow-outline"
        onClick={handleStartDraw}
      >
        폴리곤 생성하기
      </button>
      <div className="flex flex-col mt-2 gap-2">
        {labelList.map((label) => (
          <LabelCard key={label.id} label={label} />
        ))}
      </div>
    </div>
  );
};

export default LabelSection;

// src/componensts/LabelCard.jsx

// [2]
const LabelCard = ({ label }) => {
  return (
    <div className="flex items-center justify-center h-8 bg-red-300 rounded-md">
      {label.id.slice(0, 5)}
    </div>
  );
};

export default LabelCard;
  • 각각의 컴포넌트를 추가로 생성합니다.
  • [1] LabelSection 컴포넌트를 기존 App.jsx 기존 폴리곤 생성하기 버튼 영역을 대체해주세요.
  • [2] LabelCard는 LabelSection 컴포넌트 자식으로 넣어줍니다.
  • 라벨이 생성될 때마다 임시(?) 라벨이 보여지는 것을 확인할 수 있습니다.
  • 참고로 둘다, 나중 작업을 위해 컴포넌트를 분리했습니다.

챕터 - 2 완성본

챕터 2 끝!

이번 챕터는 라벨을 이동하고, 점 위치를 갱신, 캔버스 영역 밖으로 나가는 것을 막는 기능을 만들었습니다.
그리고 라벨이 생성되면서 라벨 리스트에서 보이는 것을 확인할 수 있었습니다.

코드 위주의 설명이라 글 자체가 지루할 수도 있었는데, 끝까지 봐주셔서 감사합니다.

깃허브에서 코드를 실제로 실행하고 디버깅하면 금방 이해하실 수 있습니다. 화이팅 :)

당연하게도 현재 기능들이 완성된 게 아닙니다!
그룹 기준이 아닌 그려진 폴리곤 기준으로 캔버스 영역밖으로 나가지 않게 만들어야 하고,
선택, 줌인 줌아웃, 실제 이미지 등 여러 챕터가 남아있으니 다음 챕터를 기다려주세요.

완성된 코드는 깃허브에서 확인할 수 있습니다.
주소

감사합니다.

profile
간단한 공부 기록들 https://github.com/ohjooyeong

0개의 댓글