-
Notifications
You must be signed in to change notification settings - Fork 4
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
nlom0218
wants to merge
27
commits into
develop
Choose a base branch
from
fe/refactor/654-calendar-refactoring
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
[FE] 달력 로직 개선 #727
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
071cd9d
feat: Calendar, DatePicker 클래스 생성
nlom0218 94c1c59
test: Calendar test 추가
nlom0218 23554ae
feat: 기본 Calendar 기능 구현
nlom0218 1055b9a
refactor: 달력으로 전달하는 데이터 형식 변경
nlom0218 20ddc2d
feat: 달력 내부 데이터 개수 제한 기능 추가
nlom0218 284070c
teat: ClickDayCalendar 스토리 추가
nlom0218 f51646c
feat: onClickTotalDataCount 기능 추가
nlom0218 b068f90
refactor: DayItemWrapper 컴포넌트 정리
nlom0218 7b948b7
refactor: Calendar 컴포넌트 리팩터링
nlom0218 09d5f39
test: Calender관련 mockData 추가
nlom0218 433bddd
refactor: 기존 MemberCalendar 부분 공통 컴포넌트로 변경
nlom0218 5e0c1cc
feat: render에 대한 년, 월 상태 추가하여 관리
nlom0218 4a57bdf
test: DatePicker 스토리 작성
nlom0218 df24596
feat: DatePicker 기본 기능 추가
nlom0218 e1928fb
refactor: DatePicker 컴포넌트 분리 및 Provider 적용
nlom0218 fb51636
test: StartEndDatePicker 스토리 추가
nlom0218 6a83e63
feat: onChangeDate 기능 추가
nlom0218 476cc60
feat: DatePicker mode 기능 추가
nlom0218 ae79e7f
feat: DatePicker hasButton 기능 추가
nlom0218 bad2088
feat: DatePicker isOnlyOneDay 기능 추가
nlom0218 151f86a
refactor: DatePicker startDate, endDate 타입 변경 및 스타일 수정
nlom0218 37ece7c
refactor: DatePicker 적용
nlom0218 964e8d8
feat: 스터디 기록이 없는 경우 모달 대신 노티로 알리기
nlom0218 d3c2362
refactor: DatePicker Props 명 수정 및 isOnlyOneDay일 때 endDate 무효
nlom0218 97c8320
refactor: DayItemWarpper 네이밍 CalendarItem으로 수정
nlom0218 36793cd
refctor: calendar 객체 수정
nlom0218 2ae6316
refactor: DatePicker 속성 설명 수정 및 getDayBackgroundColor 로직 수정
nlom0218 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
frontend/src/components/common/Calendar/Calendar/Calendar.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
frontend/src/components/common/Calendar/Calendar/Calendar.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
/** | ||
* 달력의 년, 월이 바뀔 때 호출되는 함수. 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; | ||
`; |
177 changes: 177 additions & 0 deletions
177
frontend/src/components/common/Calendar/Calendar/CalendarContext/CalendarProvider.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isDataLoading
는 어떤가요?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is
prefix 좋은거 같아요!