commit bba0b0864bfe4d2132bf4df5e6cfd568f00aa0fc
parent 963a915760a4fd2ec71d9f06566ca2313e6e1b9b
Author: Yongbin Kim <iam@yongbin.kim>
Date: Fri, 20 Jan 2023 10:33:19 +0900
fix: 폼 관련 버그 수정 및 개선
- 폼 입력 후 엔터 키로 진행하지 못하던 문제 수정
- 로그인 폼에서 오류 발생한 이후 다시 입력할 수 없던 문제 수정
- 오류 코드가 읽을 수 있는 텍스트로 출력되게끔 수정
- 전체 로직 개선
Signed-off-by: Yongbin Kim <iam@yongbin.kim>
Diffstat:
11 files changed, 305 insertions(+), 304 deletions(-)
diff --git a/__tests__/__snapshots__/login_test.tsx.snap b/__tests__/__snapshots__/login_test.tsx.snap
@@ -25,130 +25,55 @@ exports[`/users/login renders correctly 1`] = `
<div
class="container"
>
- <div
- class="fields-wrapper"
- >
- <label
- class="field has-no-color is-not-readonly"
- >
- <input
- class="input"
- type="text"
- value=""
- />
- <span
- class="label"
- >
- Email
- </span>
- <div
- class="label-background"
- >
- Email
- </div>
- </label>
- <label
- class="field has-no-color is-not-readonly"
- >
- <input
- class="input"
- type="password"
- value=""
- />
- <span
- class="label"
- >
- Password
- </span>
- <div
- class="label-background"
- >
- Password
- </div>
- </label>
- <button
- class="button is-primary"
- >
- Login
- </button>
- </div>
- </div>
- </div>
-</div>
-`;
-
-exports[`Login renders correctly 1`] = `
-<div>
- <div
- class="hero"
- >
- <div
- class="container"
- >
- <div
- class="hero-body"
- >
- <h1
- class="is-large is-headline"
+ <form>
+ <div
+ class="fields-wrapper"
>
- Login
- </h1>
- </div>
- </div>
- </div>
- <div
- class="section"
- >
- <div
- class="container"
- >
- <div
- class="fields-wrapper"
- >
- <label
- class="field has-no-color is-not-readonly"
- >
- <input
- class="input"
- type="text"
- value=""
- />
- <span
- class="label"
+ <label
+ class="field has-no-color is-not-readonly"
>
- Email
- </span>
- <div
- class="label-background"
+ <input
+ class="input"
+ type="text"
+ value=""
+ />
+ <span
+ class="label"
+ >
+ Email
+ </span>
+ <div
+ class="label-background"
+ >
+ Email
+ </div>
+ </label>
+ <label
+ class="field has-no-color is-not-readonly"
>
- Email
- </div>
- </label>
- <label
- class="field has-no-color is-not-readonly"
- >
+ <input
+ class="input"
+ type="password"
+ value=""
+ />
+ <span
+ class="label"
+ >
+ Password
+ </span>
+ <div
+ class="label-background"
+ >
+ Password
+ </div>
+ </label>
<input
- class="input"
- type="password"
- value=""
+ class="button is-primary"
+ type="submit"
+ value="Login"
/>
- <span
- class="label"
- >
- Password
- </span>
- <div
- class="label-background"
- >
- Password
- </div>
- </label>
- <button
- class="button is-primary"
- >
- Login
- </button>
- </div>
+ </div>
+ </form>
</div>
</div>
</div>
diff --git a/__tests__/__snapshots__/register_test.tsx.snap b/__tests__/__snapshots__/register_test.tsx.snap
@@ -25,40 +25,42 @@ exports[`/users/signup renders correctly 1`] = `
<div
class="container"
>
- <div
- class="fields-wrapper"
- >
- <label
- class="field has-no-color is-not-readonly"
+ <form>
+ <div
+ class="fields-wrapper"
>
- <input
- class="input"
- type="text"
- value=""
- />
- <span
- class="label"
- >
- Email
- </span>
- <div
- class="label-background"
+ <label
+ class="field has-no-color is-not-readonly"
>
- Email
- </div>
- </label>
- </div>
- <p>
- 입력해주신 주소로 인증 메일을 보내드립니다. 메일에 포함된 링크를 통해, 가입을 진행할 수 있습니다.
- </p>
- <p>
- 메일 주소는 공개되지 않습니다.
- </p>
- <button
- class="button is-primary"
- >
- Sign up
- </button>
+ <input
+ class="input"
+ type="text"
+ value=""
+ />
+ <span
+ class="label"
+ >
+ Email
+ </span>
+ <div
+ class="label-background"
+ >
+ Email
+ </div>
+ </label>
+ </div>
+ <p>
+ 입력해주신 주소로 인증 메일을 보내드립니다. 메일에 포함된 링크를 통해, 가입을 진행할 수 있습니다.
+ </p>
+ <p>
+ 메일 주소는 공개되지 않습니다.
+ </p>
+ <input
+ class="button is-primary"
+ type="submit"
+ value="Sign up"
+ />
+ </form>
</div>
</div>
</div>
diff --git a/components/contexts/TokenContext.tsx b/components/contexts/TokenContext.tsx
@@ -12,7 +12,7 @@ export interface AccessTokenPayload extends JwtPayload {
}
const TokenContext = createContext<AccessTokenPayload | null>(null)
-const TokenRefreshContext = createContext<() => void>(() => {
+const TokenFlushContext = createContext<() => void>(() => {
})
export function TokenProvider ({ children }: { children: ReactNode }) {
@@ -65,16 +65,16 @@ export function TokenProvider ({ children }: { children: ReactNode }) {
}
}, [accessToken, accessToken?.exp])
- const trigger = useCallback(
+ const flushTrigger = useCallback(
() => setCheckCounter(v => v + 1),
[],
)
return (
<TokenContext.Provider value={accessToken}>
- <TokenRefreshContext.Provider value={trigger}>
+ <TokenFlushContext.Provider value={flushTrigger}>
{children}
- </TokenRefreshContext.Provider>
+ </TokenFlushContext.Provider>
</TokenContext.Provider>
)
}
@@ -87,8 +87,8 @@ export function useToken () {
return token
}
-export function useTokenRefreshTrigger () {
- return useContext(TokenRefreshContext)
+export function useTokenFlushTrigger () {
+ return useContext(TokenFlushContext)
}
function getCookie (name: string) {
diff --git a/components/elements/Button.module.css b/components/elements/Button.module.css
@@ -1 +1 @@
-.button{display:inline-flex;min-width:5rem;padding:0 1rem;height:2rem;justify-content:center;align-items:center;border:1px solid rgba(0,0,0,0);border-radius:9999px;vertical-align:top;transition:background-color 100ms,box-shadow 100ms;user-select:none;cursor:pointer;background-color:#e8ebe8;color:#191c1b}.button:hover{background-color:#d8dbd9}.button:active{background-color:#e8ebe8}.button.is-disabled{background-color:#fafdfa;color:rgba(25,28,27,.5)}.button.is-primary{background-color:#e6f1ed;color:#006b5a}.button.is-primary:hover{background-color:#d5e7e2}.button.is-primary:active{background-color:#e6f1ed}.button.is-primary.is-disabled{background-color:#fafdfa;color:rgba(0,107,90,.5)}.button.is-secondary{background-color:#f0eff2;color:#7b4998}.button.is-secondary:hover{background-color:#e7e2eb}.button.is-secondary:active{background-color:#f0eff2}.button.is-secondary.is-disabled{background-color:#fafdfa;color:rgba(123,73,152,.5)}.button.is-tertiary{background-color:#ebf1f0;color:#426278}.button.is-tertiary:hover{background-color:#dee6e7}.button.is-tertiary:active{background-color:#ebf1f0}.button.is-tertiary.is-disabled{background-color:#fafdfa;color:rgba(66,98,120,.5)}.button.is-error{background-color:#fae9e6;color:red}.button.is-error:hover{background-color:#fbd7d5}.button.is-error:active{background-color:#fae9e6}.button.is-error.is-disabled{background-color:#fafdfa;color:rgba(255,0,0,.5)}@media(prefers-color-scheme: dark){.button{background-color:#292c2b;color:#e1e3e0}.button:hover{background-color:#373a39}.button:active{background-color:#292c2b}.button.is-disabled{background-color:#191c1b;color:rgba(225,227,224,.5)}.button.is-primary{background-color:#1b2c28;color:#2cdebf}.button.is-primary:hover{background-color:#1c3934}.button.is-primary:active{background-color:#1b2c28}.button.is-primary.is-disabled{background-color:#191c1b;color:rgba(44,222,191,.5)}.button.is-secondary{background-color:#29282d;color:#e6b4ff}.button.is-secondary:hover{background-color:#38333d}.button.is-secondary:active{background-color:#29282d}.button.is-secondary.is-disabled{background-color:#191c1b;color:rgba(230,180,255,.5)}.button.is-tertiary{background-color:#252a2b;color:#aacbe4}.button.is-tertiary:hover{background-color:#2f3639}.button.is-tertiary:active{background-color:#252a2b}.button.is-tertiary.is-disabled{background-color:#191c1b;color:rgba(170,203,228,.5)}.button.is-error{background-color:#2b2827;color:#ffb4ab}.button.is-error:hover{background-color:#3c3331}.button.is-error:active{background-color:#2b2827}.button.is-error.is-disabled{background-color:#191c1b;color:rgba(255,180,171,.5)}}/*# sourceMappingURL=Button.module.css.map */
+.button{display:inline-block;min-width:5rem;padding:0 1rem;height:2rem;text-align:center;border:1px solid rgba(0,0,0,0);border-radius:9999px;vertical-align:top;transition:background-color 100ms,box-shadow 100ms;user-select:none;cursor:pointer;background-color:#e8ebe8;color:#191c1b}.button:hover{background-color:#d8dbd9}.button:active{background-color:#e8ebe8}.button.is-disabled{background-color:#fafdfa;color:rgba(25,28,27,.5)}.button.is-primary{background-color:#e6f1ed;color:#006b5a}.button.is-primary:hover{background-color:#d5e7e2}.button.is-primary:active{background-color:#e6f1ed}.button.is-primary.is-disabled{background-color:#fafdfa;color:rgba(0,107,90,.5)}.button.is-secondary{background-color:#f0eff2;color:#7b4998}.button.is-secondary:hover{background-color:#e7e2eb}.button.is-secondary:active{background-color:#f0eff2}.button.is-secondary.is-disabled{background-color:#fafdfa;color:rgba(123,73,152,.5)}.button.is-tertiary{background-color:#ebf1f0;color:#426278}.button.is-tertiary:hover{background-color:#dee6e7}.button.is-tertiary:active{background-color:#ebf1f0}.button.is-tertiary.is-disabled{background-color:#fafdfa;color:rgba(66,98,120,.5)}.button.is-error{background-color:#fae9e6;color:red}.button.is-error:hover{background-color:#fbd7d5}.button.is-error:active{background-color:#fae9e6}.button.is-error.is-disabled{background-color:#fafdfa;color:rgba(255,0,0,.5)}@media(prefers-color-scheme: dark){.button{background-color:#292c2b;color:#e1e3e0}.button:hover{background-color:#373a39}.button:active{background-color:#292c2b}.button.is-disabled{background-color:#191c1b;color:rgba(225,227,224,.5)}.button.is-primary{background-color:#1b2c28;color:#2cdebf}.button.is-primary:hover{background-color:#1c3934}.button.is-primary:active{background-color:#1b2c28}.button.is-primary.is-disabled{background-color:#191c1b;color:rgba(44,222,191,.5)}.button.is-secondary{background-color:#29282d;color:#e6b4ff}.button.is-secondary:hover{background-color:#38333d}.button.is-secondary:active{background-color:#29282d}.button.is-secondary.is-disabled{background-color:#191c1b;color:rgba(230,180,255,.5)}.button.is-tertiary{background-color:#252a2b;color:#aacbe4}.button.is-tertiary:hover{background-color:#2f3639}.button.is-tertiary:active{background-color:#252a2b}.button.is-tertiary.is-disabled{background-color:#191c1b;color:rgba(170,203,228,.5)}.button.is-error{background-color:#2b2827;color:#ffb4ab}.button.is-error:hover{background-color:#3c3331}.button.is-error:active{background-color:#2b2827}.button.is-error.is-disabled{background-color:#191c1b;color:rgba(255,180,171,.5)}}/*# sourceMappingURL=Button.module.css.map */
diff --git a/components/elements/Button.module.css.map b/components/elements/Button.module.css.map
@@ -1 +1 @@
-{"version":3,"sourceRoot":"","sources":["Button.module.scss","../../styles/core/_elevate.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AA4BA,QACE,oBACA,UA1BU,KA2BV,QA1BQ,OA2BR,OA1BO,KA2BP,uBACA,mBACA,+BACA,cA5Bc,OA6Bd,mBACA,mDACA,iBACA,eCYA,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,wBAsBE,mBCMJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,wBAsBE,qBCMJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,0BAsBE,oBCMJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,yBAsBE,iBCMJ,yBDxCA,UAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE,uBEwGF,mCFpGF,QCwBE,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,2BAsBE,mBCMJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,0BAsBE,qBCMJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,2BAsBE,oBCMJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,2BAsBE,iBCMJ,yBDxCA,cAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE","file":"Button.module.css"}
-\ No newline at end of file
+{"version":3,"sourceRoot":"","sources":["Button.module.scss","../../styles/core/_elevate.scss","../../styles/core/_colors.scss"],"names":[],"mappings":"AA4BA,QACE,qBACA,UA1BU,KA2BV,QA1BQ,OA2BR,OA1BO,KA2BP,kBACA,+BACA,cA3Bc,OA4Bd,mBACA,mDACA,iBACA,eCaA,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,wBAqBE,mBCOJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,wBAqBE,qBCOJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,0BAqBE,oBCOJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,yBAqBE,iBCOJ,yBDxCA,UAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE,uBEwGF,mCFpGF,QCwBE,yBDxCA,cAEA,cCsCA,yBDlCA,eCkCA,yBD9BA,oBC8BA,yBD5BE,2BAqBE,mBCOJ,yBDxCA,cAEA,yBCsCA,yBDlCA,0BCkCA,yBD9BA,+BC8BA,yBD5BE,0BAqBE,qBCOJ,yBDxCA,cAEA,2BCsCA,yBDlCA,4BCkCA,yBD9BA,iCC8BA,yBD5BE,2BAqBE,oBCOJ,yBDxCA,cAEA,0BCsCA,yBDlCA,2BCkCA,yBD9BA,gCC8BA,yBD5BE,2BAqBE,iBCOJ,yBDxCA,cAEA,uBCsCA,yBDlCA,wBCkCA,yBD9BA,6BC8BA,yBD5BE","file":"Button.module.css"}
+\ No newline at end of file
diff --git a/components/elements/Button.module.scss b/components/elements/Button.module.scss
@@ -27,12 +27,11 @@ $border-radius: 9999px;
}
.button {
- display: inline-flex;
+ display: inline-block;
min-width: $min-width;
padding: $padding;
height: $height;
- justify-content: center;
- align-items: center;
+ text-align: center;
border: 1px solid transparent;
border-radius: $border-radius;
vertical-align: top;
diff --git a/components/elements/Button.tsx b/components/elements/Button.tsx
@@ -1,17 +1,30 @@
import classNames from '@/lib/classnames'
-import { HTMLAttributes } from 'react'
+import { HTMLAttributes, InputHTMLAttributes } from 'react'
import styles from './Button.module.css'
-export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
+export interface CommonButtonProps {
color?: 'primary' | 'secondary' | 'tertiary' | 'error'
disabled?: boolean
}
-export default function Button (props: ButtonProps) {
+export type ButtonProps = HTMLAttributes<HTMLButtonElement> & CommonButtonProps
+export type SubmitButtonProps = InputHTMLAttributes<HTMLInputElement> & CommonButtonProps
+
+export function Button (props: ButtonProps) {
+ return ButtonImpl('button', props)
+}
+
+export function SubmitButton (props: SubmitButtonProps) {
+ return ButtonImpl('input', props, {
+ type: 'submit',
+ })
+}
+
+function ButtonImpl<T extends CommonButtonProps> (Tag: any, props: T, additionalProps: Partial<T> = {}) {
const { color, disabled, ...restProps } = props
return (
- <button
+ <Tag
{...classNames(
styles.button,
color ? `is-${props.color}` : null,
@@ -19,6 +32,7 @@ export default function Button (props: ButtonProps) {
)}
disabled={disabled}
{...restProps}
+ {...additionalProps}
/>
)
}
diff --git a/components/form/Form.tsx b/components/form/Form.tsx
@@ -0,0 +1,20 @@
+import { FormEvent, ReactNode } from 'react'
+
+export interface FormProps {
+ onSubmit?: () => void
+
+ children?: ReactNode
+}
+
+export default function Form (props: FormProps) {
+ const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
+ e.preventDefault()
+ props.onSubmit?.()
+ }
+
+ return (
+ <form onSubmit={handleSubmit}>
+ {props.children}
+ </form>
+ )
+}
diff --git a/pages/users/login.tsx b/pages/users/login.tsx
@@ -1,15 +1,14 @@
-import Button from '@/components/elements/Button'
+import { useTokenFlushTrigger } from '@/components/contexts/TokenContext'
+import { SubmitButton } from '@/components/elements/Button'
import Title from '@/components/elements/Title'
import Field from '@/components/form/Field'
import Fields from '@/components/form/Fields'
+import Form from '@/components/form/Form'
import Container from '@/components/layout/Container'
import Hero from '@/components/layout/Hero'
import Section from '@/components/layout/Section'
-import { ApiError, isApiError } from '@/lib/apierror'
-import useApi from '@/lib/hooks/use_api'
-import { TokenResponse } from '@/pages/api/auth/token'
import { useRouter } from 'next/router'
-import { MouseEvent, useCallback, useEffect, useState } from 'react'
+import { useCallback, useState } from 'react'
export default function LoginPage () {
return (
@@ -31,71 +30,79 @@ function LoginForm () {
const router = useRouter()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
+ const flushToken = useTokenFlushTrigger()
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
+ const [isLoading, setLoading] = useState(false)
- const [err, setError] = useState<ApiError | null>(null)
- const [result, isLoading, statusCode, submit] = useApi<TokenResponse | ApiError, { email: string, password: string }>(
- '/api/auth/token',
- {
- method: 'POST',
- credentials: 'same-origin',
- body: () => ({ email, password }),
- },
- )
+ const handleSubmit = useCallback(() => {
+ const handleError = (status: number) => {
+ switch (status) {
+ case 401:
+ setErrorMessage('이메일 혹은 비밀번호가 잘못되었습니다.')
+ break
- const handleSubmit = useCallback((e: MouseEvent<HTMLButtonElement>) => {
- if (isLoading) {
- return
+ default:
+ setErrorMessage('알 수 없는 오류가 발생했습니다.')
+ break
+ }
}
- submit()
- }, [email, password])
+ setLoading(true)
+ setErrorMessage(null)
- // 로그인 요청 응답 처리
- useEffect(() => {
- if (result == null) {
- return
- }
+ !(async () => {
+ const res = await fetch('/api/auth/token', {
+ method: 'POST',
+ credentials: 'same-origin',
+ body: JSON.stringify({ email, password }),
+ })
- if (isApiError(result)) {
- setError(result)
- return
- } else {
- setError(null)
- }
+ if (!res.ok) {
+ handleError(res.status)
+ setLoading(false)
+ return
+ }
+
+ flushToken()
- // 메인 페이지(혹은 redirect_to)로 이동
- typeof router.query.redirect_to === 'string'
- ? router.push(router.query.redirect_to as string)
- : router.push('/')
- }, [result])
+ if (typeof router.query.redirect_to === 'string') {
+ await router.push(router.query.redirect_to as string)
+ } else {
+ await router.push('/')
+ }
+ })().catch ((err) => {
+ console.error(err)
+ setErrorMessage('알 수 없는 오류가 발생했습니다.')
+ })
+ }, [email, password])
return (
- <Fields>
- <Field
- type="text"
- color={err?.code != null ? 'error' : undefined}
- disabled={isLoading || result != null}
- placeholder="Email"
- value={email}
- onValueChange={setEmail}
- />
- <Field
- type="password"
- color={err?.code != null ? 'error' : undefined}
- disabled={isLoading || result != null}
- placeholder="Password"
- value={password}
- message={err?.code}
- onValueChange={setPassword}
- />
+ <Form onSubmit={handleSubmit}>
+ <Fields>
+ <Field
+ type="text"
+ color={errorMessage != null ? 'error' : undefined}
+ disabled={isLoading}
+ placeholder="Email"
+ value={email}
+ message={errorMessage ?? undefined}
+ onValueChange={setEmail}
+ />
+ <Field
+ type="password"
+ color={errorMessage != null ? 'error' : undefined}
+ disabled={isLoading}
+ placeholder="Password"
+ value={password}
+ onValueChange={setPassword}
+ />
- <Button
- color="primary"
- disabled={isLoading}
- onClick={handleSubmit}
- >
- Login
- </Button>
- </Fields>
+ <SubmitButton
+ color="primary"
+ disabled={isLoading}
+ value={'Login'}
+ />
+ </Fields>
+ </Form>
)
}
diff --git a/pages/users/signup/[id].tsx b/pages/users/signup/[id].tsx
@@ -1,7 +1,8 @@
-import Button from '@/components/elements/Button'
+import { Button, SubmitButton } from '@/components/elements/Button'
import Title from '@/components/elements/Title'
import Field from '@/components/form/Field'
import Fields from '@/components/form/Fields'
+import Form from '@/components/form/Form'
import Container from '@/components/layout/Container'
import Hero from '@/components/layout/Hero'
import Section from '@/components/layout/Section'
@@ -9,7 +10,7 @@ import { isApiError } from '@/lib/apierror'
import { getUnconfirmedSignupRequest } from '@/lib/models/signup_request'
import { GetServerSideProps } from 'next'
import { useRouter } from 'next/router'
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useState } from 'react'
interface PageProps {
id: number
@@ -51,6 +52,7 @@ export default function SignUpCompletePage (props: PageProps) {
const [passwordRepeatError, setPasswordRepeatError] = useState<string | null>(null)
const [nickname, setNickname] = useState('')
const [nicknameError, setNicknameError] = useState<string | null>(null)
+ const [isLoading, setLoading] = useState(false)
// 비밀번호 확인 오류 표시
useEffect(() => {
@@ -64,39 +66,74 @@ export default function SignUpCompletePage (props: PageProps) {
)
}, [password, passwordRepeat])
- const doSignUp = async () => {
- const res = await fetch(`/api/users/signup/${requestId}`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- password,
- nickname,
- }),
- })
+ const handleSubmit = useCallback(() => {
+ const handleError = (status: number, err: any) => {
+ if (!isApiError(err)) {
+ setPasswordError('알 수 없는 오류가 발생했습니다.')
+ return
+ }
- if (res.ok) {
- await router.push('/users/login')
- return
- }
+ switch (status) {
+ case 400:
+ switch (err.code) {
+ case 'invalid_password':
+ setPasswordError('비밀번호가 비어있거나 유효하지 않습니다.')
+ return
- const data = await res.json()
- if (isApiError(data)) {
- switch (data.field) {
- case 'nickname':
- setNicknameError(data.code)
+ case 'invalid_nickname':
+ setNicknameError('닉네임이 비어있거나 유효하지 않습니다.')
+ return
+ }
break
- default:
- setPasswordError(data.code)
+ case 409:
+ switch (err.code) {
+ case 'nickname_duplicated':
+ setNicknameError('이미 사용중인 닉네임입니다.')
+ return
+ }
break
}
+
+ setPasswordError('알 수 없는 오류가 발생했습니다.')
}
- }
+
+ setLoading(false)
+ setPasswordError(null)
+ setNicknameError(null)
+
+ if (password !== passwordRepeat) {
+ setPasswordRepeatError('비밀번호가 일치하지 않습니다.')
+ return
+ }
+
+ !(async () => {
+ const res = await fetch(`/api/users/signup/${requestId}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ password,
+ nickname,
+ }),
+ })
+
+ if (res.ok) {
+ await router.push('/users/login')
+ return
+ }
+
+ const err = await res.json()
+ handleError(res.status, err)
+ })().catch((err) => {
+ console.error(err)
+ setPasswordError('알 수 없는 오류가 발생했습니다.')
+ })
+ }, [])
return (
- <>
+ <Form onSubmit={handleSubmit}>
<Hero>
<Title kind="headline">Sign up</Title>
</Hero>
@@ -136,14 +173,13 @@ export default function SignUpCompletePage (props: PageProps) {
/>
</Fields>
- <Button
+ <SubmitButton
color="primary"
- onClick={doSignUp}
- >
- Sign up
- </Button>
+ disabled={isLoading}
+ value={'Sign up'}
+ />
</Container>
</Section>
- </>
+ </Form>
)
}
diff --git a/pages/users/signup/index.tsx b/pages/users/signup/index.tsx
@@ -1,15 +1,13 @@
-import Button from '@/components/elements/Button'
+import { SubmitButton } from '@/components/elements/Button'
import Title from '@/components/elements/Title'
import Field from '@/components/form/Field'
import Fields from '@/components/form/Fields'
+import Form from '@/components/form/Form'
import Container from '@/components/layout/Container'
import Hero from '@/components/layout/Hero'
import Section from '@/components/layout/Section'
-import { ApiError } from '@/lib/apierror'
-import useApi from '@/lib/hooks/use_api'
-import { SignUpResponse } from '@/pages/api/users/signup'
import { useRouter } from 'next/router'
-import { MouseEvent, useEffect, useMemo, useState } from 'react'
+import { useCallback, useState } from 'react'
export default function SignUpPage () {
return (
@@ -30,52 +28,54 @@ export default function SignUpPage () {
function SignUpForm () {
const router = useRouter()
const [email, setEmail] = useState('')
- const [result, isLoading, statusCode, submit] = useApi<SignUpResponse, { email: string }>(
- '/api/users/signup',
- {
- method: 'POST',
- body: () => ({ email }),
- },
- )
+ const [isLoading, setLoading] = useState(false)
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
- const err = useMemo(() => {
- if (result == null || 'status' in result) {
- return null
- }
- return result
- }, [result])
+ const handleSubmit = useCallback(() => {
+ const handleError = (status: number) => {
+ switch (status) {
+ case 401:
+ setErrorMessage('이메일 혹은 비밀번호가 잘못되었습니다.')
+ break
- const handleSubmit = (e: MouseEvent<HTMLButtonElement>) => {
- if (isLoading) {
- return
+ default:
+ setErrorMessage('알 수 없는 오류가 발생했습니다.')
+ break
+ }
}
- submit()
- }
+ setLoading(true)
+ setErrorMessage(null)
- useEffect(() => {
- if (result == null) {
- return
- }
+ !(async () => {
+ const res = await fetch('/api/users/signup', {
+ method: 'POST',
+ body: JSON.stringify({ email }),
+ })
- if (!('status' in result)) {
- return
- }
+ if (!res.ok) {
+ handleError(res.status)
+ setLoading(false)
+ return
+ }
- router.push('/users/signup/sent')
- .catch(console.error)
- }, [result])
+ await router.push('/users/signup/sent')
+ })().catch((err) => {
+ console.error(err)
+ setErrorMessage('알 수 없는 오류가 발생했습니다.')
+ })
+ }, [email])
return (
- <>
+ <Form onSubmit={handleSubmit}>
<Fields>
<Field
type="text"
- disabled={isLoading || result != null}
+ disabled={isLoading}
placeholder="Email"
- color={err == null ? undefined : 'error'}
+ color={errorMessage == null ? undefined : 'error'}
value={email}
- message={err == null ? undefined : err.code}
+ message={errorMessage ?? undefined}
onValueChange={setEmail}
/>
</Fields>
@@ -84,13 +84,11 @@ function SignUpForm () {
메일에 포함된 링크를 통해, 가입을 진행할 수 있습니다.</p>
<p>메일 주소는 공개되지 않습니다.</p>
- <Button
+ <SubmitButton
color="primary"
- disabled={isLoading || result != null}
- onClick={handleSubmit}
- >
- Sign up
- </Button>
- </>
+ disabled={isLoading}
+ value="Sign up"
+ />
+ </Form>
)
}