[MainProject] 이미지 업로드, 뒤로/앞으로 가기 버튼
업로드 버튼
커스텀 페이지에서 그림판에 로컬 이미지를 삽입하기 위한 업로드 버튼
구현
업로드 버튼 레이아웃
//UploadButton.tsx
import styled from 'styled-components';
import React from 'react';
import upload from '../../../assets/images/img_modal/upload.png';
// 스타일드 컴포넌트로 스타일을 정의
const InputStyled = styled.input.attrs({
type: 'file',
accept: 'image/*',
})`
// 스타일 정의
// ...
`;
// 업로드 버튼 컴포넌트 정의
type UploadButtonProps = {
onUpload: (imageUrl: string) => void;
};
const UploadButton: React.FC<UploadButtonProps> = ({ onUpload }) => {
// 업로드된 이미지 처리 함수
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
// input 요소에서 업로드된 파일을 가져옵니다.
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
// 이미지 파일을 로드한 후 처리
const image = new Image();
image.onload = () => {
// 이미지 크기 조정을 위한 작업 시작
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const aspectRatio = image.width / image.height;
const maxWidth = 200;
const maxHeight = 200;
let width = maxWidth;
let height = maxHeight;
// 이미지의 종횡비에 따라 크기 조정
if (maxWidth / maxHeight > aspectRatio) {
height = maxWidth / aspectRatio;
} else {
width = maxHeight * aspectRatio;
}
canvas.width = width;
canvas.height = height;
ctx?.drawImage(image, 0, 0, width, height);
// 조정된 이미지를 데이터 URL로 변환
const imageUrl = canvas.toDataURL('image/png');
// 변환된 이미지 URL을 부모 컴포넌트로 전달
onUpload(imageUrl);
};
// 이미지 소스를 로드
image.src = e.target?.result as string;
};
// 파일을 데이터 URL로 읽기
reader.readAsDataURL(file);
}
};
// 업로드 버튼을 렌더링
return <InputStyled type="file" onChange={handleUpload} />;
};
export default UploadButton;
input
요소에 type 속성을 'file'
로 설정하고, 이미지 파일만 업로드할 수 있도록 accept 속성을 'image/*'
로 설정
const InputStyled = styled.input.attrs({
type: 'file',
accept: 'image/*',
})`
handleUpload 함수
type UploadButtonProps = {
onUpload: (imageUrl: string) => void,
};
const UploadButton: React.FC<UploadButtonProps> = ({ onUpload }) => {
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
// ...
};
return <InputStyled type="file" onChange={handleUpload} />;
};
export default UploadButton;
onUpload
함수를 프로퍼티로 받음handleUpload
함수는 파일 업로드 시 실행- 파일을 읽어와 이미지를 크기 조정하고 데이터 URL로 변환한 후, onUpload 함수를 호출하여 변환된 이미지 URL을 부모 컴포넌트로 전달
onChange
이벤트가 발생하면handleUpload
함수가 실행되도록 한다.- 반환되는 JSX는 스타일드 컴포넌트로 정의한 InputStyled 컴포넌트로, 실제 업로드 버튼을 렌더링
이미지 업로드 및 처리
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const image = new Image();
image.onload = () => {
// ...
};
image.src = e.target?.result as string;
};
reader.readAsDataURL(file);
}
};
event.target.files
를 통해 업로드된 파일에 접근하고, 첫 번째 파일을 가져온다.- 파일이 존재할 경우 ` FileReader
를 생성하고, 해당 파일을
데이터 URL`로 읽어온다. reader.onload
콜백 함수는 데이터 URL을 이미지의 src에 할당하여 이미지를 로드한다.- 이미지 로드가 완료되면 이미지의 크기를 조정하고 데이터 URL로 변환하여 onUpload 함수를 호출하여 부모 컴포넌트로 전달한다.
이미지 크기 조정
const aspectRatio = image.width / image.height;
const maxWidth = 200;
const maxHeight = 200;
let width = maxWidth;
let height = maxHeight;
if (maxWidth / maxHeight > aspectRatio) {
height = maxWidth / aspectRatio;
} else {
width = maxHeight * aspectRatio;
}
- 이미지의
종횡비
와최대 크기
를 기준으로 이미지의 크기를 조정 aspectRatio
변수에 이미지의 가로와 세로비율을 저장
maxWidth
와maxHeight
에 최대 가로 및 세로 크기를 지정종횡비
에 따라 이미지의 크기를 조정하여 width와 height에 할당
이미지 데이터 URL 변환
canvas.width = width;
canvas.height = height;
ctx?.drawImage(image, 0, 0, width, height);
const imageUrl = canvas.toDataURL("image/png");
onUpload(imageUrl);
- 조정된 이미지를 canvas 요소에 그린다.
- canvas의 크기를 width와 height로 설정하고,
ctx?.drawImage
메서드로 이미지를 그린다. canvas.toDataURL
메서드를 사용하여 조정된 이미지를 데이터 URL로 변환한다.- 변환된 이미지 URL을 onUpload 함수를 통해 부모 컴포넌트로 전달한다.
앞으로가기 뒤로가기 버튼
버튼 레이아웃
Undo/Redo의 레이아웃은 동일하게 제작하였다.
import styled from "styled-components";
import undo from "../../../assets/images/img_modal/undo.png";
const ButtonStyled = styled.div`
position: relative;
z-index: 20;
width: 30px;
height: 30px;
margin-left: 20px;
border: none;
cursor: grab;
background-image: url("${undo}");
background-repeat: no-repeat;
background-size: cover;
background-position: center;
&::before {
content: "";
position: absolute;
top: -6px;
left: -6px;
right: -6px;
bottom: -6px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.1);
transform: scale(0);
transition: transform 0.2s ease-in-out;
}
&:hover::before {
transform: scale(1);
}
&:active {
filter: brightness(1.2);
}
`;
type UndoButtonProps = {
onUndo: () => void,
};
function UndoButton({ onUndo }: UndoButtonProps) {
return <ButtonStyled onClick={onUndo}></ButtonStyled>;
}
export default UndoButton;
이미지 Undo/Redo 구현
Undo
handleUndoButtonClick
함수는 Undo 버튼이 클릭되었을 때 실행되는 이벤트 처리 로직
const handleUndoButtonClick = () => {
// 이미지 모드에서 Undo
if (undoMode === "image" && images.length > 0) {
const updatedImages = [...images];
const lastImage = updatedImages.pop();
if (lastImage) {
setImages(updatedImages);
updateImages(updatedImages);
setRedoImages([lastImage, ...redoImages]);
setRedoMode("image");
}
}
// 그림 그리기 모드에서 Undo
else if (undoMode === "drawing" && penDrawings.length > 0) {
const updatedPenDrawings = [...penDrawings];
const lastPenDrawing = updatedPenDrawings.pop();
if (lastPenDrawing) {
setPenDrawings(updatedPenDrawings);
setRedoPenDrawings([lastPenDrawing, ...redoPenDrawings]);
setRedoMode("drawing");
}
}
setDraggedImage(null);
setDraggedImageIndex(-1);
};
먼저,
undoMode
상태에 따라 이미지 모드와 그림 그리기 모드를 구분하여 처리한다.
이미지 모드일 때
- images 배열의 복사본
updatedImages
를 만들고, 마지막 이미지를 제거 - 제거한 마지막 이미지를
lastImage
로 가져와, lastImage를Undo 이미지 배열
에 추가하고, 해당 배열을redoImages
로 설정 - setImages를 통해 이미지 배열을 업데이트하고, updateImages 함수를 호출하여 상위 컴포넌트에도 업데이트를 전파
setRedoMode
를 호출하여 Redo 모드를'image'
로 설정
그림 그리기 모드일 때
penDrawings
배열의 복사본updatedPenDrawings
를 만들고, 마지막 그림 그리기 정보를 제거- 제거한 마지막 그림 그리기 정보를
lastPenDrawing
으로 가져와, lastPenDrawing을 Undo 그림 그리기 정보 배열에 추가하고, 해당 배열을redoPenDrawings
로 설정 setPenDrawings
를 통해 그림 그리기 정보 배열을 업데이트하고, setRedoPenDrawings를 호출하여 Redo 그림 그리기 정보 배열을 업데이트- setRedoMode를 호출하여 Redo 모드를 ‘drawing’으로 설정
Redo
const handleRedoButtonClick = () => {
if (redoMode === "image" && redoImages.length > 0) {
const updatedRedoImages = [...redoImages];
const redoImage = updatedRedoImages.shift();
if (redoImage) {
// Redo 이미지를 이미지 배열에 추가하고, Redo 이미지 배열 업데이트
setImages([redoImage, ...images]);
updateImages([redoImage, ...images]);
setRedoImages(updatedRedoImages);
}
} else if (redoMode === "drawing" && redoPenDrawings.length > 0) {
const updatedRedoPenDrawings = [...redoPenDrawings];
const redoPenDrawing = updatedRedoPenDrawings.shift();
if (redoPenDrawing) {
// Redo 그림 그리기 정보를 그림 그리기 배열에 추가하고, Redo 그림 그리기 배열 업데이트
setPenDrawings([redoPenDrawing, ...penDrawings]);
setRedoPenDrawings(updatedRedoPenDrawings);
}
}
// Redo 이미지와 그림 그리기 배열이 모두 비어있으면 Redo 모드를 'none'으로 설정
if (redoImages.length === 0 && redoPenDrawings.length === 0) {
setRedoMode("none");
}
};
Undo
와 유사하게redoMode
상태에 따라 이미지 모드와 그림 그리기 모드를 구분하여 처리
이미지 모드일 때
redoImages
배열의 복사본updatedRedoImages
를 만들고, 첫 번째 Redo 이미지를 가져와shift
메서드로 배열에서 제거- 가져온 첫 번째 Redo 이미지를 현재 이미지 배열의 맨 앞에 추가하고,
updateImages
함수를 호출하여 상위 컴포넌트에 업데이트를 전파 setRedoImages
를 호출하여 Redo 이미지 배열을 업데이트
그림 그리기 모드일 때
redoPenDrawings
배열의 복사본updatedRedoPenDrawings
를 만들고, 첫 번째 Redo 그림 그리기 정보를 가져와 shift 메서드로 배열에서 제거- 가져온 첫 번째 Redo 그림 그리기 정보를 현재 그림 그리기 정보 배열의 맨 앞에 추가하고,
setRedoPenDrawings
를 호출하여 Redo 그림 그리기 정보 배열을 업데이트 - 마지막으로, Redo 이미지 배열과 Redo 그림 그리기 정보 배열이 모두 비어있으면 Redo 모드를
'none'
으로 설정하여 더 이상 Redo가 불가능한 상태임을 나타냄
댓글남기기