Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion design-system/ui/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const Button = ({ label, onClick, disabled = false, className = '', type = 'subm
type={type}
onClick={onClick}
disabled={disabled}
className={`py-2 px-4 text-white font-semibold transition text-base sm:text-xs md:text-sm lg:text-base
className={`inline-flex items-center justify-center py-2 px-4 sm:px-2 md:px-4 text-white font-semibold transition text-base sm:text-xs md:text-sm lg:text-base
${disabled ? 'bg-gray-400 cursor-not-allowed' : 'bg-main hover:bg-mainDark'}
${className}`}
>
Expand Down
4 changes: 3 additions & 1 deletion design-system/ui/buttons/TertiaryButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ const TertiaryButton = ({ label, type, color, size, disabled, onClick, className
? 'border-main text-main hover:bg-main hover:text-white hover:font-bold'
: 'border-black text-black hover:bg-black hover:text-white hover:font-bold';

const disabledStyle = 'bg-gray-200 text-gray-400 border-gray-200 cursor-not-allowed';

return (
<button
type={type}
disabled={disabled}
className={`${baseStyle} ${sizeClasses[size]} ${colorStyle} ${className}`}
className={`${baseStyle} ${sizeClasses[size]} ${disabled ? disabledStyle : colorStyle} ${className}`}
onClick={onClick}
>
{label}
Expand Down
158 changes: 133 additions & 25 deletions src/entities/user/ui/ProfileInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import ProfileCircle from '../../../../design-system/ui/Profile';
import TertiaryButton from '../../../../design-system/ui/buttons/TertiaryButton';
import DefaultTextField from '../../../../design-system/ui/textFields/DefaultTextField';
import useAuthStore from '../../../app/provider/authStore';
import { useUserInfo, useUserUpdate } from '../../../features/join/hooks/useUserHook';
import { useSendCertificationCode, useUserInfo, useUserUpdate, useVerifyCertificationCode } from '../../../features/join/hooks/useUserHook';
import { formatProfilName } from '../../../shared/lib/formatProfileName';
import Button from '../../../../design-system/ui/Button';

const ProfileInfo = () => {
const isLoggedIn = useAuthStore(state => state.isLoggedIn);
Expand All @@ -17,16 +18,23 @@ const ProfileInfo = () => {
const { mutate: updateUser } = useUserUpdate();
const [isEditing, setIsEditing] = useState(false);

const { mutate: sendCode } = useSendCertificationCode();
const { mutate: verifyCode } = useVerifyCertificationCode();
const [isVerified, setIsVerified] = useState(false);

const {
register,
handleSubmit,
formState: { errors },
setValue,
watch
} = useForm<{ name: string; phone: string }>({
defaultValues: { name: data?.name || '', phone: data?.phoneNumber || '' },
resolver: zodResolver(myPageSchema),
});

const phoneValue = watch('phone');

useEffect(() => {
if (data) {
setValue('name', data.name);
Expand Down Expand Up @@ -58,20 +66,64 @@ const ProfileInfo = () => {
setName(name);
refetch();
setIsEditing(false);
setIsVerified(false);
alert('정보가 성공적으로 업데이트 되었습니다.')
},
onError: () => {
alert('정보 업데이트에 실패했습니다. 다시 시도해주세요.');
},
});
};

// 전화번호 인증
const [isVerifyVisible, setIsVerifyVisible] = useState(true);
const [verificationCode, setVerificationCode] = useState('');
const [timer, setTimer] = useState(0);
//인증번호 발급
const handlePhoneVerifyClick = () => {
if (!phoneValue) {
alert('연락처를 입력해주세요.');
return;
}
// //인증api호출
sendCode({ phoneNumber: phoneValue }, {
onSuccess: () => {
setIsVerifyVisible(true);
setTimer(180);
}
});
};
// 인증번호 확인
const handleVerifySubmit = () => {
if (!verificationCode || verificationCode.length !== 6) {
alert('6자리 인증번호를 입력해주세요.');
return;
}
verifyCode({ phoneNumber: phoneValue, certificationCode: verificationCode }, {
onSuccess: () => {
setIsVerifyVisible(false);
setIsVerified(true);
}
});
};
useEffect(() => {
if (isVerifyVisible && timer > 0) {
const interval = setInterval(() => setTimer((prev) => prev - 1), 1000);
return () => clearInterval(interval);
}
}, [timer, isVerifyVisible]);
const formatTime = (seconds: number) => {
const m = String(Math.floor(seconds / 60)).padStart(2, '0');
const s = String(seconds % 60).padStart(2, '0');
return `${m}:${s}`;
};

if (isLoading) {
return <div>로딩 중...</div>;
}
if (error) {
return <div>정보를 불러오는데 실패했습니다. 다시 시도해주세요.</div>;
}

return (
<div className="relative w-full h-52 md:h-56">
<div className="absolute inset-0 bg-main rounded-[10px]" />
Expand Down Expand Up @@ -107,42 +159,98 @@ const ProfileInfo = () => {
name={formatProfilName(data?.name || '')}
className="w-16 h-16 md:w-18 md:h-18 text-xl md:text-2xl"
/>
<div className="flex flex-col gap-1">
<div className="flex flex-col gap-1 flex-1">
<DefaultTextField
{...register('name')}
errorPosition="right"
errorPosition="bottom"
errorMessage={errors.name?.message}
className="h-9"
/>
<DefaultTextField
{...register('phone')}
onChange={handlePhoneChange}
errorPosition="right"
errorMessage={errors.phone?.message}
className="h-9"
/>
<div className="flex gap-2 items-center">
<DefaultTextField
{...register('phone')}
onChange={handlePhoneChange}
className="h-9 flex-1"
/>
<Button
type="button"
label="인증하기"
onClick={handlePhoneVerifyClick}
className="h-9 sm:h-8 rounded-md w-24"
/>
</div>

{isVerifyVisible && (
<div className="mt-3 p-4 bg-white rounded-md shadow-md border border-gray-300">
<div className="flex gap-2 mb-2">
<DefaultTextField
placeholder="인증번호 6자리"
value={verificationCode}
onChange={(e) => setVerificationCode(e.target.value)}
className="h-9 flex-1"
/>
<Button
type="button"
label="확인"
onClick={handleVerifySubmit}
className="h-9 px-3 rounded-md"
/>
</div>
<span className="text-xs text-gray-500 pl-1 mb-3 block">
남은 시간: {formatTime(timer)}
</span>
<TertiaryButton
label="취소하기"
type="button"
color="pink"
size="full"
onClick={() => {
setIsEditing(false);
setValue('name', data?.name || '');
setValue('phone', data?.phoneNumber || '');
setIsVerified(false);
setIsVerifyVisible(false);
setVerificationCode('');
}}
/>
</div>
)}

{!isVerifyVisible && (
<div className="flex gap-2 mt-2">
<TertiaryButton
label="취소하기"
type="button"
color="pink"
size="full"
onClick={() => {
setIsEditing(false);
setValue('name', data?.name || '');
setValue('phone', data?.phoneNumber || '');
setIsVerified(false);
setVerificationCode('');
}}
/>
<TertiaryButton
label="수정하기"
type="submit"
color="pink"
size="full"
disabled={
phoneValue !== data?.phoneNumber && !isVerified
}
/>
</div>
)}
</div>
</div>
<div className="flex gap-2">
<TertiaryButton
label="취소하기"
type="button"
color="pink"
size="full"
onClick={() => {
setIsEditing(false);
setValue('name', data?.name || '');
setValue('phone', data?.phoneNumber || '');
}}
/>
<TertiaryButton label="수정하기" type="submit" color="pink" size="full" />
</div>
</form>
)}
</div>
</div>
</div>
);

};

export default ProfileInfo;
15 changes: 10 additions & 5 deletions src/features/join/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ export const agreeTerms = async (data: TermsAgreementRequest) => {
};

//인증번호 발급
export const sendCertificationCode = async (phoneNum: string): Promise<void> => {
await axiosClient.post('/sms/send', { phoneNum });
export const sendCertificationCode = async (data: { phoneNumber: string }) => {
const response = await axiosClient.post('/sms/send', data, {
headers: { isPublicApi: true },
});
return response.data;
};

//인증번호 검증
export const verifyCertificationCode = async (phoneNum: string, certificationCode: string): Promise<void> => {
await axiosClient.post('/sms/verify', { phoneNum, certificationCode });
export const verifyCertificationCode = async (data: {phoneNumber: string, certificationCode: string}): Promise<void> => {
const response = await axiosClient.post('/sms/verify', data, {
headers: { isPublicApi: true },
});
return response.data;
};
25 changes: 18 additions & 7 deletions src/features/join/hooks/useUserHook.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { agreeTerms, readUser, sendCertificationCode, updateUser, verifyCertificationCode } from '../api/user';
import { UserInfoRequest, UserInfoResponse } from '../model/userInformation';
import { AxiosError } from 'axios';

export const useUserInfo = (enabled: boolean = true) => {
return useQuery<UserInfoResponse>({
Expand Down Expand Up @@ -32,26 +33,36 @@ export const useAgreeTerms = () => {
// 인증번호 발급
export const useSendCertificationCode = () => {
return useMutation({
mutationFn: (phoneNum: string) => sendCertificationCode(phoneNum),
mutationFn: (data: { phoneNumber: string }) => sendCertificationCode(data),
onSuccess: () => {
alert('인증번호를 발송했습니다.');
},
onError: () => {
alert('인증번호 전송에 실패했습니다.');
onError: (error: AxiosError<any>) => {
if (error.result) {
const allMessages = Object.values(error.result).join('\n');
alert(allMessages);
} else {
alert(error.message || '인증번호 발송에 실패하였습니다.');
}
},
});
};

// 인증번호 확인
export const useVerifyCertificationCode = () => {
return useMutation({
mutationFn: (params: { phoneNum: string; certificationCode: string }) =>
verifyCertificationCode(params.phoneNum, params.certificationCode),
mutationFn: (params: { phoneNumber: string; certificationCode: string }) =>
verifyCertificationCode(params),
onSuccess: () => {
alert('인증에 성공했습니다.');
},
onError: () => {
alert('인증번호가 일치하지 않습니다.');
onError: (error: AxiosError<any>) => {
if (error.result) {
const allMessages = Object.values(error.result).join('\n');
alert(allMessages);
} else {
alert(error.message || '인증에 실패하였습니다.');
}
},
});
}
Loading