diff --git a/README.md b/README.md
index 86f8dcc7..64dc7e54 100644
--- a/README.md
+++ b/README.md
@@ -50,10 +50,6 @@
- 성적표 기반의 맞춤형 졸업 요건 검사 결과를 제공합니다.
-### 🎃 조회수 & 사용자 통계
-
-
-
### 🎃 사용자 후기
diff --git a/app/(sub-page)/anonymous/components/anonymous-container.tsx b/app/(sub-page)/anonymous/components/anonymous-container.tsx
new file mode 100644
index 00000000..c4871db4
--- /dev/null
+++ b/app/(sub-page)/anonymous/components/anonymous-container.tsx
@@ -0,0 +1,40 @@
+'use client';
+import useFunnel from '@/app/hooks/useFunnel';
+import SignUpTerm from '../../sign-up/components/sign-up-terms';
+import { AnonymousResultType } from '@/app/utils/parser/anonymous';
+import AnonymousUpload from './anonymous-upload';
+import { FormState } from '@/app/ui/view/molecule/form/form-root';
+import { useRouter } from 'next/navigation';
+import { useAnonymousContext } from '../result/provider';
+
+function isAnonymousResultType(formdata: any): formdata is AnonymousResultType {
+ return formdata && 'graduationResult' in formdata && 'user' in formdata;
+}
+
+export default function AnonymousContainer() {
+ const router = useRouter();
+ const { setResult } = useAnonymousContext();
+ const { Funnel, setStep } = useFunnel<'terms' | 'form'>('terms');
+
+ return (
+
+
+ {
+ setStep('form');
+ }}
+ />
+
+
+ {
+ if (isAnonymousResultType(formState?.value)) {
+ setResult(formState.value);
+ router.push('/anonymous/result');
+ }
+ }}
+ />
+
+
+ );
+}
diff --git a/app/(sub-page)/anonymous/components/anonymous-upload.tsx b/app/(sub-page)/anonymous/components/anonymous-upload.tsx
new file mode 100644
index 00000000..81d0a3ab
--- /dev/null
+++ b/app/(sub-page)/anonymous/components/anonymous-upload.tsx
@@ -0,0 +1,10 @@
+import UploadTakenLectureAnonymous from '@/app/ui/lecture/upload-taken-lecture/upload-taken-lectrue-anonymous';
+import { FormState } from '@/app/ui/view/molecule/form/form-root';
+
+interface AnonymousUploadProp {
+ onNext?: (formState?: FormState) => void;
+}
+
+export default function AnonymousUpload({ onNext }: AnonymousUploadProp) {
+ return ;
+}
diff --git a/app/(sub-page)/anonymous/layout.tsx b/app/(sub-page)/anonymous/layout.tsx
new file mode 100644
index 00000000..5f3229e8
--- /dev/null
+++ b/app/(sub-page)/anonymous/layout.tsx
@@ -0,0 +1,10 @@
+import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
+import { AnonymousProvider } from './result/provider';
+
+interface AnonymousLayoutProp {
+ children: React.ReactNode;
+}
+
+export default function AnonymousLayout({ children }: AnonymousLayoutProp) {
+ return {children};
+}
diff --git a/app/(sub-page)/anonymous/page.tsx b/app/(sub-page)/anonymous/page.tsx
new file mode 100644
index 00000000..e65e2da8
--- /dev/null
+++ b/app/(sub-page)/anonymous/page.tsx
@@ -0,0 +1,19 @@
+import { Metadata } from 'next';
+import AnonymousContainer from './components/anonymous-container';
+import { Suspense } from 'react';
+import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
+
+export const metadata: Metadata = {
+ title: '비회원으로 검사하기',
+ description: '로그인없이 졸업요건을 간편하게 검사해 보세요.',
+};
+
+export default function AnonymousPage() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/app/(sub-page)/anonymous/result/component/anonymous-result.tsx b/app/(sub-page)/anonymous/result/component/anonymous-result.tsx
new file mode 100644
index 00000000..bf36acbb
--- /dev/null
+++ b/app/(sub-page)/anonymous/result/component/anonymous-result.tsx
@@ -0,0 +1,42 @@
+'use client';
+import { ResultCategoryViewer } from '@/app/ui/result/result-category/result-category';
+import UserInfoContentViewer from '@/app/ui/user/user-info-card/user-info-content/user-info-content-viewer';
+import { AnonymousResultType, parseCredit, parseCreditDetailInfo, parseUserInfo } from '@/app/utils/parser/anonymous';
+import { useAnonymousContext } from '../provider';
+import { useRouter } from 'next/navigation';
+import ResultCategoryDetailDialog from '@/app/ui/result/result-category-detail/result-category-detail-dialog';
+import { ResultCategoryDetailInfoViewer } from '@/app/ui/result/result-category-detail/result-category-detail-info';
+import { ResultCategoryKey } from '@/app/utils/key/result-category.key';
+
+interface AnonymousResultProp {
+ category: ResultCategoryKey;
+}
+
+const AnonymousResult = ({ category }: AnonymousResultProp) => {
+ const { result } = useAnonymousContext();
+ const router = useRouter();
+ if (!result) {
+ router.back();
+ return <>>;
+ }
+
+ return (
+
+
+
+
+
+ {category ? (
+
+
+
+ ) : null}
+
+ );
+};
+
+export default AnonymousResult;
diff --git a/app/(sub-page)/anonymous/result/page.tsx b/app/(sub-page)/anonymous/result/page.tsx
new file mode 100644
index 00000000..cf98e2fe
--- /dev/null
+++ b/app/(sub-page)/anonymous/result/page.tsx
@@ -0,0 +1,37 @@
+import { Metadata } from 'next/types';
+import AnonymousResult from './component/anonymous-result';
+import { ResultCategoryKey } from '@/app/utils/key/result-category.key';
+import ContentContainer from '@/app/ui/view/atom/content-container/content-container';
+
+export const metadata: Metadata = {
+ title: '졸업 요건 검사 결과',
+ description: '회원가입없이 졸업사정 결과와, 카테고리별 미이수 / 이수 과목정보 및 잔여학점을 확인해요',
+ openGraph: {
+ siteName: '졸업을 부탁해',
+ url: 'https://mju-graduate.com/result',
+ images: [
+ {
+ url: 'https://github.com/user-attachments/assets/2093a57f-af35-4280-8acb-d403341fc8ff',
+ width: 1200,
+ height: 630,
+ alt: 'result-page iamge',
+ },
+ ],
+ },
+};
+
+interface AnonymousResultPageProp {
+ searchParams: { category: ResultCategoryKey };
+}
+
+function AnonymousResultPage({ searchParams }: AnonymousResultPageProp) {
+ const { category } = searchParams;
+
+ return (
+
+
+
+ );
+}
+
+export default AnonymousResultPage;
diff --git a/app/(sub-page)/anonymous/result/provider.tsx b/app/(sub-page)/anonymous/result/provider.tsx
new file mode 100644
index 00000000..fdba7402
--- /dev/null
+++ b/app/(sub-page)/anonymous/result/provider.tsx
@@ -0,0 +1,24 @@
+'use client';
+import { createContext, useState, ReactNode, useContext } from 'react';
+import { AnonymousResultType } from '@/app/utils/parser/anonymous';
+
+interface AnonymousContextType {
+ result?: AnonymousResultType;
+ setResult: (data: AnonymousResultType) => void;
+}
+
+export const AnonymousContext = createContext(null);
+
+export const AnonymousProvider = ({ children }: { children: ReactNode }) => {
+ const [result, setResult] = useState();
+
+ return {children};
+};
+
+export function useAnonymousContext() {
+ const context = useContext(AnonymousContext);
+ if (!context) {
+ throw new Error('useAnonymousContext must be used within an AnonymousProvider');
+ }
+ return context;
+}
diff --git a/app/(sub-page)/components/navigation-items.tsx b/app/(sub-page)/components/navigation-items.tsx
index c1aefd3e..0f741295 100644
--- a/app/(sub-page)/components/navigation-items.tsx
+++ b/app/(sub-page)/components/navigation-items.tsx
@@ -14,7 +14,10 @@ export default async function NavigationItems() {
>
) : (
-
+ <>
+
+
+ >
)}
3. 우측 상단 조회버튼 → 프린트 아이콘
4. 인쇄 정보의 대상(PDF로 저장) 설정 → 하단 저장 버튼
5. 저장한 파일 업로드
-
- • 회원 가입한 학번과 일치하는 학번의 성적표를 입력해야 합니다.
-
diff --git a/app/(sub-page)/grade-upload/page.tsx b/app/(sub-page)/grade-upload/page.tsx
index 140f400f..d93839e5 100644
--- a/app/(sub-page)/grade-upload/page.tsx
+++ b/app/(sub-page)/grade-upload/page.tsx
@@ -12,7 +12,7 @@ export const metadata: Metadata = {
export default function GradeUploadPage() {
return (
-
+
diff --git a/app/business/services/lecture/taken-lecture.command.ts b/app/business/services/lecture/taken-lecture.command.ts
index 33b69e6d..1a80cd02 100644
--- a/app/business/services/lecture/taken-lecture.command.ts
+++ b/app/business/services/lecture/taken-lecture.command.ts
@@ -4,6 +4,8 @@ import { API_PATH } from '../../api-path';
import { BadRequestError, HttpError } from '@/app/utils/http/http-error';
import { instance } from '@/app/utils/api/instance';
import { ERROR_CODE } from '@/app/utils/api/constant';
+import { revalidateTag } from 'next/cache';
+import { TAG } from '@/app/utils/http/tag';
export const registerUserGrade = async (prevState: FormState, formData: FormData) => {
try {
@@ -33,6 +35,47 @@ export const registerUserGrade = async (prevState: FormState, formData: FormData
}
};
+export const registerAnonymousGrade = async (prevState: FormState, formData: FormData) => {
+ const engLv = formData.get('engLv');
+ const file = formData.get('file');
+ if (!(file instanceof File)) {
+ return {
+ isSuccess: false,
+ isFailure: true,
+ validationError: {},
+ message: '등록할 수 없는 파일입니다.',
+ };
+ }
+
+ const gradePDF = new FormData();
+ gradePDF.append('file', file);
+
+ const parsingText = await parsePDFtoText(gradePDF);
+ const res = await fetch(`${API_PATH.graduations}/check`, {
+ method: 'POST',
+ body: JSON.stringify({ engLv, parsingText }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!res.ok) {
+ return {
+ isSuccess: false,
+ isFailure: true,
+ validationError: {},
+ message: 'fail upload grade',
+ };
+ }
+ return {
+ isSuccess: true,
+ isFailure: false,
+ validationError: {},
+ message: 'success upload grade',
+ value: await res.json(),
+ };
+};
+
export const parsePDFtoText = async (formData: FormData) => {
return (await fetch(API_PATH.parsePDFtoText, { method: 'POST', body: formData })).text();
};
@@ -42,7 +85,6 @@ export const deleteTakenLecture = async (lectureId: number) => {
await instance.delete(`${API_PATH.takenLectures}/${lectureId}`, {
responseType: 'text',
});
-
return {
isSuccess: true,
};
@@ -66,6 +108,7 @@ export const addTakenLecture = async (lectureId: string) => {
responseType: 'text',
},
);
+ revalidateTag(TAG.GET_TAKEN_LECTURES);
return {
isSuccess: true,
isFailure: false,
diff --git a/app/business/services/lecture/taken-lecture.query.ts b/app/business/services/lecture/taken-lecture.query.ts
index ffc4a484..2885e07e 100644
--- a/app/business/services/lecture/taken-lecture.query.ts
+++ b/app/business/services/lecture/taken-lecture.query.ts
@@ -1,5 +1,6 @@
import { instance } from '@/app/utils/api/instance';
import { API_PATH } from '../../api-path';
+import { TAG } from '@/app/utils/http/tag';
export interface TakenLecturesResponse {
totalCredit: number;
@@ -17,6 +18,10 @@ export interface TakenLectureInfoResponse {
}
export const fetchTakenLectures = async () => {
- const response = await instance.get(API_PATH.takenLectures);
+ const response = await instance.get(API_PATH.takenLectures, {
+ next: {
+ tags: [TAG.GET_TAKEN_LECTURES],
+ },
+ });
return response.data;
};
diff --git a/app/page.tsx b/app/page.tsx
index e0f10200..3ba68c08 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -42,9 +42,11 @@ export default function HomePage() {
명지인을 위한 간편 졸업요건 검사 사이트
-
-
-
+
+
+
+
+
);
diff --git a/app/ui/lecture/upload-taken-lecture/upload-taken-lectrue-anonymous.tsx b/app/ui/lecture/upload-taken-lecture/upload-taken-lectrue-anonymous.tsx
new file mode 100644
index 00000000..13518666
--- /dev/null
+++ b/app/ui/lecture/upload-taken-lecture/upload-taken-lectrue-anonymous.tsx
@@ -0,0 +1,41 @@
+'use client';
+import Manual from '@/app/(sub-page)/grade-upload/components/manual';
+import { registerAnonymousGrade } from '@/app/business/services/lecture/taken-lecture.command';
+import Form from '@/app/ui/view/molecule/form';
+import UploadPdf from '@/app/ui/view/molecule/upload-pdf/upload-pdf';
+
+interface UploadTakenLectureAnonymousProp {
+ onSuccess?: (data: any) => void;
+}
+
+function UploadTakenLectureAnonymous({ onSuccess }: UploadTakenLectureAnonymousProp) {
+ const handleSuccess = (data: any) => {
+ onSuccess?.(data);
+ };
+
+ return (
+
+ );
+}
+
+export default UploadTakenLectureAnonymous;
diff --git a/app/ui/result/result-category-card/result-category-card.tsx b/app/ui/result/result-category-card/result-category-card.tsx
index 36f5ad11..7d16b12b 100644
--- a/app/ui/result/result-category-card/result-category-card.tsx
+++ b/app/ui/result/result-category-card/result-category-card.tsx
@@ -16,9 +16,10 @@ import Link from 'next/link';
import { useSetAtom } from 'jotai';
import { isDialogOpenAtom } from '@/app/store/stores/dialog';
import Button from '../../view/atom/button/button';
-import { getPercentage } from '@/app/utils/chart.util';
+import { getPercentage } from '@/app/utils/calculate.util';
import PieChart from '../../view/molecule/pie-chart/pie-chart';
import Responsive from '../../responsive';
+import { usePathname } from 'next/navigation';
interface ResultCategoryCardProps {
category: ResultCategoryKey;
@@ -59,6 +60,7 @@ const displaySeveralMajor = (category: ResultCategoryKey) => {
function ResultCategoryCard({ category, totalCredit, takenCredit }: ResultCategoryCardProps) {
const { open } = useDialog(DIALOG_KEY.RESULT_CATEGORY);
const setIsOpenDialog = useSetAtom(isDialogOpenAtom);
+ const pathname = usePathname();
const percentage = getPercentage(takenCredit, totalCredit);
@@ -73,7 +75,7 @@ function ResultCategoryCard({ category, totalCredit, takenCredit }: ResultCatego
const getCategoryCountWay = (category: ResultCategoryKey) => (category === RESULT_CATEGORY.CHAPEL ? '횟수' : '학점');
const filterCategoryExistStandard = (category: ResultCategoryKey) => {
- const NONEXIST_STANDARD_CATEGORY = ['FREE_ELECTIVE', 'NORMAL_CULTURE', 'CHAPEL'];
+ const NONEXIST_STANDARD_CATEGORY = ['FREE_ELECTIVE', 'NORMAL_CULTURE', 'CHAPEL', 'TRANSFER_CHRISTIAN'];
return NONEXIST_STANDARD_CATEGORY.includes(category);
};
@@ -111,7 +113,7 @@ function ResultCategoryCard({ category, totalCredit, takenCredit }: ResultCatego
className={`${filterCategoryExistStandard(category) && 'hidden'}`}
data-cy={`${category}-button`}
href={{
- pathname: '/result',
+ pathname: pathname,
query: {
category: category,
},
diff --git a/app/ui/result/result-category-detail/result-category-detail-info.tsx b/app/ui/result/result-category-detail/result-category-detail-info.tsx
index a78a1b17..1f243c42 100644
--- a/app/ui/result/result-category-detail/result-category-detail-info.tsx
+++ b/app/ui/result/result-category-detail/result-category-detail-info.tsx
@@ -2,6 +2,7 @@ import ResultCategoryDetailContent from '@/app/ui/result/result-category-detail-
import {
CreditResponse,
ResultCategoryDetailLecturesResponse,
+ ResultCategoryDetailResponse,
useFetchCredits,
useFetchResultCategoryDetailInfo,
} from '@/app/store/querys/result';
@@ -40,10 +41,20 @@ function addChapelToCommonCulture(
];
}
-export default function ResultCategoryDetailInfo({ category }: { category: ResultCategoryKey }) {
- const { data: categoryInfo } = useFetchResultCategoryDetailInfo(category);
- const { data: categories } = useFetchCredits();
+interface ResultCategoryDetailInfoProp {
+ category: ResultCategoryKey;
+}
+
+interface ResultCategoryDetailInfoViewerProps extends ResultCategoryDetailInfoProp {
+ categories: CreditResponse[];
+ categoryInfo: ResultCategoryDetailResponse;
+}
+export function ResultCategoryDetailInfoViewer({
+ categories,
+ categoryInfo,
+ category,
+}: ResultCategoryDetailInfoViewerProps) {
const chapel = categories.find(({ category }) => category === 'CHAPEL');
const isCommonCulture = category === RESULT_CATEGORY.COMMON_CULTURE;
const detailCategory = isCommonCulture
@@ -59,3 +70,10 @@ export default function ResultCategoryDetailInfo({ category }: { category: Resul
/>
);
}
+
+export default function ResultCategoryDetailInfo({ category }: ResultCategoryDetailInfoProp) {
+ const { data: categoryInfo } = useFetchResultCategoryDetailInfo(category);
+ const { data: categories } = useFetchCredits();
+
+ return ;
+}
diff --git a/app/ui/result/result-category/result-category.tsx b/app/ui/result/result-category/result-category.tsx
index 3d260e75..909165ec 100644
--- a/app/ui/result/result-category/result-category.tsx
+++ b/app/ui/result/result-category/result-category.tsx
@@ -1,9 +1,13 @@
'use client';
import { cn } from '@/app/utils/shadcn/utils';
-import ResultCategoryCard from '../result-category-card/result-category-card';
-import { useFetchCredits } from '@/app/store/querys/result';
-import { RESULT_CATEGORY } from '@/app/utils/key/result-category.key';
-import { ResultCategoryKey } from '../result-category-detail-content/result-category-detail-content.stories';
+import { CreditResponse, useFetchCredits } from '@/app/store/querys/result';
+import { RESULT_CATEGORY, ResultCategoryKey } from '@/app/utils/key/result-category.key';
+import ResultCategoryCard from '@/app/ui/result/result-category-card/result-category-card';
+
+interface ResultCategoryViewerProp {
+ categories: CreditResponse[];
+ className?: string;
+}
const getPriority = (category: ResultCategoryKey) => {
return [
@@ -15,16 +19,14 @@ const getPriority = (category: ResultCategoryKey) => {
}, 0);
};
-function ResultCategory() {
- const { data: categories } = useFetchCredits();
-
+export function ResultCategoryViewer({ categories, className }: ResultCategoryViewerProp) {
const sortedCategories = categories.sort((a, b) => getPriority(a.category) - getPriority(b.category));
-
return (
{sortedCategories.map(({ category, totalCredit, takenCredit }, index) => (
@@ -38,4 +40,8 @@ function ResultCategory() {
);
}
-export default ResultCategory;
+
+export default function ResultCategory() {
+ const { data } = useFetchCredits();
+ return ;
+}
diff --git a/app/ui/user/user-info-card/user-info-card.tsx b/app/ui/user/user-info-card/user-info-card.tsx
index 0e6b8cd7..fba18165 100644
--- a/app/ui/user/user-info-card/user-info-card.tsx
+++ b/app/ui/user/user-info-card/user-info-card.tsx
@@ -1,6 +1,6 @@
import { InitUserInfoResponse, UserInfoResponse } from '@/app/business/services/user/user.type';
import InitUserAnnounce from './init-user-announce';
-import UserInfoContent from './user-info-content';
+import UserInfoContent from './user-info-content/user-info-content';
import { fetchUser } from '@/app/business/services/user/user.query';
async function UserInfoCard() {
diff --git a/app/ui/user/user-info-card/user-info-content.tsx b/app/ui/user/user-info-card/user-info-content.tsx
deleted file mode 100644
index 05908f9f..00000000
--- a/app/ui/user/user-info-card/user-info-content.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-'use client';
-import { UserInfoResponse } from '@/app/business/services/user/user.type';
-import { MAJOR_NOTATION } from '@/app/utils/key/result-category.key';
-import React from 'react';
-import PieChart from '../../view/molecule/pie-chart/pie-chart';
-import { getPercentage } from '@/app/utils/chart.util';
-import UserInfoMessage from './user-info-message';
-import { useFetchCredits } from '@/app/store/querys/result';
-
-interface UserInfoContentProps {
- data: UserInfoResponse;
-}
-
-function UserInfoContent({ data }: UserInfoContentProps) {
- const { data: categories } = useFetchCredits();
- const { studentNumber, studentName, completeDivision: majors, totalCredit, takenCredit, graduated } = data;
-
- const remainCredit = categories.reduce((accumulator, category) => {
- const { category: categoryName, totalCredit, takenCredit } = category;
- if (categoryName === 'CHAPEL') return accumulator;
- const categoryRemainCredit = totalCredit - takenCredit < 0 ? 0 : totalCredit - takenCredit;
- return accumulator + categoryRemainCredit;
- }, 0);
-
- const percentage = getPercentage(totalCredit - remainCredit, totalCredit);
-
- const displaySeveralMajor = (notation: 'major' | 'title'): React.ReactNode => {
- return majors.map((major, index) => {
- const { major: majorName, majorType } = major;
-
- return {notation === 'title' ? MAJOR_NOTATION[majorType] : majorName};
- });
- };
-
- return (
- <>
-
-
-
-
- - 이름
- - 학번
- {displaySeveralMajor('title')}
- - 졸업최소학점
- - 현재이수학점
- - 졸업가능여부
-
-
- - {studentNumber}
- - {studentName}
- {displaySeveralMajor('major')}
- - {totalCredit}
- - {takenCredit}
- - {graduated ? '가능' : '불가능'}
-
-
-
-
-
- * 서비스의 결과는 공식적인 효력을 갖지 않습니다. 정확한 졸업사정결과는 소속 단과대 교학팀에서의 확인을
- 권장합니다.
-
- >
- );
-}
-
-export default UserInfoContent;
diff --git a/app/ui/user/user-info-card/user-info-content/user-info-content-viewer.tsx b/app/ui/user/user-info-card/user-info-content/user-info-content-viewer.tsx
new file mode 100644
index 00000000..16c167e4
--- /dev/null
+++ b/app/ui/user/user-info-card/user-info-content/user-info-content-viewer.tsx
@@ -0,0 +1,46 @@
+import { UserInfoResponse } from '@/app/business/services/user/user.type';
+import React from 'react';
+import PieChart from '../../../view/molecule/pie-chart/pie-chart';
+import { getPercentage } from '@/app/utils/calculate.util';
+import UserInfoMessage from './user-info-message';
+import { CreditResponse } from '@/app/store/querys/result';
+import UserInfoList from './user-info-list';
+
+interface UserInfoContentViewerProps {
+ data: UserInfoResponse;
+ categories: CreditResponse[];
+}
+
+export const getRemainCredit = (categories: CreditResponse[]) => {
+ return categories.reduce((accumulator, category) => {
+ const { category: categoryName, totalCredit, takenCredit } = category;
+ if (categoryName === 'CHAPEL') return accumulator;
+ const categoryRemainCredit = totalCredit - takenCredit < 0 ? 0 : totalCredit - takenCredit;
+ return accumulator + categoryRemainCredit;
+ }, 0);
+};
+
+function UserInfoContentViewer({ data, categories }: UserInfoContentViewerProps) {
+ const { studentName, totalCredit } = data;
+
+ const remainCredit = getRemainCredit(categories);
+ const percentage = getPercentage(totalCredit - remainCredit, totalCredit);
+
+ return (
+ <>
+
+
+
+ * 서비스의 결과는 공식적인 효력을 갖지 않습니다. 정확한 졸업사정결과는 소속 단과대 교학팀에서의 확인을
+ 권장합니다.
+
+ >
+ );
+}
+
+export default UserInfoContentViewer;
diff --git a/app/ui/user/user-info-card/user-info-content/user-info-content.tsx b/app/ui/user/user-info-card/user-info-content/user-info-content.tsx
new file mode 100644
index 00000000..fcb6e9a0
--- /dev/null
+++ b/app/ui/user/user-info-card/user-info-content/user-info-content.tsx
@@ -0,0 +1,15 @@
+'use client';
+import { UserInfoResponse } from '@/app/business/services/user/user.type';
+import { useFetchCredits } from '@/app/store/querys/result';
+import UserInfoContentViewer from './user-info-content-viewer';
+
+interface UserInfoContentProp {
+ data: UserInfoResponse;
+}
+
+function UserInfoContent({ data }: UserInfoContentProp) {
+ const { data: categories } = useFetchCredits();
+ return ;
+}
+
+export default UserInfoContent;
diff --git a/app/ui/user/user-info-card/user-info-content/user-info-list.tsx b/app/ui/user/user-info-card/user-info-content/user-info-list.tsx
new file mode 100644
index 00000000..e0b17cea
--- /dev/null
+++ b/app/ui/user/user-info-card/user-info-content/user-info-list.tsx
@@ -0,0 +1,41 @@
+import { UserInfoResponse } from '@/app/business/services/user/user.type';
+import { MAJOR_NOTATION } from '@/app/utils/key/result-category.key';
+
+interface UserInfoListProps {
+ data: UserInfoResponse;
+}
+
+function UserInfoList({ data }: UserInfoListProps) {
+ const { studentNumber, studentName, completeDivision: majors, totalCredit, takenCredit, graduated } = data;
+
+ const displaySeveralMajor = (notation: 'major' | 'title'): React.ReactNode => {
+ return majors.map((major, index) => {
+ const { major: majorName, majorType } = major;
+
+ return {notation === 'title' ? MAJOR_NOTATION[majorType] : majorName};
+ });
+ };
+
+ return (
+
+
+ - 이름
+ - 학번
+ {displaySeveralMajor('title')}
+ - 졸업최소학점
+ - 현재이수학점
+ - 졸업가능여부
+
+
+ - {studentNumber}
+ - {studentName}
+ {displaySeveralMajor('major')}
+ - {totalCredit}
+ - {takenCredit}
+ - {graduated ? '가능' : '불가능'}
+
+
+ );
+}
+
+export default UserInfoList;
diff --git a/app/ui/user/user-info-card/user-info-message.tsx b/app/ui/user/user-info-card/user-info-content/user-info-message.tsx
similarity index 100%
rename from app/ui/user/user-info-card/user-info-message.tsx
rename to app/ui/user/user-info-card/user-info-content/user-info-message.tsx
diff --git a/app/utils/chart.util.ts b/app/utils/calculate.util.ts
similarity index 71%
rename from app/utils/chart.util.ts
rename to app/utils/calculate.util.ts
index 2f1bb0c5..a2cfb1d8 100644
--- a/app/utils/chart.util.ts
+++ b/app/utils/calculate.util.ts
@@ -1,4 +1,6 @@
export const getPercentage = (numerator: number, denominator: number) => {
+ if (numerator >= denominator) return 100;
const percentage = Number(((numerator / denominator) * 100).toFixed(0));
+ if (denominator === 0) return 0;
return percentage > 100 ? 100 : percentage;
};
diff --git a/app/utils/key/result-category.key.ts b/app/utils/key/result-category.key.ts
index d3ddef48..a28d78d9 100644
--- a/app/utils/key/result-category.key.ts
+++ b/app/utils/key/result-category.key.ts
@@ -11,6 +11,7 @@ export const RESULT_CATEGORY = {
NORMAL_CULTURE: 'NORMAL_CULTURE',
FREE_ELECTIVE: 'FREE_ELECTIVE',
CHAPEL: 'CHAPEL',
+ TRANSFER_CHRISTIAN: 'TRANSFER_CHRISTIAN',
} as const;
export const RESULT_CATEGORY_KO = {
@@ -26,6 +27,7 @@ export const RESULT_CATEGORY_KO = {
NORMAL_CULTURE: '일반교양',
FREE_ELECTIVE: '자유선택',
CHAPEL: '채플',
+ TRANSFER_CHRISTIAN: '편입 기독교',
} as const;
export type ResultCategoryKey = (typeof RESULT_CATEGORY)[keyof typeof RESULT_CATEGORY];
diff --git a/app/utils/parser/anonymous.ts b/app/utils/parser/anonymous.ts
new file mode 100644
index 00000000..559bf142
--- /dev/null
+++ b/app/utils/parser/anonymous.ts
@@ -0,0 +1,139 @@
+import { UserInfoResponse } from '@/app/business/services/user/user.type';
+import {
+ CreditResponse,
+ ResultCategoryDetailLecturesResponse,
+ ResultCategoryDetailResponse,
+} from '@/app/store/querys/result';
+import { RESULT_CATEGORY } from '../key/result-category.key';
+
+interface AreaType {
+ totalCredit: number;
+ detailCategory: DetailCategoryType[];
+ graduationCategory: keyof typeof RESULT_CATEGORY;
+ takenCredit: number;
+ completed: boolean;
+ normalLeftCredit: number;
+ freeElectiveLeftCredit: number;
+}
+
+interface DetailCategoryType {
+ totalCredits: number;
+ takenCredits: number;
+ takenLectures: LectureType[];
+ haveToLectures: LectureType[];
+ detailCategoryName: string;
+ normalLeftCredit: number;
+ freeElectiveLeftCredit: number;
+ completed: boolean;
+ satisfiedMandatory: boolean;
+}
+
+interface CategoryType {
+ totalCredit: number;
+ takenCredit: number;
+ categoryName: string;
+ completed: boolean;
+ leftCredit?: number;
+}
+
+interface LectureType {
+ id: string;
+ name: string;
+ credit: number;
+ duplicateCode: string;
+ isRevoked: number;
+ culture: boolean;
+}
+
+export interface AnonymousResultType {
+ user: UserInfoResponse;
+ graduationResult: {
+ chapelResult: {
+ takenCount: number;
+ completed: boolean;
+ takenChapelCredit: number;
+ };
+ detailGraduationResults: AreaType[];
+ normalCultureGraduationResult: CategoryType;
+ freeElectiveGraduationResult: CategoryType;
+ totalCredit: number;
+ takenCredit: number;
+ graduated: boolean;
+ };
+}
+
+export const parseUserInfo = (data: AnonymousResultType): UserInfoResponse => {
+ return {
+ ...data.user,
+ takenCredit: data.graduationResult.takenCredit,
+ totalCredit: data.graduationResult.totalCredit,
+ };
+};
+
+export const parseCredit = ({ graduationResult }: AnonymousResultType): CreditResponse[] => {
+ const detailCredits = graduationResult.detailGraduationResults.map(
+ (item): CreditResponse => ({
+ category: item.graduationCategory,
+ totalCredit: item.totalCredit,
+ takenCredit: item.takenCredit,
+ completed: item.completed,
+ }),
+ );
+
+ const additionalCredits: CreditResponse[] = [
+ {
+ category: 'CHAPEL',
+ totalCredit: graduationResult.chapelResult.takenCount * 0.5,
+ takenCredit: graduationResult.chapelResult.takenChapelCredit,
+ completed: graduationResult.chapelResult.completed,
+ },
+ {
+ category: 'NORMAL_CULTURE',
+ totalCredit: graduationResult.normalCultureGraduationResult.totalCredit,
+ takenCredit: graduationResult.normalCultureGraduationResult.takenCredit,
+ completed: graduationResult.normalCultureGraduationResult.completed,
+ },
+ {
+ category: 'FREE_ELECTIVE',
+ totalCredit: graduationResult.freeElectiveGraduationResult.totalCredit,
+ takenCredit: graduationResult.freeElectiveGraduationResult.takenCredit,
+ completed: graduationResult.freeElectiveGraduationResult.completed,
+ },
+ ];
+
+ return [...detailCredits, ...additionalCredits];
+};
+
+export const parseCreditDetailInfo = (result: AnonymousResultType, category: string): ResultCategoryDetailResponse => {
+ const item = result.graduationResult.detailGraduationResults.filter((value) => {
+ return category === value.graduationCategory;
+ })[0];
+
+ return {
+ detailCategory: item.detailCategory.map((detailItem): ResultCategoryDetailLecturesResponse => {
+ return {
+ categoryName: detailItem.detailCategoryName,
+ totalCredit: detailItem.totalCredits,
+ takenCredit: detailItem.takenCredits,
+ completed: detailItem.completed,
+ takenLectures: detailItem.takenLectures.map((lecture) => {
+ return {
+ id: lecture.id,
+ name: lecture.name,
+ credit: lecture.credit,
+ };
+ }),
+ haveToLectures: detailItem.haveToLectures.map((lecture) => {
+ return {
+ id: lecture.id,
+ name: lecture.name,
+ credit: lecture.credit,
+ };
+ }),
+ };
+ }),
+ totalCredit: item.totalCredit,
+ takenCredit: item.takenCredit,
+ completed: item.completed,
+ };
+};
diff --git a/app/utils/test/__mock__/next/headers.ts b/app/utils/test/__mock__/next/headers.ts
index 8a4f62dd..3d9eaa30 100644
--- a/app/utils/test/__mock__/next/headers.ts
+++ b/app/utils/test/__mock__/next/headers.ts
@@ -5,7 +5,6 @@ export function cookies() {
store[key] = {
value,
};
- console.log(store[key]);
},
get: (key: string) => {
return store[key];
diff --git a/middleware.ts b/middleware.ts
index 941185f9..fa89e80a 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -27,8 +27,8 @@ async function getAuth(request: NextRequest): Promise<{
}
const allowedOnlyGuestPath = ['/sign-in', '/sign-up', '/find-password', '/find-id'];
-const allowedGuestPath = ['/', '/tutorial', ...allowedOnlyGuestPath];
-const allowInitUserPath = ['/', '/tutorial', '/grade-upload'];
+const allowedGuestPath = ['/', '/tutorial', '/anonymous', '/anonymous/result', ...allowedOnlyGuestPath];
+const allowInitUserPath = ['/', '/tutorial', '/grade-upload', '/anonymous', '/anonymous/result'];
function isAllowedGuestPath(path: string, strict: boolean = false) {
const allowedPath = strict ? allowedOnlyGuestPath : allowedGuestPath;