Skip to content
26 changes: 26 additions & 0 deletions packages/client/src/entities/schedule/api/schedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useQuery } from '@tanstack/react-query';
import { fetchBasketOpenDate, fetchCurrentPeriod, fetchPreSeatOpenDate } from '../lib/manageSchedule';

export function useCurrentPeriod() {
return useQuery({
queryKey: ['currentPeriod'],
queryFn: fetchCurrentPeriod,
staleTime: Infinity,
});
}

export function usePreSeatOpenDate() {
return useQuery({
queryKey: ['preSeatOpenDate'],
queryFn: fetchPreSeatOpenDate,
staleTime: Infinity,
});
}

export function useBasketOpenDate() {
return useQuery({
queryKey: ['basketOpenDate'],
queryFn: fetchBasketOpenDate,
staleTime: Infinity,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ServiceMode, ServicePeriod } from './manageSchedule';

interface MainAction {
label: string;
link: string;
}

/**
* 메인 페이지에서 보여지는 액션 버튼의 모드를 반환하는 함수입니다.
* 각 수강 신청 기간에 따라 다른 액션 버튼이 메인 페이지에 표시됩니다.
* @param mode
* @returns
*/
export const getMainPageActionsMode = (mode: ServicePeriod['mode']): MainAction[] => {
switch (mode) {
case ServiceMode.WISHLIST:
return [
{ label: '관심과목 담기', link: '/wishlist' },
{ label: '수강 신청 연습하기', link: '/simulation' },
];

case ServiceMode.REGISTRATION_SENIOR:
case ServiceMode.REGISTRATION_ALL:
case ServiceMode.REGISTRATION_FRESHMAN:
case ServiceMode.CORRECTION:
return [
{ label: '실시간 여석 확인하기', link: '/live' },
{ label: '수강 신청 연습하기', link: '/simulation' },
];

default:
return [
{ label: '전체 학년 여석 확인하기', link: '/live' },
{ label: '수강 신청 연습하기', link: '/simulation' },
];
}
};
89 changes: 89 additions & 0 deletions packages/client/src/entities/schedule/lib/manageSchedule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
export enum ServiceMode {
WISHLIST = 'wishlist',
REGISTRATION_SENIOR = 'registration-senior',
REGISTRATION_ALL = 'registration-all',
REGISTRATION_FRESHMAN = 'registration-freshman',
CORRECTION = 'correction',
}

export interface ServicePeriod {
start: string;
end: string;
mode: ServiceMode;
description: string;
}

/**
* periods: 수강 신청과 관련된 각 기간의 시작과 끝, 그리고 해당 기간의 모드와 설명
* displayPeriod: 메인 배너의 수강 신청 기간
* CURRENT_PERIOD의 start, end 필드는 매 학기 일정이 나오면 업데이트 되어야합니다.
*/
export interface AcademicSchedule {
periods: ServicePeriod[];

displayPeriod: {
displayPeriodName?: string; // '수강 신청 기간' or '수강 정정 기간''
start: string; // '02월 10일(화)'
end: string; // '02월 13일(금)'
};
}

export const CURRENT_PERIOD: AcademicSchedule = {
periods: [
{
start: '2026-01-27T10:00:00',
end: '2026-02-09T16:59:59',
mode: ServiceMode.WISHLIST,
description: '관심과목 담기 기간',
},
{
start: '2026-02-10T10:00:00',
end: '2026-02-12T16:59:59',
mode: ServiceMode.REGISTRATION_SENIOR,
description: '4~1학년 수강신청 기간',
},
{
start: '2026-02-13T10:00:00',
end: '2026-02-26T16:59:59',
mode: ServiceMode.REGISTRATION_ALL,
description: '전체학년 수강신청 기간',
},
{
start: '2026-02-27T10:00:00',
end: '2026-02-27T16:59:59',
mode: ServiceMode.REGISTRATION_FRESHMAN,
description: '신입생 수강신청 기간',
},
{
start: '2026-03-04T10:00:00',
end: '2026-03-09T16:59:59',
mode: ServiceMode.CORRECTION,
description: '수강정정 기간',
},
],

displayPeriod: {
displayPeriodName: '수강정정 기간',
start: '03월 04일(수)',
end: '03월 09일(월)',
},
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

예전에 달력 업데이트 부분이 이 내용이었던 것 같아요.
이 상수 부분도, API 로 변경될 가능성이 있을 것 같아요. 직접 상수로 export 하기보다는 함수로 대신 export 해두면 좋을 것 같아요,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

이 파일이 APP 의 설정파일 역할을 하게 될 것 같습니다.
app 폴더로 이동해보는건 어떻게 생각하시나요?

추후 달력 기능이 업데이트 되면, 여기 두는게 좋을 수도 있을 것 같아요.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

예전에 달력 업데이트 부분이 이 내용이었던 것 같아요.
이 상수 부분도, API 로 변경될 가능성이 있을 것 같아요. 직접 상수로 export 하기보다는 함수로 대신 export 해두면 좋을 것 같아요,

넵! 저도 해당 방향성으로 데이터 처럼 관리하는게 좋을 거 같습니다. 좋은 의견 감사합니다!!

이 파일이 APP 의 설정파일 역할을 하게 될 것 같습니다.
app 폴더로 이동해보는건 어떻게 생각하시나요? 추후 달력 기능이 업데이트 되면, 여기 두는게 좋을 수도 있을 것 같아요.

앗 그러네요! 혹시.., 제가 이해한 방향성이 맞는지 모르겠어서,
특정 도메인과 연관 있다기 보다는 전체 서비스 관리와 연관성이 있어서 APP 설정 파일에 두는 것을 말씀하신 게 맞을까요?
맞다면 APP폴더에 두는 것도 좋을 것 같습니다!

다만, FSD 관점에서는 app의 파일을 하위 레이어 page, widget등에서 import하게 되는 방향성은 어색할 수 있을 것 같습니다.

//preSeat 데이터 업데이트 시 preSeatOpenDate를 변경해주어야합니다.
export const PRESEAT_OPEN_DATE = new Date('2026-03-09T00:00:00');

// basket 데이터 업데이트 시 basketOpenDate를 변경해주어야합니다.
export const BASKET_OPEN_DATE = new Date('2026-01-31T00:00:00');

// API 연결 확장성을 위해 fetchCurrentPeriod, fetchPreSeatOpenDate, fetchBasketOpenDate 함수를 만들어두었습니다.
export const fetchCurrentPeriod = async (): Promise<AcademicSchedule> => {
return CURRENT_PERIOD;
};

export const fetchPreSeatOpenDate = async (): Promise<Date> => {
return PRESEAT_OPEN_DATE;
};

export const fetchBasketOpenDate = async (): Promise<Date> => {
return BASKET_OPEN_DATE;
};
31 changes: 31 additions & 0 deletions packages/client/src/entities/schedule/model/useManagePeriod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useBasketOpenDate } from '../api/schedule';
import { getMainPageActionsMode } from '../lib/getMainPageActionsMode';
import { usePreSeatInfo } from './usePreSeatInfo';
import { useServiceMode } from './useServiceMode';

/**
*
* @returns
* period: 메인 배너 수강 신청 기간
* mainPageRouter: 메인 페이지 라우터 정보
* preSeat: 전체 여석 오픈, 클로즈 여부 및 시간
* basket: 관심 과목 오픈 날짜
*/
export function useManagePeriod() {
const { serviceMode, registrationDisplayPeriod } = useServiceMode();
const preSeat = usePreSeatInfo();
const { data: basketOpenDate } = useBasketOpenDate();

return {
period: {
registrationDisplayPeriod,
},
mainPageRouter: {
mainPageActionsMode: getMainPageActionsMode(serviceMode),
},
preSeat,
basket: {
basketOpenDate,
},
};
}
44 changes: 44 additions & 0 deletions packages/client/src/entities/schedule/model/usePreSeatInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useCurrentPeriod, usePreSeatOpenDate } from '../api/schedule';
import { ServiceMode, ServicePeriod } from '../lib/manageSchedule';

export function usePreSeatInfo() {
const now = new Date();
const { data: currentPeriodData } = useCurrentPeriod();
const { data: preSeatOpenDate } = usePreSeatOpenDate();

const periods = currentPeriodData?.periods ?? [];

const transitionPairs: [ServiceMode, ServiceMode][] = [
[ServiceMode.REGISTRATION_SENIOR, ServiceMode.REGISTRATION_ALL],
[ServiceMode.REGISTRATION_ALL, ServiceMode.REGISTRATION_FRESHMAN],
[ServiceMode.REGISTRATION_FRESHMAN, ServiceMode.CORRECTION],
];

let shouldPreparePreSeat = false;
let livePeriodStart: string | null = null;

for (const [beforeMode, afterMode] of transitionPairs) {
const beforePeriod = periods.find((p: ServicePeriod) => p.mode === beforeMode);
const afterPeriod = periods.find((p: ServicePeriod) => p.mode === afterMode);

if (!beforePeriod || !afterPeriod) continue;

if (
now > new Date(beforePeriod.end) &&
now < new Date(afterPeriod.start) &&
preSeatOpenDate &&
preSeatOpenDate <= now
) {
shouldPreparePreSeat = true;
livePeriodStart = afterPeriod.start;
break;
}
}

return {
shouldPreparePreSeat,
preSeatCloseDate: livePeriodStart?.split('T')[0] ?? '',
preSeatCloseTime: livePeriodStart?.split('T')[1]?.slice(0, 5) ?? '',
preSeatOpenDate,
};
}
26 changes: 26 additions & 0 deletions packages/client/src/entities/schedule/model/useServiceMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useCurrentPeriod } from '../api/schedule';
import { ServiceMode, ServicePeriod } from '../lib/manageSchedule';

/**
* 전체 학년 수강 신청 기간, 신입생 수강 신청 기간, 수강 정정 기간 등 각 기간에 따른 모드를 반환하는 커스텀 훅입니다.
* @returns
*/
export const useServiceMode = () => {
const now = new Date();
const { data: currentPeriodData } = useCurrentPeriod();

const currentPeriod =
currentPeriodData?.periods.find((period: ServicePeriod) => {
const start = new Date(period.start);
const end = new Date(period.end);
return now >= start && now <= end;
}) || null;

const currentMode = currentPeriod?.mode || ServiceMode.WISHLIST;

return {
activePeriod: currentPeriod,
serviceMode: currentMode,
registrationDisplayPeriod: currentPeriodData?.displayPeriod,
};
};
10 changes: 7 additions & 3 deletions packages/client/src/entities/seat/api/usePreRealSeats.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { fetchJsonOnPublic } from '@/shared/api/api.ts';
import { useManagePeriod } from '@/entities/schedule/model/useManagePeriod';

const SEC = 1000;
const MIN = 60 * SEC;
Expand All @@ -19,16 +20,19 @@ export const InitPreRealSeat: IPreRealSeat = {
};

function usePreRealSeats() {
const { preSeat } = useManagePeriod();

return useQuery({
queryKey: ['preRealSeats'],
queryFn: fetchPreRealSeats,
queryFn: () => fetchPreRealSeats(preSeat.preSeatOpenDate),
staleTime: 10 * MIN,
select: data => data?.preSeats ?? null,
});
}

async function fetchPreRealSeats(): Promise<IPreRealSeatsResponse> {
return await fetchJsonOnPublic<IPreRealSeatsResponse>('/pre-seats.json?date=20260215');
async function fetchPreRealSeats(preSeatOpenDate: Date): Promise<IPreRealSeatsResponse> {
//preSeat 데이터 업데이트 시 preSeatOpenDate를 변경해주어야합니다.
return await fetchJsonOnPublic<IPreRealSeatsResponse>(`/pre-seats.json?date=${preSeatOpenDate}`);
}

export default usePreRealSeats;
5 changes: 3 additions & 2 deletions packages/client/src/entities/subjects/ui/SubjectDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';
import { InitWishes } from '@/entities/wishes/model/useWishes.ts';
import { WishesWithSeat } from '@/entities/subjectAggregate/model/useWishesPreSeats.ts';
import usePreSeatGate from '@/widgets/live/preSeat/model/usePreSeatGate';
import { useManagePeriod } from '@/entities/schedule/model/useManagePeriod';
import { getSeatColor } from '@/shared/config/colors.ts';
import { Flex } from '@allcll/allcll-ui';

Expand All @@ -13,7 +13,8 @@ interface ISubjectDetailProps {
function SubjectDetail({ wishes }: ISubjectDetailProps) {
const data = wishes ?? InitWishes;
const hasPreSeats = wishes && 'seat' in wishes;
const { isPreSeatAvailable } = usePreSeatGate({ hasSeats: hasPreSeats });
const { preSeat } = useManagePeriod();
const isPreSeatAvailable = preSeat.shouldPreparePreSeat && Boolean(hasPreSeats);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

파일을 확인해보니, hasPreSeats 가 boolean 으로써 사용되고 있습니다.
hasPreSeats = Boolean(...
변수에서 Boolean으로 바꾸긴 보다, hasPreSeats를 변경하는건 어떤가요?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

넵! 좋은 의견 감사합니다~! 수정하겠습니다.


const seats = hasPreSeats ? wishes.seat : -1;

Expand Down
8 changes: 3 additions & 5 deletions packages/client/src/entities/wishes/api/wishes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@ export interface WishesApiResponse {
baskets: { subjectId: number; totalCount: number }[];
}

// baskets.json 파일 업데이트 시 반드시 `CACHE_VERSION` 값을 함께 변경해주세요.
const CACHE_VERSION = 'SPRING_26_20260131';

export const fetchWishesDataBySemester = async (semester: string) => {
return await fetchJsonOnPublic<WishesApiResponse>(`/${semester}/baskets.json?v=${CACHE_VERSION}`);
// baskets.json 파일 업데이트 시 반드시 `BASKET_OPEN_DATE` 값을 함께 변경해주세요.
export const fetchWishesDataBySemester = async (semester: string, basketOpenDate: Date) => {
return await fetchJsonOnPublic<WishesApiResponse>(`/${semester}/baskets.json?v=${basketOpenDate}`);
};

interface DetailRegistersResponse {
Expand Down
4 changes: 3 additions & 1 deletion packages/client/src/entities/wishes/model/useWishes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useSubject, { InitSubject } from '@/entities/subjects/model/useSubject.ts
import { joinData } from '@/entities/subjectAggregate/lib/joinSubjects.ts';
import { fetchWishesDataBySemester, WishesApiResponse } from '@/entities/wishes/api/wishes.ts';
import { RECENT_SEMESTERS } from '@/entities/semester/api/semester';
import { useManagePeriod } from '@/entities/schedule/model/useManagePeriod';

export const InitWishes = {
...InitSubject,
Expand All @@ -15,10 +16,11 @@ export const InitWishes = {
function useWishes(semester?: string) {
semester = semester ?? RECENT_SEMESTERS.semesterCode;
const { data: subjects, isPending, isLoading } = useSubject(semester);
const { basket } = useManagePeriod();

const query = useQuery({
queryKey: ['wishlist', semester],
queryFn: () => fetchWishesDataBySemester(semester),
queryFn: () => fetchWishesDataBySemester(semester, basket.basketOpenDate),
staleTime: Infinity,
select: data => joinSubjects(data, subjects),
});
Expand Down
5 changes: 3 additions & 2 deletions packages/client/src/features/wish/lib/useHeaderSelector.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { Wishes } from '@/shared/model/types.ts';
import { useWishesTableStore } from '../model/useWishTableColumnStore';
import { IPreRealSeat } from '@/entities/seat/api/usePreRealSeats';
import usePreSeatGate from '@/widgets/live/preSeat/model/usePreSeatGate';
import { useManagePeriod } from '@/entities/schedule/model/useManagePeriod';

/** 사용하는 헤더를 반환해줍니다. 기간에 따라, wishes, pre-seats 변경 가능 */
function useHeaderSelector(data: Wishes[] | (Wishes & IPreRealSeat)[] | null | undefined) {
const tableTitles = useWishesTableStore(state => state.tableTitles);
const hasPreSeats = !!(data && data[0] && 'seat' in data[0]);
const isWishesAvailable = data && data[0] && 'totalCount' in data[0];
const { isPreSeatAvailable } = usePreSeatGate({ hasSeats: hasPreSeats });
const { preSeat } = useManagePeriod();
const isPreSeatAvailable = preSeat.shouldPreparePreSeat && hasPreSeats;

let visibleCols = [{ title: '', visible: true, key: '' }, ...tableTitles.filter(col => col.visible)];
if (!isWishesAvailable) {
Expand Down
6 changes: 3 additions & 3 deletions packages/client/src/pages/live/Live.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { Helmet } from 'react-helmet';
import PinSearchBottomSheet from '@/widgets/live/pin/ui/PinSearchBottomSheet';
import RealtimeTable from '@/widgets/live/board/ui/RealtimeTable';
import useAlarmModalStore from '@/features/live/pin/model/useAlarmModalStore';
import usePreSeatGate from '@/widgets/live/preSeat/model/usePreSeatGate';
import { useManagePeriod } from '@/entities/schedule/model/useManagePeriod';

function Live() {
const isSearchOpen = useAlarmModalStore(state => state.isSearchOpen);
const setIsSearchOpen = useAlarmModalStore(state => state.setIsSearchOpen);
const isMobile = useMobile();
const { isPreSeatAvailable } = usePreSeatGate();
const { preSeat } = useManagePeriod();

return (
<>
Expand Down Expand Up @@ -41,7 +41,7 @@ function Live() {
<LivePinnedCourses />

<Grid columns={{ base: 1 }} gap="gap-4">
<LiveMainContent isPreSeatAvailable={isPreSeatAvailable} />
<LiveMainContent isPreSeatAvailable={preSeat.shouldPreparePreSeat} />
</Grid>
</Flex>

Expand Down
Loading