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

회원가입 기능 추가 #12

Merged
merged 5 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion app/[locale]/(navigation)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@ import useToastStore from '@/stores/toast-state';
import { useLocale } from 'next-intl';
import Link from 'next/link';
import { AuthInfoSchema, authInfoSchema } from './schema';
import { useState } from 'react';

export default function LoginPage() {
const { open } = useToastStore();
const [isLoading, setIsLoading] = useState(false);
const locale = useLocale();

const onSubmit = async (data: AuthInfoSchema) => {
try {
setIsLoading(true);
await authenticate(data);
setIsLoading(false);
} catch (error) {
open('로그인에 실패했습니다. 다시 시도해주세요.');
}
Expand All @@ -32,7 +36,7 @@ export default function LoginPage() {
placeholder='비밀번호'
/>
<Form.Group className='mt-4'>
<Form.Button type='submit' variant='bottom'>
<Form.Button type='submit' variant='bottom' isLoading={isLoading}>
로그인
</Form.Button>
<Button variant='transparent' animateOnClick>
Expand Down
41 changes: 41 additions & 0 deletions app/[locale]/signup/info/action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use server';

import api from '@/api';
import { API_ROUTES } from '@/constants';
import { SignUpSchema } from './schema';
import { TokenSchema } from '../schema';
import { redirect } from 'next/navigation';

export async function checkNicknameDuplicate(nickname: string) {
try {
const res = await api.get<{ data: boolean }>(
API_ROUTES.user.valid(nickname)
);
if (res.data === false) {
throw new Error('이미 사용중인 닉네임입니다.');
}
} catch (error) {
throw error;
}
}

type SignUpReqeust = {
nickname: SignUpSchema['nickname'];
password: SignUpSchema['password'];
};

type SignUpResponse = {
message: string;
};

export async function signUp({
nickname,
password,
token,
}: SignUpReqeust & TokenSchema) {
await api.post<SignUpReqeust, SignUpResponse>(API_ROUTES.user.signup(token), {
nickname,
password,
});
redirect('/ko/signup/complete');
}
132 changes: 132 additions & 0 deletions app/[locale]/signup/info/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
'use client';

import { Funnel, Header } from '@/components/signup';
import { AnimatePresence } from 'framer-motion';
import { useSearchParams } from 'next/navigation';
import { TransformerSubtitle } from '@/components/signup/header';
import { Button, Form } from '@/components/common';
import { useEffect, useRef, useState } from 'react';

import { useLocale } from 'next-intl';
import { useRouter } from 'next/navigation';
import { tokenSchema } from '../schema';
import { nickNameSchema, passwordSchema, signUpSchema } from './schema';
import { checkNicknameDuplicate, signUp } from './action';

const steps = ['닉네임', '비밀번호'] as const;

type Steps = (typeof steps)[number];

export default function Page() {
const [loading, setLoading] = useState(false);
const [step, setStep] = useState<Steps>('닉네임');
const currentStep = steps.indexOf(step);
const isLastStep = currentStep === steps.length;
const [nicknameError, setNicknameError] = useState<string>('');
const passwordRef = useRef<HTMLInputElement>(null);
const passwordCheckRef = useRef<HTMLInputElement>(null);

const searchParams = useSearchParams();
const token = searchParams.get('token');
const validToken = tokenSchema.safeParse({ token });
const locale = useLocale();
const router = useRouter();

if (!token || !validToken.success) {
throw new Error('비정상적인 토큰입니다.');
}

const handleSubmit = async (data: any) => {
switch (step) {
case '닉네임':
setLoading(true);
const unique = await verifyNickname(data.nickname);
setLoading(false);
unique && onNext(step);
break;
case '비밀번호':
setLoading(true);
try {
await signUp({
nickname: data.nickname,
password: data.password,
token,
});
router.push(`/${locale}/signup/complete`);
} catch (error) {
setLoading(false);
throw error;
}
break;
}
};

const verifyNickname = async (nickname: string) => {
try {
await checkNicknameDuplicate(nickname);
if (nicknameError) setNicknameError('');
return true;
} catch (error) {
setNicknameError('이미 사용중인 닉네임입니다.');
return false;
}
};

const onNext = async (currentStep: Steps) => {
if (isLastStep) return;
if (currentStep === '닉네임') {
setStep('비밀번호');
}
};

useEffect(() => {
if (passwordRef.current && step === '비밀번호') {
passwordRef.current.focus();
}
}, [passwordRef, step]);

return (
<AnimatePresence initial={false}>
<Header>
<Header.Title>사용자 정보 설정</Header.Title>
<Header.Subtitle>
{step === '닉네임' && <TransformerSubtitle text='닉네임을' />}
{step === '비밀번호' && <TransformerSubtitle text='비밀번호를' />}
<div className='ml-1'>입력해주세요.</div>
</Header.Subtitle>
</Header>
<Form
schema={step === '닉네임' ? nickNameSchema : signUpSchema}
onSubmit={handleSubmit}
validateOn='onChange'
>
<Funnel<typeof steps> step={step} steps={steps}>
<Funnel.Step name='비밀번호'>
<Form.Password
ref={passwordCheckRef}
label='비밀번호 확인'
name='passwordCheck'
placeholder='8자 이상'
/>
<Form.Password
ref={passwordRef}
label='비밀번호'
placeholder='8자 이상'
/>
</Funnel.Step>
<Funnel.Step name='닉네임'>
<Form.Text
label='닉네임'
placeholder='날으는 다람쥐'
name='nickname'
customError={nicknameError}
/>
</Funnel.Step>
</Funnel>
<Form.Button variant='bottom' isLoading={loading}>
다음
</Form.Button>
</Form>
</AnimatePresence>
);
}
27 changes: 27 additions & 0 deletions app/[locale]/signup/info/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { z } from 'zod';

export const nickNameSchema = z.object({
nickname: z.string().min(2, '닉네임은 2자리 이상 입력해주세요.'),
});

export const passwordSchema = z.object({
password: z.string().min(8, '비밀번호는 8자리 이상 입력해주세요.'),
});

export const signUpSchema = z
.object({
nickname: z.string().min(2, '닉네임은 2자리 이상 입력해주세요.'),
password: z.string().min(8, '비밀번호는 8자리 이상 입력해주세요.'),
passwordCheck: z.string(),
})
.superRefine(({ passwordCheck, password }, ctx) => {
if (passwordCheck !== password) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '비밀번호가 일치하지 않습니다.',
path: ['passwordCheck'],
});
}
});

export type SignUpSchema = z.infer<typeof signUpSchema>;
8 changes: 3 additions & 5 deletions app/[locale]/signup/phone/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import api from '@/api';
import { API_ROUTES } from '@/constants';
import { PhoneNumberSchema } from './schema';
import { redirect } from 'next/navigation';

export async function sendSMSCode({
phoneNumber,
Expand All @@ -22,9 +23,6 @@ export async function verifySMSCode({
code: string;
token: string;
}) {
try {
await api.post(API_ROUTES.user.sms.verify(token), { code });
} catch (error) {
throw new Error('인증번호가 일치하지 않습니다.');
}
await api.post(API_ROUTES.user.sms.verify(token), { code });
redirect(`/ko/signup/info?token=${token}`);
}
29 changes: 14 additions & 15 deletions app/[locale]/signup/phone/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ import {
type SMSCodeSchema,
phoneNumberSchema,
smsCodeSchema,
tokenSchema,
} from './schema';
import { useLocale } from 'next-intl';
import { useRouter } from 'next/navigation';
import { tokenSchema } from '../schema';

const steps = ['전화번호', '인증번호'] as const;

Expand Down Expand Up @@ -48,17 +48,15 @@ export default function Page() {
setLoading(false);

onNext(step);
openBT();
};

const handleSMSCodeSubmit = async ({ code }: SMSCodeSchema) => {
try {
setLoading(true);
await verifySMSCode({ code, token });

closeBT();
setLoading(false);
router.push(`/${locale}/signup/success`);
} catch (error) {
setLoading(false);
throw error;
Expand All @@ -70,7 +68,6 @@ export default function Page() {
if (currentStep === '전화번호') {
setStep('인증번호');
openBT();
} else if (currentStep === '인증번호') {
}
};

Expand All @@ -83,7 +80,7 @@ export default function Page() {
return (
<AnimatePresence initial={false}>
<Header>
<Header.Title>단국대학교 재학생 인증</Header.Title>
<Header.Title>휴대폰 인증</Header.Title>
<Header.Subtitle>
<TransformerSubtitle text='전화번호를' />
<div className='ml-1'>입력해주세요.</div>
Expand All @@ -100,6 +97,17 @@ export default function Page() {
name='phoneNumber'
label='사용자 전화번호'
placeholder='01012345678'
inputMode='tel'
onChange={async (e) => {
if (e.target.value.length === 11) {
setLoading(true);
await handlePhoneNumberSubmit({
phoneNumber: e.target.value,
});
setLoading(false);
}
return e.target.value;
}}
/>
<Form.Button variant='bottom' isLoading={loading}>
다음
Expand All @@ -124,7 +132,6 @@ export default function Page() {
placeholder='숫자 6자리'
label='발송된 인증번호 입력'
onChange={(v) => {
console.log(v);
if (v.length === 6) {
setLoading(true);
handleSMSCodeSubmit({ code: v });
Expand All @@ -136,14 +143,6 @@ export default function Page() {
<span className='text-xs mt-4'>
휴대폰으로 발송된 6자리 인증번호를 입력해주세요.
</span>
{/* <Button
type='button'
className='mt-6'
variant='transparent'
onClick={async () => await sendSMSCode({ phoneNumber, token })}
>
재전송
</Button> */}
<Form.Button
type='submit'
className='mt-14'
Expand Down
8 changes: 1 addition & 7 deletions app/[locale]/signup/phone/schema.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { z } from 'zod';

export const phoneNumberSchema = z.object({
phoneNumber: z.string().length(11, '전화번호는 1자리로 입력해주세요.'),
phoneNumber: z.string().length(11, '전화번호는 11자리로 입력해주세요.'),
});

export type PhoneNumberSchema = z.infer<typeof phoneNumberSchema>;
Expand All @@ -11,9 +11,3 @@ export const smsCodeSchema = z.object({
});

export type SMSCodeSchema = z.infer<typeof smsCodeSchema>;

export const tokenSchema = z.object({
token: z.string().uuid(),
});

export type TokenSchema = z.infer<typeof tokenSchema>;
7 changes: 7 additions & 0 deletions app/[locale]/signup/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const tokenSchema = z.object({
token: z.string().uuid(),
});

export type TokenSchema = z.infer<typeof tokenSchema>;
9 changes: 6 additions & 3 deletions app/[locale]/signup/studentId/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export default function Page() {
<Header.Subtitle>
{step === '학번' && <TransformerSubtitle text='학번을' />}
{step === '비밀번호' && <TransformerSubtitle text='비밀번호를' />}
<div className='ml-1'>입력해주세요.</div>
{step === '약관동의' && (
<TransformerSubtitle text='약관에 동의해주세요.' />
)}
{step !== '약관동의' && <div className='ml-1'>입력해주세요.</div>}
</Header.Subtitle>
</Header>
<Form
Expand All @@ -96,16 +99,16 @@ export default function Page() {
<Funnel.Step name='비밀번호'>
<Form.Password
ref={passwordRef}
label='단국대학교 포털 비밀번호'
name='dkuPassword'
label='단국대학교 포털 비밀번호'
placeholder='8자 이상의 영문, 숫자'
/>
</Funnel.Step>
<Funnel.Step name='학번'>
<Form.ID
name='dkuStudentId'
placeholder='숫자 8자리'
label='단국대학교 포털 아이디'
placeholder='숫자 8자리'
onChange={async (event) => {
if (isStudentId(event.target.value) && step === '학번') {
onNext(steps[currentStep]);
Expand Down
Loading
Loading