Skip to content

Commit

Permalink
Merge pull request #50 from Myongji-Graduate/sign-up-form/#7
Browse files Browse the repository at this point in the history
Sign up form/#7
  • Loading branch information
seonghunYang authored Mar 27, 2024
2 parents 3fb4a86 + f6e7028 commit e44512e
Show file tree
Hide file tree
Showing 38 changed files with 2,632 additions and 144 deletions.
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
};
export default config;
5 changes: 5 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import type { Preview } from '@storybook/react';
import '../app/globals.css';
import { handlers } from '../app/mocks/handlers';
import { initialize, mswDecorator } from 'msw-storybook-addon';

initialize({}, [...handlers]);

const preview: Preview = {
decorators: [mswDecorator],
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
Expand Down
34 changes: 34 additions & 0 deletions app/(sub-page)/sign-up/components/sign-up-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';
import useFunnel from '@/app/hooks/useFunnel';
import SignUpForm from '@/app/ui/user/sign-up-form/sign-up-form';
import SignUpTerm from './sign-up-terms';
import SignUpSuccess from './sign-up-success';
import ContentContainer from '@/app/ui/view/atom/content-container';

export default function SignUpContainer() {
const { Funnel, setStep } = useFunnel<'terms' | 'form' | 'success'>('terms');

return (
<div className="p-6">
<Funnel>
<Funnel.Step name="terms">
<SignUpTerm
onNext={() => {
setStep('form');
}}
/>
</Funnel.Step>
<Funnel.Step name="form">
<SignUpForm
onNext={() => {
setStep('success');
}}
/>
</Funnel.Step>
<Funnel.Step name="success">
<SignUpSuccess />
</Funnel.Step>
</Funnel>
</div>
);
}
25 changes: 25 additions & 0 deletions app/(sub-page)/sign-up/components/sign-up-success.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Button from '@/app/ui/view/atom/button/button';
import Link from 'next/link';

// 내용이랑 스타일은 mock인 상태입니다.
export default function SignUpSuccess() {
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center px-4 sm:px-6">
<div className="max-w-md w-full space-y-8">
<div className="space-y-2">
<h2 className="text-3xl font-extrabold tracking-tight">Youre all set.</h2>
<p className="text-gray-500">
Thanks for signing up! We just need to verify your email address to complete the process.
</p>
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Link className="inline-block w-full" href="/login">
<Button className="w-full" label={'로그인 하기'} />
</Link>
</div>
</div>
</div>
</div>
);
}
56 changes: 56 additions & 0 deletions app/(sub-page)/sign-up/components/sign-up-terms.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Button from '@/app/ui/view/atom/button/button';

interface SignUpTermProps {
onNext?: () => void;
}

// 약관 내용이랑 스타일은 mock인 상태입니다.
export default function SignUpTerm({ onNext }: SignUpTermProps) {
const handleAgreeButtonClick = () => {
onNext?.();
};

return (
<div className="max-w-2xl mx-auto my-8 p-6 bg-white rounded-lg shadow-md">
<h1 className="text-3xl font-bold text-center mb-6">알림독의 안내문</h1>
<ul className="list-disc space-y-4 text-sm">
<li>
현재 저희 기능은 한국-한국은 아니라 한국-외국도 가능합니다. 각사별에서 수하인 없더라도 저희가 받는데까지는
무관합니다만, 꼭 관세사무소와 협의하세요!
<ul className="list-disc ml-6 mt-2">
<li>대상: 국외발송, 북부발송, 사회복지대상, ICT용품대상, 일반대상, 미래용품대상(확인)</li>
<li>발송: 16 ~ 22시발</li>
</ul>
</li>
<li>
교직, 디자인, 연계개발, 물품, 전자, 자원관리/회계지원에 해당하는 사용자는 각사 기준에 따른 선정되지 않아 각사
별 관리하는데요.
</li>
<li>검사를 위해서 선적품을 직접 연락드려야만 PC화면에서 진행하는 것을 권장합니다.</li>
<li>
검사 기준은 최신버전 확인내역(2023.07.24) 반영하여 선정되었으며, 학사내역은 매년 개편되므로 자사이 외고 있는
구버전과 다를 수 있습니다.
<ul className="list-disc ml-6 mt-2">
<li>문자대항: 학사내역은 확인 클릭</li>
</ul>
</li>
<li>
본 서비스 정보는 공식적인 확인을 전제 않으며, 정확한 증상조사결과를 위해 서류 또는 담당과 교류해야할 사항을
잊지 않습니다.
</li>
<li>
전자문 서화지 데이터베이스는 의무화되어 저희가 고유축적 및 교육과정 등에서 사용되며, 어떤 다른 용도로 사용
되지 않습니다.
</li>
<li>특허요건 기준이 전문 선정되었거나, 오류발생 시 우측 하단 채팅창으로 또는 담당 부서로문의합니다.</li>
</ul>
<div className="mt-8 flex justify-center">
<Button
onClick={handleAgreeButtonClick}
className="ml-4 bg-blue-500 text-white py-2 px-4 rounded-full"
label={'알림독의 안내문'}
/>
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions app/(sub-page)/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ContentContainer from '@/app/ui/view/atom/content-container';
import SignUpContainer from './components/sign-up-container';
import { Suspense } from 'react';
import LoadingSpinner from '@/app/ui/view/atom/loading-spinner';

// Refactor: fallback 스켈레톤으로 대체
export default function Page() {
return (
<ContentContainer className="md:w-[768px]">
<Suspense
fallback={
<div className="h-96">
<LoadingSpinner />
</div>
}
>
<SignUpContainer />
</Suspense>
</ContentContainer>
);
}
14 changes: 0 additions & 14 deletions app/__test__/ui/invoice/revenu-chart.test.tsx

This file was deleted.

3 changes: 2 additions & 1 deletion app/business/api-path.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const BASE_URL = process.env.API_MOCKING === 'enable' ? 'http://localhost:9090' : 'http://mock.api.com';
const BASE_URL = process.env.API_MOCKING === 'enable' ? 'http://localhost:9090' : 'https://mock.api.com';

export const API_PATH = {
revenue: `${BASE_URL}/revenue`,
registerUserGrade: `${BASE_URL}/registerUserGrade`,
parsePDFtoText: `${BASE_URL}/parsePDFtoText`,
takenLectures: `${BASE_URL}/taken-lectures`,
user: `${BASE_URL}/users`,
};
67 changes: 0 additions & 67 deletions app/business/auth/user.command.ts

This file was deleted.

2 changes: 1 addition & 1 deletion app/business/lecture/taken-lecture.query.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LectureInfo } from '@/app/type/lecture';
import { API_PATH } from '../api-path';

interface TakenLectures {
export interface TakenLectures {
totalCredit: number;
takenLectures: LectureInfo[];
}
Expand Down
69 changes: 69 additions & 0 deletions app/business/user/user.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use server';

import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../api-path';
import { SignUpRequestBody } from './user.type';
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { BadRequestError } from '@/app/utils/http/http-error';
import { SignUpFormSchema } from './user.validation';

export async function createUser(prevState: FormState, formData: FormData): Promise<FormState> {
const validatedFields = SignUpFormSchema.safeParse({
authId: formData.get('authId'),
password: formData.get('password'),
confirmPassword: formData.get('confirmPassword'),
studentNumber: formData.get('studentNumber'),
engLv: formData.get('engLv'),
});

if (!validatedFields.success) {
return {
isSuccess: false,
isFailure: true,
validationError: validatedFields.error.flatten().fieldErrors,
message: '양식에 맞춰 다시 입력해주세요.',
};
}

const { authId, password, studentNumber, engLv } = validatedFields.data;
const body: SignUpRequestBody = {
authId,
password,
studentNumber,
engLv,
};

try {
const response = await fetch(`${API_PATH.user}/sign-up`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});

const result = await response.json();

httpErrorHandler(response, result);
} catch (error) {
if (error instanceof BadRequestError) {
// 잘못된 요청 처리 로직
return {
isSuccess: false,
isFailure: true,
validationError: {},
message: error.message,
};
} else {
// 나머지 에러는 더 상위 수준에서 처리
throw error;
}
}

return {
isSuccess: true,
isFailure: false,
validationError: {},
message: '회원가입이 완료되었습니다.',
};
}
9 changes: 9 additions & 0 deletions app/business/user/user.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// https://stackoverflow.com/questions/76957592/error-only-async-functions-are-allowed-to-be-exported-in-a-use-server-file
// server action 파일에서는 async function만 export 가능

export interface SignUpRequestBody {
authId: string;
password: string;
studentNumber: string;
engLv: string;
}
36 changes: 36 additions & 0 deletions app/business/user/user.validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { z } from 'zod';

export const SignUpFormSchema = z
.object({
authId: z
.string()
.min(6, {
message: '아이디는 6자 이상 20자 이하여야 합니다.',
})
.max(20, {
message: 'User ID must be at most 20 characters',
}),
password: z
.string()
.min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
.regex(/^(?=.*[A-Za-z])(?=.*\d)(?=.*[!@#$%^&*])[A-Za-z\d!@#$%^&*]{8,}$/, {
message: '비밀번호는 문자, 숫자, 특수문자(!@#$%^&*)를 포함해야 합니다.',
})
.max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
confirmPassword: z.string(),
studentNumber: z.string().length(8, { message: '학번은 8자리여야 합니다.' }).startsWith('60', {
message: '학번은 60으로 시작해야 합니다.',
}),
engLv: z.enum(['basic', 'ENG12', 'ENG34', 'bypass'], {
invalid_type_error: '올바른 영어 레벨을 선택해주세요.',
}),
})
.superRefine(({ confirmPassword, password }, ctx) => {
if (confirmPassword !== password) {
ctx.addIssue({
code: 'custom',
message: '비밀번호가 일치하지 않습니다.',
path: ['confirmPassword'],
});
}
});
6 changes: 1 addition & 5 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ export default async function Page() {
return (
<main>
<h1 className={'mb-4 text-xl md:text-2xl'}>Dashboard</h1>
<div className="mt-6">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
<div className="mt-6"></div>
</main>
);
}
Loading

0 comments on commit e44512e

Please sign in to comment.