Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement lecture search/#10 #46

Merged
merged 21 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Comment on lines 18 to 19
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[comment]

  • ์ด๋ ‡๊ฒŒ 2์ค„๋กœ ๋‚˜๋ˆ ์„œ ์ž‘์„ฑํ•œ ์ด์œ ๊ฐ€ ์žˆ์„๊นŒ์š”?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

์ €๋Š” ํ•œ ์ค„์— ์ž‘์„ฑํ•œ ๊ฒƒ๋ณด๋‹ค ๋‘ ์ค„์— ๋‚˜๋ˆ ์„œ ์ž‘์„ฑํ•˜๋Š” ๊ฒŒ ์ž‘์—…์˜ ํ๋ฆ„์ด ๋” ์ž˜ ์ฝํžŒ๋‹ค๊ณ  ์ƒ๊ฐํ•ด์„œ ๊ทธ๋ ‡๊ฒŒ ์ž‘์—…ํ–ˆ์Šต๋‹ˆ๋‹ค!


//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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[index: string]: string | number; ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉ๋˜๋‚˜์š”?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object.keys ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ์‚ฌ์šฉํ•˜๋Š” key๋Š” string ํƒ€์ž…์ธ๋ฐ, ์ด string ํƒ€์ž…์„ string literal type์— ํ• ๋‹นํ•˜๋ ค ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์—ˆ๊ณ  ์œ„ ๊ตฌ๋ฌธ์„ ํ†ตํ•ด ํ•ด๊ฒฐํ–ˆ์Šต๋‹ˆ๋‹ค

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>
);
}
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
Loading