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

feat(time): TimeTable Modal 추가기능 #215

Merged
merged 11 commits into from
Aug 20, 2024
Binary file added apps/time/public/font/PretendardVariable.woff2
Binary file not shown.
16 changes: 16 additions & 0 deletions apps/time/src/app/font.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Copyright (c) 2021 Kil Hyung-jin, with Reserved Font Name Pretendard.
https://github.com/orioncactus/pretendard

This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
*/

@font-face {
font-family: 'Pretendard Variable';
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url('/font/PretendardVariable.woff2') format('woff2-variations');
}
8 changes: 2 additions & 6 deletions apps/time/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ import { cn } from '@clab-platforms/utils';

import { Providers } from '@/shared/utils';
import type { Metadata } from 'next';
import { Noto_Sans_KR } from 'next/font/google';

import './font.css';
import './globals.css';

const inter = Noto_Sans_KR({ subsets: ['latin'] });

export const metadata: Metadata = {
title: '경기플러스',
description: '경기대학교에 계신 모든 순간을 도와드릴게요.',
Expand All @@ -23,9 +21,7 @@ export default function RootLayout({
}>) {
return (
<html lang="ko">
<body
className={cn(inter.className, 'flex min-h-screen flex-col bg-gray-50')}
>
<body className={cn('flex min-h-screen flex-col bg-gray-50 font-sans')}>
<Providers>{children}</Providers>
</body>
</html>
Expand Down
1 change: 1 addition & 0 deletions apps/time/src/shared/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export { default as useOutsideClick } from './useOutsideClick';
export { default as useModalAction } from './useModalAction';
export { default as useModalState } from './useModalState';
export { default as useDebounce } from './useDebounce';
export { default as useInfiniteScroll } from './useInfiniteScroll';
39 changes: 39 additions & 0 deletions apps/time/src/shared/hooks/useInfiniteScroll.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client';

import { useCallback, useEffect, useRef } from 'react';

export default function useInfiniteScroll(callback: () => void) {
const targetRef = useRef<HTMLElement | null>(null);
const observerRef = useRef<IntersectionObserver | null>(null);

const handleIntersection = useCallback(
([target]: IntersectionObserverEntry[]) => {
if (target.isIntersecting) {
callback();
}
},
[callback],
);

useEffect(() => {
if (observerRef.current) {
observerRef.current.disconnect(); // 기존 옵저버 해제
}

observerRef.current = new IntersectionObserver(handleIntersection, {
root: null,
rootMargin: '0px',
threshold: 1.0,
});

if (targetRef.current) {
observerRef.current.observe(targetRef.current);
}

return () => {
observerRef.current?.disconnect();
};
}, [handleIntersection]);

return targetRef;
}
4 changes: 3 additions & 1 deletion apps/time/src/shared/utils/getAPIURL.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
'use client';

export default function getAPIURL(endPoint: string) {
return new URL(`${process.env.NEXT_PUBLIC_API_URL}/api/${endPoint}`);
return new URL(`${process.env.NEXT_PUBLIC_SERVER_URL}/api/${endPoint}`);
}
6 changes: 3 additions & 3 deletions apps/time/src/widgets/time-table/api/getLectureList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface GetLectureListParams {
time: (DayPeriod | NightPeriod)[];
major: string[];
lectureName: string;
cursor: number;
cursor?: number;
limit: number;
}

Expand All @@ -45,9 +45,9 @@ export interface GetLectureListResponse {
success: boolean;
data: {
values: GetLectureListResponseValue[];
hasPrevious: boolean;
hasNext: boolean;
};
hasPrevious: boolean;
hasNext: boolean;
}

export async function getLectureList({
Expand Down
13 changes: 10 additions & 3 deletions apps/time/src/widgets/time-table/model/hooks/useLectureList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useSuspenseQuery } from '@tanstack/react-query';
import { useInfiniteQuery } from '@tanstack/react-query';

import {
type GetLectureListParams,
Expand All @@ -7,8 +7,15 @@ import {
} from '@/widgets/time-table';

export default function useLectureList({ ...params }: GetLectureListParams) {
return useSuspenseQuery({
return useInfiniteQuery({
queryKey: timeTableQueryKeys.getLectureList(params),
queryFn: () => getLectureList(params),
queryFn: ({ pageParam }) =>
getLectureList({ ...params, cursor: pageParam }),
initialPageParam: 0,
select: (data) => (data.pages ?? []).flatMap((page) => page.data.values),
getNextPageParam: (lastPage) =>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
select: (data) => (data.pages ?? []).flatMap((page) => page.data.values),
select: ({ pages = [] }) => pages.flatMap((page) => page.data.values),

lastPage.data.hasNext
? lastPage.data.values[params.limit - 1].id
: undefined,
});
}
140 changes: 96 additions & 44 deletions apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
'use client';

import { Suspense, memo } from 'react';

import { MODAL_KEY } from '@/shared/constants';
import {
useEditableSearchParams,
useInfiniteScroll,
useModalAction,
} from '@/shared/hooks';
import {
type GetLectureListParams,
type GetLectureListResponseValue,
useLectureList,
} from '@/widgets/time-table';
import { useRouter } from 'next/navigation';

interface TimeTableLectureTableProps {
selectedValues: GetLectureListParams;
Expand All @@ -29,74 +38,117 @@ const LECTURE_TABLE_ROW_HEADER = [
] as const;

function TimeTableLectureItem({ lecture }: TimeTableLectureTableItemProps) {
const searchParams = useEditableSearchParams();
const router = useRouter();
const { close } = useModalAction({ key: MODAL_KEY.timeTable });

const handleTimeTableLectureItem = (id: number) => {
const selectedId = searchParams.getAll('id');

if (!selectedId.includes(String(id))) {
searchParams.append('id', id.toString());
router.push(`/timetable?${searchParams.toString()}`);
}

close();
};

return (
<tr className="divide-x divide-gray-300 text-center">
<td>{lecture.campus}</td>
<td>{lecture.category}</td>
<td>{lecture.code}</td>
<td>{lecture.credit}</td>
<td>{lecture.grade ?? '-'}</td>
<td>{lecture.major !== 'None' ? lecture.major : '-'}</td>
<td>{lecture.name}</td>
<td>{lecture.professor}</td>
<td>{lecture.semester}</td>
<td>{lecture.time}</td>
<td>{lecture.groupName}</td>
<tr
className="h-12 cursor-pointer divide-x divide-gray-300 text-[12px] transition-colors hover:bg-gray-100"
onClick={() => handleTimeTableLectureItem(lecture.id)}
>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.campus}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.category}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.code}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.credit}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.grade ?? '-'}</td>
<td className="shrink-0 whitespace-nowrap p-2">
{lecture.major !== 'None' ? lecture.major : '-'}
</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.name}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.professor}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.semester}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.time}</td>
<td className="shrink-0 whitespace-nowrap p-2">{lecture.groupName}</td>
</tr>
);
}

function TimeTableLectureContent({
selectedValues,
}: TimeTableLectureTableProps) {
const { data } = useLectureList({
const { data, hasNextPage, fetchNextPage } = useLectureList({
campus: selectedValues.campus,
type: selectedValues.type,
grade: selectedValues.grade,
day: selectedValues.day,
time: selectedValues.time,
major: selectedValues.major,
lectureName: selectedValues.lectureName,
cursor: 0,
limit: 10,
limit: 12,
});
const scrollRef = useInfiniteScroll(() => {
if (hasNextPage) {
fetchNextPage();
}
});
const lectureList = data.data.values;

return (
<>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
<>

{lectureList.length ? (
<>
{...lectureList.map((lecture) => (
<TimeTableLectureItem
key={`lecture-${lecture.id}`}
lecture={lecture}
<tbody className="size-full divide-y divide-gray-300">
{data && (
<>
{data.length ? (
<>
{...data.map((lecture) => (
<TimeTableLectureItem
key={`lecture-${lecture.id}`}
lecture={lecture}
/>
))}
</>
) : (
<tr className="h-full">
<td
colSpan={LECTURE_TABLE_ROW_HEADER.length}
className="h-full text-center"
>
검색 결과가 없습니다
</td>
</tr>
)}
</>
)}
{hasNextPage && (
<tr className="block">
<td
ref={(node) => {
scrollRef.current = node;
}}
colSpan={LECTURE_TABLE_ROW_HEADER.length}
className="h-1"
/>
))}
</>
) : (
<tr>
<td colSpan={LECTURE_TABLE_ROW_HEADER.length} className="text-center">
검색 결과가 없습니다
</td>
</tr>
)}
</tr>
)}
</tbody>
</>
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
</>

);
}

function TimeTableLectureTable({ selectedValues }: TimeTableLectureTableProps) {
return (
<table className="mt-3 w-full table-auto border-collapse border border-gray-400 text-sm">
<thead className="w-full border border-gray-400 text-center">
<tr className="divide-x divide-gray-400 border border-gray-400 bg-gray-100">
{LECTURE_TABLE_ROW_HEADER.map((header) => (
<td className="py-2" key={header}>
{header}
</td>
))}
</tr>
</thead>
<tbody className="h-80 w-full divide-y divide-gray-300 overflow-y-scroll">
<div className="mt-3 h-96 w-full overflow-y-scroll">
<table className="size-full table-auto break-keep border border-gray-400 text-sm">
<thead className="sticky top-0 z-20 w-full border border-gray-400 text-center">
<tr className="divide-x divide-gray-400 bg-gray-100">
{LECTURE_TABLE_ROW_HEADER.map((header) => (
<th className="border border-gray-400 py-2" key={header}>
{header}
</th>
))}
</tr>
</thead>
<Suspense
fallback={
<tr>
Expand All @@ -111,8 +163,8 @@ function TimeTableLectureTable({ selectedValues }: TimeTableLectureTableProps) {
>
<TimeTableLectureContent selectedValues={selectedValues} />
</Suspense>
</tbody>
</table>
</table>
</div>
);
}

Expand Down
7 changes: 5 additions & 2 deletions apps/time/src/widgets/time-table/ui/TimeTableModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ const TimeTableModalMajorInput = memo(function TimeTableModalMajorInput({
selectedMajor,
handleMajorInputChange,
}: TimeTableModalMajorInputProps) {
const [inputValue, setInputValue] = useState('');
const [inputValue, setInputValue] = useState<string>('');
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const [inputValue, setInputValue] = useState<string>('');
const [inputValue, setInputValue] = useState('');

const [open, setOpen] = useState<boolean>(false);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
const [open, setOpen] = useState<boolean>(false);
const [open, setOpen] = useState(false);

const debouncedValue = useDebounce({
value: inputValue,
Expand All @@ -247,7 +247,10 @@ const TimeTableModalMajorInput = memo(function TimeTableModalMajorInput({
{...selectedMajor.map((major) => (
<TimeTableModalDropdownButton
key={major}
onClick={() => handleMajorInputChange(major)}
onClick={() => {
handleMajorInputChange(major);
setInputValue('');
}}
value={major}
/>
))}
Expand Down
4 changes: 4 additions & 0 deletions apps/time/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Config } from 'tailwindcss';
import defaultTheme from 'tailwindcss/defaultTheme';

const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
Expand All @@ -8,6 +9,9 @@ const config: Config = {
center: true,
padding: '2rem',
},
fontFamily: {
sans: ['"Pretendard"', ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [],
Expand Down
Loading