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): 타임테이블 모달 api 연동 #211

Merged
merged 18 commits into from
Aug 18, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3b3cb40
feat(time): add day status constant in region & add campus status con…
SWARVY Aug 17, 2024
59104f3
chore(time): install react-query & react-query-devtools (#97)
SWARVY Aug 17, 2024
bccc52f
chore(time): service rename '경기타임' -> '경기플러스' (#97)
SWARVY Aug 17, 2024
7ef2e26
feat(time): create util hook 'useDebounce' (#97)
SWARVY Aug 17, 2024
b216950
feat(time): add fetch function getLectureList / getMajorList & add en…
SWARVY Aug 17, 2024
a7f3a80
feat(time): add custom-hook useLectureList / useMajorList & add query…
SWARVY Aug 17, 2024
ed7bf6f
chore(time): add missing index.ts files (#97)
SWARVY Aug 17, 2024
3743ce0
chore(time): fix typo 'GetLectureListReponseValue' -> 'GetLectureList…
SWARVY Aug 18, 2024
c6141e5
feat(time): add TimeTable components / split LectureTable & integrate…
SWARVY Aug 18, 2024
0fc8218
chore(time): fix incorrect type definition 'GetLectureListResponseVal…
SWARVY Aug 18, 2024
e5a44ac
Merge branch 'main' of https://github.com/KGU-C-Lab/clab-platforms in…
SWARVY Aug 18, 2024
5474564
Create grumpy-beers-travel.md
gwansikk Aug 18, 2024
51b6f3e
refactor(time): add generic in `useDebounce` for correct type inference
SWARVY Aug 18, 2024
944877d
refactor(time): add `getAPIURL` util function to reduce redundant logic
SWARVY Aug 18, 2024
3e670eb
chore(time): rename input change function '-Input' -> 'InputChange'
SWARVY Aug 18, 2024
8c3fd38
Merge branch 'feature/#97' of https://github.com/KGU-C-Lab/clab-platf…
SWARVY Aug 18, 2024
23ea19d
refactor(time): apply missing generic type in `useDebounce`
SWARVY Aug 18, 2024
290fb41
refactor(time): rename endpoint object attributes to upper snake case
SWARVY Aug 18, 2024
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
2 changes: 2 additions & 0 deletions apps/time/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"@clab-platforms/design-system": "workspace:*",
"@clab-platforms/icon": "workspace:^",
"@clab-platforms/utils": "workspace:*",
"@tanstack/react-query": "^5.51.23",
"@tanstack/react-query-devtools": "^5.40.1",
"jotai": "^2.9.2",
"next": "14.1.4",
"react": "^18",
Expand Down
2 changes: 1 addition & 1 deletion apps/time/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import './globals.css';
const inter = Noto_Sans_KR({ subsets: ['latin'] });

export const metadata: Metadata = {
title: '경기타임',
title: '경기플러스',
description: '경기대학교에 계신 모든 순간을 도와드릴게요.',
icons: {
icon: '/favicon.ico',
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 @@ -2,3 +2,4 @@ export { default as useEditableSearchParams } from './useEditableSearchParams';
export { default as useOutsideClick } from './useOutsideClick';
export { default as useModalAction } from './useModalAction';
export { default as useModalState } from './useModalState';
export { default as useDebounce } from './useDebounce';
19 changes: 19 additions & 0 deletions apps/time/src/shared/hooks/useDebounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect, useState } from 'react';

interface UseDebounceParams {
value: unknown;
delay: number;
}

export default function useDebounce({ value, delay }: UseDebounceParams) {
const [debouncedValue, setDebouncedValue] = useState<unknown>(value);
Copy link
Member

@gwansikk gwansikk Aug 18, 2024

Choose a reason for hiding this comment

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

Suggested change
interface UseDebounceParams {
value: unknown;
delay: number;
}
export default function useDebounce({ value, delay }: UseDebounceParams) {
const [debouncedValue, setDebouncedValue] = useState<unknown>(value);
interface UseDebounceParams<TValue> {
value: TValue;
delay: number;
}
export default function useDebounce<TValue>({ value, delay }: UseDebounceParams<TValue>) {
const [debouncedValue, setDebouncedValue] = useState<TValue>(value);

기존 코드의 리턴 값과 value의 타입은 unkown이에요, 제너릭을 활용하여 적절한 타입이 추론되도록 유도해보는 건 어떨까요?? (타입스크립트의 타입성이 보장되지 않기 때문)

Copy link
Contributor Author

@SWARVY SWARVY Aug 18, 2024

Choose a reason for hiding this comment

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

좋은 생각이네요! 제네릭을 적용하여 재설계해볼게요


useEffect(() => {
const delayDebounceTimer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(delayDebounceTimer);
}, [value, delay]);

return debouncedValue;
}
12 changes: 11 additions & 1 deletion apps/time/src/shared/utils/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

import { PropsWithChildren } from 'react';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

import { Provider } from 'jotai';

export default function Providers({ children }: PropsWithChildren) {
return <Provider>{children}</Provider>;
const queryClient = new QueryClient();

return (
<QueryClientProvider client={queryClient}>
<Provider>{children}</Provider>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
10 changes: 5 additions & 5 deletions apps/time/src/widgets/menu/ui/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const actions = [
</button>,
] as const;

const Nav = () => {
export default function Nav() {
return (
<nav className="fixed top-0 w-full border-b bg-white">
<div className="container flex h-14 items-center justify-between text-nowrap">
Expand All @@ -45,7 +45,9 @@ const Nav = () => {
height={200}
className="size-8"
/>
<h1 className="text-xl font-semibold">경기타임</h1>
<Link href="/">
<h1 className="select-none text-xl font-semibold">경기플러스</h1>
</Link>
</div>
<ul className="hidden w-3/5 items-center justify-center gap-4 text-sm sm:flex">
{links.map((link) => link)}
Expand All @@ -56,6 +58,4 @@ const Nav = () => {
</div>
</nav>
);
};

export default Nav;
}
6 changes: 6 additions & 0 deletions apps/time/src/widgets/time-table/api/endpoint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const TIMETABLE_ENDPOINT = {
getLectureList: 'api/v1/lecture/retrieve',
getMajorList: 'api/v1/lecture/retrieve/major',
Copy link
Contributor

Choose a reason for hiding this comment

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

get 요청 말고 다른 경우에도 동일한 엔드포인트를 사용해야 할 수도 있기 때문에 접두사를 빼고 LecturList로만 작성해도 괜찮을 것 같아요.

환경변수 설정 시에 /api가 있는 API_BASE_URL을 별도로 생성 후 사용하는 건 어떨까요? 엔드 포인트마다 api를 붙이지 않아도 되므로 편리할 것 같아서 제안드려요.

또, 엔드포인트 값은 변경되지 않을 상수이므로 TIMETABLE_ENDPOINT처럼 전체 대문자로 아름을 설정해도 좋을 것 같아요!
ex)LECTURE_LIST

Copy link
Contributor Author

@SWARVY SWARVY Aug 18, 2024

Choose a reason for hiding this comment

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

저는 개인적으로 앞에 END_POINT로 앞에 대문자 스네이크 케이스를 사용했다면, 객체 내부의 key에는 따로 대문자 스네이크 케이스를 사용하지 않아요.

아래 stackoverflow에 따르면 객체 속성 내부의 값은 변할 수 있기 때문에 lowercase로 사용될 수 있다. 라고 하는데요, 저는 getLectureList, getMajorList 등의 api 엔드포인트 또한 재설계를 통해 언제든지 바뀔 수 있는 부분이라고 생각하기때문에 상수 선언문인 END_POINT는 대문자 스네이크 케이스로 두되, 내부의 속성명은 소문자로 해도 무방하다고 생각해요. 또 해당 요소가 상수인지는 END_POINT라는 대문자 스네이크 케이스를 통해 충분히 유추 가능하구요

혹시 @gwansikk @Jeong-Ag 는 어떻게 생각하시는지 궁금하네요

Copy link
Contributor

Choose a reason for hiding this comment

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

저는 코드 내에 작성 됐을 때 모두 대문자 스네이크 케이스인게 좀 더 직관적으로 선언문 내부의 속성이 상수라는 사실을 알 수 있다고 생각해서 모두 대문자 스네이크 케이스로 사용하고 있어요. 처음 코드를 보는 사람이 봤을 때도 바로 상수라는 사실을 알아차릴 수 있을 것이라고 생각해요. 이번에 의견을 나눠보고 한 방법으로 통일시키면 어떨까요?

Copy link
Member

@gwansikk gwansikk Aug 18, 2024

Choose a reason for hiding this comment

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

아래 stackoverflow에 따르면 객체 속성 내부의 값은 변할 수 있기 때문에 lowercase로 사용될 수 있다.

말씀하신대로 객체 내부 값은 변경될 수 있어요, 하지만 이와 같이 Endpoint를 나타내는 상수 값은 as const를 통해서 readonly가 되어야 한다고 생각해요 (객체 내부 값또한 상수 처리), 상수 내부 값은 당연히 상수여야하고 값이 변경되면 안돼요. 이 때문에 상수 객체의 key 또한 대문자 스네이크 케이스를 사용하고 있어요. (내부 값 또한 상수 이기 때문)

상수 선언문인 END_POINT는 대문자 스네이크 케이스로 두되, 내부의 속성명은 소문자로 해도 무방하다고 생각해요. 또 해당 요소가 상수인지는 END_POINT라는 대문자 스네이크 케이스를 통해 충분히 유추 가능하구요

TIMETABLE_ENDPOINT 객체의 요소(값)들은 과연 상수일까요? 내부 값도 as const or readonly가 명시되지 않을 경우 상수가 아니에요(타입스크립트 컴파일타임 한정), 값이 재할당이 될 수 있고 변경될 수 있지요 여기서 상수 처리는 TIMETABLE_ENDPOINT의 객체예요. 따라서 요소는 END_POINT라는 대문자 스네이크 케이스를 통해 충분히 유추 가능하지만, 실제로는 상수가 아니므로 as const(readonly)처리를 해야한다고 생각해요. 이로 인해 명확하게 해야할 필요가 있으며, 내부 속성 또한 대문자 스테이크 케이스가 명확하다고 생각해요. 타입스크립트에서 컴파일타임에서도 상수에 대한 에러를 캐치할 수 있기도 합니다.

const NEW_TIMETABLE_ENDPOINT = {
  GET_LECTURELIST: 'api/v1/lecture/retrieve', // is readonly
  GET_MAJORLIST: 'api/v1/lecture/retrieve/major' // is readonly
} as const

};

export default TIMETABLE_ENDPOINT;
75 changes: 75 additions & 0 deletions apps/time/src/widgets/time-table/api/getLectureList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { DayKor } from '@/shared/types';
import type {
DayCampus,
DayPeriod,
Grade,
LectureKey,
NightCampus,
NightPeriod,
} from '@/widgets/time-table';
import { TIMETABLE_ENDPOINT } from '@/widgets/time-table';

export interface GetLectureListParams {
campus: (DayCampus | NightCampus)[];
type: LectureKey[];
grade: Grade[];
day: DayKor[];
time: (DayPeriod | NightPeriod)[];
major: string[];
lectureName: string;
cursor: number;
limit: number;
}

export interface GetLectureListResponseValue {
id: number;
campus: string;
category: string;
code: string;
credit: number;
grade: number;
groupName: string;
isExceeded: true;
major: string;
name: string;
professor: string;
room: string;
year: number;
semester: string;
time: string;
type: LectureKey;
}

export interface GetLectureListResponse {
success: boolean;
data: {
values: GetLectureListResponseValue[];
};
hasPrevious: boolean;
hasNext: boolean;
}

export async function getLectureList({
...params
}: GetLectureListParams): Promise<GetLectureListResponse> {
const apiURL = new URL(
`${process.env.NEXT_PUBLIC_SERVER_URL}/${TIMETABLE_ENDPOINT.getLectureList}`,
);
Jeong-Ag marked this conversation as resolved.
Show resolved Hide resolved

Object.entries(params).forEach(([key, value]) => {
if (value) {
apiURL.searchParams.set(
key,
Array.isArray(value) ? value.join(',') : String(value),
);
}
});

const res = await fetch(apiURL);

if (!res.ok) {
throw new Error('강의 정보 불러오기에 실패했습니다.');
}

return res.json();
}
28 changes: 28 additions & 0 deletions apps/time/src/widgets/time-table/api/getMajorList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { TIMETABLE_ENDPOINT } from '@/widgets/time-table';

export interface GetMajorListParams {
major: string;
}

export interface GetMajorListResponse {
success: boolean;
data: string[];
}

export async function getMajorList({
major,
}: GetMajorListParams): Promise<GetMajorListResponse> {
const apiURL = new URL(
`${process.env.NEXT_PUBLIC_SERVER_URL}/${TIMETABLE_ENDPOINT.getMajorList}`,
);

apiURL.searchParams.set('major', major);

const res = await fetch(apiURL);

if (!res.ok) {
throw new Error('전공 리스트 불러오기에 실패했습니다.');
}

return res.json();
}
3 changes: 3 additions & 0 deletions apps/time/src/widgets/time-table/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default as TIMETABLE_ENDPOINT } from './endpoint';
export * from './getLectureList';
export * from './getMajorList';
1 change: 1 addition & 0 deletions apps/time/src/widgets/time-table/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ui';
export * from './model';
export * from './types';
export * from './api';
24 changes: 5 additions & 19 deletions apps/time/src/widgets/time-table/model/constants/period.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const DAY_PERIOD = {
export const DAY_PERIOD = {
'1': {
start: { hour: 9, minute: 0 },
end: { hour: 9, minute: 50 },
Expand Down Expand Up @@ -91,7 +91,7 @@ const DAY_PERIOD = {
},
} as const;

const NIGHT_PERIOD = {
export const NIGHT_PERIOD = {
'1': {
start: { hour: 17, minute: 50 },
end: { hour: 18, minute: 35 },
Expand Down Expand Up @@ -124,7 +124,7 @@ const NIGHT_PERIOD = {
},
} as const;

const DAY_PERIOD_ARRAY = Object.entries(DAY_PERIOD).sort(
export const DAY_PERIOD_ARRAY = Object.entries(DAY_PERIOD).sort(
([, periodA], [, periodB]) => {
const timeA = periodA.start.hour * 60 + periodA.start.minute;
const timeB = periodB.start.hour * 60 + periodB.start.minute;
Expand All @@ -133,7 +133,7 @@ const DAY_PERIOD_ARRAY = Object.entries(DAY_PERIOD).sort(
},
);

const NIGHT_PERIOD_ARRAY = Object.entries(NIGHT_PERIOD).sort(
export const NIGHT_PERIOD_ARRAY = Object.entries(NIGHT_PERIOD).sort(
([, periodA], [, periodB]) => {
const timeA = periodA.start.hour * 60 + periodA.start.minute;
const timeB = periodB.start.hour * 60 + periodB.start.minute;
Expand All @@ -142,18 +142,4 @@ const NIGHT_PERIOD_ARRAY = Object.entries(NIGHT_PERIOD).sort(
},
);

const DAY_STATUS = {
day: '주간',
night: '야간',
} as const;

const SPECIAL_PERIOD = ['이러닝', '교외수업', '사회봉사'] as const;

export {
DAY_PERIOD,
NIGHT_PERIOD,
SPECIAL_PERIOD,
DAY_PERIOD_ARRAY,
NIGHT_PERIOD_ARRAY,
DAY_STATUS,
};
export const SPECIAL_PERIOD = ['이러닝', '교외수업', '사회봉사'] as const;
16 changes: 16 additions & 0 deletions apps/time/src/widgets/time-table/model/constants/region.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,20 @@ export const REGION = {
campus2: '서울',
} as const;

export const DAY_STATUS = {
day: '주간',
night: '야간',
} as const;

export const CAMPUS_STATUS = {
day: [
`${REGION.campus1}${DAY_STATUS.day}`,
`${REGION.campus2}${DAY_STATUS.day}`,
],
night: [
`${REGION.campus1}${DAY_STATUS.night}`,
`${REGION.campus2}${DAY_STATUS.night}`,
],
} as const;

export const REGION_VALUE_ARRAY = Object.values(REGION);
3 changes: 3 additions & 0 deletions apps/time/src/widgets/time-table/model/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export { default as timeTableQueryKeys } from './timeTableQueryKeys';
export { default as useTimeTableParams } from './useTimeTableParams';
export { default as useMajorList } from './useMajorList';
export { default as useLectureList } from './useLectureList';
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { GetLectureListParams, GetMajorListParams } from '@/widgets/time-table';

const timeTableQueryKeys = {
getLectureList: (params: GetLectureListParams) => ['getLectureList', params],
getMajorList: (params: GetMajorListParams) => ['getMajorList', params],
};

export default timeTableQueryKeys;
14 changes: 14 additions & 0 deletions apps/time/src/widgets/time-table/model/hooks/useLectureList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useSuspenseQuery } from '@tanstack/react-query';

import {
type GetLectureListParams,
getLectureList,
timeTableQueryKeys,
} from '@/widgets/time-table';

export default function useLectureList({ ...params }: GetLectureListParams) {
return useSuspenseQuery({
queryKey: timeTableQueryKeys.getLectureList(params),
queryFn: () => getLectureList(params),
});
}
17 changes: 17 additions & 0 deletions apps/time/src/widgets/time-table/model/hooks/useMajorList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query';

import {
type GetMajorListParams,
getMajorList,
timeTableQueryKeys,
} from '@/widgets/time-table';

export default function useMajorList({ major }: GetMajorListParams) {
const { data } = useQuery({
queryKey: timeTableQueryKeys.getMajorList({ major }),
queryFn: () => getMajorList({ major }),
enabled: major.length > 0,
});

return data ? data.data : [];
}
5 changes: 0 additions & 5 deletions apps/time/src/widgets/time-table/types/day-status.ts

This file was deleted.

1 change: 0 additions & 1 deletion apps/time/src/widgets/time-table/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './day-status';
export * from './grade';
export * from './period';
export * from './lecture';
Expand Down
8 changes: 7 additions & 1 deletion apps/time/src/widgets/time-table/types/region.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
import { REGION } from '@/widgets/time-table';
import { CAMPUS_STATUS, DAY_STATUS, REGION } from '@/widgets/time-table';

export type Region = (typeof REGION)[keyof typeof REGION];

export type DayStatus = keyof typeof DAY_STATUS;

export type DayCampus = (typeof CAMPUS_STATUS.day)[number];

export type NightCampus = (typeof CAMPUS_STATUS.night)[number];
Loading
Loading