Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fe/#218 회원가입 페이지 마이그레이션 #219

Merged
merged 12 commits into from
Sep 26, 2023
Merged
10 changes: 5 additions & 5 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: ["feature"]
branches: ["feature", "FE/#207"]
pull_request:
branches: ["feature"]
branches: ["feature", "FE/#207"]

jobs:
backend-test:
Expand Down Expand Up @@ -70,15 +70,15 @@ jobs:

- name: Install packages
working-directory: frontend
run: npm ci
run: yarn install --immutable --immutable-cache --check-cache

- name: Prettier
working-directory: frontend
run: npm run format
run: yarn format

- name: ESLint
working-directory: frontend
run: npm run lint
run: yarn lint

# - name: Frontend Test
# working-directory: frontend
Expand Down
4 changes: 4 additions & 0 deletions frontend/.env.local.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# .env.local로 이름 변경하시고 사용하시면 됩니다!
BASE_URL=

NEXT_PUBLIC_BASE_URL=
4 changes: 3 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"project": "./tsconfig.json"
},
"rules": {
"react/react-in-jsx-scope": "off"
"react/react-in-jsx-scope": "off",
"import/no-extraneous-dependencies": "off",
"react-hooks/exhaustive-deps": "off"
}
}
8 changes: 6 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"format:fix": "prettier --write --ignore-path .gitignore ."
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@types/node": "20.6.3",
"@types/react": "18.2.22",
"@types/react-dom": "18.2.7",
Expand All @@ -21,14 +22,17 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-next": "13.5.1",
"next": "13.5.1",
"next": "^13.5.3",
"postcss": "8.4.30",
"prettier": "^3.0.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.46.2",
"recoil": "^0.7.7",
"recoil-persist": "^5.1.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2"
"typescript": "5.2.2",
"yup": "^1.3.0"
},
"devDependencies": {
"eslint-config-prettier": "^9.0.0"
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/app/(user)/login/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

export default function LoginError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>{error.message}</h2>
<button type="button" onClick={() => reset()}>
Try again
</button>
</div>
)
}
183 changes: 183 additions & 0 deletions frontend/src/app/(user)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
'use client'

/* eslint-disable react/jsx-props-no-spreading */

import { yupResolver } from '@hookform/resolvers/yup'
import { useEffect, useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import { useRouter } from 'next/navigation'
import { useRecoilState } from 'recoil'
import * as yup from 'yup'

import Image from 'next/image'
import Email from '../../../../public/images/svg/email.svg'
import { autoLoginState } from '../../../utils/atoms'

type DataObject = {
email: string
password: string
}

const schema = yup.object().shape({
email: yup
.string()
.email('이메일 형식이 맞지 않습니다.')
.required('이메일이 필요합니다.'),
password: yup
.string()
.min(8, '비밀번호는 최소 8자리 이상이여야 합니다.')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]+$/,
'1개 이상의 대소문자, 숫자, 특수문자가 포함되어야 합니다.',
)
.required('비밀번호가 필요합니다.'),
})

export default function Login() {
const router = useRouter()
const accessToken =
typeof window !== 'undefined' ? sessionStorage.getItem('accessToken') : null
const persistToken =
typeof window !== 'undefined' ? localStorage.getItem('persistToken') : null
const [autoLogin, setAutoLogin] = useRecoilState(autoLoginState)
const [errorMessage, setErrorMessage] = useState<string>('')

const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setAutoLogin(event.target.checked)
}

const handleButtonClick = () => {
setAutoLogin(!autoLogin)
}

const {
register,
handleSubmit,
formState: { errors },
} = useForm<DataObject>({
resolver: yupResolver(schema),
})

function toMain() {
router.push('/')
}

function toRegistration() {
router.push('/registration')
}

const onSubmit: SubmitHandler<DataObject> = async (data: DataObject) => {
const res = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/auth/signin`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: data.email,
password: data.password,
}),
})

const resData = await res.json()

if (!res.ok) {
setErrorMessage('아이디 비밀번호를 확인해주세요.')
throw new Error(resData.message)
}

setErrorMessage('')

if (autoLogin) {
localStorage.setItem('persistToken', resData.data.accessToken)
} else {
sessionStorage.setItem('accessToken', resData.data.accessToken)
}
router.push('/')
}

useEffect(() => {
if (!navigator.onLine) {
throw new Error('오프라인 상태입니다. 네트워크 연결을 확인해주세요.')
}
if (accessToken || persistToken) {
// eslint-disable-next-line no-alert
alert('이미 로그인 상태입니다.')
router.push('/')
}
}, [])

return (
<div className=" h-auto min-h-screen w-screen bg-zinc-200 pt-10 pb-10">
<button
onClick={() => toMain()}
className="mx-auto mb-10 block font-lato-b text-5xl text-graphyblue "
type="button"
>
Graphy
</button>
<div className="mx-auto flex h-auto w-[450px] flex-col rounded-xl border bg-white pt-12 ">
<span className=" ml-8 font-ng-eb text-3xl text-graphyblue">
로그인
</span>
<form
onSubmit={handleSubmit(onSubmit)}
className="mx-auto flex w-[360px] flex-col"
>
<input
{...register('email')}
type="email"
className="mt-12 h-[60px] rounded-3xl border pl-4 text-lg"
placeholder="이메일 주소"
autoComplete="email"
/>
<p className="text-sm">{errors.email?.message}</p>
<input
{...register('password')}
type="password"
className="mt-10 h-[60px] rounded-3xl border pl-4 text-lg"
placeholder="비밀번호 (8자리 이상)"
autoComplete="current-password"
/>
<p className="text-sm">{errors.password?.message}</p>
{errorMessage ? <p className="text-sm">{errorMessage}</p> : null}
<button
className=" mt-10 flex h-[60px] items-center rounded-3xl border bg-graphyblue"
type="submit"
>
<Image src={Email} alt="email" className="ml-4" />
<span className=" mx-auto pr-7 font-ng-eb text-xl text-white">
로그인
</span>
</button>
</form>
<div className="my-8 ml-12 flex">
<div>
<input
type="checkbox"
className="h-4 w-4"
checked={autoLogin}
onChange={handleCheckboxChange}
/>
<button
className="ml-2 text-xl"
type="button"
onClick={handleButtonClick}
>
로그인 상태 유지
</button>
</div>
</div>
<div className="m-auto mb-0 flex h-20 w-[450px] items-center justify-center border-t">
<span>아직 회원이 아니세요?</span>
<button
className="mx-4 font-ng-eb text-graphyblue"
type="button"
onClick={() => toRegistration()}
>
회원가입
</button>
</div>
</div>
</div>
)
}
18 changes: 18 additions & 0 deletions frontend/src/app/(user)/registration/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

export default function RegistrationError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>{error.message}</h2>
<button type="button" onClick={() => reset()}>
Try again
</button>
</div>
)
}
Loading
Loading