Skip to content

Commit

Permalink
Merge pull request #46 from Myongji-Graduate/implement-lecture-search/#…
Browse files Browse the repository at this point in the history
…10

Implement lecture search/#10
  • Loading branch information
gahyuun authored Mar 14, 2024
2 parents f8e3283 + ccf604c commit 8fbd295
Show file tree
Hide file tree
Showing 18 changed files with 250 additions and 53 deletions.
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();
});
});
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} />;
}
9 changes: 8 additions & 1 deletion app/ui/lecture/taken-lecture/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { fetchTakenLectures } from '@/app/business/lecture/taken-lecture.query';
import TakenLectureList from './taken-lecture-list';
import TakenLectureLabel from './taken-lecture-label';

export default async function TakenLecture() {
const data = await fetchTakenLectures();
return <TakenLectureList data={data.takenLectures} />;
return (
<div className="flex flex-col gap-2">
{/* w-[800px]은 w-full로 변경 예정 */}
<TakenLectureLabel data={data.takenLectures} />
<TakenLectureList data={data.takenLectures} />
</div>
);
}
24 changes: 19 additions & 5 deletions app/ui/lecture/taken-lecture/taken-lecture-label.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
'use client';
import Link from 'next/link';
import Button from '../../view/atom/button/button';
import LabelContainer from '../../view/atom/label-container/label-container';
import { useAtom, useSetAtom } from 'jotai';
import { customLectureAtom, isCustomizingAtom } from '@/app/store/custom-taken-lecture';
import { LectureInfo } from '@/app/type/lecture';

interface TakenLectureLabelProps {
isCustomizing: boolean;
changeCustomizingState: VoidFunction;
data: LectureInfo[];
}
export default function TakenLectureLabel({ data }: TakenLectureLabelProps) {
const [isCustomizing, setIsCustomizing] = useAtom(isCustomizingAtom);
const setCustomLecture = useSetAtom(customLectureAtom);

const startCustomizing = () => {
setIsCustomizing(true);
};

const cancelCustomizing = () => {
setIsCustomizing(false);
setCustomLecture(data);
};

export default function TakenLectureLabel({ isCustomizing, changeCustomizingState }: TakenLectureLabelProps) {
return (
<LabelContainer
label="내 기이수 과목"
Expand All @@ -16,15 +30,15 @@ export default function TakenLectureLabel({ isCustomizing, changeCustomizingStat
{isCustomizing ? (
<>
<Button label="저장하기" variant="primary" size="md" />
<Button label="취소하기" variant="secondary" size="md" onClick={changeCustomizingState} />
<Button label="취소하기" variant="secondary" size="md" onClick={cancelCustomizing} />
</>
) : (
<>
<Button
label="커스텀하기"
variant="secondary"
size="md"
onClick={changeCustomizingState}
onClick={startCustomizing}
data-testid="custom-button"
/>
<Link href="/file-upload">
Expand Down
43 changes: 13 additions & 30 deletions app/ui/lecture/taken-lecture/taken-lecture-list.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/* eslint-disable react-hooks/exhaustive-deps */
'use client';
import TakenLectureLabel from './taken-lecture-label';
import { Table } from '../../view/molecule/table';
import { useEffect, useState } from 'react';
import Button from '../../view/atom/button/button';
import { useEffect } from 'react';
import { LectureInfo } from '@/app/type/lecture';
import { useAtom } from 'jotai';
import { customLectureAtom, isCustomizingAtom } from '@/app/store/custom-taken-lecture';
import DeleteTakenLectureButton from './delete-taken-lecture-button';

const headerInfo = ['수강년도', '수강학기', '과목코드', '과목명', '학점'];

Expand All @@ -13,45 +14,27 @@ interface TakenLectureListProps {
}

export default function TakenLectureList({ data }: TakenLectureListProps) {
const [isCustomizing, setIsCustomizing] = useState<boolean>(false);
const [customLecture, setCustomLecture] = useState<LectureInfo[]>(data);

const deleteLecture = (id: number) => {
setCustomLecture(customLecture.filter((lecture) => lecture.id !== id));
};

const changeCustomizingState = () => {
setIsCustomizing(!isCustomizing);
};
const [isCustomizing, setIsCustomizing] = useAtom(isCustomizingAtom);
const [customLecture, setCustomLecture] = useAtom(customLectureAtom);

useEffect(() => {
if (!isCustomizing) {
return () => {
setCustomLecture(data);
}
}, [isCustomizing]);
setIsCustomizing(false);
};
}, []);

return (
<div className="w-[800px] flex flex-col gap-2">
{/* w-[800px]은 w-full로 변경 예정 */}
<TakenLectureLabel isCustomizing={isCustomizing} changeCustomizingState={changeCustomizingState} />
<>
{isCustomizing ? (
<Table
headerInfo={headerInfo}
data={customLecture}
renderActionButton={(id: number) => (
<Button
label="삭제"
variant="list"
data-testid="taken-lecture-delete-button"
onClick={() => {
deleteLecture(id);
}}
/>
)}
renderActionButton={(id: number) => <DeleteTakenLectureButton lectureId={id} />}
/>
) : (
<Table headerInfo={headerInfo} data={data} />
)}
</div>
</>
);
}
15 changes: 10 additions & 5 deletions app/ui/view/molecule/list/list-root.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { cn } from '@/app/utils/shadcn/utils';
import { ReactNode } from 'react';

export interface ListRow {
id: number;
[key: string]: string | number;
}
interface ListRootProps {
data: ListRow[];
render: (item: ListRow, index: number) => ReactNode;
interface ListRootProps<T extends ListRow> {
data: T[];
render: (item: T, index: number) => ReactNode;
isScrollList?: boolean;
emptyDataRender?: () => ReactNode;
}

export function ListRoot({ data, render }: ListRootProps) {
export function ListRoot<T extends ListRow>({ data, render, isScrollList = false, emptyDataRender }: ListRootProps<T>) {
const hasNotData = emptyDataRender && data.length === 0;
return (
<div className="rounded-2xl border-[1px] border-black-2 w-full">
<div className={cn('rounded-xl border-[1px] border-gray-300 w-full ', isScrollList && 'h-72 overflow-auto')}>
{data.map((item, index) => render(item, index))}
{hasNotData ? emptyDataRender() : null}
</div>
);
}
4 changes: 2 additions & 2 deletions app/ui/view/molecule/list/list-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export function ListRow({ children, textColor = 'black' }: ListRowProps) {
return (
<div
className={twMerge(
'border-solid border-gray-300 border-b-[1px] last:border-b-0 py-4 font-medium text-lg',
textColor === 'red' ? 'text-red-500' : 'text-black-2',
'border-solid border-gray-300 border-b-[1px] last:border-b-0 py-4 font-medium text-lg',
textColor === 'red' ? 'text-red-500' : 'text-zinc-700',
)}
>
{children}
Expand Down
4 changes: 2 additions & 2 deletions app/ui/view/molecule/select/select-root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const SelectRoot = React.forwardRef<HTMLInputElement, SelectProps>(functi
}, [selectedValue, children]);

return (
<div className="w-full min-w-[10rem] relative text-base">
<div className="w-full relative text-base">
<select
required={required}
title="select-hidden"
Expand Down Expand Up @@ -95,7 +95,7 @@ export const SelectRoot = React.forwardRef<HTMLInputElement, SelectProps>(functi
<Listbox.Button
ref={listboxButtonRef}
className={twMerge(
'w-full min-w-[10rem] outline-none text-left whitespace-nowrap truncate rounded-xl focus:ring-2 transition duration-100 border pr-8 py-2',
'w-full outline-none text-left whitespace-nowrap truncate rounded-xl focus:ring-2 transition duration-100 border pr-8 py-2',
'border-gray-800 shadow-sm focus:border-blue-400 focus:ring-blue-200 text-gray-700',
Icon ? 'pl-10' : 'pl-3',
getInputColors(disabled, error),
Expand Down
Loading

0 comments on commit 8fbd295

Please sign in to comment.