완성 화면 미리보기
프로젝트 생성
vite로 프로젝트를 생성했다.
react : v18.13.1
styled-components : v6.1.13
npm create vite@latest
cd calendar
npm install
npm i styled-components
date-fns 라이브러리 설치
date-fns는 JavaScript에서 날짜와 시간 변환등을 다루는데 매우 유용한 도구이다.
date-fns : v4.1.0
npm i date-fns
Calendar 컴포넌트 만들기
generateCalendar.ts
특정 월의 달력을 생성하는 함수
// src/generateCalendar.ts
import {
startOfMonth,
endOfMonth,
startOfWeek,
endOfWeek,
addDays,
} from "date-fns";
const generateCalendar = (date: Date) => {
// 1. 해당 월의 첫 날과 마지막 날
const startDate = startOfWeek(startOfMonth(date), { weekStartsOn: 0 }); // 주의 시작: 일요일
const endDate = endOfWeek(endOfMonth(date), { weekStartsOn: 0 }); // 주의 끝: 일요일
const calendar = [];
let currentDate = startDate;
// 2. 주 단위로 반복하여 날짜 추가
while (currentDate <= endDate) {
const week = Array.from({ length: 7 }, (_, i) => addDays(currentDate, i));
calendar.push(week);
currentDate = addDays(currentDate, 7); // 다음 주로 이동
}
return calendar; // 2D 배열 (주 단위로 구성된 달력)
};
export default generateCalendar;
Calendar.tsx
달력 컴포넌트
// src/Calendar.tsx
import styled from "styled-components";
import { format, isSameMonth, isToday } from "date-fns";
import generateCalendar from "./generateCalendar";
interface ICalendar {
currentDate: Date;
setCurrentDate: (date: Date) => void;
handleDateClick: (value: Date) => void;
}
/**
* Calendar
*
* 월별 날짜를 표시하고, 이전/다음 달로 이동할 수 있는 캘린더 컴포넌트.
* 날짜 클릭 이벤트를 처리할 수 있습니다.
*
* @component
* @param {Object} props - 컴포넌트의 props
* @param {Date} props.currentDate - 현재 캘린더가 표시할 날짜 (월의 첫 날로 설정)
* @param {Function} props.setCurrentDate - 현재 날짜를 설정하는 함수
* @param {Function} props.handleDateClick - 특정 날짜를 클릭했을 때 호출되는 함수
*/
const Calendar = ({
currentDate,
setCurrentDate,
handleDateClick,
}: ICalendar) => {
const calendar = generateCalendar(currentDate);
// 이전 달로 이동
const handlePrevMonth = () => {
const newDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() - 1,
1
);
setCurrentDate(newDate);
};
// 다음 달로 이동
const handleNextMonth = () => {
const newDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
1
);
setCurrentDate(newDate);
};
return (
<CalendarWrapper>
{/* 월 네비게이션 */}
<Header>
<Button onClick={handlePrevMonth}>이전</Button>
<MonthLabel>{format(currentDate, "yyyy년 MM월")}</MonthLabel>
<Button onClick={handleNextMonth}>다음</Button>
</Header>
{/* 요일 헤더 */}
<Weekdays>
{["일", "월", "화", "수", "목", "금", "토"].map((day) => (
<Weekday key={day}>{day}</Weekday>
))}
</Weekdays>
{/* 날짜 렌더링 */}
<Days>
{calendar.map((week, i) => (
<Week key={i}>
{week.map((day) => (
<Day
key={`${week}-${day}`}
$isSameMonth={isSameMonth(day, currentDate)}
$isToday={isToday(day)}
onClick={() => handleDateClick(day)} // 날짜 클릭 이벤트
>
{format(day, "d")}
</Day>
))}
</Week>
))}
</Days>
</CalendarWrapper>
);
};
export default Calendar;
const CalendarWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
max-width: 400px;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background-color: #ffffff;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
`;
const MonthLabel = styled.h2`
font-size: 18px;
font-weight: bold;
margin: 0;
`;
const Button = styled.button`
background: none;
border: none;
color: #333;
font-size: 14px;
cursor: pointer;
&:hover {
color: #007bff;
}
`;
const Weekdays = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
width: 100%;
margin-bottom: 8px;
`;
const Weekday = styled.div`
text-align: center;
font-size: 14px;
font-weight: bold;
color: #666;
`;
const Days = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
width: 100%;
`;
const Week = styled.div`
display: contents;
`;
const Day = styled.div<{ $isToday: boolean; $isSameMonth: boolean }>`
text-align: center;
padding: 8px;
border-radius: 4px;
background-color: ${({ $isToday }) => ($isToday ? "#f0f8ff" : "transparent")};
color: ${({ $isSameMonth }) => ($isSameMonth ? "#000000" : "#cccccc")};
font-weight: ${({ $isToday }) => ($isToday ? "bold" : "normal")};
cursor: pointer;
&:hover {
background-color: #e0e0e0;
}
`;
App.tsx
선택한 날짜와 어느달을 보여줘야하는지 저장하는 State는 App.tsx에서 관리한다.
// src/App.tsx
import { useState } from "react";
import Calendar from "./Calendar";
import styled from "styled-components";
const App = () => {
const [selectedDate, setSelectedDate] = useState<Date>(new Date());
const [currentDate, setCurrentDate] = useState<Date>(new Date()); // 캘린더의 현재 월
// 날짜 클릭 시 처리할 로직
const handleDateClick = (date: Date) => {
setSelectedDate(date);
};
// 오늘 날짜로 돌아가기
const handleResetDate = () => {
const today = new Date();
setSelectedDate(today); // 선택된 날짜 업데이트
setCurrentDate(today); // 캘린더의 현재 월도 오늘로 설정
};
return (
<Layout>
<BtnReset onClick={handleResetDate}>오늘</BtnReset>
<Calendar
currentDate={currentDate}
setCurrentDate={setCurrentDate}
handleDateClick={handleDateClick}
/>
{<p>선택된 날짜: {selectedDate ? selectedDate.toDateString() : null}</p>}
</Layout>
);
};
export default App;
const Layout = styled.div`
width: 100vw;
height: 100vh;
justify-content: center;
display: flex;
align-items: center;
flex-direction: column;
`;
const BtnReset = styled.button``;
중간 완성 미리보기
- 위 코드까지 적용하면 일반적인 달력 형태가 나오게 된다.
- 이전달, 다음달 등을 클릭시 월 단위로 끊어서 달력에 표시되며 클릭시 선택한 날짜를 state에 저장한다.
- "오늘" 클릭시 현재 날짜의 달력으로 이동하며 state도 오늘 기준으로 변경된다.
최종 근태 화면 만들기
위 중간 완성물에서 출근, 퇴근 시간이 표시될 영역을 추가해야한다.
샘플 데이터 만들기
샘플 양식은 대충 만들었는데 실무에서는 백엔드와 소통하여 데이터 양식과 타입을 맞추면 될 것 같다.
// src/attendance.ts
export const attendanceData: {
[key: string]: { checkIn: Date; checkOut: Date };
} = {
"2024-10-21": {
checkIn: new Date("2024-10-21T08:52"),
checkOut: new Date("2024-10-21T18:26"),
},
"2024-10-29": {
checkIn: new Date("2024-10-29T08:49"),
checkOut: new Date("2024-10-29T18:11"),
},
"2024-11-15": {
checkIn: new Date("2024-11-15T08:40"),
checkOut: new Date("2024-11-15T18:17"),
},
"2024-11-18": {
checkIn: new Date("2024-11-18T08:52"),
checkOut: new Date("2024-11-18T18:13"),
},
"2024-11-19": {
checkIn: new Date("2024-11-19T08:57"),
checkOut: new Date("2024-11-19T18:03"),
},
"2024-11-21": {
checkIn: new Date("2024-11-21T08:43"),
checkOut: new Date("2024-11-21T18:11"),
},
"2024-11-27": {
checkIn: new Date("2024-11-27T08:51"),
checkOut: new Date("2024-11-27T18:09"),
},
};
Calendar.tsx 파일 업그레이드
// src/ Calendar.tsx
import styled from "styled-components";
import { format, isSameMonth, isToday } from "date-fns";
import generateCalendar from "./generateCalendar";
import { attendanceData } from "./attendance";
interface ICalendar {
currentDate: Date;
setCurrentDate: (date: Date) => void;
handleDateClick: (value: Date) => void;
}
/**
* Calendar
*
* 월별 날짜를 표시하고, 이전/다음 달로 이동할 수 있는 캘린더 컴포넌트.
* 날짜 클릭 이벤트를 처리할 수 있습니다.
*
* @component
* @param {Object} props - 컴포넌트의 props
* @param {Date} props.currentDate - 현재 캘린더가 표시할 날짜 (월의 첫 날로 설정)
* @param {Function} props.setCurrentDate - 현재 날짜를 설정하는 함수
* @param {Function} props.handleDateClick - 특정 날짜를 클릭했을 때 호출되는 함수
*/
const Calendar = ({
currentDate,
setCurrentDate,
handleDateClick,
}: ICalendar) => {
const calendar = generateCalendar(currentDate);
// 이전 달로 이동
const handlePrevMonth = () => {
const newDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() - 1,
1
);
setCurrentDate(newDate);
};
// 다음 달로 이동
const handleNextMonth = () => {
const newDate = new Date(
currentDate.getFullYear(),
currentDate.getMonth() + 1,
1
);
setCurrentDate(newDate);
};
const attendanceText = (day: Date) => {
const attendanceObj = attendanceData[format(day, "yyyy-MM-dd")];
return (
<>
{attendanceObj?.checkIn && (
<Time>{`출근: ${format(attendanceObj.checkIn, "HH:mm")}`}</Time>
)}
{attendanceObj?.checkOut && (
<Time>{`퇴근: ${format(attendanceObj.checkOut, "HH:mm")}`}</Time>
)}
</>
);
};
return (
<CalendarWrapper>
{/* 월 네비게이션 */}
<Header>
<Button onClick={handlePrevMonth}>이전</Button>
<MonthLabel>{format(currentDate, "yyyy년 MM월")}</MonthLabel>
<Button onClick={handleNextMonth}>다음</Button>
</Header>
{/* 요일 헤더 */}
<Weekdays>
{["일", "월", "화", "수", "목", "금", "토"].map((day) => (
<Weekday key={day}>{day}</Weekday>
))}
</Weekdays>
{/* 날짜 렌더링 */}
<Days>
{calendar.map((week, i) => (
<Week key={i}>
{week.map((day) => (
<Day
key={`${week}-${day}`}
$isSameMonth={isSameMonth(day, currentDate)}
$isToday={isToday(day)}
onClick={() => handleDateClick(day)} // 날짜 클릭 이벤트
>
{format(day, "d")}
{attendanceData[format(day, "yyyy-MM-dd")]
? attendanceText(day)
: null}
</Day>
))}
</Week>
))}
</Days>
</CalendarWrapper>
);
};
export default Calendar;
const CalendarWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
background-color: #ffffff;
`;
const Header = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
margin-bottom: 16px;
`;
const MonthLabel = styled.h2`
font-size: 18px;
font-weight: bold;
margin: 0;
`;
const Button = styled.button`
background: none;
border: none;
color: #333;
font-size: 14px;
cursor: pointer;
&:hover {
color: #007bff;
}
`;
const Weekdays = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
width: 100%;
margin-bottom: 8px;
`;
const Weekday = styled.div`
text-align: center;
font-size: 14px;
font-weight: bold;
color: #666;
`;
const Days = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
width: 100%;
`;
const Week = styled.div`
display: contents;
`;
const Day = styled.div<{ $isToday: boolean; $isSameMonth: boolean }>`
display: flex;
flex-direction: column;
text-align: center;
gap: 5px;
width: 80px;
height: 100px;
border: 1px solid #e0e0e0;
background-color: ${({ $isToday }) => ($isToday ? "#f0f8ff" : "transparent")};
color: ${({ $isSameMonth }) => ($isSameMonth ? "#000000" : "#cccccc")};
font-weight: ${({ $isToday }) => ($isToday ? "bold" : "normal")};
cursor: pointer;
&:hover {
background-color: #e0e0e0;
}
`;
const Time = styled.span`
font-size: 10px;
font-weight: 400;
`;
완성 결과
위 코드를 반영하면 처음과 같이 최종 완성 화면을 확인할 수 있다.
'React' 카테고리의 다른 글
[React] input 태그의 왼쪽 0 제거하기 (0) | 2024.12.05 |
---|---|
[React] React Mobile Picker로 날짜 선택하기 (0) | 2024.12.05 |
[Webpack] 웹팩 환경에서 favicon 추가하는 방법 (1) | 2024.11.15 |
[React-Query] 리액트 쿼리 브라우저 복귀시 API 중복 요청 방지 (1) | 2024.10.29 |
[React] html2pdf.js를 활용해 PDF 다운로드 구현하기 (0) | 2024.07.17 |