From ed7d385ebb00fa4150c0c4ffc8fcbbf1c4263cff Mon Sep 17 00:00:00 2001 From: hyeeuncho Date: Thu, 10 Jul 2025 17:09:55 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/join/api/user.ts | 15 +++-- src/features/join/hooks/useUserHook.ts | 25 +++++-- src/pages/join/InfoInputPage.tsx | 93 ++++++++++++++++++++++++-- src/shared/types/api/apiResponse.ts | 1 + src/shared/types/api/http-client.ts | 2 + 5 files changed, 118 insertions(+), 18 deletions(-) diff --git a/src/features/join/api/user.ts b/src/features/join/api/user.ts index a66ca26d..3a19f763 100644 --- a/src/features/join/api/user.ts +++ b/src/features/join/api/user.ts @@ -24,11 +24,16 @@ export const agreeTerms = async (data: TermsAgreementRequest) => { }; //인증번호 발급 -export const sendCertificationCode = async (phoneNum: string): Promise => { - 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 => { - await axiosClient.post('/sms/verify', { phoneNum, certificationCode }); +export const verifyCertificationCode = async (data: {phoneNumber: string, certificationCode: string}): Promise => { + const response = await axiosClient.post('/sms/verify', data, { + headers: { isPublicApi: true }, + }); + return response.data; }; \ No newline at end of file diff --git a/src/features/join/hooks/useUserHook.ts b/src/features/join/hooks/useUserHook.ts index f6780339..a5e3a489 100644 --- a/src/features/join/hooks/useUserHook.ts +++ b/src/features/join/hooks/useUserHook.ts @@ -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({ @@ -32,12 +33,17 @@ 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) => { + if (error.result) { + const allMessages = Object.values(error.result).join('\n'); + alert(allMessages); + } else { + alert(error.message || '인증번호 발송에 실패하였습니다.'); + } }, }); }; @@ -45,13 +51,18 @@ export const useSendCertificationCode = () => { // 인증번호 확인 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) => { + if (error.result) { + const allMessages = Object.values(error.result).join('\n'); + alert(allMessages); + } else { + alert(error.message || '인증에 실패하였습니다.'); + } }, }); } \ No newline at end of file diff --git a/src/pages/join/InfoInputPage.tsx b/src/pages/join/InfoInputPage.tsx index 2e85f2a9..f48d181d 100644 --- a/src/pages/join/InfoInputPage.tsx +++ b/src/pages/join/InfoInputPage.tsx @@ -1,11 +1,11 @@ -import { useEffect} from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useForm, SubmitHandler } from 'react-hook-form'; import Header from '../../../design-system/ui/Header'; import Button from '../../../design-system/ui/Button'; import UnderlineTextField from '../../../design-system/ui/textFields/UnderlineTextField'; import { FormData, zodValidation } from '../../shared/lib/formValidation'; -import { useAgreeTerms, useUserInfo, useUserUpdate } from '../../features/join/hooks/useUserHook'; +import { useAgreeTerms, useSendCertificationCode, useUserInfo, useUserUpdate, useVerifyCertificationCode } from '../../features/join/hooks/useUserHook'; import useAuthStore from '../../app/provider/authStore'; import { formatPhoneNumber } from '../../shared/utils/phoneFormatter'; import { useAgreementStore } from '../../features/join/model/agreementStore'; @@ -14,6 +14,9 @@ const InfoInputPage = () => { const { data, isLoading } = useUserInfo(); const { login, setName } = useAuthStore(); const { mutate: updateUser } = useUserUpdate(); + const { mutate: sendCode } = useSendCertificationCode(); + const { mutate: verifyCode } = useVerifyCertificationCode(); + const [isVerified, setIsVerified] = useState(false); const { register, @@ -34,6 +37,7 @@ const InfoInputPage = () => { const navigate = useNavigate(); const phoneValue = watch('phone'); + const { getAgreementStates } = useAgreementStore(); const { mutate: agreeTerms } = useAgreeTerms(); @@ -41,6 +45,52 @@ const InfoInputPage = () => { const formatted = formatPhoneNumber(e.target.value); setValue('phone', formatted, { shouldValidate: true }); }; + // 전화번호 인증 + 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); + } + }, [isVerifyVisible, timer]); + 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}`; + }; + const onSubmit: SubmitHandler = formData => { const agreementStates = getAgreementStates(); const updatedData = { @@ -74,8 +124,9 @@ const InfoInputPage = () => { }; useEffect(() => { if (data) { + const trimmedName = data.name ? data.name.slice(0, 10) : ''; reset({ - name: data.name || '', + name: trimmedName || '', email: data.email || '', phone: '', }); @@ -104,7 +155,7 @@ const InfoInputPage = () => { /> {/* 연락처 필드 */} -
+
{ onChange={handlePhoneChange} />
+
+ + {/* 인증번호 입력 필드 */} + {isVerifyVisible && ( +
+
+
+ setVerificationCode(e.target.value)} + className="text-xl" + label="" + /> +
+
+ 남은 시간: {formatTime(timer)} +
+ )} {/* 이메일 필드 */} {
@@ -141,4 +222,4 @@ const InfoInputPage = () => { ); }; -export default InfoInputPage; +export default InfoInputPage; \ No newline at end of file diff --git a/src/shared/types/api/apiResponse.ts b/src/shared/types/api/apiResponse.ts index 7273c8cb..d0d401bb 100644 --- a/src/shared/types/api/apiResponse.ts +++ b/src/shared/types/api/apiResponse.ts @@ -7,4 +7,5 @@ export interface ApiResponse { export interface ApiErrorResponse { code?: string; message?: string; + result?: Record; } diff --git a/src/shared/types/api/http-client.ts b/src/shared/types/api/http-client.ts index c8e50fdd..295e5e4a 100644 --- a/src/shared/types/api/http-client.ts +++ b/src/shared/types/api/http-client.ts @@ -56,10 +56,12 @@ axiosClient.interceptors.response.use( return response; }, async (error: AxiosError) => { + console.log(error) const errorInfo = { status: error.response?.status || 'NETWORK_ERROR', message: error.response?.data?.message || error.message, code: error.response?.data.code, + result: error.response?.data.result }; const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; From 03dd3b9346c484ca7998426cd5029c28a7bc8653 Mon Sep 17 00:00:00 2001 From: hyeeuncho Date: Thu, 10 Jul 2025 18:19:51 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design-system/ui/Button.tsx | 2 +- design-system/ui/buttons/TertiaryButton.tsx | 4 +- src/entities/user/ui/ProfileInfo.tsx | 158 ++++++++++++++++---- 3 files changed, 137 insertions(+), 27 deletions(-) diff --git a/design-system/ui/Button.tsx b/design-system/ui/Button.tsx index 527acedc..25949228 100644 --- a/design-system/ui/Button.tsx +++ b/design-system/ui/Button.tsx @@ -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}`} > diff --git a/design-system/ui/buttons/TertiaryButton.tsx b/design-system/ui/buttons/TertiaryButton.tsx index cdb3e236..478b109a 100644 --- a/design-system/ui/buttons/TertiaryButton.tsx +++ b/design-system/ui/buttons/TertiaryButton.tsx @@ -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 (