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;
}
`;