diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b754741..36f220bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ + + + + //이번주 커리큘럼 조회 + @GET("v1/attend/info") + suspend fun getAttendInfo(): BaseResponse + + + //커리큘럼 출석 조회 + @GET("v1/attend/lists") + suspend fun getAttendCurrentList(): BaseResponse> + } \ No newline at end of file diff --git a/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendCurrentPayLoad.kt b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendCurrentPayLoad.kt new file mode 100644 index 00000000..e630044d --- /dev/null +++ b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendCurrentPayLoad.kt @@ -0,0 +1,20 @@ +package com.kusitms.data.remote.entity.response.home + +import com.kusitms.domain.model.home.AttendCurrentModel + +data class AttendCurrentPayLoad( + val attendId: Int, + val curriculum: String, + val date: String, + val time: String, + val status:String +) + +fun AttendCurrentPayLoad.toModel() = + AttendCurrentModel( + attendId = attendId ?: 0, + curriculum = curriculum ?: "", + date = date ?: "", + time = time ?: "", + status = status ?: "" + ) 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 new file mode 100644 index 00000000..f9b9d505 --- /dev/null +++ b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendInfoPayload.kt @@ -0,0 +1,18 @@ +package com.kusitms.data.remote.entity.response.home + +import com.kusitms.domain.model.home.AttendInfoModel + +data class AttendInfoPayload( + val curriculumId: Int, + val curriculumName: String, + val isAttended: Boolean, + val date: String +) + +fun AttendInfoPayload.toModel() = + AttendInfoModel( + curriculumId = curriculumId ?: 0, + curriculumName = curriculumName ?: "", + isAttended = isAttended ?: false, + date = date ?: "2월 17일" + ) \ No newline at end of file diff --git a/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendPayload.kt b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendPayload.kt new file mode 100644 index 00000000..88580bef --- /dev/null +++ b/data/src/main/java/com/kusitms/data/remote/entity/response/home/AttendPayload.kt @@ -0,0 +1,20 @@ +package com.kusitms.data.remote.entity.response.home + +import com.kusitms.domain.model.home.AttendModel + +data class AttendPayload( + val penalty: Int, + val present: Int, + val absent: Int, + val late: Int, + val passYn: String +) + +fun AttendPayload.toModel() = + AttendModel( + penalty = penalty ?: 0, + present = present ?: 0, + absent = absent ?: 0, + late = late ?: 0, + passYn = passYn ?: "수료 가능한 점수에요" + ) diff --git a/data/src/main/java/com/kusitms/data/repository/HomeRepositoryImpl.kt b/data/src/main/java/com/kusitms/data/repository/HomeRepositoryImpl.kt index 723c716e..a276a697 100644 --- a/data/src/main/java/com/kusitms/data/repository/HomeRepositoryImpl.kt +++ b/data/src/main/java/com/kusitms/data/repository/HomeRepositoryImpl.kt @@ -1,12 +1,9 @@ package com.kusitms.data.repository +import android.os.Build.VERSION_CODES.P import com.kusitms.data.remote.api.KusitmsApi import com.kusitms.data.remote.entity.response.home.toModel -import com.kusitms.domain.model.home.CurriculumRecentModel -import com.kusitms.domain.model.home.HomeProfileModel -import com.kusitms.domain.model.home.MemberInfoDetailModel -import com.kusitms.domain.model.home.NoticeRecentModel -import com.kusitms.domain.model.home.TeamMatchingModel +import com.kusitms.domain.model.home.* import com.kusitms.domain.model.profile.ProfileModel import com.kusitms.domain.repository.HomeRepository import javax.inject.Inject @@ -14,8 +11,6 @@ import javax.inject.Inject class HomeRepositoryImpl @Inject constructor( private val kusitmsApi: KusitmsApi, ) : HomeRepository { - - override suspend fun getMemberInfoHome(): Result { return try { val response = kusitmsApi.getMemberInfoHome() @@ -99,4 +94,31 @@ class HomeRepositoryImpl @Inject constructor( Result.failure(e) } } + + override suspend fun getAttendCurrentList(): Result> { + return try { + val response = kusitmsApi.getAttendCurrentList() + + if(response.result.code == 200) { + Result.success(response.payload.map {it.toModel()}) + } else { + Result.failure(RuntimeException("출석 리스트 조회 실패: ${response.result.message}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + override suspend fun getAttendInfo(): Result { + return try { + val response = kusitmsApi.getAttendInfo() + if(response.result.code == 200) { + Result.success(response.payload.toModel()) + } else { + Result.failure(RuntimeException("출석 리스트 조회 실패: ${response.result.message}")) + } + } catch (e: Exception) { + Result.failure(e) + } + } } \ 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 new file mode 100644 index 00000000..1e4d0450 --- /dev/null +++ b/domain/src/main/java/com/kusitms/domain/model/home/AttendCurrentModel.kt @@ -0,0 +1,24 @@ +package com.kusitms.domain.model.home + +data class AttendCurrentModel( + val attendId: Int, + val curriculum: String, + val date: String, + val time: String, + val status: String, +) + +data class AttendModel( + val penalty: Int, + val present: Int, + val absent: Int, + val late: Int, + val passYn: String +) + +data class AttendInfoModel( + val curriculumId: Int, + val curriculumName: String, + val isAttended: Boolean, + val date: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/kusitms/domain/repository/HomeRepository.kt b/domain/src/main/java/com/kusitms/domain/repository/HomeRepository.kt index a0c9be9c..2b1e752d 100644 --- a/domain/src/main/java/com/kusitms/domain/repository/HomeRepository.kt +++ b/domain/src/main/java/com/kusitms/domain/repository/HomeRepository.kt @@ -1,10 +1,6 @@ package com.kusitms.domain.repository -import com.kusitms.domain.model.home.CurriculumRecentModel -import com.kusitms.domain.model.home.HomeProfileModel -import com.kusitms.domain.model.home.MemberInfoDetailModel -import com.kusitms.domain.model.home.NoticeRecentModel -import com.kusitms.domain.model.home.TeamMatchingModel +import com.kusitms.domain.model.home.* import com.kusitms.domain.model.profile.ProfileModel interface HomeRepository { @@ -16,4 +12,6 @@ interface HomeRepository { suspend fun getMemberInfoList( teamId: Int ): Result> + suspend fun getAttendCurrentList(): Result> + suspend fun getAttendInfo(): Result } \ No newline at end of file diff --git a/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendCurrentListUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendCurrentListUseCase.kt new file mode 100644 index 00000000..a32d3cfb --- /dev/null +++ b/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendCurrentListUseCase.kt @@ -0,0 +1,21 @@ +package com.kusitms.domain.usecase.home + +import com.kusitms.domain.model.home.AttendCurrentModel +import com.kusitms.domain.repository.HomeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class GetAttendCurrentListUseCase @Inject constructor( + private val homeRepository: HomeRepository +) { + operator fun invoke(): Flow> = flow { + homeRepository.getAttendCurrentList() + .onSuccess { + emit(it) + } + .onFailure { + throw it + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendInfoUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendInfoUseCase.kt new file mode 100644 index 00000000..ea8d3ba9 --- /dev/null +++ b/domain/src/main/java/com/kusitms/domain/usecase/home/GetAttendInfoUseCase.kt @@ -0,0 +1,20 @@ +package com.kusitms.domain.usecase.home + +import com.kusitms.domain.model.home.AttendInfoModel +import com.kusitms.domain.repository.HomeRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import javax.inject.Inject + +class GetAttendInfoUseCase @Inject constructor( + private val homeRepository: HomeRepository +) { + operator fun invoke(): Flow = flow { + homeRepository.getAttendInfo() + .onSuccess { + emit(it) + }.onFailure { + throw it + } + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/kusitms/domain/usecase/signin/MemberSignOutUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/signin/MemberSignOutUseCase.kt index bec50683..40a8edba 100644 --- a/domain/src/main/java/com/kusitms/domain/usecase/signin/MemberSignOutUseCase.kt +++ b/domain/src/main/java/com/kusitms/domain/usecase/signin/MemberSignOutUseCase.kt @@ -7,6 +7,6 @@ class MemberSignOutUseCase@Inject constructor( private val authRepository: AuthRepository ) { suspend operator fun invoke(): Result { - return authRepository.logOutMember() + return authRepository.signOutMember() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 79fe3bfc..1d97be99 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,6 +44,12 @@ lottie-compose = "5.2.0" #timber timber = "5.0.1" +#camerax +camerax = "1.2.1" + +#mlkit +mlkit = "17.2.0" + #retrofit2 okhttp3 = "4.10.0" interceptor = "4.9.0" @@ -67,6 +73,15 @@ junit = { group = "junit", name = "junit", version.ref = "junit" } # timber timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } +# camera +camera2 = { group= "androidx.camera", name="camera-camera2", version.ref="camerax"} +camera2-lifecycle = { group= "androidx.camera", name="camera-lifecycle", version.ref="camerax"} +camera2-view = { group= "androidx.camera", name="camera-view", version.ref="camerax"} + +#mlkit +mlkit = {group = "com.google.mlkit", name="barcode-scanning", version.ref="mlkit"} + + # coroutines coroutine = {group = "org.jetbrains.kotlinx", name="kotlinx-coroutines-android", version.ref="coroutines"} # compose @@ -136,6 +151,13 @@ coil = [ "coil-svg" ] +camerax=[ + "camera2", + "camera2-lifecycle", + "camera2-view", + "mlkit" +] + lifecycle = [ "lifecycle-runtime", "lifecycle-viewmodel" diff --git a/presentation/build.gradle b/presentation/build.gradle index a221d889..20a343eb 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -72,6 +72,9 @@ dependencies { implementation(libs.hilt) kapt(libs.hilt.compile) + //camera2 + implementation(libs.bundles.camerax) + implementation 'com.github.bumptech.glide:glide:4.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.12.0' implementation "io.coil-kt:coil-compose:2.1.0" diff --git a/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendUiState.kt b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendUiState.kt new file mode 100644 index 00000000..59bfaf4f --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendUiState.kt @@ -0,0 +1,19 @@ +package com.kusitms.presentation.model.home.attend + +import com.kusitms.domain.model.home.AttendCurrentModel + +data class AttendUiState( + val curriculum:String, + val date: String, + val time: String, + val status: String, + val attendList: List = emptyList() +) + + +val curriDummy = listOf( + AttendUiState("전체 OT", "9월 2일", "오후 1:59", "PRESENT"), + AttendUiState("전체 OT", "9월 9일","출석 실패", "ABSENT"), + AttendUiState("전문가 초청 강연", "9월 16일","오후 2:13", "LATE"), + +) 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 new file mode 100644 index 00000000..21efcf3a --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/model/home/attend/AttendViewModel.kt @@ -0,0 +1,88 @@ +package com.kusitms.presentation.model.home.attend + +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.bumptech.glide.Glide.init +import com.kusitms.domain.model.home.AttendCurrentModel +import com.kusitms.domain.model.home.AttendModel +import com.kusitms.domain.usecase.home.GetAttendCurrentListUseCase +import com.kusitms.presentation.R +import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + + +@HiltViewModel +class AttendViewModel @Inject constructor( + getAttendCurrentListUseCase: GetAttendCurrentListUseCase +):ViewModel() { + + val attendListInit = getAttendCurrentListUseCase().catch { + }.stateIn( + viewModelScope, + started = SharingStarted.Eagerly, + initialValue = emptyList() + ) + + private val _attendCurrentList = MutableStateFlow>(emptyList()) + val attendCurrentList : StateFlow> = _attendCurrentList.asStateFlow() + + private val _attendScore = MutableStateFlow( + AttendModel(penalty = 0, present = 0, absent = 0, late = 0, passYn = "수료 가능한 점수에요") + ) + val attendScore: StateFlow = _attendScore.asStateFlow() + + fun updateAttendScore(attendModel: AttendModel) { + _attendScore.value = attendModel + } + + fun formatDate(dateString: String): String { + val originalFormat = SimpleDateFormat("MM월 dd일", Locale.KOREA) + val targetFormat = SimpleDateFormat("M월 d일", Locale.KOREA) + return try { + val parsedDate = originalFormat.parse(dateString) + parsedDate?.let { targetFormat.format(it) } ?: dateString + } catch (e: Exception) { + // 파싱에 실패시, 원본 날짜를 그대로 사용 + dateString + } + } + + init { + viewModelScope.launch { + getAttendCurrentListUseCase().catch { + + }.collect() { + _attendCurrentList.value = it.map { attendModel -> + attendModel.copy(date = formatDate(attendModel.date)) + } + } + } + } + + + enum class Status(val displayName: String) { + PRESENT("출석"), + ABSENT("결석"), + LATE("지각"); + + companion object { + fun fromString(status: String): Status? { + return values().find { it.displayName == status } + } + } + + fun toDrawable(): Int { + return when (this) { + PRESENT -> R.drawable.ic_attend_check + ABSENT -> R.drawable.ic_attend_non_check + LATE -> R.drawable.ic_attend_late + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/model/signIn/SignInViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/signIn/SignInViewModel.kt index 34040c5f..f8906885 100644 --- a/presentation/src/main/java/com/kusitms/presentation/model/signIn/SignInViewModel.kt +++ b/presentation/src/main/java/com/kusitms/presentation/model/signIn/SignInViewModel.kt @@ -167,6 +167,7 @@ class SignInViewModel @Inject constructor( inputStream?.copyTo(fileOutputStream) } + val requestFile = tempFile.asRequestBody("image/jpeg".toMediaTypeOrNull()) return MultipartBody.Part.createFormData("image", tempFile.name, requestFile) } 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 961c496d..49cc099e 100644 --- a/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt +++ b/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt @@ -43,6 +43,8 @@ import com.kusitms.presentation.model.signIn.SignInRequestViewModel import com.kusitms.presentation.model.signIn.SignInViewModel import com.kusitms.presentation.model.signIn.SplashViewModel import com.kusitms.presentation.ui.home.HomeScreen +import com.kusitms.presentation.ui.home.attend.AttendScreen +import com.kusitms.presentation.ui.home.attend.camera.CameraPreview import com.kusitms.presentation.ui.home.profile.MyProfileScreen import com.kusitms.presentation.ui.home.team.HomeTeamDetailScreen import com.kusitms.presentation.ui.login.LoginScreen @@ -230,6 +232,16 @@ fun MainNavigation() { ) } + kusitmsComposableWithAnimation(NavRoutes.AttendanceScreen.route) { + AttendScreen( + navController + ) + } + + kusitmsComposableWithAnimation(NavRoutes.CameraPreview.route) { + CameraPreview() + } + //HomeScreen kusitmsComposableWithAnimation(NavRoutes.HomeScreen.route) { HomeScreen( diff --git a/presentation/src/main/java/com/kusitms/presentation/navigation/NavRoutes.kt b/presentation/src/main/java/com/kusitms/presentation/navigation/NavRoutes.kt index 32ad4db0..ddf434c7 100644 --- a/presentation/src/main/java/com/kusitms/presentation/navigation/NavRoutes.kt +++ b/presentation/src/main/java/com/kusitms/presentation/navigation/NavRoutes.kt @@ -57,6 +57,10 @@ sealed class NavRoutes( fun createRoute(teamId: Int, curriculumName: String) = "HomeTeamDetail/${teamId}/${curriculumName}" } + object AttendanceScreen: NavRoutes("Attendance") + + object CameraPreview: NavRoutes("CameraPreview") + object SettingMember : NavRoutes("SettingMember") object SettingNonMember : NavRoutes("SettingNonMember") 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 new file mode 100644 index 00000000..8c4e299b --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendButton.kt @@ -0,0 +1,87 @@ +package com.kusitms.presentation.ui.home.attend + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +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.navigation.NavRoutes + +@Composable +fun AttendBtnOn(navController: NavHostController) { + Button( + modifier = Modifier + .wrapContentWidth() + .height(64.dp) , + colors = ButtonDefaults.buttonColors(containerColor = KusitmsColorPalette.current.Main600) , + shape = RoundedCornerShape(size = 12.dp), + 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) + } +} + +@Preview +@Composable +fun AttendBtnOff() { + Button( + modifier = Modifier + .wrapContentWidth() + .height(64.dp) , + colors = ButtonDefaults.buttonColors(containerColor = KusitmsColorPalette.current.Grey500) , + shape = RoundedCornerShape(size = 12.dp), + onClick = { }, + enabled = false + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(text = stringResource(R.string.attend_btn_attend_wait), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey400) + Text(text = stringResource(R.string.attend_btn_attend_wait), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey400) + } + } +} + +@Composable +fun AttendBtnFailure() { + Button( + modifier = Modifier + .wrapContentWidth() + .height(64.dp) , + colors = ButtonDefaults.buttonColors(containerColor = KusitmsColorPalette.current.Grey600) , + shape = RoundedCornerShape(size = 12.dp), + onClick = {}, + enabled = false + ) { + Text(text = stringResource(R.string.attend_btn_attend_failure), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub2, maxLines = 1) + } +} + +@Composable +fun AttendBtnSuccess() { + Button( + modifier = Modifier + .wrapContentWidth() + .height(64.dp) , + colors = ButtonDefaults.buttonColors(containerColor = KusitmsColorPalette.current.Grey600) , + shape = RoundedCornerShape(size = 12.dp), + onClick = {}, + enabled = false + ) { + Text(text = stringResource(R.string.attend_btn_attend_success), style = KusitmsTypo.current.Text_Semibold, 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 new file mode 100644 index 00000000..9d64d670 --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendScreen.kt @@ -0,0 +1,250 @@ +package com.kusitms.presentation.ui.home.attend + +import androidx.annotation.StringRes +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Text +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role.Companion.Image +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import com.kusitms.presentation.common.ui.KusitmsMarginVerticalSpacer +import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette +import com.kusitms.presentation.R +import com.kusitms.presentation.common.ui.KusitmsMarginHorizontalSpacer +import com.kusitms.presentation.common.ui.KusitsmTopBarTextWithIcon +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 AttendScreen( + viewModel: AttendViewModel, + navController: NavHostController +) { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .background(color = KusitmsColorPalette.current.Grey800) // 배경색을 Grey800으로 적용 + ) { + Column( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .background(color = KusitmsColorPalette.current.Grey900), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + AttendTopBar() + KusitmsMarginVerticalSpacer(size = 8) + AttendPreColumn(navController) + KusitmsMarginVerticalSpacer(size = 24) + AttendRecordColumn() + KusitmsMarginVerticalSpacer(size = 32) + AttendNotAttend() + Spacer(modifier = Modifier + .weight(1f) + .background(color = KusitmsColorPalette.current.Grey800)) + } + } + ScrollBtn(scrollState = scrollState) +} + +@Composable +fun AttendTopBar() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(48.dp), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Text( + text = "출석", + style = KusitmsTypo.current.SubTitle1_Semibold, + color = KusitmsColorPalette.current.Grey100 + ) + } +} + +@Composable +fun AttendPreColumn(navController: NavHostController) { + Box(modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .height(108.dp) + .background(color = KusitmsColorPalette.current.Grey800, shape = RoundedCornerShape(24.dp)) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column( + modifier = Modifier + .width(188.dp) + .height(56.dp), + verticalArrangement = Arrangement.Center + ) { + Text(text = stringResource(R.string.attend_box1_title), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Main500) + KusitmsMarginVerticalSpacer(size = 4) + Text(text = stringResource(R.string.attend_box1_subTitle), style = KusitmsTypo.current.SubTitle1_Semibold, color = KusitmsColorPalette.current.White) + } + AttendBtnOn(navController) + } + } +} + +@Composable +fun AttendRecordColumn() { + Box(modifier = Modifier + .fillMaxWidth() + .height(266.dp) + .padding(horizontal = 20.dp) + .background(color = KusitmsColorPalette.current.Grey800, shape = RoundedCornerShape(24.dp))){ + + Column( + modifier = Modifier + .fillMaxSize() + .padding(20.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Top + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = stringResource(R.string.attend_box2_title), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Grey100) + Box(modifier = Modifier + .width(123.dp) + .height(36.dp) + .background( + color = KusitmsColorPalette.current.Grey600, + shape = RoundedCornerShape(8.dp) + ) + ){ + Text(text = stringResource(R.string.attend_btn2_attend), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Grey400, modifier = Modifier.align(Alignment.Center)) + } + } + KusitmsMarginVerticalSpacer(size = 24) + Text(text = stringResource(R.string.attend_box3_title), style = KusitmsTypo.current.Header2, color = KusitmsColorPalette.current.Grey100) + KusitmsMarginVerticalSpacer(size = 14) + AttendCanComplete() + KusitmsMarginVerticalSpacer(size = 24) + AttendBoxRow() + } + } +} + +@Composable +fun AttendCanComplete() { + Row(modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) { + Text(text = stringResource(R.string.attend_box3_subTitle_ok), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub1) + KusitmsMarginHorizontalSpacer(size = 6) + Icon(painter = painterResource(id = R.drawable.ic_thumb), contentDescription = null, tint = Color.Unspecified) + } +} + +@Composable +fun AttendNotComplete() { + Text(text = stringResource(R.string.attend_box3_subTitle_fail), style = KusitmsTypo.current.Text_Semibold, color = KusitmsColorPalette.current.Sub2) +} + +@Composable +fun AttendNotAttend() { + Column(modifier = Modifier + .fillMaxWidth() + .background(color = KusitmsColorPalette.current.Grey800), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + KusitmsMarginVerticalSpacer(size = 88) + Text(text = stringResource(R.string.attend_not_attend), style = KusitmsTypo.current.Caption1, color = KusitmsColorPalette.current.Grey400) + } +} + +@Composable +fun ScrollBtn(scrollState: ScrollState) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomEnd) { + FloatingActionButton( + modifier = Modifier.padding(16.dp), + onClick = { + CoroutineScope(Dispatchers.Main).launch { + scrollState.animateScrollTo(0) + } + }, + backgroundColor = Color.Transparent + ) { Icon(painter = painterResource(id = R.drawable.ic_up_arrow_fill), contentDescription = "Go to top", tint = Color.Unspecified) } + } +} + +@Composable +fun AttendBoxRow() { + Row(modifier = Modifier + .fillMaxWidth() + .height(74.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically) { + + AttendBoxItem(title = R.string.attend_box4_attend, Modifier.weight(1f)) + Spacer(Modifier.width(12.dp)) + + AttendBoxItem(title = R.string.attend_box4_non_attend, Modifier.weight(1f)) + Spacer(Modifier.width(12.dp)) + + AttendBoxItem(title = R.string.attend_box4_non_late, Modifier.weight(1f)) + } +} + +@Composable +fun AttendBoxItem( + @StringRes title: Int, + modifier: Modifier = Modifier +) { + Box(modifier = modifier + .height(74.dp) + .background( + color = KusitmsColorPalette.current.Grey600, + shape = RoundedCornerShape(12.dp) + ) + ){ + Column(modifier = Modifier + .fillMaxSize() + .padding(vertical = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween) { + Text(text = stringResource(id = title), + style = KusitmsTypo.current.Caption1, + color = KusitmsColorPalette.current.Grey300, + ) + Text(text ="0회", + style = KusitmsTypo.current.SubTitle1_Semibold, + color = KusitmsColorPalette.current.Grey100, + ) + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendSnackbar.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendSnackbar.kt new file mode 100644 index 00000000..fef94329 --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/AttendSnackbar.kt @@ -0,0 +1,8 @@ +package com.kusitms.presentation.ui.home.attend + +import androidx.compose.runtime.Composable + +@Composable +fun AttendSnackBar() { + +} \ No newline at end of file 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 new file mode 100644 index 00000000..c3590c1f --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/camera/CameraPreview.kt @@ -0,0 +1,130 @@ +package com.kusitms.presentation.ui.home.attend.camera + +import android.util.Log +import android.view.Surface +import android.view.ViewGroup +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.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode.Companion.Overlay +import androidx.compose.ui.graphics.Color +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.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.content.ContextCompat +import com.google.mlkit.vision.barcode.BarcodeScanning +import com.google.mlkit.vision.common.InputImage +import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette + +@Composable +fun CameraPreviewWithOverlay() { + Box { + CameraPreview() + Overlay() + } +} + +@androidx.compose.ui.tooling.preview.Preview +@Composable +fun Overlay() { + Surface { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + Canvas( + modifier = Modifier + .align(Alignment.Center) + .size(200.dp) + ) { + val strokeWidth = 4.dp.toPx() + val inset = strokeWidth / 2 + drawRect( + color = Color.Black, + topLeft = this.center.copy(x = inset, y = inset), + size = Size(size.width - strokeWidth, size.height - strokeWidth), + ) + drawRect( + color = Color.White, + size = this.size, + style = Stroke(width = strokeWidth) + ) + } + } + } +} + +@Composable +@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class) +fun CameraPreview() { + val context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + + AndroidView(factory = { context -> + PreviewView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + implementationMode = PreviewView.ImplementationMode.COMPATIBLE + } + }, update = { previewView -> + val cameraProviderFuture = ProcessCameraProvider.getInstance(context) + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + val preview = Preview.Builder().build().also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(lifecycleOwner, CameraSelector.DEFAULT_BACK_CAMERA, preview) + } catch (e: Exception) { + Log.e("CameraPreview", "Use case binding failed", e) + } + }, ContextCompat.getMainExecutor(context)) + }) + + val imageAnalysis = ImageAnalysis.Builder() + .build() + .also { + it.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy -> + val mediaImage = imageProxy.image + if (mediaImage != null) { + val inputImage = + InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) + // 바코드 스캐너 인스턴스 생성 + val scanner = BarcodeScanning.getClient() + scanner.process(inputImage) + .addOnSuccessListener { barcodes -> + for (barcode in barcodes) { + val rawValue = barcode.rawValue + // QR 코드 값을 사용 + } + } + .addOnFailureListener { + // 처리 실패 + } + .addOnCompleteListener { + imageProxy.close() + } + } + } + } + +} diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/curriItem.kt b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/curriItem.kt new file mode 100644 index 00000000..fd80f835 --- /dev/null +++ b/presentation/src/main/java/com/kusitms/presentation/ui/home/attend/curriItem.kt @@ -0,0 +1,124 @@ +package com.kusitms.presentation.ui.home.attend + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.modifier.modifierLocalMapOf +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.kusitms.domain.model.home.AttendCurrentModel +import com.kusitms.presentation.common.ui.KusitmsMarginHorizontalSpacer +import com.kusitms.presentation.common.ui.KusitmsMarginVerticalSpacer +import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette +import com.kusitms.presentation.common.ui.theme.KusitmsTypo +import com.kusitms.presentation.model.home.attend.AttendViewModel + + +@Composable +fun CurriItem( + model: AttendCurrentModel +) { + Row(modifier = Modifier + .fillMaxWidth() + .background(color = KusitmsColorPalette.current.Grey600) + .height(78.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + CurriTitleRow(model = model) + CurriBadge(model = model) + } + KusitmsMarginVerticalSpacer(size = 24) +} + +@Composable +fun CurriBadge( + model: AttendCurrentModel +) { + val status = AttendViewModel.Status.fromString(model.status) ?: AttendViewModel.Status.PRESENT + val statusColor = when(status) { + AttendViewModel.Status.PRESENT -> KusitmsColorPalette.current.Sub1 + AttendViewModel.Status.ABSENT, AttendViewModel.Status.LATE -> KusitmsColorPalette.current.Sub2 + else -> KusitmsColorPalette.current.Grey600 + } + Column( + modifier = Modifier + .width(92.dp) + .fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically) + ) { + Box(modifier = Modifier + .width(52.dp) + .wrapContentHeight() + .background( + color = KusitmsColorPalette.current.Grey600, + shape = RoundedCornerShape(40.dp) + ) + ) { + Text(text = model.status, + style = KusitmsTypo.current.Text_Semibold, + color = statusColor, + modifier = Modifier + .align(Alignment.Center) + .padding(vertical = 4.dp) + ) + } + Text(text = model.time, + style = KusitmsTypo.current.Caption1, + color = KusitmsColorPalette.current.Grey400, + ) + } +} + +@Composable +fun CurriTitleRow( + model: AttendCurrentModel +) { + val status = AttendViewModel.Status.fromString(model.status) ?: AttendViewModel.Status.PRESENT + + Row( + modifier = Modifier + .wrapContentWidth() + .fillMaxHeight(), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = status.toDrawable()), + contentDescription = "Status", + tint = Color.Unspecified + ) + KusitmsMarginHorizontalSpacer(size = 24) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically) + ) { + Text(text = model.curriculum, + style = KusitmsTypo.current.Text_Semibold, + color = KusitmsColorPalette.current.Grey100, + ) + Text(text = model.date, + style = KusitmsTypo.current.Caption1, + color = KusitmsColorPalette.current.Grey400, + ) + } + } +} + +@Preview +@Composable +fun priviewModel() { + CurriItem(model = dummyData) +} + +val dummyData = AttendCurrentModel( + 57, "파트 크로스 스터디", "1월 24일", "오후 8:40", "출석" +) \ No newline at end of file diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/splash/SplashScreen.kt b/presentation/src/main/java/com/kusitms/presentation/ui/splash/SplashScreen.kt index 1a23d0f7..5f40f1f5 100644 --- a/presentation/src/main/java/com/kusitms/presentation/ui/splash/SplashScreen.kt +++ b/presentation/src/main/java/com/kusitms/presentation/ui/splash/SplashScreen.kt @@ -34,7 +34,6 @@ fun SplashScreen(viewModel: SplashViewModel, navController: NavController) { LaunchedEffect(tokenStatus) { viewModel.verifyToken() - Log.d("tokenStatus", tokenStatus.toString()) when (tokenStatus) { TokenStatus.VALID -> { delay(2000) diff --git a/presentation/src/main/res/drawable/ic_attend_check.xml b/presentation/src/main/res/drawable/ic_attend_check.xml new file mode 100644 index 00000000..17fa8270 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_attend_check.xml @@ -0,0 +1,5 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_attend_late.xml b/presentation/src/main/res/drawable/ic_attend_late.xml new file mode 100644 index 00000000..bcbdd814 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_attend_late.xml @@ -0,0 +1,5 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_attend_non_check.xml b/presentation/src/main/res/drawable/ic_attend_non_check.xml new file mode 100644 index 00000000..c5a67154 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_attend_non_check.xml @@ -0,0 +1,5 @@ + + + diff --git a/presentation/src/main/res/drawable/ic_thumb.xml b/presentation/src/main/res/drawable/ic_thumb.xml new file mode 100644 index 00000000..e5a5e768 --- /dev/null +++ b/presentation/src/main/res/drawable/ic_thumb.xml @@ -0,0 +1,6 @@ + + + + diff --git a/presentation/src/main/res/drawable/ic_up_arrow_fill.xml b/presentation/src/main/res/drawable/ic_up_arrow_fill.xml new file mode 100644 index 00000000..e70b0e4e --- /dev/null +++ b/presentation/src/main/res/drawable/ic_up_arrow_fill.xml @@ -0,0 +1,9 @@ + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index b2b74eac..7dd7fc15 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -163,6 +163,30 @@ 로그아웃 로그아웃을 진행할까요? + 출석 + 다가오는 커리큘럼 + %s + 출석하기 + + 출석완료 + 출석실패 + 출석대기 + + 출석현황 + 출석변경 요청하기 + + 벌점 %d점 + 수료 가능한 점수예요 + 벌점 6점부터 수료가 어려워요 + + 아직 출석현황이 없어요 + + 출석 + 결석 + 지각 + + + 타 서비스, 앱, 사이트 등 게시판 외부로 회원을\n유도하거나 공동구매, 할인 쿠폰, 홍보성 이벤트 등\n허가되지 않은 광고/홍보 게시물 비아냥, 비속어 등 예의범절에 벗어나거나,\n특정인이나 단체, 지역을 비방하는 등 논란 및 분란을\n일으킬 수 있는 게시물 게시물 무단 유출, 타인의 개인정보 유출, 관리자 사칭 등 타인의 권리를 침해하거나 관련법에 위배되는 게시물