-
-
-
Youre all set.
-
- Thanks for signing up! We just need to verify your email address to complete the process.
-
-
-
-
-
-
-
-
+ <>
+
+
+
회원가입이 완료되었습니다
+
로그인 후 졸업 사정 결과를 확인해보세요!
+
+
+
+
-
+ >
);
}
diff --git a/app/(sub-page)/sign-up/components/sign-up-terms.tsx b/app/(sub-page)/sign-up/components/sign-up-terms.tsx
index 8a5deaf7..06503302 100644
--- a/app/(sub-page)/sign-up/components/sign-up-terms.tsx
+++ b/app/(sub-page)/sign-up/components/sign-up-terms.tsx
@@ -1,54 +1,71 @@
import Button from '@/app/ui/view/atom/button/button';
+import TitleBox from '@/app/ui/view/molecule/title-box/title-box';
interface SignUpTermProps {
onNext?: () => void;
}
-// 약관 내용이랑 스타일은 mock인 상태입니다.
+// 약관내용 최신화 필요(* 검사 대상)
export default function SignUpTerm({ onNext }: SignUpTermProps) {
const handleAgreeButtonClick = () => {
onNext?.();
};
return (
-
-
알림독의 안내문
-
- -
- 현재 저희 기능은 한국-한국은 아니라 한국-외국도 가능합니다. 각사별에서 수하인 없더라도 저희가 받는데까지는
- 무관합니다만, 꼭 관세사무소와 협의하세요!
-
- - 대상: 국외발송, 북부발송, 사회복지대상, ICT용품대상, 일반대상, 미래용품대상(확인)
- - 발송: 16 ~ 22시발
+
+
+
+
+ -
+ 현재 검사 가능한 학과-학번은 아래과 같습니다. 검사대상에 속하지 않다면 검사가 불가능합니다. 꼭
+ 검사대상인지 확인하세요!
+
+ -
+ 대학: 경영대학, 법과대학, 사회과학대학, ICT융합대학, 인문대학,{' '}
+ 미래융합대학(불가)
+
+ - 학번: 16 ~ 22학번
+
+
+ -
+ 교직, 다전공, 연계전공, 편입, 전과, 재외국민/외국인전형에 해당하는 사용자는 검사 기준이 따로 설정되지 않아
+ 검사가 불가능합니다.
+
+ - 검사를 위해선 성적표를 직접 업로드해야하므로 PC환경에서 진행하는 것을 권장합니다.
+ -
+ 검사 기준은 최신버전 학사안내문(2023.07.24) 반영하여 설정되었으며, 학사안내문은 매년 개편되므로 자신이
+ 알고 있는 구버전과 다를 수 있습니다.
+
+
+ -
+ 본 서비스 정보는 공식적인 효력을 갖지 않으며,{' '}
+ 정확한 졸업사정결과를 위해서는 소속 단과대 교학팀에서의 확인을 권장합니다.
+
+ -
+ 저장된 사용자 데이터베이스는 익명화되어 저장되고 과목추천 및 교양과목 통계에만 사용되며, 익명 다른 용도로
+ 사용되지 않습니다.
+
+ - 졸업요건 기준이 잘못 설정되었거나, 오류발생 시 우측 하단 채널톡으로 피드백 부탁드립니다.
-
-
-
- 교직, 디자인, 연계개발, 물품, 전자, 자원관리/회계지원에 해당하는 사용자는 각사 기준에 따른 선정되지 않아 각사
- 별 관리하는데요.
-
-
- 검사를 위해서 선적품을 직접 연락드려야만 PC화면에서 진행하는 것을 권장합니다.
-
-
- 검사 기준은 최신버전 확인내역(2023.07.24) 반영하여 선정되었으며, 학사내역은 매년 개편되므로 자사이 외고 있는
- 구버전과 다를 수 있습니다.
-
-
-
-
- 본 서비스 정보는 공식적인 확인을 전제 않으며, 정확한 증상조사결과를 위해 서류 또는 담당과 교류해야할 사항을
- 잊지 않습니다.
-
-
-
- 전자문 서화지 데이터베이스는 의무화되어 저희가 고유축적 및 교육과정 등에서 사용되며, 어떤 다른 용도로 사용
- 되지 않습니다.
-
-
- 특허요건 기준이 전문 선정되었거나, 오류발생 시 우측 하단 채팅창으로 또는 담당 부서로문의합니다.
-
+
+
+
diff --git a/app/(sub-page)/sign-up/components/sign-up.tsx b/app/(sub-page)/sign-up/components/sign-up.tsx
new file mode 100644
index 00000000..b27996d1
--- /dev/null
+++ b/app/(sub-page)/sign-up/components/sign-up.tsx
@@ -0,0 +1,27 @@
+import Responsive from '@/app/ui/responsive';
+import SignUpForm from '@/app/ui/user/sign-up-form/sign-up-form';
+import TitleBox from '@/app/ui/view/molecule/title-box/title-box';
+import MaruImage from '@/public/assets/mju-maru.jpg';
+import Image from 'next/image';
+
+interface SignUpProps {
+ onNext?: () => void;
+}
+
+export default function SignUp({ onNext }: SignUpProps) {
+ return (
+
+ );
+}
diff --git a/app/(sub-page)/sign-up/page.tsx b/app/(sub-page)/sign-up/page.tsx
index 191eb1c3..bbcee7ca 100644
--- a/app/(sub-page)/sign-up/page.tsx
+++ b/app/(sub-page)/sign-up/page.tsx
@@ -1,11 +1,9 @@
import SignUpContainer from './components/sign-up-container';
import { Suspense } from 'react';
import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
-import LoadingSpinner from '@/app/ui/view/atom/loading-spinner/loading-spinner';
+import SignUpFormSkeleton from '@/app/ui/user/sign-up-form/sign-up-form.skeleton';
import type { Metadata } from 'next';
-// Refactor: fallback 스켈레톤으로 대체
-
export const metadata: Metadata = {
title: '회원가입',
description: '졸업을 부탁해에 회원가입 하고 졸업요건을 간편하게 검사해 보세요.',
@@ -13,14 +11,8 @@ export const metadata: Metadata = {
export default function SignUpPage() {
return (
-
-
-
-
- }
- >
+
+ }>
diff --git a/app/business/api-path.ts b/app/business/api-path.ts
index 63dee0dd..ded9159c 100644
--- a/app/business/api-path.ts
+++ b/app/business/api-path.ts
@@ -1,11 +1,12 @@
-const BASE_URL = 'http://localhost:9090';
-process.env.API_MOCKING === 'enable' ? 'http://localhost:9090' : '';
+import { setupURL } from '../utils/api/setup-url.util';
+
+const { BASE_URL, PARSE_API_URL } = setupURL();
export const API_PATH = {
default: BASE_URL,
revenue: `${BASE_URL}/revenue`,
registerUserGrade: `${BASE_URL}/parsing-text`,
- parsePDFtoText: `${BASE_URL}/parsePDFtoText`,
+ parsePDFtoText: PARSE_API_URL,
takenLectures: `${BASE_URL}/taken-lectures`,
user: `${BASE_URL}/users`,
graduations: `${BASE_URL}/graduations`,
diff --git a/app/business/services/user/user.command.ts b/app/business/services/user/user.command.ts
index c639002d..ab168ffa 100644
--- a/app/business/services/user/user.command.ts
+++ b/app/business/services/user/user.command.ts
@@ -4,7 +4,7 @@ import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../../api-path';
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 { BadRequestError, UnauthorizedError } from '@/app/utils/http/http-error';
import {
SignUpFormSchema,
SignInFormSchema,
@@ -15,9 +15,13 @@ import { cookies } from 'next/headers';
import { isValidation } from '@/app/utils/zod/validation.util';
import { redirect } from 'next/navigation';
-export async function signOut() {
+function deleteCookies() {
cookies().delete('accessToken');
cookies().delete('refreshToken');
+}
+
+export async function signOut() {
+ deleteCookies();
redirect('/sign-in');
}
@@ -28,7 +32,7 @@ export async function deleteUser(prevState: FormState, formData: FormData): Prom
password: formData.get('password') as string,
};
- const response = await fetch(`${API_PATH.user}/delete-me`, {
+ const response = await fetch(`${API_PATH.user}/me`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@@ -36,10 +40,13 @@ export async function deleteUser(prevState: FormState, formData: FormData): Prom
},
body: JSON.stringify(body),
});
- const result = await response.json();
- httpErrorHandler(response, result);
+ if (response.status !== 200) {
+ const result = await response.json();
+ httpErrorHandler(response, result);
+ }
} catch (error) {
+ console.log(error);
if (error instanceof BadRequestError) {
// 잘못된 요청 처리 로직
return {
@@ -54,43 +61,8 @@ export async function deleteUser(prevState: FormState, formData: FormData): Prom
}
}
- return {
- isSuccess: true,
- isFailure: false,
- validationError: {},
- message: '회원 탈퇴가 완료되었습니다.',
- };
-}
-
-export async function validateToken(): Promise
{
- const accessToken = cookies().get('accessToken')?.value;
- const refreshToken = cookies().get('refreshToken')?.value;
- try {
- const response = await fetch(`${API_PATH.auth}/token`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${accessToken}`,
- },
- body: JSON.stringify({ refreshToken }),
- });
-
- const result = await response.json();
-
- httpErrorHandler(response, result);
-
- if (isValidation(result, ValidateTokenResponseSchema)) {
- return result;
- } else {
- throw 'Invalid token response schema.';
- }
- } catch (error) {
- if (error instanceof BadRequestError) {
- return false;
- } else {
- throw error;
- }
- }
+ deleteCookies();
+ redirect('/sign-in');
}
export async function authenticate(prevState: FormState, formData: FormData): Promise {
@@ -136,11 +108,10 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr
secure: process.env.NODE_ENV === 'production',
path: '/',
});
-
- redirect('/my');
}
} catch (error) {
- if (error instanceof BadRequestError) {
+ // 명세와 다르게 에러가 발생할 경우 BadRequestError가 아니라 UnauthorizedError가 발생
+ if (error instanceof UnauthorizedError) {
// 잘못된 요청 처리 로직
return {
isSuccess: false,
@@ -154,12 +125,36 @@ export async function authenticate(prevState: FormState, formData: FormData): Pr
}
}
- return {
- isSuccess: true,
- isFailure: false,
- validationError: {},
- message: '로그인 성공',
- };
+ redirect('/my');
+}
+
+export async function refreshToken(): Promise {
+ const refreshToken = cookies().get('refreshToken')?.value;
+ try {
+ const response = await fetch(`${API_PATH.auth}/token`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ refreshToken }),
+ });
+
+ const result = await response.json();
+
+ httpErrorHandler(response, result);
+
+ if (isValidation(result, ValidateTokenResponseSchema)) {
+ return result;
+ } else {
+ throw 'Invalid token response schema.';
+ }
+ } catch (error) {
+ if (error instanceof BadRequestError) {
+ return false;
+ } else {
+ throw error;
+ }
+ }
}
export async function createUser(prevState: FormState, formData: FormData): Promise {
@@ -197,9 +192,10 @@ export async function createUser(prevState: FormState, formData: FormData): Prom
body: JSON.stringify(body),
});
- const result = await response.json();
-
- httpErrorHandler(response, result);
+ if (response.status !== 200) {
+ const result = await response.json();
+ httpErrorHandler(response, result);
+ }
} catch (error) {
if (error instanceof BadRequestError) {
// 잘못된 요청 처리 로직
diff --git a/app/business/services/user/user.query.ts b/app/business/services/user/user.query.ts
index f5a902e0..e40d8ce0 100644
--- a/app/business/services/user/user.query.ts
+++ b/app/business/services/user/user.query.ts
@@ -29,7 +29,8 @@ export async function fetchUser(): Promise(
);
useEffect(() => {
- setStep(step ?? defaultStep);
+ if (!step) {
+ router.replace(createUrl(defaultStep));
+ }
}, [defaultStep, step, setStep]);
const Step = ({ name, children }: React.PropsWithChildren<{ name: Steps }>) => {
diff --git a/app/mocks/db.mock.ts b/app/mocks/db.mock.ts
index 1021baf2..f92598cb 100644
--- a/app/mocks/db.mock.ts
+++ b/app/mocks/db.mock.ts
@@ -83,12 +83,12 @@ export const mockDatabase: MockDatabaseAction = {
const user = mockDatabaseStore.users.find((u) => u.authId === authId);
if (!user) {
return {
- studentNumber: '',
+ studentNumber: '11111111',
studentName: null,
- completeDivision: null,
- totalCredit: null,
- takenCredit: null,
- graduated: null,
+ completeDivision: [],
+ totalCredit: 0,
+ takenCredit: 0,
+ graduated: false,
};
}
return mockDatabaseStore.userInfo;
diff --git a/app/mocks/handlers/result-handler.mock.ts b/app/mocks/handlers/result-handler.mock.ts
index 8ea50d62..3e5b7a84 100644
--- a/app/mocks/handlers/result-handler.mock.ts
+++ b/app/mocks/handlers/result-handler.mock.ts
@@ -6,7 +6,7 @@ import { CreditResponse, ResultCategoryDetailResponse } from '@/app/store/querys
export const resultHandlers = [
http.get(
- `${API_PATH.resultCategoryDetailInfo}`,
+ `${API_PATH.graduations}`,
async ({ request }) => {
const accessToken = request.headers.get('Authorization')?.replace('Bearer ', '');
if (accessToken === 'undefined' || !accessToken) {
diff --git a/app/mocks/handlers/user-handler.mock.ts b/app/mocks/handlers/user-handler.mock.ts
index 81164597..19fee9ba 100644
--- a/app/mocks/handlers/user-handler.mock.ts
+++ b/app/mocks/handlers/user-handler.mock.ts
@@ -59,7 +59,7 @@ export const userHandlers = [
if (result) {
return HttpResponse.json({ status: 200 });
} else {
- return HttpResponse.json({ status: 400, message: '비밀번호가 일치하지 않습니다' }, { status: 400 });
+ return HttpResponse.json({ status: 401, message: '비밀번호가 일치하지 않습니다' }, { status: 401 });
}
} catch {
return HttpResponse.json({ status: 401, message: 'Unauthorized' }, { status: 401 });
diff --git a/app/ui/lecture/taken-lecture/taken-lecture-label.tsx b/app/ui/lecture/taken-lecture/taken-lecture-label.tsx
index f17d3e42..bf4903a3 100644
--- a/app/ui/lecture/taken-lecture/taken-lecture-label.tsx
+++ b/app/ui/lecture/taken-lecture/taken-lecture-label.tsx
@@ -23,7 +23,7 @@ export default function TakenLectureLabel() {
onClick={open}
/>
-
+
}
diff --git a/app/ui/lecture/upload-taken-lecture/upload-taken-lecture.tsx b/app/ui/lecture/upload-taken-lecture/upload-taken-lecture.tsx
index e7cbfbfc..234b6a91 100644
--- a/app/ui/lecture/upload-taken-lecture/upload-taken-lecture.tsx
+++ b/app/ui/lecture/upload-taken-lecture/upload-taken-lecture.tsx
@@ -8,7 +8,9 @@ function UploadTakenLecture() {
return (
+
+
+
);
}
diff --git a/app/ui/user/sign-in-form/sign-in-form.tsx b/app/ui/user/sign-in-form/sign-in-form.tsx
index 5a5d56c6..a9bcfd6c 100644
--- a/app/ui/user/sign-in-form/sign-in-form.tsx
+++ b/app/ui/user/sign-in-form/sign-in-form.tsx
@@ -4,10 +4,12 @@ import { authenticate } from '@/app/business/services/user/user.command';
export default function SignInForm() {
return (
-
-
+
+
+
);
}
diff --git a/app/ui/user/sign-up-form/sign-up-form.skeleton.tsx b/app/ui/user/sign-up-form/sign-up-form.skeleton.tsx
new file mode 100644
index 00000000..de3497d4
--- /dev/null
+++ b/app/ui/user/sign-up-form/sign-up-form.skeleton.tsx
@@ -0,0 +1,30 @@
+import Skeleton from '@/app/utils/skeleton';
+
+export default function SignUpFormSkeleton() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/ui/user/sign-up-form/sign-up-form.stories.tsx b/app/ui/user/sign-up-form/sign-up-form.stories.tsx
index 8b48d133..8175dc04 100644
--- a/app/ui/user/sign-up-form/sign-up-form.stories.tsx
+++ b/app/ui/user/sign-up-form/sign-up-form.stories.tsx
@@ -9,7 +9,7 @@ const meta = {
title: 'ui/user/SignUpForm',
component: SignUpForm,
args: {
- onNext: fn(),
+ onSuccess: fn(),
},
decorators: [
(Story) => (
@@ -39,7 +39,7 @@ export const SuccessSenario: Story = {
});
await step('회원가입에 성공한다', async () => {
- await waitFor(() => expect(args.onNext).toHaveBeenCalled());
+ await waitFor(() => expect(args.onSuccess).toHaveBeenCalled());
});
},
};
@@ -61,7 +61,7 @@ export const FailureSenarioWithValidation: Story = {
await step('유효성 검사에 실패한다.', async () => {
await waitFor(() => {
- expect(args.onNext).not.toHaveBeenCalled();
+ expect(args.onSuccess).not.toHaveBeenCalled();
expect(canvas.getByText('양식에 맞춰 다시 입력해주세요.')).toBeInTheDocument();
expect(canvas.getByText('아이디는 6자 이상 20자 이하여야 합니다.')).toBeInTheDocument();
expect(canvas.getByText('비밀번호는 문자, 숫자, 특수문자(!@#$%^&*)를 포함해야 합니다.')).toBeInTheDocument();
@@ -89,7 +89,7 @@ export const FailureSenarioWithDuplicatedStudentNumber: Story = {
await step('회원가입에 실패한다.', async () => {
await waitFor(() => {
- expect(args.onNext).not.toHaveBeenCalled();
+ expect(args.onSuccess).not.toHaveBeenCalled();
expect(canvas.getByText('이미 가입된 학번입니다.')).toBeInTheDocument();
});
});
diff --git a/app/ui/user/sign-up-form/sign-up-form.tsx b/app/ui/user/sign-up-form/sign-up-form.tsx
index e1159cbe..7d66b909 100644
--- a/app/ui/user/sign-up-form/sign-up-form.tsx
+++ b/app/ui/user/sign-up-form/sign-up-form.tsx
@@ -3,12 +3,12 @@ import { createUser } from '@/app/business/services/user/user.command';
import Form from '../../view/molecule/form';
interface SignUpFormProps {
- onNext?: () => void;
+ onSuccess?: () => void;
}
-export default function SignUpForm({ onNext }: SignUpFormProps) {
+export default function SignUpForm({ onSuccess }: SignUpFormProps) {
return (
-
diff --git a/app/ui/user/user-info-navigator/user-delete-modal.tsx b/app/ui/user/user-info-navigator/user-delete-modal.tsx
index b2d68e3d..545c72a4 100644
--- a/app/ui/user/user-info-navigator/user-delete-modal.tsx
+++ b/app/ui/user/user-info-navigator/user-delete-modal.tsx
@@ -3,23 +3,25 @@ import Modal from '../../view/molecule/modal/modal';
import Form from '../../view/molecule/form';
import { deleteUser } from '@/app/business/services/user/user.command';
import { DIALOG_KEY } from '@/app/utils/key/dialog-key.util';
+import TitleBox from '../../view/molecule/title-box/title-box';
export default function UserDeleteModal() {
return (
-
-
회원 탈퇴
-
-
- 회원탈퇴를 진행하시겠습니까? 탈퇴를 진행하려면 비밀번호 입력이 필요합니다.
-
-
-
-
-
-
-
정보를 누락하여 서비스를 이용해 주셔서 감사합니다.
+
+
+
+
회원탈퇴를 진행하시겠습니까?
+
탈퇴를 진행하려면 비밀번호 입력이 필요합니다.
+
+
+
+
+
+
+ 졸업을 부탁해 서비스를 이용해 주셔서 감사합니다.
+
);
diff --git a/app/ui/view/atom/button/button.tsx b/app/ui/view/atom/button/button.tsx
index fcc97db5..1d938502 100644
--- a/app/ui/view/atom/button/button.tsx
+++ b/app/ui/view/atom/button/button.tsx
@@ -29,7 +29,7 @@ export const ButtonVariants = cva(`flex justify-center items-center`, {
sm: 'px-10 py-2.5 text-xs font-medium leading-3',
md: 'px-20 py-4 text-base font-medium leading-3',
lg: 'px-28 py-4 text-2xl font-medium leading-9',
- xl: 'px-36 py-5 text-3xl font-medium leading-9',
+ xl: 'px-16 py-4 text-xl md:px-36 md:py-5 md:text-3xl font-medium leading-9',
},
},
});
diff --git a/app/ui/view/atom/label-container/label-container.tsx b/app/ui/view/atom/label-container/label-container.tsx
index 90c28d85..7760631a 100644
--- a/app/ui/view/atom/label-container/label-container.tsx
+++ b/app/ui/view/atom/label-container/label-container.tsx
@@ -8,7 +8,7 @@ interface LabelContainerProps {
export default function LabelContainer({ label, rightElement }: LabelContainerProps) {
return (
-
+
{label}
{rightElement}
diff --git a/app/ui/view/atom/separator.tsx b/app/ui/view/atom/separator.tsx
new file mode 100644
index 00000000..f3655bc0
--- /dev/null
+++ b/app/ui/view/atom/separator.tsx
@@ -0,0 +1,26 @@
+'use client';
+
+import * as React from 'react';
+import * as SeparatorPrimitive from '@radix-ui/react-separator';
+
+import { cn } from '@/app/utils/shadcn/utils';
+
+const Separator = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef
+>(({ className, orientation = 'horizontal', decorative = true, ...props }, ref) => (
+
+));
+Separator.displayName = SeparatorPrimitive.Root.displayName;
+
+export default Separator;
diff --git a/app/ui/view/molecule/form/form-root.tsx b/app/ui/view/molecule/form/form-root.tsx
index 5bb5c37b..eb3a28c1 100644
--- a/app/ui/view/molecule/form/form-root.tsx
+++ b/app/ui/view/molecule/form/form-root.tsx
@@ -23,6 +23,7 @@ interface FormRootProps {
onSuccess?: () => void;
action: (prevState: FormState, formData: FormData) => Promise | FormState;
failMessageControl?: 'alert' | 'toast';
+ className?: string;
}
export function FormRoot({
@@ -31,6 +32,7 @@ export function FormRoot({
onSuccess,
failMessageControl = 'alert',
children,
+ className,
}: React.PropsWithChildren) {
const initialState: FormState = { isSuccess: false, isFailure: false, message: null, validationError: {} };
const [formState, dispatch] = useFormState(action, initialState);
@@ -65,7 +67,7 @@ export function FormRoot({
) : null}
-
diff --git a/app/ui/view/molecule/sheet/sheet.tsx b/app/ui/view/molecule/sheet/sheet.tsx
index 50245dbd..3f4e4198 100644
--- a/app/ui/view/molecule/sheet/sheet.tsx
+++ b/app/ui/view/molecule/sheet/sheet.tsx
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
{
const accessToken = request.cookies.get('accessToken')?.value;
if (!accessToken) {
@@ -12,21 +13,16 @@ async function getAuth(request: NextRequest): Promise<{
};
}
- const validatedResult = await validateToken();
+ const user = await auth();
- if (!validatedResult) {
- request.cookies.delete('accessToken');
- request.cookies.delete('refreshToken');
+ if (!user) {
return {
- role: 'guest',
+ role: 'expired',
};
}
- request.cookies.set('accessToken', validatedResult.accessToken);
-
- const user = await fetchUser();
return {
- role: user.studentName ? 'user' : 'init',
+ role: isInitUser(user) ? 'init' : 'user',
};
}
@@ -45,6 +41,10 @@ function isAllowedGuestPath(path: string, strict: boolean = false) {
export async function middleware(request: NextRequest) {
const auth = await getAuth(request);
+ if (auth.role === 'expired') {
+ return await retryAuth(request);
+ }
+
if (auth.role === 'init' && !request.nextUrl.pathname.startsWith('/grade-upload')) {
return Response.redirect(new URL('/grade-upload', request.url));
}
@@ -58,6 +58,22 @@ export async function middleware(request: NextRequest) {
}
}
+async function retryAuth(request: NextRequest) {
+ const response = NextResponse.redirect(request.url);
+ const result = await refreshToken();
+ if (result === false) {
+ response.cookies.delete('accessToken');
+ response.cookies.delete('refreshToken');
+ } else {
+ response.cookies.set('accessToken', result.accessToken, {
+ httpOnly: true,
+ secure: process.env.NODE_ENV === 'production',
+ path: '/',
+ });
+ }
+ return response;
+}
+
export const config = {
matcher: ['/((?!api|mockServiceWorker|_next/static|_next/image|.*\\.png$).*)'],
};
diff --git a/package-lock.json b/package-lock.json
index a9cdd275..d2e9030f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
@@ -4303,6 +4304,81 @@
}
}
},
+ "node_modules/@radix-ui/react-separator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.0.tgz",
+ "integrity": "sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
+ "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
+ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
+ "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-slot": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz",
diff --git a/package.json b/package.json
index 76b607b1..4e192ed7 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"scripts": {
"build": "next build",
"dev": "next dev",
- "dev:mock": "concurrently --kill-others \"cross-env API_MOCKING=enable next dev\" \"cross-env API_MOCKING=enable npx tsx watch ./app/mocks/http.ts\"",
- "start:mock": "concurrently --kill-others \"cross-env API_MOCKING=enable next start\" \"cross-env API_MOCKING=enable npx tsx watch ./app/mocks/http.ts\"",
+ "dev:mock": "concurrently --kill-others \"cross-env API_MOCKING=enable next dev\" \"cross-env MOCK_SERVER=true npx tsx watch ./app/mocks/http.ts\"",
+ "start:mock": "concurrently --kill-others \"cross-env API_MOCKING=enable next start\" \"cross-env MOCK_SERVER=true npx tsx watch ./app/mocks/http.ts\"",
"start": "next start",
"lint": "next lint",
"lint:fix": "next lint --fix",
@@ -31,6 +31,7 @@
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
+ "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
diff --git a/public/assets/mju-maru.jpg b/public/assets/mju-maru.jpg
new file mode 100644
index 00000000..32d6ae53
Binary files /dev/null and b/public/assets/mju-maru.jpg differ
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 3daf1b80..0a6a4383 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -48,7 +48,8 @@ const config: Config = {
zIndex: {
1: '100', // upper layout, navigation bar, main page content
2: '200', // upper content , main page graduation cap
- 3: '300', // upper all
+ 3: '300', // modal
+ 4: '400', // upper all
},
gridTemplateColumns: {
'render-button': 'repeat(5, 1fr) 0.6fr',