Skip to content

Commit

Permalink
Merge branch 'main' into grade-result/#55
Browse files Browse the repository at this point in the history
  • Loading branch information
yougyung authored Apr 2, 2024
2 parents 9056c43 + dd277b4 commit ce31012
Show file tree
Hide file tree
Showing 31 changed files with 598 additions and 117 deletions.
1 change: 1 addition & 0 deletions .storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const config: StorybookConfig = {
config.resolve.alias = {
...config.resolve.alias,
'@': path.resolve(__dirname, '../'),
'next/headers': path.resolve(__dirname, '../app/utils/test/__mock__/next/headers.ts'),
};
}
return config;
Expand Down
15 changes: 15 additions & 0 deletions app/(sub-page)/my/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import LectureSearch from '@/app/ui/lecture/lecture-search';
import TakenLecture from '@/app/ui/lecture/taken-lecture';
import ContentContainer from '@/app/ui/view/atom/content-container';

export default function MyPage() {
return (
<ContentContainer className="flex">
<div className="hidden lg:w-[30%] lg:block">정보칸</div>
<div className="w-full lg:w-[70%] lg:px-[20px]">
<LectureSearch />
<TakenLecture />
</div>
</ContentContainer>
);
}
10 changes: 10 additions & 0 deletions app/(sub-page)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import ContentContainer from '@/app/ui/view/atom/content-container';
import SignInForm from '@/app/ui/user/sign-in-form/sign-in-form';

export default function Page() {
return (
<ContentContainer className="md:w-[768px]">
<SignInForm />
</ContentContainer>
);
}
2 changes: 1 addition & 1 deletion app/(sub-page)/sign-up/components/sign-up-success.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default function SignUpSuccess() {
</div>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Link className="inline-block w-full" href="/login">
<Link className="inline-block w-full" href="/sign-in">
<Button className="w-full" label={'로그인 하기'} />
</Link>
</div>
Expand Down
36 changes: 1 addition & 35 deletions app/__test__/ui/lecture/taken-lecture-list.test.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,9 @@
import LectureSearch from '@/app/ui/lecture/lecture-search';
import TakenLecture from '@/app/ui/lecture/taken-lecture';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

describe('Taken lecture list', () => {
it('기이수 과목 리스트를 보여준다.', async () => {
render(await TakenLecture());
expect(await screen.findByTestId('table-data'));
});

it('커스텀하기 버튼을 클릭하면 기이수 과목 리스트가 변경되며 과목 검색 컴포넌트가 렌더링된다.', async () => {
//given
render(await TakenLecture());
render(<LectureSearch />);

//when
const customButton = await screen.findByTestId('custom-button');
await userEvent.click(customButton);

//then
const deleteButton = await screen.findAllByTestId('taken-lecture-delete-button');
expect(deleteButton[0]).toBeInTheDocument();

const lectureSearchComponent = await screen.findByTestId('lecture-search-component');
expect(lectureSearchComponent).toBeInTheDocument();
});

it('커스텀 시 삭제 버튼을 클릭하면 해당하는 lecture가 사라진다', async () => {
//given
render(await TakenLecture());

const customButton = await screen.findByTestId('custom-button');
await userEvent.click(customButton);

//when
const deleteButton = await screen.findAllByTestId('taken-lecture-delete-button');
await userEvent.click(deleteButton[0]);

//then
expect(screen.queryByText('딥러닝')).not.toBeInTheDocument();
expect(await screen.findAllByTestId('table-data'));
});
});
1 change: 1 addition & 0 deletions app/business/api-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const API_PATH = {
parsePDFtoText: `${BASE_URL}/parsePDFtoText`,
takenLectures: `${BASE_URL}/taken-lectures`,
user: `${BASE_URL}/users`,
auth: `${BASE_URL}/auth`,
};
73 changes: 71 additions & 2 deletions app/business/user/user.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,79 @@

import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../api-path';
import { SignUpRequestBody } from './user.type';
import { SignUpRequestBody, SignInRequestBody } 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';
import { SignUpFormSchema, SignInFormSchema, SignInResponseSchema } from './user.validation';
import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';

export async function authenticate(prevState: FormState, formData: FormData): Promise<FormState> {
const validatedFields = SignInFormSchema.safeParse({
authId: formData.get('authId'),
password: formData.get('password'),
});

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

const body: SignInRequestBody = {
...validatedFields.data,
};

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

const result = await response.json();

httpErrorHandler(response, result);

if (isValidation(result, SignInResponseSchema)) {
cookies().set('accessToken', result.accessToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
});
cookies().set('refreshToken', result.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
});
}
} 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: '로그인 성공',
};
}

export async function createUser(prevState: FormState, formData: FormData): Promise<FormState> {
const validatedFields = SignUpFormSchema.safeParse({
Expand Down
10 changes: 10 additions & 0 deletions app/business/user/user.type.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
// https://stackoverflow.com/questions/76957592/error-only-async-functions-are-allowed-to-be-exported-in-a-use-server-file
// server action 파일에서는 async function만 export 가능

import { SignInResponseSchema } from './user.validation';
import z from 'zod';

export interface SignUpRequestBody {
authId: string;
password: string;
studentNumber: string;
engLv: string;
}

export interface SignInRequestBody {
authId: string;
password: string;
}

export type SignInResponse = z.infer<typeof SignInResponseSchema>;
10 changes: 10 additions & 0 deletions app/business/user/user.validation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { z } from 'zod';

export const SignInFormSchema = z.object({
authId: z.string(),
password: z.string(),
});

export const SignInResponseSchema = z.object({
accessToken: z.string(),
refreshToken: z.string(),
});

export const SignUpFormSchema = z
.object({
authId: z
Expand Down
7 changes: 6 additions & 1 deletion app/mocks/db.mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { TakenLectures } from '../business/lecture/taken-lecture.query';
import { SignUpRequestBody } from '../business/user/user.type';
import { SignUpRequestBody, SignInRequestBody } from '../business/user/user.type';
import { takenLectures } from './data.mock';

interface MockUser {
Expand All @@ -19,6 +19,7 @@ type MockDatabaseAction = {
getTakenLectures: () => TakenLectures[];
getUser: (authId: string) => MockUser | undefined;
createUser: (user: MockUser) => boolean;
signIn: (userData: SignInRequestBody) => boolean;
};

export const mockDatabase: MockDatabaseAction = {
Expand All @@ -31,6 +32,10 @@ export const mockDatabase: MockDatabaseAction = {
mockDatabaseStore.users = [...mockDatabaseStore.users, user];
return true;
},
signIn: (userData: SignInRequestBody) => {
const user = mockDatabaseStore.users.find((u) => u.authId === userData.authId && u.password === userData.password);
return !!user;
},
};

const initialState: MockDatabaseState = {
Expand Down
25 changes: 24 additions & 1 deletion app/mocks/handlers/user-handler.mock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { HttpResponse, http, delay } from 'msw';
import { API_PATH } from '../../business/api-path';
import { mockDatabase } from '../db.mock';
import { SignUpRequestBody } from '@/app/business/user/user.type';
import { SignUpRequestBody, SignInRequestBody, SignInResponse } from '@/app/business/user/user.type';
import { ErrorResponseData } from '@/app/utils/http/http-error-handler';

export const userHandlers = [
http.post<never, SignUpRequestBody, never>(`${API_PATH.user}/sign-up`, async ({ request }) => {
Expand All @@ -16,4 +17,26 @@ export const userHandlers = [

return HttpResponse.json({ status: 200 });
}),

http.post<never, SignInRequestBody, SignInResponse | ErrorResponseData>(
`${API_PATH.auth}/sign-in`,
async ({ request }) => {
const signInData = await request.json();

const isSuccess = mockDatabase.signIn(signInData);
await delay(500);

if (!isSuccess) {
return HttpResponse.json(
{ status: 400, message: '아이디 또는 비밀번호가 일치하지 않습니다.' },
{ status: 400 },
);
}

return HttpResponse.json({
accessToken: 'fake-access-token',
refreshToken: 'fake-refresh-token',
});
},
),
];
Loading

0 comments on commit ce31012

Please sign in to comment.