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
66 changes: 0 additions & 66 deletions src/app/(auth)/signin/page.tsx

This file was deleted.

10 changes: 10 additions & 0 deletions src/app/(auth)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use client'

import SignupForm from '@/app/features/auth/components/SignupForm'
export default function Signup() {
return (
<>
<SignupForm />
</>
)
}
105 changes: 105 additions & 0 deletions src/app/features/auth/components/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
'use client'

import Input from '@components/Input'
import { cn } from '@lib/cn'
import { useState } from 'react'
import { useForm } from 'react-hook-form'

import { useConfirmPasswordValidation } from '../hooks/useConfirmPasswordValidation'
import { useSignupSubmit } from '../hooks/useSignupSubmit'
import { signupValidation } from '../schemas/signupValidation'
import { SignupFormData } from '../types/auth.type'

export default function SignupForm() {
const {
register,
handleSubmit,
trigger,
getValues,
formState: { errors, isSubmitting, isValid },
} = useForm<SignupFormData>({
mode: 'onChange',
defaultValues: {
email: '',
nickname: '',
password: '',
confirmPassword: '',
},
})

const [isChecked, setIsChecked] = useState(false)
const { submit } = useSignupSubmit()
const validation = useConfirmPasswordValidation(getValues)

function handleAgree() {
setIsChecked((prev) => !prev)
}

return (
<form className="flex flex-col gap-16" onSubmit={handleSubmit(submit)}>
<Input
labelName="이메일"
type="email"
placeholder="이메일을 입력해 주세요"
autoComplete="email"
{...register('email', signupValidation.email)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기는 어떤 내용을 포함하고 있는걸까용?

Copy link
Author

@Insung-Jo Insung-Jo Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정확히 어느 부분을 말씀하신 건지는 파악할 수 없으나

만약 ...register를 말씀하신 거라면 기본적으로 register(name, options)를 호출해 반환된 값을 props로 전달하는 형태입니다.
이때 register의 반환 값은 { ref, name, onChange, onBlur } 형태의 객체이며 options에는 다양한 유효성 검증과 관련된 옵션이 있습니다!

만약 전체 옵션이 궁금하시다면 공식 문서를 참고하시면 됩니다!

hasError={!!errors.email}
errorMessage={errors.email?.message}
/>
<Input
labelName="닉네임"
type="text"
placeholder="닉네임을 입력해 주세요"
autoComplete="off"
{...register('nickname', signupValidation.nickname)}
hasError={!!errors.nickname}
errorMessage={errors.nickname?.message}
/>
<Input
labelName="비밀번호"
type="password"
placeholder="8자 이상 입력해 주세요"
autoComplete="new-password"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 적어주면 어떻게 자동완성이 이루어지는건가용?

Copy link
Author

@Insung-Jo Insung-Jo Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아니요 그 반대 입니다!

autoComplete라는 속성은 브라우저의 자동 완성 동작을 제어하는 속성입니다.
이 중에서 new-password는 해당 입력창이 새로운 비밀번호를 설정하는 필드임을 브라우저에게 알려주게 됩니다!

이 속성을 명시 하지 않았을 경우 브라우저에서는 과거에 입력한 데이터를 기준으로 자동 완성을 띄우게 됩니다. 이러면 UX 측면으로도 혼란을 야기할 수 있기 때문에 해당 속성을 사용하게 되었습니다.
공식 문서

{...register('password', {
...signupValidation.password,
onChange: () => trigger('confirmPassword'),
})}
hasError={!!errors.password}
errorMessage={errors.password?.message}
/>
<Input
labelName="비밀번호 확인"
type="password"
placeholder="비밀번호를 한번 더 입력해주세요"
autoComplete="new-password"
{...register('confirmPassword', validation)}
hasError={!!errors.confirmPassword}
errorMessage={errors.confirmPassword?.message}
/>
<div className="flex items-center gap-8">
<input
type="checkbox"
id="terms"
className="size-16 rounded-4"
onChange={handleAgree}
checked={isChecked}
/>
<label className="text-base" htmlFor="terms">
이용약관에 동의합니다.
</label>
</div>
<button
type="submit"
className={cn(
'mt-8 h-50 w-full rounded-8 text-lg font-medium text-white',
isValid && isChecked && !isSubmitting
? 'BG-blue'
: 'BG-blue-disabled',
)}
disabled={isSubmitting || !isValid || !isChecked}
>
회원가입
</button>
</form>
)
}
11 changes: 11 additions & 0 deletions src/app/features/auth/hooks/useConfirmPasswordValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SignupFormData } from '../types/auth.type'

export function useConfirmPasswordValidation(getValues: () => SignupFormData) {
return {
required: '비밀번호를 한번 더 입력해 주세요.',
validate: (value: string) => {
const password = getValues().password
return value === password || '비밀번호가 일치하지 않습니다'
},
}
}
28 changes: 28 additions & 0 deletions src/app/features/auth/hooks/useSignupSubmit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import axios from 'axios'
import { useRouter } from 'next/navigation'
import { toast } from 'sonner'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토스트 사용해주셨군용


import { SignupRequest } from '../types/auth.type'
import { useAuth } from './useAuth'

export function useSignupSubmit() {
const { signup } = useAuth()
const router = useRouter()

async function submit(data: SignupRequest) {
try {
await signup(data)
toast.success('가입이 완료되었습니다!')
router.push('/login')
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const message = e.response?.data?.message
toast.error(message ?? '회원가입에 실패하였습니다.')
} else {
toast.error('알 수 없는 에러 발생')
}
}
}

return { submit }
}
23 changes: 23 additions & 0 deletions src/app/features/auth/schemas/signupValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const signupValidation = {
email: {
required: '이메일을 입력해 주세요.',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '유효한 이메일 주소를 입력해주세요',
},
},
password: {
required: '비밀번호를 입력해 주세요.',
minLength: {
value: 8,
message: '비밀번호는 최소 8자 이상이어야 합니다.',
},
},
nickname: {
required: '닉네임을 입력해 주세요.',
pattern: {
value: /^[a-zA-Z가-힣]{1,10}$/,
message: '한글 또는 영어만 입력할 수 있으며, 최대 10자까지 가능합니다.',
},
},
}
4 changes: 4 additions & 0 deletions src/app/features/auth/types/auth.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface SignupRequest {
password: string
}

export interface SignupFormData extends SignupRequest {
confirmPassword: string
}

export interface AuthState {
accessToken: string | null
user: User | null
Expand Down