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

[FE] 달력 로직 개선 #727

Open
wants to merge 27 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
071cd9d
feat: Calendar, DatePicker 클래스 생성
nlom0218 Nov 5, 2023
94c1c59
test: Calendar test 추가
nlom0218 Nov 5, 2023
23554ae
feat: 기본 Calendar 기능 구현
nlom0218 Nov 8, 2023
1055b9a
refactor: 달력으로 전달하는 데이터 형식 변경
nlom0218 Nov 8, 2023
20ddc2d
feat: 달력 내부 데이터 개수 제한 기능 추가
nlom0218 Nov 8, 2023
284070c
teat: ClickDayCalendar 스토리 추가
nlom0218 Nov 8, 2023
f51646c
feat: onClickTotalDataCount 기능 추가
nlom0218 Nov 8, 2023
b068f90
refactor: DayItemWrapper 컴포넌트 정리
nlom0218 Nov 8, 2023
7b948b7
refactor: Calendar 컴포넌트 리팩터링
nlom0218 Nov 8, 2023
09d5f39
test: Calender관련 mockData 추가
nlom0218 Nov 8, 2023
433bddd
refactor: 기존 MemberCalendar 부분 공통 컴포넌트로 변경
nlom0218 Nov 8, 2023
5e0c1cc
feat: render에 대한 년, 월 상태 추가하여 관리
nlom0218 Nov 9, 2023
4a57bdf
test: DatePicker 스토리 작성
nlom0218 Nov 9, 2023
df24596
feat: DatePicker 기본 기능 추가
nlom0218 Nov 9, 2023
e1928fb
refactor: DatePicker 컴포넌트 분리 및 Provider 적용
nlom0218 Nov 9, 2023
fb51636
test: StartEndDatePicker 스토리 추가
nlom0218 Nov 9, 2023
6a83e63
feat: onChangeDate 기능 추가
nlom0218 Nov 9, 2023
476cc60
feat: DatePicker mode 기능 추가
nlom0218 Nov 9, 2023
ae79e7f
feat: DatePicker hasButton 기능 추가
nlom0218 Nov 9, 2023
bad2088
feat: DatePicker isOnlyOneDay 기능 추가
nlom0218 Nov 9, 2023
151f86a
refactor: DatePicker startDate, endDate 타입 변경 및 스타일 수정
nlom0218 Nov 10, 2023
37ece7c
refactor: DatePicker 적용
nlom0218 Nov 10, 2023
964e8d8
feat: 스터디 기록이 없는 경우 모달 대신 노티로 알리기
nlom0218 Nov 10, 2023
d3c2362
refactor: DatePicker Props 명 수정 및 isOnlyOneDay일 때 endDate 무효
nlom0218 Nov 10, 2023
97c8320
refactor: DayItemWarpper 네이밍 CalendarItem으로 수정
nlom0218 Nov 11, 2023
36793cd
refctor: calendar 객체 수정
nlom0218 Nov 11, 2023
2ae6316
refactor: DatePicker 속성 설명 수정 및 getDayBackgroundColor 로직 수정
nlom0218 Nov 12, 2023
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: 1 addition & 1 deletion frontend/.eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"position": "after"
},
{
"pattern": "@Utils/*",
"pattern": "@Utils/**/*",
"group": "internal",
"position": "after"
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ export const requestGetMemberListRecord = (
memberId: string,
page: number,
size: number,
startDate?: string,
endDate?: string,
startDate: string | null,
endDate: string | null,
) => {
if (startDate && endDate)
return http.get<ResponseMemberListRecord>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Meta, StoryObj } from '@storybook/react';
import { STUDY_LIST_9_ARRAY } from 'mocks/mockData';

import format from '@Utils/format';

import Calendar from './Calendar';

type Story = StoryObj<typeof Calendar>;

/**
* `Calendar`는 일정과 같이 day에 대한 정보를 제공하는 달력 컴포넌트입니다.
*/
const meta: Meta<typeof Calendar> = {
title: 'COMPONENTS/Calendar',
component: Calendar,
};

export default meta;

/**
* `DefaultCalendar`는 현재 년, 월을 렌더링한 기본적인 Calendar의 스토리입니다.
*/
export const DefaultCalendar: Story = {
args: {
year: 2023,
month: 11,
},
};

/**
* `Calendar202309`는 2023년 9월 달력으로 외부에서 데이터를 받는 스토리입니다.
*/
export const Calendar202309: Story = {
args: {
year: 2023,
month: 9,
children: STUDY_LIST_9_ARRAY.map((item, index) => {
return (
<Calendar.Item key={index} date={new Date(item.createdDate)}>
{item.name}
</Calendar.Item>
);
}),
},
};

/**
* `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다.
*/
export const LimitCountCalendar202309: Story = {
args: {
year: 2023,
month: 9,
limit: 3,
onClickRestDataCount: (date) => window.alert(format.date(new Date(date), '-')),
onClickTotalDataCount: (date) => window.alert(format.date(new Date(date), '-')),
children: STUDY_LIST_9_ARRAY.map((item, index) => {
return (
<Calendar.Item key={index} date={new Date(item.createdDate)}>
{item.name}
</Calendar.Item>
);
}),
},
};

/**
* `LimitCountCalendar202309`는 데이터의 개수가 제한된 스토리입니다.
*/
export const ClickDayCalendar: Story = {
args: {
year: 2023,
month: 9,
onClickDay: (date) => window.alert(format.date(new Date(date), '-')),
},
};
116 changes: 116 additions & 0 deletions frontend/src/components/common/Calendar/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { PropsWithChildren } from 'react';
import { useRef } from 'react';
import { styled } from 'styled-components';

import CalendarProvider from './CalendarContext/CalendarProvider';
import CalendarItem from './CalendarItem/CalendarItem';
import ControlBar from './ControlBar/ControlBar';
import DayList from './DayList/DayList';
import DayOfWeeks from '../common/DayOfWeeks/DayOfWeeks';

type Props = {
/**
* 달력의 년도를 지정하는 속성.
*
*/
year: number;
/**
* 달력의 월을 지정하는 속성.
*
*/
month: number;
/**
* 달력 내 Data 개수를 제한하는 속성.
*
*/
limit?: number;
/**
* 달력에 렌더링 되는 Data 형식이 바뀌는 기준 너비를 지정하는 속성. 지정된 값보다 달력의 너비가 줄어들면 Data의 전체 개수가 렌더링됨.
*
* * @default 750
*/
formatChangedWidth?: number;
/**
* 달력에 렌더링되는 Data의 로딩 상태를 지정하는 속성
*
* * @default false
*/
dataLoading?: boolean;
Copy link
Collaborator

Choose a reason for hiding this comment

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

isDataLoading는 어떤가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

is prefix 좋은거 같아요!

/**
* 달력의 년, 월이 바뀔 때 호출되는 함수. year, month를 매개변수로 받음.
*
*/
onChangeCalendar?: (year: number, month: number) => void;
/**
* 달력의 Day의 클릭할 때 호출되는 함수. 해당 Day의 Date 객체를 매개변수로 받음.
*
*/
onClickDay?: (date: Date) => void;
/**
* 달력에 보여지지 않는 Data의 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음.
*
*/
onClickRestDataCount?: (date: Date) => void;
/**
* formatChangedWidth 속성의 값보다 달력의 너비가 줄어들었을 때, 렌덩이 되는 전체 데이터 개수를 클릭했을 때 호출되는 함수. 해당 Data가 위치한 Date 객체를 매개변수로 받음.
*
*/
onClickTotalDataCount?: (date: Date) => void;
};

const Calendar = ({
year,
month,
limit,
formatChangedWidth = 750,
children,
dataLoading = false,
onChangeCalendar,
onClickDay,
onClickRestDataCount,
onClickTotalDataCount,
}: PropsWithChildren<Props>) => {
const calendarRef = useRef<HTMLUListElement>(null);

return (
<CalendarProvider
year={year}
month={month}
limit={limit}
formatChangedWidth={formatChangedWidth}
calendarDataChildren={children}
calendarRef={calendarRef}
dataLoading={dataLoading}
onChangeCalendar={onChangeCalendar}
onClickDay={onClickDay}
onClickRestDataCount={onClickRestDataCount}
onClickTotalDataCount={onClickTotalDataCount}
>
<Layout>
<ControlBar />
<CalendarContainer>
<DayOfWeeks />
<DayList calendarRef={calendarRef} />
</CalendarContainer>
</Layout>
</CalendarProvider>
);
};

Calendar.Item = CalendarItem;

export default Calendar;

const Layout = styled.div`
display: flex;
flex-direction: column;
gap: 40px;

user-select: none;
`;

const CalendarContainer = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import type { PropsWithChildren, ReactElement, ReactNode, RefObject } from 'react';
import { Children, createContext, useContext, useEffect, useState } from 'react';

import type { CalendarStorage } from '@Utils/calendar';
import calendar from '@Utils/calendar';
import format from '@Utils/format';

type CalendarContext = {
year: number;
month: number;
navigationYear: number;
navigationMonth: number;
limit?: number;
calendarStorage: CalendarStorage;
calendarDataFormat: 'long' | 'short';
dataLoading: boolean;
isToday: (date: Date) => boolean;
shiftMonth: (type: 'next' | 'prev' | 'today') => void;
navigateYear: (year: number) => void;
navigateMonth: (month: number) => void;
navigate: (year?: number, month?: number) => void;
onClickDay?: (date: Date) => void;
onClickRestDataCount?: (date: Date) => void;
onClickTotalDataCount?: (date: Date) => void;
};

type Props = {
year: number;
month: number;
limit?: number;
formatChangedWidth: number;
calendarDataChildren: ReactNode;
calendarRef: RefObject<HTMLUListElement>;
dataLoading: boolean;
onChangeCalendar?: (year: number, month: number) => void;
onClickDay?: (date: Date) => void;
onClickRestDataCount?: (date: Date) => void;
onClickTotalDataCount?: (date: Date) => void;
};

const CalendarContext = createContext<CalendarContext | null>(null);

const CalendarProvider = ({
year,
month,
limit,
formatChangedWidth,
calendarDataChildren,
children,
calendarRef,
dataLoading,
onChangeCalendar,
onClickDay,
onClickRestDataCount,
onClickTotalDataCount,
}: PropsWithChildren<Props>) => {
const [navigationYear, setNavigationYear] = useState(year);
const [navigationMonth, setNavigationMonth] = useState(month);
const [calendarDataFormat, setCalendarDataFormat] = useState<'long' | 'short'>('long');

const isToday = (date: Date) => {
const today = format.date(new Date());
const inputDate = format.date(date);

return today === inputDate;
};

const shiftMonth = (type: 'next' | 'prev' | 'today') => {
let newYear = year;
let newMonth = month;

if (type === 'today') {
const today = new Date();

newYear = today.getFullYear();
newMonth = today.getMonth() + 1;
}

const changedMonth = month + (type === 'next' ? +1 : -1);

if (type !== 'today' && changedMonth === 0) {
newYear -= 1;
newMonth = 12;
}

if (type !== 'today' && changedMonth === 13) {
newYear += 1;
newMonth = 1;
}

if (type !== 'today' && changedMonth > 0 && changedMonth < 13) {
newMonth = changedMonth;
}

setNavigationYear(newYear);
setNavigationMonth(newMonth);

if (onChangeCalendar) onChangeCalendar(newYear, newMonth);
};

const navigateYear = (year: number) => setNavigationYear(year);

const navigateMonth = (month: number) => setNavigationMonth(month);

const navigate = (year?: number, month?: number) => {
if (year && month) {
if (onChangeCalendar) onChangeCalendar(year, month);
return;
}

if (onChangeCalendar) onChangeCalendar(navigationYear, navigationMonth);
};

useEffect(() => {
const calendarResizeObserver = new ResizeObserver(([calendar]) => {
const calendarWidth = calendar.target.clientWidth;

if (calendarWidth < formatChangedWidth) return setCalendarDataFormat('short');

return setCalendarDataFormat('long');
});

if (!calendarRef.current) return;

calendarResizeObserver.observe(calendarRef.current);
}, [calendarRef, formatChangedWidth]);

const calendarDataObject: Record<string, ReactElement[]> = {};

Children.forEach(calendarDataChildren, (child) => {
const item = child as ReactElement;

const { date } = item.props as { date: Date };

const formatDate = format.date(date, '-');
calendarDataObject[formatDate] = calendarDataObject[formatDate]
? [...calendarDataObject[formatDate], item]
: [item];
});

const calendarStorage = calendar.getCalendarStorage(year, month).map((item) => {
const formatDate = format.date(item.date, '-');

return { ...item, children: calendarDataObject[formatDate] };
});

const initValue = {
year,
month,
navigationYear,
navigationMonth,
limit,
calendarStorage,
calendarDataFormat,
dataLoading,
isToday,
shiftMonth,
navigateYear,
navigateMonth,
navigate,
onClickDay,
onClickRestDataCount,
onClickTotalDataCount,
};

return <CalendarContext.Provider value={initValue}>{children}</CalendarContext.Provider>;
};

export default CalendarProvider;

export const useCalendar = () => {
const value = useContext(CalendarContext);

if (!value) throw new Error('적절하지 않는 곳에서 useCalendar를 호출했습니다.');

return value;
};
Loading
Loading