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 5a668287..9b02e476 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/remote/entity/response/home/AttendInfoPayload.kt b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt index 264d2dbe..f9b9d505 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/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt b/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt index 561afe2e..ccbd2049 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/model/home/AttendCurrentModel.kt b/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt index fcaa6fd8..0411bde9 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/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/home/PostAttendCheckUseCase.kt index c2645c85..79c71eb7 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 8d06da89..0a092790 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 @@ -27,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 @@ -37,19 +40,21 @@ 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, "수료 가능한 점수에요")) val attendScore : StateFlow = _attendScore.asStateFlow() + private val _qrEnabled = MutableStateFlow(true) + val qrEnabled: StateFlow = _qrEnabled.asStateFlow() + private val _attendCheckModel = MutableStateFlow( AttendCheckModel(curriculumId = upcomingAttend.value.curriculumId, text = "") ) @@ -71,7 +76,7 @@ class AttendViewModel @Inject constructor( ) } }.collect { - _attendListInit + _attendListInit.value = it } } } @@ -84,7 +89,7 @@ class AttendViewModel @Inject constructor( }.collect { _upcomingAttend.emit(it) _attendCheckModel.emit( - AttendCheckModel(curriculumId = it.curriculumId, text = "") + AttendCheckModel(curriculumId = it.curriculumId, text = it.curriculumName) ) } } @@ -119,16 +124,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) + 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초 대기 + } } } @@ -145,24 +155,44 @@ 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 = 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 = "" + ) - 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, 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 minutesDiff = ChronoUnit.MINUTES.between(currentTime, 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("지각"); @@ -183,6 +213,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/model/setting/SettingUiModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/setting/SettingUiModel.kt index bd6a2cb0..32ef9442 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,8 @@ 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 import androidx.compose.ui.text.font.FontVariation import androidx.lifecycle.ViewModel @@ -7,6 +10,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 @@ -38,13 +42,17 @@ class SettingViewModel @Inject constructor( fun logOut() { viewModelScope.launch { - memberLogOutUseCase() - .onSuccess { - updateSettingStatus(SettingStatus.LOGOUT) - } - .onFailure { + try { + val result = memberLogOutUseCase() + if (result.isSuccess) { + Log.d("로그아웃", "성공") + updateSettingStatus(SettingStatus.LOGOUT_SUCCESS) + } else { updateSettingStatus(SettingStatus.ERROR) } + } catch (e: Exception) { + updateSettingStatus(SettingStatus.ERROR) + } } } @@ -52,7 +60,7 @@ class SettingViewModel @Inject constructor( viewModelScope.launch { memberSignOutUseCase() .onSuccess { - updateSettingStatus(SettingStatus.SIGNOUT) + updateSettingStatus(SettingStatus.SIGNOUT_SUCCESS) } .onFailure { updateSettingStatus(SettingStatus.ERROR) @@ -60,8 +68,34 @@ 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 + } + } + @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 + } + } } } @@ -84,8 +118,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/navigation/NavGraph.kt b/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt index 43a66677..03665392 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/AttendButton.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt index 430aa8c1..9a59c5bc 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) } @@ -52,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) } } } @@ -68,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) } } @@ -83,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 b8f83881..1b0b2d25 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 e7cf49aa..1ce35c5e 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,79 +1,129 @@ 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 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 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 +import androidx.compose.ui.res.painterResource 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.navigation.NavController -import com.google.mlkit.vision.barcode.BarcodeScanning -import com.google.mlkit.vision.common.InputImage +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 +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 +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( viewModel: AttendViewModel, - navController: NavController ) { + val message by viewModel.snackbarEvent.collectAsState(initial = AttendViewModel.AttendSnackBarEvent.None) + val qrEnabled by viewModel.qrEnabled.collectAsState() 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 -> - viewModel.updateScannedQrCode(qrText) - viewModel.postAttendCheck() + if(qrText != "" && qrEnabled) { + viewModel.updateScannedQrCode(qrText) + viewModel.postAttendCheck() + } + } + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.TopCenter + ) { + + CameraPreview(viewModel = viewModel, onQrCodeScanned = onQrCodeScanned) + + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 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) + ) + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center), + contentAlignment = Alignment.Center + ) { + Icon(painter = painterResource(id = R.drawable.ic_union), contentDescription = null, tint = KusitmsColorPalette.current.White) + } } - CameraPreview(onQrCodeScanned = onQrCodeScanned) } ) } + @Composable fun CameraOverlay() { Canvas(modifier = Modifier @@ -100,28 +150,39 @@ 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 lifecycleOwner : LifecycleOwner = LocalLifecycleOwner.current + var permissionGranted by remember { mutableStateOf( - ContextCompat.checkSelfPermission( - ctx, - permission - ) == PackageManager.PERMISSION_GRANTED + ContextCompat.checkSelfPermission(ctx, permission) == PackageManager.PERMISSION_GRANTED ) } - if (grantState) { + + 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 { - 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) + } } } @@ -201,11 +262,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) } @@ -216,32 +278,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() } } } @@ -252,7 +318,6 @@ fun CameraPreview(onQrCodeScanned: (String) -> Unit) { cameraProvider.unbindAll() // 기존 바인딩 제거 try { - // 카메라와 분석기 바인딩 cameraProvider.bindToLifecycle( lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, @@ -264,10 +329,7 @@ fun CameraPreview(onQrCodeScanned: (String) -> Unit) { } } - // Compose에 카메라 프리뷰 표시 AndroidView({ previewView }, modifier = Modifier.fillMaxSize()) - - // QR 코드 스캔 영역 표시 (CameraOverlay) CameraOverlay() } 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 4fa89384..bb379325 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,23 +39,32 @@ 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.Companion.SettingStatus.ERROR, SettingViewModel.Companion.SettingStatus.DEFAULT -> {} + } + SettingViewModel.SettingStatus.LOGOUT -> { + openDialogState = true + } + 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 @@ -63,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/drawable/ic_union.xml b/presentation/src/main/res/drawable/ic_union.xml new file mode 100644 index 00000000..4ee37326 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_union.xml @@ -0,0 +1,5 @@ + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 7dd7fc15..b7df5b62 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -162,6 +162,13 @@ 로그아웃 로그아웃을 진행할까요? + 로그아웃하기 + + + 회원탈퇴 + + 회원탈퇴를 진행할까요?\n 탈퇴하게 되면 정보가 삭제되며 되돌릴 수 없습니다. + 탈퇴하기 출석 다가오는 커리큘럼