parent
75e05dc5ab
commit
8ac669cabb
@ -0,0 +1,15 @@ |
|||||||
|
{ |
||||||
|
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요. |
||||||
|
// 기존 특성에 대한 설명을 보려면 가리킵니다. |
||||||
|
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요. |
||||||
|
"version": "0.2.0", |
||||||
|
"configurations": [ |
||||||
|
{ |
||||||
|
"type": "chrome", |
||||||
|
"request": "launch", |
||||||
|
"name": "Next.js cliend debug", |
||||||
|
"url": "http://localhost:3000", |
||||||
|
"webRoot": "${workspaceFolder}" |
||||||
|
} |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,195 @@ |
|||||||
|
"use client"; |
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"; |
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; |
||||||
|
import { |
||||||
|
Form, |
||||||
|
FormControl, |
||||||
|
FormField, |
||||||
|
FormItem, |
||||||
|
FormMessage, |
||||||
|
} from "@/components/ui/form"; |
||||||
|
import { Input } from "@/components/ui/input"; |
||||||
|
import { api } from "@/lib/api"; |
||||||
|
import { zodResolver } from "@hookform/resolvers/zod"; |
||||||
|
import axios from "axios"; |
||||||
|
import Link from "next/link"; |
||||||
|
import { useRouter } from "next/navigation"; |
||||||
|
import { useState } from "react"; |
||||||
|
import { useForm } from "react-hook-form"; |
||||||
|
import z, { email } from "zod"; |
||||||
|
|
||||||
|
const SignupSchema = z |
||||||
|
.object({ |
||||||
|
name: z.string().min(1, "아이디를 입력하세요."), |
||||||
|
password: z.string().min(4, "비밀번호는 4자 이상이어야 합니다."), |
||||||
|
confirmPassword: z.string().min(1, "비밀번호 확인을 입력하세요."), |
||||||
|
email: z.email("이메일 형식이 아닙니다.").optional(), |
||||||
|
}) |
||||||
|
.refine((v) => v.password === v.confirmPassword, { |
||||||
|
path: ["confirmPassword"], |
||||||
|
message: "비밀번호가 일치하지 않습니다.", |
||||||
|
}); |
||||||
|
|
||||||
|
type SignupInput = z.infer<typeof SignupSchema>; |
||||||
|
|
||||||
|
export default function SignupPage() { |
||||||
|
const form = useForm<SignupInput>({ |
||||||
|
resolver: zodResolver(SignupSchema), |
||||||
|
defaultValues: { |
||||||
|
name: "", |
||||||
|
password: "", |
||||||
|
confirmPassword: "", |
||||||
|
email: undefined, |
||||||
|
}, |
||||||
|
mode: "onTouched", |
||||||
|
}); |
||||||
|
|
||||||
|
const [error, setError] = useState<string | null>(null); |
||||||
|
const [ok, setOk] = useState<string | null>(null); |
||||||
|
const [loading, setLoading] = useState(false); |
||||||
|
const router = useRouter(); |
||||||
|
|
||||||
|
const onSubmit = async (values: SignupInput) => { |
||||||
|
setError(null); |
||||||
|
setOk(null); |
||||||
|
setLoading(true); |
||||||
|
try { |
||||||
|
const payload: Record<string, unknown> = { |
||||||
|
name: values.name.trim(), |
||||||
|
password: values.password, |
||||||
|
}; |
||||||
|
const emailTrim = values.email?.trim(); |
||||||
|
if (emailTrim) payload.email = emailTrim; |
||||||
|
|
||||||
|
await api.post("/auth/signup", payload); |
||||||
|
setOk("회원가입이 완료되어습니다. 로그인 페이지로 이동합니다."); |
||||||
|
setTimeout(() => router.replace("/login"), 800); |
||||||
|
} catch (e: unknown) { |
||||||
|
if (axios.isAxiosError(e)) { |
||||||
|
setError( |
||||||
|
e.request?.data?.message ?? e.message ?? "회원가입에 실패했습니다." |
||||||
|
); |
||||||
|
} else if (e instanceof Error) { |
||||||
|
setError(e.message); |
||||||
|
} else { |
||||||
|
setError("회원가입에 실패했습니다."); |
||||||
|
} |
||||||
|
} finally { |
||||||
|
setLoading(false); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="flex items-center justify-center min-h-screen"> |
||||||
|
<div className="mx-auto w-full max-w-sm"> |
||||||
|
<Card className="shadow-lg"> |
||||||
|
<CardHeader> |
||||||
|
<CardTitle>회원가입</CardTitle> |
||||||
|
</CardHeader> |
||||||
|
<CardContent className="flex flex-col gap-y-2 pb-4"> |
||||||
|
<Form {...form}> |
||||||
|
<form |
||||||
|
className="space-y-4" |
||||||
|
onSubmit={form.handleSubmit(onSubmit)} |
||||||
|
> |
||||||
|
{/* name */} |
||||||
|
<FormField |
||||||
|
control={form.control} |
||||||
|
name="name" |
||||||
|
render={({ field }) => ( |
||||||
|
<FormItem> |
||||||
|
<FormControl> |
||||||
|
<Input |
||||||
|
placeholder="아이디" |
||||||
|
autoComplete="username" |
||||||
|
{...field} |
||||||
|
/> |
||||||
|
</FormControl> |
||||||
|
<FormMessage /> |
||||||
|
</FormItem> |
||||||
|
)} |
||||||
|
/> |
||||||
|
|
||||||
|
{/* email */} |
||||||
|
<FormField |
||||||
|
control={form.control} |
||||||
|
name="email" |
||||||
|
render={({ field }) => ( |
||||||
|
<FormItem> |
||||||
|
<FormControl> |
||||||
|
<Input |
||||||
|
placeholder="이메일(선택)" |
||||||
|
type="email" |
||||||
|
autoComplete="email" |
||||||
|
{...field} |
||||||
|
/> |
||||||
|
</FormControl> |
||||||
|
<FormMessage /> |
||||||
|
</FormItem> |
||||||
|
)} |
||||||
|
/> |
||||||
|
|
||||||
|
{/* password */} |
||||||
|
<FormField |
||||||
|
control={form.control} |
||||||
|
name="password" |
||||||
|
render={({ field }) => ( |
||||||
|
<FormItem> |
||||||
|
<FormControl> |
||||||
|
<Input |
||||||
|
placeholder="비밀번호" |
||||||
|
type="password" |
||||||
|
autoComplete="new-password" |
||||||
|
{...field} |
||||||
|
/> |
||||||
|
</FormControl> |
||||||
|
<FormMessage /> |
||||||
|
</FormItem> |
||||||
|
)} |
||||||
|
/> |
||||||
|
|
||||||
|
{/* confirmPassword */} |
||||||
|
<FormField |
||||||
|
control={form.control} |
||||||
|
name="confirmPassword" |
||||||
|
render={({ field }) => ( |
||||||
|
<FormItem> |
||||||
|
<FormControl> |
||||||
|
<Input |
||||||
|
placeholder="비밀번호 확인" |
||||||
|
type="password" |
||||||
|
autoComplete="new-password" |
||||||
|
{...field} |
||||||
|
/> |
||||||
|
</FormControl> |
||||||
|
<FormMessage /> |
||||||
|
</FormItem> |
||||||
|
)} |
||||||
|
/> |
||||||
|
|
||||||
|
<Button type="submit" className="w-full" disabled={loading}> |
||||||
|
{loading ? "처리 중..." : "가입하기"} |
||||||
|
</Button> |
||||||
|
|
||||||
|
{error && ( |
||||||
|
<p className="text-center text-sm text-red-600">{error}</p> |
||||||
|
)} |
||||||
|
{ok && ( |
||||||
|
<p className="text-center text-sm text-green-600">{ok}</p> |
||||||
|
)} |
||||||
|
|
||||||
|
<p className="text-center text-xs text-gray-500"> |
||||||
|
이미 계정이 있으신가요?{" "} |
||||||
|
<Link href="/login" className="underline"> |
||||||
|
로그인 |
||||||
|
</Link> |
||||||
|
</p> |
||||||
|
</form> |
||||||
|
</Form> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -1,17 +1,26 @@ |
|||||||
const itemKey = "access_token"; |
const KEY = "access_token"; |
||||||
let token: string | null = null; |
let token: string | null | undefined; |
||||||
|
|
||||||
export const getToken = () => token; |
export const getToken = (): string | null => { |
||||||
|
if (typeof window === "undefined") return null; |
||||||
|
if (token === undefined) { |
||||||
|
token = localStorage.getItem(KEY) ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
return token ?? null; |
||||||
|
}; |
||||||
|
|
||||||
|
export const hasToken = (): boolean => !!getToken(); |
||||||
|
|
||||||
export const setToken = (t: string | null) => { |
export const setToken = (t: string | null) => { |
||||||
token = t; |
token = t; |
||||||
if (typeof window !== "undefined") { |
if (typeof window !== "undefined") { |
||||||
if (t) localStorage.setItem(itemKey, t); |
if (t) localStorage.setItem(KEY, t); |
||||||
else localStorage.removeItem(itemKey); |
else localStorage.removeItem(KEY); |
||||||
} |
} |
||||||
}; |
}; |
||||||
|
|
||||||
export const loadTokenFromStorage = () => { |
export const loadTokenFromStorage = () => { |
||||||
if (typeof window === "undefined") return; |
if (typeof window === "undefined") return; |
||||||
token = localStorage.getItem(itemKey); |
token = localStorage.getItem(KEY); |
||||||
}; |
}; |
||||||
|
Loading…
Reference in new issue