Skip to content

Commit

Permalink
[FE] 캘린더 UI 개선 (#221)
Browse files Browse the repository at this point in the history
* chore: 캘린더 UI 개선에 사용되는 스타일 속성들 상수화

* refactor: useCalendarInfo 커스텀 훅 로직 개선

- 지난 달 버튼 비활성화 판단을 위한 isCurrentDate 변수 추가
- 일요일이 제일 왼쪽으로 배치되는 디자인 변경 사항을 반영하기 위한 로직 수정, 나머지 연산자 활용 로직 제거
- 지난 날, 토요일 스타일 적용을 위한 판단 로직 추가

* chore: 캘린더 UI 구성에 활용되는 매직넘버 의미있는 상수화

* refactor: 캘린더 UI 개선

- 일요일을 가장 왼쪽으로 오도록 수정
- 선택된 날짜, 일요일, 토요일, 오늘, 지날 날에 대한 스타일 적용 및 수정

* chore: global button 스타일에 padding을 0으로 설정

* design: 캘린더 스타일 파일 수정

- 오늘 표시하는 UI 변경
- 특정 날짜를 강조하는 스타일 함수에 적용한 early return 패턴에 {} 제거

* refactor: 캘린더 컴포넌트 수정

- 이전 달, 다음 달로 이동시키는 함수명 변경
- 날짜 도메인 용어 변경사항 반영

* chore: jsDoc을 활용해 커스텀 훅 설명 및 날짜 도메인 용어 변경사항 반영

* feat: 모든 키들이 불리언 값을 가지는 객체 타입을 정의하는 유틸리티 타입 구현
  • Loading branch information
hwinkr authored Aug 15, 2024
1 parent 2326bb9 commit b585fa4
Show file tree
Hide file tree
Showing 8 changed files with 258 additions and 89 deletions.
120 changes: 100 additions & 20 deletions frontend/src/components/_common/Calendar/Calendar.styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { css } from '@emotion/react';
import type { FlagObject } from 'types/utility';

import theme from '@styles/theme';

import CALENDAR_PROPERTIES from '@constants/calendar';

export const s_calendarContainer = css`
display: flex;
flex-direction: column;
Expand All @@ -19,7 +22,7 @@ export const s_dayOfWeekContainer = css`
margin-bottom: 2rem;
`;

export const s_dayOfWeek = css`
export const s_baseDayOfWeek = css`
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -29,11 +32,25 @@ export const s_dayOfWeek = css`
height: 4rem;
min-height: 4rem;
font-size: 1.2rem;
font-weight: normal;
color: gray;
${theme.typography.bodyMedium}
`;

export const s_dayOfWeek = (index: number) => {
if (index === CALENDAR_PROPERTIES.sundayNumber)
return css`
${DAY_SLOT_TEXT_STYLES.holiday}
`;

if (index === CALENDAR_PROPERTIES.saturdayNumber)
return css`
${DAY_SLOT_TEXT_STYLES.saturday}
`;

return css`
${DAY_SLOT_TEXT_STYLES.default}
`;
};

export const s_monthHeader = css`
display: flex;
align-items: center;
Expand All @@ -43,46 +60,109 @@ export const s_monthHeader = css`
margin-bottom: 2rem;
padding: 0 1rem;
font-size: 1.5rem;
font-weight: bold;
${theme.typography.bodyMedium}
`;

export const s_monthNavigation = css`
cursor: pointer;
background-color: transparent;
border: none;
${theme.typography.titleMedium}
&:disabled {
color: ${theme.colors.grey.primary};
}
`;

export const s_daySlotButton = css`
export const s_baseDaySlot = css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 3.6rem;
height: 3.6rem;
`;

export const s_daySlotButton = css`
cursor: pointer;
background-color: transparent;
border: none;
`;
${theme.typography.bodyLight}
// TODO : 공휴일 색 변경 논의 필요(@해리)
// TODO : s_todayDaySlot 추가 예정(@해리)
export const s_daySlot = (isHoliday: boolean) => css`
cursor: pointer;
font-size: 1.5rem;
color: ${isHoliday ? theme.colors.holiday : theme.colors.black};
&:disabled {
cursor: default;
}
`;

export const s_selectedDaySlot = (isSelected: boolean) => css`
export const s_baseDaySlotText = css`
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 3.6rem;
height: 3.6rem;
`;

${isSelected &&
css`
background-color: #fcc;
type DaySlotStatus = 'isSelectedFullDate' | 'isPrevDate' | 'isHoliday' | 'isSaturday' | 'isToday';

export const s_daySlotText = ({
isSelectedFullDate,
isPrevDate,
isHoliday,
isSaturday,
isToday,
}: FlagObject<DaySlotStatus>) => {
/* 덕지덕지 if문인데 어쩔 수 없다고 생각하기는 했습니다. 가독성을 위해서 switch문을 사용하는 것도 고려해보면 좋을 것 같은데, 코멘트로 의견 부탁드려요(@해리) */
/* if문 위에서부터 아래로, 스타일이 적용되어야 하는 우선순위입니다. 선택된 날짜의 스타일이 가장 우선적으로 고려되어야 하고, 그 다음은 지난날짜,,,순 입니다. 그래서 early return 패턴을 활용했어요(@해리) */
if (isSelectedFullDate) return DAY_SLOT_TEXT_STYLES.selected;
if (isPrevDate) return DAY_SLOT_TEXT_STYLES.prevDay;
if (isHoliday) return DAY_SLOT_TEXT_STYLES.holiday;
if (isSaturday) return DAY_SLOT_TEXT_STYLES.saturday;
if (isToday) return DAY_SLOT_TEXT_STYLES.today;

return DAY_SLOT_TEXT_STYLES.default;
};

// background-color: ${theme.colors.calendar.backgroundColor.today};
const DAY_SLOT_TEXT_STYLES = {
selected: css`
color: ${theme.colors.calendar.color.selected};
background-color: ${theme.colors.calendar.backgroundColor.selected};
border-radius: 50%;
`}
`;
`,
holiday: css`
color: ${theme.colors.calendar.color.holiday};
`,
today: css`
color: ${theme.colors.calendar.color.today};
border-radius: 50%;
&::after {
content: '';
position: absolute;
bottom: 0.4rem;
left: 50%;
transform: translateX(-50%);
width: 0.4rem;
height: 0.4rem;
background-color: ${theme.colors.calendar.color.today};
border-radius: 50%;
}
`,
saturday: css`
color: #8c9eff;
`,
prevDay: css`
color: ${theme.colors.grey.primary};
`,
default: css`
color: ${theme.colors.black};
`,
};
57 changes: 40 additions & 17 deletions frontend/src/components/_common/Calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,85 @@
import useCalendarInfo from '@hooks/useCalendarInfo/useCalendarInfo';

import {
s_baseDayOfWeek,
s_baseDaySlot,
s_baseDaySlotText,
s_calendarContainer,
s_calendarContent,
s_dayOfWeek,
s_dayOfWeekContainer,
s_daySlot,
s_daySlotButton,
s_daySlotText,
s_monthHeader,
s_monthNavigation,
s_selectedDaySlot,
} from './Calendar.styles';

const DAY_OF_WEEK = ['월', '화', '수', '목', '금', '토', '일'] as const;
const DAY_OF_WEEK = ['일', '월', '화', '수', '목', '금', '토'] as const;

// TODO : 선택된 날짜에 대한 강조 색을 외부에서 주입받을 수 있도록 props 수정 예정 (@해리)
interface CalendarProps {
hasDate: (date: string) => boolean;
onDateClick: (date: string) => void;
}

export default function Calendar({ hasDate, onDateClick }: CalendarProps) {
const { yearMonthInfo, handleGetDayInfo, handlePrevMonth, handleNextMonth } = useCalendarInfo();
const {
yearMonthInfo,
handleGetDateInfo,
handlePrevMonthMove,
handleNextMonthMove,
isCurrentDate,
} = useCalendarInfo();
const { year, month, daySlotCount } = yearMonthInfo;

return (
<div css={s_calendarContainer} aria-label={`${year}${month}월 달력`}>
<header css={s_monthHeader}>
{/* TODO : 캘린더 헤더 버튼 스타일 수정 예정(@해리) */}
<button css={s_monthNavigation} onClick={handlePrevMonth}>
<button
css={s_monthNavigation}
onClick={handlePrevMonthMove}
aria-label="지난 달"
disabled={isCurrentDate}
>
{'<'}
</button>
<span>
{year}{month}
</span>
<button css={s_monthNavigation} onClick={handleNextMonth}>
<button css={s_monthNavigation} onClick={handleNextMonthMove} aria-label="다음 달">
{'>'}
</button>
</header>
<section css={[s_calendarContent, s_dayOfWeekContainer]}>
{DAY_OF_WEEK.map((day) => (
<div key={day} css={s_dayOfWeek}>
{DAY_OF_WEEK.map((day, index) => (
<div key={day} css={[s_baseDayOfWeek, s_dayOfWeek(index)]}>
{day}
</div>
))}
</section>
<section css={s_calendarContent}>
{Array.from({ length: daySlotCount }, (_, index) => {
// TODO : isToday 변수를 활용한 스타일 변경 논의 필요 (@해리)
const { date, dateString, isDate, isToday, isHoliday } = handleGetDayInfo(index);
const isSelectedDate = hasDate(dateString);
const { date, fullDate, isValidDate, isToday, isHoliday, isSaturday, isPrevDate } =
handleGetDateInfo(index);
const isSelectedFullDate = hasDate(fullDate);

return isDate ? (
<button key={dateString} onClick={() => onDateClick(dateString)} css={s_daySlotButton}>
<span css={[s_daySlot(isHoliday), s_selectedDaySlot(isSelectedDate)]}>{date}</span>
return isValidDate ? (
<button
key={fullDate}
onClick={() => onDateClick(fullDate)}
disabled={isPrevDate}
css={[s_baseDaySlot, s_daySlotButton]}
>
<span
css={[
s_baseDaySlotText,
s_daySlotText({ isSelectedFullDate, isPrevDate, isHoliday, isSaturday, isToday }),
]}
>
{date}
</span>
</button>
) : (
<div key={dateString} css={s_daySlotButton}></div>
<div key={fullDate} css={s_baseDaySlot}></div>
);
})}
</section>
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/constants/calendar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const CALENDAR_PROPERTIES = {
firstMonth: 1,
lastMonth: 12,
daysInOneWeek: 7,
sundayNumber: 0,
saturdayNumber: 6,
};

export default CALENDAR_PROPERTIES;
87 changes: 60 additions & 27 deletions frontend/src/hooks/useCalendarInfo/useCalendarInfo.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,71 @@
import { useState } from 'react';

import { getDayInfo, getYearMonthInfo } from './useCalendarInfo.utils';
import CALENDAR_PROPERTIES from '@constants/calendar';

export default function useCalendarInfo() {
// TODO : L7 ~ L9 getCurrentDate 함수로 추상화 예정(@해리)
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth() + 1;
import { generateMonthDaySlots, getCurrentDateInfo, getDateInfo } from './useCalendarInfo.utils';

interface YearMonthInfo {
year: number;
month: number;
daySlotCount: number;
}

interface UseCalendarInfoReturn {
yearMonthInfo: YearMonthInfo;
handleGetDateInfo: (index: number) => ReturnType<typeof getDateInfo>;
handlePrevMonthMove: () => void;
handleNextMonthMove: () => void;
isCurrentDate: boolean;
}

type UseCalendarInfoHook = () => UseCalendarInfoReturn;

/**
* useCalendarInfo 훅은 달력 컴포넌트에서 사용할 연도, 월, 날짜 관련 정보를 관리합니다.
* 현재 연도와 월을 기반으로 달력의 날짜 슬롯을 생성하고, 이전 달 또는 다음 달로 이동하는 기능을 제공합니다. (@해리)
*
* @returns {UseCalendarInfoReturn}
*
* 아래는 useCalendarInfo 훅 반환 타입에 대한 간단한 설명입니다. :)
* @property {YearMonthInfo} yearMonthInfo - 현재 선택된 연도, 월, 그리고 해당 월의 일 수 정보를 담고 있는 객체
* @property {(index: number) => ReturnType<typeof getDateInfo>} handleGetDateInfo - 특정 날짜 슬롯의 정보를 가져오는 함수
* @property {() => void} handlePrevMonthMove - 이전 달로 이동하는 함수
* @property {() => void} handleNextMonthMove - 다음 달로 이동하는 함수
* @property {boolean} isCurrentDate - 현재 연도와 월이 오늘의 연도와 월과 일치하는지 여부
*/

const useCalendarInfo: UseCalendarInfoHook = () => {
const { currentDate, currentYear, currentMonth } = getCurrentDateInfo();

const [year, setYear] = useState(currentYear);
const [month, setMonth] = useState(currentMonth);
const isCurrentDate = year === currentYear && month === currentMonth;
const { firstDayIndex, daySlotCount } = generateMonthDaySlots(year, month);

const { firstDayIndex, daySlotCount } = getYearMonthInfo(year, month);

const handleGetDayInfo = (index: number) => {
return getDayInfo({ year, month, firstDayIndex, index, currentDate });
const handleGetDateInfo = (index: number) => {
return getDateInfo({ year, month, firstDayIndex, index, currentDate });
};

const handlePrevMonth = () => {
// TODO : isCurrentDate 함수로 추상화(@해리)
if (year === currentYear && month === currentMonth) return;
const handlePrevMonthMove = () => {
if (isCurrentDate) return;

if (month === 1) {
if (month === CALENDAR_PROPERTIES.firstMonth) {
setYear(year - 1);
setMonth(12); // TODO : 상수화(@해리)
} else {
setMonth(month - 1);
setMonth(CALENDAR_PROPERTIES.lastMonth);
return;
}

setMonth(month - 1);
};

const handleNextMonth = () => {
if (month === 12) {
const handleNextMonthMove = () => {
if (month === CALENDAR_PROPERTIES.lastMonth) {
setYear(year + 1);
setMonth(1);
} else {
setMonth(month + 1);
setMonth(CALENDAR_PROPERTIES.firstMonth);
return;
}

setMonth(month + 1);
};

return {
Expand All @@ -44,8 +74,11 @@ export default function useCalendarInfo() {
month,
daySlotCount,
},
handleGetDayInfo,
handlePrevMonth,
handleNextMonth,
} as const;
}
handleGetDateInfo,
handlePrevMonthMove,
handleNextMonthMove,
isCurrentDate,
};
};

export default useCalendarInfo;
Loading

0 comments on commit b585fa4

Please sign in to comment.