Looky 프로젝트에 사진을 업로드 할 시 원하는 부위에( 옷 착용 부위 ) 태그를 이동시킬 수 있고, 이동된 태그들의 좌표를 DB에 담는 기능을 구현하려고 사용한 라이브러리이다. ( 리액트 환경에서 드래그 앤 드롭 인터페이스를 구현하도록 해주는 라이브러리 )
npm i react-dnd react-dnd-html5-backend
DndProvider와 HTML5Backend가 설정한다.
DndProvider 로 item 들을 감싸준다.
HTML5Backend: 브라우저의 HTML5 Drag and Drop API를 활용하기 위한 백엔드입니다.
DndProvider: 드래그 앤 드롭 관련 컴포넌트를 감싸는 최상위 Provider로, 하위 컴포넌트에서 useDrag 및 useDrop을 사용할 수 있게 합니다.
<DndProvider backend={HTML5Backend}>
<PictureContainer ref={containerRef}>
{/* 태그와 이미지 */}
</PictureContainer>
</DndProvider>
드래그 가능한 태그 정의
드래그 가능한 태그는 useDrag 훅을 통해 구현하였다.
const [{ isDragging }, drag] = useDrag(() => ({
type: "TAG", // 드래그 타입 (드롭 영역과 매칭)
item: { id: tag.id }, // 드래그할 때 전달할 데이터
end: (item, monitor) => {
const offset = monitor.getClientOffset(); // 드래그 종료 위치
if (offset) {
onMoveTag(tag.id, offset.x, offset.y); // 드래그 종료 시 위치 업데이트
}
},
collect: (monitor) => ({
isDragging: monitor.isDragging(), // 드래그 중인지 상태 확인
}),
}));
- type: "TAG" - 드래그 타입을 설정. 드래그 대상(useDrag)과 드롭 대상(useDrop)은 동일한 타입이어야 매칭. -> 따로 드롭 훅을 사용하진 않았지만 타입을 지정해주었다.
- item: { id: tag.id } - 드래그가 시작될 때 전달되는 데이터. 여기서는 태그의 고유 ID를 전달.
- end - 드래그가 종료되었을 때 호출. monitor.getClientOffset()을 통해 드래그 종료 위치를 가져옴.
- collect - 드래그 상태를 수집하는 함수. isDragging 값은 현재 드래그 중인지 여부.
drag(ref)로 DOM 요소를 드래그 가능하도록 만듬!
모바일 환경에서 HTML5 Drag and Drop API는 제대로 동작하지 않으므로 터치 이벤트를 별도로 처리하였다.
handleTouchMove 이벤트:
const handleTouchMove = (e: TouchEvent) => {
const touch = e.touches[0];
const offsetX = touch.clientX;
const offsetY = touch.clientY;
onMoveTag(tag.id, offsetX, offsetY); // 태그 이동
};
터치 이동 시 좌표를 추적하여 onMoveTag를 호출하고 태그 위치를 업데이트
ref가 연결된 DOM 요소에 터치 이벤트를 추가
useEffect(() => {
if (isMobile && ref.current) {
const element = ref.current;
element.addEventListener("touchmove", handleTouchMove as EventListener);
return () => {
element.removeEventListener("touchmove", handleTouchMove as EventListener);
};
}
}, [isMobile]);
태그 이동 처리 (handleMoveTag)
태그를 드래그하거나 터치로 이동할 때 호출되는 함수
좌표를 컨테이너의 상대적인 백분율 값으로 변환하여 저장
const handleMoveTag = (id: number, x: number, y: number) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect(); // 컨테이너의 위치와 크기
const adjustedX = Math.min(Math.max(x - rect.left, 0), rect.width - 70); // X 좌표 보정
const adjustedY = Math.min(Math.max(y - rect.top, 0), rect.height - 60); // Y 좌표 보정
const percentageX = (adjustedX / rect.width) * 100; // X 좌표 백분율
const percentageY = (adjustedY / rect.height) * 100; // Y 좌표 백분율
setTags((prevTags) =>
prevTags.map((tag) =>
tag.id === id ? { ...tag, coordinates: { x: percentageX, y: percentageY } } : tag
)
);
}
};
getBoundingClientRect() - 컨테이너의 크기 및 화면 내 위치를 가져옴
좌표 보정 - 태그가 컨테이너 바깥으로 벗어나지 않도록 Math.min과 Math.max로 보정
백분율 계산 - 화면 절대 좌표를 컨테이너의 백분율 좌표로 변환
상태 업데이트 - 해당 태그의 coordinates 값을 업데이트
태그 추가 처리 (handleAddTag)
새로운 태그를 추가하는 로직으로, 태그의 좌표는 랜덤한 백분율 값으로 설정
const handleAddTag = () => {
if (containerRef.current) {
const randomX = Math.random() * 70 + 10; // 10% ~ 80% 범위
const randomY = Math.random() * 70 + 10; // 10% ~ 80% 범위
const newTag: Tag = {
id: Date.now(), // 고유 ID
category: tagInfo.category,
price: tagInfo.price,
productName: tagInfo.productName,
coordinates: { x: randomX, y: randomY }, // 초기 좌표
};
setTags((prevTags) => [...prevTags, newTag]); // 태그 추가
setTagInfo({ category: Category.OUTER, price: 0, productName: "" }); // 입력 초기화
}
};
UI 렌더링
드래그 가능한 태그와 드롭 영역은 각각의 ref에 연결된다!
<TagWrapper
ref={ref} // 드래그 가능한 DOM 요소
style={{
top: `${tag.coordinates.y}%`, // 태그 위치
left: `${tag.coordinates.x}%`,
}}
>
<BlurTag
category={tag.category}
price={tag.price}
name={tag.productName}
onDelete={() => onDeleteTag(tag.id)} // 태그 삭제
/>
</TagWrapper>
정리 !
- React DnD 사용: useDrag 훅을 사용해 태그를 드래그 가능한 요소로 설정
- type: "TAG"로 드래그 타입을 지정했습니다.
- 드래그 종료 시(end), monitor.getClientOffset()를 통해 태그의 위치를 가져와 onMoveTag 함수로 이동 좌표를 업데이트
- 좌표 비율 계산:
- 컨테이너(containerRef)의 크기와 드래그 종료 위치를 기반으로 태그의 위치를 컨테이너 내 비율(% 단위)로 계산하여 저장했습니다.
- 좌표는 Math.min 및 Math.max로 컨테이너 범위 내에서 제한했습니다.
- 모바일 터치 지원:
- 모바일 환경을 확인(useIsMobile)한 후, touchmove 이벤트를 추가로 처리했습니다.
- 터치 좌표(touch.clientX, touch.clientY)를 가져와 onMoveTag 함수로 위치를 업데이트했습니다.
- CSS로 위치 설정:
- 태그의 coordinates 속성을 CSS의 top과 left로 적용해 화면에 배치했습니다.
'프로젝트 > LOOKY' 카테고리의 다른 글
GEMINI API (0) | 2025.02.07 |
---|---|
Zustand (0) | 2025.02.03 |
LOOKY 이미지 최적화 (0) | 2025.02.02 |
Looky 파이어 베이스 데이터 필터링 / 무한스크롤 (0) | 2025.01.28 |
파이어베이스 Looky 데이터 구조 (0) | 2025.01.22 |