Skip to content

Commit

Permalink
feat: 로그인 페이지, 기능 추가 #11
Browse files Browse the repository at this point in the history
  • Loading branch information
yemsu committed Feb 19, 2024
1 parent 925b9fb commit 0a581a1
Show file tree
Hide file tree
Showing 10 changed files with 853 additions and 58 deletions.
434 changes: 434 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-scroll-area": "^1.0.5",
Expand Down
54 changes: 4 additions & 50 deletions src/app/auth/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import Link from 'next/link'

import { pickValueToKey } from '@/utils'
import setMetadata from '@/utils/setMetadata'

import { PLACEHOLDERS } from '@/constants/form'

import { Button } from '@/components/ui/button'
import {
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'

import ButtonWrap from '@/components/ButtonWrap'
import FormArea, { FormInfo, InputNames } from '@/components/FormArea'
import MainCardBox from '@/components/MainCardBox'
import LoginForm from '@/components/auth/LoginForm'

export async function generateMetadata() {
return setMetadata({
Expand All @@ -25,55 +18,16 @@ export async function generateMetadata() {
})
}

const formInfo: FormInfo[] = [
{
name: 'email',
type: 'email',
label: '이메일',
placeholder: PLACEHOLDERS.EMAIL,
autoComplete: 'email',
},
{
name: 'password',
type: 'password',
label: '비밀번호',
placeholder: PLACEHOLDERS.PASSWORD,
autoComplete: 'password',
},
] as const

const defaultValues = pickValueToKey<FormInfo, InputNames>(formInfo, 'name', '')

function Page() {
return (
<MainCardBox>
<CardHeader className="text-center">
<CardTitle className="text-center text-xl">로그인</CardTitle>
<CardDescription>
로그인 후 테라코타를 더 편리하게 이용해보세요
</CardDescription>
<CardDescription>테라코타에 오신 것을 환영합니다!</CardDescription>
</CardHeader>
<CardContent>
<FormArea
formName="login"
defaultValues={defaultValues}
formInfo={formInfo}
submitText="로그인"
/>
<CardContent className="flex flex-col gap-10">
<LoginForm />
</CardContent>
<div className="border-t py-4">
<ButtonWrap alignX="center">
<Button variant="link" className="text-muted-foreground" asChild>
<Link href="/auth/join">회원가입</Link>
</Button>
<Button variant="link" className="text-muted-foreground" asChild>
<Link href="/auth/help/id">아이디 찾기</Link>
</Button>
<Button variant="link" className="text-muted-foreground" asChild>
<Link href="/auth/help/password">비밀번호 찾기</Link>
</Button>
</ButtonWrap>
</div>
</MainCardBox>
)
}
Expand Down
10 changes: 2 additions & 8 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import Link from 'next/link'

import { Button } from '@/components/ui/button'

import ButtonWrap from '@/components/ButtonWrap'
import DarkModeButton from '@/components/DarkModeButton'
import HeaderAuthButtons from '@/components/HeaderAuthButtons'
import Inner from '@/components/Inner'

function Header() {
Expand All @@ -24,13 +23,8 @@ function Header() {
</Link>
</h1>
<ButtonWrap>
<HeaderAuthButtons />
<DarkModeButton />
<Button variant="outline" asChild>
<Link href="/auth/login">로그인</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/auth/join">회원가입</Link>
</Button>
</ButtonWrap>
</div>
</Inner>
Expand Down
70 changes: 70 additions & 0 deletions src/components/HeaderAuthButtons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client'

import Link from 'next/link'
import { useEffect } from 'react'

import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'

import useAuthStore from '@/store/useAuthStore'

function HeaderAuthButtons() {
const { isLogin, logout, userInfo, checkLogin } = useAuthStore()

function onClickLogout() {
logout()
}

useEffect(() => {
checkLogin()
}, [])

if (isLogin === undefined) return

return (
<>
{isLogin ? (
<>
<DropdownMenu>
<DropdownMenuTrigger className="flex h-9 w-9 items-center justify-center overflow-hidden whitespace-nowrap rounded-full bg-accent text-xs leading-none">
{userInfo?.nickname}
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem asChild>
<Link href="/auth/my" className="cursor-pointer">
마이페이지
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<button
className="w-full cursor-pointer"
onClick={onClickLogout}
>
로그아웃
</button>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
) : (
<>
<Button variant="outline" asChild>
<Link href="/auth/login">로그인</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/auth/join">회원가입</Link>
</Button>
</>
)}
</>
)
}

export default HeaderAuthButtons
110 changes: 110 additions & 0 deletions src/components/auth/LoginForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client'

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

import { loginSchema } from '@/utils/formSchema'

import { PLACEHOLDERS, VALIDATIONS } from '@/constants/form'
import { ERROR_NOTICE, RESPONSE_STATES } from '@/constants/response'

import { Button } from '@/components/ui/button'
import { Form } from '@/components/ui/form'

import BaseInput from '@/components/BaseInput'
import ButtonWrap from '@/components/ButtonWrap'

import { postLogin } from '@/service/member'
import useAuthStore from '@/store/useAuthStore'

const defaultValues = {
email: '',
password: '',
}

function LoginForm() {
const router = useRouter()
const { login } = useAuthStore()

const form = useForm<z.infer<typeof loginSchema>>({
resolver: zodResolver(loginSchema),
defaultValues: defaultValues,
})

async function onSubmit(values: z.infer<typeof loginSchema>) {
const { email, password } = values

const res = await postLogin({
email,
password,
})

if (res.state === RESPONSE_STATES.SUCCESS) {
console.log('onSubmit', res)
const { email, nickname, token } = res.result
const userInfo = { email, nickname }
login(token, userInfo)
router.push('/')
return
}

const {
error: { errorMessage },
} = res

switch (errorMessage) {
case '이메일 또는 패스워드가 일치하지 않습니다.':
form.setError('email', { type: 'custom', message: '' })
form.setError('password', {
type: 'custom',
message: VALIDATIONS.LOGIN,
})
break
default:
form.setError('email', {
type: 'custom',
message: ERROR_NOTICE.COMMON(res.error),
})
}
}

function onErrors(errors: FieldErrors) {
console.log('form submit validation', errors)
}

return (
<div className="space-y-8">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit, onErrors)}
className="space-y-8"
>
<BaseInput
control={form.control}
name="email"
label="이메일"
placeholder={PLACEHOLDERS.EMAIL}
autoComplete="nickname"
/>
<BaseInput
control={form.control}
type="password"
name="password"
label="비밀번호"
placeholder={PLACEHOLDERS.PASSWORD}
autoComplete="new-password"
/>
<ButtonWrap alignX="center">
<Button type="submit" className="mx-auto">
로그인
</Button>
</ButtonWrap>
</form>
</Form>
</div>
)
}

export default LoginForm
Loading

0 comments on commit 0a581a1

Please sign in to comment.