Skip to content

Commit

Permalink
chore: Merge branch 'main' into sign-up-form/#7
Browse files Browse the repository at this point in the history
  • Loading branch information
seonghunYang committed Mar 27, 2024
2 parents 397f150 + 3fb4a86 commit f6e7028
Show file tree
Hide file tree
Showing 32 changed files with 473 additions and 64 deletions.
10 changes: 0 additions & 10 deletions app/(sub-page)/file-upload/page.tsx

This file was deleted.

32 changes: 32 additions & 0 deletions app/(sub-page)/grade-upload/components/manual.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export default function Manual() {
return (
<div className="flex justify-center">
<div className="flex flex-col gap-6">
<h1 className="text-center text-3xl font-bold p-4 border-b-2 border-gray-100 md:text-4xl">
한 번의 성적표 입력으로
<br /> 맞춤형 결과를 확인하세요 !
</h1>
<div className="text-base flex flex-col gap-2 md:text-lg">
<div>
1.
<a
target="_blank"
className="pl-1 text-primary hover:text-dark-hover"
href="https://msi.mju.ac.kr/servlet/security/MySecurityStart"
>
MyiWeb MSI
</a>
에 접속 후 로그인(PC환경 권장)
</div>
<div>2. 좌측 성적/졸업 메뉴 → 성적표(상담용,B4)클릭</div>
<div>3. 우측 상단 조회버튼 클릭 → 프린트 아이콘 클릭</div>
<div>4. 인쇄 정보의 대상(PDF로 저장) 설정 → 하단 저장 버튼 클릭 </div>
<div>5. 저장한 파일 업로드 </div>
<div className="text-xs md:text-sm text-primary">
• 회원 가입한 학번과 일치하는 학번의 성적표를 입력해야 합니다.
</div>
</div>
</div>
</div>
);
}
12 changes: 12 additions & 0 deletions app/(sub-page)/grade-upload/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import ContentContainer from '../../ui/view/atom/content-container';
import Manual from './components/manual';
import UploadTakenLecture from '../../ui/lecture/upload-taken-lecture/upload-taken-lecture';

export default function GradeUploadPage() {
return (
<ContentContainer className="flex flex-col justify-center gap-8 min-h-[70vh]">
<Manual />
<UploadTakenLecture />
</ContentContainer>
);
}
17 changes: 15 additions & 2 deletions app/__test__/ui/lecture/taken-lecture-list.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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';
Expand All @@ -7,24 +8,36 @@ describe('Taken lecture list', () => {
render(await TakenLecture());
expect(await screen.findByTestId('table-data'));
});
it('커스텀하기 클릭 시 기이수 과목 리스트가 변경된다.', async () => {

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 () => {

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();
});
});
30 changes: 30 additions & 0 deletions app/__test__/ui/view/upload-pdf.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import UploadPdf from '@/app/ui/view/molecule/upload-pdf/upload-pdf';
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';

describe('성적 업로드', () => {
it('파일이 업로드 될 때, pdf file을 업로드 하면 file명이 노출된다.', async () => {
render(<UploadPdf />);

const targetFile = new File(['grade'], 'grade.pdf', {
type: 'text/plain',
});

const uploadBox = await screen.findByTestId('upload-box');
await userEvent.upload(uploadBox, targetFile);

expect(screen.getByText(targetFile.name)).toBeInTheDocument();
});

it('파일이 업로드 될 때, pdf가 아닌 file을 업로드 하면 변화가 발생하지않는다.', async () => {
render(<UploadPdf />);

const targetFile = new File(['grade'], 'grade.png', {
type: 'text/plain',
});

const uploadBox = await screen.findByTestId('upload-box');
await userEvent.upload(uploadBox, targetFile);
expect(screen.queryByText('마우스로 드래그 하거나 아이콘을 눌러 추가해주세요.')).toBeInTheDocument();
});
});
2 changes: 2 additions & 0 deletions app/business/api-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const BASE_URL = process.env.API_MOCKING === 'enable' ? 'http://localhost:9090'

export const API_PATH = {
revenue: `${BASE_URL}/revenue`,
registerUserGrade: `${BASE_URL}/registerUserGrade`,
parsePDFtoText: `${BASE_URL}/parsePDFtoText`,
takenLectures: `${BASE_URL}/taken-lectures`,
user: `${BASE_URL}/users`,
};
35 changes: 35 additions & 0 deletions app/business/lecture/taken-lecture.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use server';
import { FormState } from '@/app/ui/view/molecule/form/form-root';
import { API_PATH } from '../api-path';

export const registerUserGrade = async (prevState: FormState, formData: FormData) => {
const parsingText = await parsePDFtoText(formData);

const res = await fetch(API_PATH.registerUserGrade, {
method: 'POST',
body: JSON.stringify({ parsingText }),
});

if (!res.ok) {
return {
errors: {},
message: 'fail upload grade',
};
}

return {
errors: {},
message: '',
};
};

export const parsePDFtoText = async (formData: FormData) => {
const res = await fetch(API_PATH.parsePDFtoText, { method: 'POST', body: formData });
if (!res.ok) {
return {
errors: {},
message: 'fail parsing to text',
};
}
return await res.json();
};
19 changes: 19 additions & 0 deletions app/hooks/usePdfFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from 'react';
import { z } from 'zod';

export type FileType = File | null;

export default function usePdfFile() {
const [file, setFile] = useState<FileType>(null);

const changeFile = (file: File) => {
if (!validate.parse(file.name)) return;
setFile(file);
};

const validate = z.string().refine((value) => value.endsWith('.pdf'), {
message: 'File must be a PDF',
});

return { file, changeFile };
}
2 changes: 2 additions & 0 deletions app/mocks/data.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export const revenue = [
{ month: 'Nov', revenue: 3000 },
{ month: 'Dec', revenue: 4800 },
];
export const parsePDF =
'출력일자 : 2022/10/05|1/1|ICT융합대학 융합소프트웨어학부 응용소프트웨어전공, 모유경(60201671), 현학적 - 재학, 이수 - 4, 입학 - 신입학(2020/03/02)|토익 - 440, 영어교과목면제 - 면제없음, 최종학적변동 - 불일치복학(2022/07/12)|편입생 인정학점 - 교양 0, 전공 0, 자유선택 0, 성경과인간이해 0|교환학생 인정학점 - 학문기초교양 0, 일반교양 0, 전공 0, 복수전공학문기초교양 0, 복수전공 0, 연계전공 0, 부전공 0, 자유선택 0|공통교양 17, 핵심교양 12, 학문기초교양 12, 일반교양 3, 전공 33, 복수전공 0, 연계전공 0, 부전공 0, 교직 0, 자유선택 0|총 취득학점 - 77, 총점 - 296.5, 평균평점 - 4.06|이수구분|수강년도/학기|한글코드|과목코드|과목명|학점|등급|중복|공통교양 (구 필수교양)|2020년 2학기|교필141|KMA02141|4차산업혁명과미래사회진로선택|2|P|공통교양 (구 필수교양)|2021년 1학기|교필104|KMA02104|글쓰기|3|A+|공통교양 (구 필수교양)|2021년 2학기|교필122|KMA02122|기독교와문화|2|A0|공통교양 (구 필수교양)|2020년 1학기|교필106|KMA02106|영어1|2|A0|공통교양 (구 필수교양)|2021년 1학기|교필107|KMA02107|영어2|2|A0|공통교양 (구 필수교양)|2021년 1학기|교필108|KMA02108|영어회화1|1|B+|공통교양 (구 필수교양)|2021년 2학기|교필109|KMA02109|영어회화2|1|B+|공통교양 (구 필수교양)|2020년 1학기|교필101|KMA02101|채플|0.5|P|공통교양 (구 필수교양)|2020년 2학기|교필101|KMA02101|채플|0.5|P|공통교양 (구 필수교양)|2021년 1학기|교필101|KMA02101|채플|0.5|P|공통교양 (구 필수교양)|2021년 2학기|교필101|KMA02101|채플|0.5|P|공통교양 (구 필수교양)|2020년 2학기|교필102|KMA02102|현대사회와기독교윤리|2|A+|핵심교양 (구 선택교양)|2021년 1학기|교선130|KMA02130|고전으로읽는인문학|3|B+|핵심교양 (구 선택교양)|2020년 1학기|교선127|KMA02127|창업입문|3|A+|핵심교양 (구 선택교양)|2021년 2학기|교선110|KMA02110|철학과인간|3|A+|핵심교양 (구 선택교양)|2020년 2학기|교선142|KMA02142|현대사회와심리학|3|A+|학문기초교양 (구 기초교양)|2020년 2학기|기사133|KMD02133|ICT비즈니스와경영|3|A+|학문기초교양 (구 기초교양)|2020년 1학기|기사134|KMD02134|마케팅과ICT융합기술|3|A+|학문기초교양 (구 기초교양)|2020년 1학기|기사135|KMD02135|저작권과소프트웨어|3|A+|학문기초교양 (구 기초교양)|2020년 2학기|기컴112|KMI02112|컴퓨터논리의이해|3|A+|일반교양 (구 균형교양)|2020년 2학기|기컴125|KMI02125|생활속의스마트IT(KCU)|3|A+|전공1단계|2021년 1학기|응소204|HEC01204|DB설계및구현1|3|B+|전공1단계|2021년 2학기|응소305|HEC01305|DB설계및구현2|3|B+|전공1단계|2020년 2학기|융소103|HEB01103|객체지향적사고와프로그래밍|3|B0|전공1단계|2021년 1학기|응소208|HEC01208|데이터구조와알고리즘1|3|B+|전공1단계|2021년 2학기|응소207|HEC01207|데이터구조와알고리즘2|3|B+|전공1단계|2021년 2학기|응소212|HEC01212|시스템프로그래밍1|3|B+|전공1단계|2021년 1학기|응소211|HEC01211|웹프로그래밍1|3|A+|전공1단계|2021년 2학기|응소209|HEC01209|웹프로그래밍2|3|A0|전공1단계|2020년 1학기|융소101|HEB01101|절차적사고와프로그래밍|3|A+|전공1단계|2021년 2학기|응소210|HEC01210|클라이언트서버프로그래밍|3|A0|전공1단계|2021년 1학기|응소202|HEC01202|패턴중심사고와프로그래밍|3|A0||';

export const takenLectures = JSON.parse(`{
"totalCredit": 115,
Expand Down
13 changes: 12 additions & 1 deletion app/mocks/handlers/taken-lecture-handler.mock.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { HttpResponse, http } from 'msw';
import { HttpResponse, http, delay } from 'msw';
import { API_PATH } from '../../business/api-path';
import { mockDatabase } from '../db.mock';
import { parsePDF } from '../data.mock';

export const takenLectureHandlers = [
http.get(API_PATH.takenLectures, () => {
const takenLectures = mockDatabase.getTakenLectures();

return HttpResponse.json(takenLectures[0]);
}),
http.post(API_PATH.parsePDFtoText, async () => {
await delay(1000);
console.log(parsePDF);
return HttpResponse.json(parsePDF);
}),

http.post(API_PATH.registerUserGrade, async () => {
await delay(1000);
throw new HttpResponse(null, { status: 200 });
}),
];
6 changes: 6 additions & 0 deletions app/store/custom-taken-lecture.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { atom } from 'jotai';
import { LectureInfo } from '../type/lecture';

export const isCustomizingAtom = atom<boolean>(false);

export const customLectureAtom = atom<LectureInfo[]>([]);
13 changes: 11 additions & 2 deletions app/type/lecture.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
export type LectureInfo = {
export interface LectureInfo {
[index: string]: string | number;
id: number;
year: string;
semester: string;
lectureCode: string;
lectureName: string;
credit: number;
};
}

export interface SearchedLectureInfo {
[index: string]: string | number;
id: number;
lectureCode: string;
name: string;
credit: number;
}
17 changes: 17 additions & 0 deletions app/ui/lecture/lecture-search/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';
import React from 'react';
import LectureSearchBar from './lecture-search-bar';
import LectureSearchResultContainer from './lecture-search-result-container';
import { isCustomizingAtom } from '@/app/store/custom-taken-lecture';
import { useAtomValue } from 'jotai';

export default function LectureSearch() {
const isCustomizing = useAtomValue(isCustomizingAtom);
if (!isCustomizing) return null;
return (
<div className="flex flex-col gap-4" data-testid="lecture-search-component">
<LectureSearchBar />
<LectureSearchResultContainer />
</div>
);
}
20 changes: 20 additions & 0 deletions app/ui/lecture/lecture-search/lecture-search-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Select from '../../view/molecule/select';
import TextInput from '../../view/atom/text-input/text-input';
import { MagnifyingGlassIcon } from '@radix-ui/react-icons';

export default function LectureSearchBar() {
// 검색 기능을 해당 컴포넌트에서 구현 예정
return (
<div className="flex justify-between">
<div className="w-[15%]">
<Select defaultValue="lectureName" placeholder="과목명">
<Select.Item value="lectureName" placeholder="과목명" />
<Select.Item value="lectureCode" placeholder="과목코드" />
</Select>
</div>
<div className="w-[40%] flex justify-between">
<TextInput placeholder="검색어를 입력해주세요" icon={MagnifyingGlassIcon} />
</div>
</div>
);
}
47 changes: 47 additions & 0 deletions app/ui/lecture/lecture-search/lecture-search-result-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import List from '../../view/molecule/list';
import Image from 'next/image';
import searchResultIcon from '@/public/assets/searchResultIcon.svg';
import Grid from '../../view/molecule/grid';
import { SearchedLectureInfo } from '@/app/type/lecture';
import AddTakenLectureButton from '../taken-lecture/add-taken-lecture-button';

const emptyDataRender = () => {
return (
<div className="flex flex-col items-center justify-center gap-2">
<Image src={searchResultIcon} alt="search-result-icon" width={40} height={40} />
<div className="text-md font-medium text-gray-400">검색 결과가 표시됩니다</div>
</div>
);
};

export default function LectureSearchResultContainer() {
const renderAddActionButton = (item: SearchedLectureInfo) => {
return <AddTakenLectureButton lectureItem={item} />;
};
const render = (item: SearchedLectureInfo, index: number) => {
const searchLectureItem = item;
return (
<List.Row key={index}>
<Grid cols={4}>
{Object.keys(searchLectureItem).map((key, index) => {
if (key === 'id') return null;
return <Grid.Column key={index}>{searchLectureItem[key]}</Grid.Column>;
})}
{renderAddActionButton ? <Grid.Column>{renderAddActionButton(searchLectureItem)}</Grid.Column> : null}
</Grid>
</List.Row>
);
};

return (
<List
data={[
{ id: 3, lectureCode: 'HCB03490', name: '경영정보사례연구', credit: 3 },
{ id: 4, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
]}
render={render}
isScrollList={true}
emptyDataRender={emptyDataRender}
/>
);
}
25 changes: 25 additions & 0 deletions app/ui/lecture/taken-lecture/add-taken-lecture-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SearchedLectureInfo } from '@/app/type/lecture';
import Button from '../../view/atom/button/button';
import { useAtom } from 'jotai';
import { customLectureAtom } from '@/app/store/custom-taken-lecture';

interface AddTakenLectureButtonProps {
lectureItem: SearchedLectureInfo;
}
export default function AddTakenLectureButton({ lectureItem }: AddTakenLectureButtonProps) {
const [customLecture, setCustomLecture] = useAtom(customLectureAtom);
const addLecture = () => {
setCustomLecture([
...customLecture,
{
id: lectureItem.id,
year: 'CUSTOM',
semester: 'CUSTOM',
lectureCode: lectureItem.lectureCode,
lectureName: lectureItem.name,
credit: lectureItem.credit,
},
]);
};
return <Button variant="list" label="추가" onClick={addLecture} />;
}
14 changes: 14 additions & 0 deletions app/ui/lecture/taken-lecture/delete-taken-lecture-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useAtom } from 'jotai';
import Button from '../../view/atom/button/button';
import { customLectureAtom } from '@/app/store/custom-taken-lecture';

interface DeleteTakenLectureButtonProps {
lectureId: number;
}
export default function DeleteTakenLectureButton({ lectureId }: DeleteTakenLectureButtonProps) {
const [customLecture, setCustomLecture] = useAtom(customLectureAtom);
const deleteLecture = () => {
setCustomLecture(customLecture.filter((lecture) => lecture.id !== lectureId));
};
return <Button label="삭제" variant="list" data-testid="taken-lecture-delete-button" onClick={deleteLecture} />;
}
Loading

0 comments on commit f6e7028

Please sign in to comment.