Skip to content

Commit

Permalink
Refactor: #69 UserSettingPage에서 이미지, 링크 관련 코드를 컴포넌트로 분리
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoonyesol committed Aug 17, 2024
1 parent a1a0143 commit 6db43d9
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 177 deletions.
97 changes: 97 additions & 0 deletions src/components/user/LinkForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { ChangeEvent, useState } from 'react';
import { FaPlus, FaMinus } from 'react-icons/fa6';
import { UseFormSetValue } from 'react-hook-form';
import { USER_SETTINGS } from '@/constants/userSettings';
import useToast from '@/hooks/useToast';
import { EditUserInfoForm, UserSignUpForm } from '@/types/UserType';

type LinkFormProps = {
initialLinks: string[];
setValue: UseFormSetValue<UserSignUpForm | EditUserInfoForm>;
};

export default function LinkForm({ initialLinks, setValue }: LinkFormProps) {
const [link, setLink] = useState<string>('');
const [linksList, setLinksList] = useState<string[]>(initialLinks);
const [isFocused, setIsFocused] = useState(false);
const { toastWarn } = useToast();

const handleFocus = () => {
setIsFocused(true);
};

const handleBlur = () => {
setIsFocused(false);
};

const handleLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
setLink(e.target.value);
};

const handleAddLink = (newLink: string) => {
if (newLink.trim() === '') return;
if (linksList.length === USER_SETTINGS.MAX_LINK_COUNT)
return toastWarn(`링크는 최대 ${USER_SETTINGS.MAX_LINK_COUNT}개까지 등록할 수 있습니다.`);

const updatedLinks = [...linksList, newLink.trim()];
setLinksList(updatedLinks);
setValue('links', updatedLinks);
setLink('');
};

const handleRemoveLink = (removeLink: string) => {
const filteredData = linksList.filter((item) => item !== removeLink);
setLinksList(filteredData);
setValue('links', filteredData);
};

return (
<div>
<h1 className="font-bold">링크</h1>
<div className="flex flex-col gap-4">
{linksList.map((item) => (
<div key={item} className="flex h-30 items-center rounded-lg border border-input px-6 text-sm">
<div className="flex h-full w-full flex-row items-center gap-8">
<div className="flex grow items-center overflow-hidden border-transparent bg-inherit">
<a href={`http://${item}`} target="_blank" rel="noreferrer">
{item}
</a>
</div>
<button
type="button"
onClick={() => handleRemoveLink(item)}
className="flex h-20 w-20 items-center justify-center rounded-lg bg-sub px-8 font-bold shadow-md"
aria-label="삭제"
>
<FaMinus />
</button>
</div>
</div>
))}
<div
className={`flex h-30 items-center rounded-lg border border-input ${isFocused ? 'bg-white' : 'bg-disable'} px-6 text-sm`}
>
<div className="flex h-full w-full flex-row items-center gap-8">
<input
placeholder="ex) www.github.com"
value={link}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleLinkChange}
type="text"
className="flex grow bg-inherit outline-none placeholder:text-emphasis"
/>
<button
type="button"
onClick={() => handleAddLink(link)}
className="flex h-20 w-20 items-center justify-center rounded-lg bg-sub px-8 font-bold shadow-md"
aria-label="추가"
>
<FaPlus />
</button>
</div>
</div>
</div>
</div>
);
}
64 changes: 64 additions & 0 deletions src/components/user/ProfileImgForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useState } from 'react';
import { GoPlusCircle } from 'react-icons/go';
import { FaRegTrashCan } from 'react-icons/fa6';
import { UseFormSetValue } from 'react-hook-form';
import { convertBytesToString } from '@/utils/converter';
import { USER_SETTINGS } from '@/constants/userSettings';
import useToast from '@/hooks/useToast';
import { EditUserInfoForm, UserSignUpForm } from '@/types/UserType';

type ProfileImgFormProps = {
initialImage: string;
setValue: UseFormSetValue<UserSignUpForm | EditUserInfoForm>;
};

export default function ProfileImgForm({ initialImage, setValue }: ProfileImgFormProps) {
const [imageUrl, setImageUrl] = useState(initialImage);
const { toastWarn } = useToast();

// 이미지 관련 코드
const handleChangeImg = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

if (file.size > USER_SETTINGS.MAX_IMAGE_SIZE) {
toastWarn(`최대 ${convertBytesToString(USER_SETTINGS.MAX_IMAGE_SIZE)} 이하의 이미지 파일만 업로드 가능합니다.`);
e.target.value = '';
return;
}

const image = URL.createObjectURL(file);
setImageUrl(image);
setValue('profileUrl', image);
};

const handleRemoveImg = () => {
setImageUrl('');
setValue('profileUrl', '');
};

return (
<div className="flex flex-col items-center gap-8">
<div className="group relative h-100 w-100 overflow-hidden rounded-[50%] border border-input">
{imageUrl ? (
<>
<img src={imageUrl} alt="profileImage" className="h-full w-full object-cover" />
<div className="absolute inset-0 hidden items-center justify-center bg-black bg-opacity-50 group-hover:flex">
<p role="presentation" className="cursor-pointer" onClick={handleRemoveImg} onKeyDown={handleRemoveImg}>
<FaRegTrashCan size="1.5rem" color="white" />
</p>
</div>
</>
) : (
<label
htmlFor="image"
className="absolute inset-0 flex cursor-pointer flex-col items-center justify-center gap-1 text-center"
>
<input id="image" type="file" className="hidden" onChange={handleChangeImg} />
<GoPlusCircle size="1.5rem" color="#5E5E5E" />
</label>
)}
</div>
</div>
);
}
187 changes: 11 additions & 176 deletions src/pages/setting/UserSettingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,160 +1,42 @@
import { GoPlusCircle } from 'react-icons/go';
import { FaRegTrashCan, FaPlus, FaMinus } from 'react-icons/fa6';
import { ChangeEvent, useState } from 'react';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { UserSignUpForm } from '@/types/UserType';
import { EditUserInfoForm } from '@/types/UserType';
import ValidationInput from '@/components/common/ValidationInput';
import { USER_AUTH_VALIDATION_RULES } from '@/constants/formValidationRules';
import reduceImageSize from '@/utils/reduceImageSize';
import { USER_SETTINGS } from '@/constants/userSettings';
import useToast from '@/hooks/useToast';
import { convertBytesToString } from '@/utils/converter';
import { USER_INFO_DUMMY } from '@/mocks/mockData';
import ProfileImageUploader from '@/components/user/ProfileImgForm';
import LinkForm from '@/components/user/LinkForm';

export default function UserSettingPage() {
const [imageUrl, setImageUrl] = useState('');
const [isFocused, setIsFocused] = useState(false);
const [link, setLink] = useState<string>('');
const [linksList, setLinksList] = useState<string[]>(USER_INFO_DUMMY.links);
const { toastSuccess, toastError, toastWarn } = useToast();

const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
} = useForm<UserSignUpForm>({
} = useForm<EditUserInfoForm>({
mode: 'onChange',
defaultValues: {
id: USER_INFO_DUMMY.id,
email: USER_INFO_DUMMY.email,
code: '',
nickname: USER_INFO_DUMMY.nickname,
bio: USER_INFO_DUMMY.bio,
links: USER_INFO_DUMMY.links,
profileUrl: USER_INFO_DUMMY.profileUrl,
},
});

// 이미지 관련 코드
const handleChangeImg = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;

if (file.size > USER_SETTINGS.MAX_IMAGE_SIZE) {
toastWarn(`최대 ${convertBytesToString(USER_SETTINGS.MAX_IMAGE_SIZE)} 이하의 이미지 파일만 업로드 가능합니다.`);
e.target.value = '';
return;
}

setImageUrl(URL.createObjectURL(file));
};

const handleRemoveImg = () => {
setValue('profileUrl', '');
setImageUrl('');
};

// 웹사이트 링크 관련 코드
const handleFocus = () => {
setIsFocused(true);
};

const handleBlur = () => {
setIsFocused(false);
};

const handleLinkChange = (e: ChangeEvent<HTMLInputElement>) => {
setLink(e.target.value);
};

const handleAddLink = (newLink: string) => {
if (newLink.trim() === '') return;
if (linksList.length === USER_SETTINGS.MAX_LINK_COUNT)
return toastWarn(`링크는 최대 ${USER_SETTINGS.MAX_LINK_COUNT}개까지 등록할 수 있습니다.`);

setLinksList([...linksList, newLink.trim()]);
setValue('links', [...linksList, newLink.trim()]);
setLink('');
};

const handleRemoveLink = (removeLink: string) => {
const filteredData = linksList.filter((item) => item !== removeLink);
setLinksList(filteredData);
setValue('links', filteredData);
};

// form 전송 함수
const onSubmit = async (data: UserSignUpForm) => {
const { id, ...filteredData } = data;
const onSubmit = async (data: EditUserInfoForm) => {
const { id, email, profileUrl, ...filteredData } = data;
console.log(data);

// TODO: 폼 제출 로직 수정 필요
try {
// 프로필 수정 폼
const registrationResponse = await axios.post(`http://localhost:8080/api/v1/user/${id}`, filteredData);
if (registrationResponse.status !== 200) return toastError('프로필 수정에 실패했습니다. 다시 시도해 주세요.');

// 이미지 폼
if (!imageUrl) return toastSuccess('프로필 수정이 완료되었습니다.');
const imgFormData = new FormData();
try {
const jpeg = await reduceImageSize(imageUrl);
const file = new File([jpeg], new Date().toISOString(), { type: 'image/jpeg' });
imgFormData.append('profile', file);
imgFormData.append('id', id);

const imageResponse = await axios.post(`http://localhost:8080/api/v1/users/file`, imgFormData, {
headers: { 'Content-Type': 'multipart/form-data' },
});

if (imageResponse.status !== 200) return toastError('이미지 업로드에 실패했습니다. 다시 시도해 주세요.');

toastSuccess('프로필 수정이 완료되었습니다.');
} catch (err) {
toastError(`이미지 업로드에 실패했습니다: ${err}`);
}
} catch (error) {
toastError(`프로필 수정 진행 중 오류가 발생했습니다: ${error}`);
}
// TODO: 폼 제출 로직 작성
};

return (
<div className="my-60 flex h-full items-center justify-center">
<form onSubmit={handleSubmit(onSubmit)} className="flex w-full max-w-300 flex-col gap-8">
{/* 프로필 이미지 */}
<div className="flex flex-col items-center gap-8">
<div className="group relative h-100 w-100 overflow-hidden rounded-[50%] border border-input">
{imageUrl ? (
<>
<img src={imageUrl} alt="profileImage" className="h-full w-full object-cover" />
<div className="absolute inset-0 hidden items-center justify-center bg-black bg-opacity-50 group-hover:flex">
<p
role="presentation"
className="cursor-pointer"
onClick={handleRemoveImg}
onKeyDown={handleRemoveImg}
>
<FaRegTrashCan size="1.5rem" color="white" />
</p>
</div>
</>
) : (
<label
htmlFor="image"
className="absolute inset-0 flex cursor-pointer flex-col items-center justify-center gap-1 text-center"
>
<input
{...register('profileUrl')}
id="image"
type="file"
className="hidden"
onChange={handleChangeImg}
/>
<GoPlusCircle size="1.5rem" color="#5E5E5E" />
</label>
)}
</div>
</div>
<ProfileImageUploader initialImage={USER_INFO_DUMMY.profileUrl} setValue={setValue} />

{/* 아이디 */}
<ValidationInput
Expand Down Expand Up @@ -198,54 +80,7 @@ export default function UserSettingPage() {
</div>

{/* 링크 */}
<div>
<h1 className="font-bold">링크</h1>
<div className="flex flex-col gap-4">
{linksList &&
linksList.map((item) => (
<div key={item} className="flex h-30 items-center rounded-lg border border-input px-6 text-sm">
<div className="flex h-full w-full flex-row items-center gap-8">
<div className="flex grow items-center overflow-hidden border-transparent bg-inherit">
<a href={`http://${item}`} target="_blank" rel="noreferrer">
{item}
</a>
</div>
<button
type="button"
onClick={() => handleRemoveLink(item)}
className="flex h-20 w-20 items-center justify-center rounded-lg bg-sub px-8 font-bold shadow-md"
aria-label="삭제"
>
<FaMinus />
</button>
</div>
</div>
))}
<div
className={`flex h-30 items-center rounded-lg border border-input ${isFocused ? 'bg-white' : 'bg-disable'} px-6 text-sm`}
>
<div className="flex h-full w-full flex-row items-center gap-8">
<input
placeholder="ex) www.github.com"
value={link}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={handleLinkChange}
type="text"
className="flex grow bg-inherit outline-none placeholder:text-emphasis"
/>
<button
type="button"
onClick={() => handleAddLink(link)}
className="flex h-20 w-20 items-center justify-center rounded-lg bg-sub px-8 font-bold shadow-md"
aria-label="추가"
>
<FaPlus />
</button>
</div>
</div>
</div>
</div>
<LinkForm initialLinks={USER_INFO_DUMMY.links} setValue={setValue} />

{/* 개인정보 수정 버튼 */}
<div className="flex flex-col gap-8 text-center">
Expand Down
Loading

0 comments on commit 6db43d9

Please sign in to comment.