diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5024c388..c7705019 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,6 +120,7 @@ dependencies { implementation(project(":feature:daily")) implementation(project(":feature:daily-edit")) implementation(project(":feature:daily-expand")) + implementation(project(":feature:daily-certify")) implementation(project(":feature:memorycard-registration")) implementation(project(":feature:question")) implementation(project(":feature:question-expand")) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index de53e4c5..14546588 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -1,6 +1,7 @@ package com.teampatch.harmony import android.net.Uri +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -9,10 +10,13 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.teampatch.core.common.findActivity -import com.teampatch.feature.answer.addAnswerScreen +import com.teampatch.daily.certify.addDailyCertifyAlarmScreen +import com.teampatch.daily.certify.navigateToDailyCertifyScreen import com.teampatch.feature.answer.navigateToAnswerScreen +import com.teampatch.feature.daily.edit.addDailyEditScreen import com.teampatch.feature.daily.edit.navigateToDailyEditScreen import com.teampatch.feature.daily.expand.addDailyExpandScreen +import com.teampatch.feature.daily.expand.navigateToDailyExpandScreen import com.teampatch.feature.family.info.addFamilyInfoScreen import com.teampatch.feature.family.info.navigateToFamilyInfoScreen import com.teampatch.feature.home.HomeRoute @@ -47,7 +51,6 @@ import com.teampatch.feature.onboarding.make.navigateToShareInvitationScreen import com.teampatch.feature.profile.edit.addProfileEditScreen import com.teampatch.feature.profile.edit.navigateToProfileEditScreen import com.teampatch.feature.question.addQuestionScreen -import com.teampatch.feature.question.detail.QuestionDetailParams import com.teampatch.feature.question.detail.addQuestionDetailScreen import com.teampatch.feature.question.detail.navigateToQuestionDetailScreen import com.teampatch.feature.question.expand.addQuestionExpandScreen @@ -179,14 +182,14 @@ fun MainNavHost( answerEditPageRequest = navController::navigateToAnswerScreen ) - addAnswerScreen( - onBackRequest = navController::navigateUp, - onCompleteRequest = { answer -> - navController.previousBackStackEntry?.savedStateHandle?.set( - key = QuestionDetailParams.ANSWER_UPDATE_DATA, - value = answer - ) - navController.popBackStack() + addDailyEditScreen( + onDismissRequest = navController::navigateUp, + onCompleteRequest = { todo -> + Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("new_todo", todo) + navController.navigateUp() } ) @@ -213,7 +216,8 @@ fun MainNavHost( ) addDailyScreen( - dailyExpandPageRequest = { navController.navigateToDailyScreen() } + dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, + dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) addDailyExpandScreen( @@ -222,6 +226,34 @@ fun MainNavHost( onDeleteClick = {} // 임시 ) + addDailyEditScreen( + onDismissRequest = navController::navigateUp, + onCompleteRequest = { todo -> + Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("todo_added", true) // 결과 저장 + navController.navigateUp() + } + ) + + addDailyCertifyAlarmScreen( + onPickImageScreenRequest = { navController.navigateToDailyCertifyScreen() }, + onDismissRequest = { navController.navigateToHomeScreen() }, + fromNotification = true + ) + +// addDailyCertifyScreen( +// viewModel = DailyCertifyViewModel(), +// onBackRequest = navController::navigateUp, +// onCertifyCompleteRequest = { navController.navigateToDailyCertifyDetailScreen() }, +// onNavigateToDetailRequest = { navController.navigateToDailyCertifyDetailScreen() } +// ) +// +// addDailyCertifyDetailScreen( +// onBackRequest = navController::navigateUp +// ) + addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, onRestartConversation = { navController.navigateToMemoryCardRegistrationScreen("memoryCardId") } diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 765718d2..68caa8f5 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -14,6 +14,9 @@ dependencies { implementation(project(":core:network")) implementation(project(":core:authentication")) implementation(project(":core:database")) + implementation(project(":core:designsystem")) + + implementation("androidx.work:work-runtime-ktx:2.9.0") implementation(libs.google.play.app.update) @@ -21,6 +24,7 @@ dependencies { implementation(libs.androidx.paging.compose) implementation(libs.androidx.security.crypto) + implementation(project(":feature:daily-certify")) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) diff --git a/core/data/src/main/AndroidManifest.xml b/core/data/src/main/AndroidManifest.xml index 241a6c89..d378463b 100644 --- a/core/data/src/main/AndroidManifest.xml +++ b/core/data/src/main/AndroidManifest.xml @@ -1,7 +1,9 @@ - + + \ No newline at end of file diff --git a/core/data/src/main/java/com/teampatch/core/data/repository/NotificationRepositoryImpl.kt b/core/data/src/main/java/com/teampatch/core/data/repository/NotificationRepositoryImpl.kt new file mode 100644 index 00000000..0fc15e67 --- /dev/null +++ b/core/data/src/main/java/com/teampatch/core/data/repository/NotificationRepositoryImpl.kt @@ -0,0 +1,103 @@ +package com.teampatch.core.data.repository + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.teampatch.core.designsystem.R.drawable.img_upload_cert +import com.teampatch.core.domain.repository.NotificationRepository +import com.teampatch.daily.certify.EditCertifyActivity +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import java.time.Duration +import java.time.LocalDateTime +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class NotificationRepositoryImpl @Inject constructor( + @ApplicationContext private val context: Context, +) : NotificationRepository { + override fun scheduleCertifyNotification(time: LocalDateTime) { + val delay = Duration.between(LocalDateTime.now(), time).toMillis() + val request = OneTimeWorkRequestBuilder() + .setInitialDelay(delay, TimeUnit.MILLISECONDS) + .build() + WorkManager.getInstance(context).enqueue(request) + } +} + +class CertifyNotificationWorker( + context: Context, + workerParams: WorkerParameters, +) : Worker(context, workerParams) { + + override fun doWork(): Result { + // ✅ 1. 먼저 채널 생성 + createNotificationChannel(applicationContext) + + // ✅ 2. 알림 생성 + val notificationManager = + applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val notification = NotificationCompat.Builder(applicationContext, "certify_channel") + .setContentTitle("인증 시간이에요!") + .setContentText("지금 바로 인증하러 가볼까요?") + .setContentIntent(createPendingIntent()) + .setSmallIcon(img_upload_cert) // ← 아이콘도 꼭 지정해야 보임! + .setAutoCancel(true) + .build() + + notificationManager.notify(1001, notification) + return Result.success() + } + + // ✅ 여기에 채널 생성 함수 추가 + private fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "인증 알림" + val description = "Daily certify notification" + val importance = NotificationManager.IMPORTANCE_HIGH + val channel = NotificationChannel("certify_channel", name, importance).apply { + this.description = description + } + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + notificationManager.createNotificationChannel(channel) + } + } + + private fun createPendingIntent(): PendingIntent { + val intent = Intent(applicationContext, EditCertifyActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra("FROM_NOTIFICATION", true) + } + + return PendingIntent.getActivity( + applicationContext, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + } +} + +@Module +@InstallIn(SingletonComponent::class) +abstract class NotificationModule { + + @Binds + abstract fun bindNotificationRepository( + impl: NotificationRepositoryImpl, + ): NotificationRepository +} \ No newline at end of file diff --git a/core/designsystem/src/main/res/drawable/img_upload_cert.xml b/core/designsystem/src/main/res/drawable/img_upload_cert.xml new file mode 100644 index 00000000..4731adba --- /dev/null +++ b/core/designsystem/src/main/res/drawable/img_upload_cert.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/core/domain/src/main/java/com/teampatch/core/domain/fake/FakeDailyRepository.kt b/core/domain/src/main/java/com/teampatch/core/domain/fake/FakeDailyRepository.kt new file mode 100644 index 00000000..aecbae81 --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/fake/FakeDailyRepository.kt @@ -0,0 +1,12 @@ +package com.teampatch.core.domain.fake + +import com.teampatch.core.domain.model.Todo +import com.teampatch.core.domain.repository.DailyRepository +import javax.inject.Inject + +class FakeDailyRepository @Inject constructor() : DailyRepository { + override suspend fun addDaily(todo: Todo): Result { + println("새 일과 추가됨: ${todo.title}") + return Result.success(Unit) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/teampatch/core/domain/model/DailyComment.kt b/core/domain/src/main/java/com/teampatch/core/domain/model/DailyComment.kt index 6798a140..66e819a6 100644 --- a/core/domain/src/main/java/com/teampatch/core/domain/model/DailyComment.kt +++ b/core/domain/src/main/java/com/teampatch/core/domain/model/DailyComment.kt @@ -5,4 +5,6 @@ data class DailyComment( val writerUid: String, val writerName: String, val content: String, + val imageUrl: String? = null, + val profileImageUrl: String? = null, ) \ No newline at end of file diff --git a/core/domain/src/main/java/com/teampatch/core/domain/repository/DailyRepository.kt b/core/domain/src/main/java/com/teampatch/core/domain/repository/DailyRepository.kt new file mode 100644 index 00000000..67ea6517 --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/repository/DailyRepository.kt @@ -0,0 +1,7 @@ +package com.teampatch.core.domain.repository + +import com.teampatch.core.domain.model.Todo + +interface DailyRepository { + suspend fun addDaily(todo: Todo): Result +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/teampatch/core/domain/repository/NotificationRepository.kt b/core/domain/src/main/java/com/teampatch/core/domain/repository/NotificationRepository.kt new file mode 100644 index 00000000..fb2529ed --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/repository/NotificationRepository.kt @@ -0,0 +1,7 @@ +package com.teampatch.core.domain.repository + +import java.time.LocalDateTime + +interface NotificationRepository { + fun scheduleCertifyNotification(time: LocalDateTime) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/AddDailyRoutineUseCase.kt b/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/AddDailyRoutineUseCase.kt new file mode 100644 index 00000000..064ce6b8 --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/AddDailyRoutineUseCase.kt @@ -0,0 +1,11 @@ +package com.teampatch.core.domain.usecase.daily + +import com.teampatch.core.domain.fake.FakeDailyRepository +import com.teampatch.core.domain.model.Todo +import javax.inject.Inject + +class AddDailyRoutineUseCase @Inject constructor( + private val repository: FakeDailyRepository, +) { + suspend operator fun invoke(todo: Todo): Result = repository.addDaily(todo) +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/ScheduleCertifyNotificationUseCase.kt b/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/ScheduleCertifyNotificationUseCase.kt new file mode 100644 index 00000000..eae2ba6f --- /dev/null +++ b/core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/ScheduleCertifyNotificationUseCase.kt @@ -0,0 +1,13 @@ +package com.teampatch.core.domain.usecase.daily + +import com.teampatch.core.domain.repository.NotificationRepository +import java.time.LocalDateTime +import javax.inject.Inject + +class ScheduleCertifyNotificationUseCase @Inject constructor( + private val notificationRepository: NotificationRepository, +) { + operator fun invoke(time: LocalDateTime) { + notificationRepository.scheduleCertifyNotification(time) + } +} \ No newline at end of file diff --git a/feature/daily-certify/.gitignore b/feature/daily-certify/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/feature/daily-certify/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/feature/daily-certify/build.gradle.kts b/feature/daily-certify/build.gradle.kts new file mode 100644 index 00000000..91956260 --- /dev/null +++ b/feature/daily-certify/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("teampatch.android.library") + id("teampatch.android.library.compose") + id("teampatch.android.hilt") + id("teampatch.android.feature") +} + +android { + namespace = "com.teampatch.feature.daily.certify" +} + +dependencies { + + implementation(project(":core:domain")) + implementation(project(":core:designsystem")) + + implementation("io.coil-kt:coil-compose:2.7.0") + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/AndroidManifest.xml b/feature/daily-certify/src/main/AndroidManifest.xml new file mode 100644 index 00000000..72e8de64 --- /dev/null +++ b/feature/daily-certify/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/CertifyStatus.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/CertifyStatus.kt new file mode 100644 index 00000000..14d4451a --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/CertifyStatus.kt @@ -0,0 +1,7 @@ +package com.teampatch.daily.certify + +enum class CertifyStatus { + BEFORE, // 인증 전 + PENDING, // 인증 중 or 서버 처리 대기 (비활성화 상태 등) + CONFIRMED, // 인증 완료 (댓글 남기기 가능 상태) +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt new file mode 100644 index 00000000..8dbf1ccc --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt @@ -0,0 +1,151 @@ +package com.teampatch.daily.certify + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.widthIn +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.teampatch.core.designsystem.R +import com.teampatch.core.designsystem.component.AppBar +import com.teampatch.core.designsystem.component.DefaultButton +import com.teampatch.core.designsystem.component.DefaultButtonColor +import com.teampatch.core.designsystem.component.SpeechBubble +import com.teampatch.core.designsystem.component.TypeWriterText +import com.teampatch.core.designsystem.theme.BL +import com.teampatch.core.designsystem.theme.HarmonyTheme +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun DailyAlarmRoute( + fromNotification: Boolean, + onPickImageScreenRequest: () -> Unit, + onDismissRequest: () -> Unit, +) { + val viewModel: DailyCertifyViewModel = hiltViewModel() + val uiState by viewModel.uiState + + // isLoading 추가해서 작업 필요 + DailyAlarmScreen( + onPickImageScreenRequest = onPickImageScreenRequest, + onDismissRequest = onDismissRequest, + uiState = uiState + ) + + // ✅ event 처리 +// LaunchedEffect(viewModel.sideEffect) { +// viewModel.sideEffect.flowWithLifecycle(lifecycleOwner.lifecycle).collect { +// when (it) { +// DailyCertifySideEffect.LoadError -> +// Toast.makeText(context, "데이터를 불러오지 못했습니다.", Toast.LENGTH_SHORT).show() +// DailyCertifySideEffect.PushPermissionDenied -> +// Toast.makeText(context, "푸시 권한을 허용해 주세요.", Toast.LENGTH_LONG).show() +// } +// } +// } +} + +@Composable +internal fun DailyAlarmScreen( + onPickImageScreenRequest: () -> Unit, + onDismissRequest: () -> Unit, + uiState: DailyCertifyUiState, +) { + Scaffold( + topBar = { + AppBar( + title = { + Text( + text = "일과 알림", + maxLines = 1, + modifier = Modifier.widthIn(max = 240.dp) + ) + } + ) + }, + bottomBar = { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) + ) { + // ✅ ✅ MemoryCard와 동일하게 DefaultButton 사용 + DefaultButton( + onClick = onPickImageScreenRequest, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = "인증사진 남기러 가기") + } + + Spacer(modifier = Modifier.height(8.dp)) + + DefaultButton( + onClick = onDismissRequest, + modifier = Modifier.fillMaxWidth(), + color = DefaultButtonColor(BL) + ) { + Text(text = "나중에 남기기") + } + } + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + SpeechBubble { + TypeWriterText( + text = buildString { + uiState.missionTime?.let { + append(it.format(DateTimeFormatter.ofPattern("a h:mm"))) + append("\n") + } + append(uiState.missionText) + } + ) + } + + Image( + painter = painterResource(R.drawable.ic_harmony_talk), + contentDescription = "icon", + modifier = Modifier + .padding(top = 24.dp) + .size(120.dp) + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun DailyAlarmScreenPreview() { + HarmonyTheme { + DailyAlarmScreen( + onPickImageScreenRequest = { }, + onDismissRequest = { }, + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30) + ) + ) + } +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyImageEvent.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyImageEvent.kt new file mode 100644 index 00000000..82833453 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyImageEvent.kt @@ -0,0 +1,6 @@ +package com.teampatch.daily.certify + +sealed class DailyCertifyImageEvent { +// data object OnImageSelected(val uri: Uri) : DailyCertifyImageEvent() + data object OnNextClicked : DailyCertifyImageEvent() +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyInfoEvent.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyInfoEvent.kt new file mode 100644 index 00000000..a3db2f4c --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyInfoEvent.kt @@ -0,0 +1,7 @@ +package com.teampatch.daily.certify + +sealed class DailyCertifyInfoEvent { + data class OnMissonChanged(val value: String) : DailyCertifyInfoEvent() + data class OnTimeChanged(val value: String) : DailyCertifyInfoEvent() + data object OnCompleteClicked : DailyCertifyInfoEvent() +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyNavigation.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyNavigation.kt new file mode 100644 index 00000000..2e262eee --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyNavigation.kt @@ -0,0 +1,82 @@ +package com.teampatch.daily.certify + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.Navigator +import androidx.navigation.compose.composable +import kotlinx.serialization.Serializable + +@Serializable +data object DailyCertifyAlarmScreenRoute + +fun NavController.navigateToDailyCertifyAlarmScreen( + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, +) { + navigate(DailyCertifyAlarmScreenRoute, navOptions, navigatorExtras) +} + +fun NavGraphBuilder.addDailyCertifyAlarmScreen( + onPickImageScreenRequest: () -> Unit, + onDismissRequest: () -> Unit, + fromNotification: Boolean, +) { + composable { + DailyAlarmRoute( + onPickImageScreenRequest = onPickImageScreenRequest, + onDismissRequest = onDismissRequest, + fromNotification = fromNotification + ) + } +} + +@Serializable +data object DailyCertifyScreenRoute + +fun NavController.navigateToDailyCertifyScreen( + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, +) { + navigate(DailyCertifyScreenRoute, navOptions, navigatorExtras) +} + +fun NavGraphBuilder.addDailyCertifyScreen( + viewModel: DailyCertifyViewModel, + onBackRequest: () -> Unit, + onCertifyCompleteRequest: () -> Unit, + onNavigateToDetailRequest: () -> Unit, +) { + composable { + DailyCertifyRoute( + viewModel = viewModel, + onBackRequest = onBackRequest, + onCertifyCompleteRequest = onCertifyCompleteRequest, + onNavigateToDetailRequest = onNavigateToDetailRequest + ) + } +} + +@Serializable +data object DailyCertifyDetailScreenRoute + +fun NavController.navigateToDailyCertifyDetailScreen( + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, +) { + navigate(DailyCertifyDetailScreenRoute, navOptions, navigatorExtras) +} + +@OptIn(ExperimentalMaterial3Api::class) +fun NavGraphBuilder.addDailyCertifyDetailScreen( + viewModel: DailyCertifyViewModel, + onBackRequest: () -> Unit, +) { + composable { + DailyCertifyDetailRoute( + viewModel = viewModel, + onBackRequest = onBackRequest + ) + } +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyScreen.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyScreen.kt new file mode 100644 index 00000000..59ae544c --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyScreen.kt @@ -0,0 +1,682 @@ +package com.teampatch.daily.certify + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.rememberModalBottomSheetState +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 +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.rememberAsyncImagePainter +import com.teampatch.core.designsystem.R.drawable.ic_camera_profile +import com.teampatch.core.designsystem.R.drawable.ic_more_question +import com.teampatch.core.designsystem.R.drawable.ic_my_appbar +import com.teampatch.core.designsystem.R.drawable.img_upload_cert +import com.teampatch.core.designsystem.component.BackButtonAppBar +import com.teampatch.core.designsystem.component.DefaultButton +import com.teampatch.core.designsystem.component.RoundButton +import com.teampatch.core.designsystem.theme.BL +import com.teampatch.core.designsystem.theme.G1 +import com.teampatch.core.designsystem.theme.G5 +import com.teampatch.core.designsystem.theme.HarmonyTheme +import com.teampatch.core.designsystem.theme.MainGreen +import com.teampatch.core.designsystem.theme.PretendardFontFamily +import com.teampatch.core.designsystem.theme.SubRed +import com.teampatch.core.designsystem.theme.WH +import com.teampatch.core.domain.model.DailyComment +import com.teampatch.feature.daily.certify.R.string.text_float_add_comment +import com.teampatch.feature.daily.certify.R.string.text_title_appbar +import java.time.LocalTime +import java.time.format.DateTimeFormatter + +@Composable +internal fun DailyCertifyRoute( + viewModel: DailyCertifyViewModel, + onBackRequest: () -> Unit, + onCertifyCompleteRequest: () -> Unit, + onNavigateToDetailRequest: () -> Unit, +) { + val uiState by viewModel.uiState + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { uri: Uri? -> + uri?.let { + viewModel.updateImage(it.toString()) + } + } + ) + + val hasNavigated = rememberSaveable { mutableStateOf(false) } + + LaunchedEffect(uiState.certifyStatus) { + if (uiState.certifyStatus == CertifyStatus.CONFIRMED && !hasNavigated.value) { + hasNavigated.value = true + onNavigateToDetailRequest() + } + } + + DailyCertifyScreen( + uiState = uiState, + onBackRequest = onBackRequest, + onCommentEditRequest = { viewModel.editComment(it) }, + onCertifyCompleteRequest = onCertifyCompleteRequest, + onOpenCommentSheet = { viewModel.openCommentSheet() }, + onImagePickRequest = { launcher.launch("image/*") }, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, + onBottomSheetDismiss = {}, + commentImageUri = null, + onCommentDeleteRequest = {} + ) +} + +@ExperimentalMaterial3Api +@Composable +fun DailyCertifyDetailRoute( + viewModel: DailyCertifyViewModel, + onBackRequest: () -> Unit, +) { + val uiState by viewModel.uiState + + val commentImageUri = remember { mutableStateOf(null) } + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { uri: Uri? -> + uri?.let { + commentImageUri.value = it.toString() + } + } + ) + + DailyCertifyScreen( + uiState = uiState, + onBackRequest = onBackRequest, + onCommentEditRequest = { viewModel.editComment(it) }, + onCertifyCompleteRequest = {}, + onOpenCommentSheet = { + commentImageUri.value = null + viewModel.openCommentSheet() + }, + onImagePickRequest = { launcher.launch("image/*") }, + onCommentWrite = { text -> viewModel.addComment(text, commentImageUri.value) }, + onCommentEditSubmit = { id, content -> viewModel.updateComment(id, content) }, + onBottomSheetDismiss = { + viewModel.closeCommentSheet() + commentImageUri.value = null + }, + commentImageUri = commentImageUri.value, + onCommentDeleteRequest = { viewModel.deleteComment(it) } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun DailyCertifyScreen( + uiState: DailyCertifyUiState, + onBackRequest: () -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onCommentDeleteRequest: (DailyComment?) -> Unit, + onCertifyCompleteRequest: () -> Unit, + onOpenCommentSheet: () -> Unit, + onImagePickRequest: () -> Unit, + onCommentWrite: (String) -> Unit, + onCommentEditSubmit: (String, String) -> Unit, + onBottomSheetDismiss: () -> Unit, + commentImageUri: String?, +) { + val commentEditSheetState = rememberModalBottomSheetState() + val commentWriteSheetState = rememberModalBottomSheetState() + val isImageUploaded = uiState.imageUrl?.isNotBlank() == true + val isCertifyConfirmed = uiState.certifyStatus == CertifyStatus.CONFIRMED + val lazyListState = rememberLazyListState() + val isFloatingVisible by remember { + derivedStateOf { + !lazyListState.isScrollInProgress && !lazyListState.canScrollBackward + } + } + + LaunchedEffect(commentWriteSheetState.isVisible) { + if (!commentWriteSheetState.isVisible && uiState.showCommentSheet) { + onBottomSheetDismiss() + } + } + + LaunchedEffect(commentEditSheetState.isVisible) { + if (!commentEditSheetState.isVisible && uiState.editingComment != null) { + onBottomSheetDismiss() + } + } + + Scaffold( + topBar = { + BackButtonAppBar( + onBackRequest = onBackRequest, + title = { + Text(stringResource(text_title_appbar)) + } + ) + }, + bottomBar = { + when { + !isImageUploaded -> { + DefaultButton( + onClick = onCertifyCompleteRequest, + enabled = false, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text("인증 완료") + } + } + + uiState.certifyStatus == CertifyStatus.PENDING -> { + DefaultButton( + onClick = onCertifyCompleteRequest, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text("인증 완료") + } + } + + isCertifyConfirmed -> { + // ✅ 인증 완료 시 bottomBar는 비우거나 "제어용 UI"로 대체 가능 + Spacer(modifier = Modifier.height(1.dp)) // 유지용 + } + } + } + ) { paddingValues -> + Box(Modifier.fillMaxSize()) { + Column( + Modifier + .padding(paddingValues) + .fillMaxSize() + .background(color = WH) + ) { + when { + uiState.imageUrl?.isNotBlank() == true -> { + CertifyImage(imageUrl = uiState.imageUrl) + } + + else -> { + CertifyImagePlaceholder(onClick = onImagePickRequest) + } + } + + Spacer(Modifier.height(12.dp)) + + MissionInfoSection( + missionText = uiState.missionText, + missionTime = uiState.missionTime + ) + + if (isCertifyConfirmed) { + LazyColumn( + state = lazyListState, + modifier = Modifier + .fillMaxSize() + .background(G1), + contentPadding = PaddingValues( + top = 8.dp, + bottom = 120.dp // 댓글 남기기 UI를 위한 padding + ) + ) { + item { + Text( + text = "댓글 ${uiState.comments.size}", + fontFamily = PretendardFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = G5, + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 12.dp) + ) + } + + items(uiState.comments, key = { it.commentId }) { comment -> + DailyCertifyCommentItem( + comment = comment, + onEditClick = { onCommentEditRequest(comment) }, + onDeleteClick = { onCommentDeleteRequest(comment) } + ) + } + } + } + } + + if (uiState.showCommentSheet) { + ModalBottomSheet( + onDismissRequest = { + onBottomSheetDismiss() + }, + sheetState = commentWriteSheetState + ) { + CommentWriteSheetContent( + onSubmit = { text -> + onCommentWrite(text) + onBottomSheetDismiss() + }, + onImagePick = onImagePickRequest, + imageUrl = commentImageUri + ) + } + } + + uiState.editingComment?.let { editingComment -> + ModalBottomSheet( + onDismissRequest = { + onCommentEditRequest(null) + }, + sheetState = commentEditSheetState + ) { + CommentEditSheetContent( + initialText = editingComment.content, + onSubmit = { updatedText -> + onCommentEditSubmit(editingComment.commentId, updatedText) + onBottomSheetDismiss() + } + ) + } + } + + // 댓글 남기기 UI (Floating UI) + if (isCertifyConfirmed) { + val shouldShowFloatingCommentButton = isCertifyConfirmed && isFloatingVisible + // IDE에서 always true로 추론하는 건 Preview 상의 오해임 – 런타임에는 유동적 + + AnimatedVisibility( + visible = shouldShowFloatingCommentButton, + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) + ) { + RoundButton( + onClick = onOpenCommentSheet, + modifier = Modifier + .padding(bottom = 8.dp) + .size(200.dp, 68.dp) + ) { + Text( + text = stringResource(text_float_add_comment), + fontSize = 22.sp + ) + } + } + } + } + } +} + +@Composable +private fun MissionInfoSection( + missionText: String, + missionTime: LocalTime?, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = missionText, + fontFamily = PretendardFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + color = BL + ) + + missionTime?.let { + Spacer(Modifier.height(4.dp)) + Text( + text = it.format(DateTimeFormatter.ofPattern("a h:mm")), + fontFamily = PretendardFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + color = MainGreen + ) + } + } +} + +@Composable +fun CertifyImagePlaceholder( + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Column( + modifier = modifier + .fillMaxWidth() + .height(240.dp) + .background(G1) + .clickable(onClick = onClick), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(ic_camera_profile), + contentDescription = null, + tint = MainGreen, + modifier = Modifier.size(48.dp) + ) + Spacer(Modifier.height(8.dp)) + Text( + text = "이곳을 눌러 사진을 남겨보세요!", + color = MainGreen, + fontSize = 16.sp, + fontWeight = FontWeight.Bold + ) + } +} + +// preview용 +@Composable +fun CertifyImage(imageUrl: String?) { + val isPreview = LocalInspectionMode.current + + Image( + painter = if (isPreview) { + painterResource(id = img_upload_cert) + } else { + rememberAsyncImagePainter(imageUrl) + }, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .height(240.dp), + contentScale = ContentScale.Crop + ) +} + +@Composable +fun DailyCertifyCommentItem( + comment: DailyComment, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var showMenu by remember { mutableStateOf(false) } + + Column( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 12.dp) + .background(color = WH) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = comment.profileImageUrl?.let { + rememberAsyncImagePainter(it) + } ?: painterResource(ic_my_appbar), + contentDescription = "user profile", + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + ) + + Text( + text = comment.writerName, + fontFamily = PretendardFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 18.sp, + color = G5, + modifier = Modifier + .padding(start = 16.dp) + .weight(1f) + ) + + Box { + IconButton(onClick = { showMenu = true }) { + Icon( + painter = painterResource(ic_more_question), + contentDescription = "더보기 메뉴", + tint = G5 + ) + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + shape = RoundedCornerShape(10.dp) + ) { + DropdownMenuItem( + text = { Text("수정", fontSize = 16.sp) }, + onClick = { + onEditClick() + showMenu = false + } + ) + DropdownMenuItem( + text = { Text("삭제", fontSize = 16.sp, color = SubRed) }, + onClick = { + onDeleteClick() + showMenu = false + } + ) + } + } + } + + Text( + text = comment.content, + fontFamily = PretendardFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 20.sp, + color = BL, + modifier = Modifier.padding(top = 12.dp) + ) + } +} + +@Composable +fun CommentWriteSheetContent( + onSubmit: (String) -> Unit, + onImagePick: () -> Unit, + imageUrl: String?, +) { + var comment by remember { mutableStateOf("") } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text("댓글 남기기", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(12.dp)) + + if (imageUrl.isNullOrBlank()) { + CertifyImagePlaceholder(onClick = onImagePick) + } else { + CertifyImage(imageUrl) + } + + TextField( + value = comment, + onValueChange = { comment = it }, + placeholder = { Text("댓글을 입력해주세요.") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(12.dp)) + DefaultButton( + onClick = { onSubmit(comment) }, + enabled = comment.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) { + Text("작성 완료") + } + } +} + +@Composable +fun CommentEditSheetContent( + initialText: String, + onSubmit: (String) -> Unit, +) { + var comment by remember { mutableStateOf(initialText) } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text("댓글 수정", fontSize = 18.sp, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(12.dp)) + TextField( + value = comment, + onValueChange = { comment = it }, + placeholder = { Text("댓글을 입력해주세요.") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(12.dp)) + DefaultButton( + onClick = { onSubmit(comment) }, + enabled = comment.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) { + Text("수정 완료") + } + } +} + +@Preview(name = "1. 인증 전 (작성 전)", showBackground = true) +@Composable +private fun DailyCertifyScreenPreview_Initial() { + HarmonyTheme { + DailyCertifyScreen( + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30), + imageUrl = "", // 아직 사진 없음 + comments = emptyList(), + editingComment = null, + certifyStatus = CertifyStatus.BEFORE + ), + onBackRequest = {}, + onCommentEditRequest = {}, + onCertifyCompleteRequest = {}, + onOpenCommentSheet = {}, + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, + onBottomSheetDismiss = {}, + commentImageUri = null, + onCommentDeleteRequest = {} + ) + } +} + +@Preview(name = "2. 인증 완료 (댓글 없음)", showBackground = true) +@Composable +private fun DailyCertifyScreenPreview_Completed_NoComment() { + HarmonyTheme { + DailyCertifyScreen( + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30), + imageUrl = "file:///Users/t2023-m0086/Desktop/%E1%84%8F%E1%85%A1%E1%84%85%E1%85%B5%E1%84%82%E1%85%A1.jpg", + comments = emptyList(), + editingComment = null, + certifyStatus = CertifyStatus.PENDING + + ), + onBackRequest = {}, + onCommentEditRequest = {}, + onCertifyCompleteRequest = {}, + onOpenCommentSheet = {}, + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, + onBottomSheetDismiss = {}, + commentImageUri = null, + onCommentDeleteRequest = {} + ) + } +} + +@Preview(name = "3. 인증 완료 (댓글 있음)", showBackground = true) +@Composable +private fun DailyCertifyScreenPreview_Completed_WithComment() { + HarmonyTheme { + DailyCertifyScreen( + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30), + imageUrl = "file:///Users/t2023-m0086/Desktop/%E1%84%8F%E1%85%A1%E1%84%85%E1%85%B5%E1%84%82%E1%85%A1.jpg", + comments = listOf( + DailyComment("1", "순대 조던", "유정상", "비둘기 너무 귀여워요"), + DailyComment("2", "김소라", "씹희", "부산 날씨 완전 봄이야") + ), + editingComment = null, + certifyStatus = CertifyStatus.CONFIRMED // 이걸 넣어야 댓글 목록이 나타남! + + ), + onBackRequest = {}, + onCommentEditRequest = {}, + onCertifyCompleteRequest = {}, + onOpenCommentSheet = {}, + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, + onBottomSheetDismiss = {}, + commentImageUri = null, + onCommentDeleteRequest = {} + ) + } +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyUiState.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyUiState.kt new file mode 100644 index 00000000..ef5cab94 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyUiState.kt @@ -0,0 +1,15 @@ +package com.teampatch.daily.certify + +import com.teampatch.core.domain.model.DailyComment +import java.time.LocalTime + +data class DailyCertifyUiState( + val missionText: String = "", + val missionTime: LocalTime? = null, + val imageUrl: String? = null, // (이미지가 URL인 경우) + val certifyStatus: CertifyStatus = CertifyStatus.BEFORE, + val comments: List = emptyList(), + val editingComment: DailyComment? = null, // 수정 중인 댓글이 있으면 bottomSheet 띄움 + val showCertifyDialog: Boolean = false, + val showCommentSheet: Boolean = false, +) \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyViewModel.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyViewModel.kt new file mode 100644 index 00000000..eb379d3b --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyViewModel.kt @@ -0,0 +1,90 @@ +package com.teampatch.daily.certify + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import com.teampatch.core.domain.model.DailyComment +import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalTime +import java.util.UUID +import javax.inject.Inject + +@HiltViewModel +class DailyCertifyViewModel @Inject constructor() : ViewModel() { + private val _uiState = mutableStateOf( + DailyCertifyUiState( + missionText = "오늘의 인증 미션", + missionTime = LocalTime.of(14, 0), // 예시 시간, + imageUrl = null, // ✅ 중요 + certifyStatus = CertifyStatus.PENDING, + comments = listOf( + DailyComment("1", "소금형", "씹직", "고양이 귀엽네"), + DailyComment("2", "짱구맘", "인직", "나도 사진 찍을래!") + ) + ) + ) + val uiState: State = _uiState + + fun completeCertify() { + _uiState.value = _uiState.value.copy( + certifyStatus = CertifyStatus.CONFIRMED + ) + } + + fun editComment(comment: DailyComment?) { + _uiState.value = _uiState.value.copy( + editingComment = comment + ) + } + + fun deleteComment(comment: DailyComment?) { + _uiState.value = _uiState.value.copy( + comments = _uiState.value.comments.filterNot { it.commentId == comment?.commentId } + ) + } + + fun openCommentSheet() { + _uiState.value = _uiState.value.copy( + showCommentSheet = true + ) + } + + fun onDismiss() { + // TODO: 인증 완료 종료 시 처리할 것 + } + + fun addComment(content: String, imageUrl: String?) { + val newComment = DailyComment( + commentId = UUID.randomUUID().toString(), + writerName = "작성자", + writerUid = "", + content = content, + imageUrl = imageUrl, + profileImageUrl = imageUrl // ✅ 첨부한 이미지를 프로필로도 활용 + ) + _uiState.value = _uiState.value.copy( + comments = _uiState.value.comments + newComment, + showCommentSheet = false + ) + } + + fun updateComment(commentId: String, newContent: String) { + _uiState.value = _uiState.value.copy( + comments = _uiState.value.comments.map { + if (it.commentId == commentId) it.copy(content = newContent) else it + }, + editingComment = null + ) + } + + fun closeCommentSheet() { + _uiState.value = _uiState.value.copy( + showCommentSheet = false, + editingComment = null + ) + } + + fun updateImage(uri: String) { + _uiState.value = _uiState.value.copy(imageUrl = uri) + } +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/java/com/teampatch/daily/certify/EditCertifyActivity.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/EditCertifyActivity.kt new file mode 100644 index 00000000..c6e97cb2 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/EditCertifyActivity.kt @@ -0,0 +1,63 @@ +package com.teampatch.daily.certify + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.teampatch.core.designsystem.theme.HarmonyTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class EditCertifyActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val isFromNotification = intent.getBooleanExtra("FROM_NOTIFICATION", false) + + setContent { + HarmonyTheme { + val navController = rememberNavController() + val viewModel: DailyCertifyViewModel = viewModel() // ✅ Activity 범위에서 생성 + + NavHost( + navController = navController, + startDestination = "daily_alarm" + ) { + composable("daily_alarm") { + DailyAlarmRoute( + fromNotification = isFromNotification, + onDismissRequest = { finish() }, + onPickImageScreenRequest = { + navController.navigate(DailyCertifyScreenRoute) + } + ) + } + + // Type-safe composable 추가 + composable { + DailyCertifyRoute( + viewModel = viewModel, + onBackRequest = { navController.popBackStack() }, + onCertifyCompleteRequest = { viewModel.completeCertify() }, + onNavigateToDetailRequest = { + navController.navigate(DailyCertifyDetailScreenRoute) + } + ) + } + + composable { + DailyCertifyDetailRoute( + viewModel = viewModel, + onBackRequest = { navController.popBackStack() } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/feature/daily-certify/src/main/res/values/strings.xml b/feature/daily-certify/src/main/res/values/strings.xml new file mode 100644 index 00000000..bd62e608 --- /dev/null +++ b/feature/daily-certify/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + 일과 인증 + 댓글 남기기 + \ No newline at end of file diff --git a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditNavigation.kt b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditNavigation.kt index 729f7015..75e0b33a 100644 --- a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditNavigation.kt +++ b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditNavigation.kt @@ -5,23 +5,24 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.compose.composable +import com.teampatch.core.domain.model.Todo import kotlinx.serialization.Serializable @Serializable -data object DailyEditRoute +data object DailyEditScreenRoute fun NavController.navigateToDailyEditScreen( navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null, ) { - navigate(DailyEditRoute, navOptions, navigatorExtras) + navigate(DailyEditScreenRoute, navOptions, navigatorExtras) } fun NavGraphBuilder.addDailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, ) { - composable { + composable { DailyEditRoute( onDismissRequest = onDismissRequest, onCompleteRequest = onCompleteRequest diff --git a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditScreen.kt b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditScreen.kt index 0a672423..88310ba7 100644 --- a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditScreen.kt +++ b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditScreen.kt @@ -6,10 +6,10 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -17,7 +17,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.material3.FilterChip -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,15 +24,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -43,87 +41,86 @@ import com.teampatch.core.designsystem.R import com.teampatch.core.designsystem.component.AppBar import com.teampatch.core.designsystem.component.DefaultButton import com.teampatch.core.designsystem.component.DefaultTextField -import com.teampatch.core.designsystem.theme.BL import com.teampatch.core.designsystem.theme.G2 import com.teampatch.core.designsystem.theme.HarmonyTheme -import com.teampatch.core.designsystem.theme.PretendardFontFamily import com.teampatch.core.designsystem.theme.WH import com.teampatch.core.designsystem.utils.noRippleClickable -import com.teampatch.core.domain.fake.FakeDailyManage +import com.teampatch.core.domain.model.Todo import com.teampatch.feature.daily.edit.R.string.btn_complete_daily import com.teampatch.feature.daily.edit.R.string.select_time import com.teampatch.feature.daily.edit.R.string.select_week_days import com.teampatch.feature.daily.edit.R.string.text_per_daily import com.teampatch.feature.daily.edit.R.string.title_daily import java.time.DayOfWeek +import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime import java.time.format.TextStyle -import java.util.Calendar import java.util.Locale +import java.util.UUID @Composable internal fun DailyEditRoute( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, viewModel: DailyEditViewModel = hiltViewModel(), ) { val context = LocalContext.current val uiState by viewModel.dailyEditUiState - if (!uiState.isLoading) { - DailyEditScreen( - onDismissRequest = onDismissRequest, - onCompleteRequest = { - onCompleteRequest(it) + + val timePickerLauncher = rememberUpdatedState { + val now = LocalTime.now() + TimePickerDialog( + context, + { _, hour, minute -> + val selectedTime = LocalTime.of(hour, minute) + viewModel.changeSelectedTime(selectedTime) }, - uiState = uiState, - selectedDays = uiState.selectedDays, - onDaySelected = { viewModel.toggleSelectedDay(it) }, - onChangeDaily = { viewModel.changeDailyContent(it) } - ) + now.hour, + now.minute, + true + ).show() } LaunchedEffect(Unit) { viewModel.event.collect { when (it) { is DailyEditEvent.AddDailyError -> { - Toast.makeText(context, "서버로 부터 데이터 전송 오류", Toast.LENGTH_LONG).show() + Toast.makeText(context, "서버 전송 오류", Toast.LENGTH_LONG).show() } - is DailyEditEvent.LoadError -> { - Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_LONG).show() + Toast.makeText(context, "데이터 불러오기 실패", Toast.LENGTH_LONG).show() } } } } + + if (!uiState.isLoading) { + DailyEditScreen( + onDismissRequest = onDismissRequest, + onCompleteRequest = { todo -> + viewModel.onTimeSelected(todo.dateTime) + onCompleteRequest(todo) + }, + selectedDays = uiState.selectedDays, + onDaySelected = { viewModel.toggleSelectedDay(it) }, + selectedTime = uiState.selectedTime, + onTimePickRequest = { timePickerLauncher.value() } // 👈 NEW + + ) + } } @Composable internal fun DailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, - uiState: DailyEditUiState, + onCompleteRequest: (Todo) -> Unit, selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, - onChangeDaily: (String) -> Unit, + selectedTime: LocalTime?, + onTimePickRequest: () -> Unit, ) { - val context = LocalContext.current - val daily = uiState.dailyExpand.content - var time: LocalTime? by rememberSaveable { mutableStateOf(null) } - val calendar = remember { Calendar.getInstance() } - val hour = remember { calendar.get(Calendar.HOUR_OF_DAY) } - val minute = remember { calendar.get(Calendar.MINUTE) } - - val timePickerDialog = remember { - TimePickerDialog( - context, - { _, selectedHour, selectedMinute -> - time = LocalTime.of(selectedHour, selectedMinute) - }, - hour, - minute, - true - ) - } + val textState = rememberSaveable { mutableStateOf("") } val daysOfWeek = remember { DayOfWeek.values() } Scaffold( @@ -143,8 +140,17 @@ internal fun DailyEditScreen( }, bottomBar = { DefaultButton( - onClick = { onCompleteRequest(daily) }, - enabled = daily.isNotBlank(), + onClick = { + val todo = Todo( + id = UUID.randomUUID().toString(), + title = textState.value, + dateTime = selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } + ?: LocalDateTime.now(), + isFinished = false + ) + onCompleteRequest(todo) + }, + enabled = textState.value.isNotBlank() && selectedDays.isNotEmpty() && selectedTime != null, modifier = Modifier .fillMaxWidth() .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) @@ -156,48 +162,29 @@ internal fun DailyEditScreen( Column( modifier = Modifier .padding(scaffoldPaddingValues) - .height(IntrinsicSize.Max) - .background( - color = MaterialTheme.colorScheme.background, - shape = RoundedCornerShape(10.dp) - ) - .padding(top = 24.dp, bottom = 14.dp, start = 16.dp, end = 16.dp) + .fillMaxHeight() + .padding(16.dp) ) { - Text( - text = stringResource(text_per_daily), - fontFamily = PretendardFontFamily, - color = BL, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(top = 32.dp, bottom = 8.dp) - ) + Text(stringResource(text_per_daily), fontSize = 18.sp) - Box( - modifier = Modifier - .padding(horizontal = 20.dp) - ) { - DefaultTextField( - value = uiState.dailyExpand.content, - onValueChange = { - if (it.length <= 200) { - onChangeDaily(it) // ViewModel의 함수 호출 - } - }, - singleLine = false, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.None), - modifier = Modifier.height(IntrinsicSize.Max) - ) - } + Spacer(modifier = Modifier.height(12.dp)) - Text( - text = stringResource(select_week_days), - fontFamily = PretendardFontFamily, - color = BL, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(top = 32.dp, bottom = 8.dp) + // ✅ placeholder 적용 + DefaultTextField( + value = textState.value, + onValueChange = { + if (it.length <= 200) textState.value = it + }, + hint = { Text("예) 아침식사 먹기") }, + singleLine = false, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.None), + modifier = Modifier.fillMaxWidth() ) + Spacer(modifier = Modifier.height(24.dp)) + + Text(stringResource(select_week_days), fontSize = 18.sp) + Row( horizontalArrangement = Arrangement.SpaceEvenly, modifier = Modifier @@ -209,44 +196,33 @@ internal fun DailyEditScreen( selected = selectedDays.contains(day), onClick = { onDaySelected(day) }, label = { Text(day.getDisplayName(TextStyle.SHORT, Locale.KOREAN)) }, - modifier = Modifier.padding(horizontal = 2.dp), - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = Color.Green - ) + modifier = Modifier.padding(horizontal = 2.dp) ) } } - Text( - text = stringResource(select_time), - fontFamily = PretendardFontFamily, - color = BL, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(top = 32.dp, bottom = 8.dp) - ) + Spacer(modifier = Modifier.height(24.dp)) + + Text(stringResource(select_time), fontSize = 18.sp) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .height(52.dp) - .background(color = WH, shape = RoundedCornerShape(10.dp)) - .border(width = 1.dp, color = G2, shape = RoundedCornerShape(10.dp)) + .background(WH, RoundedCornerShape(10.dp)) + .border(1.dp, G2, RoundedCornerShape(10.dp)) .padding(horizontal = 20.dp) - .noRippleClickable { timePickerDialog.show() } + .noRippleClickable { onTimePickRequest() } ) { Image( painter = painterResource(R.drawable.ic_date_memory_card), contentDescription = "time" ) Text( - text = time?.let { String.format("%02d:%02d", it.hour, it.minute) } + text = selectedTime?.let { "%02d:%02d".format(it.hour, it.minute) } ?: stringResource(select_time), - color = BL, fontSize = 20.sp, - fontFamily = PretendardFontFamily, - fontWeight = FontWeight.Medium, modifier = Modifier.padding(start = 20.dp) ) } @@ -254,27 +230,24 @@ internal fun DailyEditScreen( } } -@Preview +@Preview(showBackground = true) @Composable private fun DailyEditScreenPreview() { HarmonyTheme { - var selectedDays by remember { mutableStateOf(setOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY)) } // ✅ 상태 관리 + var selectedDays by remember { mutableStateOf(setOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY)) } + var selectedTime by remember { mutableStateOf(null) } DailyEditScreen( onDismissRequest = {}, onCompleteRequest = {}, - uiState = DailyEditUiState( - dailyExpand = FakeDailyManage().get(), - isLoading = false, - selectedDays = selectedDays - ), selectedDays = selectedDays, onDaySelected = { day -> selectedDays = selectedDays.toMutableSet().apply { if (contains(day)) remove(day) else add(day) } }, - onChangeDaily = {} + selectedTime = selectedTime, + onTimePickRequest = { } ) } } \ No newline at end of file diff --git a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditUiState.kt b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditUiState.kt index 49d22572..b889a582 100644 --- a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditUiState.kt +++ b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditUiState.kt @@ -3,6 +3,7 @@ package com.teampatch.feature.daily.edit import com.teampatch.core.domain.model.DailyManage import java.time.DayOfWeek import java.time.LocalDateTime +import java.time.LocalTime internal data class DailyEditUiState( val dailyExpand: DailyManage = DailyManage( @@ -15,4 +16,5 @@ internal data class DailyEditUiState( ), val isLoading: Boolean = true, val selectedDays: Set = emptySet(), + val selectedTime: LocalTime? = null, ) \ No newline at end of file diff --git a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditViewModel.kt b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditViewModel.kt index 979643aa..15905f49 100644 --- a/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditViewModel.kt +++ b/feature/daily-edit/src/main/java/com/teampatch/feature/daily/edit/DailyEditViewModel.kt @@ -5,20 +5,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.teampatch.core.domain.usecase.daily.GetDailyManageUseCase +import com.teampatch.core.domain.usecase.daily.ScheduleCertifyNotificationUseCase import dagger.hilt.android.lifecycle.HiltViewModel import java.time.DayOfWeek +import java.time.LocalDateTime +import java.time.LocalTime import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -/** - * DailyEdit를 위한 뷰모델과 Navigation이 덜 완성되었다... usecase도 더 만들어야될거같은데 민준님 도와주세요.. - */ @HiltViewModel internal class DailyEditViewModel @Inject constructor( private val getDailyManageUseCase: GetDailyManageUseCase, + private val scheduleCertifyNotificationUseCase: ScheduleCertifyNotificationUseCase, ) : ViewModel() { private val _dailyEditUiState = mutableStateOf(DailyEditUiState()) @@ -33,7 +34,7 @@ internal class DailyEditViewModel @Inject constructor( private fun load() = viewModelScope.launch { runCatching { - getDailyManageUseCase("someId") // 올바른 dailyId 사용 + getDailyManageUseCase("someId") // 임의의 id }.onSuccess { dailyManage -> _dailyEditUiState.value = DailyEditUiState( dailyExpand = dailyManage, @@ -45,13 +46,6 @@ internal class DailyEditViewModel @Inject constructor( } } - fun changeDailyContent(content: String) { - _dailyEditUiState.value = _dailyEditUiState.value.copy( - dailyExpand = _dailyEditUiState.value.dailyExpand.copy(content = content) - ) - } - - /** ✅ 요일 선택을 업데이트하는 메서드 추가 **/ fun toggleSelectedDay(day: DayOfWeek) { _dailyEditUiState.value = dailyEditUiState.value.copy( selectedDays = dailyEditUiState.value.selectedDays.toMutableSet().apply { @@ -59,4 +53,14 @@ internal class DailyEditViewModel @Inject constructor( } ) } + + fun changeSelectedTime(time: LocalTime) { + _dailyEditUiState.value = _dailyEditUiState.value.copy( + selectedTime = time + ) + } + + fun onTimeSelected(time: LocalDateTime) { + scheduleCertifyNotificationUseCase(time) + } } \ No newline at end of file diff --git a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandNavigation.kt b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandNavigation.kt index 43dc210e..88038588 100644 --- a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandNavigation.kt +++ b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandNavigation.kt @@ -5,7 +5,6 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.compose.composable -import com.teampatch.core.domain.model.DailyManage import kotlinx.serialization.Serializable @Serializable @@ -20,8 +19,8 @@ fun NavController.navigateToDailyExpandScreen( fun NavGraphBuilder.addDailyExpandScreen( onBackRequest: () -> Unit, - dailyEditPageRequest: (DailyManage) -> Unit, - onDeleteClick: (DailyManage) -> Unit, + dailyEditPageRequest: () -> Unit, + onDeleteClick: () -> Unit, ) { composable { DailyExpandRoute( diff --git a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandScreen.kt b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandScreen.kt index c3f864bb..ca2430b7 100644 --- a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandScreen.kt +++ b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandScreen.kt @@ -12,8 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -21,6 +21,7 @@ import androidx.compose.material3.Scaffold 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 @@ -38,48 +39,48 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems import com.teampatch.core.designsystem.R.drawable.ic_more_question import com.teampatch.core.designsystem.component.BackButtonAppBar import com.teampatch.core.designsystem.theme.BL import com.teampatch.core.designsystem.theme.G1 -import com.teampatch.core.designsystem.theme.G4 import com.teampatch.core.designsystem.theme.G5 import com.teampatch.core.designsystem.theme.HarmonyTheme import com.teampatch.core.designsystem.theme.PretendardFontFamily import com.teampatch.core.designsystem.theme.SubRed -import com.teampatch.core.domain.fake.FakeDailyManage -import com.teampatch.core.domain.model.DailyManage +import com.teampatch.core.domain.model.Todo import com.teampatch.feature.daily.expand.R.string.dropdown_delete_daily import com.teampatch.feature.daily.expand.R.string.dropdown_edit_daily import com.teampatch.feature.daily.expand.model.DailyExpandEvent -import com.teampatch.feature.daily.expand.model.DailyExpandUiState +import java.time.LocalDateTime @Composable internal fun DailyExpandRoute( onBackRequest: () -> Unit, - onEditDailyRequest: (DailyManage) -> Unit, - onDeleteDailyRequest: (DailyManage) -> Unit, - viewModel: DailyExpandViewModel = hiltViewModel(), + onEditDailyRequest: () -> Unit, + onDeleteDailyRequest: () -> Unit, ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current - val uiState: DailyExpandUiState by viewModel.dailyExpandUiState + val viewModel: DailyExpandViewModel = hiltViewModel() - if (!uiState.isLoading) { - DailyExpandScreen( - onBackRequest = onBackRequest, - onEditDailyRequest = onEditDailyRequest, - onDeleteDailyRequest = onDeleteDailyRequest, - uiState = uiState - ) - } + val dailyRoutines = viewModel.dailyRoutine.collectAsLazyPagingItems() + + DailyExpandScreen( + dailyRoutines = dailyRoutines, + onBackRequest = onBackRequest, + onEditRequest = onEditDailyRequest, + onDeleteRequest = onDeleteDailyRequest + ) LaunchedEffect(Unit) { - lifecycleOwner.repeatOnLifecycle(state = Lifecycle.State.STARTED) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.event.collect { when (it) { - is DailyExpandEvent.LoadError -> + is DailyExpandEvent.LoadError -> { Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() + } } } } @@ -88,12 +89,11 @@ internal fun DailyExpandRoute( @Composable internal fun DailyExpandScreen( + dailyRoutines: LazyPagingItems, onBackRequest: () -> Unit, - onEditDailyRequest: (DailyManage) -> Unit, - onDeleteDailyRequest: (DailyManage) -> Unit, - uiState: DailyExpandUiState, + onEditRequest: () -> Unit, + onDeleteRequest: () -> Unit, ) { - val daily = uiState.dailyManage Scaffold( topBar = { BackButtonAppBar( @@ -104,21 +104,20 @@ internal fun DailyExpandScreen( ) } ) { scaffoldPaddingValues -> - Box( + LazyColumn( modifier = Modifier - .fillMaxWidth() .padding(scaffoldPaddingValues) - .padding(top = 16.dp), - contentAlignment = Alignment.Center + .padding(top = 16.dp) + .fillMaxSize() ) { - if (daily == null) { - CircularProgressIndicator() // 로딩 상태 처리 - } else { - DailyItem( - dailyItem = daily, - onEditDailyRequest = { onEditDailyRequest(daily) }, - onDeleteDailyRequest = { onDeleteDailyRequest(daily) } - ) + items(dailyRoutines.itemCount) { index -> + dailyRoutines[index]?.let { item -> + DailyItem( + title = item.title, + onEditClick = onEditRequest, + onDeleteClick = onDeleteRequest + ) + } } } } @@ -126,9 +125,9 @@ internal fun DailyExpandScreen( @Composable fun DailyItem( - dailyItem: DailyManage, - onEditDailyRequest: () -> Unit, - onDeleteDailyRequest: () -> Unit, + title: String, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, ) { var isDropDownMenuShow by remember { mutableStateOf(false) } @@ -148,14 +147,15 @@ fun DailyItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp, bottom = 16.dp) + .padding(horizontal = 24.dp, vertical = 8.dp) ) { Text( - text = "#${dailyItem.number}", + text = title, fontFamily = PretendardFontFamily, fontWeight = FontWeight.SemiBold, - fontSize = 18.sp, - color = G4 + fontSize = 20.sp, + color = BL, + modifier = Modifier.widthIn(max = 240.dp) ) Box( modifier = Modifier @@ -174,15 +174,11 @@ fun DailyItem( expanded = isDropDownMenuShow, onDismissRequest = { isDropDownMenuShow = false }, shape = RoundedCornerShape(10.dp), - modifier = Modifier - .widthIn(min = 200.dp) + modifier = Modifier.widthIn(min = 200.dp) ) { DropdownMenuItem( text = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( text = stringResource(dropdown_edit_daily), fontFamily = PretendardFontFamily, @@ -193,16 +189,13 @@ fun DailyItem( } }, onClick = { - onEditDailyRequest() + onEditClick() isDropDownMenuShow = false } ) DropdownMenuItem( text = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text( text = stringResource(dropdown_delete_daily), fontFamily = PretendardFontFamily, @@ -213,43 +206,50 @@ fun DailyItem( } }, onClick = { - onDeleteDailyRequest() + onDeleteClick() isDropDownMenuShow = false } ) } } } - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp, bottom = 16.dp) - ) { - Text( - text = dailyItem.title, - fontFamily = PretendardFontFamily, - fontWeight = FontWeight.SemiBold, - fontSize = 20.sp, - color = BL, - modifier = Modifier.widthIn(max = 240.dp) - ) - } } } } @Preview @Composable -private fun DailyExpandScreenPreview() { - HarmonyTheme { - DailyExpandScreen( - onBackRequest = {}, - onEditDailyRequest = {}, - onDeleteDailyRequest = {}, - uiState = DailyExpandUiState(dailyManage = FakeDailyManage().get()) +fun DailyExpandScreenPreview() { + val dummyTodos = listOf( + Todo( + id = "1", + dateTime = LocalDateTime.now(), + title = "샘플 투두 1", + isFinished = false + ), + Todo( + id = "2", + dateTime = LocalDateTime.now().plusHours(1), + title = "샘플 투두 2", + isFinished = true ) + ) + + val lazyItems = remember { + derivedStateOf { + dummyTodos.map { it } // CheckableData 없음 + } + } + + HarmonyTheme { + LazyColumn { + items(lazyItems.value.size) { index -> + DailyItem( + title = lazyItems.value[index].title, + onEditClick = {}, + onDeleteClick = {} + ) + } + } } } \ No newline at end of file diff --git a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandViewModel.kt b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandViewModel.kt index 9769dcde..793fa8f8 100644 --- a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandViewModel.kt +++ b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/DailyExpandViewModel.kt @@ -1,42 +1,52 @@ package com.teampatch.feature.daily.expand -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.teampatch.core.domain.usecase.daily.GetDailyManageUseCase +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.teampatch.core.common.flowErrorCatch +import com.teampatch.core.common.toPagingData +import com.teampatch.core.domain.model.Todo +import com.teampatch.core.domain.usecase.daily.GetDailyRoutineUseCase import com.teampatch.feature.daily.expand.model.DailyExpandEvent import com.teampatch.feature.daily.expand.model.DailyExpandUiState import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.update @HiltViewModel internal class DailyExpandViewModel @Inject constructor( - private val getDailyManageUseCase: GetDailyManageUseCase, + private val getDailyRoutineUseCase: GetDailyRoutineUseCase, ) : ViewModel() { - var dailyExpandUiState = mutableStateOf(DailyExpandUiState()) - private set + + private val _dailyExpandUiState = MutableStateFlow(DailyExpandUiState()) + val dailyExpandUiState: StateFlow = _dailyExpandUiState.asStateFlow() private val _event = Channel() val event = _event.receiveAsFlow() - init { - load() - } + val dailyRoutine: Flow> = + flowErrorCatch( + block = { + getDailyRoutineUseCase() + .cachedIn(viewModelScope) + } + ) { + it.printStackTrace() + emit(it.toPagingData()) + } - private fun load() = viewModelScope.launch { - runCatching { - getDailyManageUseCase("someId") // 단일 데이터 반환 - }.onSuccess { daily -> - dailyExpandUiState.value = DailyExpandUiState( - dailyManage = daily, + init { + _dailyExpandUiState.update { + it.copy( isLoading = false ) - }.onFailure { - _event.send(DailyExpandEvent.LoadError(it)) - it.printStackTrace() } } } \ No newline at end of file diff --git a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/model/DailyExpandUiState.kt b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/model/DailyExpandUiState.kt index b624a8d0..560b9650 100644 --- a/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/model/DailyExpandUiState.kt +++ b/feature/daily-expand/src/main/java/com/teampatch/feature/daily/expand/model/DailyExpandUiState.kt @@ -1,8 +1,11 @@ package com.teampatch.feature.daily.expand.model -import com.teampatch.core.domain.model.DailyManage +import androidx.paging.PagingData +import com.teampatch.core.domain.model.Todo +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf -internal data class DailyExpandUiState( - val dailyManage: DailyManage? = null, // 기본값을 null로 설정 +data class DailyExpandUiState( + val dailyRoutine: Flow> = flowOf(PagingData.empty()), val isLoading: Boolean = true, ) \ No newline at end of file diff --git a/feature/daily/build.gradle.kts b/feature/daily/build.gradle.kts index d3d9e953..0e2f284d 100644 --- a/feature/daily/build.gradle.kts +++ b/feature/daily/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { + implementation(project(":core:common")) implementation(project(":core:domain")) implementation(project(":core:designsystem")) diff --git a/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt b/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt index d2eafa69..fb79ba92 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt @@ -19,10 +19,12 @@ fun NavController.navigateToDailyScreen( fun NavGraphBuilder.addDailyScreen( dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, ) { composable { DailyRoute( - dailyExpandPageRequest = dailyExpandPageRequest + dailyExpandPageRequest = dailyExpandPageRequest, + dailyEditPageRequest = dailyEditPageRequest ) } } \ No newline at end of file diff --git a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt index a79ec469..240326ef 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -12,14 +12,21 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color @@ -47,34 +54,44 @@ import com.teampatch.core.designsystem.theme.G5 import com.teampatch.core.designsystem.theme.HarmonyTheme import com.teampatch.core.designsystem.theme.MainGreen import com.teampatch.core.designsystem.theme.PretendardFontFamily -import com.teampatch.core.designsystem.utils.noRippleClickable import com.teampatch.core.domain.model.Todo import com.teampatch.feature.daily.R import com.teampatch.harmony.model.DailySideEffect -import com.teampatch.harmony.model.DailyUiState import java.time.LocalDateTime import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, ) { val context = LocalContext.current - val dailyViewModel: DailyViewModel = hiltViewModel() - val uiState by dailyViewModel.dailyUiState + val viewModel: DailyViewModel = hiltViewModel() + val uiState by viewModel.dailyUiState.collectAsState() + val dailyRoutine = uiState.dailyRoutine.collectAsLazyPagingItems() - if (!uiState.isLoading) { - DailyScreen( - progress = 0f, - onDailyRoutineClick = {}, - onDailyRoutineCheckChanged = { _, _ -> }, - dailyRoutine = uiState.daily.collectAsLazyPagingItems(), - dailyExpandPageRequest = dailyExpandPageRequest, - uiState = uiState - ) + val progress = remember(dailyRoutine.itemSnapshotList.items) { + val items = dailyRoutine.itemSnapshotList.items + val total = items.size + val done = items.count { it.checked.value } + if (total == 0) 0f else done.toFloat() / total } + + DailyScreen( + progress = progress, + onDailyRoutineCheckChanged = { id, checked -> + val item = dailyRoutine.itemSnapshotList.items.find { it.data.id == id } + if (item != null) { + viewModel.changeDailyRoutine(item, checked) + } + }, + dailyRoutine = dailyRoutine, + dailyExpandPageRequest = dailyExpandPageRequest, + dailyEditPageRequest = dailyEditPageRequest + ) + LaunchedEffect(Unit) { - dailyViewModel.sideEffect.collect { sideEffect -> + viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { is DailySideEffect.LoadError -> { Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() @@ -86,13 +103,11 @@ internal fun DailyRoute( @Composable internal fun DailyScreen( - // TODO: Route - progress: Float, // 진행률 (0f부터 1f까지의 값) - onDailyRoutineClick: (String) -> Unit, // id - onDailyRoutineCheckChanged: (String, Boolean) -> Unit, // id, checked + progress: Float, + onDailyRoutineCheckChanged: (String, Boolean) -> Unit, dailyRoutine: LazyPagingItems>, dailyExpandPageRequest: () -> Unit, - uiState: DailyUiState, + dailyEditPageRequest: () -> Unit, ) { Scaffold( topBar = { @@ -124,7 +139,22 @@ internal fun DailyScreen( modifier = Modifier .padding(horizontal = 20.dp) ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { dailyEditPageRequest() }, + containerColor = MainGreen, + shape = CircleShape, + contentColor = Color.White, + modifier = Modifier.padding(bottom = 12.dp, end = 12.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "일과 추가" + ) + } } + ) { scaffoldPaddingValues -> LazyColumn( verticalArrangement = Arrangement.spacedBy(1.dp), @@ -148,7 +178,7 @@ internal fun DailyScreen( ) Spacer(modifier = Modifier.height(4.dp)) // 간격 추가 Text( - text = "33% 완료", + text = "${(progress * 100).toInt()}% 완료", fontFamily = PretendardFontFamily, fontWeight = FontWeight.Bold, fontSize = 24.sp, @@ -175,27 +205,24 @@ internal fun DailyScreen( } } items(dailyRoutine.itemCount) { index -> - val lastDateTime = - if (index > 0) dailyRoutine.peek(index - 1)?.data?.dateTime else null - val dateTime = dailyRoutine.peek(index)?.data?.dateTime - val title = dailyRoutine[index]?.data?.title + val data = dailyRoutine[index]?.data ?: return@items + val checkedState = dailyRoutine[index]?.checked?.value ?: false + val dateTime = dailyRoutine[index]?.data?.dateTime?.stringHour().orEmpty() + val title = dailyRoutine[index]?.data?.title.orEmpty() + DailyRoutineCard( onCheckedChange = { - val data = dailyRoutine[index]?.data ?: return@DailyRoutineCard dailyRoutine.itemSnapshotList.items[index].checked.value = it onDailyRoutineCheckChanged(data.id, it) }, - checked = dailyRoutine[index]?.checked?.value ?: false, - dateTime = dateTime?.stringHour() ?: "", - text = title ?: "", + checked = checkedState, + dateTime = dateTime, + text = title, modifier = Modifier .padding(horizontal = 24.dp) - .noRippleClickable { - val data = dailyRoutine[index]?.data ?: return@noRippleClickable - onDailyRoutineClick(data.id) - } ) } + if (dailyRoutine.itemCount != 0) { item { Box(modifier = Modifier.height(20.dp)) @@ -207,11 +234,10 @@ internal fun DailyScreen( @Preview @Composable -private fun DailyManageScreenPreview() { +private fun DailyScreenPreview() { HarmonyTheme { DailyScreen( progress = 0f, - onDailyRoutineClick = {}, onDailyRoutineCheckChanged = { _, _ -> }, dailyRoutine = flowOf( PagingData.from( @@ -221,7 +247,7 @@ private fun DailyManageScreenPreview() { ) .collectAsLazyPagingItems(), dailyExpandPageRequest = { }, - uiState = DailyUiState() + dailyEditPageRequest = {} ) } } diff --git a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt index ab6f06b1..e03c5af9 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt @@ -3,48 +3,83 @@ package com.teampatch.harmony import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import androidx.paging.map +import com.teampatch.core.common.flowErrorCatch +import com.teampatch.core.common.toPagingData import com.teampatch.core.designsystem.model.CheckableData +import com.teampatch.core.domain.model.Todo import com.teampatch.core.domain.usecase.daily.GetDailyRoutineUseCase -import com.teampatch.core.domain.usecase.user.GetUserInfoUseCase +import com.teampatch.core.domain.usecase.daily.ToggleDailyRoutineStatusUseCase +import com.teampatch.harmony.model.DailyErrorHandler import com.teampatch.harmony.model.DailySideEffect import com.teampatch.harmony.model.DailyUiState import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @HiltViewModel internal class DailyViewModel @Inject constructor( - private val getUserInfoUseCase: GetUserInfoUseCase, private val getDailyRoutineUseCase: GetDailyRoutineUseCase, + private val toggleDailyRoutineStatusUseCase: ToggleDailyRoutineStatusUseCase, ) : ViewModel() { - var dailyUiState = mutableStateOf(DailyUiState()) - private set + private val _dailyUiState = MutableStateFlow(DailyUiState()) + val dailyUiState: StateFlow = _dailyUiState.asStateFlow() private val _sideEffect = Channel() val sideEffect = _sideEffect.receiveAsFlow() + private val _errorHandler = MutableSharedFlow() + val errorHandler: SharedFlow = _errorHandler.asSharedFlow() + + val dailyRoutine: Flow>> = + flowErrorCatch( + block = { + getDailyRoutineUseCase() + .map { pagingData -> + pagingData.map { + CheckableData(it, mutableStateOf(it.isFinished)) + } + } + .cachedIn(viewModelScope) + } + ) { + it.printStackTrace() + emit(it.toPagingData()) + } + init { - load() + _dailyUiState.update { + it.copy( + dailyRoutine = dailyRoutine, + isLoading = false // 또는 refresh 상태를 기반으로 갱신 + ) + } } - private fun load() = viewModelScope.launch { + fun changeDailyRoutine(todo: CheckableData, checked: Boolean) = viewModelScope.launch { try { - val user = getUserInfoUseCase().first() - val todo = getDailyRoutineUseCase().map { pagingData -> - pagingData.map { - CheckableData(it, mutableStateOf(it.isFinished)) - } - } - dailyUiState.value = DailyUiState(user = user, daily = todo, isLoading = false) + // 1. UI 상태 변경 + todo.checked.value = checked + + // 2. 서버 상태 반영 + toggleDailyRoutineStatusUseCase(todo.data.id, checked) } catch (e: Exception) { - _sideEffect.send(DailySideEffect.LoadError(e)) e.printStackTrace() + _errorHandler.emit(DailyErrorHandler.ChangeRoutineError(e)) } } } \ No newline at end of file diff --git a/feature/daily/src/main/java/com/teampatch/harmony/model/DailyErrorHandler.kt b/feature/daily/src/main/java/com/teampatch/harmony/model/DailyErrorHandler.kt new file mode 100644 index 00000000..f14c35ba --- /dev/null +++ b/feature/daily/src/main/java/com/teampatch/harmony/model/DailyErrorHandler.kt @@ -0,0 +1,5 @@ +package com.teampatch.harmony.model + +sealed interface DailyErrorHandler { + data class ChangeRoutineError(val throwable: Throwable) : DailyErrorHandler +} \ No newline at end of file diff --git a/feature/daily/src/main/java/com/teampatch/harmony/model/DailyUiState.kt b/feature/daily/src/main/java/com/teampatch/harmony/model/DailyUiState.kt index 965d3693..747e9c59 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/model/DailyUiState.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/model/DailyUiState.kt @@ -3,12 +3,10 @@ package com.teampatch.harmony.model import androidx.paging.PagingData import com.teampatch.core.designsystem.model.CheckableData import com.teampatch.core.domain.model.Todo -import com.teampatch.core.domain.model.User import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -internal data class DailyUiState( - val user: User = User.createEmptyUser(), - val daily: Flow>> = flowOf(PagingData.empty()), +data class DailyUiState( + val dailyRoutine: Flow>> = flowOf(PagingData.empty()), val isLoading: Boolean = true, ) \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index def3ace6..43dd7e4e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,3 +51,4 @@ include(":feature:onboarding-enter") include(":core:database") include(":feature:memorystorage") include(":feature:memorystorage-detail") +include(":feature:daily-certify")