From dd29a54a2795a05a0e203433c2c4174da46b51dd Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 16:32:49 +0900 Subject: [PATCH 01/34] =?UTF-8?q?feat:=20=EC=98=A4=EB=A5=B8=EC=AA=BD?= =?UTF-8?q?=EC=83=81=EB=8B=A8=EC=95=84=EC=9D=B4=EC=BD=98=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=ED=95=98=EB=A9=B4=20daily-expand=EB=A1=9C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=B4=EB=8F=99=20#166?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/java/com/teampatch/harmony/MainNavHost.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index 10236d4d..cf1b308e 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -12,6 +12,7 @@ import com.teampatch.feature.answer.addAnswerScreen import com.teampatch.feature.answer.navigateToAnswerScreen 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 @@ -192,7 +193,7 @@ fun MainNavHost( ) addDailyScreen( - dailyExpandPageRequest = { navController.navigateToDailyScreen() } + dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() } ) addDailyExpandScreen( From 7adc196a58fd362cc0088ce31078cfa366464d5c Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 17:49:48 +0900 Subject: [PATCH 02/34] =?UTF-8?q?feat:=20fab=EB=A5=BC=20=EB=88=84=EB=A5=B4?= =?UTF-8?q?=EB=A9=B4=20daily-edit=EB=A1=9C=20=ED=99=94=EB=A9=B4=EC=9D=B4?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 9 ++++++- .../com/teampatch/harmony/DailyNavigation.kt | 4 +++- .../java/com/teampatch/harmony/DailyScreen.kt | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index cf1b308e..bc301c21 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.navOptions import com.teampatch.core.common.findActivity import com.teampatch.feature.answer.addAnswerScreen 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 @@ -193,7 +194,8 @@ fun MainNavHost( ) addDailyScreen( - dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() } + dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, + dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) addDailyExpandScreen( @@ -202,6 +204,11 @@ fun MainNavHost( onDeleteClick = {} // 임시 ) + addDailyEditScreen( + onDismissRequest = navController::navigateUp, + onCompleteRequest = { } + ) + addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, onRestartConversation = { navController.navigateToMemoryCardRegistrationScreen("memoryCardId") } 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..3e8ab942 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -12,7 +12,12 @@ 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 @@ -58,6 +63,7 @@ import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, ) { val context = LocalContext.current val dailyViewModel: DailyViewModel = hiltViewModel() @@ -70,6 +76,7 @@ internal fun DailyRoute( onDailyRoutineCheckChanged = { _, _ -> }, dailyRoutine = uiState.daily.collectAsLazyPagingItems(), dailyExpandPageRequest = dailyExpandPageRequest, + dailyEditPageRequest = dailyEditPageRequest, uiState = uiState ) } @@ -92,6 +99,7 @@ internal fun DailyScreen( onDailyRoutineCheckChanged: (String, Boolean) -> Unit, // id, checked dailyRoutine: LazyPagingItems>, dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, uiState: DailyUiState, ) { Scaffold( @@ -124,7 +132,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), @@ -221,6 +244,7 @@ private fun DailyManageScreenPreview() { ) .collectAsLazyPagingItems(), dailyExpandPageRequest = { }, + dailyEditPageRequest = {}, uiState = DailyUiState() ) } From 41a9e635420ae978e6690957192f31b22feef646 Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 17:49:48 +0900 Subject: [PATCH 03/34] =?UTF-8?q?feat:=20fab=EB=A5=BC=20=EB=88=84=EB=A5=B4?= =?UTF-8?q?=EB=A9=B4=20daily-edit=EB=A1=9C=20=ED=99=94=EB=A9=B4=EC=9D=B4?= =?UTF-8?q?=EB=8F=99=20#165?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 9 ++++++- .../com/teampatch/harmony/DailyNavigation.kt | 4 +++- .../java/com/teampatch/harmony/DailyScreen.kt | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index cf1b308e..bc301c21 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.navOptions import com.teampatch.core.common.findActivity import com.teampatch.feature.answer.addAnswerScreen 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 @@ -193,7 +194,8 @@ fun MainNavHost( ) addDailyScreen( - dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() } + dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, + dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) addDailyExpandScreen( @@ -202,6 +204,11 @@ fun MainNavHost( onDeleteClick = {} // 임시 ) + addDailyEditScreen( + onDismissRequest = navController::navigateUp, + onCompleteRequest = { } + ) + addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, onRestartConversation = { navController.navigateToMemoryCardRegistrationScreen("memoryCardId") } 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..3e8ab942 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -12,7 +12,12 @@ 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 @@ -58,6 +63,7 @@ import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, ) { val context = LocalContext.current val dailyViewModel: DailyViewModel = hiltViewModel() @@ -70,6 +76,7 @@ internal fun DailyRoute( onDailyRoutineCheckChanged = { _, _ -> }, dailyRoutine = uiState.daily.collectAsLazyPagingItems(), dailyExpandPageRequest = dailyExpandPageRequest, + dailyEditPageRequest = dailyEditPageRequest, uiState = uiState ) } @@ -92,6 +99,7 @@ internal fun DailyScreen( onDailyRoutineCheckChanged: (String, Boolean) -> Unit, // id, checked dailyRoutine: LazyPagingItems>, dailyExpandPageRequest: () -> Unit, + dailyEditPageRequest: () -> Unit, uiState: DailyUiState, ) { Scaffold( @@ -124,7 +132,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), @@ -221,6 +244,7 @@ private fun DailyManageScreenPreview() { ) .collectAsLazyPagingItems(), dailyExpandPageRequest = { }, + dailyEditPageRequest = {}, uiState = DailyUiState() ) } From ccbe560865372ca10927a4bf1e25e83600982e02 Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 22:46:56 +0900 Subject: [PATCH 04/34] =?UTF-8?q?feat:=20=EC=9D=BC=EA=B3=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EB=A5=BC=20=EC=9C=84=ED=95=9C=20usecase=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 확신은 없다.. --- .../core/domain/fake/FakeDailyRepository.kt | 12 ++++++++++++ .../core/domain/repository/DailyRepository.kt | 7 +++++++ .../domain/usecase/daily/AddDailyRoutineUseCase.kt | 11 +++++++++++ 3 files changed, 30 insertions(+) create mode 100644 core/domain/src/main/java/com/teampatch/core/domain/fake/FakeDailyRepository.kt create mode 100644 core/domain/src/main/java/com/teampatch/core/domain/repository/DailyRepository.kt create mode 100644 core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/AddDailyRoutineUseCase.kt 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/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/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 From 6ae22b109adc9142d106108cf5c2b7a0149799f4 Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 23:26:12 +0900 Subject: [PATCH 05/34] =?UTF-8?q?feat:=20daily-edit=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=ED=95=98=EB=A9=B4=20daily=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 흐름은 그러하게 화면 이동이 가능한데.. 제대로 동작은 안함 --- .../java/com/teampatch/harmony/MainNavHost.kt | 8 +++- .../feature/daily/edit/DailyEditNavigation.kt | 3 +- .../feature/daily/edit/DailyEditScreen.kt | 42 +++++++++++++++---- .../feature/daily/edit/DailyEditUiState.kt | 2 + .../feature/daily/edit/DailyEditViewModel.kt | 8 ++++ .../com/teampatch/harmony/DailyNavigation.kt | 2 + .../java/com/teampatch/harmony/DailyScreen.kt | 20 +++++++-- .../com/teampatch/harmony/DailyViewModel.kt | 18 +++++++- 8 files changed, 89 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index bc301c21..986c7262 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -194,6 +194,7 @@ fun MainNavHost( ) addDailyScreen( + navController = navController, dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) @@ -206,7 +207,12 @@ fun MainNavHost( addDailyEditScreen( onDismissRequest = navController::navigateUp, - onCompleteRequest = { } + onCompleteRequest = { todo -> + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("todo_added", true) // 결과 저장 + navController.navigateUp() + } ) addMemoryStorageDetailScreen( 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..241a44ba 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,6 +5,7 @@ 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 @@ -19,7 +20,7 @@ fun NavController.navigateToDailyEditScreen( fun NavGraphBuilder.addDailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, ) { composable { DailyEditRoute( 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..57711f2c 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 @@ -50,35 +50,50 @@ 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 + + // 예시: 완료 버튼 클릭 시 + val state = viewModel.dailyEditUiState.value + if (!uiState.isLoading) { DailyEditScreen( onDismissRequest = onDismissRequest, onCompleteRequest = { - onCompleteRequest(it) + val todo = Todo( + id = UUID.randomUUID().toString(), + title = state.dailyExpand.content, + dateTime = state.selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), + isFinished = false + ) + onCompleteRequest(todo) }, uiState = uiState, selectedDays = uiState.selectedDays, onDaySelected = { viewModel.toggleSelectedDay(it) }, - onChangeDaily = { viewModel.changeDailyContent(it) } + onChangeDaily = { viewModel.changeDailyContent(it) }, + onTimeSelected = { viewModel.changeSelectedTime(it) } // 여기서 처리! ) } @@ -100,11 +115,12 @@ internal fun DailyEditRoute( @Composable internal fun DailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, uiState: DailyEditUiState, selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, onChangeDaily: (String) -> Unit, + onTimeSelected: (LocalTime) -> Unit, // 추가 ) { val context = LocalContext.current val daily = uiState.dailyExpand.content @@ -117,7 +133,9 @@ internal fun DailyEditScreen( TimePickerDialog( context, { _, selectedHour, selectedMinute -> - time = LocalTime.of(selectedHour, selectedMinute) + val selectedTime = LocalTime.of(selectedHour, selectedMinute) + time = selectedTime + onTimeSelected(selectedTime) // 콜백으로 전달 }, hour, minute, @@ -143,7 +161,16 @@ internal fun DailyEditScreen( }, bottomBar = { DefaultButton( - onClick = { onCompleteRequest(daily) }, + onClick = { + val selectedTime = uiState.selectedTime + val todo = Todo( + id = UUID.randomUUID().toString(), + title = uiState.dailyExpand.content, + dateTime = selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), + isFinished = false + ) + onCompleteRequest(todo) + }, enabled = daily.isNotBlank(), modifier = Modifier .fillMaxWidth() @@ -274,7 +301,8 @@ private fun DailyEditScreenPreview() { if (contains(day)) remove(day) else add(day) } }, - onChangeDaily = {} + onChangeDaily = {}, + onTimeSelected = {} // Preview용 빈 함수 ) } } \ 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..de2b157e 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.teampatch.core.domain.usecase.daily.GetDailyManageUseCase import dagger.hilt.android.lifecycle.HiltViewModel import java.time.DayOfWeek +import java.time.LocalTime import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -59,4 +60,11 @@ internal class DailyEditViewModel @Inject constructor( } ) } + + // DailyEditViewModel에 추가 + fun changeSelectedTime(time: LocalTime) { + _dailyEditUiState.value = _dailyEditUiState.value.copy( + selectedTime = time + ) + } } \ No newline at end of file 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 fb79ba92..2f87f948 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt @@ -18,11 +18,13 @@ fun NavController.navigateToDailyScreen( } fun NavGraphBuilder.addDailyScreen( + navController: NavController, dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { composable { DailyRoute( + navController = navController, dailyExpandPageRequest = dailyExpandPageRequest, dailyEditPageRequest = dailyEditPageRequest ) 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 3e8ab942..3c01a365 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -62,6 +64,7 @@ import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( + navController: NavController, // ← NavController를 전달받도록 수정 dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { @@ -69,6 +72,15 @@ internal fun DailyRoute( val dailyViewModel: DailyViewModel = hiltViewModel() val uiState by dailyViewModel.dailyUiState + val currentBackStackEntry = navController.currentBackStackEntryAsState().value + + LaunchedEffect(currentBackStackEntry?.savedStateHandle?.get("todo_added")) { + if (currentBackStackEntry?.savedStateHandle?.get("todo_added") == true) { + dailyViewModel.load() + currentBackStackEntry.savedStateHandle.set("todo_added", false) // 초기화 + } + } + if (!uiState.isLoading) { DailyScreen( progress = 0f, @@ -80,14 +92,14 @@ internal fun DailyRoute( uiState = uiState ) } + LaunchedEffect(Unit) { dailyViewModel.sideEffect.collect { sideEffect -> - when (sideEffect) { - is DailySideEffect.LoadError -> { - Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() - } + if (sideEffect is DailySideEffect.LoadError) { + Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() } } + dailyViewModel.load() } } 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..7f6ae757 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt @@ -5,11 +5,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.map import com.teampatch.core.designsystem.model.CheckableData +import com.teampatch.core.domain.model.Todo +import com.teampatch.core.domain.usecase.daily.AddDailyRoutineUseCase import com.teampatch.core.domain.usecase.daily.GetDailyRoutineUseCase import com.teampatch.core.domain.usecase.user.GetUserInfoUseCase import com.teampatch.harmony.model.DailySideEffect import com.teampatch.harmony.model.DailyUiState import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDateTime import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first @@ -21,6 +24,7 @@ import kotlinx.coroutines.launch internal class DailyViewModel @Inject constructor( private val getUserInfoUseCase: GetUserInfoUseCase, private val getDailyRoutineUseCase: GetDailyRoutineUseCase, + private val addDailyRoutineUseCase: AddDailyRoutineUseCase, ) : ViewModel() { var dailyUiState = mutableStateOf(DailyUiState()) @@ -33,7 +37,7 @@ internal class DailyViewModel @Inject constructor( load() } - private fun load() = viewModelScope.launch { + fun load() = viewModelScope.launch { try { val user = getUserInfoUseCase().first() val todo = getDailyRoutineUseCase().map { pagingData -> @@ -47,4 +51,16 @@ internal class DailyViewModel @Inject constructor( e.printStackTrace() } } + + fun addDailyRoutine(id: String, title: String, time: LocalDateTime, isFinished: Boolean) { + viewModelScope.launch { + val input = Todo(title = title, dateTime = time, id = id, isFinished = isFinished) + val result = addDailyRoutineUseCase(input) + if (result.isFailure) { + _sideEffect.send(DailySideEffect.LoadError(Exception("일과 추가 실패"))) + } else { + load() // 추가 후 다시 목록 갱신 + } + } + } } \ No newline at end of file From 34f881bb1168a85bfddc5cd5e33ebb44999efa28 Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 13 May 2025 23:26:12 +0900 Subject: [PATCH 06/34] =?UTF-8?q?feat:=20daily-edit=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=ED=95=98=EB=A9=B4=20daily=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EA=B8=B0=20#164?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 현재 흐름은 그러하게 화면 이동이 가능한데.. 제대로 동작은 안함 --- .../java/com/teampatch/harmony/MainNavHost.kt | 8 +++- .../feature/daily/edit/DailyEditNavigation.kt | 3 +- .../feature/daily/edit/DailyEditScreen.kt | 42 +++++++++++++++---- .../feature/daily/edit/DailyEditUiState.kt | 2 + .../feature/daily/edit/DailyEditViewModel.kt | 8 ++++ .../com/teampatch/harmony/DailyNavigation.kt | 2 + .../java/com/teampatch/harmony/DailyScreen.kt | 20 +++++++-- .../com/teampatch/harmony/DailyViewModel.kt | 18 +++++++- 8 files changed, 89 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index bc301c21..986c7262 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -194,6 +194,7 @@ fun MainNavHost( ) addDailyScreen( + navController = navController, dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) @@ -206,7 +207,12 @@ fun MainNavHost( addDailyEditScreen( onDismissRequest = navController::navigateUp, - onCompleteRequest = { } + onCompleteRequest = { todo -> + navController.previousBackStackEntry + ?.savedStateHandle + ?.set("todo_added", true) // 결과 저장 + navController.navigateUp() + } ) addMemoryStorageDetailScreen( 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..241a44ba 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,6 +5,7 @@ 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 @@ -19,7 +20,7 @@ fun NavController.navigateToDailyEditScreen( fun NavGraphBuilder.addDailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, ) { composable { DailyEditRoute( 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..57711f2c 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 @@ -50,35 +50,50 @@ 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 + + // 예시: 완료 버튼 클릭 시 + val state = viewModel.dailyEditUiState.value + if (!uiState.isLoading) { DailyEditScreen( onDismissRequest = onDismissRequest, onCompleteRequest = { - onCompleteRequest(it) + val todo = Todo( + id = UUID.randomUUID().toString(), + title = state.dailyExpand.content, + dateTime = state.selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), + isFinished = false + ) + onCompleteRequest(todo) }, uiState = uiState, selectedDays = uiState.selectedDays, onDaySelected = { viewModel.toggleSelectedDay(it) }, - onChangeDaily = { viewModel.changeDailyContent(it) } + onChangeDaily = { viewModel.changeDailyContent(it) }, + onTimeSelected = { viewModel.changeSelectedTime(it) } // 여기서 처리! ) } @@ -100,11 +115,12 @@ internal fun DailyEditRoute( @Composable internal fun DailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (String) -> Unit, + onCompleteRequest: (Todo) -> Unit, uiState: DailyEditUiState, selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, onChangeDaily: (String) -> Unit, + onTimeSelected: (LocalTime) -> Unit, // 추가 ) { val context = LocalContext.current val daily = uiState.dailyExpand.content @@ -117,7 +133,9 @@ internal fun DailyEditScreen( TimePickerDialog( context, { _, selectedHour, selectedMinute -> - time = LocalTime.of(selectedHour, selectedMinute) + val selectedTime = LocalTime.of(selectedHour, selectedMinute) + time = selectedTime + onTimeSelected(selectedTime) // 콜백으로 전달 }, hour, minute, @@ -143,7 +161,16 @@ internal fun DailyEditScreen( }, bottomBar = { DefaultButton( - onClick = { onCompleteRequest(daily) }, + onClick = { + val selectedTime = uiState.selectedTime + val todo = Todo( + id = UUID.randomUUID().toString(), + title = uiState.dailyExpand.content, + dateTime = selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), + isFinished = false + ) + onCompleteRequest(todo) + }, enabled = daily.isNotBlank(), modifier = Modifier .fillMaxWidth() @@ -274,7 +301,8 @@ private fun DailyEditScreenPreview() { if (contains(day)) remove(day) else add(day) } }, - onChangeDaily = {} + onChangeDaily = {}, + onTimeSelected = {} // Preview용 빈 함수 ) } } \ 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..de2b157e 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 @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import com.teampatch.core.domain.usecase.daily.GetDailyManageUseCase import dagger.hilt.android.lifecycle.HiltViewModel import java.time.DayOfWeek +import java.time.LocalTime import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -59,4 +60,11 @@ internal class DailyEditViewModel @Inject constructor( } ) } + + // DailyEditViewModel에 추가 + fun changeSelectedTime(time: LocalTime) { + _dailyEditUiState.value = _dailyEditUiState.value.copy( + selectedTime = time + ) + } } \ No newline at end of file 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 fb79ba92..2f87f948 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt @@ -18,11 +18,13 @@ fun NavController.navigateToDailyScreen( } fun NavGraphBuilder.addDailyScreen( + navController: NavController, dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { composable { DailyRoute( + navController = navController, dailyExpandPageRequest = dailyExpandPageRequest, dailyEditPageRequest = dailyEditPageRequest ) 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 3e8ab942..3c01a365 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -62,6 +64,7 @@ import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( + navController: NavController, // ← NavController를 전달받도록 수정 dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { @@ -69,6 +72,15 @@ internal fun DailyRoute( val dailyViewModel: DailyViewModel = hiltViewModel() val uiState by dailyViewModel.dailyUiState + val currentBackStackEntry = navController.currentBackStackEntryAsState().value + + LaunchedEffect(currentBackStackEntry?.savedStateHandle?.get("todo_added")) { + if (currentBackStackEntry?.savedStateHandle?.get("todo_added") == true) { + dailyViewModel.load() + currentBackStackEntry.savedStateHandle.set("todo_added", false) // 초기화 + } + } + if (!uiState.isLoading) { DailyScreen( progress = 0f, @@ -80,14 +92,14 @@ internal fun DailyRoute( uiState = uiState ) } + LaunchedEffect(Unit) { dailyViewModel.sideEffect.collect { sideEffect -> - when (sideEffect) { - is DailySideEffect.LoadError -> { - Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() - } + if (sideEffect is DailySideEffect.LoadError) { + Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() } } + dailyViewModel.load() } } 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..7f6ae757 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt @@ -5,11 +5,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.map import com.teampatch.core.designsystem.model.CheckableData +import com.teampatch.core.domain.model.Todo +import com.teampatch.core.domain.usecase.daily.AddDailyRoutineUseCase import com.teampatch.core.domain.usecase.daily.GetDailyRoutineUseCase import com.teampatch.core.domain.usecase.user.GetUserInfoUseCase import com.teampatch.harmony.model.DailySideEffect import com.teampatch.harmony.model.DailyUiState import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDateTime import javax.inject.Inject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.first @@ -21,6 +24,7 @@ import kotlinx.coroutines.launch internal class DailyViewModel @Inject constructor( private val getUserInfoUseCase: GetUserInfoUseCase, private val getDailyRoutineUseCase: GetDailyRoutineUseCase, + private val addDailyRoutineUseCase: AddDailyRoutineUseCase, ) : ViewModel() { var dailyUiState = mutableStateOf(DailyUiState()) @@ -33,7 +37,7 @@ internal class DailyViewModel @Inject constructor( load() } - private fun load() = viewModelScope.launch { + fun load() = viewModelScope.launch { try { val user = getUserInfoUseCase().first() val todo = getDailyRoutineUseCase().map { pagingData -> @@ -47,4 +51,16 @@ internal class DailyViewModel @Inject constructor( e.printStackTrace() } } + + fun addDailyRoutine(id: String, title: String, time: LocalDateTime, isFinished: Boolean) { + viewModelScope.launch { + val input = Todo(title = title, dateTime = time, id = id, isFinished = isFinished) + val result = addDailyRoutineUseCase(input) + if (result.isFailure) { + _sideEffect.send(DailySideEffect.LoadError(Exception("일과 추가 실패"))) + } else { + load() // 추가 후 다시 목록 갱신 + } + } + } } \ No newline at end of file From 66854e6b3c5b27bbcfe8e030b23224a0916a8202 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 14 May 2025 02:26:46 +0900 Subject: [PATCH 07/34] =?UTF-8?q?=EC=99=9C=EC=95=88=EB=90=A0=EA=B9=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 19 +- .../feature/daily/edit/DailyEditNavigation.kt | 8 +- .../feature/daily/edit/DailyEditScreen.kt | 188 +++++++----------- .../com/teampatch/harmony/DailyNavigation.kt | 2 - .../java/com/teampatch/harmony/DailyScreen.kt | 20 +- .../com/teampatch/harmony/DailyViewModel.kt | 2 + 6 files changed, 91 insertions(+), 148 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index 986c7262..139b2edb 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -1,5 +1,6 @@ package com.teampatch.harmony +import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -160,14 +161,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() } ) @@ -194,7 +195,6 @@ fun MainNavHost( ) addDailyScreen( - navController = navController, dailyExpandPageRequest = { navController.navigateToDailyExpandScreen() }, dailyEditPageRequest = { navController.navigateToDailyEditScreen() } ) @@ -208,6 +208,7 @@ fun MainNavHost( addDailyEditScreen( onDismissRequest = navController::navigateUp, onCompleteRequest = { todo -> + Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 navController.previousBackStackEntry ?.savedStateHandle ?.set("todo_added", true) // 결과 저장 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 241a44ba..644499e1 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 @@ -9,20 +9,20 @@ 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: (Todo) -> 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 57711f2c..aafa4baa 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 @@ -1,6 +1,7 @@ package com.teampatch.feature.daily.edit import android.app.TimePickerDialog +import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -10,6 +11,8 @@ 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 @@ -69,79 +72,47 @@ import java.util.UUID internal fun DailyEditRoute( onDismissRequest: () -> Unit, onCompleteRequest: (Todo) -> Unit, - viewModel: DailyEditViewModel = hiltViewModel(), + viewModel: DailyEditViewModel = hiltViewModel() ) { val context = LocalContext.current val uiState by viewModel.dailyEditUiState - // 예시: 완료 버튼 클릭 시 - val state = viewModel.dailyEditUiState.value - - if (!uiState.isLoading) { - DailyEditScreen( - onDismissRequest = onDismissRequest, - onCompleteRequest = { - val todo = Todo( - id = UUID.randomUUID().toString(), - title = state.dailyExpand.content, - dateTime = state.selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), - isFinished = false - ) - onCompleteRequest(todo) - }, - uiState = uiState, - selectedDays = uiState.selectedDays, - onDaySelected = { viewModel.toggleSelectedDay(it) }, - onChangeDaily = { viewModel.changeDailyContent(it) }, - onTimeSelected = { viewModel.changeSelectedTime(it) } // 여기서 처리! - ) - } - 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 = onCompleteRequest, + selectedDays = uiState.selectedDays, + onDaySelected = { viewModel.toggleSelectedDay(it) }, + selectedTime = uiState.selectedTime, + onTimeSelected = { viewModel.changeSelectedTime(it) } + ) + } } @Composable internal fun DailyEditScreen( onDismissRequest: () -> Unit, onCompleteRequest: (Todo) -> Unit, - uiState: DailyEditUiState, selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, - onChangeDaily: (String) -> Unit, - onTimeSelected: (LocalTime) -> Unit, // 추가 + selectedTime: LocalTime?, + onTimeSelected: (LocalTime) -> 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 -> - val selectedTime = LocalTime.of(selectedHour, selectedMinute) - time = selectedTime - onTimeSelected(selectedTime) // 콜백으로 전달 - }, - hour, - minute, - true - ) - } + val textState = rememberSaveable { mutableStateOf("") } val daysOfWeek = remember { DayOfWeek.values() } Scaffold( @@ -162,16 +133,17 @@ internal fun DailyEditScreen( bottomBar = { DefaultButton( onClick = { - val selectedTime = uiState.selectedTime val todo = Todo( id = UUID.randomUUID().toString(), - title = uiState.dailyExpand.content, - dateTime = selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } ?: LocalDateTime.now(), + title = textState.value, + dateTime = selectedTime?.let { LocalDateTime.of(LocalDate.now(), it) } + ?: LocalDateTime.now(), isFinished = false ) + Log.d("DEBUG", "1. DailyEditScreen: onCompleteRequest todo = $todo") onCompleteRequest(todo) }, - enabled = daily.isNotBlank(), + enabled = textState.value.isNotBlank(), modifier = Modifier .fillMaxWidth() .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) @@ -183,48 +155,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 @@ -236,30 +189,38 @@ 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) + + 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 -> + onTimeSelected(LocalTime.of(selectedHour, selectedMinute)) + }, + hour, + minute, + true + ) + } 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() } ) { @@ -268,12 +229,9 @@ internal fun DailyEditScreen( 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) ) } @@ -281,28 +239,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 = {}, - onTimeSelected = {} // Preview용 빈 함수 + selectedTime = selectedTime, + onTimeSelected = { time -> selectedTime = time } ) } } \ No newline at end of file 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 2f87f948..fb79ba92 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyNavigation.kt @@ -18,13 +18,11 @@ fun NavController.navigateToDailyScreen( } fun NavGraphBuilder.addDailyScreen( - navController: NavController, dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { composable { DailyRoute( - navController = navController, dailyExpandPageRequest = dailyExpandPageRequest, dailyEditPageRequest = dailyEditPageRequest ) 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 3c01a365..3e8ab942 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -39,8 +39,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.navigation.NavController -import androidx.navigation.compose.currentBackStackEntryAsState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -64,7 +62,6 @@ import kotlinx.coroutines.flow.flowOf @Composable internal fun DailyRoute( - navController: NavController, // ← NavController를 전달받도록 수정 dailyExpandPageRequest: () -> Unit, dailyEditPageRequest: () -> Unit, ) { @@ -72,15 +69,6 @@ internal fun DailyRoute( val dailyViewModel: DailyViewModel = hiltViewModel() val uiState by dailyViewModel.dailyUiState - val currentBackStackEntry = navController.currentBackStackEntryAsState().value - - LaunchedEffect(currentBackStackEntry?.savedStateHandle?.get("todo_added")) { - if (currentBackStackEntry?.savedStateHandle?.get("todo_added") == true) { - dailyViewModel.load() - currentBackStackEntry.savedStateHandle.set("todo_added", false) // 초기화 - } - } - if (!uiState.isLoading) { DailyScreen( progress = 0f, @@ -92,14 +80,14 @@ internal fun DailyRoute( uiState = uiState ) } - LaunchedEffect(Unit) { dailyViewModel.sideEffect.collect { sideEffect -> - if (sideEffect is DailySideEffect.LoadError) { - Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() + when (sideEffect) { + is DailySideEffect.LoadError -> { + Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() + } } } - dailyViewModel.load() } } 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 7f6ae757..4e6e0567 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt @@ -1,5 +1,6 @@ package com.teampatch.harmony +import android.util.Log import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -55,6 +56,7 @@ internal class DailyViewModel @Inject constructor( fun addDailyRoutine(id: String, title: String, time: LocalDateTime, isFinished: Boolean) { viewModelScope.launch { val input = Todo(title = title, dateTime = time, id = id, isFinished = isFinished) + Log.d("DEBUG", "DailyViewModel: addDailyRoutine input = $input") // ✅ 디버그 추가 val result = addDailyRoutineUseCase(input) if (result.isFailure) { _sideEffect.send(DailySideEffect.LoadError(Exception("일과 추가 실패"))) From b61aa1dd78141c8d4aff52f86dda5f63253ec8d6 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 14 May 2025 17:50:34 +0900 Subject: [PATCH 08/34] =?UTF-8?q?feat:=20daily-certify=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/daily-certify/.gitignore | 1 + feature/{answer => daily-certify}/build.gradle.kts | 2 +- feature/daily-certify/src/main/AndroidManifest.xml | 4 ++++ settings.gradle.kts | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 feature/daily-certify/.gitignore rename feature/{answer => daily-certify}/build.gradle.kts (88%) create mode 100644 feature/daily-certify/src/main/AndroidManifest.xml 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/answer/build.gradle.kts b/feature/daily-certify/build.gradle.kts similarity index 88% rename from feature/answer/build.gradle.kts rename to feature/daily-certify/build.gradle.kts index dec28e63..3678ae03 100644 --- a/feature/answer/build.gradle.kts +++ b/feature/daily-certify/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "com.teampatch.feature.answer" + namespace = "com.teampatch.feature.daily.certify" } dependencies { diff --git a/feature/daily-certify/src/main/AndroidManifest.xml b/feature/daily-certify/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/feature/daily-certify/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ 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") From d9c71d42f75ffa7e2d268d54e8b5380000505194 Mon Sep 17 00:00:00 2001 From: theBettor Date: Thu, 15 May 2025 01:19:31 +0900 Subject: [PATCH 09/34] =?UTF-8?q?feat:=20=EC=9D=BC=EA=B3=BC=EC=95=8C?= =?UTF-8?q?=EB=9E=8C=ED=99=94=EB=A9=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../java/com/teampatch/harmony/MainNavHost.kt | 12 +- .../daily/certify/DailyAlarmScreen.kt | 151 ++++++++++++++++++ .../daily/certify/DailyCertifyImageEvent.kt | 6 + .../daily/certify/DailyCertifyInfoEvent.kt | 7 + .../daily/certify/DailyCertifyNavigation.kt | 30 ++++ .../daily/certify/DailyCertifyUiState.kt | 9 ++ .../daily/certify/DailyCertifyViewModel.kt | 22 +++ .../feature/daily/edit/DailyEditNavigation.kt | 2 +- .../feature/daily/edit/DailyEditScreen.kt | 12 +- 10 files changed, 237 insertions(+), 15 deletions(-) create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyImageEvent.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyInfoEvent.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyNavigation.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyUiState.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyViewModel.kt 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 139b2edb..c6c53952 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -9,7 +9,7 @@ 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.feature.answer.navigateToAnswerScreen import com.teampatch.feature.daily.edit.addDailyEditScreen import com.teampatch.feature.daily.edit.navigateToDailyEditScreen @@ -43,7 +43,6 @@ import com.teampatch.feature.onboarding.make.navigateToMakeRelationScreen 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 @@ -164,7 +163,7 @@ fun MainNavHost( addDailyEditScreen( onDismissRequest = navController::navigateUp, onCompleteRequest = { todo -> - Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 + Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 navController.previousBackStackEntry ?.savedStateHandle ?.set("new_todo", todo) @@ -208,7 +207,7 @@ fun MainNavHost( addDailyEditScreen( onDismissRequest = navController::navigateUp, onCompleteRequest = { todo -> - Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 + Log.d("DEBUG", "MainNavHost: 전달받은 todo = $todo") // ✅ 여기 navController.previousBackStackEntry ?.savedStateHandle ?.set("todo_added", true) // 결과 저장 @@ -216,6 +215,11 @@ fun MainNavHost( } ) + addDailyCertifyAlarmScreen( + onPickImageScreenRequest = {}, + onDismissRequest = { navController.navigateToHomeScreen() } + ) + addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, onRestartConversation = { navController.navigateToMemoryCardRegistrationScreen("memoryCardId") } 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..7da950c8 --- /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( + 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), + isCompleted = false + ) + ) + } +} \ 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..eeb1a598 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyNavigation.kt @@ -0,0 +1,30 @@ +package com.teampatch.daily.certify + +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, +) { + composable { + DailyAlarmRoute( + onPickImageScreenRequest = onPickImageScreenRequest, + onDismissRequest = onDismissRequest + ) + } +} \ 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..9033ffe8 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyUiState.kt @@ -0,0 +1,9 @@ +package com.teampatch.daily.certify + +import java.time.LocalTime + +internal data class DailyCertifyUiState( + val missionText: String = "", + val missionTime: LocalTime? = null, + val isCompleted: 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..39b76018 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyViewModel.kt @@ -0,0 +1,22 @@ +package com.teampatch.daily.certify + +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { + private val _uiState = mutableStateOf( + DailyCertifyUiState( +// title = "오늘의 인증 미션", +// time = LocalTime.now() // 예시 + ) + ) + val uiState: State = _uiState + + fun onDismiss() { + // 예: 상태 초기화 또는 Analytics 전송 등 + } +} \ 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 644499e1..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 @@ -20,7 +20,7 @@ fun NavController.navigateToDailyEditScreen( fun NavGraphBuilder.addDailyEditScreen( onDismissRequest: () -> Unit, - onCompleteRequest: (Todo) -> Unit + onCompleteRequest: (Todo) -> Unit, ) { composable { DailyEditRoute( 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 aafa4baa..1dc80512 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 @@ -7,9 +7,7 @@ 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 @@ -20,7 +18,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 @@ -32,11 +29,9 @@ 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 @@ -46,13 +41,10 @@ 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 @@ -72,7 +64,7 @@ import java.util.UUID internal fun DailyEditRoute( onDismissRequest: () -> Unit, onCompleteRequest: (Todo) -> Unit, - viewModel: DailyEditViewModel = hiltViewModel() + viewModel: DailyEditViewModel = hiltViewModel(), ) { val context = LocalContext.current val uiState by viewModel.dailyEditUiState @@ -109,7 +101,7 @@ internal fun DailyEditScreen( selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, selectedTime: LocalTime?, - onTimeSelected: (LocalTime) -> Unit + onTimeSelected: (LocalTime) -> Unit, ) { val context = LocalContext.current val textState = rememberSaveable { mutableStateOf("") } From 5a34f1de727d4bebb434be7aecec9ca9391877b3 Mon Sep 17 00:00:00 2001 From: theBettor Date: Thu, 15 May 2025 02:28:28 +0900 Subject: [PATCH 10/34] =?UTF-8?q?fix:=20=EB=88=84=EB=9D=BD=EB=90=9C=20answ?= =?UTF-8?q?er=EB=AA=A8=EB=93=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/answer/build.gradle.kts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 feature/answer/build.gradle.kts diff --git a/feature/answer/build.gradle.kts b/feature/answer/build.gradle.kts new file mode 100644 index 00000000..dec28e63 --- /dev/null +++ b/feature/answer/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("teampatch.android.library") + id("teampatch.android.library.compose") + id("teampatch.android.hilt") + id("teampatch.android.feature") +} + +android { + namespace = "com.teampatch.feature.answer" +} + +dependencies { + + implementation(project(":core:domain")) + implementation(project(":core:designsystem")) + + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) +} \ No newline at end of file From 82fa7e631c5d6e302d92dbadce589451c2d61665 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 21 May 2025 02:09:24 +0900 Subject: [PATCH 11/34] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=84=20enum=EC=9C=BC=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EA=B8=B0=EC=B2=98=EB=A6=AC=20#162?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/img_upload_cert.xml | 37 ++ feature/daily-certify/build.gradle.kts | 2 + .../teampatch/daily/certify/CertifyStatus.kt | 7 + .../daily/certify/DailyAlarmScreen.kt | 3 +- .../daily/certify/DailyCertifyScreen.kt | 315 ++++++++++++++++++ .../daily/certify/DailyCertifyUiState.kt | 6 +- .../src/main/res/values/strings.xml | 4 + 7 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/img_upload_cert.xml create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/CertifyStatus.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyScreen.kt create mode 100644 feature/daily-certify/src/main/res/values/strings.xml 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/feature/daily-certify/build.gradle.kts b/feature/daily-certify/build.gradle.kts index 3678ae03..91956260 100644 --- a/feature/daily-certify/build.gradle.kts +++ b/feature/daily-certify/build.gradle.kts @@ -14,6 +14,8 @@ 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) 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 index 7da950c8..aa46c3ca 100644 --- 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 @@ -143,8 +143,7 @@ private fun DailyAlarmScreenPreview() { onDismissRequest = { }, uiState = DailyCertifyUiState( missionText = "공원 산책 가서 비둘기 사진 찍기", - missionTime = LocalTime.of(14, 30), - isCompleted = false + missionTime = LocalTime.of(14, 30) ) ) } 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..16265de7 --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyCertifyScreen.kt @@ -0,0 +1,315 @@ +package com.teampatch.daily.certify + +import androidx.compose.foundation.Image +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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.img_upload_cert +import com.teampatch.core.designsystem.component.BackButtonAppBar +import com.teampatch.core.designsystem.component.DefaultButton +import com.teampatch.core.designsystem.theme.BL +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.domain.model.DailyComment +import com.teampatch.feature.daily.certify.R.string.text_title_appbar +import java.time.LocalTime +import java.time.format.DateTimeFormatter +import kotlinx.coroutines.launch + +@Composable +internal fun DailyCertifyRoute( + onBackRequest: () -> Unit, +) { +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +internal fun DailyCertifyScreen( + uiState: DailyCertifyUiState, + onBackRequest: () -> Unit, + onCommentSubmit: (String) -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onCommentEditComplete: (String) -> Unit, + onCertifyComplete: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState() + val scope = rememberCoroutineScope() + + if (uiState.editingComment != null) { + ModalBottomSheet( + onDismissRequest = { onCommentEditRequest(null) }, + sheetState = sheetState + ) { + Column(Modifier.padding(16.dp)) { + Text(text = "댓글 수정", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(8.dp)) + var text by remember { mutableStateOf(uiState.editingComment.content) } + OutlinedTextField( + value = text, + onValueChange = { text = it }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + DefaultButton(onClick = { + onCommentEditComplete(text) + scope.launch { sheetState.hide() } + }) { + Text("수정 완료") + } + } + } + } + + Scaffold( + topBar = { + BackButtonAppBar( + onBackRequest = onBackRequest, + title = { + Text(stringResource(text_title_appbar)) + } + ) + }, + bottomBar = { + when (uiState.certifyStatus) { + CertifyStatus.BEFORE -> { + DefaultButton( + onClick = onCertifyComplete, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp) + ) { + Text("인증 완료") + } + } + + CertifyStatus.PENDING -> { + DefaultButton( + onClick = {}, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + enabled = false + ) { + Text("처리 중...") + } + } + + CertifyStatus.CONFIRMED -> { + Column(Modifier.padding(16.dp)) { + OutlinedTextField( + value = "", + onValueChange = {}, + placeholder = { Text("댓글을 입력해주세요") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + DefaultButton(onClick = { onCommentSubmit("작성한 댓글") }) { + Text("댓글 남기기") + } + } + } + } + } + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + // 인증 이미지 + uiState.imageUrl?.let { + CertifyImage(imageUrl = it) + } + + Spacer(Modifier.height(12.dp)) + MissionInfoSection( + missionText = uiState.missionText, + missionTime = uiState.missionTime + ) + + when (uiState.certifyStatus) { + CertifyStatus.CONFIRMED -> { + // 댓글 목록 + LazyColumn { + items(uiState.comments) { comment -> + CommentItem( + comment = comment, + onEditClick = { onCommentEditRequest(comment) } + ) + } + } + } + + CertifyStatus.BEFORE -> { + // 아무것도 안 보여도 됨, 또는 안내 메시지 + Spacer(modifier = Modifier.height(32.dp)) + } + + CertifyStatus.PENDING -> { + Spacer(modifier = Modifier.height(32.dp)) + } + } + } + } +} + +@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 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 CommentItem( + comment: DailyComment, + onEditClick: () -> Unit, +) { + Column(Modifier.padding(16.dp)) { + Text(text = comment.writerName, fontWeight = FontWeight.Bold) + Text(text = comment.content, modifier = Modifier.padding(top = 4.dp)) + TextButton(onClick = onEditClick) { + 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 + ), + onBackRequest = {}, + onCommentSubmit = {}, + onCommentEditRequest = {}, + onCommentEditComplete = {}, + onCertifyComplete = {} + ) + } +} + +@Preview(name = "2. 인증 완료 (댓글 없음)", showBackground = true) +@Composable +private fun DailyCertifyScreenPreview_Completed_NoComment() { + HarmonyTheme { + DailyCertifyScreen( + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30), + imageUrl = "", + comments = emptyList(), + editingComment = null + ), + onBackRequest = {}, + onCommentSubmit = {}, + onCommentEditRequest = {}, + onCommentEditComplete = {}, + onCertifyComplete = {} + ) + } +} + +@Preview(name = "3. 인증 완료 (댓글 있음)", showBackground = true) +@Composable +private fun DailyCertifyScreenPreview_Completed_WithComment() { + HarmonyTheme { + DailyCertifyScreen( + uiState = DailyCertifyUiState( + missionText = "공원 산책 가서 비둘기 사진 찍기", + missionTime = LocalTime.of(14, 30), + imageUrl = "", + comments = listOf( + DailyComment("1", "순대 조던", "비둘기 너무 귀여워요", ""), + DailyComment("2", "김소라", "부산 날씨 완전 봄이야", "") + ), + editingComment = null + ), + onBackRequest = {}, + onCommentSubmit = {}, + onCommentEditRequest = {}, + onCommentEditComplete = {}, + onCertifyComplete = {} + ) + } +} \ 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 index 9033ffe8..64d2a22f 100644 --- 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 @@ -1,9 +1,13 @@ package com.teampatch.daily.certify +import com.teampatch.core.domain.model.DailyComment import java.time.LocalTime internal data class DailyCertifyUiState( val missionText: String = "", val missionTime: LocalTime? = null, - val isCompleted: Boolean = false, + val imageUrl: String? = null, // (이미지가 URL인 경우) + val certifyStatus: CertifyStatus = CertifyStatus.BEFORE, + val comments: List = emptyList(), + val editingComment: DailyComment? = null, // 수정 중인 댓글이 있으면 bottomSheet 띄움 ) \ 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..15029bd2 --- /dev/null +++ b/feature/daily-certify/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + 일과 인증 + \ No newline at end of file From 7b8643b57a22c0cc8dfaf3ae7a3aa1d17743e1a6 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 21 May 2025 17:34:12 +0900 Subject: [PATCH 12/34] =?UTF-8?q?feat:=20bottomSheet,=20dialog=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 --- .../daily/certify/DailyCertifyScreen.kt | 256 ++++++++++++++---- .../daily/certify/DailyCertifyUiState.kt | 2 + 2 files changed, 210 insertions(+), 48 deletions(-) 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 index 16265de7..e45f0d1d 100644 --- 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 @@ -1,7 +1,13 @@ package com.teampatch.daily.certify 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 @@ -9,7 +15,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.AlertDialog +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.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField @@ -34,13 +48,19 @@ 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_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.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_title_appbar import java.time.LocalTime @@ -62,10 +82,16 @@ internal fun DailyCertifyScreen( onCommentEditRequest: (DailyComment?) -> Unit, onCommentEditComplete: (String) -> Unit, onCertifyComplete: () -> Unit, + onDismissDialog: () -> Unit, + onOpenCommentSheet: () -> Unit, + onCloseCommentSheet: () -> Unit, ) { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() + val isImageUploaded = uiState.imageUrl?.isNotBlank() == true + val isCertifyConfirmed = uiState.certifyStatus == CertifyStatus.CONFIRMED + // ✅ 댓글 수정 BottomSheet if (uiState.editingComment != null) { ModalBottomSheet( onDismissRequest = { onCommentEditRequest(null) }, @@ -91,6 +117,48 @@ internal fun DailyCertifyScreen( } } + // ✅ 사진 인증 안내 Dialog + if (uiState.showCertifyDialog) { + AlertDialog( + onDismissRequest = onDismissDialog, + confirmButton = { + TextButton(onClick = onDismissDialog) { + Text("확인") + } + }, + title = { Text("사진 인증 안내") }, + text = { Text("이 사진이 정말 맞나요?") } + ) + } + + // ✅ 댓글 작성 BottomSheet + if (uiState.showCommentSheet) { + ModalBottomSheet( + onDismissRequest = onCloseCommentSheet, + sheetState = sheetState + ) { + Column(Modifier.padding(16.dp)) { + Text("댓글 작성", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(8.dp)) + var text by remember { mutableStateOf("") } + OutlinedTextField( + value = text, + onValueChange = { text = it }, + placeholder = { Text("댓글을 입력해주세요") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(8.dp)) + DefaultButton(onClick = { + onCommentSubmit(text) + scope.launch { sheetState.hide() } + onCloseCommentSheet() + }) { + Text("댓글 남기기") + } + } + } + } + Scaffold( topBar = { BackButtonAppBar( @@ -101,10 +169,11 @@ internal fun DailyCertifyScreen( ) }, bottomBar = { - when (uiState.certifyStatus) { - CertifyStatus.BEFORE -> { + when { + !isImageUploaded -> { DefaultButton( - onClick = onCertifyComplete, + onClick = {}, + enabled = false, modifier = Modifier .fillMaxWidth() .padding(20.dp) @@ -113,28 +182,33 @@ internal fun DailyCertifyScreen( } } - CertifyStatus.PENDING -> { + uiState.certifyStatus == CertifyStatus.BEFORE -> { DefaultButton( - onClick = {}, + onClick = onCertifyComplete, modifier = Modifier .fillMaxWidth() - .padding(20.dp), - enabled = false + .padding(20.dp) ) { - Text("처리 중...") + Text("인증 완료") } } - CertifyStatus.CONFIRMED -> { + isCertifyConfirmed -> { Column(Modifier.padding(16.dp)) { - OutlinedTextField( - value = "", - onValueChange = {}, - placeholder = { Text("댓글을 입력해주세요") }, - modifier = Modifier.fillMaxWidth() - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onDismissDialog() }, // 아이콘 클릭 시 Dialog 띄우기 + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "댓글을 남겨보세요!") + Icon(Icons.Default.MoreVert, contentDescription = "댓글 안내") + } + Spacer(modifier = Modifier.height(8.dp)) - DefaultButton(onClick = { onCommentSubmit("작성한 댓글") }) { + + DefaultButton(onClick = onOpenCommentSheet) { Text("댓글 남기기") } } @@ -146,6 +220,7 @@ internal fun DailyCertifyScreen( Modifier .padding(paddingValues) .fillMaxSize() + .background(color = WH) ) { // 인증 이미지 uiState.imageUrl?.let { @@ -158,26 +233,32 @@ internal fun DailyCertifyScreen( missionTime = uiState.missionTime ) - when (uiState.certifyStatus) { - CertifyStatus.CONFIRMED -> { - // 댓글 목록 - LazyColumn { - items(uiState.comments) { comment -> - CommentItem( - comment = comment, - onEditClick = { onCommentEditRequest(comment) } - ) - } + if (isCertifyConfirmed) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(G1), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + 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) + ) } - } - - CertifyStatus.BEFORE -> { - // 아무것도 안 보여도 됨, 또는 안내 메시지 - Spacer(modifier = Modifier.height(32.dp)) - } - CertifyStatus.PENDING -> { - Spacer(modifier = Modifier.height(32.dp)) + items(uiState.comments, key = { it.commentId }) { comment -> + DailyCertifyCommentItem( + comment = comment, + onEditClick = { onCommentEditRequest(comment) }, + onDeleteClick = { /* TODO: 삭제 핸들러 추가 */ } + ) + } } } } @@ -235,16 +316,81 @@ fun CertifyImage(imageUrl: String?) { } @Composable -fun CommentItem( +fun DailyCertifyCommentItem( comment: DailyComment, onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, ) { - Column(Modifier.padding(16.dp)) { - Text(text = comment.writerName, fontWeight = FontWeight.Bold) - Text(text = comment.content, modifier = Modifier.padding(top = 4.dp)) - TextButton(onClick = onEditClick) { - Text("댓글 수정") + 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() + ) { + // TODO: 실제 사용자 프로필 이미지 연결 필요 + Image( + painter = painterResource(ic_my_appbar), + contentDescription = "user profile" + ) + + 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) + ) } } @@ -258,13 +404,17 @@ private fun DailyCertifyScreenPreview_Initial() { missionTime = LocalTime.of(14, 30), imageUrl = "", // 아직 사진 없음 comments = emptyList(), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.BEFORE ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } @@ -279,13 +429,18 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { missionTime = LocalTime.of(14, 30), imageUrl = "", comments = emptyList(), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.PENDING + ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } @@ -300,16 +455,21 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { missionTime = LocalTime.of(14, 30), imageUrl = "", comments = listOf( - DailyComment("1", "순대 조던", "비둘기 너무 귀여워요", ""), - DailyComment("2", "김소라", "부산 날씨 완전 봄이야", "") + DailyComment("1", "순대 조던", "유정상", "비둘기 너무 귀여워요"), + DailyComment("2", "김소라", "씹희", "부산 날씨 완전 봄이야") ), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.CONFIRMED // ✅ 이걸 넣어야 댓글 목록이 나타남! + ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } \ 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 index 64d2a22f..435777d4 100644 --- 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 @@ -10,4 +10,6 @@ internal data class DailyCertifyUiState( 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 From 341ae1cddc21180f2a47312913416413cb26cf62 Mon Sep 17 00:00:00 2001 From: theBettor Date: Thu, 22 May 2025 01:49:53 +0900 Subject: [PATCH 13/34] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20BottomSheet=EB=A5=BC=20=EC=A0=9C=EC=99=B8=ED=95=9C?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=A7=88=EB=AC=B4=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 250 +++++++----------- .../src/main/res/values/strings.xml | 1 + 2 files changed, 103 insertions(+), 148 deletions(-) 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 index e45f0d1d..e941278c 100644 --- 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 @@ -1,9 +1,11 @@ package com.teampatch.daily.certify +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 @@ -13,29 +15,25 @@ 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.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.AlertDialog 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.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,6 +51,7 @@ 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 @@ -62,10 +61,10 @@ 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 -import kotlinx.coroutines.launch @Composable internal fun DailyCertifyRoute( @@ -78,84 +77,34 @@ internal fun DailyCertifyRoute( internal fun DailyCertifyScreen( uiState: DailyCertifyUiState, onBackRequest: () -> Unit, - onCommentSubmit: (String) -> Unit, onCommentEditRequest: (DailyComment?) -> Unit, - onCommentEditComplete: (String) -> Unit, onCertifyComplete: () -> Unit, - onDismissDialog: () -> Unit, onOpenCommentSheet: () -> Unit, - onCloseCommentSheet: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() - val scope = rememberCoroutineScope() + val commentEditSheetState = rememberModalBottomSheetState() + val commentWriteSheetState = rememberModalBottomSheetState() val isImageUploaded = uiState.imageUrl?.isNotBlank() == true val isCertifyConfirmed = uiState.certifyStatus == CertifyStatus.CONFIRMED - - // ✅ 댓글 수정 BottomSheet - if (uiState.editingComment != null) { - ModalBottomSheet( - onDismissRequest = { onCommentEditRequest(null) }, - sheetState = sheetState - ) { - Column(Modifier.padding(16.dp)) { - Text(text = "댓글 수정", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - var text by remember { mutableStateOf(uiState.editingComment.content) } - OutlinedTextField( - value = text, - onValueChange = { text = it }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - DefaultButton(onClick = { - onCommentEditComplete(text) - scope.launch { sheetState.hide() } - }) { - Text("수정 완료") - } - } + val lazyListState = rememberLazyListState() + val isFloatingVisible by remember { + derivedStateOf { + !lazyListState.isScrollInProgress && !lazyListState.canScrollBackward } } - // ✅ 사진 인증 안내 Dialog - if (uiState.showCertifyDialog) { - AlertDialog( - onDismissRequest = onDismissDialog, - confirmButton = { - TextButton(onClick = onDismissDialog) { - Text("확인") - } - }, - title = { Text("사진 인증 안내") }, - text = { Text("이 사진이 정말 맞나요?") } - ) + LaunchedEffect(uiState.editingComment) { + if (uiState.editingComment != null) { + commentEditSheetState.show() + } else { + commentEditSheetState.hide() + } } - // ✅ 댓글 작성 BottomSheet - if (uiState.showCommentSheet) { - ModalBottomSheet( - onDismissRequest = onCloseCommentSheet, - sheetState = sheetState - ) { - Column(Modifier.padding(16.dp)) { - Text("댓글 작성", style = MaterialTheme.typography.titleMedium) - Spacer(Modifier.height(8.dp)) - var text by remember { mutableStateOf("") } - OutlinedTextField( - value = text, - onValueChange = { text = it }, - placeholder = { Text("댓글을 입력해주세요") }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(8.dp)) - DefaultButton(onClick = { - onCommentSubmit(text) - scope.launch { sheetState.hide() } - onCloseCommentSheet() - }) { - Text("댓글 남기기") - } - } + LaunchedEffect(uiState.showCommentSheet) { + if (uiState.showCommentSheet) { + commentWriteSheetState.show() + } else { + commentWriteSheetState.hide() } } @@ -182,7 +131,7 @@ internal fun DailyCertifyScreen( } } - uiState.certifyStatus == CertifyStatus.BEFORE -> { + uiState.certifyStatus == CertifyStatus.PENDING -> { DefaultButton( onClick = onCertifyComplete, modifier = Modifier @@ -194,69 +143,86 @@ internal fun DailyCertifyScreen( } isCertifyConfirmed -> { - Column(Modifier.padding(16.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onDismissDialog() }, // 아이콘 클릭 시 Dialog 띄우기 - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "댓글을 남겨보세요!") - Icon(Icons.Default.MoreVert, contentDescription = "댓글 안내") - } + // ✅ 인증 완료 시 bottomBar는 비우거나 "제어용 UI"로 대체 가능 + Spacer(modifier = Modifier.height(1.dp)) // 유지용 + } + } + } + ) { paddingValues -> + Box(Modifier.fillMaxSize()) { + Column( + Modifier + .padding(paddingValues) + .fillMaxSize() + .background(color = WH) + ) { + uiState.imageUrl?.let { + CertifyImage(imageUrl = it) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) - DefaultButton(onClick = onOpenCommentSheet) { - Text("댓글 남기기") + 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 = { /* TODO */ } + ) } } } } - } - ) { paddingValues -> - Column( - Modifier - .padding(paddingValues) - .fillMaxSize() - .background(color = WH) - ) { - // 인증 이미지 - uiState.imageUrl?.let { - CertifyImage(imageUrl = it) - } - - Spacer(Modifier.height(12.dp)) - MissionInfoSection( - missionText = uiState.missionText, - missionTime = uiState.missionTime - ) + // ✅ 댓글 남기기 UI (Floating UI) if (isCertifyConfirmed) { - LazyColumn( + val shouldShowFloatingCommentButton = isCertifyConfirmed && isFloatingVisible + // IDE에서 always true로 추론하는 건 Preview 상의 오해임 – 런타임에는 유동적 + + AnimatedVisibility( + visible = shouldShowFloatingCommentButton, + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), modifier = Modifier - .fillMaxSize() - .background(G1), - contentPadding = PaddingValues(vertical = 8.dp) + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) ) { - item { + RoundButton( + onClick = onOpenCommentSheet, + modifier = Modifier + .padding(bottom = 8.dp) + .size(200.dp, 68.dp) + ) { 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 = { /* TODO: 삭제 핸들러 추가 */ } + text = stringResource(text_float_add_comment), + fontSize = 22.sp ) } } @@ -408,13 +374,9 @@ private fun DailyCertifyScreenPreview_Initial() { certifyStatus = CertifyStatus.BEFORE ), onBackRequest = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } @@ -427,20 +389,16 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { uiState = DailyCertifyUiState( missionText = "공원 산책 가서 비둘기 사진 찍기", missionTime = LocalTime.of(14, 30), - imageUrl = "", + 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 = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } @@ -453,7 +411,7 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { uiState = DailyCertifyUiState( missionText = "공원 산책 가서 비둘기 사진 찍기", missionTime = LocalTime.of(14, 30), - imageUrl = "", + 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", "김소라", "씹희", "부산 날씨 완전 봄이야") @@ -463,13 +421,9 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { ), onBackRequest = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } \ 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 index 15029bd2..bd62e608 100644 --- a/feature/daily-certify/src/main/res/values/strings.xml +++ b/feature/daily-certify/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ 일과 인증 + 댓글 남기기 \ No newline at end of file From 96398da162e6fd4e01cc37211d647a6fc5562dae Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 21 May 2025 17:34:12 +0900 Subject: [PATCH 14/34] =?UTF-8?q?feat:=20bottomSheet,=20dialog=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20#162?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 256 ++++++++++++++---- .../daily/certify/DailyCertifyUiState.kt | 2 + 2 files changed, 210 insertions(+), 48 deletions(-) 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 index 16265de7..e45f0d1d 100644 --- 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 @@ -1,7 +1,13 @@ package com.teampatch.daily.certify 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 @@ -9,7 +15,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.AlertDialog +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.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField @@ -34,13 +48,19 @@ 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_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.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_title_appbar import java.time.LocalTime @@ -62,10 +82,16 @@ internal fun DailyCertifyScreen( onCommentEditRequest: (DailyComment?) -> Unit, onCommentEditComplete: (String) -> Unit, onCertifyComplete: () -> Unit, + onDismissDialog: () -> Unit, + onOpenCommentSheet: () -> Unit, + onCloseCommentSheet: () -> Unit, ) { val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() + val isImageUploaded = uiState.imageUrl?.isNotBlank() == true + val isCertifyConfirmed = uiState.certifyStatus == CertifyStatus.CONFIRMED + // ✅ 댓글 수정 BottomSheet if (uiState.editingComment != null) { ModalBottomSheet( onDismissRequest = { onCommentEditRequest(null) }, @@ -91,6 +117,48 @@ internal fun DailyCertifyScreen( } } + // ✅ 사진 인증 안내 Dialog + if (uiState.showCertifyDialog) { + AlertDialog( + onDismissRequest = onDismissDialog, + confirmButton = { + TextButton(onClick = onDismissDialog) { + Text("확인") + } + }, + title = { Text("사진 인증 안내") }, + text = { Text("이 사진이 정말 맞나요?") } + ) + } + + // ✅ 댓글 작성 BottomSheet + if (uiState.showCommentSheet) { + ModalBottomSheet( + onDismissRequest = onCloseCommentSheet, + sheetState = sheetState + ) { + Column(Modifier.padding(16.dp)) { + Text("댓글 작성", style = MaterialTheme.typography.titleMedium) + Spacer(Modifier.height(8.dp)) + var text by remember { mutableStateOf("") } + OutlinedTextField( + value = text, + onValueChange = { text = it }, + placeholder = { Text("댓글을 입력해주세요") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.height(8.dp)) + DefaultButton(onClick = { + onCommentSubmit(text) + scope.launch { sheetState.hide() } + onCloseCommentSheet() + }) { + Text("댓글 남기기") + } + } + } + } + Scaffold( topBar = { BackButtonAppBar( @@ -101,10 +169,11 @@ internal fun DailyCertifyScreen( ) }, bottomBar = { - when (uiState.certifyStatus) { - CertifyStatus.BEFORE -> { + when { + !isImageUploaded -> { DefaultButton( - onClick = onCertifyComplete, + onClick = {}, + enabled = false, modifier = Modifier .fillMaxWidth() .padding(20.dp) @@ -113,28 +182,33 @@ internal fun DailyCertifyScreen( } } - CertifyStatus.PENDING -> { + uiState.certifyStatus == CertifyStatus.BEFORE -> { DefaultButton( - onClick = {}, + onClick = onCertifyComplete, modifier = Modifier .fillMaxWidth() - .padding(20.dp), - enabled = false + .padding(20.dp) ) { - Text("처리 중...") + Text("인증 완료") } } - CertifyStatus.CONFIRMED -> { + isCertifyConfirmed -> { Column(Modifier.padding(16.dp)) { - OutlinedTextField( - value = "", - onValueChange = {}, - placeholder = { Text("댓글을 입력해주세요") }, - modifier = Modifier.fillMaxWidth() - ) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onDismissDialog() }, // 아이콘 클릭 시 Dialog 띄우기 + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "댓글을 남겨보세요!") + Icon(Icons.Default.MoreVert, contentDescription = "댓글 안내") + } + Spacer(modifier = Modifier.height(8.dp)) - DefaultButton(onClick = { onCommentSubmit("작성한 댓글") }) { + + DefaultButton(onClick = onOpenCommentSheet) { Text("댓글 남기기") } } @@ -146,6 +220,7 @@ internal fun DailyCertifyScreen( Modifier .padding(paddingValues) .fillMaxSize() + .background(color = WH) ) { // 인증 이미지 uiState.imageUrl?.let { @@ -158,26 +233,32 @@ internal fun DailyCertifyScreen( missionTime = uiState.missionTime ) - when (uiState.certifyStatus) { - CertifyStatus.CONFIRMED -> { - // 댓글 목록 - LazyColumn { - items(uiState.comments) { comment -> - CommentItem( - comment = comment, - onEditClick = { onCommentEditRequest(comment) } - ) - } + if (isCertifyConfirmed) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(G1), + contentPadding = PaddingValues(vertical = 8.dp) + ) { + 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) + ) } - } - - CertifyStatus.BEFORE -> { - // 아무것도 안 보여도 됨, 또는 안내 메시지 - Spacer(modifier = Modifier.height(32.dp)) - } - CertifyStatus.PENDING -> { - Spacer(modifier = Modifier.height(32.dp)) + items(uiState.comments, key = { it.commentId }) { comment -> + DailyCertifyCommentItem( + comment = comment, + onEditClick = { onCommentEditRequest(comment) }, + onDeleteClick = { /* TODO: 삭제 핸들러 추가 */ } + ) + } } } } @@ -235,16 +316,81 @@ fun CertifyImage(imageUrl: String?) { } @Composable -fun CommentItem( +fun DailyCertifyCommentItem( comment: DailyComment, onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, ) { - Column(Modifier.padding(16.dp)) { - Text(text = comment.writerName, fontWeight = FontWeight.Bold) - Text(text = comment.content, modifier = Modifier.padding(top = 4.dp)) - TextButton(onClick = onEditClick) { - Text("댓글 수정") + 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() + ) { + // TODO: 실제 사용자 프로필 이미지 연결 필요 + Image( + painter = painterResource(ic_my_appbar), + contentDescription = "user profile" + ) + + 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) + ) } } @@ -258,13 +404,17 @@ private fun DailyCertifyScreenPreview_Initial() { missionTime = LocalTime.of(14, 30), imageUrl = "", // 아직 사진 없음 comments = emptyList(), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.BEFORE ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } @@ -279,13 +429,18 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { missionTime = LocalTime.of(14, 30), imageUrl = "", comments = emptyList(), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.PENDING + ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } @@ -300,16 +455,21 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { missionTime = LocalTime.of(14, 30), imageUrl = "", comments = listOf( - DailyComment("1", "순대 조던", "비둘기 너무 귀여워요", ""), - DailyComment("2", "김소라", "부산 날씨 완전 봄이야", "") + DailyComment("1", "순대 조던", "유정상", "비둘기 너무 귀여워요"), + DailyComment("2", "김소라", "씹희", "부산 날씨 완전 봄이야") ), - editingComment = null + editingComment = null, + certifyStatus = CertifyStatus.CONFIRMED // ✅ 이걸 넣어야 댓글 목록이 나타남! + ), onBackRequest = {}, onCommentSubmit = {}, onCommentEditRequest = {}, onCommentEditComplete = {}, - onCertifyComplete = {} + onCertifyComplete = {}, + onDismissDialog = {}, + onOpenCommentSheet = {}, + onCloseCommentSheet = {} ) } } \ 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 index 64d2a22f..435777d4 100644 --- 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 @@ -10,4 +10,6 @@ internal data class DailyCertifyUiState( 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 From b430129323e5412c4bda04b26daf06071d244580 Mon Sep 17 00:00:00 2001 From: theBettor Date: Thu, 22 May 2025 01:49:53 +0900 Subject: [PATCH 15/34] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=EC=82=AD?= =?UTF-8?q?=EC=A0=9C,=20BottomSheet=EB=A5=BC=20=EC=A0=9C=EC=99=B8=ED=95=9C?= =?UTF-8?q?=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=A7=88=EB=AC=B4=EB=A6=AC=20?= =?UTF-8?q?#162?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 250 +++++++----------- .../src/main/res/values/strings.xml | 1 + 2 files changed, 103 insertions(+), 148 deletions(-) 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 index e45f0d1d..e941278c 100644 --- 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 @@ -1,9 +1,11 @@ package com.teampatch.daily.certify +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 @@ -13,29 +15,25 @@ 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.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material3.AlertDialog 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.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton 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.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -53,6 +51,7 @@ 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 @@ -62,10 +61,10 @@ 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 -import kotlinx.coroutines.launch @Composable internal fun DailyCertifyRoute( @@ -78,84 +77,34 @@ internal fun DailyCertifyRoute( internal fun DailyCertifyScreen( uiState: DailyCertifyUiState, onBackRequest: () -> Unit, - onCommentSubmit: (String) -> Unit, onCommentEditRequest: (DailyComment?) -> Unit, - onCommentEditComplete: (String) -> Unit, onCertifyComplete: () -> Unit, - onDismissDialog: () -> Unit, onOpenCommentSheet: () -> Unit, - onCloseCommentSheet: () -> Unit, ) { - val sheetState = rememberModalBottomSheetState() - val scope = rememberCoroutineScope() + val commentEditSheetState = rememberModalBottomSheetState() + val commentWriteSheetState = rememberModalBottomSheetState() val isImageUploaded = uiState.imageUrl?.isNotBlank() == true val isCertifyConfirmed = uiState.certifyStatus == CertifyStatus.CONFIRMED - - // ✅ 댓글 수정 BottomSheet - if (uiState.editingComment != null) { - ModalBottomSheet( - onDismissRequest = { onCommentEditRequest(null) }, - sheetState = sheetState - ) { - Column(Modifier.padding(16.dp)) { - Text(text = "댓글 수정", style = MaterialTheme.typography.titleMedium) - Spacer(modifier = Modifier.height(8.dp)) - var text by remember { mutableStateOf(uiState.editingComment.content) } - OutlinedTextField( - value = text, - onValueChange = { text = it }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - DefaultButton(onClick = { - onCommentEditComplete(text) - scope.launch { sheetState.hide() } - }) { - Text("수정 완료") - } - } + val lazyListState = rememberLazyListState() + val isFloatingVisible by remember { + derivedStateOf { + !lazyListState.isScrollInProgress && !lazyListState.canScrollBackward } } - // ✅ 사진 인증 안내 Dialog - if (uiState.showCertifyDialog) { - AlertDialog( - onDismissRequest = onDismissDialog, - confirmButton = { - TextButton(onClick = onDismissDialog) { - Text("확인") - } - }, - title = { Text("사진 인증 안내") }, - text = { Text("이 사진이 정말 맞나요?") } - ) + LaunchedEffect(uiState.editingComment) { + if (uiState.editingComment != null) { + commentEditSheetState.show() + } else { + commentEditSheetState.hide() + } } - // ✅ 댓글 작성 BottomSheet - if (uiState.showCommentSheet) { - ModalBottomSheet( - onDismissRequest = onCloseCommentSheet, - sheetState = sheetState - ) { - Column(Modifier.padding(16.dp)) { - Text("댓글 작성", style = MaterialTheme.typography.titleMedium) - Spacer(Modifier.height(8.dp)) - var text by remember { mutableStateOf("") } - OutlinedTextField( - value = text, - onValueChange = { text = it }, - placeholder = { Text("댓글을 입력해주세요") }, - modifier = Modifier.fillMaxWidth() - ) - Spacer(Modifier.height(8.dp)) - DefaultButton(onClick = { - onCommentSubmit(text) - scope.launch { sheetState.hide() } - onCloseCommentSheet() - }) { - Text("댓글 남기기") - } - } + LaunchedEffect(uiState.showCommentSheet) { + if (uiState.showCommentSheet) { + commentWriteSheetState.show() + } else { + commentWriteSheetState.hide() } } @@ -182,7 +131,7 @@ internal fun DailyCertifyScreen( } } - uiState.certifyStatus == CertifyStatus.BEFORE -> { + uiState.certifyStatus == CertifyStatus.PENDING -> { DefaultButton( onClick = onCertifyComplete, modifier = Modifier @@ -194,69 +143,86 @@ internal fun DailyCertifyScreen( } isCertifyConfirmed -> { - Column(Modifier.padding(16.dp)) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onDismissDialog() }, // 아이콘 클릭 시 Dialog 띄우기 - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "댓글을 남겨보세요!") - Icon(Icons.Default.MoreVert, contentDescription = "댓글 안내") - } + // ✅ 인증 완료 시 bottomBar는 비우거나 "제어용 UI"로 대체 가능 + Spacer(modifier = Modifier.height(1.dp)) // 유지용 + } + } + } + ) { paddingValues -> + Box(Modifier.fillMaxSize()) { + Column( + Modifier + .padding(paddingValues) + .fillMaxSize() + .background(color = WH) + ) { + uiState.imageUrl?.let { + CertifyImage(imageUrl = it) + } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(Modifier.height(12.dp)) - DefaultButton(onClick = onOpenCommentSheet) { - Text("댓글 남기기") + 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 = { /* TODO */ } + ) } } } } - } - ) { paddingValues -> - Column( - Modifier - .padding(paddingValues) - .fillMaxSize() - .background(color = WH) - ) { - // 인증 이미지 - uiState.imageUrl?.let { - CertifyImage(imageUrl = it) - } - - Spacer(Modifier.height(12.dp)) - MissionInfoSection( - missionText = uiState.missionText, - missionTime = uiState.missionTime - ) + // ✅ 댓글 남기기 UI (Floating UI) if (isCertifyConfirmed) { - LazyColumn( + val shouldShowFloatingCommentButton = isCertifyConfirmed && isFloatingVisible + // IDE에서 always true로 추론하는 건 Preview 상의 오해임 – 런타임에는 유동적 + + AnimatedVisibility( + visible = shouldShowFloatingCommentButton, + enter = fadeIn(tween(500)), + exit = fadeOut(tween(500)), modifier = Modifier - .fillMaxSize() - .background(G1), - contentPadding = PaddingValues(vertical = 8.dp) + .align(Alignment.BottomCenter) + .padding(bottom = 16.dp) ) { - item { + RoundButton( + onClick = onOpenCommentSheet, + modifier = Modifier + .padding(bottom = 8.dp) + .size(200.dp, 68.dp) + ) { 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 = { /* TODO: 삭제 핸들러 추가 */ } + text = stringResource(text_float_add_comment), + fontSize = 22.sp ) } } @@ -408,13 +374,9 @@ private fun DailyCertifyScreenPreview_Initial() { certifyStatus = CertifyStatus.BEFORE ), onBackRequest = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } @@ -427,20 +389,16 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { uiState = DailyCertifyUiState( missionText = "공원 산책 가서 비둘기 사진 찍기", missionTime = LocalTime.of(14, 30), - imageUrl = "", + 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 = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } @@ -453,7 +411,7 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { uiState = DailyCertifyUiState( missionText = "공원 산책 가서 비둘기 사진 찍기", missionTime = LocalTime.of(14, 30), - imageUrl = "", + 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", "김소라", "씹희", "부산 날씨 완전 봄이야") @@ -463,13 +421,9 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { ), onBackRequest = {}, - onCommentSubmit = {}, onCommentEditRequest = {}, - onCommentEditComplete = {}, onCertifyComplete = {}, - onDismissDialog = {}, - onOpenCommentSheet = {}, - onCloseCommentSheet = {} + onOpenCommentSheet = {} ) } } \ 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 index 15029bd2..bd62e608 100644 --- a/feature/daily-certify/src/main/res/values/strings.xml +++ b/feature/daily-certify/src/main/res/values/strings.xml @@ -1,4 +1,5 @@ 일과 인증 + 댓글 남기기 \ No newline at end of file From fd75fc4492ecd29ccbd634978becaee3cf79824b Mon Sep 17 00:00:00 2001 From: theBettor Date: Mon, 26 May 2025 02:27:19 +0900 Subject: [PATCH 16/34] =?UTF-8?q?feat:=20workmanager=EB=A1=9C=20notificati?= =?UTF-8?q?on=20=EB=9D=84=EC=9A=B0=EA=B8=B0=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 3 +- core/data/build.gradle.kts | 4 + core/data/src/main/AndroidManifest.xml | 4 +- .../repository/NotificationRepositoryImpl.kt | 103 ++++++++++++++++++ .../repository/NotificationRepository.kt | 7 ++ .../ScheduleCertifyNotificationUseCase.kt | 13 +++ .../daily/certify/DailyAlarmScreen.kt | 1 + .../daily/certify/DailyCertifyNavigation.kt | 4 +- .../daily/certify/EditCertifyActivity.kt | 26 +++++ .../feature/daily/edit/DailyEditScreen.kt | 42 +++---- .../feature/daily/edit/DailyEditViewModel.kt | 7 ++ 11 files changed, 190 insertions(+), 24 deletions(-) create mode 100644 core/data/src/main/java/com/teampatch/core/data/repository/NotificationRepositoryImpl.kt create mode 100644 core/domain/src/main/java/com/teampatch/core/domain/repository/NotificationRepository.kt create mode 100644 core/domain/src/main/java/com/teampatch/core/domain/usecase/daily/ScheduleCertifyNotificationUseCase.kt create mode 100644 feature/daily-certify/src/main/java/com/teampatch/daily/certify/EditCertifyActivity.kt diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index d9046891..d4d07e07 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -238,7 +238,8 @@ fun MainNavHost( addDailyCertifyAlarmScreen( onPickImageScreenRequest = {}, - onDismissRequest = { navController.navigateToHomeScreen() } + onDismissRequest = { navController.navigateToHomeScreen() }, + fromNotification = true ) addMemoryStorageDetailScreen( 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/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/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/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/DailyAlarmScreen.kt index aa46c3ca..8dbf1ccc 100644 --- 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 @@ -33,6 +33,7 @@ import java.time.format.DateTimeFormatter @Composable internal fun DailyAlarmRoute( + fromNotification: Boolean, onPickImageScreenRequest: () -> Unit, onDismissRequest: () -> Unit, ) { 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 index eeb1a598..f8a48c58 100644 --- 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 @@ -20,11 +20,13 @@ fun NavController.navigateToDailyCertifyAlarmScreen( fun NavGraphBuilder.addDailyCertifyAlarmScreen( onPickImageScreenRequest: () -> Unit, onDismissRequest: () -> Unit, + fromNotification: Boolean, ) { composable { DailyAlarmRoute( onPickImageScreenRequest = onPickImageScreenRequest, - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + fromNotification = fromNotification ) } } \ 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..2db9169b --- /dev/null +++ b/feature/daily-certify/src/main/java/com/teampatch/daily/certify/EditCertifyActivity.kt @@ -0,0 +1,26 @@ +package com.teampatch.daily.certify + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import com.teampatch.core.designsystem.theme.HarmonyTheme +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class EditCertifyActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val isFromNotification = intent.getBooleanExtra("FROM_NOTIFICATION", false) + + setContent { + HarmonyTheme { + DailyAlarmRoute( + fromNotification = isFromNotification, + onDismissRequest = {}, + onPickImageScreenRequest = {} + ) + } + } + } +} \ No newline at end of file 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 1dc80512..f2f28558 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 @@ -25,6 +25,7 @@ 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 @@ -56,7 +57,6 @@ 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 @@ -69,6 +69,21 @@ internal fun DailyEditRoute( val context = LocalContext.current val uiState by viewModel.dailyEditUiState + val timePickerLauncher = rememberUpdatedState { + val now = LocalTime.now() + TimePickerDialog( + context, + { _, hour, minute -> + val selectedTime = LocalTime.of(hour, minute) + viewModel.changeSelectedTime(selectedTime) + viewModel.onTimeSelected(LocalDateTime.of(LocalDate.now(), selectedTime)) + }, + now.hour, + now.minute, + true + ).show() + } + LaunchedEffect(Unit) { viewModel.event.collect { when (it) { @@ -89,7 +104,8 @@ internal fun DailyEditRoute( selectedDays = uiState.selectedDays, onDaySelected = { viewModel.toggleSelectedDay(it) }, selectedTime = uiState.selectedTime, - onTimeSelected = { viewModel.changeSelectedTime(it) } + onTimePickRequest = { timePickerLauncher.value() } // 👈 NEW + ) } } @@ -101,7 +117,7 @@ internal fun DailyEditScreen( selectedDays: Set, onDaySelected: (DayOfWeek) -> Unit, selectedTime: LocalTime?, - onTimeSelected: (LocalTime) -> Unit, + onTimePickRequest: () -> Unit, ) { val context = LocalContext.current val textState = rememberSaveable { mutableStateOf("") } @@ -190,22 +206,6 @@ internal fun DailyEditScreen( Text(stringResource(select_time), fontSize = 18.sp) - 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 -> - onTimeSelected(LocalTime.of(selectedHour, selectedMinute)) - }, - hour, - minute, - true - ) - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier @@ -214,7 +214,7 @@ internal fun DailyEditScreen( .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), @@ -248,7 +248,7 @@ private fun DailyEditScreenPreview() { } }, selectedTime = selectedTime, - onTimeSelected = { time -> selectedTime = time } + onTimePickRequest = { } ) } } \ 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 de2b157e..7da55316 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,8 +5,10 @@ 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 @@ -20,6 +22,7 @@ import kotlinx.coroutines.launch @HiltViewModel internal class DailyEditViewModel @Inject constructor( private val getDailyManageUseCase: GetDailyManageUseCase, + private val scheduleCertifyNotificationUseCase: ScheduleCertifyNotificationUseCase, ) : ViewModel() { private val _dailyEditUiState = mutableStateOf(DailyEditUiState()) @@ -67,4 +70,8 @@ internal class DailyEditViewModel @Inject constructor( selectedTime = time ) } + + fun onTimeSelected(time: LocalDateTime) { + scheduleCertifyNotificationUseCase(time) + } } \ No newline at end of file From dfd9a18e8bfdb00e8d0496c15190b2c04a05fca3 Mon Sep 17 00:00:00 2001 From: theBettor Date: Mon, 26 May 2025 18:11:48 +0900 Subject: [PATCH 17/34] =?UTF-8?q?fix:=20=EA=BC=AD=20=EB=8F=84=EC=B0=A9?= =?UTF-8?q?=ED=95=B4=EC=95=BC=ED=95=A0=20=EC=95=A1=ED=8B=B0=EB=B9=84?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20manifest=20=EB=AA=85?= =?UTF-8?q?=EC=8B=9C=ED=95=B4=EC=A3=BC=EA=B8=B0=20#169?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- feature/daily-certify/src/main/AndroidManifest.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/feature/daily-certify/src/main/AndroidManifest.xml b/feature/daily-certify/src/main/AndroidManifest.xml index a5918e68..72e8de64 100644 --- a/feature/daily-certify/src/main/AndroidManifest.xml +++ b/feature/daily-certify/src/main/AndroidManifest.xml @@ -1,4 +1,10 @@ - + + + + \ No newline at end of file From a58149356cfa5623d63c7b06c06cc41637f78a4e Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 27 May 2025 01:15:16 +0900 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20Alarm->Certify=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=97=B0=EA=B2=B0=20#180?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 3 +- .../daily/certify/DailyCertifyNavigation.kt | 20 ++++++++++++ .../daily/certify/DailyCertifyScreen.kt | 22 +++++++++++++ .../daily/certify/DailyCertifyViewModel.kt | 31 +++++++++++++++++-- .../daily/certify/EditCertifyActivity.kt | 29 ++++++++++++++--- 5 files changed, 96 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index d4d07e07..28d72869 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -11,6 +11,7 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.teampatch.core.common.findActivity 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 @@ -237,7 +238,7 @@ fun MainNavHost( ) addDailyCertifyAlarmScreen( - onPickImageScreenRequest = {}, + onPickImageScreenRequest = { navController.navigateToDailyCertifyScreen() }, onDismissRequest = { navController.navigateToHomeScreen() }, fromNotification = true ) 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 index f8a48c58..94e84474 100644 --- 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 @@ -29,4 +29,24 @@ fun NavGraphBuilder.addDailyCertifyAlarmScreen( fromNotification = fromNotification ) } +} + +@Serializable +data object DailyCertifyScreenRoute + +fun NavController.navigateToDailyCertifyScreen( + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, +) { + navigate(DailyCertifyScreenRoute, navOptions, navigatorExtras) +} + +fun NavGraphBuilder.addDailyCertifyScreen( + onBackRequest: () -> Unit, +) { + composable { + DailyCertifyRoute( + 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 index e941278c..170fdc7f 100644 --- 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 @@ -1,5 +1,6 @@ package com.teampatch.daily.certify +import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -45,6 +46,7 @@ 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 androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter import com.teampatch.core.designsystem.R.drawable.ic_more_question import com.teampatch.core.designsystem.R.drawable.ic_my_appbar @@ -70,6 +72,26 @@ import java.time.format.DateTimeFormatter internal fun DailyCertifyRoute( onBackRequest: () -> Unit, ) { + val viewModel: DailyCertifyViewModel = hiltViewModel() + val uiState by viewModel.uiState + + LaunchedEffect(Unit) { + Log.d("DEBUG", "DailyCertifyRoute Loaded") + } + + DailyCertifyScreen( + uiState = uiState, + onBackRequest = onBackRequest, + onCommentEditRequest = { comment -> + viewModel.editComment(comment) // 💡 댓글 수정 요청 + }, + onCertifyComplete = { + viewModel.completeCertify() // 💡 인증 완료 처리 + }, + onOpenCommentSheet = { + viewModel.openCommentSheet() // 💡 댓글 작성 바텀시트 열기 + } + ) } @Composable 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 index 39b76018..b77c552f 100644 --- 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 @@ -3,20 +3,45 @@ 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 javax.inject.Inject @HiltViewModel internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { private val _uiState = mutableStateOf( DailyCertifyUiState( -// title = "오늘의 인증 미션", -// time = LocalTime.now() // 예시 + missionText = "오늘의 인증 미션", + missionTime = LocalTime.of(14, 0), // 예시 시간 + 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 openCommentSheet() { + _uiState.value = _uiState.value.copy( + showCommentSheet = true + ) + } + fun onDismiss() { - // 예: 상태 초기화 또는 Analytics 전송 등 + // TODO: 인증 완료 종료 시 처리할 것 } } \ 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 index 2db9169b..ead417b5 100644 --- 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 @@ -3,6 +3,9 @@ package com.teampatch.daily.certify import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +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 @@ -15,11 +18,27 @@ class EditCertifyActivity : ComponentActivity() { setContent { HarmonyTheme { - DailyAlarmRoute( - fromNotification = isFromNotification, - onDismissRequest = {}, - onPickImageScreenRequest = {} - ) + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = "daily_alarm" + ) { + composable("daily_alarm") { + DailyAlarmRoute( + fromNotification = isFromNotification, + onDismissRequest = { finish() }, + onPickImageScreenRequest = { + navController.navigate(DailyCertifyScreenRoute) + } + ) + } + + // Type-safe composable 추가 + addDailyCertifyScreen( + onBackRequest = { navController.popBackStack() } + ) + } } } } From 91720536172477212748b3908310e71f69bbdd95 Mon Sep 17 00:00:00 2001 From: theBettor Date: Tue, 27 May 2025 02:20:59 +0900 Subject: [PATCH 19/34] =?UTF-8?q?refactore:=20=ED=94=84=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EC=9A=A9=EC=9D=B4=20=EC=95=84=EB=8B=8C=20PlaceHolder=EB=A1=9C?= =?UTF-8?q?=20=EA=B5=90=EC=B2=B4=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyNavigation.kt | 28 ++++- .../daily/certify/DailyCertifyScreen.kt | 112 +++++++++++++++--- .../daily/certify/DailyCertifyViewModel.kt | 7 +- .../daily/certify/EditCertifyActivity.kt | 23 +++- 4 files changed, 149 insertions(+), 21 deletions(-) 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 index 94e84474..874a7d66 100644 --- 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 @@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.compose.composable +import com.teampatch.core.domain.model.DailyComment import kotlinx.serialization.Serializable @Serializable @@ -43,10 +44,35 @@ fun NavController.navigateToDailyCertifyScreen( fun NavGraphBuilder.addDailyCertifyScreen( onBackRequest: () -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onCertifyComplete: () -> Unit, + onOpenCommentSheet: () -> Unit, + onNavigateToDetail: () -> Unit, ) { composable { DailyCertifyRoute( - onBackRequest = onBackRequest + onBackRequest = onBackRequest, + onCommentEditRequest = onCommentEditRequest, + onCertifyComplete = onCertifyComplete, + onOpenCommentSheet = onOpenCommentSheet, + onNavigateToDetail = onNavigateToDetail + ) + } +} + +@Serializable +data object DailyCertifyDetailScreenRoute + +fun NavGraphBuilder.addDailyCertifyDetailScreen( + onBackRequest: () -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onOpenCommentSheet: () -> Unit, +) { + composable { + DailyCertifyDetailRoute( + onBackRequest = onBackRequest, + onCommentEditRequest = onCommentEditRequest, + onOpenCommentSheet = onOpenCommentSheet ) } } \ 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 index 170fdc7f..8cb2bc95 100644 --- 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 @@ -1,12 +1,16 @@ package com.teampatch.daily.certify -import android.util.Log +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 @@ -39,6 +43,7 @@ import androidx.compose.runtime.setValue 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.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -48,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel 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 @@ -71,29 +77,61 @@ import java.time.format.DateTimeFormatter @Composable internal fun DailyCertifyRoute( onBackRequest: () -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onCertifyComplete: () -> Unit, + onOpenCommentSheet: () -> Unit, + onNavigateToDetail: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 ) { + val context = LocalContext.current val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState - LaunchedEffect(Unit) { - Log.d("DEBUG", "DailyCertifyRoute Loaded") + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + onResult = { uri: Uri? -> + uri?.let { + viewModel.updateImage(it.toString()) + } + } + ) + + LaunchedEffect(uiState.certifyStatus) { + if (uiState.certifyStatus == CertifyStatus.CONFIRMED) { + onNavigateToDetail() // ✅ 상태 변경 시 자동 이동 + } } DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, - onCommentEditRequest = { comment -> - viewModel.editComment(comment) // 💡 댓글 수정 요청 - }, - onCertifyComplete = { - viewModel.completeCertify() // 💡 인증 완료 처리 - }, - onOpenCommentSheet = { - viewModel.openCommentSheet() // 💡 댓글 작성 바텀시트 열기 + onCommentEditRequest = onCommentEditRequest, + onCertifyComplete = onCertifyComplete, + onOpenCommentSheet = onOpenCommentSheet, + onImagePickRequest = { + launcher.launch("image/*") } ) } +@Composable +fun DailyCertifyDetailRoute( + onBackRequest: () -> Unit, + onCommentEditRequest: (DailyComment?) -> Unit, + onOpenCommentSheet: () -> Unit, +) { + val viewModel: DailyCertifyViewModel = hiltViewModel() + val uiState by viewModel.uiState + + DailyCertifyScreen( + uiState = uiState, + onBackRequest = onBackRequest, + onCommentEditRequest = onCommentEditRequest, + onCertifyComplete = {}, // 완료 상태이므로 버튼 없음 + onOpenCommentSheet = onOpenCommentSheet, + onImagePickRequest = { } + ) +} + @Composable @OptIn(ExperimentalMaterial3Api::class) internal fun DailyCertifyScreen( @@ -102,6 +140,7 @@ internal fun DailyCertifyScreen( onCommentEditRequest: (DailyComment?) -> Unit, onCertifyComplete: () -> Unit, onOpenCommentSheet: () -> Unit, + onImagePickRequest: () -> Unit, // ✅ 추가 ) { val commentEditSheetState = rememberModalBottomSheetState() val commentWriteSheetState = rememberModalBottomSheetState() @@ -178,8 +217,14 @@ internal fun DailyCertifyScreen( .fillMaxSize() .background(color = WH) ) { - uiState.imageUrl?.let { - CertifyImage(imageUrl = it) + when { + uiState.imageUrl?.isNotBlank() == true -> { + CertifyImage(imageUrl = uiState.imageUrl) + } + + else -> { + CertifyImagePlaceholder(onClick = onImagePickRequest) + } } Spacer(Modifier.height(12.dp)) @@ -285,6 +330,37 @@ private fun MissionInfoSection( } } +@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 @@ -398,7 +474,8 @@ private fun DailyCertifyScreenPreview_Initial() { onBackRequest = {}, onCommentEditRequest = {}, onCertifyComplete = {}, - onOpenCommentSheet = {} + onOpenCommentSheet = {}, + onImagePickRequest = {} ) } } @@ -420,7 +497,9 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { onBackRequest = {}, onCommentEditRequest = {}, onCertifyComplete = {}, - onOpenCommentSheet = {} + onOpenCommentSheet = {}, + onImagePickRequest = {} + ) } } @@ -445,7 +524,8 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { onBackRequest = {}, onCommentEditRequest = {}, onCertifyComplete = {}, - onOpenCommentSheet = {} + onOpenCommentSheet = {}, + onImagePickRequest = {} ) } } \ 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 index b77c552f..bd00cb02 100644 --- 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 @@ -13,7 +13,8 @@ internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { private val _uiState = mutableStateOf( DailyCertifyUiState( missionText = "오늘의 인증 미션", - missionTime = LocalTime.of(14, 0), // 예시 시간 + missionTime = LocalTime.of(14, 0), // 예시 시간, + imageUrl = null, // ✅ 중요 certifyStatus = CertifyStatus.PENDING, comments = listOf( DailyComment("1", "소금형", "씹직", "고양이 귀엽네"), @@ -44,4 +45,8 @@ internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { fun onDismiss() { // TODO: 인증 완료 종료 시 처리할 것 } + + 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 index ead417b5..96a2e27b 100644 --- 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 @@ -3,6 +3,7 @@ package com.teampatch.daily.certify import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -35,9 +36,25 @@ class EditCertifyActivity : ComponentActivity() { } // Type-safe composable 추가 - addDailyCertifyScreen( - onBackRequest = { navController.popBackStack() } - ) + composable { + val viewModel: DailyCertifyViewModel = hiltViewModel() + + DailyCertifyRoute( + onBackRequest = { navController.popBackStack() }, + onCommentEditRequest = { comment -> + viewModel.editComment(comment) // ✅ 바텀시트 열기 + }, + onCertifyComplete = { + viewModel.completeCertify() // ✅ 인증 완료 처리 + }, + onOpenCommentSheet = { + viewModel.openCommentSheet() // ✅ 댓글 작성 시트 열기 + }, + onNavigateToDetail = { + navController.navigate(DailyCertifyDetailScreenRoute) // ✅ 상세화면 이동 + } + ) + } } } } From 8cf5b85109870e70fa5c1ced1c6189772f80f01b Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 28 May 2025 01:58:20 +0900 Subject: [PATCH 20/34] =?UTF-8?q?feat:=20Certify->=20CertiyDetail=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=EC=97=B0=EA=B2=B0=20#180?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/MainNavHost.kt | 13 +++++++ .../daily/certify/DailyCertifyNavigation.kt | 26 +++++++------- .../daily/certify/DailyCertifyScreen.kt | 34 ++++++++----------- .../daily/certify/EditCertifyActivity.kt | 17 +++++----- 4 files changed, 48 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index 28d72869..7a91b73c 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -11,6 +11,9 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.teampatch.core.common.findActivity import com.teampatch.daily.certify.addDailyCertifyAlarmScreen +import com.teampatch.daily.certify.addDailyCertifyDetailScreen +import com.teampatch.daily.certify.addDailyCertifyScreen +import com.teampatch.daily.certify.navigateToDailyCertifyDetailScreen import com.teampatch.daily.certify.navigateToDailyCertifyScreen import com.teampatch.feature.answer.navigateToAnswerScreen import com.teampatch.feature.daily.edit.addDailyEditScreen @@ -243,6 +246,16 @@ fun MainNavHost( fromNotification = true ) + addDailyCertifyScreen( + onBackRequest = navController::navigateUp, + onCertifyCompleteRequest = { navController.navigateToDailyCertifyDetailScreen() }, + onNavigateToDetailRequest = { navController.navigateToDailyCertifyDetailScreen() } + ) + + addDailyCertifyDetailScreen( + onBackRequest = navController::navigateUp + ) + addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, onRestartConversation = { navController.navigateToMemoryCardRegistrationScreen("memoryCardId") } 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 index 874a7d66..b7e4927d 100644 --- 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 @@ -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.DailyComment import kotlinx.serialization.Serializable @Serializable @@ -44,18 +43,14 @@ fun NavController.navigateToDailyCertifyScreen( fun NavGraphBuilder.addDailyCertifyScreen( onBackRequest: () -> Unit, - onCommentEditRequest: (DailyComment?) -> Unit, - onCertifyComplete: () -> Unit, - onOpenCommentSheet: () -> Unit, - onNavigateToDetail: () -> Unit, + onCertifyCompleteRequest: () -> Unit, + onNavigateToDetailRequest: () -> Unit, ) { composable { DailyCertifyRoute( onBackRequest = onBackRequest, - onCommentEditRequest = onCommentEditRequest, - onCertifyComplete = onCertifyComplete, - onOpenCommentSheet = onOpenCommentSheet, - onNavigateToDetail = onNavigateToDetail + onCertifyCompleteRequest = onCertifyCompleteRequest, + onNavigateToDetailRequest = onNavigateToDetailRequest ) } } @@ -63,16 +58,19 @@ fun NavGraphBuilder.addDailyCertifyScreen( @Serializable data object DailyCertifyDetailScreenRoute +fun NavController.navigateToDailyCertifyDetailScreen( + navOptions: NavOptions? = null, + navigatorExtras: Navigator.Extras? = null, +) { + navigate(DailyCertifyDetailScreenRoute, navOptions, navigatorExtras) +} + fun NavGraphBuilder.addDailyCertifyDetailScreen( onBackRequest: () -> Unit, - onCommentEditRequest: (DailyComment?) -> Unit, - onOpenCommentSheet: () -> Unit, ) { composable { DailyCertifyDetailRoute( - onBackRequest = onBackRequest, - onCommentEditRequest = onCommentEditRequest, - onOpenCommentSheet = onOpenCommentSheet + 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 index 8cb2bc95..d3bee015 100644 --- 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 @@ -43,7 +43,6 @@ import androidx.compose.runtime.setValue 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.platform.LocalInspectionMode import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -77,12 +76,9 @@ import java.time.format.DateTimeFormatter @Composable internal fun DailyCertifyRoute( onBackRequest: () -> Unit, - onCommentEditRequest: (DailyComment?) -> Unit, - onCertifyComplete: () -> Unit, - onOpenCommentSheet: () -> Unit, - onNavigateToDetail: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 + onCertifyCompleteRequest: () -> Unit, + onNavigateToDetailRequest: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 ) { - val context = LocalContext.current val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState @@ -97,16 +93,16 @@ internal fun DailyCertifyRoute( LaunchedEffect(uiState.certifyStatus) { if (uiState.certifyStatus == CertifyStatus.CONFIRMED) { - onNavigateToDetail() // ✅ 상태 변경 시 자동 이동 + onNavigateToDetailRequest() // ✅ 상태 변경 시 자동 이동 } } DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, - onCommentEditRequest = onCommentEditRequest, - onCertifyComplete = onCertifyComplete, - onOpenCommentSheet = onOpenCommentSheet, + onCommentEditRequest = { }, + onCertifyCompleteRequest = onCertifyCompleteRequest, + onOpenCommentSheet = { }, onImagePickRequest = { launcher.launch("image/*") } @@ -116,8 +112,6 @@ internal fun DailyCertifyRoute( @Composable fun DailyCertifyDetailRoute( onBackRequest: () -> Unit, - onCommentEditRequest: (DailyComment?) -> Unit, - onOpenCommentSheet: () -> Unit, ) { val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState @@ -125,9 +119,9 @@ fun DailyCertifyDetailRoute( DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, - onCommentEditRequest = onCommentEditRequest, - onCertifyComplete = {}, // 완료 상태이므로 버튼 없음 - onOpenCommentSheet = onOpenCommentSheet, + onCommentEditRequest = { viewModel.completeCertify() }, + onCertifyCompleteRequest = {}, // 완료 상태이므로 버튼 없음 + onOpenCommentSheet = { viewModel.openCommentSheet() }, onImagePickRequest = { } ) } @@ -138,7 +132,7 @@ internal fun DailyCertifyScreen( uiState: DailyCertifyUiState, onBackRequest: () -> Unit, onCommentEditRequest: (DailyComment?) -> Unit, - onCertifyComplete: () -> Unit, + onCertifyCompleteRequest: () -> Unit, onOpenCommentSheet: () -> Unit, onImagePickRequest: () -> Unit, // ✅ 추가 ) { @@ -194,7 +188,7 @@ internal fun DailyCertifyScreen( uiState.certifyStatus == CertifyStatus.PENDING -> { DefaultButton( - onClick = onCertifyComplete, + onClick = onCertifyCompleteRequest, modifier = Modifier .fillMaxWidth() .padding(20.dp) @@ -473,7 +467,7 @@ private fun DailyCertifyScreenPreview_Initial() { ), onBackRequest = {}, onCommentEditRequest = {}, - onCertifyComplete = {}, + onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, onImagePickRequest = {} ) @@ -496,7 +490,7 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { ), onBackRequest = {}, onCommentEditRequest = {}, - onCertifyComplete = {}, + onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, onImagePickRequest = {} @@ -523,7 +517,7 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { ), onBackRequest = {}, onCommentEditRequest = {}, - onCertifyComplete = {}, + onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, onImagePickRequest = {} ) 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 index 96a2e27b..e7e546b9 100644 --- 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 @@ -41,20 +41,21 @@ class EditCertifyActivity : ComponentActivity() { DailyCertifyRoute( onBackRequest = { navController.popBackStack() }, - onCommentEditRequest = { comment -> - viewModel.editComment(comment) // ✅ 바텀시트 열기 - }, - onCertifyComplete = { + onCertifyCompleteRequest = { viewModel.completeCertify() // ✅ 인증 완료 처리 }, - onOpenCommentSheet = { - viewModel.openCommentSheet() // ✅ 댓글 작성 시트 열기 - }, - onNavigateToDetail = { + onNavigateToDetailRequest = { navController.navigate(DailyCertifyDetailScreenRoute) // ✅ 상세화면 이동 } ) } + + // ✅ 여기에 이거 추가: + composable { + DailyCertifyDetailRoute( + onBackRequest = { navController.popBackStack() } + ) + } } } } From e7570eab3706bab7b7f1df96757ac05120c278d1 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 28 May 2025 03:16:43 +0900 Subject: [PATCH 21/34] =?UTF-8?q?fix:=20onCertifyCompleteRequest=EC=8B=9C?= =?UTF-8?q?=20CONFIRMED=EB=A1=9C=20=EB=81=9D=EB=82=98=EB=8A=94=EA=B2=83?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20CONFIRMED->PENDING=EC=9D=B4=20?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 1. 상태가 확정된 다음 navigation을 하자. ❗ 문제 설명 DailyCertifyRoute에서 CertifyStatus.CONFIRMED가 되면 → onNavigateToDetailRequest() 호출 navController.navigate(...)로 DailyCertifyDetailRoute로 이동 그런데 DailyCertifyViewModel은 여전히 같은 인스턴스를 공유 중 그런데 Compose는 LaunchedEffect(uiState.certifyStatus)를 재실행하면서 또 navigate 호출하지 않도록 조건을 신중히 처리해야 함 ✅ 왜 CertifyStatus.PENDING으로 보이는가? 가능성: ① completeCertify()가 아직 완료되기 전에 navigation이 먼저 호출됨 → LaunchedEffect가 CONFIRMED를 감지하긴 하지만 UI 상태 업데이트보다 빠르게 navigate() 호출 ② DailyCertifyDetailRoute에서 ViewModel은 공유되지만 → 화면에 재조합되는 시점에서 여전히 이전 상태 (PENDING)이 반영되어 보임 → 특히 DailyCertifyScreen 내부가 LaunchedEffect나 remember로 캐싱된 UI 상태를 보고 있을 가능성 ----해결------ ```kotlin onCertifyCompleteRequest = { viewModel.completeCertify() // ← 상태가 바로 반영되도록 되자마자 UI recomposition 유도 // 그리고 이후에 navigate 트리거 } val hasNavigated = remember { mutableStateOf(false) } LaunchedEffect(uiState.certifyStatus) { if (uiState.certifyStatus == CertifyStatus.CONFIRMED && !hasNavigated.value) { hasNavigated.value = true onNavigateToDetailRequest() } } ``` LaunchedEffect(certifyStatus)에서 navigation 트리거는 유지하되, LaunchedEffect는 다음처럼 한 번만 실행되도록 기억시키는 것도 중요 # 2. 하나의 ViewModel 인스턴스를 공유해야 함 이전 코드 구조에선 DailyCertifyRoute와 DailyCertifyDetailRoute에서 viewModel: DailyCertifyViewModel = hiltViewModel() 를 각자 선언하고 있었기 때문에, 각 Route 진입 시 마다 다른 ViewModel 인스턴스가 생성되어 상태 초기화가 발생하는 겁니다. DailyCertifyRoute 와 DailyCertifyDetailRoute 모두 EditCertifyActivity에서 선언한 ViewModel을 prop으로 받아야 합니다. 📌 Point: hiltViewModel() 을 Route 내부에서 여러 번 호출하면, Navigation Graph Scope로 인해 서로 다른 인스턴스가 됩니다. 반드시 Activity 단에서 viewModel() 호출해서 전달하는 방식이 중요합니다. 현재 문제는 ViewModel 범위가 Route마다 달라서 상태가 공유되지 않는 것입니다. viewModel = viewModel()을 Activity에서 한 번만 호출하고, 모든 Composable에 명시적으로 넘겨주세요. --- .../java/com/teampatch/harmony/MainNavHost.kt | 20 +++++++------ .../daily/certify/DailyCertifyNavigation.kt | 4 +++ .../daily/certify/DailyCertifyScreen.kt | 30 +++++++++++-------- .../daily/certify/EditCertifyActivity.kt | 14 ++++----- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index 7a91b73c..5d90712f 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -10,6 +10,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.teampatch.core.common.findActivity +import com.teampatch.daily.certify.DailyCertifyViewModel import com.teampatch.daily.certify.addDailyCertifyAlarmScreen import com.teampatch.daily.certify.addDailyCertifyDetailScreen import com.teampatch.daily.certify.addDailyCertifyScreen @@ -246,15 +247,16 @@ fun MainNavHost( fromNotification = true ) - addDailyCertifyScreen( - onBackRequest = navController::navigateUp, - onCertifyCompleteRequest = { navController.navigateToDailyCertifyDetailScreen() }, - onNavigateToDetailRequest = { navController.navigateToDailyCertifyDetailScreen() } - ) - - addDailyCertifyDetailScreen( - onBackRequest = navController::navigateUp - ) +// addDailyCertifyScreen( +// viewModel = DailyCertifyViewModel(), +// onBackRequest = navController::navigateUp, +// onCertifyCompleteRequest = { navController.navigateToDailyCertifyDetailScreen() }, +// onNavigateToDetailRequest = { navController.navigateToDailyCertifyDetailScreen() } +// ) +// +// addDailyCertifyDetailScreen( +// onBackRequest = navController::navigateUp +// ) addMemoryStorageDetailScreen( onBackRequest = navController::navigateUp, 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 index b7e4927d..cf6dd82f 100644 --- 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 @@ -42,12 +42,14 @@ fun NavController.navigateToDailyCertifyScreen( } fun NavGraphBuilder.addDailyCertifyScreen( + viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, onCertifyCompleteRequest: () -> Unit, onNavigateToDetailRequest: () -> Unit, ) { composable { DailyCertifyRoute( + viewModel = viewModel, onBackRequest = onBackRequest, onCertifyCompleteRequest = onCertifyCompleteRequest, onNavigateToDetailRequest = onNavigateToDetailRequest @@ -66,10 +68,12 @@ fun NavController.navigateToDailyCertifyDetailScreen( } fun NavGraphBuilder.addDailyCertifyDetailScreen( + viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, ) { composable { DailyCertifyDetailRoute( + viewModel = viewModel, onBackRequest = onBackRequest ) } 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 index d3bee015..17689170 100644 --- 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 @@ -39,6 +39,7 @@ 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 @@ -75,11 +76,12 @@ import java.time.format.DateTimeFormatter @Composable internal fun DailyCertifyRoute( + viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, onCertifyCompleteRequest: () -> Unit, onNavigateToDetailRequest: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 ) { - val viewModel: DailyCertifyViewModel = hiltViewModel() +// val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState val launcher = rememberLauncherForActivityResult( @@ -91,38 +93,40 @@ internal fun DailyCertifyRoute( } ) + val hasNavigated = rememberSaveable { mutableStateOf(false) } + LaunchedEffect(uiState.certifyStatus) { - if (uiState.certifyStatus == CertifyStatus.CONFIRMED) { - onNavigateToDetailRequest() // ✅ 상태 변경 시 자동 이동 + if (uiState.certifyStatus == CertifyStatus.CONFIRMED && !hasNavigated.value) { + hasNavigated.value = true + onNavigateToDetailRequest() } } DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, - onCommentEditRequest = { }, + onCommentEditRequest = { viewModel.editComment(it) }, onCertifyCompleteRequest = onCertifyCompleteRequest, - onOpenCommentSheet = { }, - onImagePickRequest = { - launcher.launch("image/*") - } + onOpenCommentSheet = { viewModel.openCommentSheet() }, + onImagePickRequest = { launcher.launch("image/*") } ) } @Composable fun DailyCertifyDetailRoute( + viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, ) { - val viewModel: DailyCertifyViewModel = hiltViewModel() +// val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, - onCommentEditRequest = { viewModel.completeCertify() }, - onCertifyCompleteRequest = {}, // 완료 상태이므로 버튼 없음 + onCommentEditRequest = { viewModel.editComment(it) }, + onCertifyCompleteRequest = {}, // ✅ 완료 버튼 제거 onOpenCommentSheet = { viewModel.openCommentSheet() }, - onImagePickRequest = { } + onImagePickRequest = { } // ✅ 이미지 수정 불가 ) } @@ -176,7 +180,7 @@ internal fun DailyCertifyScreen( when { !isImageUploaded -> { DefaultButton( - onClick = {}, + onClick = onCertifyCompleteRequest, enabled = false, modifier = Modifier .fillMaxWidth() 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 index e7e546b9..62934d6c 100644 --- 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 @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController @@ -20,6 +21,8 @@ class EditCertifyActivity : ComponentActivity() { setContent { HarmonyTheme { val navController = rememberNavController() + val viewModel: DailyCertifyViewModel = viewModel() // ✅ Activity 범위에서 생성 + NavHost( navController = navController, @@ -37,22 +40,19 @@ class EditCertifyActivity : ComponentActivity() { // Type-safe composable 추가 composable { - val viewModel: DailyCertifyViewModel = hiltViewModel() - DailyCertifyRoute( + viewModel = viewModel, onBackRequest = { navController.popBackStack() }, - onCertifyCompleteRequest = { - viewModel.completeCertify() // ✅ 인증 완료 처리 - }, + onCertifyCompleteRequest = { viewModel.completeCertify() }, onNavigateToDetailRequest = { - navController.navigate(DailyCertifyDetailScreenRoute) // ✅ 상세화면 이동 + navController.navigate(DailyCertifyDetailScreenRoute) } ) } - // ✅ 여기에 이거 추가: composable { DailyCertifyDetailRoute( + viewModel = viewModel, onBackRequest = { navController.popBackStack() } ) } From ffefeb38fba739a317d7a9c6847d91164e4777b9 Mon Sep 17 00:00:00 2001 From: theBettor Date: Wed, 28 May 2025 03:18:56 +0900 Subject: [PATCH 22/34] fix: lint #182 --- app/src/main/java/com/teampatch/harmony/MainNavHost.kt | 4 ---- .../java/com/teampatch/daily/certify/DailyCertifyScreen.kt | 1 - .../java/com/teampatch/daily/certify/EditCertifyActivity.kt | 2 -- 3 files changed, 7 deletions(-) diff --git a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt index 5d90712f..14546588 100644 --- a/app/src/main/java/com/teampatch/harmony/MainNavHost.kt +++ b/app/src/main/java/com/teampatch/harmony/MainNavHost.kt @@ -10,11 +10,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.teampatch.core.common.findActivity -import com.teampatch.daily.certify.DailyCertifyViewModel import com.teampatch.daily.certify.addDailyCertifyAlarmScreen -import com.teampatch.daily.certify.addDailyCertifyDetailScreen -import com.teampatch.daily.certify.addDailyCertifyScreen -import com.teampatch.daily.certify.navigateToDailyCertifyDetailScreen import com.teampatch.daily.certify.navigateToDailyCertifyScreen import com.teampatch.feature.answer.navigateToAnswerScreen import com.teampatch.feature.daily.edit.addDailyEditScreen 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 index 17689170..072212f3 100644 --- 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 @@ -51,7 +51,6 @@ 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 androidx.hilt.navigation.compose.hiltViewModel import coil.compose.rememberAsyncImagePainter import com.teampatch.core.designsystem.R.drawable.ic_camera_profile import com.teampatch.core.designsystem.R.drawable.ic_more_question 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 index 62934d6c..362999f3 100644 --- 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 @@ -3,7 +3,6 @@ package com.teampatch.daily.certify import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -23,7 +22,6 @@ class EditCertifyActivity : ComponentActivity() { val navController = rememberNavController() val viewModel: DailyCertifyViewModel = viewModel() // ✅ Activity 범위에서 생성 - NavHost( navController = navController, startDestination = "daily_alarm" From 44ba8188d689ae2d384908cf97b86566990f97b0 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 00:26:09 +0900 Subject: [PATCH 23/34] =?UTF-8?q?fix=20:=20=EC=A0=91=EA=B7=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=96=B4=EC=9E=90=EB=A5=BC=20internal=EC=97=90=EC=84=9C=20publ?= =?UTF-8?q?ic=EC=9C=BC=EB=A1=9C=20=EC=88=98=EC=A0=95=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyUiState.kt | 2 +- .../daily/certify/DailyCertifyViewModel.kt | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) 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 index 435777d4..ef5cab94 100644 --- 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 @@ -3,7 +3,7 @@ package com.teampatch.daily.certify import com.teampatch.core.domain.model.DailyComment import java.time.LocalTime -internal data class DailyCertifyUiState( +data class DailyCertifyUiState( val missionText: String = "", val missionTime: LocalTime? = null, val imageUrl: String? = null, // (이미지가 URL인 경우) 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 index bd00cb02..e35fd0a5 100644 --- 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 @@ -9,7 +9,7 @@ import java.time.LocalTime import javax.inject.Inject @HiltViewModel -internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { +class DailyCertifyViewModel @Inject constructor() : ViewModel() { private val _uiState = mutableStateOf( DailyCertifyUiState( missionText = "오늘의 인증 미션", @@ -46,6 +46,19 @@ internal class DailyCertifyViewModel @Inject constructor() : ViewModel() { // TODO: 인증 완료 종료 시 처리할 것 } + fun addComment(comment: DailyComment) { + _uiState.value = _uiState.value.copy( + comments = _uiState.value.comments + comment, + showCommentSheet = false + ) + } + + fun deleteComment(commentId: String) { + _uiState.value = _uiState.value.copy( + comments = _uiState.value.comments.filterNot { it.commentId == commentId } + ) + } + fun updateImage(uri: String) { _uiState.value = _uiState.value.copy(imageUrl = uri) } From 7b1c8fe2d26f902a7acee2e8d6a2ac14656ac0ae Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 00:26:24 +0900 Subject: [PATCH 24/34] =?UTF-8?q?feat:=20BottomSheet=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 119 +++++++++++++++++- 1 file changed, 117 insertions(+), 2 deletions(-) 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 index 072212f3..a2472be7 100644 --- 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 @@ -1,6 +1,7 @@ package com.teampatch.daily.certify import android.net.Uri +import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility @@ -30,8 +31,10 @@ 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 @@ -80,7 +83,6 @@ internal fun DailyCertifyRoute( onCertifyCompleteRequest: () -> Unit, onNavigateToDetailRequest: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 ) { -// val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState val launcher = rememberLauncherForActivityResult( @@ -111,14 +113,24 @@ internal fun DailyCertifyRoute( ) } +@ExperimentalMaterial3Api @Composable fun DailyCertifyDetailRoute( viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, ) { -// val viewModel: DailyCertifyViewModel = hiltViewModel() val uiState by viewModel.uiState + val commentWriteSheetState = rememberModalBottomSheetState() + + LaunchedEffect(uiState.showCommentSheet) { + if (uiState.showCommentSheet) { + commentWriteSheetState.show() + } else { + commentWriteSheetState.hide() + } + } + DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, @@ -151,6 +163,7 @@ internal fun DailyCertifyScreen( } LaunchedEffect(uiState.editingComment) { + Log.d("DEBUG", "Editing comment: ${uiState.editingComment}") if (uiState.editingComment != null) { commentEditSheetState.show() } else { @@ -265,6 +278,45 @@ internal fun DailyCertifyScreen( } } + // ✅ 댓글 작성 BottomSheet + if (uiState.showCommentSheet) { + ModalBottomSheet( + onDismissRequest = { + // 닫기 요청 → ViewModel에서 상태 초기화 + onCommentEditRequest(null) + }, + sheetState = commentWriteSheetState + ) { + CommentWriteSheetContent( + onSubmit = { text -> + // TODO: ViewModel에 댓글 추가 메서드 연결 + // viewModel.addComment(...) + }, + onDismiss = { onCommentEditRequest(null) } + ) + } + } + +// ✅ 댓글 수정 BottomSheet + uiState.editingComment?.let { editingComment -> + ModalBottomSheet( + onDismissRequest = { + onCommentEditRequest(null) + }, + sheetState = commentEditSheetState + ) { + CommentEditSheetContent( + initialText = editingComment.content, + onSubmit = { updatedText -> + // TODO: ViewModel에 댓글 수정 메서드 연결 + // viewModel.updateComment(editingComment.commentId, updatedText) + onCommentEditRequest(null) + }, + onDismiss = { onCommentEditRequest(null) } + ) + } + } + // ✅ 댓글 남기기 UI (Floating UI) if (isCertifyConfirmed) { val shouldShowFloatingCommentButton = isCertifyConfirmed && isFloatingVisible @@ -455,6 +507,69 @@ fun DailyCertifyCommentItem( } } +@Composable +fun CommentWriteSheetContent( + onSubmit: (String) -> Unit, + onDismiss: () -> Unit, +) { + 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)) + 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, + onDismiss: () -> 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() { From 472bc2af517cf115bfa5fe173049aab716126237 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 01:22:53 +0900 Subject: [PATCH 25/34] =?UTF-8?q?refactor:=20=ED=8C=8C=EB=9D=BC=EB=AF=B8?= =?UTF-8?q?=ED=84=B0=203=EA=B0=9C=20=EC=B6=94=EA=B0=80,=20BottomSheet?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=EC=97=B4=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=ED=8A=B8=EB=A6=AC=EA=B1=B0=20=EC=B6=94=EA=B0=80=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 댓글 작성·수정·닫기 핸들러 파라미터 추가로 ViewModel 로직 분리 - onCommentWrite, onCommentEditSubmit, onBottomSheetDismiss 파라미터 추가 - Route에서 ViewModel 로직 처리, Screen은 UI 렌더링에 집중하도록 분리 2. BottomSheet dismiss 후 상태 재호출이 불가능한 문제 수정 - sheetState.isVisible과 ViewModel 상태 간 불일치 감지해 dismiss 처리 - dismiss 이후에도 다시 ViewModel 호출로 BottomSheet를 재진입 가능하게 개선 --- .../daily/certify/DailyCertifyNavigation.kt | 2 + .../daily/certify/DailyCertifyScreen.kt | 75 ++++++++++--------- .../daily/certify/DailyCertifyViewModel.kt | 26 ++++++- .../daily/certify/EditCertifyActivity.kt | 2 + 4 files changed, 67 insertions(+), 38 deletions(-) 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 index cf6dd82f..2e262eee 100644 --- 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 @@ -1,5 +1,6 @@ package com.teampatch.daily.certify +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions @@ -67,6 +68,7 @@ fun NavController.navigateToDailyCertifyDetailScreen( navigate(DailyCertifyDetailScreenRoute, navOptions, navigatorExtras) } +@OptIn(ExperimentalMaterial3Api::class) fun NavGraphBuilder.addDailyCertifyDetailScreen( viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, 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 index a2472be7..919d14b9 100644 --- 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 @@ -1,7 +1,6 @@ package com.teampatch.daily.certify import android.net.Uri -import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility @@ -109,7 +108,10 @@ internal fun DailyCertifyRoute( onCommentEditRequest = { viewModel.editComment(it) }, onCertifyCompleteRequest = onCertifyCompleteRequest, onOpenCommentSheet = { viewModel.openCommentSheet() }, - onImagePickRequest = { launcher.launch("image/*") } + onImagePickRequest = { launcher.launch("image/*") }, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, + onBottomSheetDismiss = {} ) } @@ -121,28 +123,23 @@ fun DailyCertifyDetailRoute( ) { val uiState by viewModel.uiState - val commentWriteSheetState = rememberModalBottomSheetState() - - LaunchedEffect(uiState.showCommentSheet) { - if (uiState.showCommentSheet) { - commentWriteSheetState.show() - } else { - commentWriteSheetState.hide() - } - } - DailyCertifyScreen( uiState = uiState, onBackRequest = onBackRequest, onCommentEditRequest = { viewModel.editComment(it) }, onCertifyCompleteRequest = {}, // ✅ 완료 버튼 제거 onOpenCommentSheet = { viewModel.openCommentSheet() }, - onImagePickRequest = { } // ✅ 이미지 수정 불가 + onImagePickRequest = { }, // ✅ 이미지 수정 불가 + + // ✅ 추가 + onCommentWrite = { viewModel.addComment(it) }, + onCommentEditSubmit = { id, content -> viewModel.updateComment(id, content) }, + onBottomSheetDismiss = { viewModel.closeCommentSheet() } ) } -@Composable @OptIn(ExperimentalMaterial3Api::class) +@Composable internal fun DailyCertifyScreen( uiState: DailyCertifyUiState, onBackRequest: () -> Unit, @@ -150,6 +147,9 @@ internal fun DailyCertifyScreen( onCertifyCompleteRequest: () -> Unit, onOpenCommentSheet: () -> Unit, onImagePickRequest: () -> Unit, // ✅ 추가 + onCommentWrite: (String) -> Unit, // ✅ + onCommentEditSubmit: (String, String) -> Unit, // ✅ (commentId, content) + onBottomSheetDismiss: () -> Unit, // ✅ ) { val commentEditSheetState = rememberModalBottomSheetState() val commentWriteSheetState = rememberModalBottomSheetState() @@ -162,20 +162,16 @@ internal fun DailyCertifyScreen( } } - LaunchedEffect(uiState.editingComment) { - Log.d("DEBUG", "Editing comment: ${uiState.editingComment}") - if (uiState.editingComment != null) { - commentEditSheetState.show() - } else { - commentEditSheetState.hide() + LaunchedEffect(commentWriteSheetState.isVisible) { + if (!commentWriteSheetState.isVisible && uiState.showCommentSheet) { + // 사용자가 dismiss해서 꺼졌지만 상태는 true인 경우 → 상태도 false로 정리 + onBottomSheetDismiss() } } - LaunchedEffect(uiState.showCommentSheet) { - if (uiState.showCommentSheet) { - commentWriteSheetState.show() - } else { - commentWriteSheetState.hide() + LaunchedEffect(commentEditSheetState.isVisible) { + if (!commentEditSheetState.isVisible && uiState.editingComment != null) { + onBottomSheetDismiss() } } @@ -289,10 +285,10 @@ internal fun DailyCertifyScreen( ) { CommentWriteSheetContent( onSubmit = { text -> - // TODO: ViewModel에 댓글 추가 메서드 연결 - // viewModel.addComment(...) + onCommentWrite(text) + onBottomSheetDismiss() }, - onDismiss = { onCommentEditRequest(null) } + onDismiss = { onBottomSheetDismiss() } ) } } @@ -308,11 +304,10 @@ internal fun DailyCertifyScreen( CommentEditSheetContent( initialText = editingComment.content, onSubmit = { updatedText -> - // TODO: ViewModel에 댓글 수정 메서드 연결 - // viewModel.updateComment(editingComment.commentId, updatedText) - onCommentEditRequest(null) + onCommentEditSubmit(editingComment.commentId, updatedText) + onBottomSheetDismiss() }, - onDismiss = { onCommentEditRequest(null) } + onDismiss = { onBottomSheetDismiss() } ) } } @@ -587,7 +582,10 @@ private fun DailyCertifyScreenPreview_Initial() { onCommentEditRequest = {}, onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, - onImagePickRequest = {} + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onBottomSheetDismiss = {} // ✅ ) } } @@ -610,8 +608,10 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { onCommentEditRequest = {}, onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, - onImagePickRequest = {} - + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onBottomSheetDismiss = {} // ✅ ) } } @@ -637,7 +637,10 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { onCommentEditRequest = {}, onCertifyCompleteRequest = {}, onOpenCommentSheet = {}, - onImagePickRequest = {} + onImagePickRequest = {}, + onCommentWrite = {}, + onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onBottomSheetDismiss = {} // ✅ ) } } \ 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 index e35fd0a5..fc7c487f 100644 --- 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 @@ -6,6 +6,7 @@ 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 @@ -46,13 +47,34 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { // TODO: 인증 완료 종료 시 처리할 것 } - fun addComment(comment: DailyComment) { + fun addComment(text: String) { _uiState.value = _uiState.value.copy( - comments = _uiState.value.comments + comment, + comments = _uiState.value.comments + DailyComment( + commentId = UUID.randomUUID().toString(), + writerUid = "", + writerName = "유저이름", + content = text + ), 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 deleteComment(commentId: String) { _uiState.value = _uiState.value.copy( comments = _uiState.value.comments.filterNot { it.commentId == commentId } 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 index 362999f3..c6e97cb2 100644 --- 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 @@ -3,6 +3,7 @@ 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 @@ -12,6 +13,7 @@ import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class EditCertifyActivity : ComponentActivity() { + @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) From 6b114bc260f09767cc1f5c0c2ce7ab9bcdb37d16 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 01:37:31 +0900 Subject: [PATCH 26/34] =?UTF-8?q?feat:=20BottomSheet=EC=97=90=20photopicke?= =?UTF-8?q?r=20=EB=84=A3=EC=9D=84=20=EC=88=98=20=EC=9E=88=EA=B2=8C=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 index 919d14b9..cb9f06ce 100644 --- 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 @@ -278,8 +278,7 @@ internal fun DailyCertifyScreen( if (uiState.showCommentSheet) { ModalBottomSheet( onDismissRequest = { - // 닫기 요청 → ViewModel에서 상태 초기화 - onCommentEditRequest(null) + onBottomSheetDismiss() }, sheetState = commentWriteSheetState ) { @@ -288,7 +287,8 @@ internal fun DailyCertifyScreen( onCommentWrite(text) onBottomSheetDismiss() }, - onDismiss = { onBottomSheetDismiss() } + onImagePick = onImagePickRequest, // ✅ 포토피커 트리거 + imageUrl = uiState.imageUrl // ✅ 현재 이미지 ) } } @@ -505,7 +505,8 @@ fun DailyCertifyCommentItem( @Composable fun CommentWriteSheetContent( onSubmit: (String) -> Unit, - onDismiss: () -> Unit, + onImagePick: () -> Unit, // ✅ 추가 + imageUrl: String?, // ✅ 현재 이미지 표시 ) { var comment by remember { mutableStateOf("") } @@ -516,6 +517,13 @@ fun CommentWriteSheetContent( ) { 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 }, From 39feb6a4964d6f5cc9d186942dd1680f172db381 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 02:29:42 +0900 Subject: [PATCH 27/34] =?UTF-8?q?feat:=20=EB=B0=94=ED=85=80=EC=8B=9C?= =?UTF-8?q?=ED=8A=B8=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Screen에 파라미터 만들고, remember 변수도 만들고 --- .../core/domain/model/DailyComment.kt | 1 + .../daily/certify/DailyCertifyScreen.kt | 45 ++++++++++++++----- .../daily/certify/DailyCertifyViewModel.kt | 16 ++++--- 3 files changed, 43 insertions(+), 19 deletions(-) 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..d929f7de 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,5 @@ data class DailyComment( val writerUid: String, val writerName: String, val content: String, + val imageUrl: String? = null, ) \ 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 index cb9f06ce..c71ebdea 100644 --- 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 @@ -111,7 +111,8 @@ internal fun DailyCertifyRoute( onImagePickRequest = { launcher.launch("image/*") }, onCommentWrite = {}, onCommentEditSubmit = { _, _ -> }, - onBottomSheetDismiss = {} + onBottomSheetDismiss = {}, + commentImageUri = null ) } @@ -123,18 +124,34 @@ fun DailyCertifyDetailRoute( ) { 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 = { viewModel.openCommentSheet() }, - onImagePickRequest = { }, // ✅ 이미지 수정 불가 - - // ✅ 추가 - onCommentWrite = { viewModel.addComment(it) }, + 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() } + onBottomSheetDismiss = { + viewModel.closeCommentSheet() + commentImageUri.value = null + }, + commentImageUri = commentImageUri.value ) } @@ -150,6 +167,7 @@ internal fun DailyCertifyScreen( onCommentWrite: (String) -> Unit, // ✅ onCommentEditSubmit: (String, String) -> Unit, // ✅ (commentId, content) onBottomSheetDismiss: () -> Unit, // ✅ + commentImageUri: String?, // ✅ 댓글 작성용 임시 이미지 상태 ) { val commentEditSheetState = rememberModalBottomSheetState() val commentWriteSheetState = rememberModalBottomSheetState() @@ -287,8 +305,8 @@ internal fun DailyCertifyScreen( onCommentWrite(text) onBottomSheetDismiss() }, - onImagePick = onImagePickRequest, // ✅ 포토피커 트리거 - imageUrl = uiState.imageUrl // ✅ 현재 이미지 + onImagePick = onImagePickRequest, // ✅ 포토피커 트리거 + imageUrl = commentImageUri ) } } @@ -593,7 +611,8 @@ private fun DailyCertifyScreenPreview_Initial() { onImagePickRequest = {}, onCommentWrite = {}, onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) - onBottomSheetDismiss = {} // ✅ + onBottomSheetDismiss = {}, + commentImageUri = null ) } } @@ -619,7 +638,8 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { onImagePickRequest = {}, onCommentWrite = {}, onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) - onBottomSheetDismiss = {} // ✅ + onBottomSheetDismiss = {}, + commentImageUri = null ) } } @@ -648,7 +668,8 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { onImagePickRequest = {}, onCommentWrite = {}, onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) - onBottomSheetDismiss = {} // ✅ + onBottomSheetDismiss = {}, + commentImageUri = null ) } } \ 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 index fc7c487f..49f6cf08 100644 --- 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 @@ -47,14 +47,16 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { // TODO: 인증 완료 종료 시 처리할 것 } - fun addComment(text: String) { + fun addComment(content: String, imageUrl: String?) { + val newComment = DailyComment( + commentId = UUID.randomUUID().toString(), + writerName = "작성자", + writerUid = "", + content = content, + imageUrl = imageUrl + ) _uiState.value = _uiState.value.copy( - comments = _uiState.value.comments + DailyComment( - commentId = UUID.randomUUID().toString(), - writerUid = "", - writerName = "유저이름", - content = text - ), + comments = _uiState.value.comments + newComment, showCommentSheet = false ) } From 1c44cccfefc690d284c782a5f7413ea6a05ef098 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 02:36:03 +0900 Subject: [PATCH 28/34] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=99=84=EB=A3=8C=EB=90=98=EB=A9=B4=20Item?= =?UTF-8?q?=EC=97=90=20=EB=B0=94=EB=A1=9C=20=EC=A0=81=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EA=B8=B0=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/teampatch/core/domain/model/DailyComment.kt | 1 + .../teampatch/daily/certify/DailyCertifyScreen.kt | 12 +++++++++--- .../teampatch/daily/certify/DailyCertifyViewModel.kt | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) 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 d929f7de..f6959819 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 @@ -6,4 +6,5 @@ data class DailyComment( val writerName: String, val content: String, val imageUrl: String? = null, + val profileImageUrl: String? = null ) \ 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 index c71ebdea..5e0e1afe 100644 --- 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 @@ -24,6 +24,7 @@ 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 @@ -45,6 +46,7 @@ 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 @@ -460,10 +462,14 @@ fun DailyCertifyCommentItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - // TODO: 실제 사용자 프로필 이미지 연결 필요 Image( - painter = painterResource(ic_my_appbar), - contentDescription = "user profile" + painter = comment.profileImageUrl?.let { + rememberAsyncImagePainter(it) + } ?: painterResource(ic_my_appbar), + contentDescription = "user profile", + modifier = Modifier + .size(40.dp) + .clip(CircleShape) ) Text( 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 index 49f6cf08..39d3d9ff 100644 --- 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 @@ -53,7 +53,8 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { writerName = "작성자", writerUid = "", content = content, - imageUrl = imageUrl + imageUrl = imageUrl, + profileImageUrl = imageUrl // ✅ 첨부한 이미지를 프로필로도 활용 ) _uiState.value = _uiState.value.copy( comments = _uiState.value.comments + newComment, From 824c11ec898904154d0bed4010972ccb90cf32c4 Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 02:55:22 +0900 Subject: [PATCH 29/34] =?UTF-8?q?feat:=20=EB=8C=93=EA=B8=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80,=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=EC=A0=9C=EA=B1=B0=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/certify/DailyCertifyScreen.kt | 59 ++++++++++--------- .../daily/certify/DailyCertifyViewModel.kt | 13 ++-- 2 files changed, 38 insertions(+), 34 deletions(-) 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 index 5e0e1afe..a3edf612 100644 --- 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 @@ -82,7 +82,7 @@ internal fun DailyCertifyRoute( viewModel: DailyCertifyViewModel, onBackRequest: () -> Unit, onCertifyCompleteRequest: () -> Unit, - onNavigateToDetailRequest: () -> Unit, // ✅ 추가: 인증 완료 시 이동할 상세 화면 + onNavigateToDetailRequest: () -> Unit, ) { val uiState by viewModel.uiState @@ -114,7 +114,8 @@ internal fun DailyCertifyRoute( onCommentWrite = {}, onCommentEditSubmit = { _, _ -> }, onBottomSheetDismiss = {}, - commentImageUri = null + commentImageUri = null, + onCommentDeleteRequest = {} ) } @@ -141,7 +142,7 @@ fun DailyCertifyDetailRoute( uiState = uiState, onBackRequest = onBackRequest, onCommentEditRequest = { viewModel.editComment(it) }, - onCertifyCompleteRequest = {}, // ✅ 완료 버튼 제거 + onCertifyCompleteRequest = {}, onOpenCommentSheet = { commentImageUri.value = null viewModel.openCommentSheet() @@ -153,7 +154,8 @@ fun DailyCertifyDetailRoute( viewModel.closeCommentSheet() commentImageUri.value = null }, - commentImageUri = commentImageUri.value + commentImageUri = commentImageUri.value, + onCommentDeleteRequest = {viewModel.deleteComment(it)} ) } @@ -163,13 +165,14 @@ 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, // ✅ (commentId, content) - onBottomSheetDismiss: () -> Unit, // ✅ - commentImageUri: String?, // ✅ 댓글 작성용 임시 이미지 상태 + onImagePickRequest: () -> Unit, + onCommentWrite: (String) -> Unit, + onCommentEditSubmit: (String, String) -> Unit, + onBottomSheetDismiss: () -> Unit, + commentImageUri: String?, ) { val commentEditSheetState = rememberModalBottomSheetState() val commentWriteSheetState = rememberModalBottomSheetState() @@ -184,7 +187,6 @@ internal fun DailyCertifyScreen( LaunchedEffect(commentWriteSheetState.isVisible) { if (!commentWriteSheetState.isVisible && uiState.showCommentSheet) { - // 사용자가 dismiss해서 꺼졌지만 상태는 true인 경우 → 상태도 false로 정리 onBottomSheetDismiss() } } @@ -262,13 +264,13 @@ internal fun DailyCertifyScreen( if (isCertifyConfirmed) { LazyColumn( - state = lazyListState, // ✅ 상태 연결 + state = lazyListState, modifier = Modifier .fillMaxSize() .background(G1), contentPadding = PaddingValues( top = 8.dp, - bottom = 120.dp // ✅ 댓글 남기기 UI를 위한 padding + bottom = 120.dp // 댓글 남기기 UI를 위한 padding ) ) { item { @@ -287,14 +289,13 @@ internal fun DailyCertifyScreen( DailyCertifyCommentItem( comment = comment, onEditClick = { onCommentEditRequest(comment) }, - onDeleteClick = { /* TODO */ } + onDeleteClick = { onCommentDeleteRequest(comment) } ) } } } } - // ✅ 댓글 작성 BottomSheet if (uiState.showCommentSheet) { ModalBottomSheet( onDismissRequest = { @@ -307,13 +308,12 @@ internal fun DailyCertifyScreen( onCommentWrite(text) onBottomSheetDismiss() }, - onImagePick = onImagePickRequest, // ✅ 포토피커 트리거 + onImagePick = onImagePickRequest, imageUrl = commentImageUri ) } } -// ✅ 댓글 수정 BottomSheet uiState.editingComment?.let { editingComment -> ModalBottomSheet( onDismissRequest = { @@ -327,12 +327,11 @@ internal fun DailyCertifyScreen( onCommentEditSubmit(editingComment.commentId, updatedText) onBottomSheetDismiss() }, - onDismiss = { onBottomSheetDismiss() } ) } } - // ✅ 댓글 남기기 UI (Floating UI) + // 댓글 남기기 UI (Floating UI) if (isCertifyConfirmed) { val shouldShowFloatingCommentButton = isCertifyConfirmed && isFloatingVisible // IDE에서 always true로 추론하는 건 Preview 상의 오해임 – 런타임에는 유동적 @@ -371,7 +370,7 @@ private fun MissionInfoSection( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), - horizontalAlignment = Alignment.CenterHorizontally // ✅ 가운데 정렬 핵심 + horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = missionText, @@ -529,8 +528,8 @@ fun DailyCertifyCommentItem( @Composable fun CommentWriteSheetContent( onSubmit: (String) -> Unit, - onImagePick: () -> Unit, // ✅ 추가 - imageUrl: String?, // ✅ 현재 이미지 표시 + onImagePick: () -> Unit, + imageUrl: String?, ) { var comment by remember { mutableStateOf("") } @@ -569,7 +568,6 @@ fun CommentWriteSheetContent( fun CommentEditSheetContent( initialText: String, onSubmit: (String) -> Unit, - onDismiss: () -> Unit, ) { var comment by remember { mutableStateOf(initialText) } @@ -616,9 +614,10 @@ private fun DailyCertifyScreenPreview_Initial() { onOpenCommentSheet = {}, onImagePickRequest = {}, onCommentWrite = {}, - onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onCommentEditSubmit = { _, _ -> }, onBottomSheetDismiss = {}, - commentImageUri = null + commentImageUri = null, + onCommentDeleteRequest = {} ) } } @@ -643,9 +642,10 @@ private fun DailyCertifyScreenPreview_Completed_NoComment() { onOpenCommentSheet = {}, onImagePickRequest = {}, onCommentWrite = {}, - onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onCommentEditSubmit = { _, _ -> }, onBottomSheetDismiss = {}, - commentImageUri = null + commentImageUri = null, + onCommentDeleteRequest = {} ) } } @@ -664,7 +664,7 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { DailyComment("2", "김소라", "씹희", "부산 날씨 완전 봄이야") ), editingComment = null, - certifyStatus = CertifyStatus.CONFIRMED // ✅ 이걸 넣어야 댓글 목록이 나타남! + certifyStatus = CertifyStatus.CONFIRMED // 이걸 넣어야 댓글 목록이 나타남! ), onBackRequest = {}, @@ -673,9 +673,10 @@ private fun DailyCertifyScreenPreview_Completed_WithComment() { onOpenCommentSheet = {}, onImagePickRequest = {}, onCommentWrite = {}, - onCommentEditSubmit = { _, _ -> }, // ✅ (commentId, content) + onCommentEditSubmit = { _, _ -> }, onBottomSheetDismiss = {}, - commentImageUri = null + commentImageUri = null, + onCommentDeleteRequest = {} ) } } \ 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 index 39d3d9ff..466255a4 100644 --- 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 @@ -37,6 +37,12 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { ) } + 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 @@ -62,6 +68,7 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { ) } + fun updateComment(commentId: String, newContent: String) { _uiState.value = _uiState.value.copy( comments = _uiState.value.comments.map { @@ -78,11 +85,7 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { ) } - fun deleteComment(commentId: String) { - _uiState.value = _uiState.value.copy( - comments = _uiState.value.comments.filterNot { it.commentId == commentId } - ) - } + fun updateImage(uri: String) { _uiState.value = _uiState.value.copy(imageUrl = uri) From eb0e8d007b045476cb8a8c69836372654c35c6cb Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 02:59:09 +0900 Subject: [PATCH 30/34] fix: lint --- .../main/java/com/teampatch/core/domain/model/DailyComment.kt | 2 +- .../java/com/teampatch/daily/certify/DailyCertifyScreen.kt | 4 ++-- .../java/com/teampatch/daily/certify/DailyCertifyViewModel.kt | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) 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 f6959819..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 @@ -6,5 +6,5 @@ data class DailyComment( val writerName: String, val content: String, val imageUrl: String? = null, - val profileImageUrl: String? = null + val profileImageUrl: String? = null, ) \ 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 index a3edf612..59ae544c 100644 --- 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 @@ -155,7 +155,7 @@ fun DailyCertifyDetailRoute( commentImageUri.value = null }, commentImageUri = commentImageUri.value, - onCommentDeleteRequest = {viewModel.deleteComment(it)} + onCommentDeleteRequest = { viewModel.deleteComment(it) } ) } @@ -326,7 +326,7 @@ internal fun DailyCertifyScreen( onSubmit = { updatedText -> onCommentEditSubmit(editingComment.commentId, updatedText) onBottomSheetDismiss() - }, + } ) } } 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 index 466255a4..eb379d3b 100644 --- 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 @@ -68,7 +68,6 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { ) } - fun updateComment(commentId: String, newContent: String) { _uiState.value = _uiState.value.copy( comments = _uiState.value.comments.map { @@ -85,8 +84,6 @@ class DailyCertifyViewModel @Inject constructor() : ViewModel() { ) } - - fun updateImage(uri: String) { _uiState.value = _uiState.value.copy(imageUrl = uri) } From cfd5115bc8063e5fbf3cc50f6740882253375f1b Mon Sep 17 00:00:00 2001 From: theBettor Date: Fri, 30 May 2025 17:41:32 +0900 Subject: [PATCH 31/34] =?UTF-8?q?feat:=20workmanager=EB=A5=BC=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=EB=B2=84=ED=8A=BC=EC=9D=84=20=EB=88=84=EB=A5=BC=20?= =?UTF-8?q?=EB=95=8C=EC=9D=98=20=EC=8B=9C=EC=A0=90=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20#182?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../teampatch/feature/daily/edit/DailyEditScreen.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 f2f28558..e6f2cefa 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 @@ -76,7 +76,6 @@ internal fun DailyEditRoute( { _, hour, minute -> val selectedTime = LocalTime.of(hour, minute) viewModel.changeSelectedTime(selectedTime) - viewModel.onTimeSelected(LocalDateTime.of(LocalDate.now(), selectedTime)) }, now.hour, now.minute, @@ -100,7 +99,10 @@ internal fun DailyEditRoute( if (!uiState.isLoading) { DailyEditScreen( onDismissRequest = onDismissRequest, - onCompleteRequest = onCompleteRequest, + onCompleteRequest = { todo -> + viewModel.onTimeSelected(todo.dateTime) + onCompleteRequest(todo) + }, selectedDays = uiState.selectedDays, onDaySelected = { viewModel.toggleSelectedDay(it) }, selectedTime = uiState.selectedTime, @@ -119,7 +121,6 @@ internal fun DailyEditScreen( selectedTime: LocalTime?, onTimePickRequest: () -> Unit, ) { - val context = LocalContext.current val textState = rememberSaveable { mutableStateOf("") } val daysOfWeek = remember { DayOfWeek.values() } @@ -148,10 +149,9 @@ internal fun DailyEditScreen( ?: LocalDateTime.now(), isFinished = false ) - Log.d("DEBUG", "1. DailyEditScreen: onCompleteRequest todo = $todo") onCompleteRequest(todo) }, - enabled = textState.value.isNotBlank(), + enabled = textState.value.isNotBlank() && selectedDays.isNotEmpty() && selectedTime != null, modifier = Modifier .fillMaxWidth() .padding(start = 20.dp, end = 20.dp, bottom = 8.dp) From 5317903f27314fc7d187a6d319b55a77c7cb71fe Mon Sep 17 00:00:00 2001 From: theBettor Date: Sun, 1 Jun 2025 00:34:06 +0900 Subject: [PATCH 32/34] =?UTF-8?q?refactor:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=ED=95=A8=EC=88=98=20=EC=A0=95=EB=A6=AC=20#184?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/daily/edit/DailyEditScreen.kt | 1 - .../feature/daily/edit/DailyEditViewModel.kt | 13 +-- feature/daily/build.gradle.kts | 1 + .../java/com/teampatch/harmony/DailyScreen.kt | 77 ++++++++++-------- .../com/teampatch/harmony/DailyViewModel.kt | 81 +++++++++++-------- .../harmony/model/DailyErrorHandler.kt | 5 ++ .../teampatch/harmony/model/DailyUiState.kt | 6 +- 7 files changed, 101 insertions(+), 83 deletions(-) create mode 100644 feature/daily/src/main/java/com/teampatch/harmony/model/DailyErrorHandler.kt 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 e6f2cefa..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 @@ -1,7 +1,6 @@ package com.teampatch.feature.daily.edit import android.app.TimePickerDialog -import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background 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 7da55316..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 @@ -16,9 +16,6 @@ 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, @@ -37,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, @@ -49,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 { @@ -64,7 +54,6 @@ internal class DailyEditViewModel @Inject constructor( ) } - // DailyEditViewModel에 추가 fun changeSelectedTime(time: LocalTime) { _dailyEditUiState.value = _dailyEditUiState.value.copy( selectedTime = time 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/DailyScreen.kt b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt index 3e8ab942..8fbbc1b7 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -1,5 +1,6 @@ package com.teampatch.harmony +import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -23,8 +24,10 @@ 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 @@ -39,6 +42,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -52,11 +56,9 @@ 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 @@ -66,22 +68,37 @@ internal fun DailyRoute( 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) { + Log.d("dailyRoutine", "itemCount=${dailyRoutine.itemCount}") + val isLoading = dailyRoutine.loadState.refresh is LoadState.Loading + + 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 + } + + if (!isLoading) { DailyScreen( - progress = 0f, - onDailyRoutineClick = {}, - onDailyRoutineCheckChanged = { _, _ -> }, - dailyRoutine = uiState.daily.collectAsLazyPagingItems(), + 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, - uiState = uiState + dailyEditPageRequest = dailyEditPageRequest ) } + LaunchedEffect(Unit) { - dailyViewModel.sideEffect.collect { sideEffect -> + viewModel.sideEffect.collect { sideEffect -> when (sideEffect) { is DailySideEffect.LoadError -> { Toast.makeText(context, "데이터를 불러오지 못하였습니다.", Toast.LENGTH_SHORT).show() @@ -93,14 +110,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, dailyEditPageRequest: () -> Unit, - uiState: DailyUiState, ) { Scaffold( topBar = { @@ -171,7 +185,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, @@ -198,27 +212,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)) @@ -230,11 +241,10 @@ internal fun DailyScreen( @Preview @Composable -private fun DailyManageScreenPreview() { +private fun DailyScreenPreview() { HarmonyTheme { DailyScreen( progress = 0f, - onDailyRoutineClick = {}, onDailyRoutineCheckChanged = { _, _ -> }, dailyRoutine = flowOf( PagingData.from( @@ -244,8 +254,7 @@ private fun DailyManageScreenPreview() { ) .collectAsLazyPagingItems(), dailyExpandPageRequest = { }, - dailyEditPageRequest = {}, - 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 4e6e0567..e03c5af9 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyViewModel.kt @@ -1,68 +1,85 @@ package com.teampatch.harmony -import android.util.Log 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.AddDailyRoutineUseCase 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 java.time.LocalDateTime 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 addDailyRoutineUseCase: AddDailyRoutineUseCase, + 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 상태를 기반으로 갱신 + ) + } } - 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() - } - } - - fun addDailyRoutine(id: String, title: String, time: LocalDateTime, isFinished: Boolean) { - viewModelScope.launch { - val input = Todo(title = title, dateTime = time, id = id, isFinished = isFinished) - Log.d("DEBUG", "DailyViewModel: addDailyRoutine input = $input") // ✅ 디버그 추가 - val result = addDailyRoutineUseCase(input) - if (result.isFailure) { - _sideEffect.send(DailySideEffect.LoadError(Exception("일과 추가 실패"))) - } else { - load() // 추가 후 다시 목록 갱신 - } + _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 From ca60eeaf86b5ed43b274514b70f3e80b4823978d Mon Sep 17 00:00:00 2001 From: theBettor Date: Sun, 1 Jun 2025 02:07:16 +0900 Subject: [PATCH 33/34] =?UTF-8?q?fix:=20=ED=99=94=EB=A9=B4=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EA=B2=8C=20if=EB=AC=B8=20=EC=A0=9C=EA=B1=B0=20#184?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/teampatch/harmony/DailyScreen.kt | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) 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 8fbbc1b7..240326ef 100644 --- a/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt +++ b/feature/daily/src/main/java/com/teampatch/harmony/DailyScreen.kt @@ -1,6 +1,5 @@ package com.teampatch.harmony -import android.util.Log import android.widget.Toast import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -42,7 +41,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import androidx.paging.LoadState import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -72,9 +70,6 @@ internal fun DailyRoute( val uiState by viewModel.dailyUiState.collectAsState() val dailyRoutine = uiState.dailyRoutine.collectAsLazyPagingItems() - Log.d("dailyRoutine", "itemCount=${dailyRoutine.itemCount}") - val isLoading = dailyRoutine.loadState.refresh is LoadState.Loading - val progress = remember(dailyRoutine.itemSnapshotList.items) { val items = dailyRoutine.itemSnapshotList.items val total = items.size @@ -82,20 +77,18 @@ internal fun DailyRoute( if (total == 0) 0f else done.toFloat() / total } - if (!isLoading) { - 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 - ) - } + 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) { viewModel.sideEffect.collect { sideEffect -> From 76e28e4de7c558efbfe9b116ffd6b35b15eb2b25 Mon Sep 17 00:00:00 2001 From: theBettor Date: Sun, 1 Jun 2025 03:17:32 +0900 Subject: [PATCH 34/34] =?UTF-8?q?refactor:=20expand=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=20DailyManage=EA=B0=80=20=EC=95=84=EB=8B=8C?= =?UTF-8?q?=20GetDailyRoutine=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20#185?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../daily/expand/DailyExpandNavigation.kt | 5 +- .../feature/daily/expand/DailyExpandScreen.kt | 156 +++++++++--------- .../daily/expand/DailyExpandViewModel.kt | 46 ++++-- .../daily/expand/model/DailyExpandUiState.kt | 9 +- 4 files changed, 114 insertions(+), 102 deletions(-) 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