Skip to content

Commit 67ee4b2

Browse files
authored
Merge pull request #304 from prgrms-web-devcourse-final-project/refactor/password-edit
[refactor] 비밀번호 변경 리펙토링
2 parents 4117938 + 3fb4e4d commit 67ee4b2

File tree

15 files changed

+246
-178
lines changed

15 files changed

+246
-178
lines changed

src/apis/user.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ export const getIdAvailability = async (loginId: string) => {
2828
};
2929

3030
//현재 비밀번호 확인
31-
export const checkPassword = async (currentPassword: string) => {
31+
export const checkPassword = async (password: string) => {
3232
const { data } = await axiosInstance.post(`/user/checkPassword`, {
33-
password: currentPassword,
33+
password,
3434
});
3535
return data;
3636
};
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Button } from '@/components/button';
2+
import { SpinLoading, ErrorShake, Complete } from '@/components/loading';
3+
import useDelayedLoading from '@/hooks/button/useDelayedLoading';
4+
import { cn } from '@/utils';
5+
6+
interface StatusButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
7+
isLoading: boolean;
8+
isSuccess: boolean;
9+
isError: boolean;
10+
text: string;
11+
disabled?: boolean;
12+
}
13+
14+
const StatusButton = ({
15+
isLoading,
16+
isSuccess,
17+
isError,
18+
text,
19+
disabled,
20+
className,
21+
...props
22+
}: StatusButtonProps) => {
23+
const showLoading = useDelayedLoading({ isLoading });
24+
25+
const variant = disabled ? 'disabled' : 'primary';
26+
27+
const renderContent = () => {
28+
if (showLoading) return <SpinLoading />;
29+
if (isSuccess) return <Complete />;
30+
if (isError) return <ErrorShake />;
31+
return <span>{text}</span>;
32+
};
33+
34+
return (
35+
<Button
36+
variant={variant}
37+
disabled={isLoading || disabled}
38+
className={cn(className, isError && 'bg-functional-danger')}
39+
{...props}
40+
>
41+
{renderContent()}
42+
</Button>
43+
);
44+
};
45+
46+
export default StatusButton;

src/components/button/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import Button from '@/components/button/Button';
2-
import LoadingSpinnerButton from '@/components/button/LoadingSpinnerButton';
3-
export { Button, LoadingSpinnerButton };
1+
export { default as Button } from './Button';
2+
export { default as LoadingSpinnerButton } from './LoadingSpinnerButton';
3+
export { default as StatusButton } from './StatusButton';

src/components/loading/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export { default as Complete } from './Complete';
2+
export { default as ErrorShake } from './ErrorShake';
3+
export { default as Loading } from './Loading';
4+
export { default as LoadingMini } from './LoadingMini';
5+
export { default as SpinLoading } from './SpinLoading';

src/pages/editprofile/EditProfile.tsx

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,19 @@
11
import { useState } from 'react';
2-
import ProfileEditForm from '@/pages/editprofile/components/ProfileEditForm';
3-
import PasswordEditForm from '@/pages/editprofile/components/PasswordEditForm';
4-
import { twMerge } from 'tailwind-merge';
52
import { AnimatePresence, motion } from 'framer-motion';
3+
import { TabMenu, ProfileEditForm, PasswordEditForm } from '@/pages/editprofile/components';
64

75
function EditProfile() {
8-
const [activeTab, setActiveTab] = useState('profile');
6+
const [activeTab, setActiveTab] = useState<'profile' | 'password'>('profile');
97

108
return (
11-
<div className="flex flex-col w-full pt-5 pb-10">
9+
<div className="flex flex-col w-full h-full pt-5 pb-10">
1210
{/* 탭 메뉴 */}
13-
<div className="relative flex">
14-
<button
15-
className={`p-3 flex-1 ${activeTab === 'profile' ? 'font-bold' : ''}`}
16-
onClick={() => setActiveTab('profile')}
17-
>
18-
프로필 수정
19-
</button>
20-
<button
21-
className={`p-3 flex-1 ${activeTab === 'password' ? 'font-bold' : ''}`}
22-
onClick={() => setActiveTab('password')}
23-
>
24-
비밀번호 변경
25-
</button>
26-
<div
27-
className={twMerge(
28-
'absolute bottom-0 h-[2px] w-1/2 bg-primary-active transition-all duration-300',
29-
activeTab === 'profile' ? 'left-0' : 'left-1/2',
30-
)}
31-
/>
32-
</div>
11+
<TabMenu activeTab={activeTab} onChange={setActiveTab} />
3312

3413
{/* 폼 */}
3514
<AnimatePresence mode="wait">
3615
<motion.div
16+
className="h-full w-full"
3717
key={activeTab} // 핵심! key를 다르게 주면 된다
3818
initial={{ opacity: 0 }}
3919
animate={{ opacity: 1 }}

src/pages/editprofile/components/CurrentPasswordInput.tsx

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,21 @@
1-
import InputField from '@/components/input/InputField';
2-
import { useState } from 'react';
1+
import { InputField } from '@/components/input';
32

43
interface CurrentPasswordInputPros {
5-
setCurrentPassword: (val: string) => void;
4+
setValidity: (val: boolean) => void;
65
}
76

8-
function CurrentPasswordInput({ setCurrentPassword }: CurrentPasswordInputPros) {
9-
const [text, setText] = useState('');
10-
11-
const handleonBlur = () => {
12-
setCurrentPassword(text);
7+
function CurrentPasswordInput({ setValidity }: CurrentPasswordInputPros) {
8+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
9+
setValidity(e.target.value.length > 0);
1310
};
14-
1511
return (
1612
<InputField
1713
type="password"
1814
id="current-password"
15+
name="current-password"
16+
onChange={handleChange}
1917
label="현재 비밀번호"
20-
placeholder="현재 비밀번호"
21-
value={text}
22-
onChange={(e) => setText(e.target.value)}
23-
onBlur={handleonBlur}
18+
placeholder="현재 비밀번호을 입력해 주세요"
2419
/>
2520
);
2621
}

src/pages/editprofile/components/PasswordEditForm.tsx

Lines changed: 88 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,94 @@
11
import { checkPassword, patchEditProfile } from '@/apis/user';
2-
import Button from '@/components/button/Button';
3-
import Complete from '@/components/loading/Complete';
4-
import ErrorShake from '@/components/loading/ErrorShake';
5-
import SpinLoading from '@/components/loading/SpinLoading';
6-
import CurrentPasswordInput from '@/pages/editprofile/components/CurrentPasswordInput';
7-
import PasswordConfirmInput from '@/pages/signup/components/PasswordConfirmInput';
8-
import PasswordInput from '@/pages/signup/components/PasswordInput';
2+
import { StatusButton } from '@/components/button';
3+
import { CurrentPasswordInput } from '@/pages/editprofile/components';
4+
import { PasswordGroupSection } from '@/pages/signup/components';
95
import { useModalStore } from '@/store/modalStore';
6+
import { useMutation } from '@tanstack/react-query';
107
import { useState } from 'react';
118
import { useNavigate } from 'react-router';
12-
import { twMerge } from 'tailwind-merge';
13-
14-
type ProfileFormType = {
15-
password: string;
16-
nickName: string;
17-
spotifyId: string;
18-
title: string;
19-
artist: string;
20-
albumImage: string;
21-
};
229

2310
function PasswordEditForm() {
2411
const navigate = useNavigate();
2512
const { openModal, closeModal } = useModalStore(); // 모달 관리
2613

27-
const [isLoading, setIsLoading] = useState(false);
28-
const [isComplete, setIsComplete] = useState(false);
29-
const [isError, setIsError] = useState(false);
30-
const [currentPassword, setCurrentPassword] = useState<string>('');
31-
const [newPassword, setNewPassword] = useState('');
3214
const [validity, setValidity] = useState({
15+
currentPassword: false,
3316
password: false,
34-
passwordConfirm: false,
3517
});
3618

37-
const validateCurrentPassword = async () => {
38-
//현재 비밀번호 확인 api
39-
try {
40-
const data = await checkPassword(currentPassword);
41-
if (data.code === 200) {
42-
return true;
43-
} else if (data.code === 400) {
44-
return false;
45-
}
46-
} catch {
47-
console.error('비밀번호 확인도중 에러가 발생했습니다.');
48-
return false;
49-
}
19+
const buttonEnabled = Object.values(validity).every(Boolean);
20+
21+
// validity 업데이트 함수
22+
const updateValidity = (key: 'currentPassword' | 'password', value: boolean) => {
23+
setValidity((prev) => {
24+
if (prev[key] === value) return prev; // 값이 동일하면 변경 X
25+
return { ...prev, [key]: value };
26+
});
5027
};
5128

52-
const handlePasswordSubmit = async () => {
53-
try {
54-
setIsLoading(true);
29+
// 현재 비밀번호 확인 useMutation
30+
const {
31+
mutateAsync: checkCurrentPassword,
32+
isPending: isCheckingPassword,
33+
isError: isCheckPasswordError,
34+
reset,
35+
} = useMutation({
36+
mutationFn: async (password: string) => {
37+
const res = await checkPassword(password);
38+
if (res.code !== 200) {
39+
throw new Error('비밀번호 틀림');
40+
}
41+
return res;
42+
},
43+
});
5544

56-
// 현재 비밀번호 확인
57-
const isCurrentPasswordValid = await validateCurrentPassword();
58-
if (!isCurrentPasswordValid) {
59-
openModal({
60-
title: '현재 비밀번호를 다시 확인해주세요',
61-
onConfirm: () => {
62-
closeModal();
63-
},
64-
});
65-
return;
45+
// 비밀번호 변경 useMutation
46+
const {
47+
mutateAsync: changePassword,
48+
isPending: isChangingPassword,
49+
isError: isChangePasswordError,
50+
isSuccess,
51+
} = useMutation({
52+
mutationFn: async (password: string) => {
53+
const res = await patchEditProfile({ password });
54+
if (res.code !== 200) {
55+
throw new Error('비밀번호 변경 실패');
6656
}
57+
return res;
58+
},
59+
});
6760

68-
const updatedData: Partial<ProfileFormType> = {
69-
password: newPassword,
70-
};
61+
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
62+
e.preventDefault();
63+
const formData = new FormData(e.currentTarget);
64+
const currentPassword = formData.get('current-password') as string;
65+
const password = formData.get('password') as string;
66+
// 현재 비밀번호 확인
67+
try {
68+
await checkCurrentPassword(currentPassword);
69+
} catch {
70+
openModal({
71+
title: '현재 비밀번호를 다시 확인해주세요',
72+
onConfirm: () => {
73+
reset();
74+
closeModal();
75+
},
76+
});
77+
return;
78+
}
7179

72-
// API 호출
73-
const data = await patchEditProfile(updatedData);
80+
// 비밀 번호 변경
81+
try {
82+
await changePassword(password);
7483

75-
if (data.code === 200) {
76-
setIsComplete(true);
77-
openModal({
78-
title: '비밀번호 변경이 완료되었습니다',
79-
onConfirm: () => {
80-
navigate('/mypage');
81-
closeModal();
82-
},
83-
});
84-
}
85-
} catch (error) {
86-
setIsError(true);
87-
console.error('비밀번호 변경 오류:', error);
84+
openModal({
85+
title: '비밀번호 변경이 완료되었습니다',
86+
onConfirm: () => {
87+
navigate('/mypage');
88+
closeModal();
89+
},
90+
});
91+
} catch {
8892
openModal({
8993
title: '비밀번호 변경 실패',
9094
message: '잠시 후 다시 시도해주세요.',
@@ -93,46 +97,29 @@ function PasswordEditForm() {
9397
navigate(-1);
9498
},
9599
});
96-
} finally {
97-
setIsLoading(false);
98100
}
99101
};
100102

101-
const isPasswordEditable = currentPassword && validity.password && validity.password;
102-
const renderButtonContent = () => {
103-
if (isLoading) {
104-
return <SpinLoading />;
105-
} else if (isComplete) {
106-
return <Complete />;
107-
} else if (isError) {
108-
return <ErrorShake />;
109-
} else return <span>저장하기</span>;
110-
};
111-
112103
return (
113-
<form className="flex flex-col justify-between h-full p-5" onSubmit={(e) => e.preventDefault()}>
114-
<div className="flex flex-col gap-5">
115-
<CurrentPasswordInput setCurrentPassword={setCurrentPassword} />
116-
<PasswordInput
117-
label="새 비밀번호"
118-
placeholder="새 비밀번호를 입력해 주세요"
119-
changeFormPassword={(password) => setNewPassword(password)}
120-
setValidity={(password) => setValidity((prev) => ({ ...prev, password }))}
121-
/>
122-
<PasswordConfirmInput
123-
label="새 비밀번호 확인"
124-
placeholder="새 비밀번호를 다시 입력해 주세요"
125-
setValidity={(passwordConfirm) => setValidity((prev) => ({ ...prev, passwordConfirm }))}
126-
password={newPassword}
104+
<form className="flex flex-col justify-between h-full p-5" onSubmit={handleSubmit}>
105+
<div className="flex flex-col">
106+
<CurrentPasswordInput setValidity={(value) => updateValidity('currentPassword', value)} />
107+
<PasswordGroupSection
108+
setValidity={(value) => updateValidity('password', value)}
109+
passwordLabel="새 비밀번호"
110+
confirmLabel="새 비밀번호 확인"
111+
passwordPlaceholder="새 비밀번호를 입력해 주세요"
112+
confirmPlaceholder="새 비밀번호를 다시 입력해 주세요"
127113
/>
128114
</div>
129-
<Button
130-
onClick={handlePasswordSubmit}
131-
variant={isPasswordEditable ? 'primary' : 'disabled'}
132-
className={twMerge('py-3 body-m mt-5', isError ? 'bg-functional-danger' : '')}
133-
>
134-
{renderButtonContent()}
135-
</Button>
115+
<StatusButton
116+
isLoading={isCheckingPassword || isChangingPassword}
117+
isSuccess={isSuccess}
118+
isError={isCheckPasswordError || isChangePasswordError}
119+
disabled={!buttonEnabled}
120+
type="submit"
121+
text="저장하기"
122+
/>
136123
</form>
137124
);
138125
}

src/pages/editprofile/components/ProfileEditForm.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,10 @@ function ProfileEditForm() {
182182
rightElement="button" // 오른쪽 요소 타입
183183
/>
184184
</div>
185-
<NicknameInput
186-
initialText={nickname}
187-
changeFormNickname={(nickname) => setNickname(nickname)}
188-
setValidity={(val) => setIsNicknameValid(val)}
189-
/>
185+
<NicknameInput initialText={nickname} setValidity={(val) => setIsNicknameValid(val)} />
190186
</div>
191187
<Button
188+
type="submit"
192189
variant={isProfileEditable ? 'primary' : 'disabled'}
193190
className={twMerge('py-3 body-m mt-5', isError ? 'bg-functional-danger' : '')}
194191
>

0 commit comments

Comments
 (0)