프로젝트/할일목록 앱 (RN)

5. 할일목록 날짜별로 필터링해서 보여주기 & 할일목록 최신순 정렬해서 보여주기

syleemomo 2023. 9. 25. 16:46
728x90

* 페이지 확장을 위한 코드 리팩토링하기

import React, { useState, useEffect } from 'react'
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import Icon from 'react-native-vector-icons/MaterialIcons'

import HomeScreen from './screens/HomeScreen';
import CalendarScreen from './screens/CalendarScreen';
import DashBoardSceen from './screens/DashBoardScreen';
import SettingsScreen from './screens/SettingsScreen';

import DropdownCategory from './components/DropdownCategory'

import { // 할일목록 조회를 위한 유틸리티 함수 추가
  getCollection,
} from './apis/firebase'


// const Stack = createNativeStackNavigator()
const Tab = createBottomTabNavigator()

export default function App() {
  const [todos, setTodos] = useState([]) // 할일목록 상태 (HomeScreen -> App 이동)
  const [loading, setLoading] = useState(true) // 할일목록 상태 (HomeScreen -> App 이동)
  const [caretType, setCaretType] = useState(false)
  // console.log("캐럿[app]: ", caretType)

  useEffect(() => { // 할일목록 조회 (HomeScreen -> App 이동)
    function onResult(querySnapshot){
      const list = []
      querySnapshot.forEach(doc => {
        console.log(doc.data())
        list.push({
          ...doc.data(),
          id: doc.id,
        })
      })
      setTodos(list)
      setLoading(false)
    }
    function onError(error){
      console.error(`${error} occured when reading todos`)
    }
    return getCollection('todos', 
                          onResult, onError,
                          null, null, null)
  }, [])


  return (
    <NavigationContainer>
      <Tab.Navigator initialRouteName = "Home" screenOptions={{
        tabBarActiveTintColor: '#a8c8ffff',
        // tabBarStyle: {
        //   backgroundColor: '#333'
        // }
      }}>
        <Tab.Screen name="Home" children={(props) => <HomeScreen {...props} caretType={caretType} setCaretType={setCaretType} todos={todos} loading={loading}/>} options={{
          title: '홈',
          tabBarIcon: ({ color, size }) => <Icon name="home" color={color} size={size}/>,
          headerTitle: (props) => <DropdownCategory {...props} caretType={caretType} setCaretType={setCaretType}/>,
          headerStyle: {
            backgroundColor: '#a8c8ffff',
          },
          headerTitleStyle: {
            fontWeight: 'bold',
            color: '#fff'
          },
        }}/>
        <Tab.Screen name="Calendar" component={CalendarScreen} options={{
          title: '달력',
          tabBarIcon: ({ color, size }) => <Icon name="calendar-month" color={color} size={size}/>
        }}/>
        <Tab.Screen name="DashBoard" component={DashBoardSceen} options={{
          title: '통계',
          tabBarIcon: ({ color, size }) => <Icon name="dashboard" color={color} size={size}/>
        }}/>
        <Tab.Screen name="Settings" component={SettingsScreen} options={{
          title: '설정',
          tabBarIcon: ({ color, size }) => <Icon name="settings" color={color} size={size}/>
        }}/>
      </Tab.Navigator>
    </NavigationContainer>
  );
}

App 컴포넌트를 위와 같이 수정한다. 

import { // 할일목록 조회를 위한 유틸리티 함수 추가
  getCollection,
} from './apis/firebase'

기존에는 HomeScreen 컴포넌트에서 할일목록을 조회하였다. 하지만 할일목록은 캘린더 탭에서도 사용하고, 대쉬보드 탭에서도 통계분석을 위하여 할일목록을 이용한다. 이때 모든 탭에서 업데이트된 최신 할일목록을 조회하기 위해서는 App 컴포넌트에서 조회한 다음에 공통적으로 모든 탭에 전달해주는 것이 데이터 일관성을 위하여 좋다. 그래서 HomeSceen 컴포넌트에서 조회를 하는 기능을 App 컴포넌트로 이동하도록 한다. 

const [todos, setTodos] = useState([]) // 할일목록 상태 (HomeScreen -> App 이동)
const [loading, setLoading] = useState(true) // 할일목록 상태 (HomeScreen -> App 이동)

getCollection 함수 내부에서는 Cloud Firestore 의 onSnapshot 메서드를 이용하기 때문에 앱의 특정부분에서 할일목록을 추가/수정/삭제한 경우에만 다시 파이어베이스 데이터베이스에 접속하여 최신 할일목록을 가져온다. 또한, 최신 목록을 모든 탭에 주입해준다. 이렇게 하면 서버접속을 최소화할 수 있다. 만약 각 탭이 초기 렌더링할때마다 서버에 접속하게 되면 최신 데이터인지 보장하지 못하고, 서버도 여러번 접속하게 된다.

useEffect(() => { // 할일목록 조회 (HomeScreen -> App 이동)
    function onResult(querySnapshot){
      const list = []
      querySnapshot.forEach(doc => {
        console.log(doc.data())
        list.push({
          ...doc.data(),
          id: doc.id,
        })
      })
      setTodos(list)
      setLoading(false)
    }
    function onError(error){
      console.error(`${error} occured when reading todos`)
    }
    return getCollection('todos', 
                          onResult, onError,
                          null, null, null)
  }, [])

 

* Cloud Firestore 유틸리티 함수 추가 및 수정하기

import firestore from '@react-native-firebase/firestore';

const getRef = (collections) => {
    return firestore().collection(collections);
}
export const addData = async (collections, data) => {
    await getRef(collections).add(data)
    console.log(`${collections} : ${JSON.stringify(data)} added in firestore!`)
}
export const getCollection = (collections, onResult, onError, query, order, limit) => {
    let ref = getRef(collections)
    
    // 조건쿼리
    if(query && query.exists && query.condition && query.condition.length !== 0){
        for(let cond of query.condition){ // Multiple Query
            ref = ref.where(...cond)
        }
    }
    if(order && order.exists && order.condition && order.condition.length !== 0){
        ref = ref.orderBy(...order.condition)
    }
    if(limit && limit.exists && limit.condition){
        ref = ref.limit(limit.condition)
    }
    return ref.onSnapshot(onResult, onError)
}
export const getCurrentTime = () => {
    return firestore.FieldValue.serverTimestamp() // 파이어베이스 해당서버의 로컬시각
}
export const changeTimeFormat = (date) => { // 시간포멧변경 (Date -> Timestamp)
    return firestore.Timestamp.fromDate(date)
}

apis > firebase.js 파일을 위와 같이 수정한다. 

// 조건쿼리
if(query && query.exists && query.condition && query.condition.length !== 0){
    ref = ref.where(...query.condition)
}

기존의 조건쿼리는 where 메서드를 한번만 사용하기 때문에 한번의 쿼리밖에 수행하지 못한다.

// 조건쿼리
if(query && query.exists && query.condition && query.condition.length !== 0){
    for(let cond of query.condition){ // Multiple Query
        ref = ref.where(...cond)
    }
}

현재는 반복문을 이용하여 쿼리를 여러번 수행할 수 있도록 기능을 확장하였다. query.condition 은 이제 하나의 조건을 설정하는 것이 아니라 여러개의 조건을 추가할 수 있다. 이렇게 하면 2차원 배열을 사용하여 쿼리조건을 설정할 수 있다. 

export const changeTimeFormat = (date) => { // 시간포멧변경 (Date -> Timestamp)
    return firestore.Timestamp.fromDate(date)
}

시간포맷을 변경하는 유틸리티 함수이다. 자바스크립트의 Date 객체 포맷을 Cloud Firestore 의 Timestamp 포맷으로 변경해준다. 

 

* 날짜와 시각을 계산하는 유틸리티 함수 구현하기 

export const getToday = (date) => {
    const {year, month, day} = getDayWithoutTime(date)
    return new Date(year, month-1, day)
}
export const getTomorrow = (today) => {
    return new Date(today.setDate(today.getDate()+1))
}
export const getDayWithoutTime = (date) => {
    const year = date.getFullYear()
    const month = date.getMonth() + 1
    const day = date.getDate()
    return {year,month,day}
}

utils > time.js 파일을 생성하고 위와 같이 작성한다. 

export const getToday = (date) => {
    const {year, month, day} = getDayWithoutTime(date)
    return new Date(year, month-1, day)
}

현재 시각(date)으로부터 년/월/일만 추출한 다음 새로운 Date 객체를 생성하고 반환한다. 이렇게 하면 시각 부분은 자정(0시)로 설정된다. 년/월/일을 추출할때 월에 1을 더해주었기 때문에 새로운 월을 설정할때는 반대로 1을 빼주어야 한다. 

export const getTomorrow = (today) => {
    return new Date(today.setDate(today.getDate()+1))
}

해당 코드는 오늘날짜(today)를 기준으로 날짜에 1을 더하여 내일날짜를 계산한 다음에 반환한다. 입력/출력되는 날짜의 시각은 모두 자정(0시)이다. 

export const getDayWithoutTime = (date) => {
    const year = date.getFullYear()
    const month = date.getMonth() + 1
    const day = date.getDate()
    return {year,month,day}
}

현재 시각(date)로부터 년/월/일만 추출하고 시간대는 버린다. 

 

* 전달받은 할일목록을 선택한 날짜로 필터링하고 정렬해서 보여주기 - 홈화면

import React, { useState, useEffect, useRef } from 'react' // 카테고리 저장을 위한 useRef 임포트(수정)
import { 
  addData,
  getCurrentTime,
  // getCollection // 주석처리
} from '../apis/firebase'

import { // 오늘과 내일 날짜기준을 계산하는 유틸리티 함수
  getToday,
  getTomorrow
} from '../utils/time' 

import { 
  SafeAreaView, 
  View, Text, 
  StyleSheet, 
  StatusBar, 
  Keyboard, 
  FlatList,
  TouchableHighlight 
} from 'react-native'

import DateHeader from '../components/DateHeader'
import Default from '../components/Default'
import TodoInsert from '../components/TodoInsert'
import TodoList from '../components/TodoList'
import DropdownItem from '../components/DropdownItem'

function HomeScreen({ navigation, caretType, setCaretType, todos, loading, route }){ // 필요한 데이터 추가 (todos, loading, route)
  const categories = ['자기계발', '업무', '오락', '여행', '연애', 'IT', '취미']
  const [todoText, setTodoText] = useState('')
  const [warning, setWarning] = useState(false)

  // 오늘/내일의 날짜를 기준으로 할일목록을 필터링하고 정렬함
  const category = useRef('') // 카테고리 변수
  const date = (route.params && route.params.date) ? new Date(route.params.date) : new Date()
  const today = getToday(date) // 시간제외
  const tomorrow = getTomorrow(getToday(date))
  const todosToday = todos.filter(todo => todo.createdAt?.toDate() >= today && todo.createdAt?.toDate() < tomorrow)
  const todosTodayLatest = [...todosToday] // 원본복사
  todosTodayLatest.sort((a, b) => b.createdAt.seconds - a.createdAt.seconds) // 최신순 정렬

  console.log("현재 선택날짜: ", date)
  console.log("날짜비교: ", date.getTime(), today.getTime() != getToday(new Date()).getTime())

  const onInsertTodo = async (trimedText) => {
    if(!category.current){ // 카테고리를 선택하지 않은 경우
      setTodoText('카테고리를 먼저 선택해주세요!')
      setWarning(true)
      return 
    }
    if(trimedText && trimedText.length > 3){ // 최소 글자수 제한
      if(todos.filter(todo => todo.title === trimedText).length > 0){
        setTodoText('중복된 할일입니다.')
        setWarning(true)
      }else{
        const newTodo = {
          title: trimedText,
          category: category.current || '자기계발', // 선택한 카테고리 설정 (수정)
          isDone: false,
          createdAt: getCurrentTime(), // 클라이언트 기준이 아니라 서버기준 저장시각
        }
        await addData('todos', newTodo)
        Keyboard.dismiss() // 추가버튼 클릭시 키보드 감추기 
        setTodoText('') // 입력창 초기화
        category.current = '' // 카테고리 초기화 (추가)
      }
    }else{
      console.log('3자 이상 입력하세요!')
      setTodoText('3자 이상 입력하세요!')
      setWarning(true)
    }
  }

  const closeDropdown = () => {
    caretType && setCaretType(false)
  }
  const selectCategory = (item, e) => { // 카테고리 드롭다운 선택시 (추가)
    console.log("카테고리: ", item)
    closeDropdown()
    category.current = item 
  }
  const handleOutSideOfMenu = (e) => {
    console.log('홈화면을 터치하셨습니다.')
    closeDropdown()
  }

  useEffect(() => navigation.addListener('focus', () => console.log('페이지 로딩')), [])

  useEffect(() => navigation.addListener('blur', () => console.log('페이지 벗어남')), [])
  
  if (loading) {
    return (
      <View>
        <Text>로딩중...</Text>
      </View> 
    )
  }

  return (
    <SafeAreaView 
        style={styles.block} 
        onTouchStart={handleOutSideOfMenu}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      
        {caretType 
        && (
            <View 
              style={styles.dropdownShadow}
              onTouchStart={(e) => { // 터치 시작점 설정 : 캡쳐링 방지 (추가)
                console.log('여기를 지나침')
                e.stopPropagation() // 터치 버블링 방지
              }}
              >
              <FlatList
                data={categories} 
                keyExtractor={item => item}
                renderItem={({item}) => (
                  <DropdownItem category={item} selectCategory={(e) => selectCategory(item, e)}/> // 아이템 각각의 뷰 화면 : 카테고리 선택시 이벤트핸들러 함수 등록 (수정)
                )}
                style={styles.dropdownList}
              />
          </View>
        )}
        <DateHeader date={date}/>
        {/* 해당날짜의 최순순으로 정렬된 할일목록 */}
        {todosTodayLatest.length === 0 ? 
            <Default/> : 
            <TodoList todos={todosTodayLatest} 
        />}
        {/* 필터링된 할일목록의 날짜와 현재 날짜가 동일하지 않은 경우 */}
        <TodoInsert 
          onInsertTodo={onInsertTodo} 
          todoText={todoText} 
          setTodoText={setTodoText} 
          warning={warning} 
          setWarning={setWarning}
          disabled={today.getTime()!==getToday(new Date()).getTime()}/> 
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
  },
  dropdownList: {
    padding: 5
  },
  dropdownShadow: {
    shadowOffset: { width: 0, height: 20 },
    shadowColor: '#000',
    shadowOpacity: 0.25,
    backgroundColor : "#fff", // invisible color
    zIndex: 1,
    elevation: 1,
    position: 'absolute',
    top: -15,
    borderRadius: 5,
    margin: 15
  }
})
export default HomeScreen

HomeScreen 컴포넌트를 위와 같이 수정한다. 

import { 
  addData,
  getCurrentTime,
  // getCollection // 주석처리
} from '../apis/firebase'

홈 화면에서는 이제 전체 할일목록을 조회하지 않으므로 getCollection 함수는 주석처리한다.

import { // 오늘과 내일 날짜기준을 계산하는 유틸리티 함수
  getToday,
  getTomorrow
} from '../utils/time'

전체 할일목록에서 필터링할 날짜 기준을 계산하기 위한 유틸리티 함수를 임포트한다.

function HomeScreen({ navigation, caretType, setCaretType, todos, loading, route }){ // 필요한 데이터 추가 (todos, loading, route)
    // 중략
}

App 컴포넌트로부터 전체 할일목록과 로딩상태에 대한 props 를 추가적으로 전달받는다. 또한, 캘린더 탭에서 특정 날짜 선택시 홈화면으로 이동하면서 선택한 날짜를 전달하는데 이때 route 객체를 이용하여 캘린더 탭에서 넘어온 날짜를 조회한다. 

const date = new Date() // 주석처리
const categories = ['자기계발', '업무', '오락', '여행', '연애', 'IT', '취미']
const [todos, setTodos] = useState([]) // 주석처리
const [todoText, setTodoText] = useState('')
const [warning, setWarning] = useState(false)
const [loading, setLoading ] = useState(true) // 주석처리

기존의 코드를 아래와 같이 변경한다. 전체 할일목록을 저장하는 todos 상태와 loading 상태는 App 컴포넌트로 이동하였으므로 주석처리하거나 제거한다. 

const categories = ['자기계발', '업무', '오락', '여행', '연애', 'IT', '취미']
const [todoText, setTodoText] = useState('')
const [warning, setWarning] = useState(false)

우선 페이지가 처음 로딩된 경우 오늘 날짜를 date 변수에 저장한다. 왜냐하면 초기 로딩시 오늘 날짜에 대한 할일목록을 보여주기 위함이다. 하지만 캘린더 탭에서 조회하고 싶은 날짜를 선택한 경우 route.params 에 선택한 날짜가 담겨서 홈화면으로 전달된다. 초기 렌더링시에는 route.params 는 undefined 이므로 아래와 같이 에러처리를 해준다.  

const date = (route.params && route.params.date) ? new Date(route.params.date) : new Date()
const today = getToday(date) // 시간제외
const tomorrow = getTomorrow(getToday(date))
const todosToday = todos.filter(todo => todo.createdAt?.toDate() >= today && todo.createdAt?.toDate() < tomorrow)
const todosTodayLatest = [...todosToday] // 원본복사
todosTodayLatest.sort((a, b) => b.createdAt.seconds - a.createdAt.seconds) // 최신순 정렬

console.log("현재 선택날짜: ", date)
console.log("날짜비교: ", date.getTime(), today.getTime() != getToday(new Date()).getTime())

getToday 와 getTomorrow 를 이용하여 필터링할 날짜 기준을 계산한다. 예를 들어, 초기 렌더링시에는 조회하는 현재 시각이 date 변수에 담기는데 이를 그대로 필터링 기준으로 사용하면 안된다. 만약 조회하는 시각이 2023년 9월 25일 저녁인데 해당 기준 그대로 할일목록을 필터링하면 9월 25일 할일목록 중에서 저녁에 작성한 할일목록만 가져오게 된다. 그러므로 9월 25일 저녁에 조회하더라도 new Date() 를 그대로 사용하지 않고 getToday 함수를 이용하여 시각을 9월 25일 자정(0시)로 계산해준다. getTomorrow 함수는 getToday 로 계산된 오늘 자정시각을 기준으로 내일 자정시각을 계산해준다. 예를 들어 오늘이 9월 25일 자정(0시)이면 getTomorrow 함수로 계산된 내일은 9월 26일 자정(0시)이 된다. 

필터링할 오늘/내일 시각을 이용하여 전체 할일목록에서 특정날짜의 할일목록만 추출한다. 이때 createdAt 속성은 Cloud Firestore 의 timestamp 포맷이므로 원할한 날짜비교를 위하여 toDate 함수를 이용하여 자바스크립트 Date 객체로 변경해준다. 

정렬을 위하여 특정 날짜의 할일목록(todosToday) 원본배열을 todosTodayLatest 변수에 복사한다. 그런 다음 sort 함수를 이용하여 할일이 생성된 시각을 기준으로 내림차순정렬하여 최신순으로 할일목록을 보여준다. 정렬시 createdAt 을 출력해보면 객체 형태이고 seconds 속성이 존재한다. toDate 함수로 포맷을 변경하지 않고 이를 이용해도 날짜 비교가 가능하여 해당 속성을 이용하여 정렬하였다.

useEffect(() => {
    function onResult(querySnapshot){
      const list = []
      querySnapshot.forEach(doc => {
        console.log(doc.data())
        list.push({
          ...doc.data(),
          id: doc.id,
        })
      })
      setTodos(list)

      if (loading) {
        setLoading(false)
      }
    }
    function onError(error){
      console.error(`${error} occured when reading todos`)
    }
    return getCollection('todos', 
                          onResult, onError,
                          null,
                          {exists: true, condition: ['createdAt', 'asc']},
                          null)
  }, [])

해당 코드블럭은 App 컴포넌트로 이동하였으므로 HomeScreen 컴포넌트에서는 제거하도록 한다.

{todos.length === 0 ? <Default/> : <TodoList todos={todos} />}

기존에는 날짜와 상관없이 전체 할일목록을 보여주지만 현재는 아래와 같이 특정 날짜로 필터링해서 해당 날짜의 할일목록만 보여주도록 한다. 

{/* 해당날짜 기준 최신순으로 정렬된 할일목록 */}
{todosTodayLatest.length === 0 ? 
    <Default/> : 
    <TodoList todos={todosTodayLatest} 
/>}

TodoInsert 컴포넌트에 disabled 속성을 추가하였다. 현재 날짜와 조회하려는 날짜가 일치하지 않는 경우에는 할일을 추가하지 못하도록 한다. 예를 들어 현재 날짜는 2023년 9월 25일인데 사용자가 보고 싶은 날짜가 2023년 9월 18일인 경우 해당 할일목록에서는 입력창과 추가버튼을 비활성화하여 할일을 추가하지 못하게 하였다.  

{/* 필터링된 할일목록의 날짜와 현재 날짜가 동일하지 않은 경우 */}
<TodoInsert 
  onInsertTodo={onInsertTodo} 
  todoText={todoText} 
  setTodoText={setTodoText} 
  warning={warning} 
  setWarning={setWarning}
  disabled={today.getTime()!==getToday(new Date()).getTime()}/>

 

* DateHeader 컴포넌트 수정하기

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
import { getDayWithoutTime } from '../utils/time' // 추가 

function DateHeader({ date }){
  const { year, month, day } = getDayWithoutTime(date) // 수정
  
  return (
    <View style={styles.container}>
      <Text style={styles.dateText}>{`${year}년 ${month}월 ${day}일`}</Text>
    </View>
  )
}
const styles = StyleSheet.create({
  container: {
    padding: 10,
    backgroundColor: '#a8c8ffff',
    zIndex: 0,
    elevation: 0
  },
  dateText: {
    fontSize: 30,
    color: 'white'
  }
})

export default DateHeader

components > DateHeader.js 파일을 위와 같이 수정한다. 

import { getDayWithoutTime } from '../utils/time'

현재시각으로부터 년/월/일을 추출하기 위하여 해당 함수를 임포트한다. 

const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()

기존에 DateHeader 컴포넌트에서 하던 작업은 getDayWithoutTime 함수에서 하므로 위 코드는 제거하고 아래와 같이 수정한다. 

const { year, month, day } = getDayWithoutTime(date)

 

* 특정상황에서 입력창 및 버튼 비활성화하기

import React from 'react'
import { 
    View, 
    Text,
    TextInput, 
    TouchableOpacity, 
    StyleSheet, 
    Keyboard
} from 'react-native'

function TodoInsert({ onInsertTodo, todoText, setTodoText, warning, setWarning, disabled }){ // disabled props 추가함
  const onPress = () => {
    const trimedText = todoText.trim()
    onInsertTodo(trimedText)
  }
  const handleChange = (text) => { 
    if (/\n/.test(text)) { // 엔터키 입력시 
      console.log("제출")
      onPress() // 할일추가
    }else {
      console.log("엔터키 아님")
      setTodoText(text)
      setWarning(false)
    }
  }
  const hideKeyboard = () => {
    Keyboard.dismiss()
  }
  console.log(todoText)
  return (
    <View style={styles.container}> 
      <TextInput 
        editable={!disabled} // disabled 값에 따른 입력창 비활성화
        selectTextOnFocus={!disabled} // disabled 값에 따른 입력창 비활성화
        placeholder={disabled ? 'X 할일을 작성할 수 없습니다 X' : '할일을 작성해주세요!'} // disabled 값에 따른 안내문구 변경
        placeholderTextColor={disabled ? 'red': '#a8c8ffff'}  // disabled 값에 따른 안내문구 색상 변경 
        selectionColor={'#d6e3ffff'}  // 커서색상
        style={[styles.input, { color: warning ? 'red': '#a8c8ffff' } ]}
        value={disabled ? "" : todoText} // disabled 값이 true 인 경우 입력창 초기화
        blurOnSubmit={ false } // 탭키 누를때 키보드 사라지지 않게 하기
        onChangeText={handleChange} // 입력창에 글자를 입력할때
        returnKeyType="done" // 엔터키 아이콘 변경
        maxLength={50} // 최대 글자수 제한
        autoCorrect={false} // 자동완성기능 끄기
        onSubmitEditing={hideKeyboard} // 여기서 하면 엔터키 두번 눌러야 할일추가됨 (키보드만 닫는걸로 수정함)
        />
        <TouchableOpacity 
            disabled={disabled} // disabled 값에 따른 버튼 비활성화
            activeOpacity={0.7} // 버튼 클릭시 투명도 변경
            onPress={onPress}   // 버튼 클릭시 실행
        > 
        {/* disabled 값에 따른 버튼 색상 변경 */}
            <View style={[styles.button, { backgroundColor: disabled ? "red" : '#a8c8ffff' }]}> 
                <Text style={styles.buttonText}>추가</Text>
            </View>
        </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    height: 70,
    paddingLeft: 10,
    borderColor: 'transparent',
    borderTopWidth: 3,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    backgroundColor: '#fff',
  },
  input: {
    color: '#a8c8ffff',
    fontSize: 20,
    paddingVertical: 20,
    flex: 1
  },
  button: {
    width: 80,
    height: 35,
    borderRadius: 20,
    marginRight: 10,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden'
  },
  buttonText: {
    color: '#fff',
    letterSpacing: 3,
    fontWeight: 'bold',
    fontSize: 15
  }
})

export default TodoInsert

components > TodoInsert.js 파일을 위와 같이 수정한다. 

function TodoInsert({ onInsertTodo, todoText, setTodoText, warning, setWarning, disabled }){
   // 중략
}

disabled props 를 추가한다. 

<TextInput 
        editable={!disabled} // disabled 값에 따른 입력창 비활성화
        selectTextOnFocus={!disabled} // disabled 값에 따른 입력창 비활성화
        placeholder={disabled ? 'X 할일을 작성할 수 없습니다 X' : '할일을 작성해주세요!'} // disabled 값에 따른 안내문구 변경
        placeholderTextColor={disabled ? 'red': '#a8c8ffff'}  // disabled 값에 따른 안내문구 색상 변경 
        selectionColor={'#d6e3ffff'}  // 커서색상
        style={[styles.input, { color: warning ? 'red': '#a8c8ffff' } ]}
        value={disabled ? "" : todoText} // disabled 값이 true 인 경우 입력창 초기화
        blurOnSubmit={ false } // 탭키 누를때 키보드 사라지지 않게 하기
        onChangeText={handleChange} // 입력창에 글자를 입력할때
        returnKeyType="done" // 엔터키 아이콘 변경
        maxLength={50} // 최대 글자수 제한
        autoCorrect={false} // 자동완성기능 끄기
        onSubmitEditing={hideKeyboard} // 여기서 하면 엔터키 두번 눌러야 할일추가됨 (키보드만 닫는걸로 수정함)
        />

입력창에서 아래 부분을 수정한다. disabled 속성값에 따라 입력창 안내문구와 안내문구 색상을 다르게 보여주도록 한다. 

placeholder={disabled ? 'X 할일을 작성할 수 없습니다 X' : '할일을 작성해주세요!'} // disabled 값에 따른 안내문구 변경
placeholderTextColor={disabled ? 'red': '#a8c8ffff'}  // disabled 값에 따른 안내문구 색상 변경

disabled 속성값에 따라 실제로 입력창을 비활성화한다. 

editable={!disabled} // disabled 값에 따른 입력창 비활성화
selectTextOnFocus={!disabled} // disabled 값에 따른 입력창 비활성화

disabled 속성값이 true 인 경우 입력창에 입력된 텍스트를 빈 문자열로 초기화한다.

value={disabled ? "" : todoText} // disabled 값이 true 인 경우 입력창 초기화

버튼에 disabled 속성을 추가하여 disabled 속성값에 따라 버튼을 비활성화한다. 

<TouchableOpacity 
            disabled={disabled} // disabled 값에 따른 버튼 비활성화
            activeOpacity={0.7} // 버튼 클릭시 투명도 변경
            onPress={onPress}   // 버튼 클릭시 실행
        >

disabled 속성값에 따라 버튼의 색상을 다르게 설정한다. 

{/* disabled 값에 따른 버튼 색상 변경 */}
<View style={[styles.button, { backgroundColor: disabled ? "red" : '#a8c8ffff' }]}> 
    <Text style={styles.buttonText}>추가</Text>
</View>

버튼 색상은 disabled 속성값에 따라 다르게 설정해줘야 하므로 아래 코드는 주석처리하거나 제거한다.

button: {
    // backgroundColor: '#a8c8ffff',
    // 중략
 }

 

* TodoItem 컴포넌트 수정하기 

import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
import moment from 'moment'

function TodoItem({ id, title, category, isDone, createdAt }){
    console.log("할일 생성시각: ", title, createdAt)
    return (
        <View style={styles.item}>
            <View style={styles.titleMargin}>
                <Text style={styles.title}>{title}</Text>
            </View>
            <View>
                <Text>{category} ({isDone ? "종료": "진행중"})</Text>
                <Text style={styles.dateText}>{createdAt && moment(createdAt.toDate()).format('YY-MM-DD hh:mm:ss')}</Text>
            </View>
        </View>
    )
}

const styles = StyleSheet.create({
    item: {
        flexDirection: 'row',
        alignItems: 'flex-start',
        paddingLeft: 10,
        paddingVertical: 10,
        // backgroundColor: '#d6e3ffff',
        // borderBottomWidth: 1,
        // borderBottomColor: '#a8c8ffff',
    },
    titleMargin: {
        marginRight: 10
    },
    title: {
        fontWeight: 'bold',
        fontSize: 20,
    },
    dateText: {
        fontSize: 12
    }
})

export default React.memo(TodoItem)

components > TodoItem.js 파일을 위와 같이 수정한다.

<Text style={styles.dateText}>{createdAt && moment(createdAt.toDate()).format('YY-MM-DD hh:mm:ss')}</Text>

실제로 수정된 부분은 위와 같다. 날짜순으로 정렬하기 위하여 날짜를 확인할 수 있도록 시간포맷을 변경하였다. 

 

* 캘린더 탭에서 특정 날짜 선택후 홈화면에 넘겨주기 

import React from 'react'
import { SafeAreaView, View, Text, StyleSheet, StatusBar, Button } from 'react-native'

function CalendarScreen({navigation}){
  const testDate = "2023-09-25"
  const getTodoForDate = () => {
    navigation.navigate('Home', {
      date: testDate
    })
  }
  return (
    <SafeAreaView style={styles.block}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <View>
        <Text>캘린더</Text>
        <Button title="날짜선택" onPress={getTodoForDate}/>
      </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  block: {
    flex: 1
  }
})
export default CalendarScreen

screens > CalendarScreen.js 파일을 위와 같이 수정한다. 

import { SafeAreaView, View, Text, StyleSheet, StatusBar, Button } from 'react-native'

캘린더 탭에 특정날짜를 선택하기 위한 버튼 컴포넌트를 추가한다.

const testDate = "2023-09-25"
  const getTodoForDate = () => {
    navigation.navigate('Home', {
      date: testDate
    })
  }

getTodoForDate 함수는 사용자가 버튼을 클릭할때 실행된다. 해당 함수는 홈 화면으로 이동하면서 testDate 변수에 설정된 날짜를 넘겨준다. 현재는 하드코딩되어 있지만 추후 달력을 추가해서 달력에서 특정날짜를 선택하도록 구현할 예정이다. 현재는 테스트를 위해 확장할 수 있도록만 구현해둔다. 

<Button title="날짜선택" onPress={getTodoForDate}/>

버튼에 이벤트핸들러 함수를 등록한다. 

 

* 날짜별 할일목록 조회 및 정렬 기능 테스트 

오늘 할일목록 조회하기

초기 로딩시 오늘의 할일목록을 최신순으로 정렬해서 보여준다. 

캘린더 앱에서 특정날짜 선택시 해당 날짜의 할일목록 보여주기

특정날짜의 할일목록을 보여주는 경우에는 조회는 가능하지만 변경은 불가능하다. 

728x90