ReactNative Project - 투두리스트 + 달력 [2]

bi_sz·2023년 10월 26일
0

ReactNative

목록 보기
16/37
post-thumbnail

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) 로 터치한 dateselectedDate에 담아줍니다.

selectedDate가 바뀔 때마다 인지할 수 있도록 useEffect를 생성해주었습니다.

처음 랜더링될 때 현재 날짜인 10월 26일이 찍혔고, 캘린더의 날짜를 선택하면 해당 날짜가 selectedDate에 담기는 것을 확인했습니다.


선택된 날짜를 구분하기 위해 조금 수정해주겠습니다.

Coulmn 컴포넌트에 isSelected 속성을 추가해주었고,
stylebackgroundColor 속성과 borderRadius 속성을 추가해주었습니다.

rederItem에서 isSelecteddayjs를 이용하여 dateselectedDate가 같은지 비교하여 구해줍니다.

처음 랜더링될 때 현재 날짜에 회색 원 표시가 나타났습니다.
날짜를 선택하면, 선택한 날짜가 표시됩니다.


일,월,년 별로 날짜 선택

날짜를 선택하는 것으로 이전달, 다음달 정도로 날짜를 옮겨다닐 수 있지만, 몇개월 전 후 혹은 몇년 전 으로도 날짜를 옮겨다닐 수 있어야합니다.

날짜 선택 라이브러리

  • DateTimePicker ( RN에서 제공)
  • react-native-modal-datetime-picker ( DateTimePicker 기반 제작 )

react-native-modal-datetime-picker

저는 좀더 기능이 좋은 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을 추가해주었습니다.
마찬가지로 예제코드에 나와있어서 그대로 복붙!

날짜 버튼을 클릭해서 원하는 날짜로 자유자재로 이동할 수 있게되었습니다.


월 별 이동

함수를 추가해주었습니다.

좌측버튼에서는 dayjssubtract를 이용하여 현재날짜에서 한 달을 빼서 newSelectedDate에 담아 setSelecteDate 해주었습니다.

우측버튼에서는 add함수를 이용해서 더해주었습니다.

버튼을 눌렀을 때 이전달과 다음달로 잘 이동되는 모습입니다.


Refactoring

TodoList에 대한 로직과 UI를 작성하기 전에 혼란을 주지 않도록 리팩토링을 먼저한 후에 진행하려합니다.

지금도 5개의 함수를 사용하고 있는데, 더 추가를 하게되면 가독성이 안 좋아지기 때문에 use-calendar 라는 hook을 만들어서 필요한 함수만 꺼내서 사용할 수 있게 리팩토링 하겠습니다.

src폴더에 hook 폴더를 만들어주었고, use-calendar.js파일을 생성해주었습니다.

App.js에 선언해두었던 useState와 함수들을 가져왔습니다.
좌우 버튼의 경우 subtract1Month, add1Month 로 새로 이름을 지어 추가 선언해주었습니다.

App.js에서 hook 에서 함수를 꺼내서 사용할 수 있도록 해주었습니다.
사진에는 나와있지 않지만 use-calendar.js 파일도 import 해주었습니다.


useTodoList 제작

src 폴더의 hook 폴더 안에 use-todo-list.js 파일을 생성해주었습니다.

todoListstate로 설정해줍니다.

초기값은 원래 비어있어야하지만 우선 예시로 defaultTodoList를 작성하여 넣어주었습니다.

id, content, date, isSuccess 의 속성을 갖고있습니다.


TodoList 추가 로직

content를 입력할 inputstate를 추가해주었습니다.

Todo를 추가할 addTodo 로직을 추가해주었고,
새로운 Todo를 추가할 newTodoList 을 만들어주었습니다.

Todo의 속성으로 필요한 id의 경우 현재 등록된 Todo의 마지막 id를 구하고 + 1 해주었습니다.

content의 경우 입력한 input이 되고, date는 선택한 날짜인 selectedDate가 되어야합니다.

App.js에서 Hook을 사용하면서 argumentselecteDate를 전달해줍니다.

isSuccess는 처음 Todo를 추가할 때는 성공하지 않은 상태이므로 false를 넣어주었습니다.


TodoList 삭제 로직

removeTodo를 생성해주었고, 삭제할 Todoid를 미리 알아야하기 때문에 todoId를 받아줍니다.

newTodoList에 기존에 있는 리스트에서 filter를 통해 삭제할 todoitem으로 오고, argument로 넘어온 todoId가 아닌 것만 필터링을 해줍니다.

todoIdid2 라면, 2를 제외한 todo들이 세팅됩니다.

새로운 newTodoListsetTodoList 해줍니다.


Todo 체크 로직

Todo를 성공할 수도 있지만, 실패할 수도 있기 때문에 toggleTodo로 이름을 지어주었습니다.

removeTodo와 마찬가지로 어떤 Todo의 상태를 변경시킬지 id를 미리 알아야하기 때문에 todoId를 받아줍니다.

토글로직의 경우에는 TodoList의 배열은 변경되지 않고, 특정 오브젝트의 isSuccess만 변경합니다.

todoListmap을 돌려줍니다.
기존의 todoargument로 받아온 todoId가 같지 않으면 기존 todo 그대로 반환해줍니다.

나머지는 todo를 그대로 사용하고, isSuccess 만 반대값으로 설정해줍니다.

마지막으로 변경된 newTodoListsetTodoList 해줍니다.


배경화면 설정

UI를 추가하기 전에 배경화면을 설정해주도록 하겠습니다.

배경화면은 안전한 영역 바깥에서도 그려주어야하기 때문에 최상단이 SafeAreaView가 아닌 View여야 합니다.

기존 SafeAreaViewView로 변경해주었고, Image태그를 추가하여 이미지를 추가해주었습니다.

기존에는 paddingTopStyleSheet에서 주고있었는데, 배경이미지에도 paddingTop이 적용되기 때문에, StyleSheet에서 제거해주고, returnFlatList에만 적용해주었습니다.

예쁜 배경이 적용되니 아주 흡족합니다 💟

사용된 배경이미지의 저작권은 Sailor🌙 정아💜 님에게 있습니다.
무단으로 도용 및 불법으로 복사(캡처)하여 사용을 금지합니다.


Todo UI

App.jsreturn부분에 FlatList를 추가하여 todoListcontent를 반환해주었습니다.

hook에서 todoList도 받아와야합니다!

use-todo-list.js hookreturn을 작성해주지 않았어서 마저 추가해주었습니다.

defaultTodoList로 작성해두었던 data가 나타난 모습입니다.


Calendar 리팩토링

todoHeader는 캘린더가 되어야하기 때문에

캘린더와 관련된 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}
/>

캘린더 부분을 받아왔고, 변경내용이 없으면서도 많아서 자세한 과정은 생략하고 전체 코드를 첨부하였습니다.. ㅎㅎ

리팩토링만 진행하였기 때문에 결과는 같습니다.


TodoList 헤더에 캘린더 넣어주기

return 부분에서 주석처리해놨던 ListHeaderComponent={ListHeaderComponent} 부분을 살려주고, 그 위에 있던 Calendar 컴포넌트를 잘라냈습니다.

잘라낸 CalendarListHeaderComponent 로 옮겨주었습니다.

TodoHeader로 캘린더가 잘 적용된 모습입니다.

리팩토링하면서 paddingTop을 넣어주었던 부분이 깨졌네요.

Calendar.jsreturn 부분에 있던 contentContainerStyle 을 잘라냅니다.

App.jsFlatList 부분으로 옮겨주었습니다.

Calendar.js로 옮겼던 함수와 라이브러리도 다시 import 해줍니다.

다시 정상적으로 돌아온 모습입니다!


App.jsFlatList 부분에 있던 renderItem을 위에 따로 정의해주려합니다.

잘라낸 renderItem을 붙여넣고, style을 추가해주었습니다.

> import { Ionicons } from '@expo/vector-icons';

아이콘은 Ionicons 에서 import 해주었습니다.

스타일링까지 적용된 모습입니다.
defaultTodoList 에 넣어두었던 data 중에서 퇴근하기는 false로 해 두어서 색상이 다르게 표시된 모습입니다.

ListHeaderComponentView 로 감싸주고, 점과 Margin을 주어 스타일링 해주었습니다.

Todo 추가 UI

src 폴더에 AddTodoInput.js 파일을 생성해주었습니다.

useCalendarhook에서 미리 만들어두었으니 그대로 이용해줍니다.

App.jsreturn 부분에서 FlatList 아래에 AppTodoInput을 추가해주었습니다.

FlatListfooter로 붙어도 되지만 따로 구분한 이유는, Input 부분은 FlatList의 내용이 많아져 스크롤되더라도 항상 보여야하기 때문에 구분해주었습니다.

구분하기위해 배경색을 노란색으로 넣어주어서 잘 보입니다!


Input 부분 높이를 220으로 주었는데, 기존에 있던 util.js에서 따로 관리하려 합니다.

src 폴더의 util.js 파일에 상단바하단, 그리고 Item_WIDTH를 정의해주었습니다.

bottomSpaceITEM_WIDTHutil에서 import하여 사용해줍니다.

스타일링을 좀 더 추가해주었고, TouchableOpacity 요소를 추가하여 plus아이콘도 추가해주었습니다.

UI가 어느정도 완성이 되었습니다.

Input을 눌렀을 때 키보드가 올라오면서 View를 가리게되는 경우를 보완하려합니다.

keyBoardAvoidingView

키보드를 피하는 뷰 라이브러리입니다.

KeyboardAvoidingView로 묶어주었습니다.

화면의 양옆 부분을 터치했을때도 키보드가 내려가도록 수정해주었습니다.

키보드가 나와도 Input 부분을 가리지 않고,
키보드 외에 어떤 영역을 터치해도 키보드가 내려가는 모습입니다.


실제 Todo 기능이 적용되는 부분은 다음에 이어서 작성하겠습니다.

profile
https://li-yo.tistory.com/ 티스토리 블로그 이전 하였습니다.

0개의 댓글