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"; |
||||
let token: string | null = null; |
||||
const KEY = "access_token"; |
||||
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) => { |
||||
token = t; |
||||
if (typeof window !== "undefined") { |
||||
if (t) localStorage.setItem(itemKey, t); |
||||
else localStorage.removeItem(itemKey); |
||||
if (t) localStorage.setItem(KEY, t); |
||||
else localStorage.removeItem(KEY); |
||||
} |
||||
}; |
||||
|
||||
export const loadTokenFromStorage = () => { |
||||
if (typeof window === "undefined") return; |
||||
token = localStorage.getItem(itemKey); |
||||
token = localStorage.getItem(KEY); |
||||
}; |
||||
|
Loading…
Reference in new issue