Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mz 130 수수 통계 #120

Merged
merged 18 commits into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ded1749
refactor: RecentSpentGraph graph 이름을 지정하도록 수정
yangsooplus Jan 25, 2024
8b8e445
feat: 수수 통계 옵션 선택 컴포넌트
yangsooplus Jan 25, 2024
5ba458d
feat: 수수 통계 UI
yangsooplus Jan 25, 2024
258670c
chore: 수수 통계 옵션 선택 컴포넌트 수정
yangsooplus Jan 26, 2024
a7e6b9f
feat: 수수 통계 옵션 (봉투 생성 config) 불러오기
yangsooplus Jan 26, 2024
72b229a
feat: 불러온 수수 통계 옵션을 StatisticOptionSlot으로 선택하기
yangsooplus Jan 26, 2024
d554914
refactor: StatisticsOption을 CategoryConfig와 RelationshipConfig로 대체
yangsooplus Feb 1, 2024
28202a1
feat: 수수 통계 api 연동
yangsooplus Feb 1, 2024
befc99a
fix: 통계 화면에 잘못 적용된 패딩, 스크롤 고침
yangsooplus Feb 1, 2024
cb42da1
feat: 숫자가 바뀌는 효과가 있는 Text 컴포넌트 추가
yangsooplus Feb 1, 2024
7c86191
chore: 불필요한 로그 출력 제거
yangsooplus Feb 1, 2024
e375e16
feat: 통계에 숫자 변경 애니메이션 컴포넌트 적용
yangsooplus Feb 1, 2024
1aead52
feat: 수수 통계 변화하는 항목에 애니메이션 적용
yangsooplus Feb 1, 2024
2230e76
chore: stringRes 분리
yangsooplus Feb 2, 2024
156f0f0
feat: 통계 옵션을 불러오지 못했을 경우 핸들링
yangsooplus Feb 2, 2024
fc39f50
chore: ktlint, detekt check
yangsooplus Feb 2, 2024
d646198
chore: 통계 옵션 불러오기 실패 시 Exception 변경
yangsooplus Feb 2, 2024
5c20ed7
chore: detekt check
yangsooplus Feb 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.susu.core.designsystem.component.text

import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import com.susu.core.designsystem.theme.Gray100
import com.susu.core.designsystem.theme.SusuTheme
import java.text.DecimalFormat

private val upward = slideInVertically { it } togetherWith slideOutVertically { -it }
private val downward = slideInVertically { -it } togetherWith slideOutVertically { it }

@Composable
fun AnimatedCounterText(
number: Int,
modifier: Modifier = Modifier,
prefix: String? = null,
postfix: String? = null,
style: TextStyle = SusuTheme.typography.title_xs,
color: Color = Gray100,
) {
val moneyFormat = remember { DecimalFormat("#,###") }
val currentString = moneyFormat.format(number)

Row(
modifier = modifier,
horizontalArrangement = Arrangement.End,
) {
prefix?.let {
Text(
text = it,
style = style,
color = color,
)
}
for (i in currentString.indices) {
val char = currentString[i]

AnimatedContent(
targetState = char,
transitionSpec = {
when {
initialState.isDigit() && targetState.isDigit() &&
initialState.digitToInt() < targetState.digitToInt() -> upward

initialState.isDigit() && targetState.isDigit() &&
initialState.digitToInt() > targetState.digitToInt() -> downward

else -> upward
}
},
label = "animatedCounterChar$i",
) {
Text(
text = it.toString(),
style = style,
color = color,
)
}
}
postfix?.let {
Text(
text = it,
style = style,
color = color,
)
}
}
}

@Preview(showBackground = true)
@Composable
fun AnimatedCounterTextPreview() {
SusuTheme {
var number by remember { mutableStateOf(10000) }
Column {
Button(onClick = { number += 6000 }) {
Text(text = "증가")
}
AnimatedCounterText(number = number)
}
}
}
16 changes: 16 additions & 0 deletions core/model/src/main/java/com/susu/core/model/SusuStatistics.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.susu.core.model

import androidx.compose.runtime.Stable

@Stable
data class SusuStatistics(
val averageSent: Int = 0,
val averageRelationship: StatisticsElement = StatisticsElement(),
val averageCategory: StatisticsElement = StatisticsElement(),
val recentSpent: List<StatisticsElement> = emptyList(),
val recentTotalSpent: Int = 0,
val recentMaximumSpent: Int = 0,
val mostSpentMonth: Int = 0,
val mostRelationship: StatisticsElement = StatisticsElement(),
val mostCategory: StatisticsElement = StatisticsElement(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.susu.data.data.repository

import com.susu.core.model.MyStatistics
import com.susu.core.model.StatisticsElement
import com.susu.core.model.SusuStatistics
import com.susu.data.remote.api.StatisticsService
import com.susu.data.remote.model.response.toModel
import com.susu.domain.repository.StatisticsRepository
Expand All @@ -26,4 +27,26 @@ class StatisticsRepositoryImpl @Inject constructor(
recentMaximumSpent = originalStatistic.recentMaximumSpent,
)
}

override suspend fun getSusuStatistics(age: String, relationshipId: Int, categoryId: Int): SusuStatistics {
val originalStatistic = statisticsService.getSusuStatistics(
age = age,
relationshipId = relationshipId,
categoryId = categoryId,
).getOrThrow().toModel()
val sortedRecentSpent = originalStatistic.recentSpent.sortedBy { it.title.toInt() }
.map { StatisticsElement(title = it.title.substring(it.title.length - 2).toInt().toString(), value = it.value) }

return SusuStatistics(
averageSent = originalStatistic.averageSent,
averageRelationship = originalStatistic.averageRelationship,
averageCategory = originalStatistic.averageCategory,
recentSpent = sortedRecentSpent,
mostSpentMonth = originalStatistic.mostSpentMonth % 100,
mostRelationship = originalStatistic.mostRelationship,
mostCategory = originalStatistic.mostCategory,
recentTotalSpent = originalStatistic.recentTotalSpent,
recentMaximumSpent = originalStatistic.recentMaximumSpent,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.susu.data.remote.api

import com.susu.data.remote.model.response.MyStatisticsResponse
import com.susu.data.remote.model.response.SusuStatisticsResponse
import com.susu.data.remote.retrofit.ApiResult
import retrofit2.http.GET
import retrofit2.http.Query

interface StatisticsService {
@GET("statistics/mine")
suspend fun getMyStatistics(): ApiResult<MyStatisticsResponse>

@GET("statistics/susu")
suspend fun getSusuStatistics(
@Query("age") age: String,
@Query("relationshipId") relationshipId: Int,
@Query("categoryId") categoryId: Int,
): ApiResult<SusuStatisticsResponse>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.susu.data.remote.model.response

import com.susu.core.model.MyStatistics
import com.susu.core.model.SusuStatistics
import kotlinx.serialization.Serializable

@Serializable
Expand All @@ -15,8 +16,19 @@ data class MyStatisticsResponse(

@Serializable
data class StatisticsElement(
val title: String,
val value: Int,
val title: String = "",
val value: Int = 0,
)

@Serializable
data class SusuStatisticsResponse(
val averageSent: Int = 0,
val averageRelationship: StatisticsElement = StatisticsElement(),
val averageCategory: StatisticsElement = StatisticsElement(),
val recentSpent: List<StatisticsElement> = emptyList(),
val mostSpentMonth: Int = 0,
val mostRelationship: StatisticsElement = StatisticsElement(),
val mostCategory: StatisticsElement = StatisticsElement(),
)

fun MyStatisticsResponse.toModel() = MyStatistics(
Expand All @@ -34,3 +46,15 @@ fun StatisticsElement.toModel() = com.susu.core.model.StatisticsElement(
title = title,
value = value,
)

fun SusuStatisticsResponse.toModel() = SusuStatistics(
averageSent = averageSent,
averageRelationship = averageRelationship.toModel(),
averageCategory = averageCategory.toModel(),
recentSpent = recentSpent.map { it.toModel() },
mostSpentMonth = mostSpentMonth,
mostRelationship = mostRelationship.toModel(),
mostCategory = mostCategory.toModel(),
recentMaximumSpent = recentSpent.maxOfOrNull { it.value } ?: 0,
recentTotalSpent = recentSpent.sumOf { it.value },
)
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.susu.domain.repository

import com.susu.core.model.MyStatistics
import com.susu.core.model.SusuStatistics

interface StatisticsRepository {
suspend fun getMyStatistics(): MyStatistics
suspend fun getSusuStatistics(age: String, relationshipId: Int, categoryId: Int): SusuStatistics
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.susu.domain.usecase.statistics

import com.susu.core.common.runCatchingIgnoreCancelled
import com.susu.domain.repository.StatisticsRepository
import javax.inject.Inject

class GetSusuStatisticsUseCase @Inject constructor(
private val statisticsRepository: StatisticsRepository,
) {
suspend operator fun invoke(age: String, relationshipId: Int, categoryId: Int) = runCatchingIgnoreCancelled {
statisticsRepository.getSusuStatistics(age, relationshipId, categoryId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ internal fun MainScreen(
)

statisticsNavGraph(
padding = innerPadding,
navigateToMyInfo = navigator::navigateMyPageInfo,
onShowDialog = viewModel::onShowDialog,
handleException = viewModel::handleException,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ package com.susu.feature.statistics
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
Expand All @@ -26,10 +25,12 @@ import com.susu.core.designsystem.theme.SusuTheme
import com.susu.core.ui.DialogToken
import com.susu.core.ui.extension.collectWithLifecycle
import com.susu.feature.statistics.component.StatisticsTab
import com.susu.feature.statistics.content.MyStatisticsRoute
import com.susu.feature.statistics.content.my.MyStatisticsRoute
import com.susu.feature.statistics.content.susu.SusuStatisticsRoute

@Composable
fun StatisticsRoute(
padding: PaddingValues,
viewModel: StatisticsViewModel = hiltViewModel(),
navigateToMyInfo: () -> Unit,
onShowDialog: (DialogToken) -> Unit,
Expand Down Expand Up @@ -58,6 +59,7 @@ fun StatisticsRoute(
}

StatisticsScreen(
padding = padding,
uiState = uiState,
onTabSelected = viewModel::selectStatisticsTab,
handleException = handleException,
Expand All @@ -66,13 +68,13 @@ fun StatisticsRoute(

@Composable
fun StatisticsScreen(
padding: PaddingValues = PaddingValues(),
uiState: StatisticsState = StatisticsState(),
onTabSelected: (StatisticsTab) -> Unit = {},
handleException: (Throwable, () -> Unit) -> Unit = { _, _ -> },
) {
Box(
modifier = Modifier.fillMaxSize()
.verticalScroll(rememberScrollState()),
modifier = Modifier.fillMaxSize().padding(padding),
) {
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = SusuTheme.spacing.spacing_m),
Expand All @@ -96,7 +98,11 @@ fun StatisticsScreen(
handleException = handleException,
)

StatisticsTab.AVERAGE -> {}
StatisticsTab.AVERAGE -> SusuStatisticsRoute(
isBlind = uiState.isBlind,
modifier = Modifier.fillMaxSize(),
handleException = handleException,
)
}
}

Expand Down
Loading
Loading