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일으킬 수 있는 게시물
게시물 무단 유출, 타인의 개인정보 유출, 관리자 사칭 등 타인의 권리를 침해하거나 관련법에 위배되는 게시물