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

21. 할일목록과 사용자 연동하기

syleemomo 2023. 11. 6. 14:51
728x90

* 로그인한 사용자의 할일목록만 조회하기

import React, { useEffect } from 'react'
import { View, Button, StyleSheet, Alert } from 'react-native'
import { GoogleSignin, GoogleSigninButton, statusCodes  } from '@react-native-google-signin/google-signin'

function LoginButton({navigation}){
    const googleSigninConfigure = () => { 
        GoogleSignin.configure({
        webClientId:
            '137262950194-gcouccatiffjp4ei8aqfi05uruv5j4dl.apps.googleusercontent.com',
        })
    }

    const signInWithGoogle = async () => {
        try {
            await GoogleSignin.hasPlayServices()
            const userInfoFromGoogle = await GoogleSignin.signIn()

            if(userInfoFromGoogle){
                console.log("사용자 연락처: ", userInfoFromGoogle.user)
                navigation.navigate('App', { userInfo: userInfoFromGoogle.user})
            }
        } catch (error) {
            if (error.code === statusCodes.SIGN_IN_CANCELLED) {
                console.log('user cancelled the login flow')
                Alert.alert('user cancelled the login flow')
            } else if (error.code === statusCodes.IN_PROGRESS) {
                console.log('sign in is in progress already')
                Alert.alert('sign in is in progress already')
            } else if (error.code === statusCodes.PLAY_SERVICES_NOT_AVAILABLE) {
                console.log('play services not available or outdated')
                Alert.alert('play services not available or outdated')
            } else {
                console.log('some other error happened')
                Alert.alert('some other error happened')
            }
        }
    }

    useEffect(() => {
        googleSigninConfigure()
    }, [])

    return (
        <View style={styles.buttonWrapper}>
            <GoogleSigninButton
                size={GoogleSigninButton.Size.Wide}
                color={GoogleSigninButton.Color.Dark}
                onPress={signInWithGoogle}
                disabled={false}
                style={styles.signInBtn}
            />
        </View>
    )
}
export default LoginButton

const styles = StyleSheet.create({
    buttonWrapper: {
        position: 'absolute',
        left: 0, right: 0,
        bottom: 100,
    },
    signInBtn: {
        marginTop: 10,
        marginLeft: 'auto',
        marginRight: 'auto'
    },
})

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

const signInWithGoogle = async () => {
        try {
            await GoogleSignin.hasPlayServices()
            const userInfoFromGoogle = await GoogleSignin.signIn()

            if(userInfoFromGoogle){
                console.log("사용자 연락처: ", userInfoFromGoogle.user)
                navigation.navigate('App', { userInfo: userInfoFromGoogle.user}) // 수정
            }
        } catch (error) {
        	// 중략
        }
 }

랜딩페이지에서 로그인할때 App 컴포넌트로 화면이 이동하는데 이때 로그인한 사용자정보를 해당 컴포넌트로 전달해준다.

useEffect(() => {
        const result = getUserInfo()
        // if(user){
        //     navigation.navigate('App')
        // }
        result.then(user => {
            console.log('user : ',user)
            if(user){
              navigation.navigate('App', { userInfo: user.user }) // 수정
            }
          })
    }, [])

screens > LandingScreens.js 파일에서 해당부분을 위와 같이 수정한다. 이미 로그인되어 있는 경우 랜딩페이지에서 곧바로 홈화면으로 이동하게 되는데 이때 로그인된 사용자정보를 함께 전달한다. 

import React, { useState, useEffect } from 'react'
import { View, Text, StyleSheet, ActivityIndicator } from 'react-native'
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({navigation, route}) {
  const [todos, setTodos] = useState([]) // 할일목록 상태 (HomeScreen -> App 이동)
  const [loading, setLoading] = useState(true) // 할일목록 상태 (HomeScreen -> App 이동)
  
  const [caretType, setCaretType] = useState(false)
  const [yearCaret, setYearCaret] = useState(false)
  const [monthCaret, setMonthCaret] = useState(false)
  const [numOfTodosToday, setNumOfTodosToday] = useState(0)

  const { userInfo } = route.params
  console.log("로그인한 사용자 정보: ", userInfo)

  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`)
    }
    // 문제점 : onSnapshot 을 사용시 다른 사용자가 할일을 추가하면 전체할일목록이 업데이트되기 때문에 나의 화면도 새로고침됨 
    return getCollection('todos', 
                          onResult, onError,
                          { exists: true, condition: [['userEmail', '==', userInfo.email]] },
                          null, null)
  }, [])

  if (loading) {
    return (
      <View style={styles.block}>
        <ActivityIndicator size="large" color="#0047AB"/>
        <Text style={styles.loadingText}>Loading ...</Text>
      </View> 
    )
  }

  return (
    <>
      <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} setNumOfTodosToday={setNumOfTodosToday} userInfo={userInfo}/>} options={{
          title: '홈',
          tabBarIcon: ({ color, size }) => <Icon name="home" color={color} size={size}/>,
          headerTitle: (props) => <DropdownCategory {...props} caretType={caretType} setCaretType={setCaretType} categoryTitle="카테고리"/>,
          headerStyle: {
            backgroundColor: '#a8c8ffff',
          },
          headerTitleStyle: {
            fontWeight: 'bold',
            color: '#fff'
          },
          tabBarBadge: numOfTodosToday
        }}/>
        <Tab.Screen name="Calendar" children={(props) => <CalendarScreen 
                                                          {...props}
                                                          yearCaret={yearCaret}
                                                          setYearCaret={setYearCaret}
                                                          monthCaret={monthCaret}
                                                          setMonthCaret={setMonthCaret}/>} options={{
          title: '달력',
          tabBarIcon: ({ color, size }) => <Icon name="calendar-month" color={color} size={size}/>,
          headerTitle: (props) => (<View style={{flexDirection: 'row'}}>
            <DropdownCategory {...props} caretType={yearCaret} setCaretType={setYearCaret} categoryTitle="Year"/>
            <DropdownCategory {...props} caretType={monthCaret} setCaretType={setMonthCaret} categoryTitle="Month"/>
          </View>),
          headerStyle: {
            backgroundColor: '#a8c8ffff',
          },
          headerTitleStyle: {
            fontWeight: 'bold',
            color: '#fff'
          },
        }}/>
        <Tab.Screen name="DashBoard" children={(props) => <DashBoardSceen todos={todos}/>} 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>
    </>
  );
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
    backgroundColor: '#a8c8ffff',
    justifyContent: 'center',
    alignItems: 'center',
  },
  loadingText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#fff',
    marginTop: 10,
    textAlign: 'center'
  }
})

App.js 파일을 위와 같이 수정한다. 

export default function App({navigation, route}) {
	// 중략
}

랜딩페이지에서 전달된 사용자정보를 조회하기 위하여 route props 를 추가한다.

const { userInfo } = route.params
console.log("로그인한 사용자 정보: ", userInfo)

route 객체의 params 프로퍼티를 조회하면 전달받은 사용자정보를 얻을수 있다. 

// 문제점 : onSnapshot 을 사용시 다른 사용자가 할일을 추가하면 전체할일목록이 업데이트되기 때문에 나의 화면도 새로고침됨 
return getCollection('todos', 
                      onResult, onError,
                      { exists: true, condition: [['userEmail', '==', userInfo.email]] },
                      null, null)

 getCollection 함수의 네번째 인자에 null 대신 위와 같이 객체로 쿼리(query)를 수행한다. 전체 할일목록에서 userEmail 필드가 현재 로그인한 사용자의 이메일과 동일한 경우 해당 사용자가 작성한 투두(todo)이므로 위와 같이 쿼리한다. 한가지 해결해야 할 문제는 A 사용자가 할일을 추가하면 onSnapshot 메서드에 의하여 전체 할일목록을 새로 조회하므로 B나 C와 같은 다른 사용자의 앱화면도 리렌더링되는 문제가 존재한다. 이는 추후 해결할 예정이다.  

 

<Tab.Screen name="Home" children={(props) => <HomeScreen {...props} caretType={caretType} setCaretType={setCaretType} todos={todos} loading={loading} setNumOfTodosToday={setNumOfTodosToday} userInfo={userInfo}/>}
// 중략
/>

HomeScreen 컴포넌트로 로그인한 사용자정보를 props로 넘겨준다.

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

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

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

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

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

  // 오늘/내일의 날짜를 기준으로 할일목록을 필터링하고 정렬함
  const category = useRef('') // 카테고리 변수
  const date = (route.params && route.params.date) ? new Date(route.params.date) : new Date()
  const {todosToday, today} = getTodosBySpecificDate(date, todos) // dateOfTodo 기준으로 필터링
  const todosTodayLatest = [...todosToday] // 원본복사
  todosTodayLatest.sort((a, b) => b.createdAt?.seconds - a.createdAt?.seconds) // 최신순 정렬 (업데이트되는 시간차 때문에 createdAt 이 null 일수 있음)

  console.log("현재 선택날짜: ", date, todosToday)
  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(), // 클라이언트 기준이 아니라 서버기준 저장시각,
          dateOfTodo: date, // createdAt 기준으로 todo 를 필터링하는게 아니라 해당 필드 기준으로 걸러냄
          userEmail: userInfo.email // 할일을 생성한 사용자 필드 추가 
        }
        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()
  }
  const removeTodo = (id, title) => {
    setModalOpen(true)
    setTodoToRemove({id, title})
    console.log(`할일 [${title}] 제거`)
  }
  const handleRemove = () => {
    setModalOpen(false)
    setTodoToRemove({id: null, title: ''})
    removeData('todos', todoToRemove.id)
  }

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

  useEffect(() => navigation.addListener('blur', () => console.log('페이지 벗어남')), [])

  useEffect(() => {
    setNumOfTodosToday(todosToday.length)
  })

  return (
    <SafeAreaView 
        style={styles.block} 
        onTouchStart={handleOutSideOfMenu}>
      <StatusBar backgroundColor="#a8c8ffff"></StatusBar>
      <Modal
        animationType="fade"
        transparent={true}
        visible={modalOpen}
        onRequestClose={() => {
          Alert.alert('Modal has been closed.');
          setModalOpen(!modalOpen);
        }}
      >
        <View style={styles.centeredView}>
          <View style={styles.modalView}>
            <Text style={styles.guideText}>할일 "{todoToRemove.title}" 을 제거하시겠습니까?</Text>
            <View style={styles.alignHorizontal}>
              <Pressable
                style={[styles.button, styles.buttonClose, styles.remove]}
                onPress={handleRemove}>
                <Text style={styles.textStyle}>삭제</Text>
              </Pressable>
              <Pressable
                style={[styles.button, styles.buttonClose]}
                onPress={() => setModalOpen(false)}>
                <Text style={styles.textStyle}>닫기</Text>
              </Pressable>
            </View>
          </View>
        </View>
      </Modal>
      
        {caretType && <DropdownList categories={categories} selectCategory={selectCategory} top={-15}/>}
        <DateHeader date={date}/>
        {/* 해당날짜 기준 최신순으로 정렬된 할일목록 */}
        {todosTodayLatest.length === 0 ? 
            <Default/> : 
            <TodoList todos={todosTodayLatest} removeTodo={removeTodo}
        />}
        {/* 필터링된 할일목록의 날짜와 현재 날짜가 동일하지 않은 경우 */}
        <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,
  },
  centeredView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 22,
  },
  modalView: {
    margin: 50,
    backgroundColor: 'white',
    borderRadius: 20,
    padding: 20,
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 5,
  },
  alignHorizontal: {
    flexDirection: 'row',
    justifyContent: 'flex-end'
  },
  guideText: {
    fontWeight: 'bold',
    fontSize: 15
  },
  button: {
    width: 70,
    height: 40,
    borderRadius: 10,
    padding: 0,
    elevation: 2,
    marginTop: 30,
    marginRight: 5,
    justifyContent: 'center'
  },
  buttonOpen: {
    backgroundColor: '#F194FF',
  },
  buttonClose: {
    backgroundColor: '#a8c8ffff',
  },
  textStyle: {
    color: 'white',
    fontWeight: 'bold',
    textAlign: 'center',
  },
  remove: {
    backgroundColor: 'red'
  },
  modalText: {
    marginBottom: 15,
    textAlign: 'center',
  },
})
export default HomeScreen

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

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

App 컴포넌트에서 전달받은 사용자정보를 홈화면에서 사용하기 위하여 props로 전달받는다.

const newTodo = {
          title: trimedText,
          category: category.current || '자기계발', // 선택한 카테고리 설정 (수정)
          isDone: false,
          createdAt: getCurrentTime(), // 클라이언트 기준이 아니라 서버기준 저장시각,
          dateOfTodo: date, // createdAt 기준으로 todo 를 필터링하는게 아니라 해당 필드 기준으로 걸러냄
          userEmail: userInfo.email // 할일을 생성한 사용자 필드 추가 
        }

새로운 할일을 추가할때 현재 로그인한 사용자의 이메일을 userEmail 필드에 추가적으로 설정하였다. 이는 추후 로그인한 사용자의 할일목록을 조회할때 userEmail 필드로 필터링하기 위함이다. 

 

* 로그인한 사용자의 할일목록만 가져오기 - 테스트

전체 할일목록 (11월 6일)

11월 6일에 저장된 전체 할일목록이다. 

11월 6일 특정 사용자의 할일

 

get userinfo 라는 할일제목은 userEmail 필드가 포함된 로그인한 사용자의 할일이다. App 컴포넌트에서 getCollection 함수로 할일목록을 가져올때 네번째 인자에 null 값이 아니라 쿼리를 하여 특정 사용자의 할일목록만 조회하면 아래와 같다.

로그인한 사용자의 할일목록 (11월 6일)
전체 할일목록 (11월 15일)

11월 15일의 전체 할일목록은 위와 같지만, 로그인한 사용자의 할일목록은 아래와 같다. 

로그인한 사용자의 할일목록 (11월 15일)

728x90