diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt index 61535f14..9e1d9fa8 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/DataSourceModule.kt @@ -1,5 +1,9 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogLocalDataSource +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogRemoteDataSource +import com.threegap.bitnagil.data.activitylog.datasourceImpl.ActivityLogLocalDataSourceImpl +import com.threegap.bitnagil.data.activitylog.datasourceImpl.ActivityLogRemoteDataSourceImpl import com.threegap.bitnagil.data.address.datasource.AddressDataSource import com.threegap.bitnagil.data.address.datasource.LocationDataSource import com.threegap.bitnagil.data.address.datasourceImpl.AddressDataSourceImpl @@ -93,4 +97,12 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindReportDataSource(impl: ReportDataSourceImpl): ReportDataSource + + @Binds + @Singleton + abstract fun bindActivityLogRemoteDataSource(impl: ActivityLogRemoteDataSourceImpl): ActivityLogRemoteDataSource + + @Binds + @Singleton + abstract fun bindActivityLogLocalDataSource(impl: ActivityLogLocalDataSourceImpl): ActivityLogLocalDataSource } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt index b6670f22..8d325187 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/RepositoryModule.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.activitylog.repositoryImpl.ActivityLogRepositoryImpl import com.threegap.bitnagil.data.address.repositoryImpl.AddressRepositoryImpl import com.threegap.bitnagil.data.auth.repositoryimpl.AuthRepositoryImpl import com.threegap.bitnagil.data.emotion.repositoryImpl.EmotionRepositoryImpl @@ -10,6 +11,7 @@ import com.threegap.bitnagil.data.report.repositoryImpl.ReportRepositoryImpl import com.threegap.bitnagil.data.routine.repositoryImpl.RoutineRepositoryImpl import com.threegap.bitnagil.data.user.repositoryImpl.UserRepositoryImpl import com.threegap.bitnagil.data.version.repositoryImpl.VersionRepositoryImpl +import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository import com.threegap.bitnagil.domain.address.repository.AddressRepository import com.threegap.bitnagil.domain.auth.repository.AuthRepository import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository @@ -69,4 +71,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindReportRepository(impl: ReportRepositoryImpl): ReportRepository + + @Binds + @Singleton + abstract fun bindActivityLogRepository(impl: ActivityLogRepositoryImpl): ActivityLogRepository } diff --git a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt index d3c76973..58b6c0d5 100644 --- a/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt +++ b/app/src/main/java/com/threegap/bitnagil/di/data/ServiceModule.kt @@ -1,5 +1,6 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.activitylog.service.ActivityLogService import com.threegap.bitnagil.data.address.service.AddressService import com.threegap.bitnagil.data.auth.service.AuthService import com.threegap.bitnagil.data.auth.service.LoginService @@ -74,4 +75,8 @@ object ServiceModule { @Provides @Singleton fun provideReportService(@Auth retrofit: Retrofit): ReportService = retrofit.create() + + @Provides + @Singleton + fun provideActivityLogService(@Auth retrofit: Retrofit): ActivityLogService = retrofit.create() } diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt index a2d10b5f..922dc08f 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt @@ -29,6 +29,7 @@ import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.screen.home.HomeScreenContainer import com.threegap.bitnagil.presentation.screen.mypage.MyPageScreenContainer import com.threegap.bitnagil.presentation.screen.recommendroutine.RecommendRoutineScreenContainer +import com.threegap.bitnagil.presentation.screen.summary.SummaryScreenContainer import com.threegap.bitnagil.presentation.util.statusbar.NavStatusBarEffect import com.threegap.bitnagil.presentation.util.toast.GlobalBitnagilToast @@ -101,6 +102,12 @@ fun HomeNavHost( navigateToReportHistory = navigateToReportHistory, ) } + + composable { + SummaryScreenContainer( + navigateToYouthPolicies = {} // todo - 청년공고 화면 구현 후 연결 필요 + ) + } } }, ) diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt index a589ae35..774c7576 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt @@ -19,6 +19,7 @@ class HomeNavigator( destination?.hasRoute(HomeRoute.Home::class) == true -> HomeRoute.Home destination?.hasRoute(HomeRoute.RecommendRoutine::class) == true -> HomeRoute.RecommendRoutine destination?.hasRoute(HomeRoute.MyPage::class) == true -> HomeRoute.MyPage + destination?.hasRoute(HomeRoute.Summary::class) == true -> HomeRoute.Summary else -> null } } diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt index 0a448e3f..02864e5e 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt @@ -21,6 +21,11 @@ sealed interface HomeRoute { data object MyPage : HomeRoute { override val showFloatingButton: Boolean = false } + + @Serializable + data object Summary : HomeRoute { + override val showFloatingButton: Boolean = false + } } data class HomeTab( @@ -32,5 +37,6 @@ data class HomeTab( val homeTabList = listOf( HomeTab(HomeRoute.Home, "홈", R.drawable.ic_home), HomeTab(HomeRoute.RecommendRoutine, "추천 루틴", R.drawable.ic_routine_recommend), + HomeTab(HomeRoute.Summary, "리포트", R.drawable.ic_report), HomeTab(HomeRoute.MyPage, "마이페이지", R.drawable.ic_profile), ) diff --git a/app/src/main/res/drawable/ic_report.xml b/app/src/main/res/drawable/ic_report.xml new file mode 100644 index 00000000..ed16912b --- /dev/null +++ b/app/src/main/res/drawable/ic_report.xml @@ -0,0 +1,37 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_badge_question.xml b/core/designsystem/src/main/res/drawable/ic_badge_question.xml new file mode 100644 index 00000000..a6a57aab --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_badge_question.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogLocalDataSource.kt new file mode 100644 index 00000000..8fbd5f23 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogLocalDataSource.kt @@ -0,0 +1,15 @@ +package com.threegap.bitnagil.data.activitylog.datasource + +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import kotlinx.coroutines.flow.StateFlow +import java.time.YearMonth + +interface ActivityLogLocalDataSource { + val badgesByMonth: StateFlow> + val emotionMarblesByMonth: StateFlow>> + fun saveBadges(yearMonth: YearMonth, monthlyBadge: MonthlyBadge) + fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) + fun removeEmotionMarbles(yearMonth: YearMonth) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt new file mode 100644 index 00000000..4a57b02b --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.data.activitylog.datasource + +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse +import com.threegap.bitnagil.data.activitylog.model.response.MonthlyBadgeResponse + +interface ActivityLogRemoteDataSource { + suspend fun getBadges(year: Int, month: Int): Result + suspend fun getEmotionMarbles(startDate: String, endDate: String): Result> +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt new file mode 100644 index 00000000..4b61906d --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt @@ -0,0 +1,36 @@ +package com.threegap.bitnagil.data.activitylog.datasourceImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogLocalDataSource +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import java.time.YearMonth +import javax.inject.Inject + +class ActivityLogLocalDataSourceImpl @Inject constructor() : ActivityLogLocalDataSource { + private val _badgesByMonth = MutableStateFlow>(emptyMap()) + override val badgesByMonth: StateFlow> = _badgesByMonth.asStateFlow() + + private val _emotionMarblesByMonth = MutableStateFlow>>(emptyMap()) + override val emotionMarblesByMonth: StateFlow>> = _emotionMarblesByMonth.asStateFlow() + + override fun saveBadges(yearMonth: YearMonth, monthlyBadge: MonthlyBadge) { + _badgesByMonth.update { it + (yearMonth to monthlyBadge) } + } + + override fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) { + _emotionMarblesByMonth.update { it + (yearMonth to emotionMarbles) } + } + + override fun removeEmotionMarbles(yearMonth: YearMonth) { + _emotionMarblesByMonth.update { it - yearMonth } + } + + override fun clearCache() { + _badgesByMonth.update { emptyMap() } + _emotionMarblesByMonth.update { emptyMap() } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt new file mode 100644 index 00000000..73b6eeeb --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt @@ -0,0 +1,17 @@ +package com.threegap.bitnagil.data.activitylog.datasourceImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogRemoteDataSource +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse +import com.threegap.bitnagil.data.activitylog.model.response.MonthlyBadgeResponse +import com.threegap.bitnagil.data.activitylog.service.ActivityLogService +import javax.inject.Inject + +class ActivityLogRemoteDataSourceImpl @Inject constructor( + private val activityLogService: ActivityLogService, +) : ActivityLogRemoteDataSource { + override suspend fun getBadges(year: Int, month: Int): Result = + activityLogService.getBadges(year = year, month = month) + + override suspend fun getEmotionMarbles(startDate: String, endDate: String): Result> = + activityLogService.getEmotionMarbles(startDate = startDate, endDate = endDate) +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/BadgeResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/BadgeResponse.kt new file mode 100644 index 00000000..2714a2cb --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/BadgeResponse.kt @@ -0,0 +1,45 @@ +package com.threegap.bitnagil.data.activitylog.model.response + +import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.BadgeType +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.LocalDateTime + +@Serializable +data class MonthlyBadgeResponse( + @SerialName("badgeTitle") + val badgeTitle: String, + @SerialName("badgeDescription") + val badgeDescription: String, + @SerialName("badges") + val badges: List, +) + +@Serializable +data class BadgeResponse( + @SerialName("badgeType") + val badgeType: String, + @SerialName("imageUrl") + val imageUrl: String, + @SerialName("acquiredAt") + val acquiredAt: String?, +) + +fun MonthlyBadgeResponse.toDomain(): MonthlyBadge = + MonthlyBadge( + badgeTitle = badgeTitle, + badgeDescription = badgeDescription, + badges = badges.map { it.toDomain() }, + ) + +fun BadgeResponse.toDomain(): Badge = + Badge( + type = badgeType.toBadgeType(), + imageUrl = imageUrl, + acquiredAt = acquiredAt?.let { LocalDateTime.parse(it) }, + ) + +private fun String.toBadgeType(): BadgeType = + runCatching { BadgeType.valueOf(this) }.getOrDefault(BadgeType.UNKNOWN) diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/EmotionMarbleResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/EmotionMarbleResponse.kt new file mode 100644 index 00000000..3e6ae873 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/EmotionMarbleResponse.kt @@ -0,0 +1,27 @@ +package com.threegap.bitnagil.data.activitylog.model.response + +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.LocalDate + +@Serializable +data class EmotionMarbleResponse( + @SerialName("date") + val date: String, + @SerialName("emotionMarbleType") + val emotionMarbleType: EmotionMarbleType, + @SerialName("emotionMarbleName") + val emotionMarbleName: String, + @SerialName("imageUrl") + val imageUrl: String, +) + +fun EmotionMarbleResponse.toDomain(): EmotionMarble = + EmotionMarble( + date = LocalDate.parse(date), + type = emotionMarbleType, + name = emotionMarbleName, + imageUrl = imageUrl, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt new file mode 100644 index 00000000..41ad77c3 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt @@ -0,0 +1,63 @@ +package com.threegap.bitnagil.data.activitylog.repositoryImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogLocalDataSource +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogRemoteDataSource +import com.threegap.bitnagil.data.activitylog.model.response.toDomain +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.time.YearMonth +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ActivityLogRepositoryImpl @Inject constructor( + private val activityLogRemoteDataSource: ActivityLogRemoteDataSource, + private val activityLogLocalDataSource: ActivityLogLocalDataSource, +) : ActivityLogRepository { + + private val badgeFetchMutex = Mutex() + private val emotionMarbleFetchMutex = Mutex() + + override suspend fun getBadges(yearMonth: YearMonth): Result { + activityLogLocalDataSource.badgesByMonth.value[yearMonth]?.let { return Result.success(it) } + + return badgeFetchMutex.withLock { + activityLogLocalDataSource.badgesByMonth.value[yearMonth]?.let { return@withLock Result.success(it) } + + activityLogRemoteDataSource.getBadges( + year = yearMonth.year, + month = yearMonth.monthValue, + ) + .map { it.toDomain() } + .onSuccess { activityLogLocalDataSource.saveBadges(yearMonth, it) } + } + } + + override suspend fun getEmotionMarbles(yearMonth: YearMonth, forceRefresh: Boolean): Result> { + if (!forceRefresh) { + activityLogLocalDataSource.emotionMarblesByMonth.value[yearMonth]?.let { return Result.success(it) } + } + + return emotionMarbleFetchMutex.withLock { + if (!forceRefresh) { + activityLogLocalDataSource.emotionMarblesByMonth.value[yearMonth]?.let { return@withLock Result.success(it) } + } + + activityLogRemoteDataSource.getEmotionMarbles( + startDate = yearMonth.atDay(1).toString(), + endDate = yearMonth.atEndOfMonth().toString(), + ) + .map { marbles -> marbles.map { it.toDomain() } } + .onSuccess { activityLogLocalDataSource.saveEmotionMarbles(yearMonth, it) } + } + } + + override suspend fun invalidateEmotionMarbleCache(yearMonth: YearMonth) { + emotionMarbleFetchMutex.withLock { + activityLogLocalDataSource.removeEmotionMarbles(yearMonth) + } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/service/ActivityLogService.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/service/ActivityLogService.kt new file mode 100644 index 00000000..24aaa1e2 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/service/ActivityLogService.kt @@ -0,0 +1,20 @@ +package com.threegap.bitnagil.data.activitylog.service + +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse +import com.threegap.bitnagil.data.activitylog.model.response.MonthlyBadgeResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface ActivityLogService { + @GET("/api/v1/activity-logs/badges") + suspend fun getBadges( + @Query("year") year: Int, + @Query("month") month: Int, + ): Result + + @GET("/api/v1/activity-logs/emotion-marbles") + suspend fun getEmotionMarbles( + @Query("startDate") startDate: String, + @Query("endDate") endDate: String, + ): Result> +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt index 73ed05a1..48c62a05 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt @@ -16,7 +16,9 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.time.LocalDate import javax.inject.Inject +import javax.inject.Singleton +@Singleton class EmotionRepositoryImpl @Inject constructor( private val emotionRemoteDataSource: EmotionRemoteDataSource, private val emotionLocalDataSource: EmotionLocalDataSource, @@ -36,7 +38,9 @@ class EmotionRepositoryImpl @Inject constructor( emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) + if (it.isSuccess) { + fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) + } } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/Badge.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/Badge.kt new file mode 100644 index 00000000..a59758c6 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/Badge.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.activitylog.model + +import java.time.LocalDateTime + +data class Badge( + val type: BadgeType, + val imageUrl: String, + val acquiredAt: LocalDateTime?, +) { + val acquired: Boolean get() = acquiredAt != null +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/BadgeType.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/BadgeType.kt new file mode 100644 index 00000000..6c38509c --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/BadgeType.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.domain.activitylog.model + +/** + * 활동 뱃지 타입 + * + * @property MOTIVATION_EXPERT 의욕 전문가 - 그 달 감정 구슬 선택 3회 + * @property CHECK_EXPERT 체크 전문가 - 그 달 루틴(메인) 완료 1회 + * @property OUTING_EXPERT 외출 전문가 - 그 달 제보 등록 1회 + * @property RESERVE_EXPERT 예비 전문가 - 그 달 획득 뱃지가 0개일 때 서버가 내려주는 기본 표시 + * @property UNKNOWN 서버가 내려준 값이 위 항목 중 어디에도 매칭되지 않을 때의 fallback + */ +enum class BadgeType { MOTIVATION_EXPERT, CHECK_EXPERT, OUTING_EXPERT, RESERVE_EXPERT, UNKNOWN } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/EmotionMarble.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/EmotionMarble.kt new file mode 100644 index 00000000..1831110a --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/EmotionMarble.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.activitylog.model + +import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType +import java.time.LocalDate + +data class EmotionMarble( + val date: LocalDate, + val type: EmotionMarbleType, + val name: String, + val imageUrl: String, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/MonthlyBadge.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/MonthlyBadge.kt new file mode 100644 index 00000000..4df5d055 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/MonthlyBadge.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.domain.activitylog.model + +data class MonthlyBadge( + val badgeTitle: String, + val badgeDescription: String, + val badges: List, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/repository/ActivityLogRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/repository/ActivityLogRepository.kt new file mode 100644 index 00000000..69e9288f --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/repository/ActivityLogRepository.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.domain.activitylog.repository + +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import java.time.YearMonth + +interface ActivityLogRepository { + suspend fun getBadges(yearMonth: YearMonth): Result + suspend fun getEmotionMarbles(yearMonth: YearMonth, forceRefresh: Boolean = false): Result> + suspend fun invalidateEmotionMarbleCache(yearMonth: YearMonth) +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetBadgesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetBadgesUseCase.kt new file mode 100644 index 00000000..f7c66330 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetBadgesUseCase.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.domain.activitylog.usecase + +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge +import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository +import java.time.YearMonth +import javax.inject.Inject + +class GetBadgesUseCase @Inject constructor( + private val activityLogRepository: ActivityLogRepository, +) { + suspend operator fun invoke(yearMonth: YearMonth): Result { + return activityLogRepository.getBadges(yearMonth = yearMonth) + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt new file mode 100644 index 00000000..8b3e764b --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt @@ -0,0 +1,19 @@ +package com.threegap.bitnagil.domain.activitylog.usecase + +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository +import java.time.LocalDate +import java.time.YearMonth +import javax.inject.Inject + +class GetEmotionMarblesUseCase @Inject constructor( + private val activityLogRepository: ActivityLogRepository, +) { + suspend operator fun invoke( + yearMonth: YearMonth, + forceRefresh: Boolean = false, + ): Result> { + return activityLogRepository.getEmotionMarbles(yearMonth = yearMonth, forceRefresh = forceRefresh) + .map { marbles -> marbles.associateBy { it.date } } + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt index 3e51863e..94332c44 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/RegisterEmotionUseCase.kt @@ -1,13 +1,18 @@ package com.threegap.bitnagil.domain.emotion.usecase +import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import java.time.YearMonth import javax.inject.Inject class RegisterEmotionUseCase @Inject constructor( private val emotionRepository: EmotionRepository, + private val activityLogRepository: ActivityLogRepository, ) { suspend operator fun invoke(emotionType: String): Result> { - return emotionRepository.registerEmotion(emotionType) + return emotionRepository.registerEmotion(emotionType).onSuccess { + activityLogRepository.invalidateEmotionMarbleCache(YearMonth.now()) + } } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/EmotionScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/EmotionScreen.kt index f8e85bd3..711282f8 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/EmotionScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/EmotionScreen.kt @@ -41,7 +41,7 @@ fun EmotionScreenContainer( EmotionScreenStep.Emotion -> BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { val height = constraints.maxHeight.pxToDp() - if (height > 600.dp) { + if (height >= 700.dp) { SwipeEmotionSelectionScreen( state = state, onClickPreviousButton = navigateToBack, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/component/template/SwipeEmotionSelectionScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/component/template/SwipeEmotionSelectionScreen.kt index 3abe19ee..9b1f905f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/component/template/SwipeEmotionSelectionScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/emotion/component/template/SwipeEmotionSelectionScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -263,7 +264,7 @@ private fun GestureDescriptionText( ) { Text("선택한 감정 구슬을 아래로 놓아주세요", style = BitnagilTheme.typography.body2Medium.copy(color = BitnagilTheme.colors.coolGray50)) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(10.dp)) Image( painter = painterResource(R.drawable.ic_double_down_arrow_24), @@ -317,7 +318,7 @@ private fun EmotionPager( val density = LocalDensity.current val screenWidth = with(density) { constraints.maxWidth.toDp() } - val itemSize = 140.dp + val itemSize = 132.dp val centerItemYOffset = 50.dp val contentPadding = (screenWidth - itemSize) / 2 val pageSpacing = ((screenWidth - itemSize * 2) / 2) @@ -374,9 +375,14 @@ private fun EmotionPagerItem( enabled: Boolean, onSelectEmotion: (String) -> Unit, ) { - val pageOffset = ( - (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction - ).absoluteValue + val pageOffsetState = remember(pagerState, page) { + derivedStateOf { + ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue + } + } + val isCenterPage by remember(pageOffsetState) { + derivedStateOf { pageOffsetState.value == 0f } + } val offsetY = remember { Animatable(0f) } val coroutineScope = rememberCoroutineScope() @@ -387,14 +393,14 @@ private fun EmotionPagerItem( .size(size) .aspectRatio(1f) .graphicsLayer { - translationY = lerp(start = centerItemYOffset * 1f, stop = 0f, pageOffset) + translationY = lerp(start = centerItemYOffset * 1f, stop = 0f, pageOffsetState.value) } .offset { IntOffset(0, offsetY.value.toInt()) } .draggable( orientation = Orientation.Vertical, - enabled = (pageOffset == 0f && enabled), + enabled = (isCenterPage && enabled), state = rememberDraggableState { deltaY -> coroutineScope.launch { val newOffsetY = offsetY.value + deltaY diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryScreen.kt new file mode 100644 index 00000000..07e2597e --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryScreen.kt @@ -0,0 +1,186 @@ +package com.threegap.bitnagil.presentation.screen.summary + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.screen.summary.component.template.emotiondaybottomsheet.EmotionDayBottomSheet +import com.threegap.bitnagil.presentation.screen.summary.component.template.summarybadge.SummaryBadgeView +import com.threegap.bitnagil.presentation.screen.summary.component.template.summarycalendar.SummaryCalendarView +import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.compose.collectAsState +import java.time.LocalDate +import java.time.YearMonth +import java.time.temporal.ChronoUnit + +@Composable +fun SummaryScreenContainer( + viewModel: SummaryViewModel = hiltViewModel(), + navigateToYouthPolicies: () -> Unit +) { + val state by viewModel.collectAsState() + + state.selectedEmotionDay?.let { selectedEmotionDay -> + EmotionDayBottomSheet( + onDismiss = viewModel::clearSelectedEmotionDay, + emotionDay = selectedEmotionDay, + ) + } + + SummaryScreen( + state = state, + onMonthChanged = viewModel::onMonthChanged, + onClickEmotionDay = viewModel::selectEmotionDay, + onClickYouthPolicies = navigateToYouthPolicies + ) +} + +private const val INITIAL_PAGE = Int.MAX_VALUE / 2 + +@Composable +fun SummaryScreen( + state: SummaryState, + onMonthChanged: (YearMonth) -> Unit = {}, + onClickEmotionDay: (LocalDate, SummaryEmotionCellUiModel) -> Unit = { _, _ -> }, + onClickYouthPolicies: () -> Unit, +) { + val verticalScrollState = rememberScrollState() + val pagerState = rememberPagerState(initialPage = INITIAL_PAGE) { Int.MAX_VALUE } + val scope = rememberCoroutineScope() + + // 페이지 변경 감지하여 ViewModel 업데이트 + LaunchedEffect(pagerState.currentPage) { + val monthOffset = pagerState.currentPage - INITIAL_PAGE + val targetMonth = YearMonth.now().plusMonths(monthOffset.toLong()) + if (state.currentMonth != targetMonth) { + onMonthChanged(targetMonth) + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(BitnagilTheme.colors.white) + .verticalScroll(verticalScrollState), + ) { + SummaryBadgeView( + summaryBadge = state.currentMonthBadges, + modifier = Modifier + .fillMaxWidth() + .height(360.dp), + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = BitnagilTheme.colors.orange25) + .padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(R.drawable.ic_complaint), + contentDescription = null + ) + + Text("청년 공고도 확인할 수 있어요!", style = BitnagilTheme.typography.body2SemiBold, modifier = Modifier.padding(start = 10.dp)) + + Spacer(modifier = Modifier.weight(1f)) + + Text("더보기", color = BitnagilTheme.colors.orange500, modifier = Modifier.clickableWithoutRipple(onClick = onClickYouthPolicies).padding(10.dp), style = BitnagilTheme.typography.body2SemiBold) + } + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text("감정 구슬 기록", style = BitnagilTheme.typography.title3SemiBold) + Spacer(modifier = Modifier.weight(1f)) + BitnagilIconButton( + id = R.drawable.ic_back_arrow_20, + onClick = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + modifier = Modifier.size(48.dp) + ) + Text( + "${state.currentMonth.year}년 ${state.currentMonth.monthValue}월", + style = BitnagilTheme.typography.subtitle1SemiBold + ) + BitnagilIconButton( + id = R.drawable.ic_right_arrow_20, + onClick = { + scope.launch { + pagerState.animateScrollToPage(pagerState.currentPage + 1) + } + }, + modifier = Modifier.size(48.dp) + ) + } + + Spacer(modifier = Modifier.height(36.dp)) + + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top + ) { page -> + val monthOffset = page - INITIAL_PAGE + val displayMonth = YearMonth.now().plusMonths(monthOffset.toLong()) + + SummaryCalendarView( + yearMonth = displayMonth, + emotionDays = state.emotionCellsAround(displayMonth), + onClickOtherMonthDay = { targetMonth -> + scope.launch { + val monthDiff = ChronoUnit.MONTHS.between(displayMonth, targetMonth) + pagerState.animateScrollToPage(page + monthDiff.toInt()) + } + }, + onClickEmotionDay = onClickEmotionDay, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@Preview(showBackground = true, heightDp = 800) +@Composable +private fun SummaryScreenPreview() { + BitnagilTheme { + SummaryScreen(state = SummaryState.INIT, onClickYouthPolicies = {}) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryViewModel.kt new file mode 100644 index 00000000..87be4ce0 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryViewModel.kt @@ -0,0 +1,108 @@ +package com.threegap.bitnagil.presentation.screen.summary + +import android.util.Log +import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.domain.activitylog.usecase.GetBadgesUseCase +import com.threegap.bitnagil.domain.activitylog.usecase.GetEmotionMarblesUseCase +import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionDayUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.toUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import java.time.LocalDate +import java.time.YearMonth +import javax.inject.Inject + +@HiltViewModel +class SummaryViewModel @Inject constructor( + private val getBadgesUseCase: GetBadgesUseCase, + private val getEmotionMarblesUseCase: GetEmotionMarblesUseCase, +) : ContainerHost, ViewModel() { + + override val container = container(initialState = SummaryState.INIT) + + init { + onMonthChanged(YearMonth.now()) + } + + fun onMonthChanged(newMonth: YearMonth) = intent { + reduce { state.copy(currentMonth = newMonth) } + + // 현재, 이전, 다음 달 데이터 프리페칭. 이미 캐시된 달은 Repository가 즉시 반환한다. + val monthsToLoad = listOf( + newMonth.minusMonths(1), + newMonth, + newMonth.plusMonths(1), + ) + + coroutineScope { + monthsToLoad.forEach { targetMonth -> + launch { fetchBadges(targetMonth) } + launch { fetchEmotionMarbles(targetMonth) } + } + } + } + + fun selectEmotionDay(date: LocalDate, emotionCell: SummaryEmotionCellUiModel) = intent { + reduce { + state.copy( + selectedEmotionDay = SummaryEmotionDayUiModel( + date = date, + emotionType = emotionCell.emotionType, + imageUrl = emotionCell.imageUrl, + ), + ) + } + } + + fun clearSelectedEmotionDay() = intent { + reduce { state.copy(selectedEmotionDay = null) } + } + + private suspend fun fetchBadges(yearMonth: YearMonth) { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + + getBadgesUseCase(yearMonth).fold( + onSuccess = { monthlyBadge -> + reduce { + state.copy( + badgesByMonth = state.badgesByMonth + (yearMonth to monthlyBadge.toUiModel()), + loadingCount = state.loadingCount - 1, + ) + } + }, + onFailure = { + Log.e("SummaryViewModel", "뱃지 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) + } + } + + private suspend fun fetchEmotionMarbles(yearMonth: YearMonth) { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + + getEmotionMarblesUseCase(yearMonth).fold( + onSuccess = { marblesByDate -> + val emotionCells = marblesByDate.values.map { it.toUiModel() } + reduce { + state.copy( + emotionCellsByMonth = state.emotionCellsByMonth + (yearMonth to emotionCells), + loadingCount = state.loadingCount - 1, + ) + } + }, + onFailure = { + Log.e("SummaryViewModel", "감정 구슬 가져오기 실패: ${it.message}") + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/emotiondaybottomsheet/EmotionDayBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/emotiondaybottomsheet/EmotionDayBottomSheet.kt new file mode 100644 index 00000000..f2c4aeb1 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/emotiondaybottomsheet/EmotionDayBottomSheet.kt @@ -0,0 +1,140 @@ +package com.threegap.bitnagil.presentation.screen.summary.component.template.emotiondaybottomsheet + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionDayUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionType +import kotlinx.coroutines.launch +import java.time.LocalDate + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun EmotionDayBottomSheet( + onDismiss: () -> Unit, + emotionDay: SummaryEmotionDayUiModel, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = onDismiss, + containerColor = BitnagilTheme.colors.coolGray99, + contentColor = BitnagilTheme.colors.coolGray99, + dragHandle = null, + ) { + EmotionDayBottomSheetContent( + onDismiss = { + coroutineScope.launch { sheetState.hide() } + .invokeOnCompletion { + if (!sheetState.isVisible) { + onDismiss() + } + } + }, + emotionDay = emotionDay, + ) + } +} + +@Composable +private fun EmotionDayBottomSheetContent( + onDismiss: () -> Unit, + emotionDay: SummaryEmotionDayUiModel, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 18.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = emotionDay.dayText, + style = BitnagilTheme.typography.title3SemiBold, + color = BitnagilTheme.colors.coolGray10, + maxLines = 1, + modifier = Modifier + .padding(horizontal = 24.dp), + ) + + BitnagilIconButton( + id = R.drawable.ic_close, + onClick = onDismiss, + paddingValues = PaddingValues(12.dp), + tint = BitnagilTheme.colors.coolGray10, + modifier = Modifier.size(48.dp), + ) + } + + Text( + emotionDay.emotionText, + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray40, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(emotionDay.imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier.fillMaxWidth() + .height(200.dp) + .padding(start = 24.dp, end = 24.dp, bottom = 18.dp) + .background( + color = BitnagilTheme.colors.white, + shape = RoundedCornerShape(12.dp), + ) + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun EmotionDayBottomSheetContentPreview() { + EmotionDayBottomSheetContent( + onDismiss = {}, + emotionDay = SummaryEmotionDayUiModel( + date = LocalDate.now(), + emotionType = SummaryEmotionType.SATISFACTION, + imageUrl = "", + ), + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarybadge/SummaryBadgeView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarybadge/SummaryBadgeView.kt new file mode 100644 index 00000000..c1757bd5 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarybadge/SummaryBadgeView.kt @@ -0,0 +1,256 @@ +package com.threegap.bitnagil.presentation.screen.summary.component.template.summarybadge + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.presentation.screen.summary.model.BadgeImage +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryBadgeItemUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryBadgeTypeUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryBadgeUiModel +import com.threegap.bitnagil.presentation.util.dimension.pxToDp + +@Composable +fun SummaryBadgeView( + summaryBadge: SummaryBadgeUiModel?, + modifier: Modifier = Modifier +) { + Background( + modifier = modifier.clipToBounds() + ) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.Center) + ) { + summaryBadge?.let { + Text( + summaryBadge.badgeDescription, + style = BitnagilTheme.typography.cafe24SsurroundAir, + color = BitnagilTheme.colors.white, + textAlign = TextAlign.Center + ) + + SummaryBadgeListView( + badges = summaryBadge.badges + ) + + BadgeTitleView( + badgeTitle = summaryBadge.badgeTitle, + isBadgeReserved = summaryBadge.badges.firstOrNull()?.type?.isReserved ?: false, + modifier = Modifier + ) + } + } + } +} + +@Composable +private fun Background( + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + BoxWithConstraints( + modifier = modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + 0.0f to Color(0xFFFF964B), + 0.3f to Color(0xFFFE7120), + ), + ) + .statusBarsPadding(), + ) { + val width = constraints.maxWidth.pxToDp() + + StarImage( + modifier = Modifier + .width(width * 0.55f) + .aspectRatio(1f) + .offset(x = (-93).dp, y = (-81).dp) + .rotate(103f) + ) + + StarImage( + modifier = Modifier + .width(width * 0.13f) + .aspectRatio(1f) + .offset(x = 24.dp, y = 188.dp) + .rotate(24f) + ) + + StarImage( + modifier = Modifier + .width(width * 0.06f) + .aspectRatio(1f) + .offset(x = 50.dp, y = 270.dp) + .rotate(330f) + ) + + StarImage( + modifier = Modifier + .width(width * 0.83f) + .aspectRatio(1f) + .align(Alignment.BottomEnd) + .offset(x = 90.dp, y = 90.dp) + .rotate(330f), + ) + + StarImage( + modifier = Modifier + .width(width * 0.18f) + .aspectRatio(1f) + .align(Alignment.TopEnd) + .offset(x = 30.dp, y = 50.dp) + .rotate(342f), + color = BitnagilTheme.colors.orange500 + ) + + content() + } +} + +@Composable +private fun StarImage( + modifier: Modifier = Modifier, + color: Color = BitnagilTheme.colors.orange400, + contentScale: ContentScale = ContentScale.Fit +) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_shine), + contentDescription = null, + colorFilter = ColorFilter.tint(color), + contentScale = contentScale, + modifier = modifier + ) +} + +@Composable +private fun SummaryBadgeListView( + badges: List, + modifier: Modifier = Modifier, +) { + val isSingleItem = badges.size <= 1 + + Row( + modifier = modifier.fillMaxWidth().padding(horizontal = 30.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + badges.forEach { badge -> + if (isSingleItem) + SummaryBadgeItemView( + badge = badge, + modifier = Modifier.size(125.dp).aspectRatio(1f) + ) + else + SummaryBadgeItemView( + badge = badge, + modifier = Modifier.widthIn(max = 100.dp).weight(1f, fill = false) + ) + } + } +} + +@Composable +private fun SummaryBadgeItemView( + badge: SummaryBadgeItemUiModel, + modifier: Modifier = Modifier, +) { + when (val image = badge.image) { + BadgeImage.Default -> + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_badge_question), + contentDescription = null, + modifier = modifier.aspectRatio(1f).padding(9.dp) + ) + + is BadgeImage.Remote -> + AsyncImage( + model = image.url, + contentDescription = null, + modifier = modifier.aspectRatio(1f) + ) + } +} + +@Composable +private fun BadgeTitleView( + badgeTitle: String, + isBadgeReserved: Boolean, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .background(color = BitnagilTheme.colors.orange700, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Image( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_shine), + contentDescription = null, + modifier = Modifier.size(15.dp), + colorFilter = ColorFilter.tint(if (isBadgeReserved) BitnagilTheme.colors.coolGray95 else BitnagilTheme.colors.kakao) + ) + + Text( + text = badgeTitle, + style = BitnagilTheme.typography.caption1Medium, + color = BitnagilTheme.colors.white + ) + } +} + +@Preview(showBackground = true, heightDp = 360) +@Composable +private fun SummaryBadgePreview() { + BitnagilTheme { + SummaryBadgeView( + summaryBadge = SummaryBadgeUiModel( + badgeTitle = "외출 전문가", + badges = listOf( + SummaryBadgeItemUiModel( + type = SummaryBadgeTypeUiModel.Unknown, + image = BadgeImage.Default, + acquired = false, + ), + ), + badgeDescription = "덕분에 도시가\n개선되고 있어요!" + ), + modifier = Modifier + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarycalendar/SummaryCalendarView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarycalendar/SummaryCalendarView.kt new file mode 100644 index 00000000..6f12ef61 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarycalendar/SummaryCalendarView.kt @@ -0,0 +1,158 @@ +package com.threegap.bitnagil.presentation.screen.summary.component.template.summarycalendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionType +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun SummaryCalendarView( + yearMonth: YearMonth, + emotionDays: List, + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, + onClickOtherMonthDay: (YearMonth) -> Unit = {}, + onClickEmotionDay: (LocalDate, SummaryEmotionCellUiModel) -> Unit = { _, _ -> }, +) { + val firstDayOfMonth = yearMonth.atDay(1) + + // 시작 요일에 맞춰 이전 달의 며칠을 가져올지 계산 + val firstDayOfWeekValue = firstDayOfWeek.value + val daysFromPrevMonth = (firstDayOfMonth.dayOfWeek.value - firstDayOfWeekValue + 7) % 7 + val startDate = firstDayOfMonth.minusDays(daysFromPrevMonth.toLong()) + + Column(modifier = modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth()) { + val dayOfWeekOrder = List(7) { i -> + DayOfWeek.of((firstDayOfWeekValue + i - 1) % 7 + 1) + } + + dayOfWeekOrder.forEach { dayOfWeek -> + Text( + text = dayNames[dayOfWeek] ?: "", + modifier = Modifier.weight(1f), + textAlign = TextAlign.Center, + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray40 + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + repeat(6) { weekIndex -> + Row(modifier = Modifier.fillMaxWidth()) { + repeat(7) { dayIndex -> + val date = startDate.plusDays((weekIndex * 7 + dayIndex).toLong()) + val isCurrentMonth = date.monthValue == yearMonth.monthValue + val emotionCellUiModel = emotionDays.firstOrNull { it.date == date } + + // 이전/다음 달 날짜는 해당 달로 이동하고, 이번 달 날짜는 감정이 기록된 경우에만 선택할 수 있다. + val onClick: (() -> Unit)? = when { + !isCurrentMonth -> { + { onClickOtherMonthDay(YearMonth.from(date)) } + } + emotionCellUiModel != null -> { + { onClickEmotionDay(date, emotionCellUiModel) } + } + else -> null + } + + SummaryCalendarCell( + isCurrentMonth = isCurrentMonth, + emotionType = emotionCellUiModel?.emotionType, + day = date.dayOfMonth, + onClick = onClick, + modifier = Modifier.weight(1f), + ) + } + } + } + } +} + +private val dayNames = mapOf( + DayOfWeek.MONDAY to "월", + DayOfWeek.TUESDAY to "화", + DayOfWeek.WEDNESDAY to "수", + DayOfWeek.THURSDAY to "목", + DayOfWeek.FRIDAY to "금", + DayOfWeek.SATURDAY to "토", + DayOfWeek.SUNDAY to "일", +) + +@Composable +fun SummaryCalendarCell( + isCurrentMonth: Boolean, + emotionType: SummaryEmotionType?, + day: Int, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + Box( + modifier = modifier + .aspectRatio(1f) + .then(if (onClick != null) Modifier.clickableWithoutRipple(onClick = onClick) else Modifier), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(34.dp) + .background( + color = emotionType?.let { if (isCurrentMonth) it.backgroundColor else BitnagilTheme.colors.coolGray98 } ?: Color.Transparent, + shape = CircleShape, + ), + ) + + Text( + text = day.toString(), + style = BitnagilTheme.typography.body1Medium, + color = if (isCurrentMonth) emotionType?.textColor ?: BitnagilTheme.colors.coolGray10 else BitnagilTheme.colors.coolGray80, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun SummaryCalendarPreview() { + val currentMonth = YearMonth.now() + val prevMonth = currentMonth.minusMonths(1) + val nextMonth = currentMonth.plusMonths(1) + BitnagilTheme { + SummaryCalendarView( + yearMonth = currentMonth, + emotionDays = listOf( + SummaryEmotionCellUiModel(prevMonth.atEndOfMonth(), SummaryEmotionType.CALM, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(6), SummaryEmotionType.CALM, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(8), SummaryEmotionType.ANXIETY, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(9), SummaryEmotionType.VITALITY, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(11), SummaryEmotionType.LETHARGY, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(14), SummaryEmotionType.SATISFACTION, ""), + SummaryEmotionCellUiModel(currentMonth.atDay(16), SummaryEmotionType.FATIGUE, ""), + SummaryEmotionCellUiModel(nextMonth.atDay(1), SummaryEmotionType.FATIGUE, ""), + ), + firstDayOfWeek = DayOfWeek.SUNDAY + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/contract/SummaryState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/contract/SummaryState.kt new file mode 100644 index 00000000..ad1c6eba --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/contract/SummaryState.kt @@ -0,0 +1,36 @@ +package com.threegap.bitnagil.presentation.screen.summary.contract + +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryBadgeUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionDayUiModel +import java.time.YearMonth + +data class SummaryState( + val loadingCount: Int, + val currentMonth: YearMonth, + val emotionCellsByMonth: Map>, + val badgesByMonth: Map, + val selectedEmotionDay: SummaryEmotionDayUiModel?, +) { + val isLoading: Boolean + get() = loadingCount > 0 + + val currentMonthBadges: SummaryBadgeUiModel? + get() = badgesByMonth[currentMonth] + + fun emotionCellsOf(yearMonth: YearMonth): List = + emotionCellsByMonth[yearMonth].orEmpty() + + fun emotionCellsAround(yearMonth: YearMonth): List = + emotionCellsOf(yearMonth.minusMonths(1)) + emotionCellsOf(yearMonth) + emotionCellsOf(yearMonth.plusMonths(1)) + + companion object { + val INIT = SummaryState( + loadingCount = 0, + currentMonth = YearMonth.now(), + emotionCellsByMonth = emptyMap(), + badgesByMonth = emptyMap(), + selectedEmotionDay = null, + ) + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/BadgeImage.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/BadgeImage.kt new file mode 100644 index 00000000..708d2378 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/BadgeImage.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +sealed interface BadgeImage { + data class Remote(val url: String) : BadgeImage + data object Default : BadgeImage +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeTypeUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeTypeUiModel.kt new file mode 100644 index 00000000..0bf15fcc --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeTypeUiModel.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import com.threegap.bitnagil.domain.activitylog.model.BadgeType + +sealed class SummaryBadgeTypeUiModel( + val isReserved: Boolean = false +) { + data object MotivationExpert : SummaryBadgeTypeUiModel() + data object CheckExpert : SummaryBadgeTypeUiModel() + data object OutingExpert : SummaryBadgeTypeUiModel() + data object ReserveExpert : SummaryBadgeTypeUiModel(isReserved = true) + data object Unknown : SummaryBadgeTypeUiModel(isReserved = true) +} + +fun BadgeType.toUiModel(): SummaryBadgeTypeUiModel = + when (this) { + BadgeType.MOTIVATION_EXPERT -> SummaryBadgeTypeUiModel.MotivationExpert + BadgeType.CHECK_EXPERT -> SummaryBadgeTypeUiModel.CheckExpert + BadgeType.OUTING_EXPERT -> SummaryBadgeTypeUiModel.OutingExpert + BadgeType.RESERVE_EXPERT -> SummaryBadgeTypeUiModel.ReserveExpert + BadgeType.UNKNOWN -> SummaryBadgeTypeUiModel.Unknown + } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeUiModel.kt new file mode 100644 index 00000000..f15bbfd4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeUiModel.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge + +data class SummaryBadgeUiModel( + val badgeTitle: String, + val badgeDescription: String, + val badges: List, +) + +data class SummaryBadgeItemUiModel( + val type: SummaryBadgeTypeUiModel, + val image: BadgeImage, + val acquired: Boolean, +) + +fun MonthlyBadge.toUiModel(): SummaryBadgeUiModel = + SummaryBadgeUiModel( + badgeTitle = badgeTitle, + badgeDescription = badgeDescription, + badges = badges.map { it.toUiModel() }, + ) + +fun Badge.toUiModel(): SummaryBadgeItemUiModel { + val uiType = type.toUiModel() + return SummaryBadgeItemUiModel( + type = uiType, + image = if (uiType == SummaryBadgeTypeUiModel.Unknown) BadgeImage.Default else BadgeImage.Remote(imageUrl), + acquired = acquired, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionCellUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionCellUiModel.kt new file mode 100644 index 00000000..efa729d8 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionCellUiModel.kt @@ -0,0 +1,28 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType +import java.time.LocalDate + +data class SummaryEmotionCellUiModel( + val date: LocalDate, + val emotionType: SummaryEmotionType, + val imageUrl: String, +) + +fun EmotionMarble.toUiModel(): SummaryEmotionCellUiModel = + SummaryEmotionCellUiModel( + date = date, + emotionType = type.toUiModel(), + imageUrl = imageUrl, + ) + +fun EmotionMarbleType.toUiModel(): SummaryEmotionType = + when (this) { + EmotionMarbleType.CALM -> SummaryEmotionType.CALM + EmotionMarbleType.VITALITY -> SummaryEmotionType.VITALITY + EmotionMarbleType.LETHARGY -> SummaryEmotionType.LETHARGY + EmotionMarbleType.ANXIETY -> SummaryEmotionType.ANXIETY + EmotionMarbleType.SATISFACTION -> SummaryEmotionType.SATISFACTION + EmotionMarbleType.FATIGUE -> SummaryEmotionType.FATIGUE + } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionDayUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionDayUiModel.kt new file mode 100644 index 00000000..04471fb6 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionDayUiModel.kt @@ -0,0 +1,22 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import java.time.LocalDate + +data class SummaryEmotionDayUiModel( + val date: LocalDate, + val emotionType: SummaryEmotionType, + val imageUrl: String, +) { + val dayText: String + get() = "${date.year}년 ${date.monthValue}월 ${date.dayOfMonth}일의 감정" + + val emotionText: String + get() = when(emotionType) { + SummaryEmotionType.CALM -> "이날은 평온했나봐요! 평온함은 마음이 고요하고 편안해 균형을 이루는 상태예요." + SummaryEmotionType.VITALITY -> "이날은 활기찼나봐요! 활기참은 생기가 가득 차 활발하고 적극적인 상태예요." + SummaryEmotionType.LETHARGY -> "이날은 무기력했나봐요! 무기력함은 의욕이 없어 아무것도 하기 힘든 상태예요." + SummaryEmotionType.ANXIETY -> "이날은 불안했나봐요! 불안함은 마음이 불안정하고 쉽게 안심하기 어려운 상태예요." + SummaryEmotionType.SATISFACTION -> "이날은 만족스러웠나봐요! 만족함은 기대가 충족되어 더 바랄 것이 없는 상태예요." + SummaryEmotionType.FATIGUE -> "이날은 피곤했나봐요! 피곤함은 몸과 마음이 지쳐 휴식이 필요한 상태예요." + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionType.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionType.kt new file mode 100644 index 00000000..17c981ef --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionType.kt @@ -0,0 +1,16 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import androidx.compose.ui.graphics.Color + +enum class SummaryEmotionType( + val backgroundColor: Color, + val textColor: Color, + val displayName: String, +) { + CALM(Color(0xFFEFECFF), Color(0xFF692BD0), "평온함"), + VITALITY(Color(0xFFE9FAD0), Color(0xFF609F00), "활기참"), + LETHARGY(Color(0xFFEAEBEC), Color(0xFF5A5C63), "무기력함"), + ANXIETY(Color(0xFFFFEEE4), Color(0xFFFE7120), "불안함"), + SATISFACTION(Color(0xFFE2F3F6), Color(0xFF26A792), "만족함"), + FATIGUE(Color(0xFFFFE1E1), Color(0xFFFF5151), "피곤함") +}