먼저 죄송합니다! 블로그를 방치하다 못해 유기했습니다. 사죄의 말씀을 드립니다.. ㅠ
다시 글을 쓴 이유는 글을 적긴 해야겠다고 생각은 했는데 어영부영~ 넘어가다가
테오 프론트엔드방의 어떤 분께서 제 블로그 글을 봤는데 다음 챕터가 언제 나오나 기다렸다고 하셨고 다른 분도 요청하셨기에 마음을 다시 잡고 작성해보려고 합니다.
시작합니다. 화이팅 :)
오늘의 챕터를 간략하게 설명하자면, 라벨 이동, 라벨 점 위치 변경, 캔버스 범위 밖 나가는 것 막기 및 라벨리스트 표출입니다.
먼저, 코드입니다. (전체 코드는 깃허브를 확인해주세요.)
// 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} // 업데이트 함수 전달
/>
))}
// ...
)
}
전체 변수 및 컴포넌트 이름을 변경했습니다.
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 객체를 넣어줍니다.
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;
챕터 - 2 완성본
이번 챕터는 라벨을 이동하고, 점 위치를 갱신, 캔버스 영역 밖으로 나가는 것을 막는 기능을 만들었습니다.
그리고 라벨이 생성되면서 라벨 리스트에서 보이는 것을 확인할 수 있었습니다.
코드 위주의 설명이라 글 자체가 지루할 수도 있었는데, 끝까지 봐주셔서 감사합니다.
깃허브에서 코드를 실제로 실행하고 디버깅하면 금방 이해하실 수 있습니다. 화이팅 :)
당연하게도 현재 기능들이 완성된 게 아닙니다!
그룹 기준이 아닌 그려진 폴리곤 기준으로 캔버스 영역밖으로 나가지 않게 만들어야 하고,
선택, 줌인 줌아웃, 실제 이미지 등 여러 챕터가 남아있으니 다음 챕터를 기다려주세요.
완성된 코드는 깃허브에서 확인할 수 있습니다.
주소
감사합니다.