From e44d101a5fca7ba9aeb05f33d4f803ec3a07be1a Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Fri, 9 Feb 2024 18:47:27 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[feat/#76]=20snackbar=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/home/attend/AttendViewModel.kt | 2 +- .../ui/home/attend/AttendButton.kt | 4 +- .../ui/home/attend/camera/CameraPreview.kt | 79 +++++++++++++++++-- 3 files changed, 76 insertions(+), 9 deletions(-) diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt index 3eaa5ba..c013ed1 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -162,6 +162,6 @@ class AttendViewModel @Inject constructor( } enum class AttendSnackBarEvent { - Attend_success, Attend_fail + Attend_success, Attend_fail, None } } \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt index 430aa8c..0d719ad 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt @@ -28,7 +28,9 @@ fun AttendBtnOn(navController: NavHostController) { .height(64.dp) , colors = ButtonDefaults.buttonColors(containerColor = KusitmsColorPalette.current.Main600) , shape = RoundedCornerShape(size = 12.dp), - onClick = { navController.navigate(NavRoutes.CameraPreview.route) } + onClick = { + navController.navigate(NavRoutes.CameraPreview.route) + } ) { Text(text = stringResource(R.string.attend_btn_attend), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.White, maxLines = 1) } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index e7cf49a..b50572a 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -14,10 +14,11 @@ import androidx.camera.core.Preview import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -33,6 +34,7 @@ import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView @@ -42,7 +44,9 @@ import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.common.InputImage import com.google.zxing.* import com.google.zxing.common.HybridBinarizer +import com.kusitms.presentation.R import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette +import com.kusitms.presentation.common.ui.theme.KusitmsTypo import com.kusitms.presentation.model.home.attend.AttendViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -51,8 +55,20 @@ import kotlinx.coroutines.launch @Composable fun CameraScreen( viewModel: AttendViewModel, - navController: NavController ) { + val snackbarHostState = remember { SnackbarHostState() } + val snackbarContentState = remember { mutableStateOf>(AttendViewModel.AttendSnackBarEvent.None to "") } + + LaunchedEffect(key1 = Unit) { + viewModel.snackbarEvent.collect { event -> + val message = when (event) { + AttendViewModel.AttendSnackBarEvent.Attend_success -> "출석이 완료되었습니다" + AttendViewModel.AttendSnackBarEvent.Attend_fail -> "QR코드를 다시 확인해주세요" + else -> "화면 정가운데에 QR코드를 스캔해주세요" + } + snackbarContentState.value = event to message + } + } ComposablePermission( permission = Manifest.permission.CAMERA, onDenied = { requester -> @@ -69,11 +85,64 @@ fun CameraScreen( viewModel.updateScannedQrCode(qrText) viewModel.postAttendCheck() } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + CameraSnackbar(snackbarHostState = snackbarHostState) { + val (event, message) = snackbarContentState.value + when (event) { + AttendViewModel.AttendSnackBarEvent.Attend_success -> { + Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey200) + } + AttendViewModel.AttendSnackBarEvent.Attend_fail -> { + Image(painterResource(id = R.drawable.ic_warning_sigb), contentDescription = null) + Spacer(modifier = Modifier.width(10.dp)) + Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub2) + } + else -> { Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey200) } + } + } + } CameraPreview(onQrCodeScanned = onQrCodeScanned) } ) } +@Composable +fun CameraSnackbar( + snackbarHostState: SnackbarHostState, + content: @Composable () -> Unit +) { + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier + .padding(top = 64.dp) + .fillMaxWidth(), + snackbar = { snackbarData -> + Box(modifier = androidx.compose.ui.Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 20.dp) + .background( + color = KusitmsColorPalette.current.Grey600, + shape = RoundedCornerShape(12.dp) + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(horizontal = 24.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + content() + } + } + }) +} + @Composable fun CameraOverlay() { Canvas(modifier = Modifier @@ -252,7 +321,6 @@ fun CameraPreview(onQrCodeScanned: (String) -> Unit) { cameraProvider.unbindAll() // 기존 바인딩 제거 try { - // 카메라와 분석기 바인딩 cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, @@ -264,10 +332,7 @@ fun CameraPreview(onQrCodeScanned: (String) -> Unit) { } } - // Compose에 카메라 프리뷰 표시 AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) - - // QR 코드 스캔 영역 표시 (CameraOverlay) CameraOverlay() } From 01e0e15239035762336d8388bfb2789bd7355b8a Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Fri, 9 Feb 2024 21:58:47 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[feat/#76]=20setting=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/home/attend/AttendViewModel.kt | 4 ++ .../model/setting/SettingUiModel.kt | 33 ++++++++-- .../ui/home/attend/camera/CameraPreview.kt | 61 +++++++++++-------- .../presentation/ui/setting/SettingMember.kt | 14 +++-- presentation/src/main/res/values/strings.xml | 5 ++ 5 files changed, 80 insertions(+), 37 deletions(-) diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt index c013ed1..40c2103 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -65,6 +65,9 @@ class AttendViewModel @Inject constructor( initialValue = AttendInfoModel(0, "", false, "", "") ) + private val _qrEnabled = MutableStateFlow(true) + val qrEnabled: StateFlow = _qrEnabled.asStateFlow() + private val _attendCheckModel = MutableStateFlow( AttendCheckModel(curriculumId = upcomingAttend.value.curriculumId, text = "") ) @@ -107,6 +110,7 @@ class AttendViewModel @Inject constructor( .collectLatest { Log.d("출석 확인", "출석 성공") _snackbarEvent.emit(AttendSnackBarEvent.Attend_success) + _qrEnabled.value = true } } } diff --git a/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt index bd6a2cb..2f76c88 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt @@ -1,5 +1,7 @@ package com.kusitms.presentation.model.setting +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.text.font.FontVariation import androidx.lifecycle.ViewModel @@ -7,6 +9,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.NavHostController import com.kusitms.domain.usecase.signin.MemberLogOutUseCase import com.kusitms.domain.usecase.signin.MemberSignOutUseCase +import com.kusitms.presentation.R import com.kusitms.presentation.model.login.LoginStatus import com.kusitms.presentation.model.signIn.SignInViewModel import com.kusitms.presentation.navigation.NavRoutes @@ -40,7 +43,7 @@ class SettingViewModel @Inject constructor( viewModelScope.launch { memberLogOutUseCase() .onSuccess { - updateSettingStatus(SettingStatus.LOGOUT) + updateSettingStatus(SettingStatus.LOGOUT_SUCCESS) } .onFailure { updateSettingStatus(SettingStatus.ERROR) @@ -52,7 +55,7 @@ class SettingViewModel @Inject constructor( viewModelScope.launch { memberSignOutUseCase() .onSuccess { - updateSettingStatus(SettingStatus.SIGNOUT) + updateSettingStatus(SettingStatus.SIGNOUT_SUCCESS) } .onFailure { updateSettingStatus(SettingStatus.ERROR) @@ -60,8 +63,26 @@ class SettingViewModel @Inject constructor( } } - companion object { - enum class SettingStatus { LOGOUT, SIGNOUT, DEFAULT, ERROR} + + enum class SettingStatus { + LOGOUT, LOGOUT_SUCCESS, SIGNOUT_INIT,SIGNOUT, SIGNOUT_SUCCESS, DEFAULT, ERROR; + + @StringRes + fun getTitleResId(): Int { + return when (this) { + LOGOUT -> R.string.logout_dialog_title + SIGNOUT -> R.string.signout_dialog_title + else -> R.string.signout_blank_title + } + } + @StringRes + fun getContentResId(): Int { + return when (this) { + LOGOUT -> R.string.logout_dialog_content + SIGNOUT -> R.string.signout_dialog_content + else -> R.string.signout_blank_title + } + } } } @@ -84,8 +105,8 @@ fun getMemberSetting(viewModel: SettingViewModel, navController: NavHostControll SettingUiModel(title = "개인정보 처리 방침", url = "https://sheer-billboard-d63.notion.site/KUSITMS-9e6619383bcd4ce68b6ba4b2b6ef0d40?pvs=4"), SettingUiModel(title = "서비스 이용약관", url = "https://sheer-billboard-d63.notion.site/24a4639559d4433cb89c8f1abb889726?pvs=4"), SettingUiModel(title = "비밀번호 변경", onClick = { goToSetPw(navController) }), - SettingUiModel(title = "로그아웃", onClick = { viewModel.logOut() }), - SettingUiModel(title = "회원탈퇴", onClick = { viewModel.signOut() }) + SettingUiModel(title = "로그아웃", onClick = { viewModel.updateSettingStatus(SettingViewModel.SettingStatus.LOGOUT) }), + SettingUiModel(title = "회원탈퇴", onClick = { viewModel.updateSettingStatus(SettingViewModel.SettingStatus.SIGNOUT_INIT) }) ) } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index b50572a..c0f3033 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -82,8 +82,10 @@ fun CameraScreen( }, onGranted = { val onQrCodeScanned: (String) -> Unit = { qrText -> - viewModel.updateScannedQrCode(qrText) - viewModel.postAttendCheck() + if(qrText != "") { + viewModel.updateScannedQrCode(qrText) + viewModel.postAttendCheck() + } } Box( modifier = Modifier.fillMaxSize(), @@ -104,7 +106,7 @@ fun CameraScreen( } } } - CameraPreview(onQrCodeScanned = onQrCodeScanned) + CameraPreview(viewModel = viewModel, onQrCodeScanned = onQrCodeScanned) } ) } @@ -270,11 +272,12 @@ private fun createCornersPath( @Composable @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) -fun CameraPreview(onQrCodeScanned: (String) -> Unit) { +fun CameraPreview(viewModel: AttendViewModel, onQrCodeScanned: (String) -> Unit) { val lifecycleOwner = LocalLifecycleOwner.current val context = LocalContext.current val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) } val previewView = remember { PreviewView(context) } + val qrEnabled by viewModel.qrEnabled.collectAsState() val preview = Preview.Builder().build().also { it.setSurfaceProvider(previewView.surfaceProvider) } @@ -285,32 +288,36 @@ fun CameraPreview(onQrCodeScanned: (String) -> Unit) { .build() .also { it.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> - val image = imageProxy.image - if (image != null) { - val buffer = image.planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) - val source = PlanarYUVLuminanceSource( - bytes, image.width, image.height, 0, 0, image.width, image.height, false - ) - val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) - try { - val reader = MultiFormatReader().apply { - val hints = mapOf( - DecodeHintType.POSSIBLE_FORMATS to listOf( - BarcodeFormat.QR_CODE + if(qrEnabled) { + val image = imageProxy.image + if (image != null) { + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + val source = PlanarYUVLuminanceSource( + bytes, image.width, image.height, 0, 0, image.width, image.height, false + ) + val binaryBitmap = BinaryBitmap(HybridBinarizer(source)) + try { + val reader = MultiFormatReader().apply { + val hints = mapOf( + DecodeHintType.POSSIBLE_FORMATS to listOf( + BarcodeFormat.QR_CODE + ) ) - ) - setHints(hints) + setHints(hints) + } + val result = reader.decode(binaryBitmap) + val qrText = result.text + onQrCodeScanned(qrText) + } catch (e: NotFoundException) { + // QR 코드를 찾지 못함 + } finally { + imageProxy.close() } - val result = reader.decode(binaryBitmap) - val qrText = result.text - onQrCodeScanned(qrText) - } catch (e: NotFoundException) { - // QR 코드를 찾지 못함 - } finally { - imageProxy.close() } + } else { + imageProxy.close() } } } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt b/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt index 4fa8938..be81fe3 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt @@ -39,14 +39,20 @@ fun SettingMember( LaunchedEffect(settingStatus) { when(settingStatus) { - SettingViewModel.Companion.SettingStatus.LOGOUT, SettingViewModel.Companion.SettingStatus.SIGNOUT -> { + SettingViewModel.SettingStatus.LOGOUT_SUCCESS, SettingViewModel.SettingStatus.SIGNOUT_SUCCESS-> { navController.navigate(NavRoutes.LogInScreen.route) { popUpTo(NavRoutes.SettingMember.route) { inclusive = true } } - - } + } + SettingViewModel.SettingStatus.LOGOUT -> { + openDialogState = true + } + SettingViewModel.SettingStatus.SIGNOUT -> { + openDialogState = true + } //처리.. 필요.. - SettingViewModel.Companion.SettingStatus.ERROR, SettingViewModel.Companion.SettingStatus.DEFAULT -> {} + SettingViewModel.SettingStatus.ERROR, SettingViewModel.SettingStatus.DEFAULT -> {} + else -> {} } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 7dd7fc1..46b3188 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -163,6 +163,11 @@ 로그아웃 로그아웃을 진행할까요? + 회원탈퇴 + + + 회원탈퇴를 진행할까요?\n 탈퇴하게 되면 정보가 삭제되며 되돌릴 수 없습니다. + 출석 다가오는 커리큘럼 %s From f396d261a0ecf74df47dfbf6c50802ae8ae1b492 Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Sat, 10 Feb 2024 02:17:23 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[feat/#76]=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=88=98=EC=A0=95=20&=20authInterceptor=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/di/AuthTokenInterceptor.kt | 23 ++++++++----- .../data/repository/AuthRepositoryImpl.kt | 3 ++ .../usecase/home/PostAttendCheckUseCase.kt | 12 ++----- .../model/home/attend/AttendViewModel.kt | 33 +++++++++++-------- .../model/setting/SettingUiModel.kt | 21 +++++++++--- .../presentation/navigation/NavGraph.kt | 3 +- .../ui/home/attend/AttendScreen.kt | 2 +- .../ui/home/attend/camera/CameraPreview.kt | 9 +++-- .../presentation/ui/setting/SettingMember.kt | 22 +++++++++---- presentation/src/main/res/values/strings.xml | 4 ++- 10 files changed, 84 insertions(+), 48 deletions(-) diff --git a/data/src/main/java/com/kusitms/data/remote/di/AuthTokenInterceptor.kt b/data/src/main/java/com/kusitms/data/remote/di/AuthTokenInterceptor.kt index 5a66828..9b02e47 100644 --- a/data/src/main/java/com/kusitms/data/remote/di/AuthTokenInterceptor.kt +++ b/data/src/main/java/com/kusitms/data/remote/di/AuthTokenInterceptor.kt @@ -23,18 +23,25 @@ class AuthTokenInterceptor @Inject constructor( authDataStore.authToken.first() } ?: "" + val refresh: String? = runBlocking { + authDataStore.refreshToken.first() + }?: "" + // "auth/logout" 엔드포인트 확인 - if (originalRequest.url.encodedPath.endsWith("auth/logout")) { - // logout 엔드포인트의 경우 refreshToken 사용 - requestBuilder.addHeader("Authorization", "${authDataStore.refreshToken}") - } else if(originalRequest.url.encodedPath.endsWith("auth/logout")) - else { - // 다른 엔드포인트의 경우 authToken 사용 + if (!token.isNullOrEmpty()) { requestBuilder.addHeader("Authorization", "$token") } - Log.d("Token is","${authDataStore.authToken}") - Log.d("Token is","${authDataStore.refreshToken}") + // 로그아웃 요청에는 리프레시 토큰도 추가 + if (originalRequest.url.encodedPath.endsWith("v1/auth/logout")) { + val refreshToken: String? = runBlocking { + authDataStore.refreshToken.first() + } + if (!refreshToken.isNullOrEmpty()) { + requestBuilder.addHeader("RefreshToken", "$refresh") + } + } + var request = requestBuilder.build() var response = chain.proceed(request) diff --git a/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt index 561afe2..ccbd204 100644 --- a/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt @@ -2,6 +2,7 @@ package com.kusitms.data.repository import android.content.Context import android.net.ConnectivityManager +import android.util.Log import com.kusitms.data.local.AuthDataStore import com.kusitms.data.local.DataStoreUtils import com.kusitms.data.remote.api.KusitmsApi @@ -43,7 +44,9 @@ class AuthRepositoryImpl @Inject constructor( override suspend fun logOutMember(): Result { return try { val response = kusitmsApi.logOutMember() + Log.d("response", response.toString()) if (response.result.code == 200) { + Log.d("로그아웃", "성공") authDataStore.clearAllData() Result.success(Unit) } else { diff --git a/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt index c2645c8..79c71eb 100644 --- a/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt +++ b/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt @@ -8,16 +8,10 @@ import javax.inject.Inject class PostAttendCheckUseCase @Inject constructor( private val homeRepository: HomeRepository ) { - operator fun invoke( + suspend operator fun invoke( curriculumId: Int, qrText: String - ): Flow = flow { - homeRepository.postAttendCheck( - curriculumId, qrText - ).onSuccess { - emit(Unit) - }.onFailure { - throw it + ): Result { + return homeRepository.postAttendCheck(curriculumId, qrText) } - } } \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt index 0d3726e..8bb750b 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -17,7 +17,9 @@ import com.kusitms.presentation.R import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette import com.kusitms.presentation.ui.notice.detail.NoticeDetailViewModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import timber.log.Timber import java.text.SimpleDateFormat @@ -37,14 +39,13 @@ class AttendViewModel @Inject constructor( private val getAttendInfoUseCase: GetAttendInfoUseCase, private val getAttendScoreUseCase: GetAttendScoreUseCase, getAttendQrUseCase: GetAttendQrUseCase, - private val getIsLoginUseCase: GetIsLoginUseCase, private val PostAttendCheckUseCase: PostAttendCheckUseCase ):ViewModel() { private val _attendListInit = MutableStateFlow>(emptyList()) val attendListInit : StateFlow> = _attendListInit.asStateFlow() - private val _upcomingAttend = MutableStateFlow(AttendInfoModel(0, "", false, "", "")) + private val _upcomingAttend = MutableStateFlow(AttendInfoModel(0, "커리큘럼이 없습니다", false, "", "")) val upcomingAttend : StateFlow = _upcomingAttend.asStateFlow() private val _attendScore = MutableStateFlow(AttendModel(0, 0, 0, 0, "수료 가능한 점수에요")) @@ -74,7 +75,7 @@ class AttendViewModel @Inject constructor( ) } }.collect { - _attendListInit + _attendListInit.value = it } } } @@ -87,7 +88,7 @@ class AttendViewModel @Inject constructor( }.collect { _upcomingAttend.emit(it) _attendCheckModel.emit( - AttendCheckModel(curriculumId = it.curriculumId, text = "") + AttendCheckModel(curriculumId = it.curriculumId, text = it.curriculumName) ) } } @@ -122,17 +123,21 @@ class AttendViewModel @Inject constructor( fun postAttendCheck() { viewModelScope.launch { - val model = attendCheckModel - PostAttendCheckUseCase(curriculumId = model.value.curriculumId, qrText = model.value.text). - catch { - Log.d("출석 확인", "출석 실패") - _snackbarEvent.emit(AttendSnackBarEvent.Attend_fail) - } - .collectLatest { - Log.d("출석 확인", "출석 성공") - _snackbarEvent.emit(AttendSnackBarEvent.Attend_success) - _qrEnabled.value = true + while (isActive) { + val model = attendCheckModel.value + if(model.curriculumId != 0) { + PostAttendCheckUseCase(curriculumId = model.curriculumId, qrText = model.text) + .onFailure{ + _snackbarEvent.emit(AttendSnackBarEvent.Attend_fail) + } + .onSuccess { + Log.d("출석 확인", "출석 성공") + _snackbarEvent.emit(AttendSnackBarEvent.Attend_success) + _qrEnabled.value = false + } + delay(10000L) // 다음 반복까지 10초 대기 } + } } } diff --git a/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt index 2f76c88..32ef944 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt @@ -1,5 +1,6 @@ package com.kusitms.presentation.model.setting +import android.util.Log import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.platform.UriHandler @@ -41,13 +42,17 @@ class SettingViewModel @Inject constructor( fun logOut() { viewModelScope.launch { - memberLogOutUseCase() - .onSuccess { + try { + val result = memberLogOutUseCase() + if (result.isSuccess) { + Log.d("로그아웃", "성공") updateSettingStatus(SettingStatus.LOGOUT_SUCCESS) - } - .onFailure { + } else { updateSettingStatus(SettingStatus.ERROR) } + } catch (e: Exception) { + updateSettingStatus(SettingStatus.ERROR) + } } } @@ -83,6 +88,14 @@ class SettingViewModel @Inject constructor( else -> R.string.signout_blank_title } } + @StringRes + fun getOkTextResId(): Int { + return when (this) { + LOGOUT -> R.string.logout_dialog_ok + SIGNOUT -> R.string.signout_dialog_ok + else -> R.string.signout_blank_title + } + } } } diff --git a/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt b/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt index 43a6667..0366539 100644 --- a/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt +++ b/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt @@ -171,8 +171,7 @@ fun MainNavigation() { kusitmsComposableWithAnimation(NavRoutes.CameraPreview.route) { CameraScreen( - attendViewModel, - navController + attendViewModel ) } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt index b8f8388..4bddeef 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt @@ -119,7 +119,7 @@ fun AttendPreColumn( navController: NavHostController ) { val curri by viewModel.upcomingAttend.collectAsState() - val curriculum = curri?.curriculumName ?: "" + val curriculum = curri.curriculumName ?: "커리큘럼이 없습니다" val eventDateTime = viewModel.combineDateAndTime(curri.date, curri.time) val currentTime = remember { mutableStateOf(LocalDateTime.now()) } val duration = eventDateTime?.let { diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index c0f3033..fefc926 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import com.google.mlkit.vision.barcode.BarcodeScanning import com.google.mlkit.vision.common.InputImage @@ -57,6 +58,8 @@ fun CameraScreen( viewModel: AttendViewModel, ) { val snackbarHostState = remember { SnackbarHostState() } + val qrEnabled by viewModel.qrEnabled.collectAsState() + val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope val snackbarContentState = remember { mutableStateOf>(AttendViewModel.AttendSnackBarEvent.None to "") } LaunchedEffect(key1 = Unit) { @@ -66,7 +69,9 @@ fun CameraScreen( AttendViewModel.AttendSnackBarEvent.Attend_fail -> "QR코드를 다시 확인해주세요" else -> "화면 정가운데에 QR코드를 스캔해주세요" } - snackbarContentState.value = event to message + lifecycleScope.launch { + snackbarHostState.showSnackbar(message) + } } } ComposablePermission( @@ -82,7 +87,7 @@ fun CameraScreen( }, onGranted = { val onQrCodeScanned: (String) -> Unit = { qrText -> - if(qrText != "") { + if(qrText != "" && qrEnabled) { viewModel.updateScannedQrCode(qrText) viewModel.postAttendCheck() } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt b/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt index be81fe3..bb37932 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/setting/SettingMember.kt @@ -50,18 +50,21 @@ fun SettingMember( SettingViewModel.SettingStatus.SIGNOUT -> { openDialogState = true } - //처리.. 필요.. SettingViewModel.SettingStatus.ERROR, SettingViewModel.SettingStatus.DEFAULT -> {} else -> {} } } if(openDialogState) { + val titleResId = settingStatus.getTitleResId() + val contentResId = settingStatus.getContentResId() + val okTextResId = settingStatus.getOkTextResId() + KusitmsDialog( - title = stringResource(id = R.string.logout_dialog_title), + title = stringResource(id = titleResId), content = { Text( - text = stringResource(id = R.string.logout_dialog_content), + text = stringResource(id = contentResId), textAlign = TextAlign.Center, style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Grey400 @@ -69,14 +72,19 @@ fun SettingMember( KusitmsMarginVerticalSpacer(size = 24) }, okColor = KusitmsColorPalette.current.Sub2, - okText = "로그아웃하기", + okText = stringResource(id = okTextResId), onOk = { - viewModel.logOut() + when (settingStatus) { + SettingViewModel.SettingStatus.LOGOUT -> viewModel.logOut() + SettingViewModel.SettingStatus.SIGNOUT -> viewModel.signOut() + else -> {} + } + openDialogState = false }, onCancel = { - !openDialogState + openDialogState = false + viewModel.updateSettingStatus(SettingViewModel.SettingStatus.DEFAULT) }) { - openDialogState = false } } diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 46b3188..b7df5b6 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -162,11 +162,13 @@ 로그아웃 로그아웃을 진행할까요? + 로그아웃하기 - 회원탈퇴 + 회원탈퇴 회원탈퇴를 진행할까요?\n 탈퇴하게 되면 정보가 삭제되며 되돌릴 수 없습니다. + 탈퇴하기 출석 다가오는 커리큘럼 From 48c3c2872b4ee596fcd01576440e51441491cf7b Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Sat, 10 Feb 2024 03:33:19 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[feat/#76]=20Snackbar=20=EC=82=AD=EC=A0=9C,?= =?UTF-8?q?=20Box=20Content=EB=A1=9C=20=EB=8C=80=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/attend/camera/CameraPreview.kt | 112 ++++++++---------- .../src/main/res/drawable/ic_union.xml | 5 + 2 files changed, 56 insertions(+), 61 deletions(-) create mode 100644 presentation/src/main/res/drawable/ic_union.xml diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index fefc926..13d4d15 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -46,6 +46,7 @@ import com.google.mlkit.vision.common.InputImage import com.google.zxing.* import com.google.zxing.common.HybridBinarizer import com.kusitms.presentation.R +import com.kusitms.presentation.common.ui.KusitmsMarginHorizontalSpacer import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette import com.kusitms.presentation.common.ui.theme.KusitmsTypo import com.kusitms.presentation.model.home.attend.AttendViewModel @@ -57,23 +58,9 @@ import kotlinx.coroutines.launch fun CameraScreen( viewModel: AttendViewModel, ) { - val snackbarHostState = remember { SnackbarHostState() } + val message by viewModel.snackbarEvent.collectAsState(initial = AttendViewModel.AttendSnackBarEvent.None) val qrEnabled by viewModel.qrEnabled.collectAsState() - val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope - val snackbarContentState = remember { mutableStateOf>(AttendViewModel.AttendSnackBarEvent.None to "") } - - LaunchedEffect(key1 = Unit) { - viewModel.snackbarEvent.collect { event -> - val message = when (event) { - AttendViewModel.AttendSnackBarEvent.Attend_success -> "출석이 완료되었습니다" - AttendViewModel.AttendSnackBarEvent.Attend_fail -> "QR코드를 다시 확인해주세요" - else -> "화면 정가운데에 QR코드를 스캔해주세요" - } - lifecycleScope.launch { - snackbarHostState.showSnackbar(message) - } - } - } + ComposablePermission( permission = Manifest.permission.CAMERA, onDenied = { requester -> @@ -96,60 +83,63 @@ fun CameraScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter ) { - CameraSnackbar(snackbarHostState = snackbarHostState) { - val (event, message) = snackbarContentState.value - when (event) { - AttendViewModel.AttendSnackBarEvent.Attend_success -> { - Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey200) - } - AttendViewModel.AttendSnackBarEvent.Attend_fail -> { - Image(painterResource(id = R.drawable.ic_warning_sigb), contentDescription = null) - Spacer(modifier = Modifier.width(10.dp)) - Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub2) - } - else -> { Text(text = message, style= KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey200) } + + CameraPreview(viewModel = viewModel, onQrCodeScanned = onQrCodeScanned) + + Box( + modifier = Modifier + .align(Alignment.TopCenter) // 상위 Box의 중앙 상단에 정렬 + .padding(top = 64.dp) // 상단에서 64.dp 만큼 떨어짐 + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(48.dp) + .background( + color = KusitmsColorPalette.current.Grey600, + shape = RoundedCornerShape(12.dp) + ) + ) { + when(message) { + AttendViewModel.AttendSnackBarEvent.Attend_fail -> + Row(modifier = Modifier + .fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Image(painter = painterResource(id = R.drawable.ic_warning_sigb), contentDescription = null) + KusitmsMarginHorizontalSpacer(size = 10) + Text( + text = "QR코드를 다시 확인해주세요", + style = KusitmsTypo.current.Text_Semibold, + color = KusitmsColorPalette.current.Sub2 + ) + } + else -> Text( + text = when (message) { + AttendViewModel.AttendSnackBarEvent.Attend_success -> "출석이 완료되었습니다" + AttendViewModel.AttendSnackBarEvent.None -> "화면 정가운데에 QR코드를 스캔해주세요" + else -> "" + }, + style = KusitmsTypo.current.Text_Semibold, + color = KusitmsColorPalette.current.Grey200, + modifier = Modifier.align(Alignment.Center) + ) } } - } - CameraPreview(viewModel = viewModel, onQrCodeScanned = onQrCodeScanned) - } - ) -} -@Composable -fun CameraSnackbar( - snackbarHostState: SnackbarHostState, - content: @Composable () -> Unit -) { - SnackbarHost( - hostState = snackbarHostState, - modifier = Modifier - .padding(top = 64.dp) - .fillMaxWidth(), - snackbar = { snackbarData -> - Box(modifier = androidx.compose.ui.Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 20.dp) - .background( - color = KusitmsColorPalette.current.Grey600, - shape = RoundedCornerShape(12.dp) - ) - ) { - Row( + Box( modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .padding(horizontal = 24.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + .fillMaxSize() + .align(Alignment.Center), + contentAlignment = Alignment.Center ) { - content() + Icon(painter = painterResource(id = R.drawable.ic_union), contentDescription = null, tint = KusitmsColorPalette.current.White) } } - }) + } + ) } + @Composable fun CameraOverlay() { Canvas(modifier = Modifier diff --git a/presentation/src/main/res/drawable/ic_union.xml b/presentation/src/main/res/drawable/ic_union.xml new file mode 100644 index 0000000..4ee3732 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_union.xml @@ -0,0 +1,5 @@ + + + From 9879183ae12ce54ded0799c65e7ec9efb721b218 Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Sat, 10 Feb 2024 04:17:53 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[feat/#76]=20=EA=B6=8C=ED=95=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/attend/camera/CameraPreview.kt | 61 ++++++------------- 1 file changed, 18 insertions(+), 43 deletions(-) diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index 13d4d15..bb8fdda 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -1,13 +1,13 @@ package com.kusitms.presentation.ui.home.attend.camera import android.Manifest +import android.content.Context +import android.content.Intent import android.content.pm.PackageManager +import android.media.audiofx.BassBoost +import android.net.Uri +import android.provider.Settings import android.util.Log -import android.view.Surface -import android.view.ViewGroup -import androidx.activity.compose.ManagedActivityResultLauncher -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.camera.core.CameraSelector import androidx.camera.core.ImageAnalysis import androidx.camera.core.Preview @@ -23,14 +23,9 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* -import androidx.compose.ui.graphics.BlendMode.Companion.Overlay -import androidx.compose.ui.graphics.drawscope.DrawScope -import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner @@ -39,10 +34,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavController -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.common.InputImage import com.google.zxing.* import com.google.zxing.common.HybridBinarizer import com.kusitms.presentation.R @@ -50,9 +41,6 @@ import com.kusitms.presentation.common.ui.KusitmsMarginHorizontalSpacer import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette import com.kusitms.presentation.common.ui.theme.KusitmsTypo import com.kusitms.presentation.model.home.attend.AttendViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @Composable fun CameraScreen( @@ -63,15 +51,6 @@ fun CameraScreen( ComposablePermission( permission = Manifest.permission.CAMERA, - onDenied = { requester -> - Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "카메라 권한이 필요합니다.") - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = requester) { - Text(text = "권한 요청") - } - } - }, onGranted = { val onQrCodeScanned: (String) -> Unit = { qrText -> if(qrText != "" && qrEnabled) { @@ -88,8 +67,8 @@ fun CameraScreen( Box( modifier = Modifier - .align(Alignment.TopCenter) // 상위 Box의 중앙 상단에 정렬 - .padding(top = 64.dp) // 상단에서 64.dp 만큼 떨어짐 + .align(Alignment.TopCenter) + .padding(top = 64.dp) .fillMaxWidth() .padding(horizontal = 20.dp) .height(48.dp) @@ -166,28 +145,24 @@ fun CameraOverlay() { @Composable fun ComposablePermission( permission: String, - onDenied: @Composable (requester: () -> Unit) -> Unit, - onGranted: @Composable () -> Unit + ctx: Context = LocalContext.current, + onGranted: @Composable () -> Unit, ) { - val ctx = LocalContext.current - - // check initial state of permission, it may be already granted - var grantState by remember { + val permissionGranted = remember { mutableStateOf( - ContextCompat.checkSelfPermission( - ctx, - permission - ) == PackageManager.PERMISSION_GRANTED + ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED ) } - if (grantState) { + if (permissionGranted.value) { onGranted() } else { - val launcher: ManagedActivityResultLauncher = - rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) { - grantState = it + LaunchedEffect(key1 = true) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", ctx.packageName, null) + flags = Intent.FLAG_ACTIVITY_NEW_TASK } - onDenied { launcher.launch(permission) } + ctx.startActivity(intent) + } } } From 11129faa2807ece9fb436221507a2245b4d65014 Mon Sep 17 00:00:00 2001 From: Jung SeokJoon Date: Sat, 10 Feb 2024 09:59:58 +0900 Subject: [PATCH 6/8] =?UTF-8?q?add=20onResume=20=EC=9D=BC=20=EB=95=8C=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=EC=B2=B4=ED=81=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/attend/camera/CameraPreview.kt | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index bb8fdda..1ef963b 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -34,6 +34,9 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.content.ContextCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner import com.google.zxing.* import com.google.zxing.common.HybridBinarizer import com.kusitms.presentation.R @@ -148,12 +151,27 @@ fun ComposablePermission( ctx: Context = LocalContext.current, onGranted: @Composable () -> Unit, ) { - val permissionGranted = remember { + val lifecycleOwner : LifecycleOwner = LocalLifecycleOwner.current + var permissionGranted by remember { mutableStateOf( ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED ) } - if (permissionGranted.value) { + + DisposableEffect(lifecycleOwner) { + val observer = LifecycleEventObserver { _, event -> + if(event == Lifecycle.Event.ON_RESUME){ + permissionGranted = ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED + } + } + lifecycleOwner.lifecycle.addObserver(observer) + + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + if (permissionGranted) { onGranted() } else { LaunchedEffect(key1 = true) { From 3aa81f3a6a46fc82061201f3e7aed0bfe5ef4fe4 Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Sat, 10 Feb 2024 12:12:33 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[feat/#76]=20btn=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/response/home/AttendInfoPayload.kt | 6 +- .../domain/model/home/AttendCurrentModel.kt | 1 - .../model/home/attend/AttendViewModel.kt | 45 +++++++++----- .../ui/home/attend/AttendButton.kt | 6 +- .../ui/home/attend/AttendScreen.kt | 58 +++++++++---------- .../ui/home/attend/camera/CameraPreview.kt | 4 +- 6 files changed, 67 insertions(+), 53 deletions(-) diff --git a/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt index 264d2db..f9b9d50 100644 --- a/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt +++ b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt @@ -6,8 +6,7 @@ data class AttendInfoPayload( val curriculumId: Int, val curriculumName: String, val isAttended: Boolean, - val date: String, - val time: String + val date: String ) fun AttendInfoPayload.toModel() = @@ -15,6 +14,5 @@ fun AttendInfoPayload.toModel() = curriculumId = curriculumId ?: 0, curriculumName = curriculumName ?: "", isAttended = isAttended ?: false, - date = date ?: "2월 17일", - time = time ?: "오후 02:00" + date = date ?: "2월 17일" ) \ No newline at end of file diff --git a/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt b/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt index fcaa6fd..0411bde 100644 --- a/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt +++ b/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt @@ -21,5 +21,4 @@ data class AttendInfoModel( val curriculumName: String, val isAttended: Boolean, val date: String, - val time:String ) \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt index 8bb750b..07cb2e1 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -29,6 +29,7 @@ import java.time.LocalDateTime import java.time.Year import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException +import java.time.temporal.ChronoUnit import java.util.* import javax.inject.Inject @@ -45,7 +46,7 @@ class AttendViewModel @Inject constructor( private val _attendListInit = MutableStateFlow>(emptyList()) val attendListInit : StateFlow> = _attendListInit.asStateFlow() - private val _upcomingAttend = MutableStateFlow(AttendInfoModel(0, "커리큘럼이 없습니다", false, "", "")) + private val _upcomingAttend = MutableStateFlow(AttendInfoModel(0, "커리큘럼이 없습니다", false, "")) val upcomingAttend : StateFlow = _upcomingAttend.asStateFlow() private val _attendScore = MutableStateFlow(AttendModel(0, 0, 0, 0, "수료 가능한 점수에요")) @@ -135,8 +136,8 @@ class AttendViewModel @Inject constructor( _snackbarEvent.emit(AttendSnackBarEvent.Attend_success) _qrEnabled.value = false } - delay(10000L) // 다음 반복까지 10초 대기 } + delay(10000L) // 다음 반복까지 10초 대기 } } } @@ -154,24 +155,38 @@ class AttendViewModel @Inject constructor( } } - @RequiresApi(Build.VERSION_CODES.O) - fun combineDateAndTime(date: String, time: String): LocalDateTime? { - val currentYear = LocalDate.now().year - // 날짜 형식에 연도 추가 - val dateFormatter = DateTimeFormatter.ofPattern("MM월 dd일", Locale.KOREAN) - val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 a hh:mm", Locale.KOREAN) + val timeUntilEvent = upcomingAttend + .map { attend -> calculateTimeTerm(attend.date) } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), // 스트림이 시작되는 조건 + initialValue = "" + ) - return try { - // 날짜와 시간 문자열을 현재 연도와 결합 - val dateTimeStr = "${currentYear}년 $date $time" - LocalDateTime.parse(dateTimeStr, dateTimeFormatter) - } catch (e: Exception) { - null // 파싱 실패 시 null 반환 + @RequiresApi(Build.VERSION_CODES.O) + fun calculateTimeTerm(date: String, durationMinutes: Long = 120): String { + if (date.isEmpty()) return "" + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") + val eventDate = LocalDateTime.parse(date, formatter) + val currentDate = LocalDateTime.now() + val minutesDiff = ChronoUnit.MINUTES.between(currentDate, eventDate) + + return when { + minutesDiff > 1440 -> "D-${minutesDiff / 1440}" // 하루 이상 + minutesDiff in 1..1439 -> { + val hours = minutesDiff / 60 + val minutes = minutesDiff % 60 + String.format("%02d:%02d", hours, minutes) + } // 하루 이하 + minutesDiff in -30..120 -> "Soon" // 이벤트 시작 2시간 이내 + minutesDiff <= -30 -> "Ended" // 이벤트 시작 후 30분 지남 + else -> "No Event" } } - enum class Status(val displayName: String) { + enum class Status(val displayName: String) { PRESENT("출석"), ABSENT("결석"), LATE("지각"); diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt index 0d719ad..9a59c5b 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt @@ -54,7 +54,7 @@ fun AttendBtnOff( verticalArrangement = Arrangement.Center ) { Text(text = stringResource(R.string.attend_btn_attend_wait), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey400) - Text(text = leftTime, style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey400) + Text(text = leftTime, style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Grey400) } } } @@ -70,7 +70,7 @@ fun AttendBtnFailure() { onClick = {}, enabled = false ) { - Text(text = stringResource(R.string.attend_btn_attend_failure), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub2, maxLines = 1) + Text(text = stringResource(R.string.attend_btn_attend_failure), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Sub2, maxLines = 1) } } @@ -85,6 +85,6 @@ fun AttendBtnSuccess() { onClick = {}, enabled = false ) { - Text(text = stringResource(R.string.attend_btn_attend_success), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub1, maxLines = 1) + Text(text = stringResource(R.string.attend_btn_attend_success), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Sub1, maxLines = 1) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt index 4bddeef..1b0b2d2 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt @@ -112,6 +112,7 @@ fun AttendTopBar() { } } + @RequiresApi(Build.VERSION_CODES.O) @Composable fun AttendPreColumn( @@ -119,20 +120,9 @@ fun AttendPreColumn( navController: NavHostController ) { val curri by viewModel.upcomingAttend.collectAsState() - val curriculum = curri.curriculumName ?: "커리큘럼이 없습니다" - val eventDateTime = viewModel.combineDateAndTime(curri.date, curri.time) - val currentTime = remember { mutableStateOf(LocalDateTime.now()) } - val duration = eventDateTime?.let { - Duration.between(currentTime.value, eventDateTime) - } ?: Duration.ZERO + val curriculum = curri.curriculumName + val timeUntilEvent by viewModel.timeUntilEvent.collectAsState() - // 주기적으로 현재 시간 상태 업데이트 - LaunchedEffect(key1 = Unit) { - while (true) { - currentTime.value = LocalDateTime.now() - delay(60000) - } - } Box(modifier = Modifier .fillMaxWidth() @@ -153,25 +143,35 @@ fun AttendPreColumn( .height(56.dp), verticalArrangement = Arrangement.Center ) { - Text(text = stringResource(R.string.attend_box1_title), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Main500) + Text( + text = stringResource(R.string.attend_box1_title), + style = KusitmsTypo.current.Caption1, + color = KusitmsColorPalette.current.Main500 + ) KusitmsMarginVerticalSpacer(size = 4) - Text(text = curriculum, style = KusitmsTypo.current.SubTitle1_Semibold, color = KusitmsColorPalette.current.White) + Text( + text = curriculum, + style = KusitmsTypo.current.SubTitle1_Semibold, + color = KusitmsColorPalette.current.White + ) } - if (duration.isNegative) { - val minutesAfterStart = duration.abs().toMinutes() - if (minutesAfterStart <= 30) { - // 이벤트 시작 후 30분 이내 - AttendBtnOn(navController = navController) // 여기서 정책에 따라 AttendBtnFailure로 변경 가능 - } else { - // 이벤트 시작 후 30분 초과 - AttendBtnFailure() + when { + timeUntilEvent.startsWith("D-") -> { + AttendBtnOff(timeUntilEvent) + } + timeUntilEvent.matches(Regex("\\d{2}:\\d{2}")) -> { + AttendBtnOff(timeUntilEvent) // HH:MM 형식으로 남은 시간이 표시될 때 + } + timeUntilEvent == "Soon" -> { + AttendBtnOn(navController) + } + timeUntilEvent == "Ended" -> { + if(curri.isAttended) { + AttendBtnSuccess() + } else { + AttendBtnFailure() + } } - } else if (duration.isZero || (duration.toMinutes() in 1..30)) { - // 이벤트 시작 전 30분 이내 - AttendBtnOn(navController = navController) - } else { - // 이벤트 시작까지 30분 이상 남음 - AttendBtnOff("D-${duration.toDaysPart()} ${duration.toHoursPart()}:${duration.toMinutesPart()}") } } } diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt index 1ef963b..1ce35c5 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -44,6 +44,9 @@ import com.kusitms.presentation.common.ui.KusitmsMarginHorizontalSpacer import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette import com.kusitms.presentation.common.ui.theme.KusitmsTypo import com.kusitms.presentation.model.home.attend.AttendViewModel +import com.kusitms.presentation.ui.home.attend.AttendBtnFailure +import com.kusitms.presentation.ui.home.attend.AttendBtnOff +import com.kusitms.presentation.ui.home.attend.AttendBtnSuccess @Composable fun CameraScreen( @@ -51,7 +54,6 @@ fun CameraScreen( ) { val message by viewModel.snackbarEvent.collectAsState(initial = AttendViewModel.AttendSnackBarEvent.None) val qrEnabled by viewModel.qrEnabled.collectAsState() - ComposablePermission( permission = Manifest.permission.CAMERA, onGranted = { From ad02e0b7c9abba2032c5bd44bda16469f732036c Mon Sep 17 00:00:00 2001 From: MinseoShindor Date: Sat, 10 Feb 2024 12:21:36 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[feat/#76]=20=EC=8B=9C=EA=B0=84=20event=201?= =?UTF-8?q?=EB=B6=84=EB=8B=B9=20emit=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/home/attend/AttendViewModel.kt | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt index 07cb2e1..0a09279 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -156,22 +156,28 @@ class AttendViewModel @Inject constructor( } @RequiresApi(Build.VERSION_CODES.O) - val timeUntilEvent = upcomingAttend - .map { attend -> calculateTimeTerm(attend.date) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5000L), // 스트림이 시작되는 조건 - initialValue = "" - ) + val timeUntilEvent = flow { + while (true) { + emit(LocalDateTime.now()) + delay(60000) + } + }.flatMapLatest { currentTime -> + upcomingAttend.map { attend -> + calculateTimeTerm(attend.date, currentTime) + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000L), + initialValue = "" + ) @RequiresApi(Build.VERSION_CODES.O) - fun calculateTimeTerm(date: String, durationMinutes: Long = 120): String { + fun calculateTimeTerm(date: String, currentTime: LocalDateTime): String { if (date.isEmpty()) return "" val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") val eventDate = LocalDateTime.parse(date, formatter) - val currentDate = LocalDateTime.now() - val minutesDiff = ChronoUnit.MINUTES.between(currentDate, eventDate) + val minutesDiff = ChronoUnit.MINUTES.between(currentTime, eventDate) return when { minutesDiff > 1440 -> "D-${minutesDiff / 1440}" // 하루 이상