Skip to content

Commit

Permalink
Merge pull request #168 from Myongji-Graduate/anonymous/#164
Browse files Browse the repository at this point in the history
feat: 비회원으로 검사하기 기능 구현 & 편입 기독교 타입 추가
  • Loading branch information
gahyuun authored Dec 23, 2024
2 parents 8e4dfcd + 58bf057 commit ce2be3f
Show file tree
Hide file tree
Showing 27 changed files with 563 additions and 96 deletions.
40 changes: 40 additions & 0 deletions app/(sub-page)/anonymous/components/anonymous-container.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Funnel>
<Funnel.Step name="terms">
<SignUpTerm
onNext={() => {
setStep('form');
}}
/>
</Funnel.Step>
<Funnel.Step name="form">
<AnonymousUpload
onNext={(formState?: FormState) => {
if (isAnonymousResultType(formState?.value)) {
setResult(formState.value);
router.push('/anonymous/result');
}
}}
/>
</Funnel.Step>
</Funnel>
);
}
10 changes: 10 additions & 0 deletions app/(sub-page)/anonymous/components/anonymous-upload.tsx
Original file line number Diff line number Diff line change
@@ -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 <UploadTakenLectureAnonymous onSuccess={onNext} />;
}
10 changes: 10 additions & 0 deletions app/(sub-page)/anonymous/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 <AnonymousProvider>{children}</AnonymousProvider>;
}
19 changes: 19 additions & 0 deletions app/(sub-page)/anonymous/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Suspense>
<ContentContainer className="md:w-[768px] xl:w-[960px] p-4 py-6 md:p-8">
<AnonymousContainer />
</ContentContainer>
</Suspense>
);
}
42 changes: 42 additions & 0 deletions app/(sub-page)/anonymous/result/component/anonymous-result.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex flex-col items-center">
<div className="w-full">
<UserInfoContentViewer data={parseUserInfo(result)} categories={parseCredit(result)} />
</div>
<ResultCategoryViewer categories={parseCredit(result)} className="top-[20rem] md:top-[27rem]" />
{category ? (
<ResultCategoryDetailDialog querystring={category}>
<ResultCategoryDetailInfoViewer
category={category}
categories={parseCredit(result)}
categoryInfo={parseCreditDetailInfo(result, category)}
/>
</ResultCategoryDetailDialog>
) : null}
</div>
);
};

export default AnonymousResult;
37 changes: 37 additions & 0 deletions app/(sub-page)/anonymous/result/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ContentContainer className="max-md:max-w-[500px] md:w-[700px] p-4 py-6 md:p-8">
<AnonymousResult category={category} />
</ContentContainer>
);
}

export default AnonymousResultPage;
24 changes: 24 additions & 0 deletions app/(sub-page)/anonymous/result/provider.tsx
Original file line number Diff line number Diff line change
@@ -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<AnonymousContextType | null>(null);

export const AnonymousProvider = ({ children }: { children: ReactNode }) => {
const [result, setResult] = useState<AnonymousResultType>();

return <AnonymousContext.Provider value={{ result, setResult }}>{children}</AnonymousContext.Provider>;
};

export function useAnonymousContext() {
const context = useContext(AnonymousContext);
if (!context) {
throw new Error('useAnonymousContext must be used within an AnonymousProvider');
}
return context;
}
5 changes: 4 additions & 1 deletion app/(sub-page)/components/navigation-items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ export default async function NavigationItems() {
<NavigationItem href={'/result'} label="결과확인" />
</>
) : (
<NavigationItem href={'/sign-in'} label="로그인" />
<>
<NavigationItem href={'/sign-in'} label="로그인" />
<NavigationItem href={'/anonymous'} label="비회원 검사" />
</>
)}
<NavigationItem href={'/tutorial'} label="튜토리얼" />
<NavigationItem
Expand Down
3 changes: 0 additions & 3 deletions app/(sub-page)/grade-upload/components/manual.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ export default function Manual() {
<div>3. 우측 상단 조회버튼 → 프린트 아이콘 </div>
<div>4. 인쇄 정보의 대상(PDF로 저장) 설정 → 하단 저장 버튼 </div>
<div>5. 저장한 파일 업로드 </div>
<div className="text-xs md:text-sm text-primary">
• 회원 가입한 학번과 일치하는 학번의 성적표를 입력해야 합니다.
</div>
</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/(sub-page)/grade-upload/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const metadata: Metadata = {

export default function GradeUploadPage() {
return (
<ContentContainer className="flex flex-col justify-center gap-8 min-h-[70vh] max-md:max-w-[600px]">
<ContentContainer className="flex flex-col justify-center gap-6 min-h-[70vh] max-md:max-w-[600px]">
<Manual />
<UploadTakenLecture />
</ContentContainer>
Expand Down
41 changes: 41 additions & 0 deletions app/business/services/lecture/taken-lecture.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,47 @@ export const registerUserGrade = async (prevState: FormState, formData: FormData
redirect('/result');
};

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) => {
const res = await fetch(API_PATH.parsePDFtoText, { method: 'POST', body: formData });
if (!res.ok) {
Expand Down
8 changes: 5 additions & 3 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,11 @@ export default function HomePage() {
</div>
<div className="text-md sm:text-lg text-gray-400 font-medium">명지인을 위한 간편 졸업요건 검사 사이트</div>
</p>
<Link href="/result">
<Button label="검사 시작" variant="dark" size="xl" />
</Link>
<div className="flex flex-col gap-2 md:gap-4">
<Link href="/result">
<Button label="검사 시작" variant="dark" size="xl" />
</Link>
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Form action={registerAnonymousGrade} id="성적업로드" onSuccess={handleSuccess}>
<Manual />
<div className="mt-8 md:w-96 w-80 m-auto flex flex-col gap-4">
<Form.Select
required={true}
label="영어성적"
id="engLv"
placeholder="선택하세요"
options={[
{ value: 'BASIC', placeholder: '기초영어' },
{ value: 'ENG12', placeholder: 'Level12' },
{ value: 'ENG34', placeholder: 'Level34' },
{ value: 'FREE', placeholder: '면제' },
]}
/>
<UploadPdf />
</div>
<div className="py-6">
<Form.SubmitButton label="결과 보러가기" position="center" size="md" />
</div>
</Form>
);
}

export default UploadTakenLectureAnonymous;
8 changes: 5 additions & 3 deletions app/ui/result/result-category-card/result-category-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -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);
};

Expand Down Expand Up @@ -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,
},
Expand Down
Loading

0 comments on commit ce2be3f

Please sign in to comment.