오늘은 진행하던 프로젝트에서 타입 스크립트를 사용하여 캘린더 퍼블리싱을 진행하였습니다.
import React, { useState } from 'react';
import styled from 'styled-components';
import CalendarButton from './CalendarButton'; // CalendarButton 컴포넌트 import
const CalendarContainer = styled.div`
margin-top: 40px;
`;
const CalendarNavigation = styled.div`
display: flex;
justify-content: space-between;
padding: 0 10px;
`;
const NavButton = styled.button`
background-color: transparent;
border: none;
color: #000000;
font-size: 26px;
cursor: pointer;
&:hover {
color: #007bff;
}
`;
const MonthYearTitle = styled.span`
font-size: 26px;
font-weight: bold;
`;
const WeekdaysRow = styled.div`
display: flex;
justify-content: space-around;
text-align: center;
font-weight: medium;
color: #2f2f2f;
margin-top: 25px;
margin-bottom: 10px;
`;
interface WeekdayCellProps {
isSunday?: boolean;
isSaturday?: boolean;
}
const WeekdayCell = styled.div<WeekdayCellProps>`
width: 14.28%;
color: ${(props: WeekdayCellProps): string => {
return props.isSunday
? '#ff4343'
: props.isSaturday
? '#0085ff'
: '#2f2f2f';
}};
abbr {
text-decoration: none;
}
`;
const DaysGrid = styled.div`
display: flex;
flex-direction: column;
`;
const WeekRow = styled.div`
display: flex;
width: 100%;
`;
interface DayCellProps {
isCurrentMonth: boolean;
hasEvent?: boolean;
}
const DayCell = styled.div<DayCellProps>`
flex: 1;
padding: 15px 15px;
font-size: 18px;
background-color: ${(props: DayCellProps): string => {
if (props.hasEvent) {
return '#e0f0ff'; // 일정 있는 날 색상 변경 (원래 오늘 날짜 색상)
}
return '#ececec'; // 다른 날짜 기본 배경색
}};
border-radius: 13px;
text-align: center;
margin: 10px 5px;
cursor: pointer;
color: ${(props: DayCellProps): string => {
return props.isCurrentMonth ? '#000' : '#aaa';
}};
&:hover {
background-color: ${(props: DayCellProps): string => {
return props.hasEvent ? '#d4e5f6' : '#e0f0ff'; // hover 시 일정 있는 날짜 색상 유지
}};
}
position: relative;
`;
const EventDot = styled.div`
width: 6px;
height: 6px;
background-color: #40a3ff;
border-radius: 50%;
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
`;
const ScheduleButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
margin-top: 20px;
`;
const ProjectCalendar: React.FC = () => {
const today = new Date();
const [currentDate, setCurrentDate] = useState<Date>(new Date());
const [isSelectingDates, setIsSelectingDates] = useState<boolean>(false);
const [selectedStartDate, setSelectedStartDate] = useState<Date | null>(
null,
);
const [selectedEndDate, setSelectedEndDate] = useState<Date | null>(null);
// 일정이 있는 날짜 목록 (예: 이벤트 데이터)
const eventDates = [
new Date(2024, 9, 14), // 2024년 10월 14일에 일정
new Date(2024, 9, 20), // 2024년 10월 20일에 일정
// 추가 일정...
];
const getMonthYear = (date: Date): string => {
return date.toLocaleString('en-US', { month: 'long' });
};
const prevMonth = (): void => {
setCurrentDate(
new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1),
);
};
const nextMonth = (): void => {
setCurrentDate(
new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1),
);
};
const getWeekdays = (): string[] => {
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
};
const generateCalendar = (): Date[][] => {
const startOfMonth = new Date(
currentDate.getFullYear(),
currentDate.getMonth(),
1,
);
const endOfMonth = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
0,
);
const dates: Date[][] = [];
const current = new Date(startOfMonth);
current.setDate(current.getDate() - current.getDay());
while (current <= endOfMonth || current.getDay() !== 0) {
const week: Date[] = [];
for (let i = 0; i < 7; i += 1) {
week.push(new Date(current));
current.setDate(current.getDate() + 1);
}
dates.push(week);
}
return dates;
};
const isSameDay = (date1: Date, date2: Date): boolean => {
return (
date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate()
);
};
const handleDateClick = (date: Date): void => {
if (isSelectingDates) {
if (!selectedStartDate) {
setSelectedStartDate(date);
} else if (!selectedEndDate) {
if (date >= selectedStartDate) {
setSelectedEndDate(date);
} else {
setSelectedEndDate(selectedStartDate);
setSelectedStartDate(date);
}
} else {
setSelectedStartDate(date);
setSelectedEndDate(null);
}
} else {
console.log(date);
}
};
const dates: Date[][] = generateCalendar();
return (
<CalendarContainer>
<CalendarNavigation>
<NavButton type="button" onClick={prevMonth}>
<
</NavButton>
<MonthYearTitle>{getMonthYear(currentDate)}</MonthYearTitle>
<NavButton type="button" onClick={nextMonth}>
>
</NavButton>
</CalendarNavigation>
<WeekdaysRow>
{getWeekdays().map((day: string, index: number) => {
return (
<WeekdayCell
key={day}
isSunday={index === 0}
isSaturday={index === 6}
>
<abbr title={day}>{day}</abbr>
</WeekdayCell>
);
})}
</WeekdaysRow>
<DaysGrid>
{dates.map((week: Date[]) => {
return (
<WeekRow key={week[0].toISOString()}>
{week.map((date: Date) => {
const isCurrentMonth =
date.getMonth() === currentDate.getMonth();
// 일정이 있는 날짜인지 확인
const hasEvent = eventDates.some((eventDate) =>
isSameDay(eventDate, date),
);
return (
<DayCell
key={date.toISOString()}
isCurrentMonth={isCurrentMonth}
hasEvent={hasEvent}
onClick={() => {
handleDateClick(date);
}}
>
{date.getDate()}
{hasEvent && <EventDot />}
</DayCell>
);
})}
</WeekRow>
);
})}
</DaysGrid>
<ScheduleButtonContainer>
<CalendarButton
isSelectingDates={isSelectingDates}
hasSelectedDates={!!selectedStartDate}
onClick={() => {
if (isSelectingDates) {
if (selectedStartDate && selectedEndDate) {
console.log(
'선택된 시작 날짜:',
selectedStartDate,
);
console.log(
'선택된 종료 날짜:',
selectedEndDate,
);
setIsSelectingDates(false);
setSelectedStartDate(null);
setSelectedEndDate(null);
} else if (selectedStartDate) {
console.log('종료 날짜를 선택하세요.');
}
} else {
setIsSelectingDates(true);
setSelectedStartDate(null);
setSelectedEndDate(null);
console.log('회의 일정잡기 버튼 클릭');
}
}}
onCancel={() => {
setIsSelectingDates(false);
setSelectedStartDate(null);
setSelectedEndDate(null);
console.log('취소 버튼 클릭');
}}
/>
</ScheduleButtonContainer>
</CalendarContainer>
);
};
export default ProjectCalendar;
import React from 'react';
import styled from 'styled-components';
interface CalendarButtonProps {
isSelectingDates: boolean;
hasSelectedDates: boolean;
onClick: () => void;
onCancel?: () => void;
}
const ButtonWrapper = styled.div`
display: flex;
gap: 10px; /* 버튼 간의 간격 */
margin-top: -2%;
`;
const Button = styled.button<CalendarButtonProps>`
background-color: ${(props): string => {
return props.isSelectingDates && !props.hasSelectedDates
? '#CCCCCC'
: '#40a3ff';
}};
color: #ffffff;
border: none;
padding: 8px 20px;
font-size: 16px;
border-radius: 10px;
cursor: pointer;
&:hover {
background-color: ${(props): string => {
return props.isSelectingDates && !props.hasSelectedDates
? '#CCCCCC'
: '#3392e6';
}};
}
`;
const CancelButton = styled.button`
background-color: #40a3ff;
color: #ffffff;
border: none;
padding: 8px 20px;
font-size: 16px;
border-radius: 10px;
cursor: pointer;
&:hover {
background-color: #3392e6;
}
`;
const CalendarButton: React.FC<CalendarButtonProps> = ({
isSelectingDates,
hasSelectedDates,
onClick,
onCancel,
}) => {
console.log('isSelectingDates:', isSelectingDates);
console.log('onCancel:', onCancel);
return (
<ButtonWrapper>
{isSelectingDates && onCancel && (
<CancelButton type="button" onClick={onCancel}>
취소
</CancelButton>
)}
<Button
type="button"
isSelectingDates={isSelectingDates}
hasSelectedDates={hasSelectedDates}
onClick={onClick}
>
회의 일정잡기
</Button>
</ButtonWrapper>
);
};
// 기본값 설정
CalendarButton.defaultProps = {
onCancel: () => {}, // 기본값으로 빈 함수 설정
};
export default CalendarButton;
import React, { useState, useEffect } from 'react';
import styled from 'styled-components';
import { ActiveSettingIcon, MemberIcon, MemberAddIcon, AddIcon } from '@assets';
import {
MemberTable,
TitleTab,
EventTab,
ActionButton,
EventFile,
} from '@components';
import { FlexCol, FlexRow, ItemsCenterRow, ItemsCenterStartRow } from '@styles';
import ProjectCalendar from '@components/calendar/ProjectCalendar';
import Layout from '../Layout';
// -- 스타일 컴포넌트 --
const Container = styled(FlexRow)`
width: 100%;
max-width: 1100px;
`;
const ContentArea = styled(FlexCol)`
width: 50%;
overflow-y: auto;
height: calc(100vh - 50px);
padding: 40px;
gap: 30px;
::-webkit-scrollbar {
width: 0;
background: transparent;
}
-ms-overflow-style: none;
scrollbar-width: none;
`;
const IconImage = styled.img<{ $width: number; $height: number }>`
width: ${(props) => props.$width}px;
height: ${(props) => props.$height}px;
cursor: pointer;
`;
// 왼쪽 영역
const LeftContentArea = styled(ContentArea)``;
const MemberArea = styled(FlexCol)``;
const MemberTabArea = styled(ItemsCenterRow)`
gap: 6px;
padding-left: 3px;
font-size: 20px;
color: var(--color-gray-600);
`;
const MemberAddArea = styled(ItemsCenterStartRow)`
padding: 5px;
border-bottom: 0.5px solid var(--color-gray-300);
`;
const MemberAddButton = styled(ItemsCenterRow)`
background-color: var(--color-gray-50);
border-radius: 3px;
padding: 2px;
gap: 1px;
font-size: 7px;
color: var(--color-gray-400);
cursor: pointer;
`;
const ContentTabArea = styled(FlexCol)``;
const ContentFileArea = styled(FlexCol)`
gap: 4px;
`;
// 오른쪽 영역
const RightContentArea = styled(ContentArea)``;
// fetchData 중에서 일부 데이터들
const fetchEventData = async (): Promise<{
meetings: { id: string; meetingName: string; dateTime: string }[];
schedules: { id: string; meetingName: string; dateTime: string }[];
members: {
id: string;
name: string;
role: string | null;
email: string;
permission: string;
}[];
}> => {
return {
meetings: [
{
id: '1',
meetingName: '프로젝트 킥오프',
dateTime: '2024-09-14T10:30:00',
},
{
id: '2',
meetingName: '디자인 리뷰',
dateTime: '2024-09-15T14:00:00',
},
],
schedules: [
{
id: '3',
meetingName: '프론트 디자인 회의 일정',
dateTime: '2024-09-20T10:30:00',
},
{
id: '4',
meetingName: '프론트 기능 명세 일정',
dateTime: '2024-09-15T14:00:00',
},
],
members: [
{
id: '1',
name: '신진욱',
role: 'FE',
email: '[email protected]',
permission: 'owner',
},
{
id: '2',
name: '박건민',
role: 'DE',
email: '[email protected]',
permission: 'member',
},
],
};
};
const ProjectDetailPage: React.FC = () => {
const [activeTab, setActiveTab] = useState<string>('meeting');
const [meetingData, setMeetingData] = useState<
{ id: string; meetingName: string; dateTime: string }[]
>([]);
const [scheduleData, setScheduleData] = useState<
{ id: string; meetingName: string; dateTime: string }[]
>([]);
const [memberData, setMemberData] = useState<
{
id: string;
name: string;
role: string | null;
email: string;
permission: string;
}[]
>([]);
const onClickMeetCreateButton = (): void => {
alert('회의 생성');
};
const onClickMemberSettingButton = (): void => {
alert('멤버 설정');
};
const onClickMemberInviteButton = (): void => {
alert('멤버 초대');
};
useEffect(() => {
const fetchData = async (): Promise<void> => {
const { meetings, schedules, members } = await fetchEventData();
setMeetingData(meetings);
setScheduleData(schedules);
setMemberData(members);
};
fetchData();
}, []);
const eventData = activeTab === 'meeting' ? meetingData : scheduleData;
return (
<Layout>
<Container>
<LeftContentArea>
<TitleTab type="project" title="새로운 프로젝트" />
<MemberArea>
<MemberTabArea>
<IconImage
src={MemberIcon}
$width={28}
$height={20}
/>
Member
<IconImage
src={ActiveSettingIcon}
$width={16}
$height={16}
onClick={onClickMemberSettingButton}
/>
</MemberTabArea>
<MemberTable data={memberData} />
<MemberAddArea>
<MemberAddButton
onClick={onClickMemberInviteButton}
>
<IconImage
src={MemberAddIcon}
$width={7}
$height={7}
/>
추가하기
</MemberAddButton>
</MemberAddArea>
</MemberArea>
<ContentTabArea>
<EventTab
activeTab={activeTab}
onClickTab={setActiveTab}
/>
<ContentFileArea>
{activeTab === 'meeting' && (
<ActionButton
icon={AddIcon}
label="회의 생성"
onClick={onClickMeetCreateButton}
/>
)}
{eventData.map((event) => (
<EventFile
key={event.id}
meetingName={event.meetingName}
dateTime={event.dateTime}
/>
))}
</ContentFileArea>
</ContentTabArea>
</LeftContentArea>
<RightContentArea>
<ProjectCalendar />
</RightContentArea>
</Container>
</Layout>
);
};
export default ProjectDetailPage;
이번 프로젝트를 통해 React와 TypeScript를 활용한 컴포넌트 기반의 개발 방식을 깊이 이해하게 되었습니다. 특히, 상태 관리와 이벤트 처리에 대한 경험이 풍부해졌고, styled-components를 통해 UI를 더 직관적으로 설계할 수 있음을 느꼈습니다. 또한, 일정 관리와 같은 기능을 구현하며 데이터 처리와 비동기 작업의 중요성을 다시 한번 깨달았습니다. 이 과정에서 코드의 가독성과 유지 보수성을 높이는 방법에 대해서도 많은 배움을 얻었습니다.