Skip to content

Commit

Permalink
[FE]: 날짜를 선택할 수 있는 캘린더 UI 구현 (#68)
Browse files Browse the repository at this point in the history
* chore: global button color 스타일 삭제

* feat(useCalendarInfo): 날짜 정보를 알려주는 커스텀 훅 구현

- year, month를 상태로 관리하여 값이 변경될 때마다 날짜 정보를 계산해서 반환
- 달(Month)를 이동할 수 있는 함수 구현
- 12월에서 다음을 클릭할 경우 해(Year)가 변경되도록 구현

* feat(Calendar): 달력 컴포넌트 구현

- 오늘의 경우 어떻게 보여줘야할 것인지 스타일 논의 필요
- 일요일이 아닌, 월요일부터 테이블을 채울 수 있도록 구현

* chore: 스토리북 preview 설정 수정

- theme. global 스타일이 적용되도록 설정

* test(Calendar): 달력 컴포넌트 UI 테스트 구현

* chore(useTimePick): 함수 시그니쳐를 표현식으로 통일

* chore: px단위를 rem단위로 수정

* refactor: 빈 칸으로 표시되어야 하는 날짜인 경우, div 태그로 감싸도록 수정

- cursor : pointer 속성이 적용되지 않기 위함

* refactor: getDayInfo 함수의 인자를 객체 형태로 변경

* chore(useCalendarInfo): 의미있는 변수명을 사용하도록 수정
  • Loading branch information
hwinkr authored Jul 25, 2024
1 parent 16d4cf1 commit bd59576
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 17 deletions.
14 changes: 0 additions & 14 deletions frontend/.storybook/preview.ts

This file was deleted.

28 changes: 28 additions & 0 deletions frontend/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Global, ThemeProvider } from '@emotion/react';
import type { Preview } from '@storybook/react';
import React from 'react';

import globalStyles from '../src/styles/global';
import theme from '../src/styles/theme';

const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;

export const decorators = [
(Story) => (
<ThemeProvider theme={theme}>
<Global styles={globalStyles} />
<Story />
</ThemeProvider>
),
];
64 changes: 64 additions & 0 deletions frontend/src/components/_common/Calendar/Calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';

import Button from './index';
import Calendar from './index';

const meta = {
title: 'Components/Calendar',
component: Calendar,
tags: ['autodocs'],

parameters: {
layout: 'centered',
},
argTypes: {
hasDate: {
description: '선택된 날짜들',
type: 'function',
control: {
disable: true,
},
},
onDateClick: {
description: '선택된 날짜 리스트에 특정 날짜를 추가하거나 제거할 수 있는 함수',
},
},
decorators: [
(Story, context) => {
const [selectedDates, setSelectedDates] = useState<string[]>([]);

const hasDate = (date: string) => selectedDates.includes(date);

const handleDateClick = (date: string) => {
setSelectedDates((prevDates) =>
hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date],
);
};

return (
<Story
args={{
...context.args,
hasDate,
onDateClick: handleDateClick,
}}
/>
);
},
],
} satisfies Meta<typeof Button>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Playground: Story = {
args: {
hasDate: () => false,
onDateClick: () => {},
},
render: (args) => {
return <Calendar hasDate={args.hasDate} onDateClick={args.onDateClick} />;
},
};
86 changes: 86 additions & 0 deletions frontend/src/components/_common/Calendar/Calendar.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { css } from '@emotion/react';

export const s_calendarContainer = css`
display: flex;
flex-direction: column;
align-items: center;
`;

export const s_calendarContent = css`
display: grid;
grid-auto-rows: 4rem;
grid-template-columns: repeat(7, 1fr);
width: 100%;
`;

export const s_dayOfWeekContainer = css`
margin-bottom: 2rem;
`;

export const s_dayOfWeek = css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 4rem;
height: 4rem;
min-height: 4rem;
font-size: 1.2rem;
font-weight: normal;
color: gray;
`;

export const s_monthHeader = css`
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 2rem;
padding: 0 1rem;
font-size: 1.5rem;
font-weight: bold;
`;

export const s_monthNavigation = css`
cursor: pointer;
`;

export const s_daySlotButton = css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 3.6rem;
height: 3.6rem;
background-color: transparent;
border: none;
`;

// TODO : 공휴일 색 변경 논의 필요(@해리)
// TODO : s_todayDaySlot 추가 예정(@해리)
export const s_daySlot = (isHoliday: boolean) => css`
cursor: pointer;
font-size: 1.5rem;
color: ${isHoliday ? 'red' : '#000'};
`;

export const s_selectedDaySlot = (isSelected: boolean) => css`
display: flex;
align-items: center;
justify-content: center;
width: 3.6rem;
height: 3.6rem;
${isSelected &&
css`
background-color: #fcc;
border-radius: 50%;
`}
`;
65 changes: 65 additions & 0 deletions frontend/src/components/_common/Calendar/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import useCalendarInfo from '@hooks/useCalendarInfo/useCalendarInfo';

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

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 { 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>
<span>
{year}{month}
</span>
<button css={s_monthNavigation} onClick={handleNextMonth}>
{'>'}
</button>
</header>
<section css={[s_calendarContent, s_dayOfWeekContainer]}>
{DAY_OF_WEEK.map((day) => (
<div key={day} css={s_dayOfWeek}>
{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);

return isDate ? (
<button key={dateString} onClick={() => onDateClick(dateString)} css={s_daySlotButton}>
<span css={[s_daySlot(isHoliday), s_selectedDaySlot(isSelectedDate)]}>{date}</span>
</button>
) : (
<div key={dateString} css={s_daySlotButton}></div>
);
})}
</section>
</div>
);
}
51 changes: 51 additions & 0 deletions frontend/src/hooks/useCalendarInfo/useCalendarInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useState } from 'react';

import { getDayInfo, getYearMonthInfo } from './useCalendarInfo.utils';

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

const [year, setYear] = useState(currentYear);
const [month, setMonth] = useState(currentMonth);

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

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

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

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

const handleNextMonth = () => {
if (month === 12) {
setYear(year + 1);
setMonth(1);
} else {
setMonth(month + 1);
}
};

return {
yearMonthInfo: {
year,
month,
daySlotCount,
},
handleGetDayInfo,
handlePrevMonth,
handleNextMonth,
} as const;
}
44 changes: 44 additions & 0 deletions frontend/src/hooks/useCalendarInfo/useCalendarInfo.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export function getYearMonthInfo(year: number, month: number) {
const startDate = new Date(year, month - 1, 1);

/*
로직 설명(@hwinkr)
- 월요일을 index 0으로 변경하기 위해서 나머지 연산자를 활용한다.
- 자바스크립트 Date 객체는 기본적으로 일요일이 인덱스가 0인데, 모모 달력은 월요일을 인덱스를 0으로 만들어줘야 한다.
- 따라서, 특정 달의 시작 날짜에 대한 인덱스에 6을 더해주고 7로 나눈 나머지를 사용하는 것으로 구현했다.
*/
const firstDayIndex = (startDate.getDay() + 6) % 7;

const lastDayOfMonthDate = new Date(year, month, 0);
const lastDayNumber = lastDayOfMonthDate.getDate();

const daySlotCount = firstDayIndex + lastDayNumber;

return { year, month, firstDayIndex, daySlotCount } as const;
}

export function getDayInfo({
year,
month,
firstDayIndex,
index,
currentDate,
}: {
year: number;
month: number;
firstDayIndex: number;
index: number;
currentDate: Date;
}) {
const date = index - firstDayIndex + 1;
const isDate = index >= firstDayIndex;

const dateString = `${year}-${month}-${date}`;
const todayString = `${year}-${month}-${currentDate.getDate()}`;
const isToday = dateString === todayString;
// TODO : 일단은 일요일만 isHolday로 설정되도록 구현, 추후에 진짜 공휴일도 포함할 것인지 논의 필요, 찾아보니 라이브러리가 있더라구요.(@해리)
// TODO : 매직넘버 의미있는 상수화 필요(@해리)
const isHoliday = index % 7 === 6;

return { date, dateString, isDate, isToday, isHoliday } as const;
}
4 changes: 2 additions & 2 deletions frontend/src/hooks/useTimePick/useTimePick.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function isMouseEvent(event: Event): event is MouseEvent {
return event instanceof MouseEvent;
}

const decideDragEventTarget = (event: Event) => {
export function decideDragEventTarget(event: Event) {
let target;

if (isTouchEvent(event) && event.touches) {
Expand All @@ -17,7 +17,7 @@ const decideDragEventTarget = (event: Event) => {
}

return target;
};
}

export function getTableCellElement(event: Event): HTMLTableCellElement | null {
const targetElement = decideDragEventTarget(event);
Expand Down
1 change: 0 additions & 1 deletion frontend/src/styles/global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ const globalStyles = css`
button {
cursor: pointer;
border: none;
}
`;

Expand Down

0 comments on commit bd59576

Please sign in to comment.