Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions app/(landing)/signup/_api/check-duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import axios from 'axios'

export const checkNicknameDuplicate = async (nickname: string) => {
try {
const response = await axios.post(`/api/users/check/nickname?nickname=${nickname}`)
const response = await axios.get(`/api/users/check/nickname?nickname=${nickname}`)
return response.data
} catch (err) {
console.error(err)
Expand All @@ -12,7 +12,7 @@ export const checkNicknameDuplicate = async (nickname: string) => {

export const checkPhoneDuplicate = async (phone: string) => {
try {
const response = await axios.post(`/api/users/check/phone?phone=${phone}`)
const response = await axios.get(`/api/users/check/phone?phone=${phone}`)
return response.data
} catch (err) {
console.error(err)
Expand Down
8 changes: 8 additions & 0 deletions app/(landing)/signup/_constants/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export const SIGNUP_ERROR_MESSAGES = {
NICKNAME_LENGTH: '닉네임은 2글자 이상 10글자 이하로 입력해주세요.',
NICKNAME_CHECK_REQUIRED: '닉네임 중복확인이 필요합니다.',
NICKNAME_DUPLICATED: '이미 사용 중인 닉네임입니다.',
NICKNAME_CHECK_FAILED: '닉네임 중복 확인에 실패했습니다.',
NAME_MIN_LENGTH: '이름을 2자 이상 입력해주세요.',
PASSWORD_REQUIRED: '비밀번호를 입력해주세요.',
PASSWORD_INVALID: '비밀번호가 올바르지 않습니다.',
Expand All @@ -15,4 +16,11 @@ export const SIGNUP_ERROR_MESSAGES = {
PHONE_REQUIRED: '휴대폰 번호를 입력해주세요.',
PHONE_INVALID: '올바른 휴대폰 번호를 입력해주세요.',
PHONE_DUPLICATED: '이미 사용 중인 휴대폰 번호입니다.',
PHONE_CHECK_REQUIRED: '휴대폰 번호 인증이 필요합니다.',
PHONE_CHECK_FAILED: '휴대폰 번호 중복 확인에 실패했습니다.',
VERIFICATION_CODE_REQUIRED: '인증번호를 입력해주세요.',
VERIFICATION_CODE_MISMATCH: '인증번호가 일치하지 않습니다.',
VERIFICATION_CODE_CHECK_FAILED: '인증번호 확인에 실패했습니다.',
EMAIL_SEND_FAILED: '이메일 발송에 실패했습니다.',
SELECT_REQUIRED: '생년월일을 모두 입력해주세요.',
} as const
48 changes: 38 additions & 10 deletions app/(landing)/signup/_hooks/custom/use-signup-email.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Dispatch, SetStateAction, useState } from 'react'

import { checkEmailDuplicate } from '../../_api/check-duplicate'
import { getEmailAuthenticationResult, requestEmailAuthentication } from '../../_api/email-auth'
import { SIGNUP_ERROR_MESSAGES } from '../../_constants/signup'
import {
SignupFormErrorsModel,
SignupFormModel,
SignupFormStateModel,
} from './../../information/types'

interface Props {
form: SignupFormModel
errors: SignupFormErrorsModel
isValidated: boolean
setForm: Dispatch<SetStateAction<SignupFormModel>>
setErrors: Dispatch<SetStateAction<SignupFormErrorsModel>>
setFormState: Dispatch<SetStateAction<SignupFormStateModel>>
}

const useSignupEmail = ({ errors, isValidated, setForm, setErrors, setFormState }: Props) => {
const useSignupEmail = ({ form, errors, isValidated, setForm, setErrors, setFormState }: Props) => {
const [selectedDomain, setSelectedDomain] = useState<string>('')

const handleEmailChange = (name: string) => {
Expand Down Expand Up @@ -48,23 +52,47 @@ const useSignupEmail = ({ errors, isValidated, setForm, setErrors, setFormState

const handleEmailVerification = async () => {
try {
// TODO: 이메일 인증 API 호출
const response = await checkEmailDuplicate(form.email, form.emailDomain)

if (!response.result.isAvailable) {
setErrors((prev) => ({ ...prev, email: SIGNUP_ERROR_MESSAGES.EMAIL_DUPLICATED }))
return
}

await requestEmailAuthentication(form.email, form.emailDomain)
setFormState((prev) => ({ ...prev, isEmailSent: true }))
} catch (error) {
console.error('이메일 인증 발송 실패:', error)

if (errors.email) {
setErrors((prev) => ({ ...prev, email: null }))
}
} catch (err) {
console.error('이메일 인증 발송 실패:', err)
setErrors((prev) => ({ ...prev, email: SIGNUP_ERROR_MESSAGES.EMAIL_SEND_FAILED }))
}
}

const handleVerificationCodeCheck = async () => {
try {
// TODO: 인증번호 확인 API 호출
setFormState((prev) => ({ ...prev, isEmailVerified: true }))
const response = await getEmailAuthenticationResult(
form.email,
form.emailDomain,
form.verificationCode
)

if (errors.email) {
setErrors((prev) => ({ ...prev, email: null }))
if (response.result.isVerified) {
setFormState((prev) => ({ ...prev, isEmailVerified: true }))
if (errors.email) {
setErrors((prev) => ({ ...prev, email: null }))
}
} else {
setErrors((prev) => ({ ...prev, email: SIGNUP_ERROR_MESSAGES.VERIFICATION_CODE_MISMATCH }))
}
} catch (error) {
console.error('인증번호 확인 실패:', error)
} catch (err) {
console.error('인증번호 확인 실패:', err)
setErrors((prev) => ({
...prev,
email: SIGNUP_ERROR_MESSAGES.VERIFICATION_CODE_CHECK_FAILED,
}))
}
}

Expand Down
62 changes: 48 additions & 14 deletions app/(landing)/signup/_hooks/custom/use-signup-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { useRouter } from 'next/navigation'

import { PATH } from '@/shared/constants/path'

import { checkNicknameDuplicate, checkPhoneDuplicate } from '../../_api/check-duplicate'
import { signup } from '../../_api/signup'
import { SIGNUP_ERROR_MESSAGES } from '../../_constants/signup'
import { getUserTypeCookie, setNicknameCookie } from '../../_lib/cookies'
import {
SignupFormDataModel,
SignupFormErrorsModel,
SignupFormModel,
SignupFormStateModel,
Expand Down Expand Up @@ -65,31 +70,40 @@ const useSignupForm = () => {

const handleNicknameCheck = async () => {
try {
// TODO: 닉네임 중복 확인 API 호출
setFormState((prev) => ({ ...prev, isNicknameVerified: true }))

if (errors.nickname) {
setErrors((prev) => ({ ...prev, nickname: null }))
const response = await checkNicknameDuplicate(form.nickname)
if (response.result.isAvailable) {
setFormState((prev) => ({ ...prev, isNicknameVerified: true }))
if (errors.nickname) {
setErrors((prev) => ({ ...prev, nickname: null }))
}
} else {
setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_DUPLICATED }))
}
} catch (err) {
console.error('닉네임 중복 확인 실패:', err)
setErrors((prev) => ({ ...prev, nickname: SIGNUP_ERROR_MESSAGES.NICKNAME_CHECK_FAILED }))
}
}

const handlePhoneCheck = async () => {
try {
// TODO: 휴대폰 번호 중복 확인 API 호출
setFormState((prev) => ({ ...prev, isPhoneVerified: true }))

if (errors.phone) {
setErrors((prev) => ({ ...prev, phone: null }))
const response = await checkPhoneDuplicate(form.phone)

if (response.result.isAvailable) {
setFormState((prev) => ({ ...prev, isPhoneVerified: true }))
if (errors.phone) {
setErrors((prev) => ({ ...prev, phone: null }))
}
} else {
setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_DUPLICATED }))
}
} catch (err) {
console.error('휴대폰 번호 중복 확인 실패:', err)
setErrors((prev) => ({ ...prev, phone: SIGNUP_ERROR_MESSAGES.PHONE_CHECK_FAILED }))
}
}

const handleFormSubmit = () => {
const handleFormSubmit = async () => {
const formErrors = validateSignupForm(
form,
formState.isEmailVerified,
Expand All @@ -101,10 +115,30 @@ const useSignupForm = () => {
setErrors(formErrors)
return
}
const role = getUserTypeCookie()

// TODO: 회원가입 API 호출
console.log('폼 제출', form)
router.push(PATH.SIGN_UP_COMPLETE)
try {
const formData: SignupFormDataModel = {
name: form.name,
nickname: form.nickname,
phone: form.phone,
password: form.password,
email: form.email,
role: role || 'INVESTOR',
infoAgreement: form.isMarketingAgreed,
birthYear: form.birthYear,
birthMonth: form.birthMonth,
birthDay: form.birthDay,
emailDomain: form.emailDomain,
}

await signup(formData)
setNicknameCookie(form.nickname)
router.push(PATH.SIGN_UP_COMPLETE)
} catch (err) {
console.error('회원가입 실패:', err)
setErrors((prev) => ({ ...prev, submitError: '회원가입에 실패했습니다.' }))
}
}

return {
Expand Down
4 changes: 2 additions & 2 deletions app/(landing)/signup/_lib/cookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export const removeAllSignupCookies = () => {
})

return true
} catch (error) {
console.error('회원가입 쿠키 삭제 실패:', error)
} catch (err) {
console.error('회원가입 쿠키 삭제 실패:', err)
return false
}
}
Expand Down
21 changes: 16 additions & 5 deletions app/(landing)/signup/information/page.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@
}
}

.select-group {
flex-direction: column;

.select-wrapper {
display: flex;
gap: 12px;
margin-left: 160px;
margin-top: -50px;
}

.select-error {
margin-left: 160px;
margin-top: 8px;
}
}

.terms-wrapper {
display: flex;
flex-direction: column;
Expand Down Expand Up @@ -75,8 +91,3 @@
gap: 48px;
margin-bottom: 70px;
}

.select-wrapper {
display: flex;
gap: 12px;
}
7 changes: 7 additions & 0 deletions app/(landing)/signup/information/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import classNames from 'classnames/bind'
import { PATH } from '@/shared/constants/path'
import { Button } from '@/shared/ui/button'
import Checkbox from '@/shared/ui/check-box'
import { ErrorMessage } from '@/shared/ui/error-message'
import { Input } from '@/shared/ui/input'
import { LinkButton } from '@/shared/ui/link-button'
import Select from '@/shared/ui/select'
Expand Down Expand Up @@ -63,6 +64,7 @@ const InformationPage = () => {
setForm,
setErrors,
setFormState,
form,
})

const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
Expand Down Expand Up @@ -258,6 +260,11 @@ const InformationPage = () => {
titleStyle={selectStyle}
/>
</div>
{errors.select && (
<div className={cx('select-error')}>
<ErrorMessage errorMessage={errors.select} />
</div>
)}
</div>

<div className={cx('terms-wrapper')}>
Expand Down
5 changes: 3 additions & 2 deletions app/(landing)/signup/information/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RoleType } from '@/shared/types/auth'
import { UserType } from '@/shared/types/auth'
import { DropdownValueType } from '@/shared/ui/dropdown/types'

import { SIGNUP_ERROR_MESSAGES } from '../_constants/signup'
Expand Down Expand Up @@ -28,6 +28,7 @@ export interface SignupFormErrorsModel {
password?: SignupErrorMessageType | null
passwordConfirm?: SignupErrorMessageType | null
phone?: SignupErrorMessageType | null
select?: SignupErrorMessageType | null
}

export interface SelectOptionModel {
Expand All @@ -52,7 +53,7 @@ export interface SignupFormDataModel {
password: string
email: string
emailDomain: string
role: RoleType
role: UserType
infoAgreement: boolean
}

Expand Down
4 changes: 4 additions & 0 deletions app/(landing)/signup/information/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ export const validateSignupForm = (
const phoneError = validateField('PHONE', form.phone)
if (phoneError) errors.phone = phoneError

if (!form.birthYear || !form.birthMonth || !form.birthDay) {
errors.select = SIGNUP_ERROR_MESSAGES.SELECT_REQUIRED
}

return errors
}

Expand Down
4 changes: 2 additions & 2 deletions app/test/client/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const Sub = () => {
const data = await res.json()
setUser(data)
}
} catch (error) {
console.error('Fetch error:', error)
} catch (err) {
console.error('Fetch error:', err)
}
}

Expand Down
18 changes: 9 additions & 9 deletions shared/api/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export const createAxiosInstance = (options: { withInterceptors?: boolean } = {}
}

config.headers.Authorization = `Bearer ${accessToken}`
} catch (error) {
console.error('토큰 디코딩 실패:', error)
} catch (err) {
console.error('토큰 디코딩 실패:', err)
useAuthStore.getState().setAuthState({
isAuthenticated: false,
user: null,
Expand All @@ -46,22 +46,22 @@ export const createAxiosInstance = (options: { withInterceptors?: boolean } = {}

return config
},
(error) => Promise.reject(error)
(err) => Promise.reject(err)
)

instance.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (!error.config) return Promise.reject(error)
async (err: AxiosError) => {
if (!err.config) return Promise.reject(err)

const originalRequest = error.config
const originalRequest = err.config
const { isLoggedOut } = useAuthStore.getState()

if (isLoggedOut) {
return Promise.reject(error)
return Promise.reject(err)
}

if (error.response?.status === 401) {
if (err.response?.status === 401) {
try {
const newToken = await refreshToken()
if (newToken) {
Expand All @@ -80,7 +80,7 @@ export const createAxiosInstance = (options: { withInterceptors?: boolean } = {}
})
}

return Promise.reject(error)
return Promise.reject(err)
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion shared/constants/path.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const PATH = {
// Auth
SIGN_IN: '/signin',
SIGN_UP: '/signup',
SIGN_UP: '/signup/',
SIGN_UP_USER_TYPE: '/signup/user-type',
SIGN_UP_TERMS_OF_USE: '/signup/terms-of-use',
SIGN_UP_INFORMATION: '/signup/information',
Expand Down
Loading