https://velog.io/@bi-sz/투두리스트1
이전 게시글에서 UI 구성을 다루었고, 이번 게시글에서 기능구현을 다뤄보도록 하겠습니다.
선택된 날짜일 때 동그라미 표시를 해 주겠습니다.
기존 RederItem을 터치 가능하도록 TouchableOpacity로 변경해줍니다.
Column 컴포넌트를 사용하는 곳이 rederItem의 일 부분과, 요일부분이 있습니다. 요일은 터치할 필요가 없기 때문에 disabled
속성을 추가하여 터치 가능여부를 구분해줍니다.
터치됐을 때의 동작을 구현해야하니 onPress
속성도 추가해주었습니다.
ListHeaderComponent의 요일 부에 disabled={true}
속성을 추가하여 터치가 불가능하도록 수정합니다.
선택된 날짜는 항상 바뀌기 떄문에 state 로 작성해주었습니다.
now를 기준으로 구성하던 캘린더를, 선택된 날짜에 맞게 구성되도록 selectedDate로 관리해줄겁니다.
초기값은 now, now를 사용하는 부분을 selectedDate 로 작성해주었습니다.
now는 이제 selectedDate 의 초기값을 위해서만 사용되고, 나머지는 현재 선택된 날짜를 통해 계산되도록 해줄겁니다.
아래에 now를 사용하던 부분도 모두 selectedDate로 변경해주었습니다.
컴포넌트를 클릭했을 때 선택이 그 날짜로 되도록 세팅해줍니다.
date를 알고있으므로, setSelectedDate(date)
로 터치한 date를 selectedDate
에 담아줍니다.
selectedDate가 바뀔 때마다 인지할 수 있도록 useEffect를 생성해주었습니다.
처음 랜더링될 때 현재 날짜인 10월 26일이 찍혔고, 캘린더의 날짜를 선택하면 해당 날짜가 selectedDate에 담기는 것을 확인했습니다.
선택된 날짜를 구분하기 위해 조금 수정해주겠습니다.
Coulmn 컴포넌트에 isSelected
속성을 추가해주었고,
style에 backgroundColor
속성과 borderRadius
속성을 추가해주었습니다.
rederItem에서 isSelected
는 dayjs를 이용하여 date
와 selectedDate
가 같은지 비교하여 구해줍니다.
처음 랜더링될 때 현재 날짜에 회색 원 표시가 나타났습니다.
날짜를 선택하면, 선택한 날짜가 표시됩니다.
날짜를 선택하는 것으로 이전달, 다음달 정도로 날짜를 옮겨다닐 수 있지만, 몇개월 전 후 혹은 몇년 전 으로도 날짜를 옮겨다닐 수 있어야합니다.
날짜 선택 라이브러리
- DateTimePicker ( RN에서 제공)
- react-native-modal-datetime-picker ( DateTimePicker 기반 제작 )
저는 좀더 기능이 좋은 react-native-modal-datetime-picker 를 선택하였습니다.
https://github.com/mmazzarolo/react-native-modal-datetime-picker
해당 링크에서 예제 코드를 살펴볼 수 있습니다.
> expo install react-native-modal-datetime-picker @react-native-community/datetimepicker
expo 프로젝트이기 때문에 expo CLI를 이용하여 설치해주었습니다.
예제코드에서 복붙해서 편하게 작성하였습니다 ㅎ ㅎ.
라이브러리를 import 해줍니다.
마찬가지로 예제에서 복붙한 함수를 넣어줍니다.
DatePircker 모달창을 열고 닫고, 날짜를 선택했을 때의 함수입니다.
날짜를 선택했을때 해당날짜가 선택되도록 setSelectedDate
함수를 추가해주었습니다.
ListHeaderComponent 의 날짜부분을 터치했을 때 해당 모달창이 뜨도록 onPress
를 추가해주었습니다.
return 부분에 DateTimePickerModal
을 추가해주었습니다.
마찬가지로 예제코드에 나와있어서 그대로 복붙!
날짜 버튼을 클릭해서 원하는 날짜로 자유자재로 이동할 수 있게되었습니다.
함수를 추가해주었습니다.
좌측버튼에서는 dayjs의 subtract
를 이용하여 현재날짜에서 한 달을 빼서 newSelectedDate
에 담아 setSelecteDate 해주었습니다.
우측버튼에서는 add
함수를 이용해서 더해주었습니다.
버튼을 눌렀을 때 이전달과 다음달로 잘 이동되는 모습입니다.
TodoList에 대한 로직과 UI를 작성하기 전에 혼란을 주지 않도록 리팩토링을 먼저한 후에 진행하려합니다.
지금도 5개의 함수를 사용하고 있는데, 더 추가를 하게되면 가독성이 안 좋아지기 때문에 use-calendar 라는 hook
을 만들어서 필요한 함수만 꺼내서 사용할 수 있게 리팩토링 하겠습니다.
src
폴더에 hook
폴더를 만들어주었고, use-calendar.js파일을 생성해주었습니다.
App.js에 선언해두었던 useState와 함수들을 가져왔습니다.
좌우 버튼의 경우 subtract1Month, add1Month 로 새로 이름을 지어 추가 선언해주었습니다.
App.js에서 hook
에서 함수를 꺼내서 사용할 수 있도록 해주었습니다.
사진에는 나와있지 않지만 use-calendar.js 파일도 import
해주었습니다.
src
폴더의 hook
폴더 안에 use-todo-list.js 파일을 생성해주었습니다.
todoList를 state
로 설정해줍니다.
초기값은 원래 비어있어야하지만 우선 예시로 defaultTodoList를 작성하여 넣어주었습니다.
id
, content
, date
, isSuccess
의 속성을 갖고있습니다.
content를 입력할 input을 state
를 추가해주었습니다.
Todo를 추가할 addTodo
로직을 추가해주었고,
새로운 Todo를 추가할 newTodoList 을 만들어주었습니다.
Todo의 속성으로 필요한 id
의 경우 현재 등록된 Todo의 마지막 id
를 구하고 + 1 해주었습니다.
content
의 경우 입력한 input이 되고, date
는 선택한 날짜인 selectedDate가 되어야합니다.
App.js에서 Hook
을 사용하면서 argument
로 selecteDate를 전달해줍니다.
isSuccess
는 처음 Todo를 추가할 때는 성공하지 않은 상태이므로 false를 넣어주었습니다.
removeTodo를 생성해주었고, 삭제할 Todo의 id
를 미리 알아야하기 때문에 todoId를 받아줍니다.
newTodoList에 기존에 있는 리스트에서 filter
를 통해 삭제할 todo가 item
으로 오고, argument
로 넘어온 todoId가 아닌 것만 필터링을 해줍니다.
todoId의 id
가 2
라면, 2
를 제외한 todo들이 세팅됩니다.
새로운 newTodoList를 setTodoList
해줍니다.
Todo를 성공할 수도 있지만, 실패할 수도 있기 때문에 toggleTodo
로 이름을 지어주었습니다.
removeTodo와 마찬가지로 어떤 Todo의 상태를 변경시킬지 id
를 미리 알아야하기 때문에 todoId를 받아줍니다.
토글로직의 경우에는 TodoList의 배열은 변경되지 않고, 특정 오브젝트의 isSuccess만 변경합니다.
todoList를 map
을 돌려줍니다.
기존의 todo와 argument
로 받아온 todoId가 같지 않으면 기존 todo 그대로 반환해줍니다.
나머지는 todo를 그대로 사용하고, isSuccess
만 반대값으로 설정해줍니다.
마지막으로 변경된 newTodoList를 setTodoList
해줍니다.
UI를 추가하기 전에 배경화면을 설정해주도록 하겠습니다.
배경화면은 안전한 영역 바깥에서도 그려주어야하기 때문에 최상단이 SafeAreaView가 아닌 View여야 합니다.
기존 SafeAreaView를 View로 변경해주었고, Image
태그를 추가하여 이미지를 추가해주었습니다.
기존에는 paddingTop
을 StyleSheet에서 주고있었는데, 배경이미지에도 paddingTop
이 적용되기 때문에, StyleSheet에서 제거해주고, return의 FlatList
에만 적용해주었습니다.
예쁜 배경이 적용되니 아주 흡족합니다 💟
사용된 배경이미지의 저작권은
Sailor🌙 정아💜
님에게 있습니다.
무단으로 도용 및 불법으로 복사(캡처)하여 사용을 금지합니다.
App.js의 return부분에 FlatList
를 추가하여 todoList의 content
를 반환해주었습니다.
hook
에서 todoList도 받아와야합니다!
use-todo-list.js hook
의 return을 작성해주지 않았어서 마저 추가해주었습니다.
defaultTodoList로 작성해두었던 data
가 나타난 모습입니다.
todo의 Header
는 캘린더가 되어야하기 때문에
캘린더와 관련된 Header
컴포넌트를 따로 묶어주었습니다.
src
폴더에 Calendar.js 파일을 만들어주었고, App.js의 캘린더관련 부분을 옮겨주었습니다.
import React from 'react';
import dayjs from 'dayjs';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { getStatusBarHeight } from 'react-native-iphone-x-helper';
import { SimpleLineIcons } from '@expo/vector-icons';
import { getDayColor, getDayText } from './util';
const statusBarHeight = getStatusBarHeight(true);
const columnSize = 35;
const Column = ({
text,
color,
opacity,
disabled,
onPress,
isSelected,
}) => {
return (
<TouchableOpacity
disabled = {disabled}
onPress = { onPress }
style={{
width: columnSize,
height: columnSize,
justifyContent: "center",
alignItems: "center",
backgroundColor: isSelected ? "#c2c2c2" : "transparent",
borderRadius: columnSize / 2,
}}>
<Text style={{ color, opacity }}>{text}</Text>
</TouchableOpacity>
)
}
const ArrowButton = ({ iconName, onPress }) => {
return (
<TouchableOpacity onPress={ onPress } style={{ paddingHorizontal: 20, paddingVertical: 15 }}>
<SimpleLineIcons name={ iconName } size={15} color="#404040" />
</TouchableOpacity>
)
}
export default ({
columns,
selectedDate,
onPressLeftArrow,
onPressHeaderDate,
onPressRightArrow,
onPressDate,
}) => {
const ListHeaderComponent = () => {
const currentDateText = dayjs(selectedDate).format("YYYY.MM.DD.");
return (
<View>
{/* < YYYY.MM.DD. > */}
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "center" }}>
<ArrowButton iconName="arrow-left" onPress={onPressLeftArrow} />
<TouchableOpacity onPress={onPressHeaderDate}>
<Text style={{ fontSize: 20, color: "#404040" }}>{currentDateText}</Text>
</TouchableOpacity>
<ArrowButton iconName="arrow-right" onPress={onPressRightArrow} />
</View>
{/* 일 ~ 토 */}
<View style={{ flexDirection: "row" }}>
{[0, 1, 2, 3, 4, 5, 6].map(day => {
const dayText = getDayText(day);
const color = getDayColor(day);
return (
<Column
key={`day-${day}`}
text={dayText}
color={color}
opacity={1}
disabled={true}
/>
)
})}
</View>
</View>
)
}
const renderItem = ({ item: date }) => {
const dateText = dayjs(date).get('date');
const day = dayjs(date).get('day');
const color = getDayColor(day);
const isCurrentMonth = dayjs(date).isSame(selectedDate, 'month');
const onPress = () => onPressDate(date);
const isSelected = dayjs(date).isSame(selectedDate, 'date');
return (
<Column
text={dateText}
color={color}
opacity={isCurrentMonth ? 1 : 0.4}
onPress={onPress}
isSelected={isSelected}
/>
)
}
return (
<FlatList
data={columns}
scrollEnabled={false}
contentContainerStyle = {{ paddingTop : statusBarHeight }}
keyExtractor={(_, index) => `column-${index}`}
numColumns={7}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
/>
)
}
그리고 App.js에서 Calendar.js로 옮긴 부분을 삭제해주었습니다.
import { useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View, SafeAreaView, TouchableOpacity, Image } from 'react-native';
import dayjs from 'dayjs';
import DateTimePickerModal from "react-native-modal-datetime-picker";
import { runPracticeDayjs } from './src/practice-dayjs';
import { getCalendarColumns, getDayColor, getDayText } from './src/util';
import { useCalendar } from './src/hook/use-calendar';
import { useTodoList } from './src/hook/use-todo-list';
import Calendar from './src/Calendar';
export default function App() {
const now = dayjs();
const {
selectedDate,
setSelectedDate,
isDatePickerVisible,
showDatePicker,
hideDatePicker,
handleConfirm,
subtract1Month,
add1Month,
} = useCalendar(now);
const {
todoList
} = useTodoList(selectedDate);
const columns = getCalendarColumns(selectedDate);
const onPressLeftArrow = subtract1Month;
const onPressHeaderDate = showDatePicker;
const onPressRightArrow = add1Month;
const onPressDate = setSelectedDate;
useEffect(() => {
runPracticeDayjs();
}, []);
return (
<View style={styles.container}>
<Image
source={{
uri: "https://raw.githubusercontent.com/bi-sz/todo-calendar/master/src/image/background1.jpg",
}}
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
/>
<Calendar
columns={columns}
selectedDate={selectedDate}
onPressLeftArrow={onPressLeftArrow}
onPressHeaderDate={onPressHeaderDate}
onPressRightArrow={onPressRightArrow}
onPressDate={onPressDate}
/>
<FlatList
data={todoList}
//ListHeaderComponent={ListHeaderComponent}
renderItem={({ item: todo }) => {
return (
<Text>{todo.content}</Text>
)
}}
/>
<DateTimePickerModal
isVisible={isDatePickerVisible}
mode="date"
onConfirm={handleConfirm}
onCancel={hideDatePicker}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
Calendar.js 컴포넌트를 사용해서
<Calendar
columns={columns}
selectedDate={selectedDate}
onPressLeftArrow={onPressLeftArrow}
onPressHeaderDate={onPressHeaderDate}
onPressRightArrow={onPressRightArrow}
onPressDate={onPressDate}
/>
캘린더 부분을 받아왔고, 변경내용이 없으면서도 많아서 자세한 과정은 생략하고 전체 코드를 첨부하였습니다.. ㅎㅎ
리팩토링만 진행하였기 때문에 결과는 같습니다.
return 부분에서 주석처리해놨던 ListHeaderComponent={ListHeaderComponent}
부분을 살려주고, 그 위에 있던 Calendar 컴포넌트를 잘라냈습니다.
잘라낸 Calendar를 ListHeaderComponent
로 옮겨주었습니다.
Todo의 Header
로 캘린더가 잘 적용된 모습입니다.
리팩토링하면서 paddingTop
을 넣어주었던 부분이 깨졌네요.
Calendar.js의 return 부분에 있던 contentContainerStyle
을 잘라냅니다.
App.js의 FlatList
부분으로 옮겨주었습니다.
Calendar.js로 옮겼던 함수와 라이브러리도 다시 import
해줍니다.
다시 정상적으로 돌아온 모습입니다!
App.js의 FlatList
부분에 있던 renderItem을 위에 따로 정의해주려합니다.
잘라낸 renderItem을 붙여넣고, style
을 추가해주었습니다.
> import { Ionicons } from '@expo/vector-icons';
아이콘은 Ionicons 에서 import
해주었습니다.
스타일링까지 적용된 모습입니다.
defaultTodoList 에 넣어두었던 data
중에서 퇴근하기는 false로 해 두어서 색상이 다르게 표시된 모습입니다.
ListHeaderComponent 를 View
로 감싸주고, 점과 Margin을 주어 스타일링 해주었습니다.
src
폴더에 AddTodoInput.js 파일을 생성해주었습니다.
useCalendarhook
에서 미리 만들어두었으니 그대로 이용해줍니다.
App.js의 return
부분에서 FlatList
아래에 AppTodoInput을 추가해주었습니다.
FlatList
의 footer로 붙어도 되지만 따로 구분한 이유는, Input
부분은 FlatList
의 내용이 많아져 스크롤되더라도 항상 보여야하기 때문에 구분해주었습니다.
구분하기위해 배경색을 노란색으로 넣어주어서 잘 보입니다!
Input
부분 높이를 220으로 주었는데, 기존에 있던 util.js에서 따로 관리하려 합니다.
src
폴더의 util.js 파일에 상단바
와 하단
, 그리고 Item_WIDTH
를 정의해주었습니다.
bottomSpace와 ITEM_WIDTH를 util
에서 import하여 사용해줍니다.
스타일링을 좀 더 추가해주었고, TouchableOpacity 요소를 추가하여 plus
아이콘도 추가해주었습니다.
UI가 어느정도 완성이 되었습니다.
Input을 눌렀을 때 키보드가 올라오면서 View를 가리게되는 경우를 보완하려합니다.
키보드를 피하는 뷰 라이브러리입니다.
KeyboardAvoidingView로 묶어주었습니다.
화면의 양옆 부분을 터치했을때도 키보드가 내려가도록 수정해주었습니다.
키보드가 나와도 Input 부분을 가리지 않고,
키보드 외에 어떤 영역을 터치해도 키보드가 내려가는 모습입니다.
실제 Todo 기능이 적용되는 부분은 다음에 이어서 작성하겠습니다.