Skip to content
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]: 날짜를 선택할 수 있는 캘린더 UI 구현 #68

Merged
merged 12 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍 💯💯💯

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 : 공휴일 색 변경 논의 필요(@해리)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오👍👍 TODO 코멘트에 작업자 멘션한 부분 좋네요!!
멘션할 때 Giuhub id로 하는 방법도 있는 것 같아요! 자동 완성 할 수 장점이 있습니다 ㅎㅎ
image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오...좋은데요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

익스텐션 뭐죵!?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hwinkr 혹시 자동완성 익스텐션이 있어야 가능한가요? 지금 해리는 안 되나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안돼요...ㅋㅋㅋ

// 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}월 달력`}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aria-label 명시.. 👍 대 해 리

<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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍👍👍 💯💯💯

);
})}
</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
Loading