diff --git a/frontend/.storybook/preview.ts b/frontend/.storybook/preview.ts deleted file mode 100644 index adcda96bd..000000000 --- a/frontend/.storybook/preview.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Preview } from '@storybook/react'; - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - }, -}; - -export default preview; diff --git a/frontend/.storybook/preview.tsx b/frontend/.storybook/preview.tsx new file mode 100644 index 000000000..d8c7918cb --- /dev/null +++ b/frontend/.storybook/preview.tsx @@ -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) => ( + + + + + ), +]; diff --git a/frontend/src/components/_common/Calendar/Calendar.stories.tsx b/frontend/src/components/_common/Calendar/Calendar.stories.tsx new file mode 100644 index 000000000..3337ad535 --- /dev/null +++ b/frontend/src/components/_common/Calendar/Calendar.stories.tsx @@ -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([]); + + const hasDate = (date: string) => selectedDates.includes(date); + + const handleDateClick = (date: string) => { + setSelectedDates((prevDates) => + hasDate(date) ? prevDates.filter((d) => d !== date) : [...prevDates, date], + ); + }; + + return ( + + ); + }, + ], +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Playground: Story = { + args: { + hasDate: () => false, + onDateClick: () => {}, + }, + render: (args) => { + return ; + }, +}; diff --git a/frontend/src/components/_common/Calendar/Calendar.styles.ts b/frontend/src/components/_common/Calendar/Calendar.styles.ts new file mode 100644 index 000000000..72e9ca128 --- /dev/null +++ b/frontend/src/components/_common/Calendar/Calendar.styles.ts @@ -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%; + `} +`; diff --git a/frontend/src/components/_common/Calendar/index.tsx b/frontend/src/components/_common/Calendar/index.tsx new file mode 100644 index 000000000..d63e9bb16 --- /dev/null +++ b/frontend/src/components/_common/Calendar/index.tsx @@ -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 ( +
+
+ {/* TODO : 캘린더 헤더 버튼 스타일 수정 예정(@해리) */} + + + {year}년 {month}월 + + +
+
+ {DAY_OF_WEEK.map((day) => ( +
+ {day} +
+ ))} +
+
+ {Array.from({ length: daySlotCount }, (_, index) => { + // TODO : isToday 변수를 활용한 스타일 변경 논의 필요 (@해리) + const { date, dateString, isDate, isToday, isHoliday } = handleGetDayInfo(index); + const isSelectedDate = hasDate(dateString); + + return isDate ? ( + + ) : ( +
+ ); + })} +
+
+ ); +} diff --git a/frontend/src/hooks/useCalendarInfo/useCalendarInfo.ts b/frontend/src/hooks/useCalendarInfo/useCalendarInfo.ts new file mode 100644 index 000000000..1b341dc3b --- /dev/null +++ b/frontend/src/hooks/useCalendarInfo/useCalendarInfo.ts @@ -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; +} diff --git a/frontend/src/hooks/useCalendarInfo/useCalendarInfo.utils.ts b/frontend/src/hooks/useCalendarInfo/useCalendarInfo.utils.ts new file mode 100644 index 000000000..56764c7d9 --- /dev/null +++ b/frontend/src/hooks/useCalendarInfo/useCalendarInfo.utils.ts @@ -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; +} diff --git a/frontend/src/hooks/useTimePick/useTimePick.utils.ts b/frontend/src/hooks/useTimePick/useTimePick.utils.ts index 765cb0b51..29ab86c77 100644 --- a/frontend/src/hooks/useTimePick/useTimePick.utils.ts +++ b/frontend/src/hooks/useTimePick/useTimePick.utils.ts @@ -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) { @@ -17,7 +17,7 @@ const decideDragEventTarget = (event: Event) => { } return target; -}; +} export function getTableCellElement(event: Event): HTMLTableCellElement | null { const targetElement = decideDragEventTarget(event); diff --git a/frontend/src/styles/global.ts b/frontend/src/styles/global.ts index dac3f3d6c..ab7fff8ae 100644 --- a/frontend/src/styles/global.ts +++ b/frontend/src/styles/global.ts @@ -162,7 +162,6 @@ const globalStyles = css` button { cursor: pointer; - border: none; } `;