From 0146d07c1e9a6ea828c031d86f0dd0d3e4e6f25f Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 12 Jul 2026 21:38:55 +0900 Subject: [PATCH 01/20] =?UTF-8?q?FEATURE:=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=8B=AC=EB=A0=A5=20UI=20=EC=B4=88?= =?UTF-8?q?=EC=95=88=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/summary/SummaryScreen.kt | 181 ++++++++++++++++++ .../screen/summary/SummaryViewModel.kt | 57 ++++++ .../summarycalendar/SummaryCalendarView.kt | 131 +++++++++++++ .../model/SummaryEmotionCellUiModel.kt | 6 + .../summary/model/SummaryEmotionType.kt | 15 ++ .../screen/summary/model/SummaryUiModel.kt | 9 + 6 files changed, 399 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryScreen.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryViewModel.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarycalendar/SummaryCalendarView.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionCellUiModel.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionType.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt 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..83d45c78 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryScreen.kt @@ -0,0 +1,181 @@ +package com.threegap.bitnagil.presentation.screen.summary + +import androidx.compose.foundation.Image +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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.component.template.summarycalendar.SummaryCalendarView +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryUiState +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.compose.collectAsState +import java.time.YearMonth + +@Composable +fun SummaryScreenContainer( + viewModel: SummaryViewModel, +) { + val state by viewModel.collectAsState() + + SummaryScreen( + state = state, + onMonthChanged = viewModel::onMonthChanged + ) +} + +private const val INITIAL_PAGE = Int.MAX_VALUE / 2 + +@Composable +fun SummaryScreen( + state: SummaryUiState, + onMonthChanged: (YearMonth) -> 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), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.linearGradient( + 0.6996f to Color(0xFFFE7120), + 0.9939f to Color(0xFFFF964B), + start = Offset(0f, Float.POSITIVE_INFINITY), + end = Offset(Float.POSITIVE_INFINITY, 0f), + ), + ) + .statusBarsPadding() + .height(260.dp), + ) { + Text( + "덕분에 도시가\n개선되고 있어요!", + style = BitnagilTheme.typography.cafe24SsurroundAir, + color = Color.White, + modifier = Modifier.padding(top = 40.dp, start = 20.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.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()) + val highlightedDays = state.summaryEmotionDaysMap[displayMonth] ?: emptyList() + + SummaryCalendarView( + yearMonth = displayMonth, + emotionDays = highlightedDays, + modifier = Modifier.padding(horizontal = 20.dp) + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } +} + +@Preview(showBackground = true, heightDp = 800) +@Composable +private fun SummaryScreenPreview() { + BitnagilTheme { + SummaryScreen(state = SummaryUiState()) + } +} 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..ceb31e08 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/SummaryViewModel.kt @@ -0,0 +1,57 @@ +package com.threegap.bitnagil.presentation.screen.summary + +import androidx.lifecycle.ViewModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.ContainerHost +import org.orbitmvi.orbit.viewmodel.container +import java.time.YearMonth +import javax.inject.Inject + +// 임시 UseCase 인터페이스 정의 +interface GetHighlightedDaysUseCase { + suspend operator fun invoke(year: Int, month: Int): List +} + +@HiltViewModel +class SummaryViewModel @Inject constructor( + private val getHighlightedDaysUseCase: GetHighlightedDaysUseCase +) : ContainerHost, ViewModel() { + + override val container = container(SummaryUiState()) + + init { + onMonthChanged(YearMonth.now()) + } + + fun onMonthChanged(newMonth: YearMonth) = intent { + reduce { state.copy(currentMonth = newMonth) } + + // 현재, 이전, 다음 달 데이터 프리페칭 + val monthsToLoad = listOf( + newMonth.minusMonths(1), + newMonth, + newMonth.plusMonths(1) + ) + + monthsToLoad.forEach { targetMonth -> + fetchHighlightedDays(targetMonth) + } + } + + private fun fetchHighlightedDays(yearMonth: YearMonth) = intent { + // 이미 데이터가 있는 경우 중복 호출 방지 + if (state.summaryEmotionDaysMap.containsKey(yearMonth)) return@intent + + runCatching { + getHighlightedDaysUseCase(yearMonth.year, yearMonth.monthValue) + }.onSuccess { highlightedDays -> + reduce { + state.copy( + summaryEmotionDaysMap = state.summaryEmotionDaysMap + (yearMonth to highlightedDays) + ) + } + } + } +} 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..665156ea --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarycalendar/SummaryCalendarView.kt @@ -0,0 +1,131 @@ +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.presentation.screen.summary.model.SummaryEmotionCellUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionType +import java.time.DayOfWeek +import java.time.YearMonth + +@Composable +fun SummaryCalendarView( + yearMonth: YearMonth, + emotionDays: List, + modifier: Modifier = Modifier, + firstDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, +) { + 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 = if (isCurrentMonth) emotionDays.firstOrNull { it.day == date.dayOfMonth } else null + + SummaryCalendarCell( + isCurrentMonth = isCurrentMonth, + emotionType = emotionCellUiModel?.emotionType, + day = date.dayOfMonth, + 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, +) { + Box( + modifier = modifier.aspectRatio(1f), + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(34.dp) + .background(color = emotionType?.backgroundColor ?: 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() { + BitnagilTheme { + SummaryCalendarView( + yearMonth = YearMonth.now(), + emotionDays = listOf( + SummaryEmotionCellUiModel(6, SummaryEmotionType.CALM), + SummaryEmotionCellUiModel(8, SummaryEmotionType.ANXIETY), + SummaryEmotionCellUiModel(9, SummaryEmotionType.VITALITY), + SummaryEmotionCellUiModel(11, SummaryEmotionType.LETHARGY), + SummaryEmotionCellUiModel(14, SummaryEmotionType.SATISFACTION), + SummaryEmotionCellUiModel(16, SummaryEmotionType.FATIGUE), + ), + firstDayOfWeek = DayOfWeek.SUNDAY + ) + } +} 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..f2487add --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionCellUiModel.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +data class SummaryEmotionCellUiModel( + val day: Int, + val emotionType: SummaryEmotionType, +) 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..69a4cc04 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionType.kt @@ -0,0 +1,15 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import androidx.compose.ui.graphics.Color + +enum class SummaryEmotionType( + val backgroundColor: Color, + val textColor: Color +) { + 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)) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt new file mode 100644 index 00000000..6aa7a002 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import java.time.YearMonth + +data class SummaryUiState( + val currentMonth: YearMonth = YearMonth.now(), + val summaryEmotionDaysMap: Map> = emptyMap(), + val isLoading: Boolean = false +) From abc612f3cbde20f8c5e5398a80731b912dd991db Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Tue, 14 Jul 2026 20:58:23 +0900 Subject: [PATCH 02/20] =?UTF-8?q?FEATURE:=20=EC=9B=94=EB=B3=84=20=ED=99=9C?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=EC=A7=80=20API=20=E2=80=94=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=C2=B7=20=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20API=ED=98=B8=EC=B6=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?,=20UseCase=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/di/data/DataSourceModule.kt | 6 ++++ .../bitnagil/di/data/RepositoryModule.kt | 6 ++++ .../bitnagil/di/data/ServiceModule.kt | 5 ++++ .../datasource/ActivityLogDataSource.kt | 9 ++++++ .../ActivityLogDataSourceImpl.kt | 17 +++++++++++ .../model/response/BadgeResponse.kt | 30 +++++++++++++++++++ .../model/response/EmotionMarbleResponse.kt | 27 +++++++++++++++++ .../ActivityLogRepositoryImpl.kt | 28 +++++++++++++++++ .../activitylog/service/ActivityLogService.kt | 20 +++++++++++++ .../domain/activitylog/model/Badge.kt | 13 ++++++++ .../domain/activitylog/model/BadgeType.kt | 14 +++++++++ .../domain/activitylog/model/EmotionMarble.kt | 11 +++++++ .../repository/ActivityLogRepository.kt | 11 +++++++ .../activitylog/usecase/GetBadgesUseCase.kt | 14 +++++++++ .../usecase/GetEmotionMarblesUseCase.kt | 15 ++++++++++ 15 files changed, 226 insertions(+) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/BadgeResponse.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/EmotionMarbleResponse.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/service/ActivityLogService.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/Badge.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/BadgeType.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/EmotionMarble.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/repository/ActivityLogRepository.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetBadgesUseCase.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt 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..1c07ac3f 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,7 @@ package com.threegap.bitnagil.di.data +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource +import com.threegap.bitnagil.data.activitylog.datasourceImpl.ActivityLogDataSourceImpl 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 +95,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindReportDataSource(impl: ReportDataSourceImpl): ReportDataSource + + @Binds + @Singleton + abstract fun bindActivityLogDataSource(impl: ActivityLogDataSourceImpl): ActivityLogDataSource } 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/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt new file mode 100644 index 00000000..013b4e7f --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.data.activitylog.datasource + +import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse + +interface ActivityLogDataSource { + 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/ActivityLogDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt new file mode 100644 index 00000000..79539441 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt @@ -0,0 +1,17 @@ +package com.threegap.bitnagil.data.activitylog.datasourceImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource +import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse +import com.threegap.bitnagil.data.activitylog.service.ActivityLogService +import javax.inject.Inject + +class ActivityLogDataSourceImpl @Inject constructor( + private val activityLogService: ActivityLogService, +) : ActivityLogDataSource { + 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..94800c5a --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/model/response/BadgeResponse.kt @@ -0,0 +1,30 @@ +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 kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.time.LocalDateTime + +@Serializable +data class BadgeResponse( + @SerialName("badgeType") + val badgeType: BadgeType, + @SerialName("title") + val title: String, + @SerialName("description") + val description: String, + @SerialName("imageUrl") + val imageUrl: String, + @SerialName("acquiredAt") + val acquiredAt: String?, +) + +fun BadgeResponse.toDomain(): Badge = + Badge( + type = badgeType, + title = title, + description = description, + imageUrl = imageUrl, + acquiredAt = acquiredAt?.let { LocalDateTime.parse(it) }, + ) 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..8075ee69 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt @@ -0,0 +1,28 @@ +package com.threegap.bitnagil.data.activitylog.repositoryImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource +import com.threegap.bitnagil.data.activitylog.model.response.toDomain +import com.threegap.bitnagil.domain.activitylog.model.Badge +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 ActivityLogRepositoryImpl @Inject constructor( + private val activityLogDataSource: ActivityLogDataSource, +) : ActivityLogRepository { + override suspend fun getBadges(yearMonth: YearMonth): Result> { + return activityLogDataSource.getBadges( + year = yearMonth.year, + month = yearMonth.monthValue, + ).map { badges -> badges.map { it.toDomain() } } + } + + override suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate): Result> { + return activityLogDataSource.getEmotionMarbles( + startDate = startDate.toString(), + endDate = endDate.toString(), + ).map { marbles -> marbles.map { it.toDomain() } } + } +} 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..c039e3c1 --- /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.BadgeResponse +import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse +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/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..6c206947 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/Badge.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.domain.activitylog.model + +import java.time.LocalDateTime + +data class Badge( + val type: BadgeType, + val title: String, + val description: String, + 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..b13b23ea --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/BadgeType.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.domain.activitylog.model + +import kotlinx.serialization.Serializable + +/** + * 활동 뱃지 타입 + * + * @property MOTIVATION_EXPERT 의욕 전문가 - 그 달 감정 구슬 선택 3회 + * @property CHECK_EXPERT 체크 전문가 - 그 달 루틴(메인) 완료 1회 + * @property OUTING_EXPERT 외출 전문가 - 그 달 제보 등록 1회 + * @property RESERVE_EXPERT 예비 전문가 - 그 달 획득 뱃지가 0개일 때 기본 표시 + */ +@Serializable +enum class BadgeType { MOTIVATION_EXPERT, CHECK_EXPERT, OUTING_EXPERT, RESERVE_EXPERT } 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/repository/ActivityLogRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/repository/ActivityLogRepository.kt new file mode 100644 index 00000000..e294ca73 --- /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.Badge +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import java.time.LocalDate +import java.time.YearMonth + +interface ActivityLogRepository { + suspend fun getBadges(yearMonth: YearMonth): Result> + suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate): Result> +} 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..4e9d7682 --- /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.Badge +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..12eec897 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt @@ -0,0 +1,15 @@ +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 javax.inject.Inject + +class GetEmotionMarblesUseCase @Inject constructor( + private val activityLogRepository: ActivityLogRepository, +) { + suspend operator fun invoke(startDate: LocalDate, endDate: LocalDate): Result> { + return activityLogRepository.getEmotionMarbles(startDate = startDate, endDate = endDate) + .map { marbles -> marbles.associateBy { it.date } } + } +} From 7f1aa11a8035246dc332835c10f530418280fcbd Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Tue, 14 Jul 2026 20:59:46 +0900 Subject: [PATCH 03/20] =?UTF-8?q?FEATURE:=20=EB=B1=83=EC=A7=80=20=C2=B7=20?= =?UTF-8?q?=EC=9B=94=EB=B3=84=20=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20UseCase=EB=A5=BC=20SummaryScreenViewModel?= =?UTF-8?q?=EC=97=90=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/summary/SummaryScreen.kt | 12 +-- .../screen/summary/SummaryViewModel.kt | 95 ++++++++++++++----- .../screen/summary/contract/SummaryState.kt | 30 ++++++ .../summary/model/SummaryBadgeUiModel.kt | 18 ++++ .../model/SummaryEmotionCellUiModel.kt | 19 ++++ .../screen/summary/model/SummaryUiModel.kt | 9 -- 6 files changed, 143 insertions(+), 40 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/contract/SummaryState.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeUiModel.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt 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 index 83d45c78..96cc8060 100644 --- 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 @@ -28,18 +28,19 @@ import androidx.compose.ui.graphics.Color 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.presentation.screen.summary.component.template.summarycalendar.SummaryCalendarView -import com.threegap.bitnagil.presentation.screen.summary.model.SummaryUiState +import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import java.time.YearMonth @Composable fun SummaryScreenContainer( - viewModel: SummaryViewModel, + viewModel: SummaryViewModel = hiltViewModel(), ) { val state by viewModel.collectAsState() @@ -53,7 +54,7 @@ private const val INITIAL_PAGE = Int.MAX_VALUE / 2 @Composable fun SummaryScreen( - state: SummaryUiState, + state: SummaryState, onMonthChanged: (YearMonth) -> Unit = {} ) { val verticalScrollState = rememberScrollState() @@ -159,11 +160,10 @@ fun SummaryScreen( ) { page -> val monthOffset = page - INITIAL_PAGE val displayMonth = YearMonth.now().plusMonths(monthOffset.toLong()) - val highlightedDays = state.summaryEmotionDaysMap[displayMonth] ?: emptyList() SummaryCalendarView( yearMonth = displayMonth, - emotionDays = highlightedDays, + emotionDays = state.emotionCellsOf(displayMonth), modifier = Modifier.padding(horizontal = 20.dp) ) } @@ -176,6 +176,6 @@ fun SummaryScreen( @Composable private fun SummaryScreenPreview() { BitnagilTheme { - SummaryScreen(state = SummaryUiState()) + SummaryScreen(state = SummaryState.INIT) } } 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 index ceb31e08..d9a8bdc0 100644 --- 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 @@ -1,25 +1,30 @@ package com.threegap.bitnagil.presentation.screen.summary +import android.util.Log import androidx.lifecycle.ViewModel -import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel -import com.threegap.bitnagil.presentation.screen.summary.model.SummaryUiState +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.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.YearMonth import javax.inject.Inject -// 임시 UseCase 인터페이스 정의 -interface GetHighlightedDaysUseCase { - suspend operator fun invoke(year: Int, month: Int): List -} - @HiltViewModel class SummaryViewModel @Inject constructor( - private val getHighlightedDaysUseCase: GetHighlightedDaysUseCase -) : ContainerHost, ViewModel() { + private val getBadgesUseCase: GetBadgesUseCase, + private val getEmotionMarblesUseCase: GetEmotionMarblesUseCase, +) : ContainerHost, ViewModel() { + + override val container = container(initialState = SummaryState.INIT) - override val container = container(SummaryUiState()) + // 이미 요청한 달은 다시 부르지 않는다. 실패한 달은 제거해 다시 진입할 때 재시도되도록 한다. + private val requestedBadgeMonths = mutableSetOf() + private val requestedEmotionMarbleMonths = mutableSetOf() init { onMonthChanged(YearMonth.now()) @@ -32,26 +37,66 @@ class SummaryViewModel @Inject constructor( val monthsToLoad = listOf( newMonth.minusMonths(1), newMonth, - newMonth.plusMonths(1) + newMonth.plusMonths(1), ) - monthsToLoad.forEach { targetMonth -> - fetchHighlightedDays(targetMonth) + coroutineScope { + monthsToLoad.forEach { targetMonth -> + if (requestedBadgeMonths.add(targetMonth)) { + launch { fetchBadges(targetMonth) } + } + if (requestedEmotionMarbleMonths.add(targetMonth)) { + launch { fetchEmotionMarbles(targetMonth) } + } + } } } - private fun fetchHighlightedDays(yearMonth: YearMonth) = intent { - // 이미 데이터가 있는 경우 중복 호출 방지 - if (state.summaryEmotionDaysMap.containsKey(yearMonth)) return@intent - - runCatching { - getHighlightedDaysUseCase(yearMonth.year, yearMonth.monthValue) - }.onSuccess { highlightedDays -> - reduce { - state.copy( - summaryEmotionDaysMap = state.summaryEmotionDaysMap + (yearMonth to highlightedDays) - ) - } + private suspend fun fetchBadges(yearMonth: YearMonth) { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + + getBadgesUseCase(yearMonth).fold( + onSuccess = { badges -> + reduce { + state.copy( + badgesByMonth = state.badgesByMonth + (yearMonth to badges.map { it.toUiModel() }), + loadingCount = state.loadingCount - 1, + ) + } + }, + onFailure = { + Log.e("SummaryViewModel", "뱃지 가져오기 실패: ${it.message}") + requestedBadgeMonths.remove(yearMonth) + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) + } + } + + private suspend fun fetchEmotionMarbles(yearMonth: YearMonth) { + subIntent { + reduce { state.copy(loadingCount = state.loadingCount + 1) } + + getEmotionMarblesUseCase( + startDate = yearMonth.atDay(1), + endDate = yearMonth.atEndOfMonth(), + ).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}") + requestedEmotionMarbleMonths.remove(yearMonth) + reduce { state.copy(loadingCount = state.loadingCount - 1) } + }, + ) } } } 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..f5cf7412 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/contract/SummaryState.kt @@ -0,0 +1,30 @@ +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 java.time.YearMonth + +data class SummaryState( + val loadingCount: Int, + val currentMonth: YearMonth, + val emotionCellsByMonth: Map>, + val badgesByMonth: Map>, +) { + val isLoading: Boolean + get() = loadingCount > 0 + + val currentMonthBadges: List + get() = badgesByMonth[currentMonth].orEmpty() + + fun emotionCellsOf(yearMonth: YearMonth): List = + emotionCellsByMonth[yearMonth].orEmpty() + + companion object { + val INIT = SummaryState( + loadingCount = 0, + currentMonth = YearMonth.now(), + emotionCellsByMonth = emptyMap(), + badgesByMonth = emptyMap(), + ) + } +} 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..def51e2a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeUiModel.kt @@ -0,0 +1,18 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import com.threegap.bitnagil.domain.activitylog.model.Badge + +data class SummaryBadgeUiModel( + val title: String, + val description: String, + val imageUrl: String, + val acquired: Boolean, +) + +fun Badge.toUiModel(): SummaryBadgeUiModel = + SummaryBadgeUiModel( + title = title, + description = description, + imageUrl = 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 index f2487add..8800d01e 100644 --- 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 @@ -1,6 +1,25 @@ package com.threegap.bitnagil.presentation.screen.summary.model +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType + data class SummaryEmotionCellUiModel( val day: Int, val emotionType: SummaryEmotionType, ) + +fun EmotionMarble.toUiModel(): SummaryEmotionCellUiModel = + SummaryEmotionCellUiModel( + day = date.dayOfMonth, + emotionType = type.toUiModel(), + ) + +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/SummaryUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt deleted file mode 100644 index 6aa7a002..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryUiModel.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.threegap.bitnagil.presentation.screen.summary.model - -import java.time.YearMonth - -data class SummaryUiState( - val currentMonth: YearMonth = YearMonth.now(), - val summaryEmotionDaysMap: Map> = emptyMap(), - val isLoading: Boolean = false -) From 33f7d50157cfed2b3fe2b27efaf29e5542c4c901 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 15 Jul 2026 19:23:51 +0900 Subject: [PATCH 04/20] =?UTF-8?q?FIX:=20=EC=9B=94=EB=B3=84=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=EA=B5=AC=EC=8A=AC=20=EC=A1=B0=ED=9A=8C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=BA=90=EC=8B=B1=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=84=20SummaryViewModel=20=EB=82=B4=EB=B6=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20ActivityLogLocalDataSource=EB=A1=9C=20=EC=9C=84?= =?UTF-8?q?=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/di/data/DataSourceModule.kt | 12 +++-- .../datasource/ActivityLogLocalDataSource.kt | 14 +++++ ...urce.kt => ActivityLogRemoteDataSource.kt} | 2 +- .../ActivityLogLocalDataSourceImpl.kt | 32 +++++++++++ ....kt => ActivityLogRemoteDataSourceImpl.kt} | 6 +-- .../ActivityLogRepositoryImpl.kt | 54 +++++++++++++++---- .../repositoryImpl/EmotionRepositoryImpl.kt | 14 ++++- .../repository/ActivityLogRepository.kt | 2 +- .../usecase/GetEmotionMarblesUseCase.kt | 8 ++- .../emotion/repository/EmotionRepository.kt | 2 + .../GetEmotionRegisteredEventFlowUseCase.kt | 12 +++++ .../screen/summary/SummaryViewModel.kt | 28 +++++----- 12 files changed, 150 insertions(+), 36 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogLocalDataSource.kt rename data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/{ActivityLogDataSource.kt => ActivityLogRemoteDataSource.kt} (91%) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt rename data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/{ActivityLogDataSourceImpl.kt => ActivityLogRemoteDataSourceImpl.kt} (87%) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt 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 1c07ac3f..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,7 +1,9 @@ package com.threegap.bitnagil.di.data -import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource -import com.threegap.bitnagil.data.activitylog.datasourceImpl.ActivityLogDataSourceImpl +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 @@ -98,5 +100,9 @@ abstract class DataSourceModule { @Binds @Singleton - abstract fun bindActivityLogDataSource(impl: ActivityLogDataSourceImpl): ActivityLogDataSource + abstract fun bindActivityLogRemoteDataSource(impl: ActivityLogRemoteDataSourceImpl): ActivityLogRemoteDataSource + + @Binds + @Singleton + abstract fun bindActivityLogLocalDataSource(impl: ActivityLogLocalDataSourceImpl): ActivityLogLocalDataSource } 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..8f94e890 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogLocalDataSource.kt @@ -0,0 +1,14 @@ +package com.threegap.bitnagil.data.activitylog.datasource + +import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import kotlinx.coroutines.flow.StateFlow +import java.time.YearMonth + +interface ActivityLogLocalDataSource { + val badgesByMonth: StateFlow>> + val emotionMarblesByMonth: StateFlow>> + fun saveBadges(yearMonth: YearMonth, badges: List) + fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) + fun clearCache() +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt similarity index 91% rename from data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt rename to data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt index 013b4e7f..1aaa53f8 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasource/ActivityLogRemoteDataSource.kt @@ -3,7 +3,7 @@ package com.threegap.bitnagil.data.activitylog.datasource import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse -interface ActivityLogDataSource { +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..2f252d7e --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.data.activitylog.datasourceImpl + +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogLocalDataSource +import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +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, badges: List) { + _badgesByMonth.update { it + (yearMonth to badges) } + } + + override fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) { + _emotionMarblesByMonth.update { it + (yearMonth to emotionMarbles) } + } + + override fun clearCache() { + _badgesByMonth.update { emptyMap() } + _emotionMarblesByMonth.update { emptyMap() } + } +} diff --git a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt similarity index 87% rename from data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt rename to data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt index 79539441..ce25a153 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogRemoteDataSourceImpl.kt @@ -1,14 +1,14 @@ package com.threegap.bitnagil.data.activitylog.datasourceImpl -import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource +import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogRemoteDataSource import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse import com.threegap.bitnagil.data.activitylog.model.response.EmotionMarbleResponse import com.threegap.bitnagil.data.activitylog.service.ActivityLogService import javax.inject.Inject -class ActivityLogDataSourceImpl @Inject constructor( +class ActivityLogRemoteDataSourceImpl @Inject constructor( private val activityLogService: ActivityLogService, -) : ActivityLogDataSource { +) : ActivityLogRemoteDataSource { override suspend fun getBadges(year: Int, month: Int): Result> = activityLogService.getBadges(year = year, month = month) 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 index 8075ee69..5dd8f0a0 100644 --- 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 @@ -1,28 +1,60 @@ package com.threegap.bitnagil.data.activitylog.repositoryImpl -import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogDataSource +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.Badge import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.time.LocalDate import java.time.YearMonth import javax.inject.Inject +import javax.inject.Singleton +@Singleton class ActivityLogRepositoryImpl @Inject constructor( - private val activityLogDataSource: ActivityLogDataSource, + private val activityLogRemoteDataSource: ActivityLogRemoteDataSource, + private val activityLogLocalDataSource: ActivityLogLocalDataSource, ) : ActivityLogRepository { + + private val badgeFetchMutex = Mutex() + private val emotionMarbleFetchMutex = Mutex() + override suspend fun getBadges(yearMonth: YearMonth): Result> { - return activityLogDataSource.getBadges( - year = yearMonth.year, - month = yearMonth.monthValue, - ).map { badges -> badges.map { it.toDomain() } } + 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 { badges -> badges.map { it.toDomain() } } + .onSuccess { activityLogLocalDataSource.saveBadges(yearMonth, it) } + } } - override suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate): Result> { - return activityLogDataSource.getEmotionMarbles( - startDate = startDate.toString(), - endDate = endDate.toString(), - ).map { marbles -> marbles.map { it.toDomain() } } + override suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean): Result> { + val yearMonth = YearMonth.from(startDate) + + 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 = startDate.toString(), + endDate = endDate.toString(), + ) + .map { marbles -> marbles.map { it.toDomain() } } + .onSuccess { activityLogLocalDataSource.saveEmotionMarbles(yearMonth, it) } + } } } 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..979b0eb2 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 @@ -8,6 +8,8 @@ import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow @@ -15,14 +17,18 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.time.LocalDate +import java.time.YearMonth import javax.inject.Inject +import javax.inject.Singleton +@Singleton class EmotionRepositoryImpl @Inject constructor( private val emotionRemoteDataSource: EmotionRemoteDataSource, private val emotionLocalDataSource: EmotionLocalDataSource, ) : EmotionRepository { private val fetchMutex = Mutex() + private val _emotionRegisteredEventFlow = MutableSharedFlow() override suspend fun getEmotions(): Result> { return emotionRemoteDataSource.getEmotions().map { response -> @@ -36,10 +42,16 @@ class EmotionRepositoryImpl @Inject constructor( emotionRecommendedRoutineDto.toEmotionRecommendRoutine() } }.also { - if (it.isSuccess) fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) + if (it.isSuccess) { + val today = LocalDate.now() + fetchAndSaveDailyEmotion(today = today, forceRefresh = true) + _emotionRegisteredEventFlow.emit(YearMonth.from(today)) + } } } + override suspend fun getEmotionRegisteredEventFlow(): Flow = _emotionRegisteredEventFlow.asSharedFlow() + override fun observeDailyEmotion(): Flow> = flow { fetchAndSaveDailyEmotion(LocalDate.now()) .onFailure { 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 index e294ca73..4a18b5d5 100644 --- 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 @@ -7,5 +7,5 @@ import java.time.YearMonth interface ActivityLogRepository { suspend fun getBadges(yearMonth: YearMonth): Result> - suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate): Result> + suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Result> } 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 index 12eec897..2cb04147 100644 --- 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 @@ -8,8 +8,12 @@ import javax.inject.Inject class GetEmotionMarblesUseCase @Inject constructor( private val activityLogRepository: ActivityLogRepository, ) { - suspend operator fun invoke(startDate: LocalDate, endDate: LocalDate): Result> { - return activityLogRepository.getEmotionMarbles(startDate = startDate, endDate = endDate) + suspend operator fun invoke( + startDate: LocalDate, + endDate: LocalDate, + forceRefresh: Boolean = false, + ): Result> { + return activityLogRepository.getEmotionMarbles(startDate = startDate, endDate = endDate, forceRefresh = forceRefresh) .map { marbles -> marbles.associateBy { it.date } } } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt index a95af407..569061d7 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt @@ -4,10 +4,12 @@ import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import kotlinx.coroutines.flow.Flow +import java.time.YearMonth interface EmotionRepository { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotionMarbleType: String): Result> fun observeDailyEmotion(): Flow> + suspend fun getEmotionRegisteredEventFlow(): Flow fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt new file mode 100644 index 00000000..1499eed1 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.domain.emotion.usecase + +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import kotlinx.coroutines.flow.Flow +import java.time.YearMonth +import javax.inject.Inject + +class GetEmotionRegisteredEventFlowUseCase @Inject constructor( + private val emotionRepository: EmotionRepository, +) { + suspend operator fun invoke(): Flow = emotionRepository.getEmotionRegisteredEventFlow() +} 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 index d9a8bdc0..6e599a68 100644 --- 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 @@ -4,6 +4,7 @@ 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.domain.emotion.usecase.GetEmotionRegisteredEventFlowUseCase import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState import com.threegap.bitnagil.presentation.screen.summary.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel @@ -18,22 +19,20 @@ import javax.inject.Inject class SummaryViewModel @Inject constructor( private val getBadgesUseCase: GetBadgesUseCase, private val getEmotionMarblesUseCase: GetEmotionMarblesUseCase, + private val getEmotionRegisteredEventFlowUseCase: GetEmotionRegisteredEventFlowUseCase, ) : ContainerHost, ViewModel() { override val container = container(initialState = SummaryState.INIT) - // 이미 요청한 달은 다시 부르지 않는다. 실패한 달은 제거해 다시 진입할 때 재시도되도록 한다. - private val requestedBadgeMonths = mutableSetOf() - private val requestedEmotionMarbleMonths = mutableSetOf() - init { onMonthChanged(YearMonth.now()) + observeEmotionRegisteredEvent() } fun onMonthChanged(newMonth: YearMonth) = intent { reduce { state.copy(currentMonth = newMonth) } - // 현재, 이전, 다음 달 데이터 프리페칭 + // 현재, 이전, 다음 달 데이터 프리페칭. 이미 캐시된 달은 Repository가 즉시 반환한다. val monthsToLoad = listOf( newMonth.minusMonths(1), newMonth, @@ -42,16 +41,18 @@ class SummaryViewModel @Inject constructor( coroutineScope { monthsToLoad.forEach { targetMonth -> - if (requestedBadgeMonths.add(targetMonth)) { - launch { fetchBadges(targetMonth) } - } - if (requestedEmotionMarbleMonths.add(targetMonth)) { - launch { fetchEmotionMarbles(targetMonth) } - } + launch { fetchBadges(targetMonth) } + launch { fetchEmotionMarbles(targetMonth) } } } } + private fun observeEmotionRegisteredEvent() = intent { + getEmotionRegisteredEventFlowUseCase().collect { registeredMonth -> + fetchEmotionMarbles(registeredMonth, forceRefresh = true) + } + } + private suspend fun fetchBadges(yearMonth: YearMonth) { subIntent { reduce { state.copy(loadingCount = state.loadingCount + 1) } @@ -67,20 +68,20 @@ class SummaryViewModel @Inject constructor( }, onFailure = { Log.e("SummaryViewModel", "뱃지 가져오기 실패: ${it.message}") - requestedBadgeMonths.remove(yearMonth) reduce { state.copy(loadingCount = state.loadingCount - 1) } }, ) } } - private suspend fun fetchEmotionMarbles(yearMonth: YearMonth) { + private suspend fun fetchEmotionMarbles(yearMonth: YearMonth, forceRefresh: Boolean = false) { subIntent { reduce { state.copy(loadingCount = state.loadingCount + 1) } getEmotionMarblesUseCase( startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth(), + forceRefresh = forceRefresh, ).fold( onSuccess = { marblesByDate -> val emotionCells = marblesByDate.values.map { it.toUiModel() } @@ -93,7 +94,6 @@ class SummaryViewModel @Inject constructor( }, onFailure = { Log.e("SummaryViewModel", "감정 구슬 가져오기 실패: ${it.message}") - requestedEmotionMarbleMonths.remove(yearMonth) reduce { state.copy(loadingCount = state.loadingCount - 1) } }, ) From 5648999d002e85a341fb8e861394ee06464a5b3f Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 19 Jul 2026 20:04:19 +0900 Subject: [PATCH 05/20] =?UTF-8?q?FIX:=20=EC=9B=94=EB=B3=84=20=EB=B1=83?= =?UTF-8?q?=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20API=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EA=B0=92=20=EC=88=98=EC=A0=95=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datasource/ActivityLogLocalDataSource.kt | 6 ++--- .../datasource/ActivityLogRemoteDataSource.kt | 4 ++-- .../ActivityLogLocalDataSourceImpl.kt | 10 ++++---- .../ActivityLogRemoteDataSourceImpl.kt | 4 ++-- .../model/response/BadgeResponse.kt | 24 ++++++++++++++----- .../ActivityLogRepositoryImpl.kt | 6 ++--- .../activitylog/service/ActivityLogService.kt | 4 ++-- .../domain/activitylog/model/Badge.kt | 2 -- .../domain/activitylog/model/MonthlyBadge.kt | 7 ++++++ .../repository/ActivityLogRepository.kt | 4 ++-- .../activitylog/usecase/GetBadgesUseCase.kt | 4 ++-- .../screen/summary/SummaryViewModel.kt | 4 ++-- .../screen/summary/contract/SummaryState.kt | 6 ++--- .../summary/model/SummaryBadgeUiModel.kt | 23 ++++++++++++++---- 14 files changed, 69 insertions(+), 39 deletions(-) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/activitylog/model/MonthlyBadge.kt 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 index 8f94e890..55076583 100644 --- 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 @@ -1,14 +1,14 @@ package com.threegap.bitnagil.data.activitylog.datasource -import com.threegap.bitnagil.domain.activitylog.model.Badge 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 badgesByMonth: StateFlow> val emotionMarblesByMonth: StateFlow>> - fun saveBadges(yearMonth: YearMonth, badges: List) + fun saveBadges(yearMonth: YearMonth, monthlyBadge: MonthlyBadge) fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) 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 index 1aaa53f8..4a57b02b 100644 --- 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 @@ -1,9 +1,9 @@ package com.threegap.bitnagil.data.activitylog.datasource -import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse 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 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 index 2f252d7e..1a64e3c2 100644 --- 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 @@ -1,8 +1,8 @@ package com.threegap.bitnagil.data.activitylog.datasourceImpl import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogLocalDataSource -import com.threegap.bitnagil.domain.activitylog.model.Badge 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 @@ -11,14 +11,14 @@ 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 _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, badges: List) { - _badgesByMonth.update { it + (yearMonth to badges) } + override fun saveBadges(yearMonth: YearMonth, monthlyBadge: MonthlyBadge) { + _badgesByMonth.update { it + (yearMonth to monthlyBadge) } } override fun saveEmotionMarbles(yearMonth: YearMonth, emotionMarbles: List) { 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 index ce25a153..73b6eeeb 100644 --- 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 @@ -1,15 +1,15 @@ package com.threegap.bitnagil.data.activitylog.datasourceImpl import com.threegap.bitnagil.data.activitylog.datasource.ActivityLogRemoteDataSource -import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse 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> = + override suspend fun getBadges(year: Int, month: Int): Result = activityLogService.getBadges(year = year, month = month) override suspend fun getEmotionMarbles(startDate: String, endDate: String): Result> = 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 index 94800c5a..564f4e1d 100644 --- 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 @@ -2,29 +2,41 @@ 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: BadgeType, - @SerialName("title") - val title: String, - @SerialName("description") - val description: 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, - title = title, - description = description, imageUrl = imageUrl, acquiredAt = acquiredAt?.let { LocalDateTime.parse(it) }, ) 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 index 5dd8f0a0..34aaa894 100644 --- 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 @@ -3,8 +3,8 @@ 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.Badge 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 @@ -22,7 +22,7 @@ class ActivityLogRepositoryImpl @Inject constructor( private val badgeFetchMutex = Mutex() private val emotionMarbleFetchMutex = Mutex() - override suspend fun getBadges(yearMonth: YearMonth): Result> { + override suspend fun getBadges(yearMonth: YearMonth): Result { activityLogLocalDataSource.badgesByMonth.value[yearMonth]?.let { return Result.success(it) } return badgeFetchMutex.withLock { @@ -32,7 +32,7 @@ class ActivityLogRepositoryImpl @Inject constructor( year = yearMonth.year, month = yearMonth.monthValue, ) - .map { badges -> badges.map { it.toDomain() } } + .map { it.toDomain() } .onSuccess { activityLogLocalDataSource.saveBadges(yearMonth, it) } } } 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 index c039e3c1..24aaa1e2 100644 --- 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 @@ -1,7 +1,7 @@ package com.threegap.bitnagil.data.activitylog.service -import com.threegap.bitnagil.data.activitylog.model.response.BadgeResponse 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 @@ -10,7 +10,7 @@ interface ActivityLogService { suspend fun getBadges( @Query("year") year: Int, @Query("month") month: Int, - ): Result> + ): Result @GET("/api/v1/activity-logs/emotion-marbles") suspend fun getEmotionMarbles( 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 index 6c206947..a59758c6 100644 --- 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 @@ -4,8 +4,6 @@ import java.time.LocalDateTime data class Badge( val type: BadgeType, - val title: String, - val description: String, val imageUrl: String, val acquiredAt: LocalDateTime?, ) { 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 index 4a18b5d5..bd420b1d 100644 --- 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 @@ -1,11 +1,11 @@ package com.threegap.bitnagil.domain.activitylog.repository -import com.threegap.bitnagil.domain.activitylog.model.Badge import com.threegap.bitnagil.domain.activitylog.model.EmotionMarble +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge import java.time.LocalDate import java.time.YearMonth interface ActivityLogRepository { - suspend fun getBadges(yearMonth: YearMonth): Result> + suspend fun getBadges(yearMonth: YearMonth): Result suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Result> } 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 index 4e9d7682..f7c66330 100644 --- 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 @@ -1,6 +1,6 @@ package com.threegap.bitnagil.domain.activitylog.usecase -import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge import com.threegap.bitnagil.domain.activitylog.repository.ActivityLogRepository import java.time.YearMonth import javax.inject.Inject @@ -8,7 +8,7 @@ import javax.inject.Inject class GetBadgesUseCase @Inject constructor( private val activityLogRepository: ActivityLogRepository, ) { - suspend operator fun invoke(yearMonth: YearMonth): Result> { + suspend operator fun invoke(yearMonth: YearMonth): Result { return activityLogRepository.getBadges(yearMonth = yearMonth) } } 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 index 6e599a68..5140243e 100644 --- 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 @@ -58,10 +58,10 @@ class SummaryViewModel @Inject constructor( reduce { state.copy(loadingCount = state.loadingCount + 1) } getBadgesUseCase(yearMonth).fold( - onSuccess = { badges -> + onSuccess = { monthlyBadge -> reduce { state.copy( - badgesByMonth = state.badgesByMonth + (yearMonth to badges.map { it.toUiModel() }), + badgesByMonth = state.badgesByMonth + (yearMonth to monthlyBadge.toUiModel()), loadingCount = state.loadingCount - 1, ) } 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 index f5cf7412..769a7074 100644 --- 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 @@ -8,13 +8,13 @@ data class SummaryState( val loadingCount: Int, val currentMonth: YearMonth, val emotionCellsByMonth: Map>, - val badgesByMonth: Map>, + val badgesByMonth: Map, ) { val isLoading: Boolean get() = loadingCount > 0 - val currentMonthBadges: List - get() = badgesByMonth[currentMonth].orEmpty() + val currentMonthBadges: SummaryBadgeUiModel? + get() = badgesByMonth[currentMonth] fun emotionCellsOf(yearMonth: YearMonth): List = emotionCellsByMonth[yearMonth].orEmpty() 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 index def51e2a..54545041 100644 --- 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 @@ -1,18 +1,31 @@ package com.threegap.bitnagil.presentation.screen.summary.model import com.threegap.bitnagil.domain.activitylog.model.Badge +import com.threegap.bitnagil.domain.activitylog.model.BadgeType +import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge data class SummaryBadgeUiModel( - val title: String, - val description: String, + val badgeTitle: String, + val badgeDescription: String, + val badges: List, +) + +data class SummaryBadgeItemUiModel( + val type: BadgeType, val imageUrl: String, val acquired: Boolean, ) -fun Badge.toUiModel(): SummaryBadgeUiModel = +fun MonthlyBadge.toUiModel(): SummaryBadgeUiModel = SummaryBadgeUiModel( - title = title, - description = description, + badgeTitle = badgeTitle, + badgeDescription = badgeDescription, + badges = badges.map { it.toUiModel() }, + ) + +fun Badge.toUiModel(): SummaryBadgeItemUiModel = + SummaryBadgeItemUiModel( + type = type, imageUrl = imageUrl, acquired = acquired, ) From 841ef7687220d37778c3cb17298c8c0ae74b1bb8 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 19 Jul 2026 20:24:28 +0900 Subject: [PATCH 06/20] =?UTF-8?q?FIX:=20=EC=B6=94=ED=9B=84=20BadgeType=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=20=EC=9D=91=EB=8B=B5=20=ED=8C=8C=EC=8B=B1=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EC=8B=9C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20BadgeType=20?= =?UTF-8?q?Unknown=20=ED=83=80=EC=9E=85=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/activitylog/model/response/BadgeResponse.kt | 7 +++++-- .../bitnagil/domain/activitylog/model/BadgeType.kt | 8 +++----- 2 files changed, 8 insertions(+), 7 deletions(-) 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 index 564f4e1d..2714a2cb 100644 --- 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 @@ -20,7 +20,7 @@ data class MonthlyBadgeResponse( @Serializable data class BadgeResponse( @SerialName("badgeType") - val badgeType: BadgeType, + val badgeType: String, @SerialName("imageUrl") val imageUrl: String, @SerialName("acquiredAt") @@ -36,7 +36,10 @@ fun MonthlyBadgeResponse.toDomain(): MonthlyBadge = fun BadgeResponse.toDomain(): Badge = Badge( - type = badgeType, + 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/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 index b13b23ea..6c38509c 100644 --- 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 @@ -1,14 +1,12 @@ package com.threegap.bitnagil.domain.activitylog.model -import kotlinx.serialization.Serializable - /** * 활동 뱃지 타입 * * @property MOTIVATION_EXPERT 의욕 전문가 - 그 달 감정 구슬 선택 3회 * @property CHECK_EXPERT 체크 전문가 - 그 달 루틴(메인) 완료 1회 * @property OUTING_EXPERT 외출 전문가 - 그 달 제보 등록 1회 - * @property RESERVE_EXPERT 예비 전문가 - 그 달 획득 뱃지가 0개일 때 기본 표시 + * @property RESERVE_EXPERT 예비 전문가 - 그 달 획득 뱃지가 0개일 때 서버가 내려주는 기본 표시 + * @property UNKNOWN 서버가 내려준 값이 위 항목 중 어디에도 매칭되지 않을 때의 fallback */ -@Serializable -enum class BadgeType { MOTIVATION_EXPERT, CHECK_EXPERT, OUTING_EXPERT, RESERVE_EXPERT } +enum class BadgeType { MOTIVATION_EXPERT, CHECK_EXPERT, OUTING_EXPERT, RESERVE_EXPERT, UNKNOWN } From b192ee484ee25ae7e98fcd51eaf88fe04f3e86c7 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Tue, 21 Jul 2026 22:32:34 +0900 Subject: [PATCH 07/20] =?UTF-8?q?FEATURE:=20Badge=20=ED=91=9C=EC=8B=9C=20U?= =?UTF-8?q?I=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/res/drawable/ic_badge_question.xml | 24 ++ .../screen/summary/SummaryScreen.kt | 29 +- .../template/summarybadge/SummaryBadgeView.kt | 289 ++++++++++++++++++ .../screen/summary/model/BadgeImage.kt | 6 + .../summary/model/SummaryBadgeTypeUiModel.kt | 22 ++ .../summary/model/SummaryBadgeUiModel.kt | 15 +- 6 files changed, 354 insertions(+), 31 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_badge_question.xml create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarybadge/SummaryBadgeView.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/BadgeImage.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryBadgeTypeUiModel.kt 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/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 index 96cc8060..8f289f79 100644 --- 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 @@ -2,7 +2,6 @@ package com.threegap.bitnagil.presentation.screen.summary import androidx.compose.foundation.Image 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 @@ -10,7 +9,6 @@ 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.layout.statusBarsPadding import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState @@ -22,9 +20,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -32,6 +27,7 @@ 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.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 kotlinx.coroutines.launch @@ -76,27 +72,12 @@ fun SummaryScreen( .background(BitnagilTheme.colors.white) .verticalScroll(verticalScrollState), ) { - Box( + SummaryBadgeView( + summaryBadge = state.currentMonthBadges, modifier = Modifier .fillMaxWidth() - .background( - brush = Brush.linearGradient( - 0.6996f to Color(0xFFFE7120), - 0.9939f to Color(0xFFFF964B), - start = Offset(0f, Float.POSITIVE_INFINITY), - end = Offset(Float.POSITIVE_INFINITY, 0f), - ), - ) - .statusBarsPadding() - .height(260.dp), - ) { - Text( - "덕분에 도시가\n개선되고 있어요!", - style = BitnagilTheme.typography.cafe24SsurroundAir, - color = Color.White, - modifier = Modifier.padding(top = 40.dp, start = 20.dp), - ) - } + .height(360.dp), + ) Row( modifier = Modifier 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..b4376385 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/summarybadge/SummaryBadgeView.kt @@ -0,0 +1,289 @@ +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.Box +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.height +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.badgeTitle, + style = BitnagilTheme.typography.cafe24SsurroundAir, + color = BitnagilTheme.colors.white, + textAlign = TextAlign.Center + ) + + SummaryBadgeListView( + badges = summaryBadge.badges + ) + + BadgeDescriptionView( + badgeDescription = summaryBadge.badgeDescription, + 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() + .height(260.dp), + ) { + 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) + Box( + modifier = Modifier.height(125.dp).aspectRatio(1f) + ) { + SummaryBadgeItemView( + badge = badge, + modifier = Modifier.align(alignment = Alignment.Center) + .size(125.dp) + .aspectRatio(1f) + .rotate(14f) + ) + + StarImage( + modifier = Modifier.width(6.dp).height(5.dp) + .align(Alignment.TopStart) + .offset(x = 12.dp, y = 8.dp).rotate(-34f), + color = BitnagilTheme.colors.white, + contentScale = ContentScale.FillBounds + ) + + StarImage( + modifier = Modifier.width(13.dp).height(11.dp) + .align(Alignment.TopStart) + .offset(x = 1.dp, y = 12.dp).rotate(-13f), + color = BitnagilTheme.colors.white, + contentScale = ContentScale.FillBounds + ) + + StarImage( + modifier = Modifier.size(18.dp) + .align(Alignment.BottomEnd) + .offset(x = -(5).dp, y = -(5).dp), + color = BitnagilTheme.colors.white + ) + } + 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 BadgeDescriptionView( + badgeDescription: 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.kakao else BitnagilTheme.colors.coolGray95) + ) + + Text( + text = badgeDescription, + style = BitnagilTheme.typography.caption1Medium, + color = BitnagilTheme.colors.white + ) + } +} + +@Preview(showBackground = true, heightDp = 360) +@Composable +private fun SummaryBadgePreview() { + BitnagilTheme { + SummaryBadgeView( + summaryBadge = SummaryBadgeUiModel( + badgeTitle = "덕분에 도시가\n개선되고 있어요!", + badges = listOf( + SummaryBadgeItemUiModel( + type = SummaryBadgeTypeUiModel.Unknown, + image = BadgeImage.Default, + acquired = false, + ), + ), + badgeDescription = "외출 전문가" + ), + modifier = Modifier + ) + } +} 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 index 54545041..f15bbfd4 100644 --- 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 @@ -1,7 +1,6 @@ package com.threegap.bitnagil.presentation.screen.summary.model import com.threegap.bitnagil.domain.activitylog.model.Badge -import com.threegap.bitnagil.domain.activitylog.model.BadgeType import com.threegap.bitnagil.domain.activitylog.model.MonthlyBadge data class SummaryBadgeUiModel( @@ -11,8 +10,8 @@ data class SummaryBadgeUiModel( ) data class SummaryBadgeItemUiModel( - val type: BadgeType, - val imageUrl: String, + val type: SummaryBadgeTypeUiModel, + val image: BadgeImage, val acquired: Boolean, ) @@ -23,9 +22,11 @@ fun MonthlyBadge.toUiModel(): SummaryBadgeUiModel = badges = badges.map { it.toUiModel() }, ) -fun Badge.toUiModel(): SummaryBadgeItemUiModel = - SummaryBadgeItemUiModel( - type = type, - imageUrl = imageUrl, +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, ) +} From a5f94b00c9ac8af8df501191be551e1c77eac639 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 22 Jul 2026 21:14:30 +0900 Subject: [PATCH 08/20] =?UTF-8?q?FIX:=20Badge=20=ED=91=9C=EC=8B=9C=20UI=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/summarybadge/SummaryBadgeView.kt | 57 +++++-------------- 1 file changed, 13 insertions(+), 44 deletions(-) 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 index b4376385..85345052 100644 --- 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 @@ -3,7 +3,6 @@ package com.threegap.bitnagil.presentation.screen.summary.component.template.sum import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column @@ -59,7 +58,7 @@ fun SummaryBadgeView( ) { summaryBadge?.let { Text( - summaryBadge.badgeTitle, + summaryBadge.badgeDescription, style = BitnagilTheme.typography.cafe24SsurroundAir, color = BitnagilTheme.colors.white, textAlign = TextAlign.Center @@ -69,8 +68,8 @@ fun SummaryBadgeView( badges = summaryBadge.badges ) - BadgeDescriptionView( - badgeDescription = summaryBadge.badgeDescription, + BadgeTitleView( + badgeTitle = summaryBadge.badgeTitle, isBadgeReserved = summaryBadge.badges.firstOrNull()?.type?.isReserved ?: false, modifier = Modifier ) @@ -174,40 +173,10 @@ private fun SummaryBadgeListView( ) { badges.forEach { badge -> if (isSingleItem) - Box( - modifier = Modifier.height(125.dp).aspectRatio(1f) - ) { - SummaryBadgeItemView( - badge = badge, - modifier = Modifier.align(alignment = Alignment.Center) - .size(125.dp) - .aspectRatio(1f) - .rotate(14f) - ) - - StarImage( - modifier = Modifier.width(6.dp).height(5.dp) - .align(Alignment.TopStart) - .offset(x = 12.dp, y = 8.dp).rotate(-34f), - color = BitnagilTheme.colors.white, - contentScale = ContentScale.FillBounds - ) - - StarImage( - modifier = Modifier.width(13.dp).height(11.dp) - .align(Alignment.TopStart) - .offset(x = 1.dp, y = 12.dp).rotate(-13f), - color = BitnagilTheme.colors.white, - contentScale = ContentScale.FillBounds - ) - - StarImage( - modifier = Modifier.size(18.dp) - .align(Alignment.BottomEnd) - .offset(x = -(5).dp, y = -(5).dp), - color = BitnagilTheme.colors.white - ) - } + SummaryBadgeItemView( + badge = badge, + modifier = Modifier.size(125.dp).aspectRatio(1f) + ) else SummaryBadgeItemView( badge = badge, @@ -240,8 +209,8 @@ private fun SummaryBadgeItemView( } @Composable -private fun BadgeDescriptionView( - badgeDescription: String, +private fun BadgeTitleView( + badgeTitle: String, isBadgeReserved: Boolean, modifier: Modifier = Modifier, ) { @@ -256,11 +225,11 @@ private fun BadgeDescriptionView( imageVector = ImageVector.vectorResource(id = R.drawable.ic_shine), contentDescription = null, modifier = Modifier.size(15.dp), - colorFilter = ColorFilter.tint(if (isBadgeReserved) BitnagilTheme.colors.kakao else BitnagilTheme.colors.coolGray95) + colorFilter = ColorFilter.tint(if (isBadgeReserved) BitnagilTheme.colors.coolGray95 else BitnagilTheme.colors.kakao) ) Text( - text = badgeDescription, + text = badgeTitle, style = BitnagilTheme.typography.caption1Medium, color = BitnagilTheme.colors.white ) @@ -273,7 +242,7 @@ private fun SummaryBadgePreview() { BitnagilTheme { SummaryBadgeView( summaryBadge = SummaryBadgeUiModel( - badgeTitle = "덕분에 도시가\n개선되고 있어요!", + badgeTitle = "외출 전문가", badges = listOf( SummaryBadgeItemUiModel( type = SummaryBadgeTypeUiModel.Unknown, @@ -281,7 +250,7 @@ private fun SummaryBadgePreview() { acquired = false, ), ), - badgeDescription = "외출 전문가" + badgeDescription = "덕분에 도시가\n개선되고 있어요!" ), modifier = Modifier ) From 0104ed3a4257c2e1235da4d72d9a7959e75e7657 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 22 Jul 2026 21:29:38 +0900 Subject: [PATCH 09/20] =?UTF-8?q?Feat:=20HomeNavigator=EC=97=90=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=ED=99=94=EB=A9=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/navigation/home/HomeNavHost.kt | 7 ++++ .../bitnagil/navigation/home/HomeNavigator.kt | 1 + .../bitnagil/navigation/home/HomeRoute.kt | 6 +++ app/src/main/res/drawable/ic_report.xml | 37 +++++++++++++++++++ .../screen/summary/SummaryScreen.kt | 12 ++++-- 5 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/drawable/ic_report.xml 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/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 index 8f289f79..b920150f 100644 --- 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 @@ -27,6 +27,7 @@ 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.summarybadge.SummaryBadgeView import com.threegap.bitnagil.presentation.screen.summary.component.template.summarycalendar.SummaryCalendarView import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState @@ -37,12 +38,14 @@ import java.time.YearMonth @Composable fun SummaryScreenContainer( viewModel: SummaryViewModel = hiltViewModel(), + navigateToYouthPolicies: () -> Unit ) { val state by viewModel.collectAsState() SummaryScreen( state = state, - onMonthChanged = viewModel::onMonthChanged + onMonthChanged = viewModel::onMonthChanged, + onClickYouthPolicies = navigateToYouthPolicies ) } @@ -51,7 +54,8 @@ private const val INITIAL_PAGE = Int.MAX_VALUE / 2 @Composable fun SummaryScreen( state: SummaryState, - onMonthChanged: (YearMonth) -> Unit = {} + onMonthChanged: (YearMonth) -> Unit = {}, + onClickYouthPolicies: () -> Unit, ) { val verticalScrollState = rememberScrollState() val pagerState = rememberPagerState(initialPage = INITIAL_PAGE) { Int.MAX_VALUE } @@ -95,7 +99,7 @@ fun SummaryScreen( Spacer(modifier = Modifier.weight(1f)) - Text("더보기", color = BitnagilTheme.colors.orange500, modifier = Modifier.padding(10.dp), style = BitnagilTheme.typography.body2SemiBold) + Text("더보기", color = BitnagilTheme.colors.orange500, modifier = Modifier.clickableWithoutRipple(onClick = onClickYouthPolicies).padding(10.dp), style = BitnagilTheme.typography.body2SemiBold) } Spacer(modifier = Modifier.height(20.dp)) @@ -157,6 +161,6 @@ fun SummaryScreen( @Composable private fun SummaryScreenPreview() { BitnagilTheme { - SummaryScreen(state = SummaryState.INIT) + SummaryScreen(state = SummaryState.INIT, onClickYouthPolicies = {}) } } From 4a4454f5183e73dedb1f39dac978c55ebae3a77d Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 26 Jul 2026 20:06:40 +0900 Subject: [PATCH 10/20] =?UTF-8?q?Feat:=20=EB=8B=AC=EB=A0=A5=20=EB=82=B4=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=EC=9D=84=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EC=9D=BC=20=EB=B0=8F=20=EC=9D=B4=EC=A0=84/=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=EC=9D=BC=EC=9E=90=20=ED=81=B4=EB=A6=AD=EC=8B=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84=20:=20=EB=8B=B9?= =?UTF-8?q?=EC=9D=BC=20=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8,=20=EC=9D=B4?= =?UTF-8?q?=EC=A0=84/=EC=9D=B4=ED=9B=84=20=EC=9B=94=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/summary/SummaryScreen.kt | 20 ++ .../screen/summary/SummaryViewModel.kt | 15 ++ .../EmotionDayBottomSheet.kt | 177 ++++++++++++++++++ .../summarycalendar/SummaryCalendarView.kt | 21 ++- .../screen/summary/contract/SummaryState.kt | 3 + .../summary/model/SummaryEmotionDayUiModel.kt | 21 +++ .../summary/model/SummaryEmotionType.kt | 15 +- 7 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/emotiondaybottomsheet/EmotionDayBottomSheet.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionDayUiModel.kt 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 index b920150f..74aa43e1 100644 --- 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 @@ -28,12 +28,16 @@ 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.SummaryEmotionType 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( @@ -42,9 +46,17 @@ fun SummaryScreenContainer( ) { 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 ) } @@ -55,6 +67,7 @@ private const val INITIAL_PAGE = Int.MAX_VALUE / 2 fun SummaryScreen( state: SummaryState, onMonthChanged: (YearMonth) -> Unit = {}, + onClickEmotionDay: (LocalDate, SummaryEmotionType) -> Unit = { _, _ -> }, onClickYouthPolicies: () -> Unit, ) { val verticalScrollState = rememberScrollState() @@ -149,6 +162,13 @@ fun SummaryScreen( SummaryCalendarView( yearMonth = displayMonth, emotionDays = state.emotionCellsOf(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) ) } 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 index 5140243e..919b7a57 100644 --- 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 @@ -6,12 +6,15 @@ import com.threegap.bitnagil.domain.activitylog.usecase.GetBadgesUseCase import com.threegap.bitnagil.domain.activitylog.usecase.GetEmotionMarblesUseCase import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionRegisteredEventFlowUseCase import com.threegap.bitnagil.presentation.screen.summary.contract.SummaryState +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionDayUiModel +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionType 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 @@ -47,6 +50,18 @@ class SummaryViewModel @Inject constructor( } } + fun selectEmotionDay(date: LocalDate, emotionType: SummaryEmotionType) = intent { + reduce { + state.copy( + selectedEmotionDay = SummaryEmotionDayUiModel(date = date, emotionType = emotionType), + ) + } + } + + fun clearSelectedEmotionDay() = intent { + reduce { state.copy(selectedEmotionDay = null) } + } + private fun observeEmotionRegisteredEvent() = intent { getEmotionRegisteredEventFlowUseCase().collect { registeredMonth -> fetchEmotionMarbles(registeredMonth, forceRefresh = true) 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..7185959a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/component/template/emotiondaybottomsheet/EmotionDayBottomSheet.kt @@ -0,0 +1,177 @@ +package com.threegap.bitnagil.presentation.screen.summary.component.template.emotiondaybottomsheet + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +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.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +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)) + + Box( + 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), + ), + ) { + Text( + emotionDay.emotionType.displayName, + style = BitnagilTheme.typography.body2SemiBold, + color = emotionDay.emotionType.textColor, + maxLines = 1, + modifier = Modifier + .offset(x = 12.dp, y = 12.dp) + .background(color = emotionDay.emotionType.backgroundColor, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 18.dp, vertical = 5.dp) + ) + + Box( + modifier = Modifier.align(alignment = Alignment.BottomCenter), + contentAlignment = Alignment.BottomCenter + ) { + Image( + painter = painterResource(R.drawable.img_pomo_hand), + contentScale = ContentScale.FillBounds, + contentDescription = null, + modifier = Modifier + .width(88.dp) + .height(64.dp), + ) + + Box( + modifier = Modifier.size(130.dp) + .offset(y = -(28).dp) + ) { + // todo - 감정 구슬 이미지 + } + + Image( + painter = painterResource(R.drawable.img_pomo_thumb), + modifier = Modifier + .align(Alignment.BottomCenter) + .width(36.dp) + .height(28.dp) + .offset(x = (-6).dp, y = (-24).dp), + contentDescription = null, + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun EmotionDayBottomSheetContentPreview() { + EmotionDayBottomSheetContent( + onDismiss = {}, + emotionDay = SummaryEmotionDayUiModel( + date = LocalDate.now(), + emotionType = SummaryEmotionType.SATISFACTION, + ), + ) +} 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 index 665156ea..c10329ee 100644 --- 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 @@ -19,9 +19,11 @@ 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 @@ -30,6 +32,8 @@ fun SummaryCalendarView( emotionDays: List, modifier: Modifier = Modifier, firstDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, + onClickOtherMonthDay: (YearMonth) -> Unit = {}, + onClickEmotionDay: (LocalDate, SummaryEmotionType) -> Unit = { _, _ -> }, ) { val firstDayOfMonth = yearMonth.atDay(1) @@ -64,10 +68,22 @@ fun SummaryCalendarView( val isCurrentMonth = date.monthValue == yearMonth.monthValue val emotionCellUiModel = if (isCurrentMonth) emotionDays.firstOrNull { it.day == date.dayOfMonth } else null + // 이전/다음 달 날짜는 해당 달로 이동하고, 이번 달 날짜는 감정이 기록된 경우에만 선택할 수 있다. + val onClick: (() -> Unit)? = when { + !isCurrentMonth -> { + { onClickOtherMonthDay(YearMonth.from(date)) } + } + emotionCellUiModel != null -> { + { onClickEmotionDay(date, emotionCellUiModel.emotionType) } + } + else -> null + } + SummaryCalendarCell( isCurrentMonth = isCurrentMonth, emotionType = emotionCellUiModel?.emotionType, day = date.dayOfMonth, + onClick = onClick, modifier = Modifier.weight(1f), ) } @@ -92,9 +108,12 @@ fun SummaryCalendarCell( emotionType: SummaryEmotionType?, day: Int, modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, ) { Box( - modifier = modifier.aspectRatio(1f), + modifier = modifier + .aspectRatio(1f) + .then(if (onClick != null) Modifier.clickableWithoutRipple(onClick = onClick) else Modifier), contentAlignment = Alignment.Center, ) { Box( 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 index 769a7074..a68790f9 100644 --- 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 @@ -2,6 +2,7 @@ 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( @@ -9,6 +10,7 @@ data class SummaryState( val currentMonth: YearMonth, val emotionCellsByMonth: Map>, val badgesByMonth: Map, + val selectedEmotionDay: SummaryEmotionDayUiModel?, ) { val isLoading: Boolean get() = loadingCount > 0 @@ -25,6 +27,7 @@ data class SummaryState( currentMonth = YearMonth.now(), emotionCellsByMonth = emptyMap(), badgesByMonth = emptyMap(), + selectedEmotionDay = null, ) } } 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..232e0178 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/screen/summary/model/SummaryEmotionDayUiModel.kt @@ -0,0 +1,21 @@ +package com.threegap.bitnagil.presentation.screen.summary.model + +import java.time.LocalDate + +data class SummaryEmotionDayUiModel( + val date: LocalDate, + val emotionType: SummaryEmotionType, +) { + 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 index 69a4cc04..17c981ef 100644 --- 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 @@ -4,12 +4,13 @@ import androidx.compose.ui.graphics.Color enum class SummaryEmotionType( val backgroundColor: Color, - val textColor: 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)) + 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), "피곤함") } From c466aca6d0511e4255e4b353f1b5993a1266c4cb Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sun, 26 Jul 2026 20:16:24 +0900 Subject: [PATCH 11/20] =?UTF-8?q?FIX:=20=EB=8B=B9=EC=9D=BC=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=EA=B5=AC=EC=8A=AC=20=EC=A1=B0=ED=9A=8C=20=EB=B0=94?= =?UTF-8?q?=ED=85=80=EC=8B=9C=ED=8A=B8=20=EB=82=B4=20=EA=B0=90=EC=A0=95?= =?UTF-8?q?=EA=B5=AC=EC=8A=AC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EB=B6=80=EB=B6=84=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/summary/SummaryScreen.kt | 4 ++-- .../screen/summary/SummaryViewModel.kt | 10 ++++++--- .../EmotionDayBottomSheet.kt | 22 ++++++++++++++----- .../summarycalendar/SummaryCalendarView.kt | 16 +++++++------- .../model/SummaryEmotionCellUiModel.kt | 2 ++ .../summary/model/SummaryEmotionDayUiModel.kt | 1 + 6 files changed, 36 insertions(+), 19 deletions(-) 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 index 74aa43e1..44b2329a 100644 --- 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 @@ -32,7 +32,7 @@ import com.threegap.bitnagil.presentation.screen.summary.component.template.emot 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.SummaryEmotionType +import com.threegap.bitnagil.presentation.screen.summary.model.SummaryEmotionCellUiModel import kotlinx.coroutines.launch import org.orbitmvi.orbit.compose.collectAsState import java.time.LocalDate @@ -67,7 +67,7 @@ private const val INITIAL_PAGE = Int.MAX_VALUE / 2 fun SummaryScreen( state: SummaryState, onMonthChanged: (YearMonth) -> Unit = {}, - onClickEmotionDay: (LocalDate, SummaryEmotionType) -> Unit = { _, _ -> }, + onClickEmotionDay: (LocalDate, SummaryEmotionCellUiModel) -> Unit = { _, _ -> }, onClickYouthPolicies: () -> Unit, ) { val verticalScrollState = rememberScrollState() 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 index 919b7a57..becfea45 100644 --- 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 @@ -6,8 +6,8 @@ import com.threegap.bitnagil.domain.activitylog.usecase.GetBadgesUseCase import com.threegap.bitnagil.domain.activitylog.usecase.GetEmotionMarblesUseCase import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionRegisteredEventFlowUseCase 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.SummaryEmotionType import com.threegap.bitnagil.presentation.screen.summary.model.toUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.coroutineScope @@ -50,10 +50,14 @@ class SummaryViewModel @Inject constructor( } } - fun selectEmotionDay(date: LocalDate, emotionType: SummaryEmotionType) = intent { + fun selectEmotionDay(date: LocalDate, emotionCell: SummaryEmotionCellUiModel) = intent { reduce { state.copy( - selectedEmotionDay = SummaryEmotionDayUiModel(date = date, emotionType = emotionType), + selectedEmotionDay = SummaryEmotionDayUiModel( + date = date, + emotionType = emotionCell.emotionType, + imageUrl = emotionCell.imageUrl, + ), ) } } 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 index 7185959a..0be6f637 100644 --- 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 @@ -24,9 +24,13 @@ 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.res.painterResource 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 @@ -143,12 +147,17 @@ private fun EmotionDayBottomSheetContent( .height(64.dp), ) - Box( - modifier = Modifier.size(130.dp) - .offset(y = -(28).dp) - ) { - // todo - 감정 구슬 이미지 - } + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(emotionDay.imageUrl) + .crossfade(true) + .build(), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .size(130.dp) + .offset(y = -(28).dp), + ) Image( painter = painterResource(R.drawable.img_pomo_thumb), @@ -172,6 +181,7 @@ private fun EmotionDayBottomSheetContentPreview() { emotionDay = SummaryEmotionDayUiModel( date = LocalDate.now(), emotionType = SummaryEmotionType.SATISFACTION, + imageUrl = "", ), ) } 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 index c10329ee..e00d6853 100644 --- 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 @@ -33,7 +33,7 @@ fun SummaryCalendarView( modifier: Modifier = Modifier, firstDayOfWeek: DayOfWeek = DayOfWeek.SUNDAY, onClickOtherMonthDay: (YearMonth) -> Unit = {}, - onClickEmotionDay: (LocalDate, SummaryEmotionType) -> Unit = { _, _ -> }, + onClickEmotionDay: (LocalDate, SummaryEmotionCellUiModel) -> Unit = { _, _ -> }, ) { val firstDayOfMonth = yearMonth.atDay(1) @@ -74,7 +74,7 @@ fun SummaryCalendarView( { onClickOtherMonthDay(YearMonth.from(date)) } } emotionCellUiModel != null -> { - { onClickEmotionDay(date, emotionCellUiModel.emotionType) } + { onClickEmotionDay(date, emotionCellUiModel) } } else -> null } @@ -137,12 +137,12 @@ private fun SummaryCalendarPreview() { SummaryCalendarView( yearMonth = YearMonth.now(), emotionDays = listOf( - SummaryEmotionCellUiModel(6, SummaryEmotionType.CALM), - SummaryEmotionCellUiModel(8, SummaryEmotionType.ANXIETY), - SummaryEmotionCellUiModel(9, SummaryEmotionType.VITALITY), - SummaryEmotionCellUiModel(11, SummaryEmotionType.LETHARGY), - SummaryEmotionCellUiModel(14, SummaryEmotionType.SATISFACTION), - SummaryEmotionCellUiModel(16, SummaryEmotionType.FATIGUE), + SummaryEmotionCellUiModel(6, SummaryEmotionType.CALM, ""), + SummaryEmotionCellUiModel(8, SummaryEmotionType.ANXIETY, ""), + SummaryEmotionCellUiModel(9, SummaryEmotionType.VITALITY, ""), + SummaryEmotionCellUiModel(11, SummaryEmotionType.LETHARGY, ""), + SummaryEmotionCellUiModel(14, SummaryEmotionType.SATISFACTION, ""), + SummaryEmotionCellUiModel(16, SummaryEmotionType.FATIGUE, ""), ), firstDayOfWeek = DayOfWeek.SUNDAY ) 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 index 8800d01e..94876df9 100644 --- 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 @@ -6,12 +6,14 @@ import com.threegap.bitnagil.domain.emotion.model.EmotionMarbleType data class SummaryEmotionCellUiModel( val day: Int, val emotionType: SummaryEmotionType, + val imageUrl: String, ) fun EmotionMarble.toUiModel(): SummaryEmotionCellUiModel = SummaryEmotionCellUiModel( day = date.dayOfMonth, emotionType = type.toUiModel(), + imageUrl = imageUrl, ) fun EmotionMarbleType.toUiModel(): SummaryEmotionType = 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 index 232e0178..04471fb6 100644 --- 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 @@ -5,6 +5,7 @@ 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}일의 감정" From 513d4bbbf4b45ae2e152bb4c63969af754ea0ea2 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Mon, 27 Jul 2026 22:42:32 +0900 Subject: [PATCH 12/20] =?UTF-8?q?FIX:=20=EA=B0=90=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EB=93=B1=EB=A1=9D=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=EC=9D=B4=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=90=9C=20=EC=9B=94=EC=9D=98=20=EA=B0=90=EC=A0=95?= =?UTF-8?q?=EA=B5=AC=EC=8A=AC=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=ED=95=A8=EC=9C=BC=EB=A1=9C=EC=8D=A8=20?= =?UTF-8?q?=EB=A6=AC=ED=8F=AC=ED=8A=B8=20=ED=99=94=EB=A9=B4=20=EC=A7=84?= =?UTF-8?q?=EC=9E=85=EC=8B=9C=20=EB=8B=A4=EC=8B=9C=20=ED=95=B4=EB=8B=B9=20?= =?UTF-8?q?=EC=9B=94=EC=9D=98=20=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EC=84=9C=EB=B2=84=EB=A1=9C?= =?UTF-8?q?=EB=B6=80=ED=84=B0=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitylog/datasource/ActivityLogLocalDataSource.kt | 1 + .../datasourceImpl/ActivityLogLocalDataSourceImpl.kt | 4 ++++ .../repositoryImpl/ActivityLogRepositoryImpl.kt | 4 ++++ .../domain/activitylog/repository/ActivityLogRepository.kt | 1 + .../domain/emotion/usecase/RegisterEmotionUseCase.kt | 7 ++++++- 5 files changed, 16 insertions(+), 1 deletion(-) 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 index 55076583..8fbd5f23 100644 --- 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 @@ -10,5 +10,6 @@ interface ActivityLogLocalDataSource { 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/datasourceImpl/ActivityLogLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/datasourceImpl/ActivityLogLocalDataSourceImpl.kt index 1a64e3c2..4b61906d 100644 --- 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 @@ -25,6 +25,10 @@ class ActivityLogLocalDataSourceImpl @Inject constructor() : ActivityLogLocalDat _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/repositoryImpl/ActivityLogRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt index 34aaa894..d2fc5c32 100644 --- 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 @@ -57,4 +57,8 @@ class ActivityLogRepositoryImpl @Inject constructor( .onSuccess { activityLogLocalDataSource.saveEmotionMarbles(yearMonth, it) } } } + + override fun invalidateEmotionMarbleCache(yearMonth: YearMonth) { + activityLogLocalDataSource.removeEmotionMarbles(yearMonth) + } } 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 index bd420b1d..f24b39ad 100644 --- 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 @@ -8,4 +8,5 @@ import java.time.YearMonth interface ActivityLogRepository { suspend fun getBadges(yearMonth: YearMonth): Result suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Result> + fun invalidateEmotionMarbleCache(yearMonth: YearMonth) } 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()) + } } } From f0901c3204ae816a6198ac995c9055fde6ac6ad4 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Mon, 27 Jul 2026 22:52:57 +0900 Subject: [PATCH 13/20] =?UTF-8?q?FIX:=20=EA=B0=90=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EA=B0=B1=EC=8B=A0=20=EA=B4=80=EB=A0=A8=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=20UseCase=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/repositoryImpl/EmotionRepositoryImpl.kt | 10 +--------- .../domain/emotion/repository/EmotionRepository.kt | 2 -- .../usecase/GetEmotionRegisteredEventFlowUseCase.kt | 12 ------------ .../presentation/screen/summary/SummaryViewModel.kt | 12 +----------- 4 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt 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 979b0eb2..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 @@ -8,8 +8,6 @@ import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.emitAll import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow @@ -17,7 +15,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.time.LocalDate -import java.time.YearMonth import javax.inject.Inject import javax.inject.Singleton @@ -28,7 +25,6 @@ class EmotionRepositoryImpl @Inject constructor( ) : EmotionRepository { private val fetchMutex = Mutex() - private val _emotionRegisteredEventFlow = MutableSharedFlow() override suspend fun getEmotions(): Result> { return emotionRemoteDataSource.getEmotions().map { response -> @@ -43,15 +39,11 @@ class EmotionRepositoryImpl @Inject constructor( } }.also { if (it.isSuccess) { - val today = LocalDate.now() - fetchAndSaveDailyEmotion(today = today, forceRefresh = true) - _emotionRegisteredEventFlow.emit(YearMonth.from(today)) + fetchAndSaveDailyEmotion(today = LocalDate.now(), forceRefresh = true) } } } - override suspend fun getEmotionRegisteredEventFlow(): Flow = _emotionRegisteredEventFlow.asSharedFlow() - override fun observeDailyEmotion(): Flow> = flow { fetchAndSaveDailyEmotion(LocalDate.now()) .onFailure { diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt index 569061d7..a95af407 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt @@ -4,12 +4,10 @@ import com.threegap.bitnagil.domain.emotion.model.DailyEmotion import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine import kotlinx.coroutines.flow.Flow -import java.time.YearMonth interface EmotionRepository { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotionMarbleType: String): Result> fun observeDailyEmotion(): Flow> - suspend fun getEmotionRegisteredEventFlow(): Flow fun clearCache() } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt deleted file mode 100644 index 1499eed1..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionRegisteredEventFlowUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.usecase - -import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import kotlinx.coroutines.flow.Flow -import java.time.YearMonth -import javax.inject.Inject - -class GetEmotionRegisteredEventFlowUseCase @Inject constructor( - private val emotionRepository: EmotionRepository, -) { - suspend operator fun invoke(): Flow = emotionRepository.getEmotionRegisteredEventFlow() -} 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 index becfea45..29928396 100644 --- 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 @@ -4,7 +4,6 @@ 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.domain.emotion.usecase.GetEmotionRegisteredEventFlowUseCase 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 @@ -22,14 +21,12 @@ import javax.inject.Inject class SummaryViewModel @Inject constructor( private val getBadgesUseCase: GetBadgesUseCase, private val getEmotionMarblesUseCase: GetEmotionMarblesUseCase, - private val getEmotionRegisteredEventFlowUseCase: GetEmotionRegisteredEventFlowUseCase, ) : ContainerHost, ViewModel() { override val container = container(initialState = SummaryState.INIT) init { onMonthChanged(YearMonth.now()) - observeEmotionRegisteredEvent() } fun onMonthChanged(newMonth: YearMonth) = intent { @@ -66,12 +63,6 @@ class SummaryViewModel @Inject constructor( reduce { state.copy(selectedEmotionDay = null) } } - private fun observeEmotionRegisteredEvent() = intent { - getEmotionRegisteredEventFlowUseCase().collect { registeredMonth -> - fetchEmotionMarbles(registeredMonth, forceRefresh = true) - } - } - private suspend fun fetchBadges(yearMonth: YearMonth) { subIntent { reduce { state.copy(loadingCount = state.loadingCount + 1) } @@ -93,14 +84,13 @@ class SummaryViewModel @Inject constructor( } } - private suspend fun fetchEmotionMarbles(yearMonth: YearMonth, forceRefresh: Boolean = false) { + private suspend fun fetchEmotionMarbles(yearMonth: YearMonth) { subIntent { reduce { state.copy(loadingCount = state.loadingCount + 1) } getEmotionMarblesUseCase( startDate = yearMonth.atDay(1), endDate = yearMonth.atEndOfMonth(), - forceRefresh = forceRefresh, ).fold( onSuccess = { marblesByDate -> val emotionCells = marblesByDate.values.map { it.toUiModel() } From d8529d27d42d043834095024216c31c890fe9d2e Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Wed, 29 Jul 2026 21:28:10 +0900 Subject: [PATCH 14/20] =?UTF-8?q?FIX:=20=EB=8B=AC=EB=A0=A5=20=EB=82=B4=20?= =?UTF-8?q?=EC=9D=B4=EC=A0=84/=EC=9D=B4=ED=9B=84=20=EB=8B=AC=EC=97=90=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=ED=95=98=EB=8A=94=20=EB=82=A0=EC=9E=90=20?= =?UTF-8?q?=EC=A4=91=20=EA=B0=90=EC=A0=95=EA=B5=AC=EC=8A=AC=EC=9D=B4=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=EB=90=9C=20=EA=B2=BD=EC=9A=B0=20=EA=B0=95?= =?UTF-8?q?=EC=A1=B0=20=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screen/summary/SummaryScreen.kt | 2 +- .../summarycalendar/SummaryCalendarView.kt | 26 ++++++++++++------- .../screen/summary/contract/SummaryState.kt | 3 +++ .../model/SummaryEmotionCellUiModel.kt | 5 ++-- 4 files changed, 24 insertions(+), 12 deletions(-) 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 index 44b2329a..07e2597e 100644 --- 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 @@ -161,7 +161,7 @@ fun SummaryScreen( SummaryCalendarView( yearMonth = displayMonth, - emotionDays = state.emotionCellsOf(displayMonth), + emotionDays = state.emotionCellsAround(displayMonth), onClickOtherMonthDay = { targetMonth -> scope.launch { val monthDiff = ChronoUnit.MONTHS.between(displayMonth, targetMonth) 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 index e00d6853..08ddb2f4 100644 --- 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 @@ -66,7 +66,7 @@ fun SummaryCalendarView( repeat(7) { dayIndex -> val date = startDate.plusDays((weekIndex * 7 + dayIndex).toLong()) val isCurrentMonth = date.monthValue == yearMonth.monthValue - val emotionCellUiModel = if (isCurrentMonth) emotionDays.firstOrNull { it.day == date.dayOfMonth } else null + val emotionCellUiModel = emotionDays.firstOrNull { it.date == date } // 이전/다음 달 날짜는 해당 달로 이동하고, 이번 달 날짜는 감정이 기록된 경우에만 선택할 수 있다. val onClick: (() -> Unit)? = when { @@ -119,7 +119,10 @@ fun SummaryCalendarCell( Box( modifier = Modifier .size(34.dp) - .background(color = emotionType?.backgroundColor ?: Color.Transparent, shape = CircleShape) + .background( + color = emotionType?.let { if (isCurrentMonth) it.backgroundColor else BitnagilTheme.colors.coolGray98 } ?: Color.Transparent, + shape = CircleShape, + ), ) Text( @@ -133,16 +136,21 @@ fun SummaryCalendarCell( @Preview(showBackground = true) @Composable private fun SummaryCalendarPreview() { + val currentMonth = YearMonth.now() + val prevMonth = currentMonth.minusMonths(1) + val nextMonth = currentMonth.plusMonths(1) BitnagilTheme { SummaryCalendarView( - yearMonth = YearMonth.now(), + yearMonth = currentMonth, emotionDays = listOf( - SummaryEmotionCellUiModel(6, SummaryEmotionType.CALM, ""), - SummaryEmotionCellUiModel(8, SummaryEmotionType.ANXIETY, ""), - SummaryEmotionCellUiModel(9, SummaryEmotionType.VITALITY, ""), - SummaryEmotionCellUiModel(11, SummaryEmotionType.LETHARGY, ""), - SummaryEmotionCellUiModel(14, SummaryEmotionType.SATISFACTION, ""), - SummaryEmotionCellUiModel(16, SummaryEmotionType.FATIGUE, ""), + SummaryEmotionCellUiModel(prevMonth.atDay(30), 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 index a68790f9..ad1c6eba 100644 --- 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 @@ -21,6 +21,9 @@ data class SummaryState( 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, 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 index 94876df9..efa729d8 100644 --- 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 @@ -2,16 +2,17 @@ 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 day: Int, + val date: LocalDate, val emotionType: SummaryEmotionType, val imageUrl: String, ) fun EmotionMarble.toUiModel(): SummaryEmotionCellUiModel = SummaryEmotionCellUiModel( - day = date.dayOfMonth, + date = date, emotionType = type.toUiModel(), imageUrl = imageUrl, ) From 89fa3720d3a04ff843376b7baee990498fd621ba Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Thu, 30 Jul 2026 21:27:32 +0900 Subject: [PATCH 15/20] =?UTF-8?q?FIX:=20=EA=B0=90=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EC=84=A0=ED=83=9D=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=93=9C=EB=9E=98=EA=B7=B8=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=ED=91=9C=EC=8B=9C=20=EC=A1=B0=EA=B1=B4?= =?UTF-8?q?=EC=9D=84=20600dp=EC=97=90=EC=84=9C=20700dp=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20UI=20=EA=B0=84=EA=B2=A9=20?= =?UTF-8?q?=EC=84=B8=EB=B6=80=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/screen/emotion/EmotionScreen.kt | 2 +- .../emotion/component/template/SwipeEmotionSelectionScreen.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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..4bfc5664 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 @@ -263,7 +263,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 +317,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) From 308bd8ac16869a52ba2ab0c08c0c55ce9a503139 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Thu, 30 Jul 2026 22:36:35 +0900 Subject: [PATCH 16/20] =?UTF-8?q?FIX:=20pagerState.currentPageOffsetFracti?= =?UTF-8?q?on=20=EC=B0=B8=EC=A1=B0=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20?= =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=20recomposition=20=EA=B0=90?= =?UTF-8?q?=EC=86=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/SwipeEmotionSelectionScreen.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 4bfc5664..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 @@ -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 From 133b893df4c7eadb9c8b8bab50f51b05e609aaba Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 1 Aug 2026 14:27:58 +0900 Subject: [PATCH 17/20] =?UTF-8?q?FIX:=20SummaryBadgeView=EB=82=B4=20Backgr?= =?UTF-8?q?ound=EC=9D=98=20modifier=20height=20=EC=86=8D=EC=84=B1=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0,=20SummaryCalendarView=20Preview=EC=9D=98=20?= =?UTF-8?q?=EC=9A=94=EC=9D=BC=20=ED=91=9C=EC=8B=9C=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/summarybadge/SummaryBadgeView.kt | 4 +--- .../component/template/summarycalendar/SummaryCalendarView.kt | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) 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 index 85345052..c1757bd5 100644 --- 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 @@ -9,7 +9,6 @@ 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.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -92,8 +91,7 @@ private fun Background( 0.3f to Color(0xFFFE7120), ), ) - .statusBarsPadding() - .height(260.dp), + .statusBarsPadding(), ) { val width = constraints.maxWidth.pxToDp() 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 index 08ddb2f4..6f12ef61 100644 --- 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 @@ -143,7 +143,7 @@ private fun SummaryCalendarPreview() { SummaryCalendarView( yearMonth = currentMonth, emotionDays = listOf( - SummaryEmotionCellUiModel(prevMonth.atDay(30), SummaryEmotionType.CALM, ""), + SummaryEmotionCellUiModel(prevMonth.atEndOfMonth(), SummaryEmotionType.CALM, ""), SummaryEmotionCellUiModel(currentMonth.atDay(6), SummaryEmotionType.CALM, ""), SummaryEmotionCellUiModel(currentMonth.atDay(8), SummaryEmotionType.ANXIETY, ""), SummaryEmotionCellUiModel(currentMonth.atDay(9), SummaryEmotionType.VITALITY, ""), From fb07c1f2a9580fdadb4b3768f70fca247e6c662d Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 1 Aug 2026 14:36:09 +0900 Subject: [PATCH 18/20] =?UTF-8?q?FIX:=20=EA=B0=90=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EB=93=B1=EB=A1=9D=20=ED=9B=84=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EB=AC=B4=ED=9A=A8=ED=99=94=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9B=94=EB=B3=84=20=EA=B0=90=EC=A0=95=20=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81=EA=B3=BC=20?= =?UTF-8?q?=EB=8F=99=EC=9D=BC=ED=95=9C=20mutex=EB=A5=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitylog/repositoryImpl/ActivityLogRepositoryImpl.kt | 6 ++++-- .../domain/activitylog/repository/ActivityLogRepository.kt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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 index d2fc5c32..94138ef4 100644 --- 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 @@ -58,7 +58,9 @@ class ActivityLogRepositoryImpl @Inject constructor( } } - override fun invalidateEmotionMarbleCache(yearMonth: YearMonth) { - activityLogLocalDataSource.removeEmotionMarbles(yearMonth) + override suspend fun invalidateEmotionMarbleCache(yearMonth: YearMonth) { + emotionMarbleFetchMutex.withLock { + activityLogLocalDataSource.removeEmotionMarbles(yearMonth) + } } } 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 index f24b39ad..a4cb2902 100644 --- 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 @@ -8,5 +8,5 @@ import java.time.YearMonth interface ActivityLogRepository { suspend fun getBadges(yearMonth: YearMonth): Result suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Result> - fun invalidateEmotionMarbleCache(yearMonth: YearMonth) + suspend fun invalidateEmotionMarbleCache(yearMonth: YearMonth) } From f22d5cca910bba6d82c618580f6d9956ad7bb1b1 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 1 Aug 2026 14:47:21 +0900 Subject: [PATCH 19/20] =?UTF-8?q?FIX:=20=EC=9B=94=EB=B3=84=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=EA=B5=AC=EC=8A=AC=20=EC=A1=B0=ED=9A=8C=20UseCase=20?= =?UTF-8?q?=EB=B0=8F=20Repository=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC?= =?UTF-8?q?=20=EA=B8=B0=EC=A1=B4=20LocalDate=EA=B8=B0=EB=B0=98=EC=97=90?= =?UTF-8?q?=EC=84=9C=20YearMonth=20=EA=B8=B0=EB=B0=98=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositoryImpl/ActivityLogRepositoryImpl.kt | 9 +++------ .../activitylog/repository/ActivityLogRepository.kt | 3 +-- .../activitylog/usecase/GetEmotionMarblesUseCase.kt | 6 +++--- .../presentation/screen/summary/SummaryViewModel.kt | 5 +---- 4 files changed, 8 insertions(+), 15 deletions(-) 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 index 94138ef4..41ad77c3 100644 --- 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 @@ -8,7 +8,6 @@ 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.LocalDate import java.time.YearMonth import javax.inject.Inject import javax.inject.Singleton @@ -37,9 +36,7 @@ class ActivityLogRepositoryImpl @Inject constructor( } } - override suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean): Result> { - val yearMonth = YearMonth.from(startDate) - + override suspend fun getEmotionMarbles(yearMonth: YearMonth, forceRefresh: Boolean): Result> { if (!forceRefresh) { activityLogLocalDataSource.emotionMarblesByMonth.value[yearMonth]?.let { return Result.success(it) } } @@ -50,8 +47,8 @@ class ActivityLogRepositoryImpl @Inject constructor( } activityLogRemoteDataSource.getEmotionMarbles( - startDate = startDate.toString(), - endDate = endDate.toString(), + startDate = yearMonth.atDay(1).toString(), + endDate = yearMonth.atEndOfMonth().toString(), ) .map { marbles -> marbles.map { it.toDomain() } } .onSuccess { activityLogLocalDataSource.saveEmotionMarbles(yearMonth, it) } 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 index a4cb2902..69e9288f 100644 --- 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 @@ -2,11 +2,10 @@ 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.LocalDate import java.time.YearMonth interface ActivityLogRepository { suspend fun getBadges(yearMonth: YearMonth): Result - suspend fun getEmotionMarbles(startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): 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/GetEmotionMarblesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/activitylog/usecase/GetEmotionMarblesUseCase.kt index 2cb04147..8b3e764b 100644 --- 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 @@ -3,17 +3,17 @@ 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( - startDate: LocalDate, - endDate: LocalDate, + yearMonth: YearMonth, forceRefresh: Boolean = false, ): Result> { - return activityLogRepository.getEmotionMarbles(startDate = startDate, endDate = endDate, forceRefresh = forceRefresh) + return activityLogRepository.getEmotionMarbles(yearMonth = yearMonth, forceRefresh = forceRefresh) .map { marbles -> marbles.associateBy { it.date } } } } 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 index 29928396..87be4ce0 100644 --- 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 @@ -88,10 +88,7 @@ class SummaryViewModel @Inject constructor( subIntent { reduce { state.copy(loadingCount = state.loadingCount + 1) } - getEmotionMarblesUseCase( - startDate = yearMonth.atDay(1), - endDate = yearMonth.atEndOfMonth(), - ).fold( + getEmotionMarblesUseCase(yearMonth).fold( onSuccess = { marblesByDate -> val emotionCells = marblesByDate.values.map { it.toUiModel() } reduce { From 5ce0978bf455a568bbf32f791a28a1822bba9632 Mon Sep 17 00:00:00 2001 From: yunsehwan Date: Sat, 1 Aug 2026 17:03:41 +0900 Subject: [PATCH 20/20] =?UTF-8?q?FIX:=20EmotionDayBottomSheet=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B0=90=EC=A0=95=20=EA=B5=AC=EC=8A=AC=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=91=9C=EC=8B=9C=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9D=84=20=EC=84=9C=EB=B2=84=EC=97=90=EC=84=9C=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC=EB=B0=9B=EC=9D=80=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EB=A5=BC=20=EA=B7=B8=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8A=94=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EmotionDayBottomSheet.kt | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) 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 index 0be6f637..f2c4aeb1 100644 --- 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 @@ -1,19 +1,15 @@ package com.threegap.bitnagil.presentation.screen.summary.component.template.emotiondaybottomsheet -import androidx.compose.foundation.Image import androidx.compose.foundation.background 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.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -25,7 +21,6 @@ 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.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage @@ -113,63 +108,21 @@ private fun EmotionDayBottomSheetContent( Spacer(modifier = Modifier.height(24.dp)) - Box( - modifier = Modifier - .fillMaxWidth() + 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), - ), - ) { - Text( - emotionDay.emotionType.displayName, - style = BitnagilTheme.typography.body2SemiBold, - color = emotionDay.emotionType.textColor, - maxLines = 1, - modifier = Modifier - .offset(x = 12.dp, y = 12.dp) - .background(color = emotionDay.emotionType.backgroundColor, shape = RoundedCornerShape(8.dp)) - .padding(horizontal = 18.dp, vertical = 5.dp) - ) - - Box( - modifier = Modifier.align(alignment = Alignment.BottomCenter), - contentAlignment = Alignment.BottomCenter - ) { - Image( - painter = painterResource(R.drawable.img_pomo_hand), - contentScale = ContentScale.FillBounds, - contentDescription = null, - modifier = Modifier - .width(88.dp) - .height(64.dp), - ) - - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(emotionDay.imageUrl) - .crossfade(true) - .build(), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .size(130.dp) - .offset(y = -(28).dp), ) - - Image( - painter = painterResource(R.drawable.img_pomo_thumb), - modifier = Modifier - .align(Alignment.BottomCenter) - .width(36.dp) - .height(28.dp) - .offset(x = (-6).dp, y = (-24).dp), - contentDescription = null, - ) - } - } + ) } }