본문 바로가기
⛏️ | 개발 기록/⏰ | Schedule Planner

[Android, Kotlin] 통계 화면 구성하기 (14)

by immgga 2024. 4. 14.

이미지 출처: https://hankli0130.medium.com/support-multiple-firebase-projects-in-your-android-app-f3db25ae85e5

 

이번에 드디어 통계 화면 구현을 마무리 짓게 되었다.

firebase realtime database를 썼을 때는 어떻게 구현해야 할까 막막했는데 firestore로 구현하니까 firestore에 내장된 필터 덕분에 따로 차트용 collection을 만들 필요 없이 하나의 collection으로 해결했다.

 

그러면 바로 통계 화면을 제작하면서 겪었던 시행착오들을 알아보자.

 

1. 통계 화면 ui 요소 추가

이번에 planner를 만들면서 통계 화면에 차트 2개(이번 주 일정, 주간 일정)만 있는 건 너무 심심해 보여서 ui 요소도 추가하고 차트 데이터도 바꾸기로 했다.

이번 주 일정을 보여주는 차트는 현재 날짜를 기준으로 그 주에 생성한 일정과 완료한 일정 2개의 데이터를 보여주는 차트였다.

다양한 데이터가 있는 것도 아니고,  차트에 데이터를 넣기에는 애매해 보여서 차트 데이터를 이번 주의 날마다 일정을 생성하고 완료한 횟수를 보여주는 차트로 바꾸고, 일정 생성 횟수(전체)와 완료한 일정 수(전체)의 숫자를 보여주는 스코어카드를 만드는 형식으로 변경했다.

왼쪽: 기존 형식, 오른쪽: 바뀐 형식

그리고 주간 일정 차트의 데이터도

기존에는

  • 일정 생성 개수
  • 일정 완료율

이었지만, 일정 완료율이 굳이 있을 필요는 없을 것 같고, 완료된 횟수로 바꾸는 게 나을 것 같아서

  • 일정 생성 개수
  • 일정 완료 개수

로 변경했다.

그리고 현재 planner는 구글 로그인을 사용해서 사용자를 판단한다. 이를 이용해 구글 로그인한 사용자의 간략한 정보(이름, 프사, 이메일)를 통계 화면에 같이 만들겠다.

나는 일단 통계 화면을 다음과 같이 설계했다.

 

2. firestore where을 이용한 차트 데이터 생성

통계 화면을 구성한 걸 바탕으로 차트 데이터에 들어갈 로직 생성에 돌입했다.

차트 데이터를 불러오는 것은 어렵지 않았다.

 

일간 일정 함수를 예로 들자면,

일단 자신이 불러올 collection을 설정해 준 후,

val getDailyRef = FirebaseFirestore.getInstance()
    .collection("schedule")
    .document(uid)
    .collection("plans")

현재 날짜를 기준으로 TemporalAdjusters를 이용해 주의 첫날(일요일)과 마지막날(원래 토요일이 마지막이지만, firestore의 where에 조건을 편하게 넣기 위해 다음 주 일요일로 설정)을 설정하고, 첫날과 마지막날의 unix timestamp(epoch time) 형식으로 바꿔서 firestore에 where 조건으로 달아 주는 함수를 만들었다.

val today = LocalDate.now()
// 주의 첫날은 일요일, 주의 마지막 날은 토요일
// 당일 기준 다음주 첫날인 일요일을 마지막으로 정함.
// 이유: 토요일 동안의 일정 생성여부를 확인해야 하는데 토요일로 기준을 잡으면 토요일 오전 12시가 마지막날 기준이 되기 때문(토요일 데이터를 불러올 수 없음)
val firstDayOfWeek = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY))
val lastDayOfWeek = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))
// second가 아닌 millisecond의 timestmp를 받기 위함
val timestampFirstDay = firstDayOfWeek.toString().stringToUnixTimestamp()
val timestampLastDay = lastDayOfWeek.toString().stringToUnixTimestamp()

// 첫 주 시작일보다 크거나 같음 그리고 마지막 날보다 작은 데이터들만 불러오기.
getDailyRef.whereGreaterThanOrEqualTo("baseDate", timestampFirstDay)
    .whereLessThan("baseDate", timestampLastDay)
    .readFireStoreData(
        onSuccess = {
            // 날마다 일정의 생성 개수를 카운트할 array 생성
            val dailyPlans = Array(7) { 0 }
            var completedPlansCount = 0
            it.forEach { documentSnapshot ->
                val documentData = documentSnapshot.toObject<PlanData>()
                val documentLocalDate = documentData?.baseDate?.unixTimestampToLocalDate()

                if (documentLocalDate != null) {
                    // 날짜에 따른 인덱스 수 +1 처리: 요일에 몇 개의 일정을 생성했는지 알 수 있음.
                    when (documentLocalDate.dayOfWeek) {
                        DayOfWeek.SUNDAY -> dailyPlans[0]++
                        DayOfWeek.MONDAY -> dailyPlans[1]++
                        DayOfWeek.TUESDAY -> dailyPlans[2]++
                        DayOfWeek.WEDNESDAY -> dailyPlans[3]++
                        DayOfWeek.THURSDAY -> dailyPlans[4]++
                        DayOfWeek.FRIDAY -> dailyPlans[5]++
                        DayOfWeek.SATURDAY -> dailyPlans[6]++
                    }
                }
                if (documentData?.complete == true) completedPlansCount++
            }
            // 하루에 생성한 총 일정 개수를 넣어주기.
            _dailyStatistics.value = dailyPlans
        }
    )

firestore에 내장된 whereGreaterThanOrEqualTo(크거나 같다)와 whereLessThan(작다)를 이용해 주의 첫날과 마지막날의 사이의 시간대에 생성된 일정들을 불러올 수 있었다.

 

주간 일정 함수도 같은 방법으로 데이터를 불러올 수 있었다.

 

3. 기타 오류 수정하기

2번까지의 과정을 모두 완료한 후 테스트를 해보면서 문제 사항을 찾아보고 해결하였다.

 

발견해 해결한 여러 문제들 중에 2가지를 소개하자면,

  • 일요일에 firestore where 조건이 정상적으로 작동되지 않던 오류 해결
  • 주간 차트 데이터가 랜덤 하게 들어오던 문제 해결

위 2개의 문제를 해결했던 경험을 짧게 언급하겠다.

 

첫 번째 문제같은 경우 테스트를 한 날이 일요일이었는데 차트 데이터가 안 들어오는 현상이 있어서 찾아보았고, 바로 문제의 원인을 찾을 수 있었다.

현재 firestore의 데이터를 불러오는 방식을 다음과 같이 사용하고 있었는데,

val firstDayOfWeek = today.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY))
val lastDayOfWeek = today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY))
// second가 아닌 millisecond의 timestmp를 받기 위함
val timestampFirstDay = firstDayOfWeek.toString().stringToUnixTimestamp()
val timestampLastDay = lastDayOfWeek.toString().stringToUnixTimestamp()

// 첫 주 시작일보다 크거나 같음 그리고 마지막 날보다 작은 데이터들만 불러오기.
getDailyRef.whereGreaterThanOrEqualTo("baseDate", timestampFirstDay)
    .whereLessThan("baseDate", timestampLastDay)
    . . .

 

이 부분에서 문제가 발생했다.

firstDayOfWeek와 lastDayOfWeek의 target 요일이 일요일인데, 불러오는 형식이

previousOrSame: 지난 요일(오늘 포함)
nextOrSame: 돌아오는 요일(오늘 포함)

이었기 때문에 2개의 변수가 모두 같은 날짜를 반환하였기 때문에 문제가 발생한 것이었다.

그래서 lastDayOfWeek의 target 요일을 토요일로 변경하고 epoch time으로 변경한 timestampLastDay를 약간 변경해 주었다. 

val firstDayOfWeek = todayWeekAgo.with(TemporalAdjusters.previousOrSame(DayOfWeek.SUNDAY))
val lastDayOfWeek = todayWeekAgo.with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY))
// second가 아닌 millisecond의 timestmp를 받기 위함
val timestampFirstDay = firstDayOfWeek.toString().stringToUnixTimestamp()
val timestampLastDay = lastDayOfWeek.toString().stringToUnixTimestamp() + 86399000L

getWeeklyRef.whereGreaterThanOrEqualTo("baseDate", timestampFirstDay)
    .whereLessThanOrEqualTo("baseDate", timestampLastDay)

 

timestampLastDay에 86399000의 값을 더해주는데 이게 무슨 뜻이냐면 86399000은 epoch time인데 이를 변환하면 23시 59분 59초가 된다. 이를 더해줌으로써 토요일 동안 생성한 일정들도 불러와질 수 있도록 했고,

기존의 whereLessThan(작다)를 whereLessThanOrEqualTo(작거나 같다)로 바꿔주면서 23시 59분 59초까지 일정 생성 데이터를 불러올 수 있도록 해주었다.

 

두 번째 문제의 경우에는 주간 일정 데이터는 잘 불러와지는지 테스트하기 위해 현재 시간으로부터 약 1주일 전의 시간으로 테스트 데이터를 하나 생성해 주었다.

이 테스트 데이터가 차트로 보일 때는 1주 전으로 데이터가 보여야 하는데 차트를 reload 할 때마다 다른 위치로 데이터가 이동되는 현상이 있었다.

 

현재 나는 firestore 데이터를 불러올 때, addonCompleteListener를 사용한다.

addonCompleteListener 말고도 addonSuccessListener를 사용하는 방법이 있는데 addonCompleteListener를 선택한 이유는 비동기 처리를 지원하고 하나의 리스너 안에 성공과 실패 로직을 같이 쓸 수 있다는 점에 addonCompleteListener를 사용하고 있었다(addonSuccessListener는 실패 시 addonFailureListener를 달아서 실패 처리를 해야 한다.) 

addonCompleteListener를 이용해서 최대 5주 전의 데이터를 불러오기 위해서 반복문을 이용해서 차트 데이터를 불러오는데

반복문이 5번 돌 동안 먼저 끝나는 순서대로 StateFlow에 저장되는 형식으로 코드를 짜 놓은 게 원인이었다.

ex) 5번 반복을 돌 때, 완료 순서가 2, 3, 5, 4, 1이면 가장 먼저 완료된 2를 먼저 stateFlow에 저장한다.

 

나는 이 문제를 해결하기 위해 반복문을 돌고 데이터를 저장할 때, 저장되는 데이터의 index를 고정해서 데이터를 저장할 수 있게 해 주었다.(2번 작업이 완료되었을 때 stateList의 index 2에 작업 결과 데이터를 저장)

기존에는 mutableStateListOf()을 이용해 State 변수를 선언해 주었는데 이를 그냥 사용하려고 하면 IndexBoundException이 발생해서 list의 size를 정해주고 초기값을 설정해 주는 방법으로 수정했다.

 

기존 코드

private val _weeklyStatistics = mutableStateListOf<StatisticsData>()
val weeklyStatistics: List<StatisticsData> get() = _weeklyStatistics

 

수정 코드

private val _weeklyStatistics = MutableStateFlow<MutableList<StatisticsData?>>(MutableList(5) { null })
val weeklyStatistics = _weeklyStatistics.asStateFlow()

 

4. 구현 결과 및 마무리

구현 결과(사진)

 

통계 화면을 제작하면서 firestore의 필터(쿼리)에 더 자세히 알게 되었던 것 같다.

또한 스코어카드와 프로필 composable을 만들면서 compose를 다시 공부했다(아직 감을 찾아가는 단계)

이제 거의 다 제작이 마무리되어서 일정 삭제 기능만 구현하고 오류 테스트 해본 뒤에 지인들을 대상으로 베타 테스트를 해볼 생각이다.

위에 언급했다시피 일정 삭제 기능 구현 포스팅으로 다시 돌아오겠다 :)

728x90