우선 로그인 회원가입 로직은 전부 내가 맡고 있었고
유저페이지를 맡고 있었다.
팀 내 MVP사정상 유저페이지가 아닌 설정페이지로 변경하여 분담하게 되었고
팀원이 쓰던 코드를 새로 분석하며 하루를 보낸 것 같다.
아직은 내가 아닌 팀원의 코드를 보는게 어색함ㅎㅎㅎ
import Header from '@/components/common/Header';
import React from 'react';
import UpdateNickName from '@/components/Setting/UpdateNickName';
import UpdateUserProfile from '@/components/Setting/UpdateUserProfile';
import UpdatePassword from '@/components/Setting/UpdatePassword';
import AccordianMenu from '@/components/common/AccordianMenu';
import Themes from '@/components/Setting/Themes';
import { getAuthToken, getUserSetting } from '@/api/user';
import { useQuery } from 'react-query';
import { useDispatch } from 'react-redux';
import { AUTH_USER, UserResponse } from '@/redux/reducers/userSlice';
import { SettingLayout, AccordianContent } from '@/styles/setting';
import Profile from '@/components/Setting/Profile';
import MobileBottomNav from '@/components/common/MobileBottomNav';
const Setting = () => {
const dispatch = useDispatch();
const { data: userData, isSuccess: tokenSuccess } = useQuery(
'user',
getAuthToken,
{
onSuccess: (userData: UserResponse) => {
dispatch(AUTH_USER(userData));
},
}
);
const { data: userSetting } = useQuery('userSetting', getUserSetting, {
onSuccess: (data) => {
console.log(data.user?.email);
},
});
return (
<>
<Header />
<SettingLayout>
<Profile
direction="column"
email={userSetting?.user.email}
userImage={userSetting?.user.userImage}
preset={userSetting?.user.preset}
nickname={userSetting?.user.nickname}
/>
{/* */}
<AccordianMenu tabText="닉네임 수정">
<AccordianContent>
<UpdateNickName nickname={userSetting?.user.nickname} />
</AccordianContent>
</AccordianMenu>
{/* */}
<AccordianMenu tabText="프로필 수정">
<AccordianContent>
<UpdateUserProfile
profileImage={userSetting?.user.userImage}
preset={userSetting?.user.preset}
/>
</AccordianContent>
</AccordianMenu>
{/* */}
<AccordianContent>
<Themes />
</AccordianContent>
{/* */}
<AccordianMenu tabText="비밀번호 수정">
<AccordianContent>
<UpdatePassword />
</AccordianContent>
</AccordianMenu>
<MobileBottomNav />
</SettingLayout>
</>
);
};
export default Setting;
- redux와 react-query 라이브러리를 사용해 사용자 정보를 가져오기.
- useQuery를 사용하여 getAuthToken과 getUserSetting API를 호출하고 사용자 데이터를 가져오기.
- 사용자 정보와 관련된 데이터를 가지고 있으면,
- Redux 스토어를 업데이트하고, dispatch(AUTH_USER(userData))를 호출.
- Setting 함수 컴포넌트는 다양한 서브컴포넌트 안고 있음,
- 또한 전반적인 레이아웃과 기능을 구성.
- 각각의 기능은 AccordianMenu와 AccordianContent 컴포넌트들로 레이아웃 구성.
import React, { useEffect, useState } from 'react';
import Image from 'next/image';
import { ProfileBox } from '@/styles/setting';
import { profilePreset } from '@/public/presetData';
type profileProps = {
userImage: string;
preset: number;
nickname: string;
email: string;
direction?: 'column' | 'row';
};
const Profile = ({
userImage,
preset,
nickname,
email,
direction,
}: profileProps) => {
const [profileImage, setProfileImage] = useState<string | undefined>();
useEffect(() => {
if (preset === 1) {
setProfileImage(userImage);
} else {
const foundPreset = profilePreset.find(
(presetElement, idx) => idx + 2 === preset
);
setProfileImage(foundPreset);
}
}, [preset]);
return (
<ProfileBox direction={direction}>
<div className="image">
{profileImage && (
<Image
src={profileImage}
alt={'image'}
width={74}
height={74}
objectFit="cover"
/>
)}
</div>
<div>
<h2>{nickname}</h2>
<span></span>
<p>{email}</p>
</div>
</ProfileBox>
);
};
export default Profile;
-Profile 컴포넌트는 `userImage`, `preset`, `nickname`, `email` 및 `direction` 속성을 받음.
-direction`은 선택적 속성으로 'column' 또는 'row' 값을 가질 수 있음.
-useState 훅을 사용하여 profileImage 상태를 관리.
-상태 초기값은 `undefined`이며, 이후 `useEffect`를 통해 변경.
-useEffect 훅을 사용하여 `preset` 값에 따라 프로필 이미지를 설정합니다.
- preset이 1인 경우, `userImage`를 사용.
- 그렇지 않은 경우, `profilePreset` 배열에서 해당하는 `preset` 이미지를 찾아 `profileImage`를 설정.
-반환된 JSX에서, ProfileBox 스타일 컴포넌트는 프로필 이미지 (`next/image` 사용)와 사용자 정보 (닉네임, 이메일)를 표시.
-레이아웃은 `direction` 속성에 따라 변경
import React, { FormEvent, useState } from 'react';
import useInput from '@/hooks/useInput';
import { NextPage } from 'next';
import Input from '@/components/Setting/Input';
import Button from '@/components/common/Button';
import { useMutation, useQueryClient } from 'react-query';
import { updateUserSetting } from '@/api/user';
import { TextErrorParagraph } from '@/styles/signUp';
type settingProps = {
nickname?: string;
};
const UpdateNickName: NextPage<settingProps> = ({ nickname }) => {
// console.log('리렌더링');
const [nickName, onClickNickName, setNickName] = useInput<string | undefined>(
nickname
);
const [error, setError] = useState<string>('');
const queryClient = useQueryClient();
const mutate = useMutation(
(formData: FormData) => updateUserSetting(formData),
{
onSuccess: () => {
queryClient.invalidateQueries('userSetting');
},
}
);
const validateNickname = (nickname: string) => {
const nicknamePattern = /^.{2,15}$/;
return nicknamePattern.test(nickname);
};
const onSubmitNickNameHandler = async (e: FormEvent) => {
e.preventDefault();
let errors: string = '';
if (!nickName) {
errors = '닉네임을 입력해주세요.';
} else if (!validateNickname(nickName)) {
errors = '2~15자를 입력해주세요.';
}
if (errors !== '') {
setError(errors);
return;
} else {
setError('');
}
if (errors === '') {
const formData = new FormData();
formData.append('nickname', nickName ?? '');
await mutate.mutateAsync(formData);
console.log(nickName);
}
};
return (
<form onSubmit={onSubmitNickNameHandler}>
<Input
value={nickName}
placeholder="닉네임 2자에서 15자 입력해주세요"
onChange={onClickNickName}
/>
{error && <TextErrorParagraph>{error}</TextErrorParagraph>}
<Button type="submit" marginTop={'-12px'}>
닉네임변경하기
</Button>
</form>
);
};
export default UpdateNickName;
UpdateNickName 컴포넌트는 선택적 nickname 속성을 받음
useInput 훅을 사용하여 닉네임 입력 처리를 관리하고, useState를 사용하여 입력 에러 메시지를 관리.
react-query의 useMutation과 useQueryClient를 사용하여
updateUserSetting API를 호출하는 데 필요한 mutation 및 쿼리 클라이언트를 구성.
validateNickname 함수를 사용하여 입력된 닉네임이 유효한지 확인.
onSubmitNickNameHandler 함수는 폼 제출 시 호출,
입력된 닉네임에 대한 유효성 검사를 수행한 다음,
에러 메시지를 설정하거나 변경을 적용.
이 때, FormData 객체를 사용하여 닉네임이 포함된 폼 데이터를 작성하고,
mutate.mutateAsync 함수를 호출하여 변경사항을 서버로 전송.
반환된 JSX에서, form태그 내에 Input, 에러 메시지 및 Button 컴포넌트가 포함.
이를 통해 닉네임 입력, 유효성 검사 결과 표시, 제출 버튼 기능을 포함하는 닉네임 수정 폼을 구성.
import React, { ChangeEvent, FormEvent, useState } from 'react';
import Image from 'next/image';
import { useMutation, useQueryClient } from 'react-query';
import Button from '@/components/common/Button';
import { updateUserSetting } from '@/api/user';
import { ProfilePresetList, ProfileItem, ButtonBox } from '@/styles/setting';
import { profilePreset } from '@/public/presetData';
const profileData = ['/inflearn.jpg', '/inflearn.jpg', '/inflearn.jpg'];
type profileType = {
profileImage?: string;
preset?: number;
};
const UserProfileImageUpdate = ({ profileImage, preset }: profileType) => {
const [selectPreset, setSelectPreset] = useState<number>(preset ? preset : 5);
const [userProfile, setUserProfile] = useState<any>(profileImage);
const onClickProfileHandler = (idx: number) => {
setSelectPreset(idx + 1);
};
const queryClient = useQueryClient();
const mutate = useMutation(
(formData: FormData) => updateUserSetting(formData),
{
onSuccess: () => {
alert('수정되었습니다.');
queryClient.invalidateQueries('userSetting');
},
}
);
const onChangeProfileUpdate = async (
event: ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files;
const imageData = new FormData();
if (file) {
const reader = new FileReader();
reader.readAsDataURL(file[0]);
reader.onloadend = () => {
setUserProfile(reader.result as string);
};
imageData.append('image', file[0]);
imageData.append('preset', '1');
await mutate.mutateAsync(imageData);
console.log('업로드');
}
};
const presetMutate = useMutation(
(formData: FormData) => updateUserSetting(formData),
{
onSuccess: () => {
alert('수정되었습니다.');
queryClient.invalidateQueries('userSetting');
},
}
);
const onSubmitUserProfile = async (e: FormEvent) => {
e.preventDefault();
const presetData = new FormData();
presetData.append('preset', selectPreset.toString());
await presetMutate.mutateAsync(presetData);
};
return (
<>
<form action="" onSubmit={onSubmitUserProfile}>
<ProfilePresetList>
<ProfileItem onClick={() => onClickProfileHandler(0)}>
<figure className={selectPreset === 1 ? 'active' : ''}>
<Image
src={userProfile ? userProfile : '/inflearn.jpg'}
alt="preset"
width={100}
height={100}
quality={100}
/>
</figure>
</ProfileItem>
{profilePreset.map((profile, idx) => {
return (
<ProfileItem
key={idx}
onClick={() => onClickProfileHandler(idx + 1)}
>
<figure className={selectPreset === idx + 2 ? 'active' : ''}>
<Image
src={profile}
alt="preset"
width={100}
height={100}
quality={100}
/>
</figure>
</ProfileItem>
);
})}
{/* <ProfileItem onClick={() => onClickProfileHandler(4)}>
<figure className={selectPreset === 5 ? 'active' : ''}>
<Image src={'/addImage.png'} alt="preset" fill />
</figure>
</ProfileItem> */}
</ProfilePresetList>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<ButtonBox>
<label htmlFor="profile" className="profile-button">
프로필 업로드
</label>
<input id="profile" type="file" onChange={onChangeProfileUpdate} />
<Button marginTop={'0'} type="submit" className="profile-button">
프로필 변경
</Button>
</ButtonBox>
</div>
</form>
</>
);
};
export default UserProfileImageUpdate;
profileType이라는 타입을 만들어서 prop의 유형을 정의.
UserProfileImageUpdate 컴포넌트에서는 profileImage와 preset이라는 prop을 받음.
또한 imgae 업데이트와 프로필 선택을 위한 상태를 선언하고 관리.
프로필 선택에 사용되는 onClickProfileHandler 함수를 만들어 프로필 선택에 따라 선택된 프로필을 표시.
react-query의 useMutation과 useQueryClient를 사용하여 업데이트를 위한 API 호출을 준비하고, 프로필 업로드와 프리셋 변경에 대한 뮤테이션(mutation)을 설정.
입력으로 받은 이미지에 대한 이벤트 리스너를 통해 이미지가 선택 될 때마다,
onChangeProfileUpdate 함수를 호출하여 프로필 이미지를 변경하거나 업데이트.
onSubmitUserProfile 함수를 통해 전체 폼을 제출할 때 프로필 이미지 및 선택한 프리셋이 함께 업데이트.
반환된 JSX에서는 프로필 이미지 및 프리셋 선택을 위한 프로필 목록을 렌더링하고, 파일 업로드와 프로필 변경 버튼을 포함하는 버튼 박스를 구성.
import React, { useState } from 'react';
import styled from 'styled-components';
import Button from '@/components/common/Button';
import {
StyledInput,
TextErrorParagraph,
TextParagraphSns,
} from '@/styles/signUp';
import { useMutation, useQueryClient } from 'react-query';
import { updateUserSetting } from '@/api/user';
import { TextParagraph } from '@/styles/signIn';
import { TextParagraphPwdCheck } from '@/styles/setting';
export interface Signup {
password: string;
passwordConfirm: string;
}
export interface CheckBoxInterface {
checkAll: boolean;
checkTerms: boolean;
checkPersonalInfo: boolean;
checkNewsletter: boolean;
}
type TextInputType = 'email' | 'password' | 'passwordConfirm' | 'nickname';
const InputBox = styled.div`
width: 100%;
/* margin-bottom: 32px; */
`;
const UpdatePassword = () => {
const [signUpState, setSignUpState] = useState<Signup>({
password: '',
passwordConfirm: '',
});
const [error, setError] = useState<Signup>({
password: '',
passwordConfirm: '',
});
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement & { name: TextInputType }>
) => {
const { name, value } = e.target;
setSignUpState((prevSignUpState) => ({
...prevSignUpState,
[name]: value,
}));
};
const validatePassword = (password: string) => {
const passwordPattern = /^(?=.*\d)(?=.*[a-zA-Z]).{8,}$/;
return passwordPattern.test(password);
};
const queryClient = useQueryClient();
const mutate = useMutation(
(formData: FormData) => updateUserSetting(formData),
{
onSuccess: () => {
queryClient.invalidateQueries('userSetting');
},
}
);
const submitUser = async (event: any) => {
event.preventDefault();
let errors: any = {};
if (!signUpState.password) {
errors.password = '비밀번호를 입력해주세요.';
} else if (!validatePassword(signUpState.password)) {
errors.password =
'비밀번호는 영문, 숫자를 포함하여 8자 이상이어야합니다.';
}
if (signUpState.password !== signUpState.passwordConfirm) {
errors.passwordConfirm = '비밀번호가 일치하지 않습니다.';
}
if (Object.keys(errors).length > 0) {
setError(errors);
return;
} else {
setError({ password: '', passwordConfirm: '' });
}
if (Object.keys(errors).length === 0) {
const formData = new FormData();
formData.append('password', signUpState.password);
await mutate.mutateAsync(formData);
}
};
return (
<>
<form action="" onSubmit={submitUser}>
<InputBox>
<TextParagraph>비밀번호</TextParagraph>
<TextParagraphPwdCheck>
영문, 숫자를 포함한 8자 이상의 비밀번호를 입력해주세요.
</TextParagraphPwdCheck>
<StyledInput
type="password"
name="password"
value={signUpState.password}
placeholder="비밀번호를 입력해 주세요"
onChange={handleInputChange}
/>
{error.password && (
<TextErrorParagraph>{error.password}</TextErrorParagraph>
)}
<TextParagraph style={{ margin: '10px 0 7px 0' }}>
비밀번호 확인
</TextParagraph>
<StyledInput
type="password"
name="passwordConfirm"
value={signUpState.passwordConfirm}
placeholder="비밀번호 확인"
onChange={handleInputChange}
/>
{error.passwordConfirm && (
<TextErrorParagraph>{error.passwordConfirm}</TextErrorParagraph>
)}
</InputBox>
<Button type="submit" marginTop={'10px'}>
비밀번호 수정
</Button>
</form>
</>
);
};
export default UpdatePassword;
컴포넌트 내에서는 useState를 사용하여 두 개의 state를 정의.
첫 번째 state는 SignUp 타입을 가지며, password와 passwordConfirm 두 개의 속성을 가지고 있습니다. 두 번째 state는 error 타입을 가지며, invalid한 비밀번호 정보에 대한 에러메시지를 가지고 있음
handleInputChange 함수는 입력창이 변경되었을 때 호출되며, 입력창의 name과 value를 이용하여 signupState를 업데이트.
validatePassword 함수에서는 영문과 숫자를 모두 포함하며, 최소 8자 이상인 비밀번호가 유효한지 검사.
useQueryClient를 통해 queryClient를 가져온 후, useMutation을 통해 비동기로 비밀번호를 업데이트 할 수 있는 뮤테이션을 생성합니다. onSuccess를 통해 업데이트 성공 시 userSetting 쿼리를 재요청하도록 설정.
submitUser 함수에서는 비밀번호가 올바른 형식인지 검사
검가사 실패한 경우, setError를 호출하여 에러메시지를 표시.
올바른 형식일 경우, 사용자의 비밀번호를 업데이트하도록 서버로 요청을 보내고,
뮤테이션을 호출하여 userSetting 쿼리를 재요청.
반환된 JSX에서는 입력창과 에러메시지를 표시하고, 수정 버튼을 클릭하면 submitUser 함수가 호출되도록 처리.
헤삭하는 데 시간이 조금 걸린 것 같아..ㅠㅠ
그래도 로그인 할때 사용했던 것을 재사용 하여서 생각보다 이해가 쉬웠고
리덕스 데브툴이나 쿼리데브툴을 사용하며 최대한 빨리 녹이려고 노력한 것 같다.
css부분도 거의 끝내 놨으니 나중에 2차MVP를 하면서 필요한 부분을 수정하면
충분할듯 함!
'WebDev > 항해99' 카테고리의 다른 글
스타일컴포넌트/타입스크립트 사용 - 프랍 시 $ or as prop (0) | 2023.08.19 |
---|---|
.stopPropagation() (0) | 2023.08.18 |
실전프로젝트 로그인 로직 정리 (0) | 2023.08.07 |
실전프로젝트 1주차 회고 (0) | 2023.08.06 |
Async/Await와 Promise의 차이점 (0) | 2023.08.04 |