diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 02bf502b..2b0b6f49 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,15 @@ + + + + + + + navController.navigateToExpoModify(id) }, - onProgramClick = navController::navigateToHomeDetailProgram + onProgramClick = { id -> + navController.navigateToHomeDetailProgram(id) + } ) homeSendMessageScreen( @@ -142,11 +146,14 @@ fun ExpoNavHost( homeDetailProgramScreen( onBackClick = navController::popBackStack, - navigateToProgramDetail = navController::navigateToHomeDetailProgramParticipant + navigateToProgramDetail = { id -> + navController.navigateToHomeDetailProgramParticipant(id) + } ) homeDetailProgramParticipantScreen( - onBackClick = navController::popBackStack + onBackClick = navController::popBackStack, + navigateToQrScanner = navController::navigateQrScanner ) homeDetailParticipantManagementScreen( @@ -161,5 +168,10 @@ fun ExpoNavHost( expoCreateScreen( onErrorToast = makeErrorToast ) + + qrScannerScreen( + onBackClick = navController::popBackStack, + onPermissionBlock = navController::popBackStack + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/school_of_company/expo_android/ui/ExpoApp.kt b/app/src/main/java/com/school_of_company/expo_android/ui/ExpoApp.kt index 67020c9b..09710a9a 100644 --- a/app/src/main/java/com/school_of_company/expo_android/ui/ExpoApp.kt +++ b/app/src/main/java/com/school_of_company/expo_android/ui/ExpoApp.kt @@ -69,8 +69,7 @@ fun ExpoApp( } ) { paddingValues -> // 네비게이션 호스트 - Box(modifier = Modifier.padding(paddingValues = - paddingValues)) { + Box(modifier = Modifier.padding(paddingValues = paddingValues)) { ExpoNavHost(appState = appState) } } diff --git a/core/data/src/main/java/com/school_of_company/data/di/RepositoryModule.kt b/core/data/src/main/java/com/school_of_company/data/di/RepositoryModule.kt index f17cc7a3..71281a7f 100644 --- a/core/data/src/main/java/com/school_of_company/data/di/RepositoryModule.kt +++ b/core/data/src/main/java/com/school_of_company/data/di/RepositoryModule.kt @@ -1,5 +1,7 @@ package com.school_of_company.data.di +import com.school_of_company.data.repository.attendance.AttendanceRepository +import com.school_of_company.data.repository.attendance.AttendanceRepositoryImpl import com.school_of_company.data.repository.auth.AuthRepository import com.school_of_company.data.repository.auth.AuthRepositoryImpl import com.school_of_company.data.repository.expo.ExpoRepository @@ -50,4 +52,9 @@ abstract class RepositoryModule { abstract fun bindStandardRepository( standardRepositoryImpl: StandardRepositoryImpl ) : StandardRepository + + @Binds + abstract fun bindAttendanceRepository( + attendanceRepositoryImpl: AttendanceRepositoryImpl + ) : AttendanceRepository } \ No newline at end of file diff --git a/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepository.kt b/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepository.kt new file mode 100644 index 00000000..480712e0 --- /dev/null +++ b/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepository.kt @@ -0,0 +1,8 @@ +package com.school_of_company.data.repository.attendance + +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam +import kotlinx.coroutines.flow.Flow + +interface AttendanceRepository { + fun trainingQrCode(trainingId: Long, body: TrainingQrCodeRequestParam) : Flow +} \ No newline at end of file diff --git a/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepositoryImpl.kt b/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepositoryImpl.kt new file mode 100644 index 00000000..d9889efa --- /dev/null +++ b/core/data/src/main/java/com/school_of_company/data/repository/attendance/AttendanceRepositoryImpl.kt @@ -0,0 +1,21 @@ +package com.school_of_company.data.repository.attendance + +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam +import com.school_of_company.network.datasource.attendance.AttendanceDataSource +import com.school_of_company.network.mapper.attendance.request.toDto +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class AttendanceRepositoryImpl @Inject constructor( + private val dataSource: AttendanceDataSource +) : AttendanceRepository { + override fun trainingQrCode( + trainingId: Long, + body: TrainingQrCodeRequestParam + ): Flow { + return dataSource.trainingQrCode( + trainingId = trainingId, + body = body.toDto() + ) + } +} \ No newline at end of file diff --git a/core/domain/src/main/java/com/school_of_company/domain/usecase/attendance/TrainingQrCodeRequestUseCase.kt b/core/domain/src/main/java/com/school_of_company/domain/usecase/attendance/TrainingQrCodeRequestUseCase.kt new file mode 100644 index 00000000..9649fb33 --- /dev/null +++ b/core/domain/src/main/java/com/school_of_company/domain/usecase/attendance/TrainingQrCodeRequestUseCase.kt @@ -0,0 +1,19 @@ +package com.school_of_company.domain.usecase.attendance + +import com.school_of_company.data.repository.attendance.AttendanceRepository +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam +import javax.inject.Inject + +class TrainingQrCodeRequestUseCase @Inject constructor( + private val repository: AttendanceRepository +) { + operator fun invoke( + trainingId: Long, + body: TrainingQrCodeRequestParam + ) = runCatching { + repository.trainingQrCode( + trainingId = trainingId, + body = body + ) + } +} \ No newline at end of file diff --git a/core/model/src/main/java/com/school_of_company/model/entity/training/TeacherTrainingProgramResponseEntity.kt b/core/model/src/main/java/com/school_of_company/model/entity/training/TeacherTrainingProgramResponseEntity.kt index 56e8ef7d..b8405dbf 100644 --- a/core/model/src/main/java/com/school_of_company/model/entity/training/TeacherTrainingProgramResponseEntity.kt +++ b/core/model/src/main/java/com/school_of_company/model/entity/training/TeacherTrainingProgramResponseEntity.kt @@ -1,11 +1,12 @@ package com.school_of_company.model.entity.training data class TeacherTrainingProgramResponseEntity( + val id: Long, val name: String, val organization: String, val position: String, val programName: String, val status: Boolean, - val entryTime: String, - val leaveTime: String + val entryTime: String?, + val leaveTime: String? ) \ No newline at end of file diff --git a/core/model/src/main/java/com/school_of_company/model/param/attendance/TrainingQrCodeRequestParam.kt b/core/model/src/main/java/com/school_of_company/model/param/attendance/TrainingQrCodeRequestParam.kt new file mode 100644 index 00000000..26f260cb --- /dev/null +++ b/core/model/src/main/java/com/school_of_company/model/param/attendance/TrainingQrCodeRequestParam.kt @@ -0,0 +1,5 @@ +package com.school_of_company.model.param.attendance + +data class TrainingQrCodeRequestParam ( + val traineeId: Long +) \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/api/AttendanceAPI.kt b/core/network/src/main/java/com/school_of_company/network/api/AttendanceAPI.kt new file mode 100644 index 00000000..c78f01ce --- /dev/null +++ b/core/network/src/main/java/com/school_of_company/network/api/AttendanceAPI.kt @@ -0,0 +1,15 @@ +package com.school_of_company.network.api + +import com.school_of_company.network.dto.attendance.request.TrainingQrCodeRequest +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.Path + +interface AttendanceAPI { + + @GET("/attendance/training/{trainingPro_id}") + suspend fun trainingQrCode( + @Path("trainingPro_id") trainingId: Long, + @Body body: TrainingQrCodeRequest + ) +} \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSource.kt b/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSource.kt new file mode 100644 index 00000000..ab688288 --- /dev/null +++ b/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSource.kt @@ -0,0 +1,8 @@ +package com.school_of_company.network.datasource.attendance + +import com.school_of_company.network.dto.attendance.request.TrainingQrCodeRequest +import kotlinx.coroutines.flow.Flow + +interface AttendanceDataSource { + fun trainingQrCode(trainingId: Long, body: TrainingQrCodeRequest) : Flow +} \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSourceImpl.kt b/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSourceImpl.kt new file mode 100644 index 00000000..1d4f66a2 --- /dev/null +++ b/core/network/src/main/java/com/school_of_company/network/datasource/attendance/AttendanceDataSourceImpl.kt @@ -0,0 +1,20 @@ +package com.school_of_company.network.datasource.attendance + +import com.school_of_company.network.api.AttendanceAPI +import com.school_of_company.network.dto.attendance.request.TrainingQrCodeRequest +import com.school_of_company.network.util.performApiRequest +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class AttendanceDataSourceImpl @Inject constructor( + private val service: AttendanceAPI +) : AttendanceDataSource { + override fun trainingQrCode( + trainingId: Long, + body: TrainingQrCodeRequest + ): Flow = + performApiRequest { service.trainingQrCode( + body = body, + trainingId = trainingId + ) } +} \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/di/NetworkModule.kt b/core/network/src/main/java/com/school_of_company/network/di/NetworkModule.kt index cca88a9b..50c9936b 100644 --- a/core/network/src/main/java/com/school_of_company/network/di/NetworkModule.kt +++ b/core/network/src/main/java/com/school_of_company/network/di/NetworkModule.kt @@ -4,6 +4,7 @@ import android.content.Context import android.util.Log import com.readystatesoftware.chuck.ChuckInterceptor import com.school_of_company.network.BuildConfig +import com.school_of_company.network.api.AttendanceAPI import com.school_of_company.network.api.AuthAPI import com.school_of_company.network.api.ExpoAPI import com.school_of_company.network.api.ImageAPI @@ -114,4 +115,7 @@ object NetworkModule { fun provideStandardAPI(retrofit: Retrofit) : StandardAPI = retrofit.create(StandardAPI::class.java) + @Provides + fun provideAttendanceAPI(retrofit: Retrofit) : AttendanceAPI = + retrofit.create(AttendanceAPI::class.java) } \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/di/RemoteDataSourceModule.kt b/core/network/src/main/java/com/school_of_company/network/di/RemoteDataSourceModule.kt index 2fcb291f..aa477e70 100644 --- a/core/network/src/main/java/com/school_of_company/network/di/RemoteDataSourceModule.kt +++ b/core/network/src/main/java/com/school_of_company/network/di/RemoteDataSourceModule.kt @@ -1,5 +1,7 @@ package com.school_of_company.network.di +import com.school_of_company.network.datasource.attendance.AttendanceDataSource +import com.school_of_company.network.datasource.attendance.AttendanceDataSourceImpl import com.school_of_company.network.datasource.auth.AuthDataSource import com.school_of_company.network.datasource.auth.AuthDataSourceImpl import com.school_of_company.network.datasource.expo.ExpoDataSource @@ -50,4 +52,9 @@ abstract class RemoteDataSourceModule { abstract fun bindStandardRemoteDataSource( standardDataSourceImpl: StandardDataSourceImpl ) : StandardDataSource + + @Binds + abstract fun bindAttendanceRemoteDataSource( + attendanceDataSourceImpl: AttendanceDataSourceImpl + ) : AttendanceDataSource } \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/dto/attendance/request/TrainingQrCodeRequest.kt b/core/network/src/main/java/com/school_of_company/network/dto/attendance/request/TrainingQrCodeRequest.kt new file mode 100644 index 00000000..879afe13 --- /dev/null +++ b/core/network/src/main/java/com/school_of_company/network/dto/attendance/request/TrainingQrCodeRequest.kt @@ -0,0 +1,9 @@ +package com.school_of_company.network.dto.attendance.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class TrainingQrCodeRequest( + @Json(name = "traineeId") val traineeId: Long +) \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/dto/training/response/TeacherTrainingProgramResponse.kt b/core/network/src/main/java/com/school_of_company/network/dto/training/response/TeacherTrainingProgramResponse.kt index d7535e53..849368df 100644 --- a/core/network/src/main/java/com/school_of_company/network/dto/training/response/TeacherTrainingProgramResponse.kt +++ b/core/network/src/main/java/com/school_of_company/network/dto/training/response/TeacherTrainingProgramResponse.kt @@ -5,11 +5,12 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class TeacherTrainingProgramResponse( + @Json(name = "id") val id: Long, @Json(name = "name") val name: String, @Json(name = "organization") val organization: String, @Json(name = "position") val position: String, @Json(name = "programName") val programName: String, @Json(name = "status") val status: Boolean, - @Json(name = "entryTime") val entryTime: String, - @Json(name = "leaveTime") val leaveTime: String + @Json(name = "entryTime") val entryTime: String?, + @Json(name = "leaveTime") val leaveTime: String? ) diff --git a/core/network/src/main/java/com/school_of_company/network/mapper/attendance/request/TrainingQrCodeRequestMapper.kt b/core/network/src/main/java/com/school_of_company/network/mapper/attendance/request/TrainingQrCodeRequestMapper.kt new file mode 100644 index 00000000..4fb66ab7 --- /dev/null +++ b/core/network/src/main/java/com/school_of_company/network/mapper/attendance/request/TrainingQrCodeRequestMapper.kt @@ -0,0 +1,7 @@ +package com.school_of_company.network.mapper.attendance.request + +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam +import com.school_of_company.network.dto.attendance.request.TrainingQrCodeRequest + +fun TrainingQrCodeRequestParam.toDto(): TrainingQrCodeRequest = + TrainingQrCodeRequest(traineeId = traineeId) \ No newline at end of file diff --git a/core/network/src/main/java/com/school_of_company/network/mapper/training/response/TeacherTrainingProgramResponseMapper.kt b/core/network/src/main/java/com/school_of_company/network/mapper/training/response/TeacherTrainingProgramResponseMapper.kt index 94a54d09..ab66357b 100644 --- a/core/network/src/main/java/com/school_of_company/network/mapper/training/response/TeacherTrainingProgramResponseMapper.kt +++ b/core/network/src/main/java/com/school_of_company/network/mapper/training/response/TeacherTrainingProgramResponseMapper.kt @@ -5,6 +5,7 @@ import com.school_of_company.network.dto.training.response.TeacherTrainingProgra fun TeacherTrainingProgramResponse.toEntity(): TeacherTrainingProgramResponseEntity = TeacherTrainingProgramResponseEntity( + id = this.id, name = this.name, organization = this.organization, position = this.position, diff --git a/feature/expo/src/main/java/com/school_of_company/expo/navigation/ExpoNavigation.kt b/feature/expo/src/main/java/com/school_of_company/expo/navigation/ExpoNavigation.kt index e1804203..591a9bb5 100644 --- a/feature/expo/src/main/java/com/school_of_company/expo/navigation/ExpoNavigation.kt +++ b/feature/expo/src/main/java/com/school_of_company/expo/navigation/ExpoNavigation.kt @@ -58,7 +58,7 @@ fun NavGraphBuilder.expoDetailScreen( onCheckClick: () -> Unit, onQrGenerateClick: () -> Unit, onModifyClick: (String) -> Unit, - onProgramClick: () -> Unit + onProgramClick: (String) -> Unit ) { composable(route = "$expoDetailRoute/{id}") { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: "" diff --git a/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoCreateScreen.kt b/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoCreateScreen.kt index e411dc83..a9c4a4e1 100644 --- a/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoCreateScreen.kt +++ b/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoCreateScreen.kt @@ -135,8 +135,8 @@ internal fun ExpoCreateRoute( description = viewModel.introduce_title.value, location = viewModel.location.value, coverImage = (imageUpLoadUiState as ImageUpLoadUiState.Success).data.imageURL, - x = 35.14308f, - y = 126.80043f + x = 37.511734f, + y = 127.05905f ) ) } diff --git a/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoDetailScreen.kt b/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoDetailScreen.kt index c8d1f1b4..a1945e9a 100644 --- a/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoDetailScreen.kt +++ b/feature/expo/src/main/java/com/school_of_company/expo/view/ExpoDetailScreen.kt @@ -58,7 +58,7 @@ internal fun ExpoDetailRoute( onCheckClick: () -> Unit, onQrGenerateClick: () -> Unit, onModifyClick: (String) -> Unit, - onProgramClick: () -> Unit, + onProgramClick: (String) -> Unit, viewModel: ExpoViewModel = hiltViewModel() ) { val getExpoInformationUiState by viewModel.getExpoInformationUiState.collectAsStateWithLifecycle() @@ -92,7 +92,7 @@ internal fun ExpoDetailScreen( onCheckClick: () -> Unit, onQrGenerateClick: () -> Unit, onModifyClick: (String) -> Unit, - onProgramClick: () -> Unit + onProgramClick: (String) -> Unit ) { val (openDialog, isOpenDialog) = rememberSaveable { mutableStateOf(false) } val (openQrDialog, isOpenQrDialog) = rememberSaveable { mutableStateOf(false) } @@ -296,7 +296,7 @@ internal fun ExpoDetailScreen( ExpoEnableDetailButton( text = "프로그램", - onClick = onProgramClick, + onClick = { onProgramClick(id) }, modifier = Modifier .fillMaxWidth() .border( diff --git a/feature/expo/src/main/java/com/school_of_company/expo/view/component/ExpoBottomSheet.kt b/feature/expo/src/main/java/com/school_of_company/expo/view/component/ExpoBottomSheet.kt index 7cc24c34..d2054d4e 100644 --- a/feature/expo/src/main/java/com/school_of_company/expo/view/component/ExpoBottomSheet.kt +++ b/feature/expo/src/main/java/com/school_of_company/expo/view/component/ExpoBottomSheet.kt @@ -96,6 +96,7 @@ private fun HomeBottomSheetOptions( indication = ripple(color = rippleColor), interactionSource = remember { MutableInteractionSource() } ) + .padding(horizontal = 12.dp) ) { Text( text = text, diff --git a/feature/expo/src/main/java/com/school_of_company/expo/viewmodel/ExpoViewModel.kt b/feature/expo/src/main/java/com/school_of_company/expo/viewmodel/ExpoViewModel.kt index 83381dd6..6b1232ff 100644 --- a/feature/expo/src/main/java/com/school_of_company/expo/viewmodel/ExpoViewModel.kt +++ b/feature/expo/src/main/java/com/school_of_company/expo/viewmodel/ExpoViewModel.kt @@ -33,6 +33,7 @@ import com.school_of_company.model.model.expo.ExpoRequestAndResponseModel import com.school_of_company.model.model.standard.StandardRequestModel import com.school_of_company.model.model.training.TrainingDtoModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index f82eb2c2..1fe3c42d 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -13,4 +13,18 @@ dependencies { implementation(libs.mlkit) implementation(libs.zxing.core) + + implementation(libs.swiperefresh) + + implementation(libs.camera.core) + implementation(libs.camera.view) + implementation(libs.camera.camera2) + implementation(libs.camera.lifecycle) + implementation(libs.camera.extensions) + + implementation(libs.accompanist.permission) + + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.play.services) + } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/school_of_company/home/navigation/HomeNavigation.kt index c0d3c2c2..81022cde 100644 --- a/feature/home/src/main/java/com/school_of_company/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/school_of_company/home/navigation/HomeNavigation.kt @@ -7,23 +7,37 @@ import androidx.navigation.compose.composable import com.school_of_company.home.view.HomeDetailParticipantManagementRoute import com.school_of_company.home.view.HomeDetailProgramParticipantRoute import com.school_of_company.home.view.HomeDetailProgramRoute +import com.school_of_company.home.view.QrScannerRoute import com.school_of_company.home.view.SendMessageRoute const val homeSendMessageRoute = "home_send_message_route" const val homeDetailProgramRoute = "home_detail_program_route" const val homeDetailProgramParticipantRoute = "home_detail_program_participant_route" const val homeDetailParticipantManagementRoute = "home_detail_participant_management_route" +const val qrScannerRoute = "qr_scanner_route" fun NavController.navigateToHomeSendMessage(navOptions: NavOptions? = null) { this.navigate(homeSendMessageRoute, navOptions) } -fun NavController.navigateToHomeDetailProgram(navOptions: NavOptions? = null) { - this.navigate(homeDetailProgramRoute, navOptions) +fun NavController.navigateToHomeDetailProgram( + id: String, + navOptions: NavOptions? = null +) { + this.navigate( + route = "$homeDetailProgramRoute/${id}", + navOptions + ) } -fun NavController.navigateToHomeDetailProgramParticipant(navOptions: NavOptions? = null) { - this.navigate(homeDetailProgramParticipantRoute, navOptions) +fun NavController.navigateToHomeDetailProgramParticipant( + id: Long, + navOptions: NavOptions? = null +) { + this.navigate( + route = "$homeDetailProgramParticipantRoute/${id}", + navOptions + ) } fun NavController.navigateToHomeDetailParticipantManagement(navOptions: NavOptions? = null) { @@ -40,12 +54,25 @@ fun NavGraphBuilder.homeSendMessageScreen( } } +fun NavController.navigateQrScanner( + id: Long, + traineeId: Long, + navOptions: NavOptions? = null +) { + this.navigate( + route = "$qrScannerRoute/${id}/${traineeId}", + navOptions + ) +} + fun NavGraphBuilder.homeDetailProgramScreen( onBackClick: () -> Unit, - navigateToProgramDetail: () -> Unit + navigateToProgramDetail: (Long) -> Unit ) { - composable(route = homeDetailProgramRoute) { + composable(route = "$homeDetailProgramRoute/{id}") { backStackEntry -> + val id = backStackEntry.arguments?.getString("id") ?: "" HomeDetailProgramRoute( + id = id, onBackClick = onBackClick, navigateToProgramDetail = navigateToProgramDetail ) @@ -53,12 +80,18 @@ fun NavGraphBuilder.homeDetailProgramScreen( } fun NavGraphBuilder.homeDetailProgramParticipantScreen( - onBackClick: () -> Unit + onBackClick: () -> Unit, + navigateToQrScanner: (Long, Long) -> Unit ) { - composable(route = homeDetailProgramParticipantRoute) { - HomeDetailProgramParticipantRoute( - onBackClick = onBackClick - ) + composable(route = "$homeDetailProgramParticipantRoute/{id}") { backStackEntry -> + val id = backStackEntry.arguments?.getString("id")?.toLongOrNull() + if (id != null) { + HomeDetailProgramParticipantRoute( + id = id, + onBackClick = onBackClick, + navigateToQrScanner = navigateToQrScanner + ) + } } } @@ -70,4 +103,22 @@ fun NavGraphBuilder.homeDetailParticipantManagementScreen( onBackClick = onBackClick ) } +} + +fun NavGraphBuilder.qrScannerScreen( + onBackClick: () -> Unit, + onPermissionBlock: () -> Unit +) { + composable(route = "$qrScannerRoute/{id}/{traineeId}") { backStackEntry -> + val id = backStackEntry.arguments?.getString("id")?.toLongOrNull() + val traineeId = backStackEntry.arguments?.getString("traineeId")?.toLongOrNull() + if (id != null && traineeId != null) { + QrScannerRoute( + id = id, + traineeId = traineeId, + onBackClick = onBackClick, + onPermissionBlock = onPermissionBlock + ) + } + } } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/util/QrcodeScanner.kt b/feature/home/src/main/java/com/school_of_company/home/util/QrcodeScanner.kt new file mode 100644 index 00000000..f2aabe1a --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/util/QrcodeScanner.kt @@ -0,0 +1,61 @@ +package com.school_of_company.home.util + +import android.util.Log +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageProxy +import com.google.mlkit.vision.barcode.BarcodeScannerOptions +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.barcode.common.Barcode +import com.google.mlkit.vision.common.InputImage +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.tasks.await + +class QrcodeScanner( + private val qrcodeData: (Long) -> Unit +) : ImageAnalysis.Analyzer { + + private val scanner = BarcodeScanning.getClient( + BarcodeScannerOptions.Builder() + .setBarcodeFormats( + Barcode.FORMAT_QR_CODE, + Barcode.FORMAT_CODE_128, + Barcode.FORMAT_CODE_39 + ) + .build() + ) + + @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) + override fun analyze(imageProxy: ImageProxy) { + val mediaImage = imageProxy.image + if (mediaImage != null) { + val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + CoroutineScope(Dispatchers.IO).launch { + try { + val barcodes = scanner.process(image).await() + for (barcode in barcodes) { + val rawValue = barcode.displayValue + Log.d("qrcode", "Raw QR code value: $rawValue") + + if (rawValue != null) { + try { + val qrCodeValue = rawValue.toLong() + qrcodeData(qrCodeValue) + Log.d("qrcode", "QR code scan succeeded: $qrCodeValue") + } catch (e: NumberFormatException) { + Log.d("qrcode", "QR code contains non-numeric value: $rawValue") + } + } + } + } catch (e: Exception) { + Log.e("qrcode", "Scan failed: ${e.message}") + } finally { + imageProxy.close() + } + } + } else { + imageProxy.close() + } + } +} diff --git a/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramParticipantScreen.kt b/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramParticipantScreen.kt index adb7403a..12e00305 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramParticipantScreen.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramParticipantScreen.kt @@ -17,46 +17,74 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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 com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.SwipeRefreshIndicator +import com.google.accompanist.swiperefresh.SwipeRefreshState +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.school_of_company.design_system.component.modifier.clickable.expoClickable import com.school_of_company.design_system.component.topbar.ExpoTopBar import com.school_of_company.design_system.icon.LeftArrowIcon import com.school_of_company.design_system.icon.WarnIcon import com.school_of_company.design_system.theme.ExpoAndroidTheme -import com.school_of_company.home.view.component.HomeDetailProgramParticipantData import com.school_of_company.home.view.component.HomeDetailProgramParticipantList -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toPersistentList - -fun generateParticipantSampleData(): ImmutableList { - return List(20) { - HomeDetailProgramParticipantData( - name = "이명훈", - company = "초등학교", - position = "교사", - schoolSubject = "컴퓨터공학", - phone = "010-1234-5678" - ) - }.toPersistentList() -} +import com.school_of_company.home.view.component.QrButton +import com.school_of_company.home.viewmodel.HomeViewModel +import com.school_of_company.home.viewmodel.uistate.TeacherTrainingProgramListUiState +import kotlinx.collections.immutable.toImmutableList @Composable internal fun HomeDetailProgramParticipantRoute( - onBackClick: () -> Unit + id: Long, + onBackClick: () -> Unit, + navigateToQrScanner: (Long, Long) -> Unit, + viewModel: HomeViewModel = hiltViewModel() ) { + val swipeRefreshLoading by viewModel.swipeRefreshLoading.collectAsStateWithLifecycle() + val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = swipeRefreshLoading) + + val teacherTrainingProgramListUiState by viewModel.teacherTrainingProgramListUiState.collectAsStateWithLifecycle() + + val traineeId: Long = when (teacherTrainingProgramListUiState) { + is TeacherTrainingProgramListUiState.Success -> { + (teacherTrainingProgramListUiState as TeacherTrainingProgramListUiState.Success).data.firstOrNull()?.id + ?: -1L + } + + else -> -1L + } + HomeDetailProgramParticipantScreen( + id = id, onBackClick = onBackClick, - participantData = generateParticipantSampleData() + teacherTrainingProgramListUiState = teacherTrainingProgramListUiState, + swipeRefreshState = swipeRefreshState, + getTeacherTrainingProgramList = { viewModel.teacherTrainingProgramList(id) }, + navigateToQrScanner = navigateToQrScanner, + traineeId = traineeId ) + + LaunchedEffect(Unit) { + viewModel.teacherTrainingProgramList(id) + } } @Composable internal fun HomeDetailProgramParticipantScreen( + id: Long, modifier: Modifier = Modifier, - participantData: ImmutableList, + swipeRefreshState: SwipeRefreshState, + teacherTrainingProgramListUiState: TeacherTrainingProgramListUiState, + getTeacherTrainingProgramList: () -> Unit, + traineeId: Long, + navigateToQrScanner: (Long, Long) -> Unit, onBackClick: () -> Unit ) { ExpoAndroidTheme { colors, typography -> @@ -79,17 +107,28 @@ internal fun HomeDetailProgramParticipantScreen( Spacer(modifier = Modifier.height(28.dp)) - Text( - text = "프로그램", - style = typography.bodyBold2, - color = colors.black, - modifier = Modifier.padding(start = 16.dp) - ) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "프로그램", + style = typography.bodyBold2, + color = colors.black, + modifier = Modifier.padding(start = 16.dp) + ) + + QrButton( + onClick = { navigateToQrScanner(id, traineeId) }, + modifier = Modifier.padding(end = 16.dp) + ) + } Spacer(modifier = Modifier.height(12.dp)) Row(modifier = Modifier.padding(horizontal = 16.dp)) { - Row(horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start),) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.Start)) { WarnIcon( tint = colors.gray300, modifier = Modifier.size(16.dp) @@ -120,12 +159,19 @@ internal fun HomeDetailProgramParticipantScreen( style = typography.captionRegular2, color = colors.gray500 ) + when (teacherTrainingProgramListUiState) { + is TeacherTrainingProgramListUiState.Loading -> Unit + is TeacherTrainingProgramListUiState.Success -> { + Text( + text = "${teacherTrainingProgramListUiState.data.size}명", + style = typography.captionRegular2, + color = colors.main + ) + } - Text( - text = "${participantData.size}명", - style = typography.captionRegular2, - color = colors.main - ) + is TeacherTrainingProgramListUiState.Error -> Unit + is TeacherTrainingProgramListUiState.Empty -> Unit + } } } @@ -138,9 +184,7 @@ internal fun HomeDetailProgramParticipantScreen( color = colors.gray200 ) .fillMaxWidth() - .padding( - vertical = 16.dp - ) + .padding(vertical = 16.dp) ) { Row( modifier = Modifier @@ -150,7 +194,7 @@ internal fun HomeDetailProgramParticipantScreen( horizontalArrangement = Arrangement.spacedBy(20.dp) ) { Spacer(modifier = Modifier.width(20.dp)) - + Text( text = "성명", style = typography.captionBold1, @@ -184,7 +228,27 @@ internal fun HomeDetailProgramParticipantScreen( } } - HomeDetailProgramParticipantList(item = participantData) + SwipeRefresh( + state = swipeRefreshState, + onRefresh = { getTeacherTrainingProgramList() }, + indicator = { state, refreshTrigger -> + SwipeRefreshIndicator( + state = state, + refreshTriggerDistance = refreshTrigger, + contentColor = colors.main + ) + } + ) { + when (teacherTrainingProgramListUiState) { + is TeacherTrainingProgramListUiState.Loading -> Unit + is TeacherTrainingProgramListUiState.Success -> HomeDetailProgramParticipantList( + item = teacherTrainingProgramListUiState.data.toImmutableList() + ) + + is TeacherTrainingProgramListUiState.Error -> Unit + is TeacherTrainingProgramListUiState.Empty -> Unit + } + } } } } @@ -192,8 +256,5 @@ internal fun HomeDetailProgramParticipantScreen( @Preview @Composable private fun HomeDetailProgramParticipantScreenPreview() { - HomeDetailProgramParticipantScreen( - onBackClick = {}, - participantData = generateParticipantSampleData() - ) + } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramScreen.kt b/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramScreen.kt index fce36051..2ebbe5c9 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramScreen.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/HomeDetailProgramScreen.kt @@ -19,6 +19,8 @@ import androidx.compose.material3.TabRowDefaults import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -26,49 +28,71 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.swiperefresh.SwipeRefresh +import com.google.accompanist.swiperefresh.SwipeRefreshIndicator +import com.google.accompanist.swiperefresh.SwipeRefreshState +import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.school_of_company.design_system.component.modifier.clickable.expoClickable import com.school_of_company.design_system.component.topbar.ExpoTopBar import com.school_of_company.design_system.icon.LeftArrowIcon import com.school_of_company.design_system.theme.ExpoAndroidTheme import com.school_of_company.home.view.component.ProgramList import com.school_of_company.home.view.component.ProgramTabRowItem -import com.school_of_company.home.view.component.ProgramTempList -import kotlinx.collections.immutable.ImmutableList +import com.school_of_company.home.view.component.StandardProgramList +import com.school_of_company.home.viewmodel.HomeViewModel +import com.school_of_company.home.viewmodel.uistate.StandardProgramListUiState +import com.school_of_company.home.viewmodel.uistate.TrainingProgramListUiState import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -fun generateProgramSampleData(): ImmutableList { - return List(20) { - ProgramTempList( - programName = "프로그램 이름", - check = true, - must = false - ) - }.toPersistentList() -} - @Composable internal fun HomeDetailProgramRoute( + id: String, onBackClick: () -> Unit, - navigateToProgramDetail: () -> Unit + navigateToProgramDetail: (Long) -> Unit, + viewModel: HomeViewModel = hiltViewModel() ) { + val swipeRefreshLoading by viewModel.swipeRefreshLoading.collectAsStateWithLifecycle() + val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = swipeRefreshLoading) + + val trainingProgramListUiState by viewModel.trainingProgramListUiState.collectAsStateWithLifecycle() + val standardProgramListUiState by viewModel.standardProgramListUiState.collectAsStateWithLifecycle() + + HomeDetailProgramScreen( - programItem = generateProgramSampleData(), + id = id, onBackClick = onBackClick, - navigateToProgramDetail = navigateToProgramDetail + navigateToProgramDetail = navigateToProgramDetail, + trainingProgramUiState = trainingProgramListUiState, + standardProgramListUiState = standardProgramListUiState, + swipeRefreshState = swipeRefreshState, + getTrainingProgramList = { viewModel.trainingProgramList(id) }, + getStandardProgramList = { viewModel.standardProgramList(id) } ) + + LaunchedEffect(id) { + viewModel.trainingProgramList(id) + viewModel.standardProgramList(id) + } } @Composable internal fun HomeDetailProgramScreen( + id: String, modifier: Modifier = Modifier, + swipeRefreshState: SwipeRefreshState, + trainingProgramUiState: TrainingProgramListUiState, + standardProgramListUiState: StandardProgramListUiState, pagerState: PagerState = rememberPagerState(pageCount = { 2 }), coroutineScope: CoroutineScope = rememberCoroutineScope(), - programItem: ImmutableList, onBackClick: () -> Unit, - navigateToProgramDetail: () -> Unit, + navigateToProgramDetail: (Long) -> Unit, + getTrainingProgramList: () -> Unit, + getStandardProgramList: () -> Unit ) { ExpoAndroidTheme { colors, typography -> Column( @@ -80,10 +104,12 @@ internal fun HomeDetailProgramScreen( ) { ExpoTopBar( - startIcon = { LeftArrowIcon( + startIcon = { + LeftArrowIcon( tint = colors.black, modifier = Modifier.expoClickable { onBackClick() } - ) }, + ) + }, betweenText = "프로그램", modifier = Modifier.padding(horizontal = 16.dp) ) @@ -99,7 +125,7 @@ internal fun HomeDetailProgramScreen( modifier = modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), ) }, - modifier = Modifier.width(230.dp) + modifier = Modifier.width(280.dp) ) { persistentListOf( "일반 프로그램", @@ -160,20 +186,50 @@ internal fun HomeDetailProgramScreen( } } - HorizontalPager(state = pagerState) { page -> - when (page) { - 0 -> { - ProgramList( - item = programItem, - navigateToProgramDetail = navigateToProgramDetail - ) - } - - 1 -> { - ProgramList( - item = programItem, - navigateToProgramDetail = navigateToProgramDetail - ) + SwipeRefresh( + state = swipeRefreshState, + onRefresh = { + getTrainingProgramList() + getStandardProgramList() + }, + indicator = { state, refreshTrigger -> + SwipeRefreshIndicator( + state = state, + refreshTriggerDistance = refreshTrigger, + contentColor = colors.main + ) + } + ) { + HorizontalPager(state = pagerState) { page -> + when (page) { + 0 -> { + when (standardProgramListUiState) { + is StandardProgramListUiState.Loading -> Unit + is StandardProgramListUiState.Success -> { + StandardProgramList( + standardItem = standardProgramListUiState.data.toImmutableList(), + navigateToProgramDetail = navigateToProgramDetail + ) + } + is StandardProgramListUiState.Empty -> Unit + is StandardProgramListUiState.Error -> Unit + } + } + + 1 -> { + when (trainingProgramUiState) { + is TrainingProgramListUiState.Loading -> Unit + is TrainingProgramListUiState.Success -> { + ProgramList( + trainingItem = trainingProgramUiState.data.toImmutableList(), + navigateToProgramDetail = navigateToProgramDetail + ) + } + + is TrainingProgramListUiState.Empty -> Unit + is TrainingProgramListUiState.Error -> Unit + } + } } } } @@ -185,20 +241,13 @@ internal fun HomeDetailProgramScreen( @Composable private fun HomeDetailProgramScreenPreview() { HomeDetailProgramScreen( - programItem = persistentListOf( - ProgramTempList( - programName = "adsfasfas", - check = true, - must = true - ), - - ProgramTempList( - programName = "adsfasfas", - check = true, - must = true - ), - ), onBackClick = {}, - navigateToProgramDetail = {} + navigateToProgramDetail = {}, + id = "", + trainingProgramUiState = TrainingProgramListUiState.Loading, + standardProgramListUiState = StandardProgramListUiState.Loading, + swipeRefreshState = rememberSwipeRefreshState(isRefreshing = false), + getTrainingProgramList = {}, + getStandardProgramList = {} ) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/QrScannerScreen.kt b/feature/home/src/main/java/com/school_of_company/home/view/QrScannerScreen.kt new file mode 100644 index 00000000..f02efb5f --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/view/QrScannerScreen.kt @@ -0,0 +1,114 @@ +package com.school_of_company.home.view + +import android.Manifest +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState +import com.google.accompanist.permissions.shouldShowRationale +import com.school_of_company.design_system.component.modifier.clickable.expoClickable +import com.school_of_company.design_system.component.topbar.ExpoTopBar +import com.school_of_company.design_system.icon.LeftArrowIcon +import com.school_of_company.design_system.theme.ExpoAndroidTheme +import com.school_of_company.home.view.component.QrcodeScanView +import com.school_of_company.home.viewmodel.HomeViewModel +import com.school_of_company.home.viewmodel.uistate.TrainingQrCodeUiState +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam +import com.school_of_company.ui.toast.makeToast + +@OptIn(ExperimentalPermissionsApi::class) +@Composable +internal fun QrScannerRoute( + id: Long, + traineeId: Long, + onBackClick: () -> Unit, + onPermissionBlock: () -> Unit, + viewModel: HomeViewModel = hiltViewModel() +) { + val trainingQrCodeUiState by viewModel.trainingQrCodeUiState.collectAsStateWithLifecycle() + + val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA) + + val context = LocalContext.current + + LaunchedEffect("getPermission") { + if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) { + cameraPermissionState.launchPermissionRequest() + } + } + + LaunchedEffect(trainingQrCodeUiState) { + when (trainingQrCodeUiState) { + is TrainingQrCodeUiState.Loading -> { + makeToast(context, "로딩중..") + } + is TrainingQrCodeUiState.Success -> { + makeToast(context, "인식 성공!") + onBackClick() + } + is TrainingQrCodeUiState.Error -> { + makeToast(context, "인식을 하지 못하였습니다.") + } + } + } + + if (cameraPermissionState.status.isGranted) { + QrScannerScreen( + onBackClick = onBackClick, + onQrcodeScan = { + viewModel.trainingQrCode( + trainingId = id, + body = TrainingQrCodeRequestParam(traineeId = traineeId) + ) + } + ) + } else { + onPermissionBlock() + } +} + +@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) +@Composable +internal fun QrScannerScreen( + modifier: Modifier = Modifier, + onBackClick: () -> Unit, + onQrcodeScan: (Long) -> Unit, +) { + ExpoAndroidTheme { colors, _ -> + + QrcodeScanView(onQrcodeScan = onQrcodeScan) + + Column( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding() + .statusBarsPadding() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + ExpoTopBar( + startIcon = { + LeftArrowIcon( + tint = colors.white, + modifier = Modifier + .expoClickable { onBackClick() } + .padding(top = 16.dp) + ) + } + ) + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantList.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantList.kt index 11115d3d..2ed294ef 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantList.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantList.kt @@ -6,17 +6,19 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.school_of_company.design_system.theme.ExpoAndroidTheme +import com.school_of_company.model.entity.training.TeacherTrainingProgramResponseEntity import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Composable fun HomeDetailProgramParticipantList( modifier: Modifier = Modifier, - item: ImmutableList = persistentListOf() + item: ImmutableList = persistentListOf() ) { ExpoAndroidTheme { colors, _ -> LazyColumn( @@ -38,22 +40,5 @@ fun HomeDetailProgramParticipantList( @Preview @Composable private fun HomeDetailProgramParticipantListPreview() { - HomeDetailProgramParticipantList( - item = persistentListOf( - HomeDetailProgramParticipantData( - name = "이명훈", - company = "초등학교", - position = "교사", - schoolSubject = "컴퓨터공학", - phone = "010-1234-5678" - ), - HomeDetailProgramParticipantData( - name = "이명훈", - company = "초등학교", - position = "교사", - schoolSubject = "컴퓨터공학", - phone = "010-1234-5678" - ), - ) - ) + } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantListItem.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantListItem.kt index bd4a72d8..b925da74 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantListItem.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/HomeDetailProgramParticipantListItem.kt @@ -17,20 +17,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.school_of_company.design_system.theme.ExpoAndroidTheme - -data class HomeDetailProgramParticipantData( - val name: String, - val company: String, - val position: String, - val schoolSubject: String, - val phone: String -) +import com.school_of_company.model.entity.training.TeacherTrainingProgramResponseEntity @Composable fun HomeDetailProgramParticipantListItem( modifier: Modifier = Modifier, index: Int, - data: HomeDetailProgramParticipantData, + data: TeacherTrainingProgramResponseEntity, horizontalScrollState: ScrollState = rememberScrollState() ) { ExpoAndroidTheme { colors, typography -> @@ -58,7 +51,7 @@ fun HomeDetailProgramParticipantListItem( modifier = Modifier.width(80.dp) ) Text( - text = data.company, + text = data.organization, style = typography.captionRegular2, color = colors.black, modifier = Modifier.width(100.dp) @@ -70,13 +63,13 @@ fun HomeDetailProgramParticipantListItem( modifier = Modifier.width(80.dp) ) Text( - text = data.schoolSubject, + text = "교사", style = typography.captionRegular2, color = colors.black, modifier = Modifier.width(120.dp) ) Text( - text = data.phone, + text = "010-3825-1716", style = typography.captionRegular2, color = colors.black, modifier = Modifier.width(100.dp) @@ -88,14 +81,4 @@ fun HomeDetailProgramParticipantListItem( @Preview @Composable private fun HomeDetailProgramParticipantListItemPreview() { - HomeDetailProgramParticipantListItem( - index = 1, - data = HomeDetailProgramParticipantData( - name = "이명훈", - company = "초등학교", - position = "교사", - schoolSubject = "컴퓨터공학", - phone = "010-1234-5678" - ) - ) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramList.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramList.kt index 45a45074..d0d8a135 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramList.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramList.kt @@ -1,5 +1,6 @@ package com.school_of_company.home.view.component +import android.text.style.TabStopSpan.Standard import androidx.compose.foundation.background import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -10,14 +11,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.school_of_company.design_system.theme.ExpoAndroidTheme +import com.school_of_company.model.entity.standard.StandardProgramListResponseEntity +import com.school_of_company.model.entity.training.TrainingProgramListResponseEntity import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @Composable fun ProgramList( modifier: Modifier = Modifier, - item: ImmutableList = persistentListOf(), - navigateToProgramDetail: () -> Unit + trainingItem: ImmutableList = persistentListOf(), + navigateToProgramDetail: (Long) -> Unit ) { ExpoAndroidTheme { colors, _ -> @@ -27,7 +30,7 @@ fun ProgramList( .background(color = colors.white) .padding(horizontal = 16.dp) ) { - itemsIndexed(item) { index, item -> + itemsIndexed(trainingItem) { index, item -> ProgramListItem( index = index + 1, data = item, @@ -38,42 +41,32 @@ fun ProgramList( } } +@Composable +fun StandardProgramList( + modifier: Modifier = Modifier, + standardItem: ImmutableList = persistentListOf(), + navigateToProgramDetail: (Long) -> Unit +) { + ExpoAndroidTheme { colors, _ -> + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(color = colors.white) + .padding(horizontal = 16.dp) + ) { + itemsIndexed(standardItem) { index, item -> + StandardProgramListItem( + index = index + 1, + data = item, + navigateToProgramDetail = navigateToProgramDetail + ) + } + } + } +} + @Preview @Composable private fun ProgramListPreview() { - ProgramList( - item = persistentListOf( - ProgramTempList( - programName = "adsfasfas", - check = true, - must = true - ), - ProgramTempList( - programName = "adsfasdf", - check = true, - must = false - ), - ProgramTempList( - programName = "adsfasdf", - check = false, - must = true - ), - ProgramTempList( - programName = "adsfasdf", - check = true, - must = false - ), - ProgramTempList( - programName = "adsfasdf", - check = false, - must = false - ), - ProgramTempList( - programName = "adsfasdf", - check = false, - must = false - ), - ), - navigateToProgramDetail = {} - ) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramListItem.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramListItem.kt index d33cad53..b5334971 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramListItem.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramListItem.kt @@ -18,19 +18,15 @@ import com.school_of_company.design_system.component.modifier.clickable.expoClic import com.school_of_company.design_system.icon.CircleIcon import com.school_of_company.design_system.icon.XIcon import com.school_of_company.design_system.theme.ExpoAndroidTheme - -data class ProgramTempList( - val programName: String, - val check: Boolean, - val must: Boolean -) +import com.school_of_company.home.enum.ProgramEnum +import com.school_of_company.model.entity.training.TrainingProgramListResponseEntity @Composable fun ProgramListItem( modifier: Modifier = Modifier, index: Int, - data: ProgramTempList, - navigateToProgramDetail: () -> Unit + data: TrainingProgramListResponseEntity, + navigateToProgramDetail: (Long) -> Unit ) { ExpoAndroidTheme { colors, typography -> @@ -42,7 +38,7 @@ fun ProgramListItem( .fillMaxWidth() .background(color = colors.white) .padding(vertical = 10.dp) - .expoClickable { navigateToProgramDetail() } + .expoClickable { navigateToProgramDetail(data.id) } ) { Text( @@ -54,7 +50,7 @@ fun ProgramListItem( Text( - text = data.programName, + text = data.title, style = typography.captionRegular2, color = colors.black, maxLines = 1, @@ -63,38 +59,42 @@ fun ProgramListItem( ) - if (data.check) { - CircleIcon( - tint = colors.black, - modifier = Modifier - .size(16.dp) - .weight(1f) - ) - } else { - XIcon( - tint = colors.error, - modifier = Modifier - .size(16.dp) - .weight(1f) - ) - } - + when (data.category) { + "ESSENTIAL" -> { + CircleIcon( + tint = colors.black, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + XIcon( + tint = colors.error, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + } - if (data.must) { - CircleIcon( - tint = colors.black, - modifier = Modifier - .size(16.dp) - .weight(1f) - ) - } else { - XIcon( - tint = colors.error, - modifier = Modifier - .size(16.dp) - .weight(1f) - ) + "CHOICE" -> { + XIcon( + tint = colors.error, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + CircleIcon( + tint = colors.black, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + } + + else -> { + // 기본적으로 보여줄 UI가 있다면 여기에 작성 + } } + } } } @@ -102,13 +102,4 @@ fun ProgramListItem( @Preview @Composable private fun ProgramListItemPreview() { - ProgramListItem( - index = 1, - data = ProgramTempList( - programName = "프로그램 이름", - check = true, - must = false - ), - navigateToProgramDetail = {} - ) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramTabRowItem.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramTabRowItem.kt index 238c9538..295e8b31 100644 --- a/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramTabRowItem.kt +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/ProgramTabRowItem.kt @@ -1,5 +1,6 @@ package com.school_of_company.home.view.component +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text @@ -7,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.school_of_company.design_system.component.modifier.clickable.expoClickable import com.school_of_company.design_system.theme.ExpoAndroidTheme @@ -20,6 +22,7 @@ fun ProgramTabRowItem( ) { ExpoAndroidTheme { colors, typography -> Row( + horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, modifier = modifier .expoClickable(onClick = onClick) @@ -30,6 +33,7 @@ fun ProgramTabRowItem( style = typography.bodyBold1, fontWeight = if (isCurrentIndex) FontWeight.SemiBold else FontWeight.Normal, color = if (isCurrentIndex) colors.black else colors.gray500, + textAlign = TextAlign.Center ) } } diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/QrButton.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/QrButton.kt new file mode 100644 index 00000000..6a37bbc6 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/QrButton.kt @@ -0,0 +1,55 @@ +package com.school_of_company.home.view.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.school_of_company.design_system.component.modifier.clickable.expoClickable +import com.school_of_company.design_system.theme.ExpoAndroidTheme + +@Composable +fun QrButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + ExpoAndroidTheme { colors, typography -> + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .background( + color = colors.main, + shape = RoundedCornerShape(10.dp), + ) + .expoClickable( + onClick = onClick, + rippleColor = colors.white + ) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.Start), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding( + horizontal = 12.dp, + vertical = 8.dp + ) + ) { + Text( + text = "QR 스캔", + style = typography.bodyBold2, + fontWeight = FontWeight.SemiBold, + color = colors.white, + ) + } + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/QrScannView.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/QrScannView.kt new file mode 100644 index 00000000..817cdb2c --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/QrScannView.kt @@ -0,0 +1,115 @@ +package com.school_of_company.home.view.component + +import android.util.Log +import androidx.camera.core.CameraSelector +import androidx.camera.core.FocusMeteringAction +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.Preview +import androidx.camera.core.resolutionselector.AspectRatioStrategy +import androidx.camera.core.resolutionselector.ResolutionSelector +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.camera.view.PreviewView +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import androidx.lifecycle.LifecycleOwner +import com.school_of_company.home.util.QrcodeScanner +import java.util.concurrent.Executors + +@androidx.camera.core.ExperimentalGetImage +@Composable +internal fun QrcodeScanView( + modifier: Modifier = Modifier, + onQrcodeScan: (Long) -> Unit +) { + Scaffold( + modifier = modifier.fillMaxSize(), + ) { innerPadding: PaddingValues -> + AndroidView( + factory = { context -> + var isScanningEnabled = true + + val cameraExecutor = Executors.newSingleThreadExecutor() + val previewView = PreviewView(context).apply { + scaleType = PreviewView.ScaleType.FILL_CENTER + } + + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder() + .build() + .apply { + surfaceProvider = previewView.surfaceProvider + } + + val imageCapture = ImageCapture.Builder().build() + + val imageAnalyzer = ImageAnalysis.Builder() + .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) + .setResolutionSelector( + ResolutionSelector.Builder() + .setAspectRatioStrategy( + AspectRatioStrategy.RATIO_4_3_FALLBACK_AUTO_STRATEGY + ) + .build() + ) + .setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_YUV_420_888) + .setTargetRotation(previewView.display.rotation) + .build() + .apply { + setAnalyzer(cameraExecutor, QrcodeScanner { qrcodeData -> + if (isScanningEnabled) { + isScanningEnabled = false + onQrcodeScan(qrcodeData) + } + }) + } + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + + val camera = cameraProvider.bindToLifecycle( + context as LifecycleOwner, + cameraSelector, + preview, + imageCapture, + imageAnalyzer + ) + + val cameraControl = camera.cameraControl + + val meteringPointFactory = previewView.meteringPointFactory + val meteringPoint = meteringPointFactory.createPoint( + previewView.width / 2f, + previewView.height / 2f + ) + val focusMeteringAction = FocusMeteringAction.Builder(meteringPoint) + .setAutoCancelDuration(5, java.util.concurrent.TimeUnit.SECONDS) + .build() + + cameraControl.startFocusAndMetering(focusMeteringAction) + cameraControl.setExposureCompensationIndex(0) + } catch (e: Exception) { + Log.e("QrcodeScanView", "Camera binding failed", e) + } + }, ContextCompat.getMainExecutor(context)) + + previewView + }, + modifier = Modifier + .fillMaxSize() + .padding(innerPadding) + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/view/component/StandardProgramListItem.kt b/feature/home/src/main/java/com/school_of_company/home/view/component/StandardProgramListItem.kt new file mode 100644 index 00000000..7dfb1b86 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/view/component/StandardProgramListItem.kt @@ -0,0 +1,73 @@ +package com.school_of_company.home.view.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.school_of_company.design_system.component.modifier.clickable.expoClickable +import com.school_of_company.design_system.icon.XIcon +import com.school_of_company.design_system.theme.ExpoAndroidTheme +import com.school_of_company.model.entity.standard.StandardProgramListResponseEntity + +@Composable +fun StandardProgramListItem( + modifier: Modifier = Modifier, + index: Int, + data: StandardProgramListResponseEntity, + navigateToProgramDetail: (Long) -> Unit +) { + ExpoAndroidTheme { colors, typography -> + + Spacer(modifier = Modifier.height(20.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .background(color = colors.white) + .padding(vertical = 10.dp) + .expoClickable { navigateToProgramDetail(data.id) } + ) { + + Text( + text = index.toString(), + style = typography.captionBold1, + color = colors.black, + modifier = Modifier.weight(0.5f) + ) + + + Text( + text = data.title, + style = typography.captionRegular2, + color = colors.black, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(2f) + ) + + XIcon( + tint = colors.error, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + + XIcon( + tint = colors.error, + modifier = Modifier + .size(16.dp) + .weight(1f) + ) + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/HomeViewModel.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/HomeViewModel.kt index ae7f3282..1f568304 100644 --- a/feature/home/src/main/java/com/school_of_company/home/viewmodel/HomeViewModel.kt +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/HomeViewModel.kt @@ -1,13 +1,37 @@ package com.school_of_company.home.viewmodel +import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.school_of_company.common.result.Result +import com.school_of_company.common.result.asResult +import com.school_of_company.domain.usecase.attendance.TrainingQrCodeRequestUseCase +import com.school_of_company.domain.usecase.standard.StandardProgramAttendListUseCase +import com.school_of_company.domain.usecase.standard.StandardProgramListUseCase +import com.school_of_company.domain.usecase.training.TeacherTrainingProgramListUseCase +import com.school_of_company.domain.usecase.training.TrainingProgramListUseCase +import com.school_of_company.home.viewmodel.uistate.StandardProgramAttendListUiState +import com.school_of_company.home.viewmodel.uistate.StandardProgramListUiState +import com.school_of_company.home.viewmodel.uistate.TeacherTrainingProgramListUiState +import com.school_of_company.home.viewmodel.uistate.TrainingProgramListUiState +import com.school_of_company.home.viewmodel.uistate.TrainingQrCodeUiState +import com.school_of_company.model.param.attendance.TrainingQrCodeRequestParam import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - /* todo : add UseCase */ + private val trainingProgramListUseCase: TrainingProgramListUseCase, + private val standardProgramListUseCase: StandardProgramListUseCase, + private val teacherTrainingProgramListUseCase: TeacherTrainingProgramListUseCase, + private val standardProgramAttendListUseCase: StandardProgramAttendListUseCase, + private val trainingQrCodeRequestUseCase: TrainingQrCodeRequestUseCase, private val savedStateHandle: SavedStateHandle ) : ViewModel() { companion object { @@ -15,10 +39,142 @@ class HomeViewModel @Inject constructor( private const val CONTENT = "content" } + private val _swipeRefreshLoading = MutableStateFlow(false) + val swipeRefreshLoading = _swipeRefreshLoading.asStateFlow() + + private val _trainingProgramListUiState = MutableStateFlow(TrainingProgramListUiState.Loading) + internal val trainingProgramListUiState = _trainingProgramListUiState.asStateFlow() + + private val _standardProgramListUiState = MutableStateFlow(StandardProgramListUiState.Loading) + internal val standardProgramListUiState = _standardProgramListUiState.asStateFlow() + + private val _teacherTrainingProgramListUiState = MutableStateFlow(TeacherTrainingProgramListUiState.Loading) + internal val teacherTrainingProgramListUiState = _teacherTrainingProgramListUiState.asStateFlow() + + private val _standardProgramAttendListUiState = MutableStateFlow(StandardProgramAttendListUiState.Loading) + internal val standardProgramAttendListUiState = _standardProgramAttendListUiState.asStateFlow() + + private val _trainingQrCodeUiState = MutableStateFlow(TrainingQrCodeUiState.Loading) + internal val trainingQrCodeUiState = _trainingQrCodeUiState.asStateFlow() + internal var title = savedStateHandle.getStateFlow(key = TITLE, initialValue = "") internal var content = savedStateHandle.getStateFlow(key = CONTENT, initialValue = "") + internal fun trainingProgramList(expoId: String) = viewModelScope.launch { + _swipeRefreshLoading.value = true + trainingProgramListUseCase(expoId = expoId) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _trainingProgramListUiState.value = TrainingProgramListUiState.Loading + is Result.Success -> { + if (result.data.isEmpty()) { + _trainingProgramListUiState.value = TrainingProgramListUiState.Empty + _swipeRefreshLoading.value = false + } else { + _trainingProgramListUiState.value = TrainingProgramListUiState.Success(result.data) + _swipeRefreshLoading.value = false + } + } + is Result.Error -> { + _trainingProgramListUiState.value = TrainingProgramListUiState.Error(result.exception) + _swipeRefreshLoading.value = false + } + } + } + } + + internal fun standardProgramList(expoId: String) = viewModelScope.launch { + _swipeRefreshLoading.value = true + standardProgramListUseCase(expoId = expoId) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _standardProgramListUiState.value = StandardProgramListUiState.Loading + is Result.Success -> { + if (result.data.isEmpty()) { + _standardProgramListUiState.value = StandardProgramListUiState.Empty + _swipeRefreshLoading.value = false + } else { + _standardProgramListUiState.value = StandardProgramListUiState.Success(result.data) + _swipeRefreshLoading.value = false + } + } + is Result.Error -> { + _standardProgramListUiState.value = StandardProgramListUiState.Error(result.exception) + _swipeRefreshLoading.value = false + } + } + } + } + + internal fun teacherTrainingProgramList(trainingProId: Long) = viewModelScope.launch { + _teacherTrainingProgramListUiState.value = TeacherTrainingProgramListUiState.Loading + teacherTrainingProgramListUseCase(trainingProId = trainingProId) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _teacherTrainingProgramListUiState.value = TeacherTrainingProgramListUiState.Loading + is Result.Success -> { + if (result.data.isEmpty()) { + _teacherTrainingProgramListUiState.value = TeacherTrainingProgramListUiState.Empty + } else { + _teacherTrainingProgramListUiState.value = TeacherTrainingProgramListUiState.Success(result.data) + } + } + is Result.Error -> { + _teacherTrainingProgramListUiState.value = TeacherTrainingProgramListUiState.Error(result.exception) + } + } + } + } + + internal fun standardProgramList(standardProId: Long) = viewModelScope.launch { + _swipeRefreshLoading.value = true + standardProgramAttendListUseCase(standardProId = standardProId) + .asResult() + .collectLatest { result -> + when (result) { + is Result.Loading -> _standardProgramAttendListUiState.value = StandardProgramAttendListUiState.Loading + is Result.Success -> { + if (result.data.isEmpty()) { + _standardProgramAttendListUiState.value = StandardProgramAttendListUiState.Empty + _swipeRefreshLoading.value = false + } else { + _standardProgramAttendListUiState.value = StandardProgramAttendListUiState.Success(result.data) + _swipeRefreshLoading.value = false + } + } + is Result.Error -> { + _standardProgramAttendListUiState.value = StandardProgramAttendListUiState.Error(result.exception) + _swipeRefreshLoading.value = false + } + } + } + } + + internal fun trainingQrCode( + trainingId: Long, + body: TrainingQrCodeRequestParam + ) = viewModelScope.launch { + _trainingQrCodeUiState.value = TrainingQrCodeUiState.Loading + trainingQrCodeRequestUseCase( + trainingId = trainingId, + body = body + ) + .onSuccess { + it.catch { remoteError -> + _trainingQrCodeUiState.value = TrainingQrCodeUiState.Error(remoteError) + }.collect { + _trainingQrCodeUiState.value = TrainingQrCodeUiState.Success + } + } + .onFailure { error -> + _trainingQrCodeUiState.value = TrainingQrCodeUiState.Error(error) + } + } + internal fun onTitleChange(value: String) { savedStateHandle[TITLE] = value } diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramAttendListUiState.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramAttendListUiState.kt new file mode 100644 index 00000000..058c8465 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramAttendListUiState.kt @@ -0,0 +1,10 @@ +package com.school_of_company.home.viewmodel.uistate + +import com.school_of_company.model.entity.standard.StandardAttendListResponseEntity + +sealed interface StandardProgramAttendListUiState { + object Loading : StandardProgramAttendListUiState + object Empty : StandardProgramAttendListUiState + data class Success(val data: List) : StandardProgramAttendListUiState + data class Error(val exception: Throwable) : StandardProgramAttendListUiState +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramListUiState.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramListUiState.kt new file mode 100644 index 00000000..6c212037 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/StandardProgramListUiState.kt @@ -0,0 +1,10 @@ +package com.school_of_company.home.viewmodel.uistate + +import com.school_of_company.model.entity.standard.StandardProgramListResponseEntity + +sealed interface StandardProgramListUiState { + object Loading : StandardProgramListUiState + object Empty : StandardProgramListUiState + data class Success(val data: List) : StandardProgramListUiState + data class Error(val exception: Throwable) : StandardProgramListUiState +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TeacherTrainingProgramListUiState.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TeacherTrainingProgramListUiState.kt new file mode 100644 index 00000000..c020a481 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TeacherTrainingProgramListUiState.kt @@ -0,0 +1,10 @@ +package com.school_of_company.home.viewmodel.uistate + +import com.school_of_company.model.entity.training.TeacherTrainingProgramResponseEntity + +sealed interface TeacherTrainingProgramListUiState { + object Loading: TeacherTrainingProgramListUiState + object Empty: TeacherTrainingProgramListUiState + data class Success(val data: List): TeacherTrainingProgramListUiState + data class Error(val exception: Throwable): TeacherTrainingProgramListUiState +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingProgramListUiState.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingProgramListUiState.kt new file mode 100644 index 00000000..e9840ed6 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingProgramListUiState.kt @@ -0,0 +1,10 @@ +package com.school_of_company.home.viewmodel.uistate + +import com.school_of_company.model.entity.training.TrainingProgramListResponseEntity + +sealed interface TrainingProgramListUiState { + object Loading : TrainingProgramListUiState + object Empty : TrainingProgramListUiState + data class Success(val data: List) : TrainingProgramListUiState + data class Error(val exception: Throwable) : TrainingProgramListUiState +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingQrCodeUiState.kt b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingQrCodeUiState.kt new file mode 100644 index 00000000..d01485e1 --- /dev/null +++ b/feature/home/src/main/java/com/school_of_company/home/viewmodel/uistate/TrainingQrCodeUiState.kt @@ -0,0 +1,7 @@ +package com.school_of_company.home.viewmodel.uistate + +sealed interface TrainingQrCodeUiState { + object Loading : TrainingQrCodeUiState + object Success: TrainingQrCodeUiState + data class Error(val exception: Throwable) : TrainingQrCodeUiState +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 053a5414..d40a94bb 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,7 +26,7 @@ androidx-test-ext-junit = "1.2.1" androidxWearCompose = "1.4.0" androidxWindowManager = "1.3.0" camera = "1.4.0" -cameraRc = "1.3.0-beta01-rc01@aar" +cameraRc = "1.4.0" coil = "2.4.0" converter-moshi = "2.9.0" firebaseAnalytics = "21.6.2" @@ -44,6 +44,8 @@ junit4 = "4.13.2" junit = "4.13.2" kotlin = "1.8.10" kotlinxCoroutines = "1.7.1" +kotlinxCoroutinesCore = "1.8.1" +kotlinxCoroutinesPlayServices = "1.7.3" kotlinxDateTime = "0.4.0" kotlinxImmutable = "0.3.5" kotlinxSerializationJson = "1.7.3" @@ -128,7 +130,9 @@ hilt-ext-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hi app-update-ktx = { group = "com.google.android.play", name = "app-update-ktx", version.ref = "inAppUpdate"} kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines" } +kotlinx-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDateTime" } kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinxImmutable" }