Skip to content

Commit

Permalink
feat: 로그인 기능을 추가합니다.
Browse files Browse the repository at this point in the history
  • Loading branch information
Zero-1016 committed May 24, 2024
1 parent b444705 commit a6b1743
Show file tree
Hide file tree
Showing 25 changed files with 248 additions and 59 deletions.
5 changes: 4 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ NEXT_PUBLIC_TMDB_BASE_URL=https://api.themoviedb.org/3
NEXT_PUBLIC_TMDB_ACCESS_TOKEN=TMDB에서 발급받은 ACCESS_TOKEN
NEXT_PUBLIC_TMDB_IMAGE_URL=https://image.tmdb.org/t/p/

# LOCAL
NEXT_PUBLIC_LOCAL_BASE_URL=백엔드 API

# MSW
NEXT_PUBLIC_API_MOCKING=enabled
NEXT_PUBLIC_API_MOCKING=enabled
8 changes: 7 additions & 1 deletion app/(site)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import 'react-toastify/dist/ReactToastify.css'

import { ReactNode } from 'react'
import { ToastContainer } from 'react-toastify'

import { Outlet } from '@/widgets/layout'

Expand All @@ -13,7 +16,10 @@ export default function Layout({ children, modal }: Props) {
return (
<main className={styles.container}>
{modal}
<Outlet>{children}</Outlet>
<Outlet>
<ToastContainer autoClose={1000} />
{children}
</Outlet>
</main>
)
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"start": "next start",
"lint": "next lint",
"prepare": "husky install",
"format:fix": "prettier --write"
"format:fix": "prettier --write",
"server": "npx tsx watch ./src/entities/mock/api/http.ts"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
Expand Down Expand Up @@ -40,6 +41,7 @@
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.5",
"react-intersection-observer": "^9.10.0",
"react-toastify": "^10.0.5",
"react-youtube": "^10.1.0",
"sass": "^1.75.0",
"zod": "^3.23.8"
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/app/@types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ declare global {
NEXT_PUBLIC_TMDB_ACCESS_TOKEN: string
NEXT_PUBLIC_API_MOCKING: 'enabled' | 'disabled'
NEXT_PUBLIC_TMDB_IMAGE_URL: string
NEXT_PUBLIC_LOCAL_BASE_URL: string
}
}
}
11 changes: 7 additions & 4 deletions src/entities/mock/api/handler/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ const allUser = new Map()
allUser.set(1, { id: 1, nickname: 'zero', email: 'zero@naver.com', profileUrl: null })

export const userHandlers = [
http.post('/users/sign-up', async ({ request }) => {
console.info('회원가입')
const new_user = await request.json()
http.post('/user/sign-up', async ({ request }) => {
const new_user = (await request.json()) as {
nickname: string
email: string
}

allUser.set(Math.floor(Math.random() * 1000), new_user)
const new_id = allUser.size
allUser.set(new_id, { id: new_id, ...new_user, profileUrl: null })

return HttpResponse.json('ok', {
headers: {
Expand Down
21 changes: 19 additions & 2 deletions src/features/auth/hook/use-sign-in-form.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/navigation'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'react-toastify'
import { z } from 'zod'

import { signIn } from '@/features/auth/lib'
import { signInFormSchema } from '@/features/auth/schema'
import { SITE_PATH } from '@/shared/constants'

type SignInFormSchema = z.infer<typeof signInFormSchema>

export function useSignInForm() {
const onSubmit: SubmitHandler<SignInFormSchema> = data => {
console.info('로그인을 시도합니다', data)
const router = useRouter()
const onSubmit: SubmitHandler<SignInFormSchema> = async data => {
try {
const { email, password } = data
await toast.promise(signIn({ email, pw: password }), {
pending: '로그인을 시도합니다.',
success: '로그인에 성공합니다.',
error: '회원가입에 실패합니다.',
})
router.replace(SITE_PATH.home)
} catch (err) {
if (err instanceof Error) {
console.error(err.message)
}
}
}

const {
Expand Down
24 changes: 21 additions & 3 deletions src/features/auth/hook/use-sign-up-form.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
'use client'

import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/navigation'
import { SubmitHandler, useForm } from 'react-hook-form'
import { toast } from 'react-toastify'
import { z } from 'zod'

import { signUp } from '@/features/auth/lib'
import { signUpFormSchema } from '@/features/auth/schema'
import { SITE_PATH } from '@/shared/constants'

type SignUpFormSchema = z.infer<typeof signUpFormSchema>

export function useSignUpForm() {
const onSubmit: SubmitHandler<SignUpFormSchema> = data => {
console.info('회원가입을 시도합니다.', data)
const router = useRouter()
const onSubmit: SubmitHandler<SignUpFormSchema> = async data => {
try {
const { email, password, nickname } = data
await toast.promise(signUp({ email, nickname, pw: password }), {
pending: '회원 가입을 시도합니다.',
success: '회원가입에 성공합니다.',
error: '회원가입에 실패합니다.',
})
router.replace(SITE_PATH.sign_in)
} catch (err) {
if (err instanceof Error) {
console.error(err.message)
}
}
}

const {
handleSubmit,
formState: { errors },
register,
getValues,
} = useForm<z.infer<typeof signUpFormSchema>>({
resolver: zodResolver(signUpFormSchema),
defaultValues: {
Expand All @@ -27,5 +45,5 @@ export function useSignUpForm() {
},
})

return { handleSubmit: handleSubmit(onSubmit), errors, register }
return { handleSubmit: handleSubmit(onSubmit), errors, register, getValues }
}
15 changes: 15 additions & 0 deletions src/features/auth/lib/email-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export async function emailCheck(nickname: string) {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/email?email=${nickname}`, {
method: 'GET',
headers: {
accept: 'application/json',
},
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
4 changes: 4 additions & 0 deletions src/features/auth/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { emailCheck } from './email-check'
export { nicknameCheck } from './nickname-check'
export { signIn } from './sign-in'
export { signUp } from './sign-up'
15 changes: 15 additions & 0 deletions src/features/auth/lib/nickname-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export async function nicknameCheck(nickname: string) {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/nickname?nickname=${nickname}`, {
method: 'GET',
headers: {
accept: 'application/json',
},
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
21 changes: 21 additions & 0 deletions src/features/auth/lib/sign-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
interface BodyData {
email: string
pw: string
}

export async function signIn(bodyData: BodyData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/sign-in`, {
method: 'POST',
headers: {
accept: 'application/json',
},
body: JSON.stringify(bodyData),
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
22 changes: 22 additions & 0 deletions src/features/auth/lib/sign-up.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
interface BodyData {
nickname: string
email: string
pw: string
}

export async function signUp(bodyData: BodyData) {
const res = await fetch(`${process.env.NEXT_PUBLIC_LOCAL_BASE_URL}/user/sign-up`, {
method: 'POST',
headers: {
accept: 'application/json',
},
body: JSON.stringify(bodyData),
cache: 'no-store',
})

if (!res.ok) {
throw new Error('Failed to fetch data')
}

return res.json()
}
1 change: 1 addition & 0 deletions src/features/auth/schema/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { signInFormSchema } from './sign-in-form-schema'
export { signUpFormSchema } from './sign-up-form-schema'
export { emailSchema, nicknameSchema, passwordSchema } from './user-schema'
6 changes: 3 additions & 3 deletions src/features/auth/schema/sign-in-form-schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { z } from 'zod'

import { email, password } from './user-schema'
import { emailSchema, passwordSchema } from './user-schema'

export const signInFormSchema = z.object({
email,
password,
email: emailSchema,
password: passwordSchema,
})
10 changes: 5 additions & 5 deletions src/features/auth/schema/sign-up-form-schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z } from 'zod'

import { email, nickname, password } from './user-schema'
import { emailSchema, nicknameSchema, passwordSchema } from './user-schema'

export const signUpFormSchema = z
.object({
nickname,
email,
password,
confirmPassword: password,
nickname: nicknameSchema,
email: emailSchema,
password: passwordSchema,
confirmPassword: passwordSchema,
})
.refine(data => data.password === data.confirmPassword, {
message: '비밀번호가 일치하지 않습니다.',
Expand Down
6 changes: 3 additions & 3 deletions src/features/auth/schema/user-schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { z } from 'zod'

export const email = z.string().email({ message: '이메일 형식에 맞춰 작성해주세요' })
export const emailSchema = z.string().email({ message: '이메일 형식에 맞춰 작성해주세요' })

export const nickname = z
export const nicknameSchema = z
.string()
.min(2, { message: '최소 2글자 이상의 닉네임을 작성해주세요' })
.max(6, { message: '최소 6글자 미만의 닉네임을 작성해주세요' })

export const password = z
export const passwordSchema = z
.string()
.min(6, { message: '최소 6글자 이상의 비밀번호를 작성해주세요' })
.max(20, { message: '최대 20글자 이하의 비밀번호를 작성해주세요' })
14 changes: 3 additions & 11 deletions src/features/auth/ui/SignInForm.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
'use client'

import { KeyRound, Mail } from 'lucide-react'

import { useSignInForm } from '@/features/auth/hook'
import { TextFiled } from '@/shared/ui'
import { KeyRoundIcon, MailIcon, TextFiled } from '@/shared/ui'

import styles from './SignForm.module.scss'
import { SubmitButton } from './SubmitButton'
Expand All @@ -13,19 +11,13 @@ export function SignInForm() {

return (
<form onSubmit={handleSubmit} className={styles.form}>
<TextFiled
type="email"
placeholder="Email"
error={errors['email']}
icon={<Mail color="#475069" />}
{...register('email')}
/>
<TextFiled type="email" placeholder="Email" error={errors['email']} icon={<MailIcon />} {...register('email')} />
<TextFiled
type="password"
placeholder="Password"
{...register('password')}
error={errors['password']}
icon={<KeyRound color="#475069" />}
icon={<KeyRoundIcon />}
/>
<SubmitButton disabled={isSubmitting}>Login</SubmitButton>
</form>
Expand Down
Loading

0 comments on commit a6b1743

Please sign in to comment.