Skip to content

Commit

Permalink
Merge pull request #13 from ho991217/dev
Browse files Browse the repository at this point in the history
v0.0.1 배포
  • Loading branch information
ho991217 authored Mar 24, 2024
2 parents feeeddd + ec48f5c commit 1e74fa8
Show file tree
Hide file tree
Showing 33 changed files with 5,275 additions and 120 deletions.
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

0 comments on commit 1e74fa8

Please sign in to comment.