Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/#351] 공연 등록하기 상세 이미지 클라 구현 #355

Merged
merged 7 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion src/pages/register/Register.styled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,35 @@ export const FileInputWrapper = styled.div`

${Generators.flexGenerator("row", "center", "start")}
gap: 1.4rem;
overflow-x: scroll;

&::-webkit-scrollbar {
display: none;
}
`;

export const FilesInputWrapper = styled.div`
position: relative;

${Generators.flexGenerator("row", "center", "start")}
gap: 1.4rem;
width: calc(100% + 2.4rem);
padding-right: 2.4rem;
overflow-x: scroll;

&::-webkit-scrollbar {
display: none;
}
`;

export const HiddenFileInput = styled.input`
display: none;
`;

export const CustomFileInput = styled.label<{ width?: number; height?: number }>`
${Generators.flexGenerator()}
${Generators.flexGenerator("column", "center", "center")}

flex-shrink: 0;
width: ${({ width }) => (width ? width : 10.8)}rem;
height: ${({ height }) => (height ? height : 15.4)}rem;

Expand All @@ -130,6 +151,13 @@ export const CustomFileInput = styled.label<{ width?: number; height?: number }>
border-radius: 6px;
`;

export const CustomFileInputCounter = styled.p`
color: ${({ theme }) => theme.colors.gray_500};
${({ theme }) => theme.fonts["body1-normal-medi"]};
`;
export const CustomFileInputLength = styled.span`
color: ${({ theme }) => theme.colors.white};
`;
export const PreviewImageWrapper = styled.article<{ width?: number; height?: number }>`
position: relative;
width: ${({ width }) => (width ? width : 10.8)}rem;
Expand Down
12 changes: 12 additions & 0 deletions src/pages/register/Register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,15 @@ import {
handleChange,
handleDateChange,
handleGenreSelect,
handleImagesUpload,
handleImageUpload,
handleTotalTicketCountChange,
isAllFieldsFilled,
onFreeClick,
onMinusClick,
onPlusClick,
} from "./utils/handleEvent";
import DetailImage from "./components/DetailImage";

const Register = () => {
const { isLogin } = useLogin();
Expand Down Expand Up @@ -84,6 +86,7 @@ const Register = () => {
accountNumber: "", // 계좌번호
accountHolder: "", // 예금주
posterImage: "", // 포스터 이미지 URL
performanceImageList: [], // 상세 이미지 URL
performanceTeamName: "", // 공연 팀명
performanceVenue: "", // 공연 장소
performanceContact: "", // 대표자 연락처
Expand Down Expand Up @@ -123,6 +126,7 @@ const Register = () => {
accountNumber,
accountHolder,
posterImage,
performanceImageList,
performanceTeamName,
performanceVenue,
performancePeriod,
Expand Down Expand Up @@ -221,6 +225,9 @@ const Register = () => {
};
}),
bankName: bankInfo ? bankInfo : "NONE",
performanceImageList: gigInfo.performanceImageList.map((image) => ({
performanceImage: image.performanceImage,
})),
};
try {
await postPerformance(formData);
Expand Down Expand Up @@ -364,6 +371,11 @@ const Register = () => {
/>
</InputRegisterBox>
<S.Divider />
<DetailImage
value={performanceImageList}
onImagesUpload={(performanceImage) => handleImagesUpload(performanceImage, setGigInfo)}
/>
<S.Divider />
<InputRegisterBox title="공연 소개">
<TextArea
name="performanceDescription"
Expand Down
110 changes: 110 additions & 0 deletions src/pages/register/components/DetailImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ChangeEvent, useEffect, useState } from "react";
import * as S from "../Register.styled";
import { IconCamera } from "@assets/svgs";
import Spacing from "@components/commons/spacing/Spacing";
import useModal from "./../../../hooks/useModal";

interface DetailImageProps {
value?: PreviewImageList[] | undefined;
onImagesUpload: (detailImage: PreviewImageList[]) => void;
}

interface PreviewImageList {
id: number;
performanceImage: string;
}

const DetailImage = ({ value, onImagesUpload }: DetailImageProps) => {
const { openAlert } = useModal();
const [previewImgs, setPreviewImgs] = useState<PreviewImageList[] | null>(value || null);
const [inputKey, setInputKey] = useState<number>(Date.now());

useEffect(() => {
setPreviewImgs(value || null);
}, [value]);
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) value 속성이 변해서 리렌더링이 발생했을 경우에만, DetailImage의 상태 업데이트 좋네요!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3) 저는 오히려 반대로,,, 이 로직이 필요한 이유가 뭘까요? 제가 지우고 테스트 했는데 정상적으로 동작해서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헉 그러게요... 그냥 value에 따라 바뀌어야 한다고 생각했는데, 이미지 업로드하기 전에 previewImgs를 직접 바꿔줘서 value가 독단적으로 바뀔일이 없을 것 같습니다...! 이거를 그냥 살리고 뒤에 있는 필요없는 setPreviewImgs를 지우겠습니다.


const uploadFile = (e: ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (files && files.length > 0) {
// 최대 5장 업로드 안내
if (previewImgs.length + files.length > 5) {
openAlert({
title: "가능한 이미지 수를 초과했습니다.",
okText: "확인",
});
return;
}

// 파일 순서대로 처리하기 위해 비동기 작업
const processFile = (file: File) => {
return new Promise<PreviewImageList>((resolve) => {
const fileReader = new FileReader();
fileReader.onload = function (event) {
const imageUrl = event.target?.result as string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) onload 함수의 경우 파일을 바탕으로readAsDataURL 메서드를 자체적으로 호출한 뒤 Base64 인코딩 문자열의 형태로 result 형태로 반환한다더라구요! as string 잘 쓴 걸 보면 알 것 같긴 한데 혹시 모르실까봐 참고하시라고 코멘트합니다 ~

resolve({
id: Date.now() + Math.floor(Math.random() * 1000), // ctrl 키로 동시에 이미지 선택해도 id 중복되지 않도록 랜덤 값 추가
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 와웅 id idx로 주는 게 좋지 않은 방법이라고 해서 항상 id 값 줄 때 고민이 많았는데 이런 방법도 있군요!!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 👍

Comment on lines +38 to +45
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 오옹 이런 작업을!!! 최고채현!!

performanceImage: imageUrl,
});
};
fileReader.readAsDataURL(file);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 아! 그리고 readAsDataURL 메서드의 경우에는 파일리더를 통해 파일을 비동기적으로 읽고, 읽기가 성공하면 onload 이벤트가 발생한다고 하네요. 이런 연관성도 알아두면 재밌을 것 같아요 ~

});
};

// 모든 파일 처리 완료 후 상태 업데이트
const processAllFiles = async () => {
const newPreviewImgs = await Promise.all(
Array.from(files).map((file) => processFile(file))
);
const updatedPreviewImgs = [...previewImgs, ...newPreviewImgs];
onImagesUpload(updatedPreviewImgs);
};

processAllFiles();
}
};

const removeImage = (id: number) => {
onImagesUpload(previewImgs.filter((detail) => detail.id !== id)); // 비동기적으로 반영되는 setState때문에 내부에 넣어야 필터링된 최신 이미지들을 바로 반영됨

setInputKey(Date.now());
};

return (
<S.InputRegisterBox $marginBottom={2.8}>
<S.InputTitle>공연 상세 이미지</S.InputTitle>
<S.InputDescription>선택 사항입니다. (최대 5장)</S.InputDescription>
<Spacing marginBottom="1.4" />
<S.FilesInputWrapper>
<S.HiddenFileInput
key={inputKey}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) inputKey 어디서 쓰나 계속 봤는데 여기서 사용했었네요 ㅎㅎ..
혹시 HiddenFileInput의 역할은 뭔지 설명 부탁드려도 될까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p5) 저는 inputKey의 역할이 궁금해요!

Copy link
Contributor Author

@imddoy imddoy Aug 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드는 사실 포스터 업로드하는 코드 파일을 복제해서 사용했습니다. 그래서 HiddenFileInput을 그대로 가져와서 조금만 변형시켜서 작성했습니다.

일단, HiddenFileInput의 역할 설명을 어떤 것을 원하시는건지 잘 모르겠습니다.....
HiddenFileInput은 사진 파일 업로드 버튼입니다!

InputKey는 포스터 등록할때 정의한 것 그대로 사용하고 있습니다!
사진을 올리고 내렸다가 다시 같은 사진을 올렸을 때, 변화가 일어나지 않는다고 생각해서 onChange가 작동되지 않았고, 그래서 사진을 선택해도 프리뷰에 사진이 뜨지 않았습니다!!!
이 문제를 해결하기 위해서 inputKey로 선택한 시간을 정의해서 같은 사진을 다시 선택해도 문제 없도록 했습니다!!

type="file"
id="files"
accept="image/png, image/jpg, image/jpeg, image/svg"
onChange={uploadFile}
multiple
disabled={previewImgs.length >= 5}
/>
<S.CustomFileInput htmlFor="files" width={15.7} height={21}>
<IconCamera width={"3.2rem"} />
<S.CustomFileInputCounter>
<S.CustomFileInputLength>{previewImgs.length}</S.CustomFileInputLength>/5
</S.CustomFileInputCounter>
</S.CustomFileInput>
{previewImgs &&
previewImgs.map((previewImg) => (
<S.PreviewImageWrapper key={previewImg.id} width={15.7} height={21}>
<S.PreviewImage
src={previewImg.performanceImage}
alt={`Preview-${previewImg.id}`}
width={15.7}
height={21}
/>
<S.RemoveImageButton onClick={() => removeImage(previewImg.id)} />
</S.PreviewImageWrapper>
))}
</S.FilesInputWrapper>
</S.InputRegisterBox>
);
};

export default DetailImage;
6 changes: 6 additions & 0 deletions src/pages/register/typings/gigInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export interface Staff {
staffPhoto: string;
}

export interface PerformanceImage {
id: number;
performanceImage: string;
}

export interface GigInfo {
performanceTitle: string;
genre: SHOW_TYPE_KEY;
Expand All @@ -29,6 +34,7 @@ export interface GigInfo {
accountNumber: string;
accountHolder: string;
posterImage: string;
performanceImageList: PerformanceImage[];
performanceTeamName: string;
performanceVenue: string;
performanceContact: string;
Expand Down
13 changes: 12 additions & 1 deletion src/pages/register/utils/handleEvent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Dayjs } from "dayjs";
import { ChangeEvent, Dispatch, SetStateAction } from "react";
import { GigInfo } from "../typings/gigInfo";
import { PerformanceImage, GigInfo } from "../typings/gigInfo";
import { SHOW_TYPE_KEY } from "@pages/gig/constants";

// Image 핸들링
Expand All @@ -14,6 +14,17 @@ export const handleImageUpload = (
}));
};

// Images 핸들링
export const handleImagesUpload = (
performanceImage: PerformanceImage[],
setGigInfo: Dispatch<SetStateAction<GigInfo>>
) => {
setGigInfo((prev) => ({
...prev,
performanceImageList: performanceImage,
}));
};

// Genre 핸들링
export const handleGenreSelect = (
selectedGenre: SHOW_TYPE_KEY,
Expand Down
Loading