Skip to content

Commit

Permalink
Merge pull request #78 from Myongji-Graduate/add-lecture/#77
Browse files Browse the repository at this point in the history
Add lecture/#77
  • Loading branch information
gahyuun authored Apr 20, 2024
2 parents 8d47fa1 + 74d05c6 commit 8218843
Show file tree
Hide file tree
Showing 19 changed files with 227 additions and 60 deletions.
34 changes: 33 additions & 1 deletion app/business/lecture/taken-lecture.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const parsePDFtoText = async (formData: FormData) => {
return await res.json();
};

export const fetchDeleteLecture = async (lectureId: number) => {
export const deleteTakenLecture = async (lectureId: number) => {
try {
const response = await fetch(API_PATH.takenLectures, {
method: 'DELETE',
Expand All @@ -67,3 +67,35 @@ export const fetchDeleteLecture = async (lectureId: number) => {
isSuccess: true,
};
};

export const addTakenLecture = async (lectureId: number) => {
try {
const response = await fetch(API_PATH.takenLectures, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ lectureId }),
});
const result = await response.json();
httpErrorHandler(response, result);
} catch (error) {
if (error instanceof BadRequestError) {
return {
isSuccess: false,
isFailure: true,
validationError: {},
message: '과목 추가에 실패했습니다',
};
} else {
throw error;
}
}
revalidateTag(TAG.GET_TAKEN_LECTURES);
return {
isSuccess: true,
isFailure: false,
validationError: {},
message: '과목 추가에 성공했습니다',
};
};
17 changes: 16 additions & 1 deletion app/mocks/db.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface MockDatabaseState {
type MockDatabaseAction = {
getTakenLectures: () => TakenLectures;
getResultCategoryDetailInfo: () => ResultCategoryDetailInfo;
addTakenLecture: (lectureId: number) => boolean;
deleteTakenLecture: (lectureId: number) => boolean;
getUser: (authId: string) => MockUser | undefined;
createUser: (user: SignUpRequestBody) => boolean;
Expand All @@ -31,7 +32,7 @@ type MockDatabaseAction = {

export const mockDatabase: MockDatabaseAction = {
getTakenLectures: () => mockDatabaseStore.takenLectures,
deleteTakenLecture: (lectureId: number) => {
deleteTakenLecture: (lectureId) => {
if (mockDatabaseStore.takenLectures.takenLectures.find((lecture) => lecture.id === lectureId)) {
mockDatabaseStore.takenLectures.takenLectures = mockDatabaseStore.takenLectures.takenLectures.filter(
(lecture) => lecture.id !== lectureId,
Expand All @@ -40,6 +41,20 @@ export const mockDatabase: MockDatabaseAction = {
}
return false;
},
addTakenLecture: (lectureId) => {
mockDatabaseStore.takenLectures.takenLectures = [
...mockDatabaseStore.takenLectures.takenLectures,
{
id: lectureId,
year: '2023',
semester: '2학기',
lectureCode: 'HECD140',
lectureName: '추가한과목',
credit: 3,
},
];
return true;
},
getResultCategoryDetailInfo: () => mockDatabaseStore.resultCategoryDetailInfo,
getUser: (authId: string) => mockDatabaseStore.users.find((user) => user.authId === authId),
createUser: (user: SignUpRequestBody) => {
Expand Down
7 changes: 7 additions & 0 deletions app/mocks/handlers/taken-lecture-handler.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export const takenLectureHandlers = [
await delay(100);
return HttpResponse.json(takenLectures);
}),
http.post<never, { lectureId: number }>(API_PATH.takenLectures, async ({ request }) => {
const body = await request.json();
const isAdded = mockDatabase.addTakenLecture(body.lectureId);
await delay(1000);
if (isAdded) return HttpResponse.json({ message: '과목 추가에 성공했습니다' }, { status: 200 });
return HttpResponse.json({ errorCode: 400, message: '추가에 실패했습니다' }, { status: 400 });
}),
http.delete<never, { lectureId: number }>(API_PATH.takenLectures, async ({ request }) => {
const body = await request.json();
const isDeleted = mockDatabase.deleteTakenLecture(body.lectureId);
Expand Down
2 changes: 1 addition & 1 deletion app/type/lecture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface TakenLectrueInfo {
}

export interface LectureInfo {
[index: string]: string | number;
[index: string]: string | number | boolean;
id: number;
lectureCode: string;
name: string;
Expand Down
2 changes: 1 addition & 1 deletion app/ui/lecture/lecture-search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import LectureSearchResultContainer from './lecture-search-result-container';

export default function LectureSearch() {
return (
<div className="bg-white w-full h-[500px] sm:h-[400px] z-[10] flex justify-center">
<div className="bg-white w-full h-[500px] sm:h-[400px] z-[10] flex justify-center" data-testid="lecture-search">
<div className="w-[800px] mx-auto my-7 flex flex-col gap-10 sm:gap-6">
<LectureSearchBar />
<LectureSearchResultContainer />
Expand Down
26 changes: 15 additions & 11 deletions app/ui/lecture/lecture-search/lecture-search-result-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Grid from '../../view/molecule/grid';
import { LectureInfo } from '@/app/type/lecture';
import AddTakenLectureButton from '../taken-lecture/add-taken-lecture-button';

type SearchedLectureInfo = LectureInfo & { isTakenLecture: boolean };

const emptyDataRender = () => {
return (
<div className="flex flex-col items-center justify-center gap-2">
Expand All @@ -15,19 +17,21 @@ const emptyDataRender = () => {
};

export default function LectureSearchResultContainer() {
const renderAddActionButton = (item: LectureInfo) => {
return <AddTakenLectureButton lectureItem={item} />;
const renderAddActionButton = (item: SearchedLectureInfo, isTakenLecture: boolean) => {
return <AddTakenLectureButton lectureItem={item} isTakenLecture={isTakenLecture} />;
};
const render = (item: LectureInfo, index: number) => {
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;
if (key === 'id' || key === 'isTakenLecture') return null;
return <Grid.Column key={index}>{searchLectureItem[key]}</Grid.Column>;
})}
{renderAddActionButton ? <Grid.Column>{renderAddActionButton(searchLectureItem)}</Grid.Column> : null}
{renderAddActionButton ? (
<Grid.Column>{renderAddActionButton(searchLectureItem, item.isTakenLecture)}</Grid.Column>
) : null}
</Grid>
</List.Row>
);
Expand All @@ -36,12 +40,12 @@ export default function LectureSearchResultContainer() {
return (
<List
data={[
{ id: 3, lectureCode: 'HCB03490', name: '경영정보사례연구', credit: 3 },
{ id: 4, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
{ id: 5, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
{ id: 6, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
{ id: 7, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
{ id: 8, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3 },
{ id: 3, lectureCode: 'HCB03490', name: '경영정보사례연구', credit: 3, isTakenLecture: false },
{ id: 4, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3, isTakenLecture: true },
{ id: 5, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3, isTakenLecture: false },
{ id: 6, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3, isTakenLecture: true },
{ id: 7, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3, isTakenLecture: false },
{ id: 8, lectureCode: 'HCB03490', name: '게임을통한경영의이해', credit: 3, isTakenLecture: false },
]}
render={render}
isScrollList={true}
Expand Down
55 changes: 55 additions & 0 deletions app/ui/lecture/lecture-search/lecture-search.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent } from '@storybook/test';
import { mockDatabase, resetMockDB } from '@/app/mocks/db.mock';
import TakenLectureAtomHydrator from '@/app/store/taken-lecture-atom-hydrator';
import { screen } from '@storybook/testing-library';
import TakenLectureLabel from '../taken-lecture/taken-lecture-label';
import TakenLectureList from '../taken-lecture/taken-lecture-list';
import LectureSearch from '.';
import { DIALOG_KEY } from '@/app/utils/key/dialog.key';
import Drawer from '../../view/molecule/drawer/drawer';
import { delay } from 'msw';

const meta = {
title: 'ui/lecture/lecture-search',
component: LectureSearch,
decorators: [
(Story) => {
resetMockDB();
const data = mockDatabase.getTakenLectures();
return (
<>
<TakenLectureAtomHydrator initialValue={data.takenLectures}>
<TakenLectureLabel />
<TakenLectureList />
</TakenLectureAtomHydrator>
<Drawer drawerKey={DIALOG_KEY.LECTURE_SEARCH}>
<Story />
</Drawer>
</>
);
},
],
} as Meta<typeof TakenLectureList>;

export default meta;
type Story = StoryObj<typeof meta>;

export const AddSenario: Story = {
play: async ({ step }) => {
await step('사용자가 추가를 클릭하면 lecture search drawer 창이 띄워진다', async () => {
const toggleLectureSearch = await screen.findByTestId('toggle-lecture-search');
await userEvent.click(toggleLectureSearch);

const lectureSearch = await screen.findByTestId('lecture-search');
expect(lectureSearch).toBeInTheDocument();
});
await step('추가 버튼을 클릭하면 추가 버튼이 disabled 된다', async () => {
const addButton = await screen.findAllByTestId('add-taken-lecture-button');
await userEvent.click(addButton[0]);

await delay(3000);
expect(addButton[0]).toBeDisabled();
});
},
};
38 changes: 35 additions & 3 deletions app/ui/lecture/taken-lecture/add-taken-lecture-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,41 @@
import { LectureInfo } from '@/app/type/lecture';
import Button from '../../view/atom/button/button';
import Form from '../../view/molecule/form';
import { addTakenLecture } from '@/app/business/lecture/taken-lecture.command';
import { useToast } from '../../view/molecule/toast/use-toast';
import { useState } from 'react';

interface AddTakenLectureButtonProps {
lectureItem: LectureInfo;
isTakenLecture: boolean;
}
export default function AddTakenLectureButton({ lectureItem }: AddTakenLectureButtonProps) {
return <Button variant="list" label="추가" onClick={() => {}} />;
export default function AddTakenLectureButton({ lectureItem, isTakenLecture }: AddTakenLectureButtonProps) {
const { toast } = useToast();
const [disabled, setDisabled] = useState(isTakenLecture);

const handleSuccessOfAdditionTakenLecture = () => {
setDisabled(true);
return toast({
title: '과목 추가에 성공했습니다',
});
};

return (
<Form
id={`과목추가-${lectureItem.id}`}
action={() => {
return addTakenLecture(lectureItem.id);
}}
failMessageControl="toast"
onSuccess={handleSuccessOfAdditionTakenLecture}
>
<Form.SubmitButton
label="추가"
position="center"
variant="list"
disabled={disabled}
size="default"
data-testid="add-taken-lecture-button"
/>
</Form>
);
}
8 changes: 5 additions & 3 deletions app/ui/lecture/taken-lecture/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { fetchTakenLectures } from '@/app/business/lecture/taken-lecture.query';
import TakenLectureList from './taken-lecture-list';
import TakenLectureLabel from './taken-lecture-label';
import TakenLectureAtomHydrator from '@/app/store/taken-lecture-atom-hydrator';
import { Provider } from 'jotai';
import UpdateTakenLecture from './update-taken-lecture';

export default async function TakenLecture() {
const data = await fetchTakenLectures();
return (
<div className="flex flex-col gap-2">
<TakenLectureAtomHydrator initialValue={data.takenLectures}>
<TakenLectureLabel />
<TakenLectureList />
<UpdateTakenLecture data={data.takenLectures}>
<TakenLectureLabel />
<TakenLectureList />
</UpdateTakenLecture>
</TakenLectureAtomHydrator>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions app/ui/lecture/taken-lecture/taken-lecture-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export default function TakenLectureLabel() {
label="과목 추가"
variant="secondary"
size="xs"
data-testid="lecture-add-button"
onClick={() => toggle}
data-testid="toggle-lecture-search"
onClick={toggle}
/>
<Link href="/file-upload">
<Link href="/grade-upload">
<Button label="성적표 재업로드" variant="secondary" size="xs" />
</Link>
</div>
Expand Down
15 changes: 10 additions & 5 deletions app/ui/lecture/taken-lecture/taken-lecture-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import DeleteTakenLectureButton from './delete-taken-lecture-button';
import { takenLectureAtom } from '@/app/store/custom-taken-lecture';
import { useOptimistic } from 'react';
import { useAtom } from 'jotai';
import { fetchDeleteLecture } from '@/app/business/lecture/taken-lecture.command';
import { deleteTakenLecture } from '@/app/business/lecture/taken-lecture.command';
import { useToast } from '../../view/molecule/toast/use-toast';

const headerInfo = ['수강년도', '수강학기', '과목코드', '과목명', '학점'];
Expand All @@ -19,9 +19,9 @@ export default function TakenLectureList() {
return currentTakenLectures.filter((lecture) => lecture.id !== lectureId);
},
);
const handleLectureDelete = async (lectureId: number) => {
const handleDeleteTakenLecture = async (lectureId: number) => {
deleteOptimisticLecture(lectureId);
const result = await fetchDeleteLecture(lectureId);
const result = await deleteTakenLecture(lectureId);
if (!result.isSuccess) {
return toast({
title: '과목 삭제에 실패했습니다',
Expand All @@ -39,13 +39,18 @@ export default function TakenLectureList() {
headerInfo={headerInfo}
data={optimisticLecture}
renderActionButton={(id: number) => (
<DeleteTakenLectureButton lectureId={id} onDelete={handleLectureDelete} />
<DeleteTakenLectureButton lectureId={id} onDelete={handleDeleteTakenLecture} />
)}
/>
</div>
{/* mobile */}
<div className="block lg:hidden">
<Table headerInfo={headerInfo} data={optimisticLecture} onSwipeAction={handleLectureDelete} swipeable={true} />
<Table
headerInfo={headerInfo}
data={optimisticLecture}
onSwipeAction={handleDeleteTakenLecture}
swipeable={true}
/>
</div>
</>
);
Expand Down
4 changes: 2 additions & 2 deletions app/ui/lecture/taken-lecture/taken-lecture.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import { expect, userEvent } from '@storybook/test';
import { mockDatabase, resetMockDB } from '@/app/mocks/db.mock';
import TakenLectureList from './taken-lecture-list';
import TakenLectureAtomHydrator from '@/app/store/taken-lecture-atom-hydrator';
import { screen } from '@storybook/testing-library';
import { delay } from 'msw';

const meta = {
title: 'ui/taken-lecture/TakenLecture',
title: 'ui/lecture/taken-lecture',
component: TakenLectureList,
decorators: [
(Story) => {
Expand Down
17 changes: 17 additions & 0 deletions app/ui/lecture/taken-lecture/update-taken-lecture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';
import { takenLectureAtom } from '@/app/store/custom-taken-lecture';
import { TakenLectrueInfo } from '@/app/type/lecture';
import { useEffect } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { updateDialogAtom } from '@/app/store/dialog';

export default function UpdateTakenLecture({ data, children }: React.PropsWithChildren<{ data: TakenLectrueInfo[] }>) {
const isLectureSearchOpen = useAtomValue(updateDialogAtom);
const setTakenLectures = useSetAtom(takenLectureAtom);

useEffect(() => {
setTakenLectures(data);
}, [isLectureSearchOpen]);

return children;
}
1 change: 1 addition & 0 deletions app/ui/view/atom/button/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
return (
<button
className={cn(isDisabled && 'opacity-50 cursor-not-allowed', ButtonVariants({ variant, size }))}
disabled={disabled}
{...props}
ref={ref}
>
Expand Down
Loading

0 comments on commit 8218843

Please sign in to comment.