Skip to content

Commit

Permalink
[FE] 런칭데이에서 받은 달력 UI 피드백 반영 (#346)
Browse files Browse the repository at this point in the history
* chore: 한국 공휴일을 계산해 주는 라이브러리 추가

* chore: 이전 달력 컴포넌트 & 로직 레거시 폴더로 이동

* chore: 이전에 사용했던 달력 데이터 계산 로직 커스텀 훅 삭제

* feat: 재사용할 수 있는 달력 공통 컴포넌트 구현

- useCalendar 커스텀 훅이 반환하는 달력 데이터를 각 컴포넌트가 뽑아서 사용할 수 있도록 Context API 활용
- Header, Weekdays, Body 컴포넌트로 달력을 구성하는 컴포넌트들을 합성 컴포넌트 구조로 구현

* feat: 공통 달력 컴포넌트, 헤더 컴포넌트 구현

- render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 월, 일 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현

* feat: 공통 달력 컴포넌트, 일주일 날짜를 표현하는 컴포넌트 구현

- render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 일주일 날짜 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현

* feat: 공통 달력 컴포넌트, 한 달을 구성하는 모든 날짜를 표현하는 컴포넌트 구현

- render props 패턴을 활용하여 useCalendar 컴포넌트가 반환하는 날짜 데이터를 사용해서 UI를 그리는 컴포넌트를 호출할 수 있도록 구현

* feat: 약속을 생성할 때 필요한 달력 컴포넌트 - 헤더 컴포넌트 구현

* feat: 약속을 생성할 때 필요한 달력 컴포넌트 - 일주일 날짜 컴포넌트 구현

* feat: 하나씩, 기간으로 날짜를 선택할 수 있는 인터페이스를 제공하는 useDateSelect 커스텀 훅 구현

- 날짜를 선택할 수 있는 모드인 single, range를 상태로 관리
- 시작/끝 날짜가 모두 선택되면 그 사이 모든 날짜가 선택되도록 getDatesInRange 유틸 함수 구현
- 끝 날짜가 시작 날짜보다 이른 경우 해당 끝 날짜를 다시 시작 날짜로 변경하도록 예외 처리
- 모든 범위 날짜가 선택되었을 때, 다른 날짜를 선택한 경우 해당 다른 날짜를 시작 날짜로 변경하도록 예외 처리

* feat: 한 달의 날짜 데이터, 다음/이전 달로 이동시킬 수 있는 인터페이스를 제공하는 useCalendar 커스텀 훅 구현

* chore: 폴더명 변경에 따른 import 경로 수정

* chore: 공통 달력 컴포넌트, useCalendar 커스텀 훅에서 필요한 타입 정의

* feat: 달, 년(year), 일, 날짜, 전체 날짜를 계산하는 유틸 함수 구현

* feat: 하나씩, 범위로 날짜를 선택하는 경우 UI를 구성하는 컴포넌트 구현

- 기존에는 하나의 CalendarDate로 구현을 하려고 했으나, 하나씩 선택하는 것과 범위로 선택하는 것의 UI 책임이 너무 다르다고 판단해 따로 구현하는 것으로 결정

* feat: 달력을 구성하는 각각의 컴포넌트들이 Context API로 공유되는 달력 데이터를 뽑아서 사용할 수 있도록 커스텀 훅 구현

* refactor: 변경된 달력 컴포넌트 반영

* feat: 날짜 추가 정보를 보여주는 컴포넌트 구현

* refactor: 날짜 추가 정보를 보여주는 컴포넌트를 사용하는 것으로 수정

* chore: Fragment 제거, 리액트 모듈 import 구문 제거

* chore: 현재 날짜 정보를 구할 때, 더 의미있는 변수명을 사용하는 것으로 개선

* design: 특정 날짜의 상태를 구하기 위해서 연속된 if문을 사용하는 것이 아닌 객체를 활용해서 구하는 것으로 수정

* feat: 유효한 배열의 타입인지 확인하는 타입 가드 함수 구현

* chore: 날짜 아래에 위치하는 텍스트 컴포넌트명을 더 의미있게 수정

* chore: 현재 달의 상태를 나타내는 변수명 수정 사항 반영, 날짜 상태 props 순서 수정

* chore: 달(Month) 상태 타입 네이밍 수정

* chore: 달력 정보를 Context API를 활용해서 내려주는 프로바이더 컴포넌트 위치 변경 반영

* chore: 달력 정보를 Context API를 활용해서 내려주는 프로바이더 컴포넌트 위치 변경 반영

* chore: 유효한 배열의 타입인지 확인하는 타입 가드 함수 위치 변경 반영

* refactor: 현재 달(Month)의 상태를 나타내는 로직을 함수로 분리

* chore: 함수 개행 추가

* chore: aria-label을 추가하기 위한 TabButton 컴포넌트 props 타입 변경

* chore: type import 구문 컨벤션에 맞게 수정

* chore: 실수로 지웠던 HTML Entities 복구

* chore: 필요없는 날짜 계산 로직 제거
  • Loading branch information
hwinkr authored Sep 24, 2024
1 parent c973e15 commit 8797d85
Show file tree
Hide file tree
Showing 37 changed files with 1,906 additions and 347 deletions.
64 changes: 64 additions & 0 deletions frontend/legacy/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 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 Calendar>;

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} />;
},
};
export default {};
93 changes: 93 additions & 0 deletions frontend/legacy/Calendar/Calendar.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { css } from '@emotion/react';

import theme from '@styles/theme';

import CALENDAR_PROPERTIES from '@constants/calendar';

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

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

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

export const s_baseDayOfWeek = css`
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-width: 4rem;
height: 4rem;
min-height: 4rem;
${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;
justify-content: space-between;
width: 100%;
margin-bottom: 2rem;
padding: 0 1rem;
`;

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

const DAY_SLOT_TEXT_STYLES = {
selected: css`
color: ${theme.colors.calendar.color.selected};
`,
holiday: css`
color: ${theme.colors.calendar.color.holiday};
`,
today: css`
color: ${theme.colors.calendar.color.today};
`,
saturday: css`
color: #8c9eff;
`,
prevDay: css`
color: ${theme.colors.grey.primary};
`,
default: css`
color: ${theme.colors.black};
`,
};
159 changes: 159 additions & 0 deletions frontend/legacy/Calendar/CalendarDate.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { css } from '@emotion/react';
import type { FlagObject } from 'types/utility';

import theme from '@styles/theme';

export const s_dateContainer = css`
width: 100%;
min-width: 4.8rem;
height: 4.8rem;
`;

export const s_baseDateButton = () => css`
cursor: pointer;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: none;
&:disabled {
cursor: default;
}
`;

export const s_singleDateButton = (isSelectedDate: boolean) => css`
background-color: ${isSelectedDate ? theme.colors.pink.medium : 'transparent'};
border-radius: 0.8rem;
`;

export const s_rangeDateButton = (isSelectedDate: boolean) => css`
background-color: ${isSelectedDate ? theme.colors.pink.light : 'transparent'};
`;

export const s_baseDaySlotText = css`
${theme.typography.bodyLight}
`;

type DaySlotStatus =
| 'isSelectedFullDate'
| 'isPrevDate'
| 'isSunday'
| 'isSaturday'
| 'isHoliday'
| 'isToday';

export const s_daySlotText = ({
isSelectedFullDate,
isPrevDate,
isSunday,
isSaturday,
isHoliday,
isToday,
}: FlagObject<DaySlotStatus>) => {
if (isSelectedFullDate) return DAY_SLOT_TEXT_STYLES.selected;
if (isHoliday) return DAY_SLOT_TEXT_STYLES.holiday;
if (isPrevDate) return DAY_SLOT_TEXT_STYLES.prevDay;
if (isToday) return DAY_SLOT_TEXT_STYLES.today;
if (isSunday) return DAY_SLOT_TEXT_STYLES.holiday;
if (isSaturday) return DAY_SLOT_TEXT_STYLES.saturday;

return DAY_SLOT_TEXT_STYLES.default;
};

const DAY_SLOT_TEXT_STYLES = {
selected: css`
color: ${theme.colors.calendar.color.selected};
`,
holiday: css`
color: ${theme.colors.calendar.color.holiday};
`,
today: css`
color: ${theme.colors.calendar.color.today};
`,
saturday: css`
color: #8c9eff;
`,
prevDay: css`
color: ${theme.colors.grey.primary};
`,
default: css`
color: ${theme.colors.black};
`,
};

export const s_holidayText = css`
font-size: 1rem;
font-weight: 300;
line-height: 1.2;
`;

export const s_rangeStart = (isAllRangeSelected: boolean) => css`
background-color: ${theme.colors.pink.medium};
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: 20%;
height: 100%;
background-color: ${isAllRangeSelected ? theme.colors.pink.light : theme.colors.white};
}
&::after {
content: '';
position: absolute;
top: 0;
right: 0.4px;
bottom: 0;
width: 20%;
height: 100%;
background-color: ${theme.colors.pink.medium};
clip-path: polygon(0 0, 100% 50%, 0 100%);
}
`;

export const s_rangeEnd = (isAllRangeSelected: boolean) => css`
background-color: ${theme.colors.pink.medium};
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 20%;
height: 100%;
background-color: ${isAllRangeSelected ? theme.colors.pink.light : theme.colors.white};
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0.4px;
width: 20%;
height: 100%;
background-color: ${theme.colors.pink.medium};
clip-path: polygon(100% 0, 0 50%, 100% 100%);
}
`;
74 changes: 74 additions & 0 deletions frontend/legacy/Calendar/Date/RangeCalendarDate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { DateInfo } from '@hooks/useCalendarInfo/useCalendar.type';
import { getDateInfo2 } from '@hooks/useCalendarInfo/useCalendarInfo.utils';

import {
s_baseDateButton,
s_baseDaySlotText,
s_dateContainer,
s_daySlotText,
s_holidayText,
s_rangeDateButton,
s_rangeEnd,
s_rangeStart,
} from '../CalendarDate.styles';

interface RangeCalendarDateProps {
dateInfo: DateInfo;
hasDate: (date: string) => boolean;
onDateClick: (date: string) => void;
isRangeStart: boolean;
isRangeEnd: boolean;
isAllRangeSelected: boolean;
}

export default function RangeCalendarDate({
dateInfo,
hasDate,
onDateClick,
isRangeStart,
isRangeEnd,
isAllRangeSelected,
}: RangeCalendarDateProps) {
const { key, value, status } = dateInfo;
const {
date,
currentFullDate,
isHoliday,
isToday,
isSaturday,
isSunday,
isPrevDate,
holidayName,
} = getDateInfo2(value, new Date());
const isSelectedFullDate = hasDate(currentFullDate);

return status === 'currentMonth' ? (
<button
key={key}
onClick={() => onDateClick(currentFullDate)}
disabled={isPrevDate}
css={[
s_dateContainer,
s_baseDateButton,
s_rangeDateButton(isSelectedFullDate),
isRangeStart && s_rangeStart(isAllRangeSelected),
isRangeEnd && s_rangeEnd(isAllRangeSelected),
s_daySlotText({
isSelectedFullDate,
isPrevDate,
isSunday,
isSaturday,
isHoliday,
isToday,
}),
]}
>
<span css={[s_baseDaySlotText]}>{date}</span>
{isRangeStart && <span css={s_holidayText}>시작</span>}
{isRangeEnd && <span css={s_holidayText}></span>}
{!isRangeStart && !isRangeEnd && <span css={s_holidayText}>{holidayName || '\u00A0'}</span>}
</button>
) : (
<div key={key} css={s_dateContainer}></div>
);
}
Loading

0 comments on commit 8797d85

Please sign in to comment.