Skip to content

Commit

Permalink
Merge pull request #353 from oduck-team/feat/339
Browse files Browse the repository at this point in the history
feat: 프로필 수정 시, 이미지 crop 기능 구현
  • Loading branch information
presentKey authored Jan 16, 2024
2 parents 50e9345 + 3c7adb7 commit fab08a8
Show file tree
Hide file tree
Showing 19 changed files with 685 additions and 159 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"framer-motion": "^10.16.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-easy-crop": "^5.0.4",
"react-helmet-async": "^1.3.0",
"react-router-dom": "^6.14.2",
"react-slick": "^0.29.0",
Expand Down
19 changes: 18 additions & 1 deletion src/features/users/api/profile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,24 @@ export default class ProfileApi {
return await get(`/members/${memberId}/${menu}/count`);
}

async updateProfile(form: ProfileEditFormData) {
async updateProfile(
form: ProfileEditFormData,
backgroundImage: File | undefined,
thumbnailImage: File | undefined,
) {
const data = new FormData();

for (const [key, value] of Object.entries(form)) {
data.append(key, value);
}

//TODO: 서버에서 이미지 업로드 구현 시, 주석 해제
if (backgroundImage && thumbnailImage) {
console.log("");
}
// if (backgroundImage) data.append("backgroundImage", backgroundImage);
// if (thumbnailImage) data.append("thumbnail", thumbnailImage);

return await patch("/members", form);
}

Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { PlusCircle } from "@phosphor-icons/react";

export const ImageEditButtonContainer = styled.div<{
borderRadius: string;
height: string | number;
}>`
position: absolute;
top: 0;
left: 0;
opacity: 0.7;
width: 100%;
height: ${({ height }) => (height === "100%" ? "100%" : `${height}px`)};
background-color: ${({ theme }) => theme.colors.neutral[100]};
cursor: pointer;
${({ borderRadius }) =>
borderRadius === "50%" &&
css`
border-radius: 50%;
`}
`;

export const PlusCircleIcon = styled(PlusCircle)`
position: absolute;
top: 50%;
left: 50%;
color: ${({ theme }) => theme.colors.neutral["05"]};
transform: translate(-50%, -50%);
transition:
transform linear 0.2s,
color linear 0.2s;
&.resetIcon {
color: ${({ theme }) => theme.colors.warn["50"]};
transform: translate(-50%, -50%) rotate(45deg);
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { StrictPropsWithChildren } from "@/types";

import {
ImageEditButtonContainer,
PlusCircleIcon,
} from "./ImageEditButton.style";

interface ImageEditButtonProps {
croppedImage: string | null;
borderRadius?: "50%" | "none";
/** default height: 100%, number는 px 단위 */
height?: "100%" | number;
onClick: () => void;
onReset: () => void;
}

export default function ImageEditButton({
croppedImage,
borderRadius = "none",
height = "100%",
onClick,
onReset,
children,
}: StrictPropsWithChildren<ImageEditButtonProps>) {
/* crop된 이미지가 있으면 이미지 제거, 없으면 이미지 등록 */
const handleClick = croppedImage ? onReset : onClick;

return (
<ImageEditButtonContainer
onClick={handleClick}
borderRadius={borderRadius}
height={height}
>
<PlusCircleIcon
className={croppedImage ? "resetIcon" : undefined}
size={24}
/>

{children}
</ImageEditButtonContainer>
);
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import Avatar from "@/components/Avatar";
import { StrictPropsWithChildren } from "@/types";

import AvatarEditButton from "./AvatarEditButton";
import { ProfileAvatarContainer } from "./ProfileAvatar.style";

export default function ProfileAvatar({ children }: StrictPropsWithChildren) {
return <ProfileAvatarContainer>{children}</ProfileAvatarContainer>;
}

ProfileAvatar.Avatar = Avatar;
ProfileAvatar.AvatarEditButton = AvatarEditButton;
3 changes: 0 additions & 3 deletions src/features/users/components/ProfileImageSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { StrictPropsWithChildren } from "@/types";

import ProfileArt from "./ProfileArt";
import ArtEditButton from "./ProfileArtEditButton";
import ProfileAvatar from "./ProfileAvatar";
import ProfileSetupButton from "./ProfileSetupButton";
import { ProfileImageSectionContainer } from "./style";
Expand All @@ -15,7 +14,5 @@ export default function ProfileImageSection({
}

ProfileImageSection.Art = ProfileArt; // 프로필 배경
ProfileImageSection.ArtEditButton = ArtEditButton; // 프로필 배경 수정 버튼
ProfileImageSection.ProfileSetupButton = ProfileSetupButton; // 프로필 설정 버튼

ProfileImageSection.ProfileAvatar = ProfileAvatar; // 프로필 아바타
64 changes: 64 additions & 0 deletions src/features/users/hooks/useCropModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from "react";

import useToast from "@/components/Toast/useToast";
import { readFile } from "@/libs/imageCrop";

const allowedExtensions = ["jpg", "jpeg", "png", "webp"];

export default function useCropModal(
inputRef: React.RefObject<HTMLInputElement>,
) {
const [isImageCropModal, setIsImageCropModal] = useState(false);
const [imageSrc, setImageSrc] = useState<string | null>(null); // 사용자의 원본 이미지
const toast = useToast();

const handleImageEditClick = () => {
setIsImageCropModal(true);
if (!imageSrc) inputRef.current?.click(); // 이미지 input open
};

const closeImageCropModal = () => {
setIsImageCropModal(false);
resetUploadedImage();
};

/** input file change */
const handleImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
const file = e.target.files[0];
const fileExtension = file.name.split(".").at(-1)?.toLowerCase();

if (!allowedExtensions.includes(fileExtension ?? "")) {
toast.error({
message: `'jpg' 'jpeg' 'png' 'webp' 확장자 이미지를 등록해주세요.`,
duration: 4,
});
e.target.value = "";
return;
}

const imageDataUrl = await readFile(file);
setImageSrc(imageDataUrl);
}
};

/**
* 이미지 crop을 취소하거나 완료하면
* input과 원본 이미지 상태 초기화
*/
const resetUploadedImage = () => {
if (inputRef.current) {
inputRef.current.value = "";
setImageSrc(null);
}
};

return {
imageSrc,
isImageCropModal,
handleImageEditClick,
handleImageChange,
closeImageCropModal,
resetUploadedImage,
};
}
Loading

0 comments on commit fab08a8

Please sign in to comment.