오늘은 지금까지 코드를 정리해 보고 히스토리 기능을 만들어 보려고 합니다!
고고
개인적인 공부를 위해 작성하는 블로그입니다. 혹시라도 잘못되거나 부족한 부분이 있다면 댓글로 알려주시면 감사하겠습니다.
💡 핵심 기능 구현
- 성냥개비 드래그 & 드롭 (완료)
- 성냥개비 회전 (완료)
- (new) Match 컴포넌트 분리, History 기능 (오늘 할 것)
- 퍼즐 정답 검증 시스템
정답 검증 시스템을 만들기 전에 PuzzleCanvas.jsx 가 커져 <Image/> 내용을 Matchsitck.jsx로 분리하였다.
import { Image } from "react-konva";
export default function Matchstick({ stick, image, isSelected, onSelect, onDragMove }) {
// console.log(isSelected)
return (
<Image
id={stick.id}
x={stick.x}
y={stick.y}
rotation={stick.angle}
width={18}
height={150}
image={image}
draggable
onTap={() => onSelect(stick.id)} // 모바일 지원
onClick={() => onSelect(stick.id)} // 선택
onDragMove={(e) => onDragMove(e, stick.id)} // 이동
/>
)
}
그리고 배경을 클릭했을 때 선택해제를 해주기 위해 <Stage/> 에 onClick에 setSelectedMatchstick을 null로 해줬는데
무슨 일인지 선택이 안되게 되었다.
onClick={() => setSelectedMatchstick(null)}
콘솔창에 확인해 보니 성냥을 선택했을 때도 이벤트 버블링으로 인해 Stage에 적용된 onClick 함수가 실행된 것 같아서 다음과 같이 e.target.getStage()로 현재 이벤트 타깃이 Stage일 때만 setSelectedMatchstick(null)이 실행되도록해 해결하였다.
const handleBackgroundClick = (e) => {
if (e.target === e.target.getStage()) {
setSelectedMatchstick(null)
}
}
💁🏻 History 기능
히스토리 기능을 추가하려고 보니 현재 onDragMove로 움직일 때마다 저장하기에는 비효율 적일 것 같아서 찾아보니 onDrageEnd가 있어서 바꿔주었다.
const handleDragMove = (e, id) => {
const newPosition = { x: e.target.x(), y: e.target.y() };
setMatchsticks((prev) =>
prev.map((stick) =>
stick.id === id ? { ...stick, ...newPosition } : stick
)
);
setState(`성냥ID: ${id} 상태: 드래그 중`)
};
const handleDragEnd = (e, id) => {
// 드래그 완료 후 위치 업데이트
const newPosition = { x: e.target.x(), y: e.target.y() }
setMatchsticks((prev) =>
prev.map((stick) =>
stick.id === id ? { ...stick, ...newPosition } : stick
)
)
// 상태 저장
setState(`성냥ID: ${id} 상태: 드래그 완료`)
}
이제 history를 기록할 state 배열과 현재 상태를 저장해 둘 currentStep을 만들어 보자
const [history, setHistory] = useState([]); // 상태 기록
const [currentStep, setCurrentStep] = useState(-1); // 현재 상태 포인터
다음으로는 변경이 있어났을 때 어떤 방식으로 history를 기록할 건지 생각해 봐야 하는데 아래와 같이 두 가지의 방법이 생각났다
1. 전체를 스냅샷으로 기록하는 방식
2. 변화된 부분만 기록하는 방식
1번은 쉽고 간단할 것 같지만 성냥하나의 움직임에 대하여 전체 배열을 저장하는 것은 비효율적일 것 같아 2번으로 정했다.
2번은 작업이 실행할 때 원래 상태로의 복구를 위해 변경 전 상태와 변경 후 상태를 모두 저장해야 해서 다음과 같이 구성해 봤다.
[
{ type: "move", id: "1", before: { x: 50, y: 50 }, after: { x: 100, y: 100 } },
{ type: "rotate", id: "2", before: { angle: 30 }, after: { angle: 45 } },
...
]
위의 형식에 맞춰서 저장하는 로직과 handleDragEnd()와 handleRoateEnd()를 수정해 주었다.
const handleDragEnd = (e, id) => {
// 드래그 완료 후 위치 업데이트
// 정수로 반올림
const x = Math.round(e.target.x())
const y = Math.round(e.target.y())
const newPosition = { x, y }
const findOne = matchsticks.find((stick) => stick.id === id)
console.log('findOne: ', findOne)
setMatchsticks((prev) =>
prev.map((stick) =>
stick.id === id ? { ...stick, ...newPosition } : stick
)
)
// 상태 저장
const before = {x: findOne.x, y: findOne.y }
const after = { x, y }
saveState("move", id, before, after)
}
const handleRotateEnd = (newAngle, id) => {
// 정수로 반올림
const roundedAngle = Math.round(newAngle)
const findOne = matchsticks.find((stick) => stick.id === id)
// 회전 완료 후 각도 업데이트
setMatchsticks((prev) =>
prev.map((stick) =>
stick.id === id ? { ...stick, angle: roundedAngle} : stick
)
)
// 상태 저장
const before = {angle: findOne.angle}
const after = {angle: newAngle}
saveState("rotate", id, before, after)
}
const saveState = (type, id, before, after) => {
const newHistory = history.slice(0, currentStep + 1) // 현재 상태 이후의 기록 삭제
newHistory.push({ type, id, before, after}) // 새로운 상태 저장
setHistory(newHistory)
setCurrentStep(newHistory.length - 1) // 현재 상태를 마지막으로 이동
}
콘솔 창으로 확인해 보니 잘 저장되는 것 같다. 내일은 undo와 redo를 만들어보고 횟수제한도 어떻게 구현해야 할지 생각해 봐야겠다.
읽어주셔서 감사합니다~!
'개발 > 개발로그' 카테고리의 다른 글
[React] 간단한 성냥퍼즐 웹 서비스 만들기 8일차 (게임 타입, 횟수 제한 구현) (0) | 2024.12.05 |
---|---|
[React] 간단한 성냥퍼즐 웹 서비스 만들기 7일차 (Undo, Redo, Remove구현) (0) | 2024.11.25 |
[React] 간단한 성냥퍼즐 웹 서비스 만들기 5일차 (Konva.Transformer) (0) | 2024.11.18 |
[React] 간단한 성냥퍼즐 웹 서비스 만들기 4일차 (react-konva) (3) | 2024.11.15 |
[React] 간단한 성냥퍼즐 웹 서비스 만들기 3일차 (navbar, darkmode) (2) | 2024.11.14 |