Skip to content

Commit

Permalink
Merge branch 'develop' into refactor/TEAM-BEAT#384/GigRegister
Browse files Browse the repository at this point in the history
  • Loading branch information
imddoy authored Aug 31, 2024
2 parents 1cb6230 + 8838001 commit 2487109
Show file tree
Hide file tree
Showing 26 changed files with 2,315 additions and 1,663 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"react-components": "^0.5.1",
"react-dom": "^18.3.1",
"react-helmet-async": "^2.0.5",
"react-image-crop": "^11.0.6",
"react-lottie-player": "^2.0.0",
"react-router-dom": "^6.24.0",
"shelljs": "^0.8.5",
Expand Down
47 changes: 47 additions & 0 deletions src/components/commons/imageEditor/ImageEditor.styled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import styled from "styled-components";
import { Generators } from "@styles/generator";
import ReactCrop from "react-image-crop";

export const ModalContainer = styled.div`
${Generators.flexGenerator("column", "center", "center")}
position: fixed;
z-index: 100;
gap: 2rem;
background-color: rgb(15 15 15 / 70%);
inset: 0;
`;

export const OriginImage = styled.img`
width: 32rem;
`;

export const CustomReactCrop = styled(ReactCrop)<{
aspectRatio: number;
calculatedSize: { width: number; height: number };
}>`
position: relative;
.ReactCrop__drag-handle {
width: 1rem;
height: 1rem;
background: ${({ theme }) => theme.colors.main_pink_400};
border: none;
}
.ReactCrop__crop-selection::before {
position: absolute;
top: 50%;
left: 50%;
box-sizing: border-box;
width: ${({ calculatedSize }) => calculatedSize.width}rem;
height: ${({ calculatedSize }) => calculatedSize.height}rem;
transform: translate(-50%, -50%);
border: 0.2rem solid ${({ theme }) => theme.colors.white};
border-radius: 0.6rem;
content: "";
}
`;
169 changes: 169 additions & 0 deletions src/components/commons/imageEditor/ImageEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { useState, useRef, useEffect } from "react";
import * as S from "./ImageEditor.styled";
import { Crop, PixelCrop, centerCrop, makeAspectCrop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import Button from "@components/commons/button/Button";

interface ImageEditorProps {
file: string;
aspectRatio: number;
onCropped: (croppedImageUrl: string) => void;
}

const ImageEditor = ({ file, aspectRatio, onCropped }: ImageEditorProps) => {
const [crop, setCrop] = useState<Crop>({
unit: "%",
x: 0,
y: 0,
width: 100,
height: 100,
});
const [imageSize, setImageSize] = useState<{ width: number; height: number }>({
width: 0,
height: 0,
});
const [calculatedSize, setCalculatedSize] = useState<{ width: number; height: number }>({
width: 0,
height: 0,
});
const [croppedImageUrl, setCroppedImageUrl] = useState<string | null>(null);
const imageRef = useRef<HTMLImageElement | null>(null);

const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
// 이미지의 원래 너비와 높이 가져오기
const { naturalWidth: width, naturalHeight: height } = e.currentTarget;
if (e.currentTarget) {
setImageSize({ width, height });
}

// 이미지의 중앙에 크롭 영역 설정
const centerCropped = centerCrop(
makeAspectCrop(
// 크롭 영역 설정
{
unit: "%", // 크롭 단위
width: 100, // 크롭 영역 너비
height: 100,
},
width / height,
width,
height
),
width,
height
);

setCrop(centerCropped); // 중앙에 설정된 크롭 영역을 상태에 반영
};

useEffect(() => {
if (imageSize.width > 0 && imageSize.height > 0) {
// 렌더링된 이미지 크기 계산
const renderedWidth = 32;
const renderedHeight = (imageSize.height / imageSize.width) * renderedWidth;

// 100으로 초기화했던 크롭 영역의 최대 크기 계산
const maxWidth = (crop.width / 100) * renderedWidth;
const maxHeight = (crop.height / 100) * renderedHeight;

// 최적의 width, height 계산
let width, height;
if (maxWidth / aspectRatio > maxHeight) {
height = maxHeight;
width = height * aspectRatio;
} else {
width = maxWidth;
height = width / aspectRatio;
}

setCalculatedSize({ width, height });
}
}, [imageSize, crop]);

// 이미지 크롭 업데이트
const onCropChange = (crop: Crop, percentCrop: Crop) => {
setCrop(percentCrop);
};

const onCropComplete = (crop: PixelCrop, percentCrop: Crop) => {
makeClientCrop(crop);
};

const makeClientCrop = async (crop: PixelCrop) => {
if (imageRef.current && crop.width && crop.height) {
const croppedImage = await getCroppedImg(imageRef.current, crop, "newFile.jpeg");
setCroppedImageUrl(croppedImage);
}
};

// 크롭된 이미지를 생성
const getCroppedImg = (
image: HTMLImageElement,
crop: PixelCrop,
fileName: string
): Promise<string> => {
// 캔버스에 이미지 생성
const canvas = document.createElement("canvas");
const scaleX = image.naturalWidth / image.width;
const scaleY = image.naturalHeight / image.height;
// 캔버스 크기를 원본 해상도 기준으로 설정
const pixelRatio = window.devicePixelRatio; // 실제 픽셀 크기와 CSS 픽셀 크기 간의 비율
canvas.width = crop.width * scaleX * pixelRatio;
canvas.height = crop.height * scaleY * pixelRatio;
const ctx = canvas.getContext("2d");

// 고해상도 이미지를 유지하기 위해 canvas에 스케일 적용
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
ctx.imageSmoothingQuality = "high";

ctx.drawImage(
// 원본 이미지 영역
image,
crop.x * scaleX, // 크롭 시작 x 좌표
crop.y * scaleY, // 크롭 시작 y 좌표
crop.width * scaleX, // 크롭할 이미지의 가로 길이
crop.height * scaleY, // 크롭할 이미지의 세로 길이
// 캔버스 영역
0, // 캔버스에서 이미지 시작 x 좌표
0, // 캔버스에서 이미지 시작 y 좌표
crop.width * scaleX, // 캔버스에서 이미지의 가로 길이
crop.height * scaleY // 캔버스에서 이미지의 세로 길이
);

return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (!blob) {
console.error("이미지 없음");
return reject(new Error("이미지 없음"));
}
const fileUrl = URL.createObjectURL(blob);
resolve(fileUrl);
}, "image/jpeg");
});
};

const handleComplete = () => {
if (croppedImageUrl) {
onCropped(croppedImageUrl); // 크롭된 이미지 URL을 부모 컴포넌트로 전달
} else {
onCropped(file); // 크롭하지 않으면 원래 URL 전달
}
};

return (
<S.ModalContainer>
<S.CustomReactCrop
crop={crop}
onChange={onCropChange}
onComplete={onCropComplete}
ruleOfThirds={true} // 삼분법선
calculatedSize={calculatedSize}
>
<S.OriginImage src={file} alt="Original" onLoad={onImageLoad} ref={imageRef} />
</S.CustomReactCrop>
<Button onClick={handleComplete}>완료하기</Button>
</S.ModalContainer>
);
};

export default ImageEditor;
11 changes: 11 additions & 0 deletions src/constants/bookingStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const bookingStatusText = {
CHECKING_PAYMENT: "입금 확인 예정",
BOOKING_CONFIRMED: "입금 완료",
BOOKING_CANCELLED: "예매 취소",
};

export type bookingStatusTypes = keyof typeof bookingStatusText;

export interface DefaultDepositProps {
$status;
}
29 changes: 14 additions & 15 deletions src/pages/book/Book.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const Book = () => {

useEffect(() => {
if (data) {
const isBookingAvailable = data?.scheduleList[data?.scheduleList.length - 1]?.dueDate >= 0;
const isBookingAvailable = data?.scheduleList[data?.scheduleList.length - 1]?.isBooking;

if (!isBookingAvailable) {
openAlert({
Expand All @@ -55,6 +55,10 @@ const Book = () => {
}, []);

const [selectedValue, setSelectedValue] = useState<number>();
const selectedSchedule = data?.scheduleList.find(
(schedule) => schedule.scheduleId === selectedValue
);

const [round, setRound] = useState(1);
const [bookerInfo, setBookerInfo] = useState({
bookerName: "",
Expand Down Expand Up @@ -146,6 +150,8 @@ const Book = () => {
scheduleNumber: getScheduleNumberById(data?.scheduleList!, selectedValue!),
purchaseTicketCount: round,
totalPaymentAmount: (data?.ticketPrice ?? 0) * round,
// TODO: 상수로 관리
bookingStatus: "CHECKING_PAYMENT",
} as GuestBookingRequest;

if (!isLogin) {
Expand All @@ -154,7 +160,6 @@ const Book = () => {
...formData,
...bookerInfo,
password: easyPassword.password,
isPaymentCompleted: false,
} as GuestBookingRequest;
} else {
// 회원 예매 요청
Expand All @@ -180,6 +185,11 @@ const Book = () => {
});
} catch (error) {
const errorResponse = error.response?.data as ErrorResponse;
if (errorResponse.status === 500) {
openAlert({
title: "서버 내부 오류로 예매가 불가능합니다.",
});
}
if (errorResponse.status === 409) {
openAlert({
title: "이미 매진된 공연입니다.",
Expand Down Expand Up @@ -214,7 +224,6 @@ const Book = () => {
setActiveButton(false);
}
}, [isLogin, selectedValue, bookerInfo, easyPassword, isTermChecked]);

if (isLoading) {
return <Loading />;
}
Expand Down Expand Up @@ -287,18 +296,8 @@ const Book = () => {
<Context
isDate={true}
subTitle="날짜"
date={data
?.scheduleList![
(selectedValue ?? data?.scheduleList?.[0].scheduleId) -
data?.scheduleList?.[0].scheduleId
].performanceDate?.slice(0, 10)
.toString()}
time={data
?.scheduleList![
(selectedValue ?? data?.scheduleList?.[0].scheduleId) -
data?.scheduleList?.[0].scheduleId
].performanceDate?.slice(11, 16)
.toString()}
date={selectedSchedule?.performanceDate.slice(0, 10).toString() ?? ""}
time={selectedSchedule?.performanceDate.slice(11, 16).toString() ?? ""}
/>
<Context
subTitle="가격"
Expand Down
Loading

0 comments on commit 2487109

Please sign in to comment.