diff --git a/.changeset/six-spies-deny.md b/.changeset/six-spies-deny.md
new file mode 100644
index 00000000..0930b878
--- /dev/null
+++ b/.changeset/six-spies-deny.md
@@ -0,0 +1,5 @@
+---
+"@clab-platforms/time": patch
+---
+
+feat(time): TimeTable Modal 추가기능
diff --git a/apps/time/public/font/PretendardVariable.woff2 b/apps/time/public/font/PretendardVariable.woff2
new file mode 100644
index 00000000..49c54b51
Binary files /dev/null and b/apps/time/public/font/PretendardVariable.woff2 differ
diff --git a/apps/time/src/app/font.css b/apps/time/src/app/font.css
new file mode 100644
index 00000000..9f68bab6
--- /dev/null
+++ b/apps/time/src/app/font.css
@@ -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');
+}
diff --git a/apps/time/src/app/layout.tsx b/apps/time/src/app/layout.tsx
index 18484d1d..7fe26607 100644
--- a/apps/time/src/app/layout.tsx
+++ b/apps/time/src/app/layout.tsx
@@ -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: '경기대학교에 계신 모든 순간을 도와드릴게요.',
@@ -23,9 +21,7 @@ export default function RootLayout({
}>) {
return (
-
+
{children}
diff --git a/apps/time/src/shared/hooks/index.ts b/apps/time/src/shared/hooks/index.ts
index be92bc94..7c772e57 100644
--- a/apps/time/src/shared/hooks/index.ts
+++ b/apps/time/src/shared/hooks/index.ts
@@ -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';
diff --git a/apps/time/src/shared/hooks/useInfiniteScroll.ts b/apps/time/src/shared/hooks/useInfiniteScroll.ts
new file mode 100644
index 00000000..49aa0ed1
--- /dev/null
+++ b/apps/time/src/shared/hooks/useInfiniteScroll.ts
@@ -0,0 +1,39 @@
+'use client';
+
+import { useCallback, useEffect, useRef } from 'react';
+
+export default function useInfiniteScroll(callback: () => void) {
+ const targetRef = useRef(null);
+ const observerRef = useRef(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;
+}
diff --git a/apps/time/src/shared/utils/getAPIURL.ts b/apps/time/src/shared/utils/getAPIURL.ts
index cfb490b0..ae10bd84 100644
--- a/apps/time/src/shared/utils/getAPIURL.ts
+++ b/apps/time/src/shared/utils/getAPIURL.ts
@@ -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}`);
}
diff --git a/apps/time/src/widgets/time-table/api/getLectureList.ts b/apps/time/src/widgets/time-table/api/getLectureList.ts
index 0e7373a5..aca36aa1 100644
--- a/apps/time/src/widgets/time-table/api/getLectureList.ts
+++ b/apps/time/src/widgets/time-table/api/getLectureList.ts
@@ -18,7 +18,7 @@ export interface GetLectureListParams {
time: (DayPeriod | NightPeriod)[];
major: string[];
lectureName: string;
- cursor: number;
+ cursor?: number;
limit: number;
}
@@ -45,9 +45,9 @@ export interface GetLectureListResponse {
success: boolean;
data: {
values: GetLectureListResponseValue[];
+ hasPrevious: boolean;
+ hasNext: boolean;
};
- hasPrevious: boolean;
- hasNext: boolean;
}
export async function getLectureList({
diff --git a/apps/time/src/widgets/time-table/model/hooks/useLectureList.ts b/apps/time/src/widgets/time-table/model/hooks/useLectureList.ts
index a840f2ff..8527bdc2 100644
--- a/apps/time/src/widgets/time-table/model/hooks/useLectureList.ts
+++ b/apps/time/src/widgets/time-table/model/hooks/useLectureList.ts
@@ -1,4 +1,4 @@
-import { useSuspenseQuery } from '@tanstack/react-query';
+import { useInfiniteQuery } from '@tanstack/react-query';
import {
type GetLectureListParams,
@@ -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: ({ pages = [] }) => pages.flatMap((page) => page.data.values),
+ getNextPageParam: (lastPage) =>
+ lastPage.data.hasNext
+ ? lastPage.data.values[params.limit - 1].id
+ : undefined,
});
}
diff --git a/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx b/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx
index 5b9e7ff5..07ce67b1 100644
--- a/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx
+++ b/apps/time/src/widgets/time-table/ui/TimeTableLectureTable.tsx
@@ -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;
@@ -29,19 +38,39 @@ 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 (
-
- {lecture.campus} |
- {lecture.category} |
- {lecture.code} |
- {lecture.credit} |
- {lecture.grade ?? '-'} |
- {lecture.major !== 'None' ? lecture.major : '-'} |
- {lecture.name} |
- {lecture.professor} |
- {lecture.semester} |
- {lecture.time} |
- {lecture.groupName} |
+
handleTimeTableLectureItem(lecture.id)}
+ >
+ {lecture.campus} |
+ {lecture.category} |
+ {lecture.code} |
+ {lecture.credit} |
+ {lecture.grade ?? '-'} |
+
+ {lecture.major !== 'None' ? lecture.major : '-'}
+ |
+ {lecture.name} |
+ {lecture.professor} |
+ {lecture.semester} |
+ {lecture.time} |
+ {lecture.groupName} |
);
}
@@ -49,7 +78,7 @@ function TimeTableLectureItem({ lecture }: TimeTableLectureTableItemProps) {
function TimeTableLectureContent({
selectedValues,
}: TimeTableLectureTableProps) {
- const { data } = useLectureList({
+ const { data, hasNextPage, fetchNextPage } = useLectureList({
campus: selectedValues.campus,
type: selectedValues.type,
grade: selectedValues.grade,
@@ -57,46 +86,67 @@ function TimeTableLectureContent({
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 (
- <>
- {lectureList.length ? (
+
+ {data && (
<>
- {...lectureList.map((lecture) => (
-
- ))}
+ {data.length ? (
+ <>
+ {...data.map((lecture) => (
+
+ ))}
+ >
+ ) : (
+
+
+ 검색 결과가 없습니다
+ |
+
+ )}
>
- ) : (
-
-
- 검색 결과가 없습니다
- |
+ )}
+ {hasNextPage && (
+
+ {
+ scrollRef.current = node;
+ }}
+ colSpan={LECTURE_TABLE_ROW_HEADER.length}
+ className="h-1"
+ />
|
)}
- >
+
);
}
function TimeTableLectureTable({ selectedValues }: TimeTableLectureTableProps) {
return (
-
-
-
- {LECTURE_TABLE_ROW_HEADER.map((header) => (
-
- {header}
- |
- ))}
-
-
-
+
+
+
+
+ {LECTURE_TABLE_ROW_HEADER.map((header) => (
+
+ {header}
+ |
+ ))}
+
+
@@ -111,8 +161,8 @@ function TimeTableLectureTable({ selectedValues }: TimeTableLectureTableProps) {
>
-
-
+
+
);
}
diff --git a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx
index d2f8495d..d238a04b 100644
--- a/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx
+++ b/apps/time/src/widgets/time-table/ui/TimeTableModal.tsx
@@ -229,7 +229,7 @@ const TimeTableModalMajorInput = memo(function TimeTableModalMajorInput({
selectedMajor,
handleMajorInputChange,
}: TimeTableModalMajorInputProps) {
- const [inputValue, setInputValue] = useState('');
+ const [inputValue, setInputValue] = useState('');
const [open, setOpen] = useState(false);
const debouncedValue = useDebounce({
value: inputValue,
@@ -247,7 +247,10 @@ const TimeTableModalMajorInput = memo(function TimeTableModalMajorInput({
{...selectedMajor.map((major) => (
handleMajorInputChange(major)}
+ onClick={() => {
+ handleMajorInputChange(major);
+ setInputValue('');
+ }}
value={major}
/>
))}
diff --git a/apps/time/tailwind.config.ts b/apps/time/tailwind.config.ts
index 539b5211..3d6cfbd2 100644
--- a/apps/time/tailwind.config.ts
+++ b/apps/time/tailwind.config.ts
@@ -1,4 +1,5 @@
import type { Config } from 'tailwindcss';
+import defaultTheme from 'tailwindcss/defaultTheme';
const config: Config = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
@@ -8,6 +9,9 @@ const config: Config = {
center: true,
padding: '2rem',
},
+ fontFamily: {
+ sans: ['"Pretendard"', ...defaultTheme.fontFamily.sans],
+ },
},
},
plugins: [],