diff --git a/core/designsystem/src/main/java/co/kr/tnt/designsystem/component/calendar/utils/CalendarUtils.kt b/core/designsystem/src/main/java/co/kr/tnt/designsystem/component/calendar/utils/CalendarUtils.kt new file mode 100644 index 00000000..b7f6fdcc --- /dev/null +++ b/core/designsystem/src/main/java/co/kr/tnt/designsystem/component/calendar/utils/CalendarUtils.kt @@ -0,0 +1,74 @@ +package co.kr.tnt.designsystem.component.calendar.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import com.kizitonwose.calendar.compose.CalendarLayoutInfo +import com.kizitonwose.calendar.compose.CalendarState +import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState +import com.kizitonwose.calendar.core.CalendarMonth +import kotlinx.coroutines.flow.filterNotNull +import java.time.LocalDate +import java.time.YearMonth + +/** + * 주어진 [viewportPercent] 를 기준으로, 현재 화면상으로 가장 많이 보이는 '월' 을 반환합니다. + */ +@Composable +fun rememberMostVisibleMonth( + state: CalendarState, + viewportPercent: Float = 90f, +): YearMonth { + val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) } + LaunchedEffect(state) { + snapshotFlow { state.layoutInfo.firstMostVisibleMonth(viewportPercent) } + .filterNotNull() + .collect { month -> visibleMonth.value = month } + } + return visibleMonth.value.yearMonth +} + +private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? { + return if (visibleMonthsInfo.isEmpty()) { + null + } else { + val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f + visibleMonthsInfo.firstOrNull { itemInfo -> + if (itemInfo.offset < 0) { + itemInfo.offset + itemInfo.size >= viewportSize + } else { + itemInfo.size - itemInfo.offset >= viewportSize + } + }?.month + } +} + +/** + * 화면에 보여지는 7일 중 과반 이상 보여지는 '월' 을 반환합니다. + */ +@Composable +fun rememberMostVisibleYearMonth( + state: WeekCalendarState, +): YearMonth { + val visibleYearMonth = remember { mutableStateOf(YearMonth.now()) } + + LaunchedEffect(state) { + snapshotFlow { state.firstVisibleWeek.days.map { it.date } } + .collect { visibleDays -> + val mostVisibleYearMonth = getMostFrequentYearMonth(visibleDays) + visibleYearMonth.value = mostVisibleYearMonth + } + } + + return visibleYearMonth.value +} + +private fun getMostFrequentYearMonth(visibleDays: List): YearMonth { + return visibleDays + .groupingBy { YearMonth.from(it) } + .eachCount() + .maxBy { it.value } + .key +} diff --git a/core/designsystem/src/main/res/drawable/ic_alarm.xml b/core/designsystem/src/main/res/drawable/ic_alarm.xml new file mode 100644 index 00000000..6f1d9a89 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_alarm.xml @@ -0,0 +1,18 @@ + + + + diff --git a/core/ui/src/main/java/co/kr/tnt/ui/component/TnTHomeTopBar.kt b/core/ui/src/main/java/co/kr/tnt/ui/component/TnTHomeTopBar.kt new file mode 100644 index 00000000..7a2cdb1c --- /dev/null +++ b/core/ui/src/main/java/co/kr/tnt/ui/component/TnTHomeTopBar.kt @@ -0,0 +1,74 @@ +package co.kr.tnt.ui.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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 co.kr.tnt.core.designsystem.R +import co.kr.tnt.designsystem.component.calendar.TnTCalendarSelector +import co.kr.tnt.designsystem.theme.TnTTheme +import java.time.YearMonth + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TnTHomeTopBar( + modifier: Modifier = Modifier, + yearMonth: YearMonth = YearMonth.now(), + onClickSelectorPrevious: () -> Unit = { }, + onClickSelectorNext: () -> Unit = { }, + onClickNotification: () -> Unit = { }, + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, +) { + Column { + CenterAlignedTopAppBar( + modifier = modifier + .fillMaxWidth() + .padding( + vertical = 8.dp, + horizontal = 16.dp, + ), + title = { + TnTCalendarSelector( + yearMonth = yearMonth, + onClickPrevious = onClickSelectorPrevious, + onClickNext = onClickSelectorNext, + ) + }, + actions = { + IconButton( + onClick = onClickNotification, + modifier = Modifier.size(32.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_alarm), + contentDescription = "Go to notification screen", + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = TnTTheme.colors.commonColors.Common0, + ), + windowInsets = windowInsets, + expandedHeight = 48.dp, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun TnTHomeTopBarPreview() { + TnTTheme { + TnTHomeTopBar() + } +} diff --git a/core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt b/core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt index e3245773..563279cf 100644 --- a/core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt +++ b/core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt @@ -2,10 +2,13 @@ package co.kr.tnt.ui.utils import android.content.Context import android.database.Cursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.net.Uri import android.provider.MediaStore import android.util.Log import java.io.File +import java.io.FileOutputStream fun Uri.toFile(context: Context): File? { return getRealPathFromUri(this, context)?.let { filePath -> @@ -27,3 +30,22 @@ fun getRealPathFromUri(uri: Uri, context: Context): String? { } return null } + +fun Uri.convertToAllowedImageFormat(context: Context): File { + val inputStream = context.contentResolver.openInputStream(this) + val bitmap = BitmapFactory.decodeStream(inputStream) + + val convertedFile = File(context.cacheDir, "image.png") + val outputStream = FileOutputStream(convertedFile) + + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + outputStream.flush() + outputStream.close() + + return convertedFile +} + +fun isAllowedImageFormat(file: File): Boolean { + val allowedExtensions = listOf("jpg", "jpeg", "png", "svg") + return file.extension.lowercase() in allowedExtensions +} diff --git a/domain/src/main/java/co/kr/tnt/domain/model/RecordList.kt b/domain/src/main/java/co/kr/tnt/domain/model/RecordList.kt new file mode 100644 index 00000000..7c561602 --- /dev/null +++ b/domain/src/main/java/co/kr/tnt/domain/model/RecordList.kt @@ -0,0 +1,12 @@ +package co.kr.tnt.domain.model + +import java.time.LocalDate + +data class RecordList( + val recordDate: LocalDate, + val recordType: RecordType, + val recordTime: String, + val recordImage: String?, + val recordContents: String, + val hasFeedback: Boolean, +) diff --git a/feature/trainee/home/build.gradle.kts b/feature/trainee/home/build.gradle.kts index ec9579d7..17172231 100644 --- a/feature/trainee/home/build.gradle.kts +++ b/feature/trainee/home/build.gradle.kts @@ -10,4 +10,5 @@ android { dependencies { implementation(libs.kotlinx.immutable) + implementation(libs.calendar.compose) } diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeContract.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeContract.kt new file mode 100644 index 00000000..e7160639 --- /dev/null +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeContract.kt @@ -0,0 +1,25 @@ +package co.kr.tnt.trainee.home + +import co.kr.tnt.domain.model.RecordList +import co.kr.tnt.ui.base.UiEvent +import co.kr.tnt.ui.base.UiSideEffect +import co.kr.tnt.ui.base.UiState +import java.time.LocalDate + +class TraineeHomeContract { + data class TraineeHomeUiState( + val selectedDate: LocalDate = LocalDate.now(), + val markedDates: List = emptyList(), + val recordList: List = emptyList(), + ) : UiState + + sealed interface TraineeHomeUiEvent : UiEvent { + data object OnClickNextWeek : TraineeHomeUiEvent + data object OnClickPreviousWeek : TraineeHomeUiEvent + data class OnClickDay(val date: LocalDate) : TraineeHomeUiEvent + } + + sealed interface TraineeHomeEffect : UiSideEffect { + data class ShowToast(val message: String) : TraineeHomeEffect + } +} diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt index 16048583..cf200ac1 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeScreen.kt @@ -1,37 +1,143 @@ package co.kr.tnt.trainee.home -import androidx.compose.foundation.layout.Column +import android.widget.Toast +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.designsystem.component.calendar.TnTIndicatorWeekCalendar +import co.kr.tnt.designsystem.component.calendar.model.DayIndicatorState +import co.kr.tnt.designsystem.component.calendar.model.DayState +import co.kr.tnt.designsystem.component.calendar.utils.rememberMostVisibleYearMonth +import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiEvent +import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiState +import co.kr.tnt.ui.component.TnTHomeTopBar +import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState +import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate @Composable -@Suppress("UnusedParameter") internal fun TraineeHomeRoute( viewModel: TraineeHomeViewModel = hiltViewModel(), navigateToNotification: () -> Unit, ) { - TraineeHomeScreen(navigateToNotification) + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + TraineeHomeScreen( + state = uiState, + onClickNotification = navigateToNotification, + onSelectDate = { date -> + viewModel.setEvent(TraineeHomeUiEvent.OnClickDay(date)) + }, + onClickNextWeek = { viewModel.setEvent(TraineeHomeUiEvent.OnClickNextWeek) }, + onClickPreviousWeek = { viewModel.setEvent(TraineeHomeUiEvent.OnClickPreviousWeek) }, + ) + + LaunchedEffect(viewModel.effect) { + viewModel.effect.collect { effect -> + when (effect) { + is TraineeHomeContract.TraineeHomeEffect.ShowToast -> { + Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() + } + } + } + } } @Composable -fun TraineeHomeScreen( - navigateToNotification: () -> Unit, +private fun TraineeHomeScreen( + state: TraineeHomeUiState, + onSelectDate: (LocalDate) -> Unit, + onClickNextWeek: () -> Unit, + onClickPreviousWeek: () -> Unit, + onClickNotification: () -> Unit, ) { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Column { - Text( - text = "trainee home", - modifier = Modifier.padding(innerPadding), - ) - Button(onClick = navigateToNotification) { - Text("navigate to notification") + val now = LocalDate.now() + val coroutineScope = rememberCoroutineScope() + + val weekCalendarState = rememberWeekCalendarState( + firstDayOfWeek = DayOfWeek.SUNDAY, + firstVisibleWeekDate = state.selectedDate, + startDate = now.minusYears(10), + endDate = now.plusYears(10), + ) + + val visibleYearMonth = rememberMostVisibleYearMonth(weekCalendarState) + + Scaffold( + containerColor = TnTTheme.colors.commonColors.Common0, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + LazyColumn(modifier = Modifier.padding(innerPadding)) { + item { + Spacer(modifier = Modifier.height(12.dp)) + TnTHomeTopBar( + yearMonth = visibleYearMonth, + onClickSelectorPrevious = { + coroutineScope.launch { + onClickPreviousWeek() + weekCalendarState.animateScrollToWeek(state.selectedDate) + } + }, + onClickSelectorNext = { + coroutineScope.launch { + onClickNextWeek() + weekCalendarState.animateScrollToWeek(state.selectedDate) + } + }, + onClickNotification = onClickNotification, + ) + Spacer(modifier = Modifier.height(16.dp)) + TnTIndicatorWeekCalendar( + state = weekCalendarState, + dayState = { date -> + DayState(isSelected = date == state.selectedDate) + }, + indicatorState = { date -> + DayIndicatorState(showIcon = date in state.markedDates) + }, + onClickDay = { date -> + onSelectDate(date) + }, + ) + Spacer(modifier = Modifier.height(12.dp)) } } } } + +@Preview +@Composable +private fun TraineeHomeScreenPreview() { + val now = LocalDate.now() + + val dummyUiState = TraineeHomeUiState( + selectedDate = now, + markedDates = List(5) { now.minusDays(it.toLong() * 2) }, + ) + + TnTTheme { + TraineeHomeScreen( + state = dummyUiState, + onClickNotification = { }, + onSelectDate = {}, + onClickNextWeek = { }, + onClickPreviousWeek = { }, + ) + } +} diff --git a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt index ed0103a3..0f6b9f9d 100644 --- a/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt +++ b/feature/trainee/home/src/main/java/co/kr/tnt/trainee/home/TraineeHomeViewModel.kt @@ -1,8 +1,48 @@ package co.kr.tnt.trainee.home -import androidx.lifecycle.ViewModel +import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeEffect +import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiEvent +import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiState +import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import java.time.LocalDate import javax.inject.Inject @HiltViewModel -class TraineeHomeViewModel @Inject constructor() : ViewModel() +internal class TraineeHomeViewModel @Inject constructor() : + BaseViewModel( + TraineeHomeUiState(), + ) { + init { + updateCalenderState() + } + + override suspend fun handleEvent(event: TraineeHomeUiEvent) { + when (event) { + TraineeHomeUiEvent.OnClickNextWeek -> moveToNextWeek() + TraineeHomeUiEvent.OnClickPreviousWeek -> moveToPreviousWeek() + is TraineeHomeUiEvent.OnClickDay -> selectDate(event.date) + } + } + + // TODO : 주간 캘린더 API 연동 + private fun updateCalenderState() { + val today = LocalDate.now() + val list = List(10) { + today.minusDays((0..30).random().toLong()) + } + updateState { copy(markedDates = list) } + } + + private fun selectDate(date: LocalDate) { + updateState { copy(selectedDate = date) } + } + + private fun moveToNextWeek() { + selectDate(currentState.selectedDate.plusWeeks(1)) + } + + private fun moveToPreviousWeek() { + selectDate(currentState.selectedDate.minusWeeks(1)) + } + } diff --git a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt index 71215164..49456d65 100644 --- a/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt +++ b/feature/trainee/signup/src/main/java/co/kr/tnt/trainee/signup/TraineeSignUpViewModel.kt @@ -2,6 +2,7 @@ package co.kr.tnt.trainee.signup import android.content.Context import android.net.Uri +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import co.kr.tnt.core.ui.R import co.kr.tnt.domain.model.User @@ -11,9 +12,12 @@ import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpPage import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiEvent import co.kr.tnt.trainee.signup.TraineeSignUpContract.TraineeSignUpUiState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.utils.convertToAllowedImageFormat +import co.kr.tnt.ui.utils.isAllowedImageFormat import co.kr.tnt.ui.utils.toFile import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.io.File import java.time.LocalDate import javax.inject.Inject @@ -53,11 +57,17 @@ internal class TraineeSignUpViewModel @Inject constructor( ) { viewModelScope.launch { val state = currentState - val profileImagePart = imageUri?.toFile(context) + val profileImageFile: File? = imageUri?.toFile(context)?.let { file -> + if (!isAllowedImageFormat(file)) { + file.toUri().convertToAllowedImageFormat(context) + } else { + file + } + } runCatching { signUpRepository.signUp( - profileImage = profileImagePart, + profileImage = profileImageFile, user = User.Trainee( id = id, name = state.name, diff --git a/feature/trainer/home/build.gradle.kts b/feature/trainer/home/build.gradle.kts index 37d9595e..b605fee0 100644 --- a/feature/trainer/home/build.gradle.kts +++ b/feature/trainer/home/build.gradle.kts @@ -10,4 +10,5 @@ android { dependencies { implementation(libs.kotlinx.immutable) + implementation(libs.calendar.compose) } diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt new file mode 100644 index 00000000..277db216 --- /dev/null +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeContract.kt @@ -0,0 +1,23 @@ +package co.kr.tnt.trainer.home + +import co.kr.tnt.ui.base.UiEvent +import co.kr.tnt.ui.base.UiSideEffect +import co.kr.tnt.ui.base.UiState +import java.time.LocalDate +import java.time.YearMonth + +internal class TrainerHomeContract { + data class TrainerHomeUiState( + val selectedDay: LocalDate = LocalDate.now(), + ) : UiState + + sealed interface TrainerHomeUiEvent : UiEvent { + data object OnClickNotification : TrainerHomeUiEvent + data class OnChangeVisibleMonth(val yearMonth: YearMonth) : TrainerHomeUiEvent + data class OnClickDay(val day: LocalDate) : TrainerHomeUiEvent + } + + sealed interface TrainerHomeSideEffect : UiSideEffect { + data object NavigateToNotification : TrainerHomeSideEffect + } +} diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt index 20b2dcc0..309bc88c 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeScreen.kt @@ -1,37 +1,109 @@ package co.kr.tnt.trainer.home import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import co.kr.tnt.designsystem.component.calendar.TnTIndicatorMonthCalendar +import co.kr.tnt.designsystem.component.calendar.model.DayState +import co.kr.tnt.designsystem.component.calendar.utils.rememberMostVisibleMonth +import co.kr.tnt.designsystem.theme.TnTTheme +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeSideEffect +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiEvent +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState +import co.kr.tnt.ui.component.TnTHomeTopBar +import com.kizitonwose.calendar.compose.rememberCalendarState +import kotlinx.coroutines.launch +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.YearMonth @Composable -@Suppress("UnusedParameter") internal fun TrainerHomeRoute( viewModel: TrainerHomeViewModel = hiltViewModel(), navigateToNotification: () -> Unit, ) { - TrainerHomeScreen(navigateToNotification) + val state by viewModel.uiState.collectAsStateWithLifecycle() + + TrainerHomeScreen( + state = state, + onClickNotification = { viewModel.setEvent(TrainerHomeUiEvent.OnClickNotification) }, + onChangeVisibleMonth = { viewModel.setEvent(TrainerHomeUiEvent.OnChangeVisibleMonth(it)) }, + onClickDay = { viewModel.setEvent(TrainerHomeUiEvent.OnClickDay(it)) }, + ) + + LaunchedEffect(viewModel.effect) { + viewModel.effect.collect { + when (it) { + TrainerHomeSideEffect.NavigateToNotification -> navigateToNotification() + } + } + } } @Composable private fun TrainerHomeScreen( - navigateToNotification: () -> Unit, + state: TrainerHomeUiState, + onClickNotification: () -> Unit, + onChangeVisibleMonth: (yearMonth: YearMonth) -> Unit, + onClickDay: (date: LocalDate) -> Unit, ) { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - Column { - Text( - text = "trainer home", - modifier = Modifier.padding(innerPadding), - ) - Button(onClick = navigateToNotification) { - Text("navigate to notification") + val now = remember { YearMonth.now() } + val calendarState = rememberCalendarState( + firstVisibleMonth = now, + firstDayOfWeek = DayOfWeek.SUNDAY, + startMonth = now.minusYears(10), + endMonth = now.plusYears(10), + ) + val coroutineScope = rememberCoroutineScope() + val visibleYearMonth = rememberMostVisibleMonth(calendarState) + + Scaffold( + topBar = { + Column { + Spacer(modifier = Modifier.height(12.dp)) + TnTHomeTopBar( + yearMonth = visibleYearMonth, + onClickNotification = onClickNotification, + onClickSelectorPrevious = { + coroutineScope.launch { + calendarState.animateScrollToMonth(visibleYearMonth.minusMonths(1)) + } + }, + onClickSelectorNext = { + coroutineScope.launch { + calendarState.animateScrollToMonth(visibleYearMonth.plusMonths(1)) + } + }, + ) } + }, + containerColor = TnTTheme.colors.commonColors.Common0, + modifier = Modifier.fillMaxSize(), + ) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding)) { + Spacer(modifier = Modifier.height(16.dp)) + TnTIndicatorMonthCalendar( + state = calendarState, + onClickDay = onClickDay, + dayState = { day -> DayState(isSelected = day == state.selectedDay) }, + ) } } + + LaunchedEffect(calendarState.firstVisibleMonth) { + val currentCalendarYearMonth = calendarState.firstVisibleMonth.yearMonth + onChangeVisibleMonth(currentCalendarYearMonth) + } } diff --git a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt index 2177f602..ddbe48d6 100644 --- a/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt +++ b/feature/trainer/home/src/main/java/co/kr/tnt/trainer/home/TrainerHomeViewModel.kt @@ -1,8 +1,26 @@ package co.kr.tnt.trainer.home -import androidx.lifecycle.ViewModel +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeSideEffect +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiEvent +import co.kr.tnt.trainer.home.TrainerHomeContract.TrainerHomeUiState +import co.kr.tnt.ui.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject @HiltViewModel -class TrainerHomeViewModel @Inject constructor() : ViewModel() +internal class TrainerHomeViewModel @Inject constructor() : + BaseViewModel( + TrainerHomeUiState(), + ) { + override suspend fun handleEvent(event: TrainerHomeUiEvent) { + when (event) { + TrainerHomeUiEvent.OnClickNotification -> sendEffect(TrainerHomeSideEffect.NavigateToNotification) + is TrainerHomeUiEvent.OnChangeVisibleMonth -> handleChangeVisibleMonth() + is TrainerHomeUiEvent.OnClickDay -> updateState { copy(selectedDay = event.day) } + } + } + + private fun handleChangeVisibleMonth() { + // TODO + } + } diff --git a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt index 48b8d058..f5ef5fa1 100644 --- a/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt +++ b/feature/trainer/signup/src/main/java/co/kr/tnt/trainer/signup/TrainerSignUpViewModel.kt @@ -2,6 +2,7 @@ package co.kr.tnt.trainer.signup import android.content.Context import android.net.Uri +import androidx.core.net.toUri import androidx.lifecycle.viewModelScope import co.kr.tnt.domain.model.User import co.kr.tnt.domain.repository.SignUpRepository @@ -10,9 +11,13 @@ import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpPage import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiEvent import co.kr.tnt.trainer.signup.TrainerSignUpContract.TrainerSignUpUiState import co.kr.tnt.ui.base.BaseViewModel +import co.kr.tnt.ui.utils.convertToAllowedImageFormat +import co.kr.tnt.ui.utils.isAllowedImageFormat import co.kr.tnt.ui.utils.toFile import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import java.io.File import javax.inject.Inject import co.kr.tnt.core.ui.R as uiResource @@ -45,12 +50,18 @@ internal class TrainerSignUpViewModel @Inject constructor( email: String, authType: String, ) { - viewModelScope.launch { - val profileImagePart = imageUri?.toFile(context) + viewModelScope.launch(Dispatchers.IO) { + val profileImageFile: File? = imageUri?.toFile(context)?.let { file -> + if (!isAllowedImageFormat(file)) { + file.toUri().convertToAllowedImageFormat(context) + } else { + file + } + } runCatching { signUpRepository.signUp( - profileImage = profileImagePart, + profileImage = profileImageFile, user = User.Trainer( id = id, name = currentState.name,