Skip to content

Commit

Permalink
Feat: #69 유저 개인정보 설정 페이지 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
Yoonyesol committed Aug 16, 2024
1 parent 8a924cb commit a1a0143
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 3 deletions.
8 changes: 6 additions & 2 deletions src/components/common/ValidationInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import { RiEyeFill, RiEyeOffFill } from 'react-icons/ri';
* ValidationInput 컴포넌트 params, 모든 params는 optional
*
* @params {string} [label] - 입력 필드의 label 텍스트
* @params {boolean} [required] - 입력 필드 필수 여부
* @params {boolean} [disabled] - 입력 필드 disabled 여부. 기본값은 'false'
* @params {boolean} [required] - 입력 필드 필수 여부. 기본값은 'true'
* @params {string} [errors] - 유효성 검증 후 오류 발생 시 표시할 오류 메시지
* @params {UseFormRegisterReturn} [register] - react-hook-form의 resister 함수.
* register('password', {required: ...})부분을 그대로 파라미터에 넣으면 됩니다.
Expand All @@ -30,6 +31,7 @@ import { RiEyeFill, RiEyeOffFill } from 'react-icons/ri';

type ValidationInputProps = {
label?: string;
disabled?: boolean;
required?: boolean;
errors?: string;
register?: UseFormRegisterReturn;
Expand All @@ -42,6 +44,7 @@ type ValidationInputProps = {

export default function ValidationInput({
label,
disabled = false,
required = true,
errors,
register,
Expand Down Expand Up @@ -70,10 +73,11 @@ export default function ValidationInput({
<div
className={`flex h-30 items-center rounded-lg border px-6 text-sm ${
errors ? 'border-2 border-error' : 'border-input'
}`}
} ${disabled ? 'bg-disable' : ''}`}
>
<div className="flex h-full w-full flex-row items-center gap-8">
<input
disabled={disabled}
id={label}
{...register}
type={type === 'password' && showPassword ? 'text' : type}
Expand Down
262 changes: 261 additions & 1 deletion src/pages/setting/UserSettingPage.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,263 @@
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 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';

export default function UserSettingPage() {
return <div>UserSettingPage</div>;
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>({
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,
},
});

// 이미지 관련 코드
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;

// 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}`);
}
};

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>

{/* 아이디 */}
<ValidationInput
disabled
label="아이디"
required={false}
errors={errors.id?.message}
register={register('id', USER_AUTH_VALIDATION_RULES.ID)}
/>

{/* 이메일 */}
<ValidationInput
disabled
label="이메일"
required={false}
errors={errors.email?.message}
register={register('email', USER_AUTH_VALIDATION_RULES.EMAIL)}
/>

{/* 닉네임, 중복 확인 */}
<ValidationInput
label="닉네임"
required={false}
errors={errors.nickname?.message}
register={register('nickname', USER_AUTH_VALIDATION_RULES.NICKNAME)}
isButtonInput
buttonLabel="중복확인"
/>

{/* 자기소개 */}
<div className="flex flex-col">
<label htmlFor="bio" className="font-bold">
자기소개
</label>
<textarea
{...register('bio')}
id="bio"
placeholder="ex) 안녕하세요. 홍길동입니다."
className="h-90 grow resize-none rounded-lg border border-input p-8 text-sm outline-none placeholder:text-emphasis"
/>
</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>

{/* 개인정보 수정 버튼 */}
<div className="flex flex-col gap-8 text-center">
<button
type="submit"
className="relative flex h-30 items-center justify-center rounded-lg bg-sub px-8 font-bold"
disabled={isSubmitting}
>
<span>변경</span>
</button>
</div>
</form>
</div>
);
}

0 comments on commit a1a0143

Please sign in to comment.