주간 달력 만들기
1-1. 특정 연도와 주차를 가지고 날짜를 구한다.
// 특정 달의 주 번호 목록을 반환하는 함수
getWeekNumListInMonth = (month) => {
let weekNumList = [];
let firstDayOfMonth = new Date(currentYear, month - 1, 1);
let firstMonday = getFirstDayOfWeek(firstDayOfMonth);
// 첫 번째 월요일의 주 번호
let date = moment(firstMonday);
let weekNum = date.week();
let weekStartDate = new Date(firstMonday);
let nextMonth = month % 12;
while (weekStartDate.getMonth() !== nextMonth) {
weekNumList.push(weekNum);
weekStartDate.setDate(weekStartDate.getDate() + 7);
weekNum += 1;
}
return weekNumList;
};
// 특정 연도와 주차를 가지고 날짜를 구하는 함수
const getWeekdayDatesOfYearWeek = (year, weekNum) => {
const getFirstDayOfFirstISOWeek = (yr) => {
const simple = new Date(
yr,
0,
1 + ((4 - new Date(yr, 0, 1).getDay() + 7) % 7)
);
return simple;
};
let startDate = getFirstDayOfFirstISOWeek(year);
let weekStartDate = new Date(startDate);
weekStartDate.setDate(startDate.getDate() + (weekNum - 1) * 7 - 2);
let weekdayDates = [];
for (let i = 0; i < 5; i++) {
let date = new Date(weekStartDate);
date.setDate(weekStartDate.getDate() + i);
weekdayDates.push(date.toISOString().split("T")[0]);
}
return weekdayDates;
};
1-2. 구한 날짜를 표시한다. (TouchableOpacity를 이용하여 선택할 수 있도록 만든다.)
const renderWeekDays = (index, key) => {
const weekdays = getWeekdayDatesOfYearWeek(currentYear, index);
return weekdays.slice(0, 5).map((date, i) => {
return (
<TouchableOpacity
key={i}
style={styles.weekDay}
onPress={() => selectDate(date)}
>
<Text>{date.split("-")[2]}</Text>
</TouchableOpacity>
);
});
};
할 일을 표시한다.
2-1. 할 일을 생성한다.
// 예시 할 일
[
{
id: 0,
company: "test1",
start_date: "2023-11-20",
end_date: "2023-11-22",
line: 0,
},
{
id: 1,
company: "test2",
start_date: "2023-11-21",
end_date: "2023-11-23",
line: 0,
},
{
id: 2,
company: "test3",
start_date: "2023-11-23",
end_date: "2023-11-27",
line: 0,
},
]
2-2. 날짜 밑에 할 일을 표시한다.
// 주별 날짜를 렌더링하는 함수
const renderWeekDays = (index, key) => {
const weekdays = getWeekdayDatesOfYearWeek(currentYear, index);
return weekdays.slice(0, 5).map((date, i) => {
const dayIssues = issues
.filter((issue) => issue.start_date <= date && issue.end_date >= date)
.sort((a, b) => a.line - b.line);
const renderIssue = (item) => {
const commonStyle = { backgroundColor: "red", textAlign: "center" };
let issueStyle = {};
if (item.start_date === date && item.end_date === date) {
issueStyle = styles.issueSingle;
} else if (item.start_date === date) {
issueStyle = styles.issueStart;
} else if (item.end_date === date) {
issueStyle = styles.issueEnd;
} else {
issueStyle = styles.issueMid;
}
const unique_key = String(index) + String(key) + String(i) + item.id;
const isSingleDay = item.start_date === item.end_date;
const isMiddleDay = item.start_date !== date && item.end_date !== date;
return (
<View key={unique_key} style={[issueStyle, commonStyle]}>
{(isSingleDay || isMiddleDay) && (
<Text style={{ textAlign: "center" }}>{item.company}</Text>
)}
</View>
);
};
return (
<TouchableOpacity
key={i}
style={styles.weekDay}
onPress={() => selectDate(date)}
>
<Text>{date.split("-")[2]}</Text>
<View
style={{
flex: 1,
flexDirection: "col",
padding: 0,
width: "100%",
textAlign: "center",
}}
>
{dayIssues.map(renderIssue)}
</View>
</TouchableOpacity>
);
});
};
주간 달력을 합쳐서 달력을 만들기
3-1. 한 달에 해당하는 4개의 주를 구하여 합쳐서 출력하면 달력 완성.
import React, { useState, useEffect } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Animated,
} from "react-native";
import { PanGestureHandler, State } from "react-native-gesture-handler";
import moment from "moment";
const Calendar = () => {
const DAY = ["월", "화", "수", "목", "금"];
const today = moment();
const [currentMonth, setCurrentMonth] = useState(today.month() + 1);
const [currentYear, setCurrentYear] = useState(today.year());
const [issues, setIssues] = useState([
{
id: 0,
company: "test1",
start_date: "2023-11-20",
end_date: "2023-11-22",
line: 0,
},
{
id: 1,
company: "test2",
start_date: "2023-11-21",
end_date: "2023-11-23",
line: 0,
},
{
id: 2,
company: "test3",
start_date: "2023-11-23",
end_date: "2023-11-27",
line: 0,
},
]);
const [selectedDate, setSelectedDate] = useState();
const [selectedIssue, setSelectedIssue] = useState();
// 스와이프 액션을 한 번만 취할 수 있도록 하는 state
const [swipeActionTaken, setSwipeActionTaken] = useState(false);
// 스와이프 애니메이션을 위한 state
const translateX = new Animated.Value(0);
// 스와이프 애니메이션을 위한 함수
const onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: translateX } }],
{ useNativeDriver: true }
);
// 스와이프 액션을 한 번만 취할 수 있도록 하는 state
const onHandlerStateChange = ({ nativeEvent }) => {
if (nativeEvent.state === State.END) {
setSwipeActionTaken(false);
}
};
// 스와이프 액션 기능을 구현한 함수
translateX.addListener(({ value }) => {
if (!swipeActionTaken) {
if (value > 10) {
// 왼쪽에서 오른쪽으로 스와이프하면 이전 달로 이동
prevMonth();
setSwipeActionTaken(true);
} else if (value < -10) {
// 오른쪽에서 왼쪽으로 스와이프하면 다음 달로 이동
nextMonth();
setSwipeActionTaken(true);
}
}
});
// 이슈가 겹치는지 확인하는 함수
const datesOverlap = (issue1, issue2) => {
return (
issue1.start_date <= issue2.end_date &&
issue2.start_date <= issue1.end_date
);
};
useEffect(() => {
// 이슈가 겹치는 경우, 겹치는 이슈의 줄 번호를 증가시킴
const updatedIssues = issues.map((issue, index) => {
issues.forEach((otherIssue, otherIndex) => {
if (
index !== otherIndex &&
datesOverlap(issue, otherIssue) &&
issue.line === otherIssue.line
) {
// 중복되는 이슈의 줄 번호 증가
otherIssue.line += 1;
}
});
return issue;
});
setIssues(updatedIssues);
}, []);
const nextMonth = () => {
let nextMonth = currentMonth + 1;
if (nextMonth === 13) nextMonth = 1;
setCurrentMonth(nextMonth);
if (currentMonth === 12) setCurrentYear(currentYear + 1);
};
const prevMonth = () => {
let beforeMonth = currentMonth - 1;
if (beforeMonth === 0) beforeMonth = 12;
setCurrentMonth(beforeMonth);
if (currentMonth === 1) setCurrentYear(currentYear - 1);
};
const renderDay = () => {
return DAY.map((day, i) => (
<View key={i} style={styles.weekDayOfWeek}>
<Text>{day}</Text>
</View>
));
};
// 날짜를 선택했을 때, 선택한 날짜에 해당하는 이슈를 state에 저장
const selectDate = (date) => {
setSelectedDate(date);
// 선택한 날짜에 해당하는 이슈를 찾아서 state에 저장
let selected_issue = issues.filter(
(issue) => issue.start_date <= date && issue.end_date >= date
);
selected_issue.sort((a, b) => a.start_date - b.start_date);
console.log("selected_issue :", selected_issue);
setSelectedIssue(selected_issue);
};
// 특정 날짜의 주의 첫 번째 날짜를 구하는 함수
const getFirstDayOfWeek = (date) => {
const day = date.getDay();
const diff = date.getDate() - day + (day === 0 ? -6 : 1);
return new Date(date.setDate(diff + 1));
};
// 특정 달의 주 번호 목록을 반환하는 함수
getWeekNumListInMonth = (month) => {
let weekNumList = [];
let firstDayOfMonth = new Date(currentYear, month - 1, 1);
let firstMonday = getFirstDayOfWeek(firstDayOfMonth);
// 첫 번째 월요일의 주 번호
let date = moment(firstMonday);
let weekNum = date.week();
let weekStartDate = new Date(firstMonday);
let nextMonth = month % 12;
while (weekStartDate.getMonth() !== nextMonth) {
weekNumList.push(weekNum);
weekStartDate.setDate(weekStartDate.getDate() + 7);
weekNum += 1;
}
return weekNumList;
};
// 특정 연도와 주차를 가지고 날짜를 구하는 함수
const getWeekdayDatesOfYearWeek = (year, weekNum) => {
const getFirstDayOfFirstISOWeek = (yr) => {
const simple = new Date(
yr,
0,
1 + ((4 - new Date(yr, 0, 1).getDay() + 7) % 7)
);
return simple;
};
let startDate = getFirstDayOfFirstISOWeek(year);
let weekStartDate = new Date(startDate);
weekStartDate.setDate(startDate.getDate() + (weekNum - 1) * 7 - 2);
let weekdayDates = [];
for (let i = 0; i < 5; i++) {
let date = new Date(weekStartDate);
date.setDate(weekStartDate.getDate() + i);
weekdayDates.push(date.toISOString().split("T")[0]);
}
return weekdayDates;
};
// 주별 날짜를 렌더링하는 함수
const renderWeekDays = (index, key) => {
const weekdays = getWeekdayDatesOfYearWeek(currentYear, index);
return weekdays.slice(0, 5).map((date, i) => {
const dayIssues = issues
.filter((issue) => issue.start_date <= date && issue.end_date >= date)
.sort((a, b) => a.line - b.line);
const renderIssue = (item) => {
const commonStyle = { backgroundColor: "red", textAlign: "center" };
let issueStyle = {};
if (item.start_date === date && item.end_date === date) {
issueStyle = styles.issueSingle;
} else if (item.start_date === date) {
issueStyle = styles.issueStart;
} else if (item.end_date === date) {
issueStyle = styles.issueEnd;
} else {
issueStyle = styles.issueMid;
}
const unique_key = String(index) + String(key) + String(i) + item.id;
const isSingleDay = item.start_date === item.end_date;
const isMiddleDay = item.start_date !== date && item.end_date !== date;
return (
<View key={unique_key} style={[issueStyle, commonStyle]}>
{(isSingleDay || isMiddleDay) && (
<Text style={{ textAlign: "center" }}>{item.company}</Text>
)}
</View>
);
};
return (
<TouchableOpacity
key={i}
style={styles.weekDay}
onPress={() => selectDate(date)}
>
<Text>{date.split("-")[2]}</Text>
<View
style={{
flex: 1,
flexDirection: "col",
padding: 0,
width: "100%",
textAlign: "center",
}}
>
{dayIssues.map(renderIssue)}
</View>
</TouchableOpacity>
);
});
};
return (
<View style={styles.container}>
<PanGestureHandler
onGestureEvent={onGestureEvent}
onHandlerStateChange={onHandlerStateChange}
>
<Animated.View
style={{ ...styles.container, transform: [{ translateX }] }}
>
<ScrollView style={{ ...styles.container, padding: 20 }}>
<View style={styles.header}>
<TouchableOpacity onPress={prevMonth} style={styles.navButton}>
<Text>Previous</Text>
</TouchableOpacity>
<Text>
{currentYear} {currentMonth}
</Text>
<TouchableOpacity onPress={nextMonth} style={styles.navButton}>
<Text>Next</Text>
</TouchableOpacity>
</View>
<View style={{ flexDirection: "row" }}>{renderDay()}</View>
{getWeekNumListInMonth(currentMonth).map((item, index) => {
return (
<View
key={index}
style={{
flexDirection: "row",
}}
>
{renderWeekDays(item, index)}
</View>
);
})}
<View style={styles.selectedItem}>
<Text>{selectedDate}</Text>
{selectedIssue &&
selectedIssue.map((item, index) => {
return (
<View key={index}>
<Text>{item.company}</Text>
</View>
);
})}
</View>
</ScrollView>
</Animated.View>
</PanGestureHandler>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 0,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
weekDay: {
flex: 1,
alignItems: "center",
textAlign: "center",
padding: 10,
borderColor: "grey",
borderWidth: 1,
paddingHorizontal: 0,
marginHorizontal: 0,
},
weekDayOfWeek: {
flex: 1,
alignItems: "center",
textAlign: "center",
padding: 10,
paddingHorizontal: 0,
},
navButton: {
padding: 10,
backgroundColor: "lightgrey",
marginHorizontal: 10,
},
weekInfo: {
flexDirection: "column",
alignItems: "center",
},
weekText: {
fontSize: 16,
},
issueStart: {
borderRadius: 10,
borderTopRightRadius: 0,
borderBottomRightRadius: 0,
height: 20,
marginVertical: 3,
marginLeft: 10,
zIndex: 5,
},
issueMid: {
textAlign: "center",
borderLeftWidth: 0,
borderRightWidth: 0,
borderLeftWidth: 1,
borderRightWidth: 1,
borderColor: "red",
height: 20,
marginVertical: 3,
zIndex: 5,
},
issueEnd: {
borderRadius: 10,
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
height: 20,
marginVertical: 3,
marginRight: 10,
},
issueSingle: {
borderWidth1: 1,
borderRadius: 10,
height: 20,
marginVertical: 3,
marginHorizontal: 10,
textAlign: "center",
},
selectedItem: {
padding: 10,
margin: 10,
backgroundColor: "lightgrey",
borderRadius: 10,
},
});
export default Calendar;