Skip to content

Commit

Permalink
chore: Merge branch 'main' into cypress/#108
Browse files Browse the repository at this point in the history
  • Loading branch information
seonghunYang committed May 29, 2024
2 parents 43bbee5 + 87f7562 commit 0c59c59
Show file tree
Hide file tree
Showing 70 changed files with 1,049 additions and 255 deletions.
17 changes: 17 additions & 0 deletions app/(sub-page)/components/navigation-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import Image from 'next/image';
import logo from '../../../public/assets/logo.svg';
import Responsive from '../../ui/responsive';
import SideNavigationBar from './side-navigation-bar';
import UserInfoNavigator from '@/app/ui/user/user-info-navigator/user-info-navigator';
import SignButtonGroup from '@/app/ui/user/user-info-navigator/sign-button-group';

export default function NavigationBar() {
return (
<div className="absolute flex justify-between items-center p-4 border-b-[1px] w-full z-2">
<Image className="md:h-10 h-7 w-[110px] md:w-[150px]" width={150} height={100} src={logo} alt="main-logo" />
<Responsive maxWidth={1023}>
<SideNavigationBar header={<UserInfoNavigator />} content={<div>콘텐츠</div>} footer={<SignButtonGroup />} />
</Responsive>
</div>
);
}
39 changes: 39 additions & 0 deletions app/(sub-page)/components/side-navigation-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';
import { HamburgerMenuIcon } from '@radix-ui/react-icons';
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetFooter } from '../../ui/view/molecule/sheet/sheet';
import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util';
import useDialog from '@/app/hooks/useDialog';

interface SideNavigationBarProps {
header: React.ReactNode;
footer: React.ReactNode;
content: React.ReactNode;
}

export default function SideNavigationBar({ header, content, footer }: SideNavigationBarProps) {
const { isOpen, open, close } = useDialog(DIALOG_KEY.SIDE_NAVIGATION);

const handleSideNavOpen = (value: boolean) => {
if (value) {
open();
} else {
close();
}
};

return (
<Sheet open={isOpen} onOpenChange={handleSideNavOpen}>
<SheetTrigger className="h-6">
<HamburgerMenuIcon className="w-6 h-6 text-white" />
</SheetTrigger>
<SheetContent className="z-3">
<div className="flex h-full flex-col justify-between">
<SheetHeader>{header}</SheetHeader>
<div className="w-full h-1 rounded-full my-4 bg-gray-200" />
<div className="h-full">{content}</div>
<SheetFooter>{footer}</SheetFooter>
</div>
</SheetContent>
</Sheet>
);
}
2 changes: 1 addition & 1 deletion app/(sub-page)/grade-upload/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ContentContainer from '../../ui/view/atom/content-container';
import ContentContainer from '../../ui/view/atom/content-container/content-container';
import Manual from './components/manual';
import UploadTakenLecture from '../../ui/lecture/upload-taken-lecture/upload-taken-lecture';

Expand Down
2 changes: 1 addition & 1 deletion app/(sub-page)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Image from 'next/image';
import background from '../../public/assets/background.png';
import NavigationBar from '../ui/view/molecule/navigation-bar';
import NavigationBar from './components/navigation-bar';

interface LayoutProps {
children: React.ReactNode;
Expand Down
19 changes: 13 additions & 6 deletions app/(sub-page)/my/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@ import LectureSearch from '@/app/ui/lecture/lecture-search';
import TakenLecture from '@/app/ui/lecture/taken-lecture';
import UserInfoNavigator from '@/app/ui/user/user-info-navigator/user-info-navigator';
import UserInfoNavigatorSkeleton from '@/app/ui/user/user-info-navigator/user-info-navigator.skeleton';
import ContentContainer from '@/app/ui/view/atom/content-container';
import Drawer from '@/app/ui/view/molecule/drawer/drawer';
import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util';
import { Suspense } from 'react';
import MyResultContainer from './components/my-result-container';
import SignButtonGroup from '@/app/ui/user/user-info-navigator/sign-button-group';
import Responsive from '@/app/ui/responsive';
import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
import TakenLectureSkeleton from '@/app/ui/lecture/taken-lecture/taken-lecture.skeleton';

export default function MyPage() {
return (
<>
<ContentContainer className="flex">
<div className="hidden lg:w-[30%] lg:block">
<Suspense fallback={<UserInfoNavigatorSkeleton />}>
<UserInfoNavigator />
</Suspense>
</div>
<Responsive minWidth={1023}>
<div className="lg:w-[30%]">
<Suspense fallback={<UserInfoNavigatorSkeleton />}>
<UserInfoNavigator />
<div className="mt-9">
<SignButtonGroup />
</div>
</Suspense>
</div>
</Responsive>
<div className="w-full lg:w-[70%] lg:px-[20px] pt-12 pb-2 flex flex-col gap-12">
<MyResultContainer />
<Suspense fallback={<TakenLectureSkeleton />}>
Expand Down
2 changes: 1 addition & 1 deletion app/(sub-page)/result/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import UserInfoCard from '@/app/ui/user/user-info-card/user-info-card';
import ContentContainer from '@/app/ui/view/atom/content-container';
import ResultCategoryDetail from '@/app/ui/result/result-category-detail/result-category-detail';
import { Suspense } from 'react';
import UserInfoCardSkeleton from '@/app/ui/user/user-info-card/user-info-card.skeleton';
import ResultCategory from '@/app/ui/result/result-category/result-category';
import ResultCategorySkeleton from '@/app/ui/result/result-category/result-category.skeleton';
import ContentContainer from '@/app/ui/view/atom/content-container/content-container';

interface ResultPageProp {
searchParams: { category: string };
Expand Down
2 changes: 1 addition & 1 deletion app/(sub-page)/sign-in/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ContentContainer from '@/app/ui/view/atom/content-container';
import SignInForm from '@/app/ui/user/sign-in-form/sign-in-form';
import ContentContainer from '@/app/ui/view/atom/content-container/content-container';

export default function Page() {
return (
Expand Down
1 change: 0 additions & 1 deletion app/(sub-page)/sign-up/components/sign-up-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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');
Expand Down
4 changes: 2 additions & 2 deletions app/(sub-page)/sign-up/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
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';
import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
import LoadingSpinner from '@/app/ui/view/atom/loading-spinner/loading-spinner';

// Refactor: fallback 스켈레톤으로 대체
export default function Page() {
Expand Down
21 changes: 21 additions & 0 deletions app/__test__/ui/user/user-info-navigator.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import '@testing-library/jest-dom';
import UserInfoNavigator from '@/app/ui/user/user-info-navigator/user-info-navigator';
import { render, screen } from '@testing-library/react';

jest.mock('next/headers', () => ({
cookies: jest.fn().mockReturnValue({
get: jest.fn().mockReturnValue({
value: 'fake-access-token',
}),
}),
}));

describe('UserInfoNavigator', () => {
it('UserInfoNavigator를 렌더링한다.', async () => {
render(await UserInfoNavigator());

expect(await screen.findByText(/장진욱/i)).toBeInTheDocument();
expect(await screen.findByText(/디지털콘텐츠디자인학과/i)).toBeInTheDocument();
expect(await screen.findByText(/60181666/i)).toBeInTheDocument();
});
});
27 changes: 10 additions & 17 deletions app/business/result/result.query.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import { API_PATH } from '../api-path';
import { cookies } from 'next/headers';
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { CreditResponse, ResultCategoryDetailResponse } from './result.type';
import axios from 'axios';
import { getToken } from '../auth';

export const fetchResultCategoryDetailInfo = async (category: string): Promise<ResultCategoryDetailResponse> => {
export const fetchResultCategoryDetailInfo = async (category: string) => {
//FIX : category를 querystring으로 호출하는 건은 mock단계에서는 불필요할 것으로 예상, 실제 api 연결시 변경 예정
try {
const response = await fetch(API_PATH.resultCategoryDetailInfo, {
const { data } = await axios<ResultCategoryDetailResponse>(API_PATH.resultCategoryDetailInfo, {
headers: {
Authorization: `Bearer ${cookies().get('accessToken')?.value}`,
Authorization: `Bearer ${getToken()}`,
},
});
const result = await response.json();
httpErrorHandler(response, result);

return result;
return data;
} catch (error) {
throw error;
}
};

export const fetchCredits = async (): Promise<CreditResponse[]> => {
export const fetchCredits = async () => {
try {
const response = await fetch(API_PATH.credits, {
const { data } = await axios<CreditResponse[]>(API_PATH.credits, {
headers: {
Authorization: `Bearer ${cookies().get('accessToken')?.value}`,
Authorization: `Bearer ${getToken()}`,
},
});
const result = await response.json();

httpErrorHandler(response, result);

return result;
return data;
} catch (error) {
throw error;
}
Expand Down
49 changes: 48 additions & 1 deletion app/business/user/user.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../api-path';
import { SignUpRequestBody, SignInRequestBody, ValidateTokenResponse } from './user.type';
import { SignUpRequestBody, SignInRequestBody, ValidateTokenResponse, UserDeleteRequestBody } from './user.type';
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { BadRequestError } from '@/app/utils/http/http-error';
import {
Expand All @@ -15,6 +15,53 @@ import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';
import { redirect } from 'next/navigation';

export async function signOut() {
cookies().delete('accessToken');
cookies().delete('refreshToken');

redirect('/sign-in');
}

export async function deleteUser(prevState: FormState, formData: FormData): Promise<FormState> {
try {
const body: UserDeleteRequestBody = {
password: formData.get('password') as string,
};

const response = await fetch(`${API_PATH.user}/delete-me`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${cookies().get('accessToken')?.value}`,
},
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: '회원 탈퇴가 완료되었습니다.',
};
}

export async function validateToken(): Promise<ValidateTokenResponse | false> {
const accessToken = cookies().get('accessToken')?.value;
const refreshToken = cookies().get('refreshToken')?.value;
Expand Down
34 changes: 21 additions & 13 deletions app/business/user/user.query.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import { httpErrorHandler } from '@/app/utils/http/http-error-handler';
import { API_PATH } from '../api-path';

import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';
import { InitUserInfoResponse, UserInfoResponse } from './user.type';
import axios from 'axios';
import { getToken } from '../auth';
import { isValidation } from '@/app/utils/zod/validation.util';
import { UserInfoResponseSchema, InitUserInfoResponseSchema } from './user.validation';
import { UnauthorizedError } from '@/app/utils/http/http-error';

export async function auth(): Promise<InitUserInfoResponse | UserInfoResponse | undefined> {
try {
const result = await fetchUserInfo();
return result;
} catch (error) {
if (error instanceof UnauthorizedError) {
return;
}
throw error;
}
}

export async function fetchUserInfo(): Promise<InitUserInfoResponse | UserInfoResponse> {
export async function fetchUserInfo() {
try {
const response = await fetch(`${API_PATH.user}`, {
const { data } = await axios<InitUserInfoResponse | UserInfoResponse>(API_PATH.user, {
headers: {
Authorization: `Bearer ${cookies().get('accessToken')?.value}`,
Authorization: `Bearer ${await getToken()}`,
},
});

const result = await response.json();

httpErrorHandler(response, result);

if (isValidation(result, UserInfoResponseSchema || InitUserInfoResponseSchema)) {
return result;
if (isValidation(data, UserInfoResponseSchema || InitUserInfoResponseSchema)) {
return data;
} else {
throw 'Invalid user info response schema.';
}
Expand Down
9 changes: 9 additions & 0 deletions app/business/user/user.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export interface SignUpRequestBody {
engLv: string;
}

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

export interface UserDeleteRequestBody {
password: string;
}

export type SignInResponse = z.infer<typeof SignInResponseSchema>;

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

export const UserInfoResponseSchema = z.object({
studentNumber: z.string(),
Expand Down Expand Up @@ -71,3 +72,6 @@ export const SignUpFormSchema = z
});
}
});
export function isInitUser(x: UserInfoResponse | InitUserInfoResponse): x is InitUserInfoResponse {
return typeof x.studentName === null;
}
29 changes: 0 additions & 29 deletions app/dashboard/components/dashboard-nav-links.tsx

This file was deleted.

Loading

0 comments on commit 0c59c59

Please sign in to comment.