diff --git a/app/src/main/res/drawable/launch_transparent.xml b/app/src/main/res/drawable/launch_transparent.xml
new file mode 100644
index 00000000..eadde48f
--- /dev/null
+++ b/app/src/main/res/drawable/launch_transparent.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index a3f4f56b..e36ebade 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -5,5 +5,8 @@
- #0f1011
- false
- #0f1011
+ - true
+
+
\ No newline at end of file
diff --git a/data/src/main/java/com/kusitms/data/local/AuthDataStore.kt b/data/src/main/java/com/kusitms/data/local/AuthDataStore.kt
index 3f101fb7..52a866cb 100644
--- a/data/src/main/java/com/kusitms/data/local/AuthDataStore.kt
+++ b/data/src/main/java/com/kusitms/data/local/AuthDataStore.kt
@@ -1,42 +1,79 @@
package com.kusitms.data.local
+import android.content.Context
import android.util.Log
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
import com.google.gson.Gson
import com.kusitms.domain.model.login.LoginMemberProfile
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
-@Singleton
-class AuthDataStore {
- private val KEY_AUTH_TOKEN = "KEY_AUTH_TOKEN"
- private val KEY_REFRESH_TOKEN = "KEY_REFRESH_TOKEN"
- private val KEY_LOGIN_MEMBER_PROFILE = "KEY_LOGIN_MEMBER_PROFILE"
+class AuthDataStore @Inject constructor(private val context: Context) {
+ private val Context.dataStore: DataStore by preferencesDataStore(name = "settings")
+ companion object {
+ val AUTH_TOKEN_KEY = stringPreferencesKey("auth_token")
+ val REFRESH_TOKEN_KEY = stringPreferencesKey("refresh_token")
+ val IS_LOGIN = booleanPreferencesKey("is_login")
+ val KEY_LOGIN_MEMBER_PROFILE = stringPreferencesKey("login_member_profile")
+ }
- var authToken: String
- get() = DataStoreUtils.getSyncData(KEY_AUTH_TOKEN, "")
- set(value) {
- DataStoreUtils.saveSyncStringData(KEY_AUTH_TOKEN, value)
+ suspend fun saveAuthToken(token: String) {
+ context.dataStore.edit { preferences ->
+ preferences[AUTH_TOKEN_KEY] = token
}
+ }
- var refreshToken: String
- get() = DataStoreUtils.getSyncData(KEY_REFRESH_TOKEN, "")
- set(value) {
- DataStoreUtils.saveSyncStringData(KEY_REFRESH_TOKEN, value)
+ suspend fun saveLoginMemberProfile(profile: LoginMemberProfile?) {
+ val json = Gson().toJson(profile)
+ context.dataStore.edit { preferences ->
+ preferences[KEY_LOGIN_MEMBER_PROFILE] = json
}
+ }
- var loginMemberProfile: LoginMemberProfile?
- get() {
- val json = DataStoreUtils.getSyncData(KEY_LOGIN_MEMBER_PROFILE, "")
- return if (json.isNotEmpty()) {
- Gson().fromJson(json, LoginMemberProfile::class.java)
- } else {
- null
- }
+ val loginMemberProfile: Flow = context.dataStore.data
+ .map { preferences ->
+ val json = preferences[KEY_LOGIN_MEMBER_PROFILE] ?: return@map null
+ return@map Gson().fromJson(json, LoginMemberProfile::class.java)
}
- set(value) {
- val json = Gson().toJson(value)
- DataStoreUtils.saveSyncStringData(KEY_LOGIN_MEMBER_PROFILE, json)
- Log.d("AuthDataStore_loginMemberProfile", "Saving LoginMemberProfile: $json")
+
+ suspend fun updateLogin(isLogin: Boolean) {
+ context.dataStore.edit { preferences ->
+ preferences[IS_LOGIN] = isLogin
+ }
+ }
+
+ suspend fun saveRefreshToken(token: String) {
+ context.dataStore.edit { preferences ->
+ preferences[REFRESH_TOKEN_KEY] = token
+ }
+ }
+
+ suspend fun clearAllData() {
+ context.dataStore.edit { preferences ->
+ preferences.clear()
+ }
+ }
+
+ val authToken: Flow = context.dataStore.data
+ .map { preferences ->
+ preferences[AUTH_TOKEN_KEY]
+ }
+
+ val refreshToken: Flow = context.dataStore.data
+ .map { preferences ->
+ preferences[REFRESH_TOKEN_KEY]
+ }
+
+ val isLogin: Flow = context.dataStore.data
+ .map { preferences ->
+ preferences[IS_LOGIN]
}
-}
\ No newline at end of file
+}
diff --git a/data/src/main/java/com/kusitms/data/remote/api/KusitmsTokenApi.kt b/data/src/main/java/com/kusitms/data/remote/api/KusitmsTokenApi.kt
index 4d3829e8..b18fb807 100644
--- a/data/src/main/java/com/kusitms/data/remote/api/KusitmsTokenApi.kt
+++ b/data/src/main/java/com/kusitms/data/remote/api/KusitmsTokenApi.kt
@@ -1,9 +1,14 @@
package com.kusitms.data.remote.api
+import com.kusitms.data.remote.entity.BaseResponse
import com.kusitms.data.remote.entity.response.LoginResponse
+import com.kusitms.domain.model.login.TokenModel
+import retrofit2.Call
import retrofit2.http.GET
+import retrofit2.http.Header
interface KusitmsTokenApi {
@GET("v1/auth/reissue")
- suspend fun RefreshAccessToken() : LoginResponse
+ suspend fun RefreshAccessToken(@Header("RefreshToken") refreshToken: String) : LoginResponse
+
}
\ No newline at end of file
diff --git a/data/src/main/java/com/kusitms/data/remote/di/AuthAuthenticator.kt b/data/src/main/java/com/kusitms/data/remote/di/AuthAuthenticator.kt
new file mode 100644
index 00000000..e2eb9635
--- /dev/null
+++ b/data/src/main/java/com/kusitms/data/remote/di/AuthAuthenticator.kt
@@ -0,0 +1,33 @@
+package com.kusitms.data.remote.di
+
+import com.kusitms.data.local.AuthDataStore
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import okhttp3.Authenticator
+import okhttp3.Request
+import okhttp3.Response
+import okhttp3.Route
+import javax.inject.Inject
+
+class AuthAuthenticator@Inject constructor(
+ private val authDataStore: AuthDataStore
+):Authenticator {
+ override fun authenticate(route: Route?, response: Response): Request? {
+ val refreshToken = runBlocking {
+ authDataStore.refreshToken.first()
+ }
+
+ if (refreshToken == null || refreshToken == "LOGIN") {
+ response.close()
+ return null
+ }
+ return newRequestWithToken(refreshToken, response.request)
+ }
+
+ private fun newRequestWithToken(token: String, request: Request): Request =
+ request.newBuilder()
+ .header("Authorization", token)
+ .build()
+
+
+}
\ No newline at end of file
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 cd021c33..5a668287 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
@@ -1,51 +1,68 @@
package com.kusitms.data.remote.di
+
import android.util.Log
import com.kusitms.data.local.AuthDataStore
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import okhttp3.Interceptor
-import okhttp3.Response
+import okhttp3.*
+import timber.log.Timber
import javax.inject.Inject
class AuthTokenInterceptor @Inject constructor(
private val authDataStore: AuthDataStore,
- private val tokenManager: TokenManager
) : Interceptor {
-
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val requestBuilder = originalRequest.newBuilder()
+ val token: String? = runBlocking {
+ authDataStore.authToken.first()
+ } ?: ""
+
// "auth/logout" 엔드포인트 확인
if (originalRequest.url.encodedPath.endsWith("auth/logout")) {
// logout 엔드포인트의 경우 refreshToken 사용
requestBuilder.addHeader("Authorization", "${authDataStore.refreshToken}")
- } else {
+ } else if(originalRequest.url.encodedPath.endsWith("auth/logout"))
+ else {
// 다른 엔드포인트의 경우 authToken 사용
- requestBuilder.addHeader("Authorization", "${authDataStore.authToken}")
+ requestBuilder.addHeader("Authorization", "$token")
}
+ Log.d("Token is","${authDataStore.authToken}")
+ Log.d("Token is","${authDataStore.refreshToken}")
var request = requestBuilder.build()
var response = chain.proceed(request)
- // 토큰 만료 감지 및 처리
- if (response.code == 500 || response.code == 401) {
- // 토큰 매니저 인스턴스를 가져오고 토큰 갱신을 시도
- runBlocking {
- launch(Dispatchers.IO) {
- tokenManager.refreshAccessToken()
- }.join() // 갱신 완료 대기
+ if (response.code == 200) {
+ val newAccessToken: String = response.header("Authorization", null) ?: return response
+ Timber.d("new Access Token = ${newAccessToken}")
+
+ CoroutineScope(Dispatchers.IO).launch {
+ val existedAccessToken = authDataStore.authToken.first()
+ if (existedAccessToken != newAccessToken) {
+ authDataStore.saveAuthToken(newAccessToken)
+ Timber.d("newAccessToken = ${newAccessToken}\nExistedAccessToken = ${existedAccessToken}")
+ }
}
- // 갱신된 토큰으로 요청 재시도
- request = chain.request().newBuilder()
- .addHeader("Authorization", "${authDataStore.authToken}")
- .build()
- response.close() // 이전 응답 닫기
- return chain.proceed(request)
+ } else {
+ Timber.e("${response.code} : ${response.request} \n ${response.message}")
}
+
return response
}
+
+ private fun errorResponse(request: Request): Response = Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_2)
+ .message("")
+ .code(401)
+ .body(ResponseBody.create(null, ""))
+ .build()
}
+
diff --git a/data/src/main/java/com/kusitms/data/remote/di/NetworkModule.kt b/data/src/main/java/com/kusitms/data/remote/di/NetworkModule.kt
index 90b467ef..6472e86f 100644
--- a/data/src/main/java/com/kusitms/data/remote/di/NetworkModule.kt
+++ b/data/src/main/java/com/kusitms/data/remote/di/NetworkModule.kt
@@ -1,6 +1,7 @@
package com.kusitms.data.remote.di
+import android.content.Context
import com.kusitms.data.BuildConfig
import com.kusitms.data.local.AuthDataStore
import com.kusitms.data.remote.api.KusitmsApi
@@ -9,6 +10,7 @@ import com.kusitms.data.remote.util.NullOnEmptyConverterFactory
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
@@ -54,19 +56,11 @@ class NetworkModule {
.create(KusitmsTokenApi::class.java)
}
- @Provides
- @Singleton
- fun provideAuthDataStore(): AuthDataStore {
- return AuthDataStore()
- }
@Provides
@Singleton
- fun provideTokenManager(
- kusitmsTokenApi: KusitmsTokenApi,
- authDataStore: AuthDataStore
- ): TokenManager {
- return TokenManager(kusitmsTokenApi, authDataStore)
+ fun provideAuthDataStore(@ApplicationContext context: Context): AuthDataStore {
+ return AuthDataStore(context)
}
@@ -74,9 +68,8 @@ class NetworkModule {
@Singleton
fun provideAuthTokenInterceptor(
authDataStore: AuthDataStore,
- tokenManager: TokenManager
): AuthTokenInterceptor {
- return AuthTokenInterceptor(authDataStore,tokenManager)
+ return AuthTokenInterceptor(authDataStore)
}
diff --git a/data/src/main/java/com/kusitms/data/remote/di/TokenManager.kt b/data/src/main/java/com/kusitms/data/remote/di/TokenManager.kt
deleted file mode 100644
index 7ef1336b..00000000
--- a/data/src/main/java/com/kusitms/data/remote/di/TokenManager.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package com.kusitms.data.remote.di
-
-import com.kusitms.data.local.AuthDataStore
-import com.kusitms.data.remote.api.KusitmsTokenApi
-
-class TokenManager(
- private val kusitmsApi: KusitmsTokenApi,
- private val authDataStore: AuthDataStore
-) {
- suspend fun refreshAccessToken(): Boolean {
- return try {
- val response = kusitmsApi.RefreshAccessToken()
- if (response.result.code == 200 && response.payload != null) {
- authDataStore.authToken = response.payload.accessToken
- authDataStore.refreshToken = response.payload.refreshToken
- true
- } else {
- false
- }
-
- } catch (e: Exception) {
- false
- }
- }
-}
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 3cf6a8a2..561afe2e 100644
--- a/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt
+++ b/data/src/main/java/com/kusitms/data/repository/AuthRepositoryImpl.kt
@@ -5,12 +5,14 @@ import android.net.ConnectivityManager
import com.kusitms.data.local.AuthDataStore
import com.kusitms.data.local.DataStoreUtils
import com.kusitms.data.remote.api.KusitmsApi
+import com.kusitms.data.remote.api.KusitmsTokenApi
import com.kusitms.domain.model.login.LoginMemberProfile
import com.kusitms.domain.model.login.TokenModel
import com.kusitms.domain.repository.AuthRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
@@ -22,7 +24,17 @@ class AuthRepositoryImpl @Inject constructor(
): AuthRepository {
override suspend fun getLoginMemberProfile(): Result {
return try {
- Result.success(authDataStore.loginMemberProfile)
+ val profile = authDataStore.loginMemberProfile.first()
+ Result.success(profile)
+ } catch (e: Exception) {
+ Result.failure(e)
+ }
+ }
+
+ override suspend fun getIsLogin() :Result {
+ return try {
+ val login = authDataStore.isLogin.first()
+ Result.success(login)
} catch (e: Exception) {
Result.failure(e)
}
@@ -32,7 +44,7 @@ class AuthRepositoryImpl @Inject constructor(
return try {
val response = kusitmsApi.logOutMember()
if (response.result.code == 200) {
- DataStoreUtils.clear()
+ authDataStore.clearAllData()
Result.success(Unit)
} else {
Result.failure(RuntimeException("올바른 데이터를 받지 못했습니다."))
@@ -46,7 +58,7 @@ class AuthRepositoryImpl @Inject constructor(
return try {
val response = kusitmsApi.signOutMember()
if (response.result.code == 200) {
- DataStoreUtils.clear()
+ authDataStore.clearAllData()
Result.success(Unit)
} else {
Result.failure(RuntimeException("올바른 데이터를 받지 못했습니다."))
@@ -58,8 +70,8 @@ class AuthRepositoryImpl @Inject constructor(
override suspend fun getAuthToken(): Result {
return try {
- val authToken = authDataStore.authToken
- val refreshToken = authDataStore.refreshToken
+ val authToken = authDataStore.authToken.first()
+ val refreshToken = authDataStore.refreshToken.first()
if (!authToken.isNullOrEmpty() && !refreshToken.isNullOrEmpty()) {
Result.success(TokenModel(authToken, refreshToken))
} else {
@@ -75,4 +87,5 @@ class AuthRepositoryImpl @Inject constructor(
val activeNetwork = connectivity.activeNetworkInfo
return activeNetwork != null && activeNetwork.isConnected
}
+
}
diff --git a/data/src/main/java/com/kusitms/data/repository/LoginRepositoryImpl.kt b/data/src/main/java/com/kusitms/data/repository/LoginRepositoryImpl.kt
index 52b3110f..db39cf49 100644
--- a/data/src/main/java/com/kusitms/data/repository/LoginRepositoryImpl.kt
+++ b/data/src/main/java/com/kusitms/data/repository/LoginRepositoryImpl.kt
@@ -11,7 +11,8 @@ import javax.inject.Inject
class LoginRepositoryImpl @Inject constructor(
- private val kusitmsApi: KusitmsApi
+ private val kusitmsApi: KusitmsApi,
+ private val authDataStore: AuthDataStore
): LoginRepository {
override suspend fun LoginMember(
email: String,
@@ -23,8 +24,11 @@ class LoginRepositoryImpl @Inject constructor(
val response = kusitmsApi.loginMember(request)
if (response.result.code == 200 && response.payload != null) {
Log.d("auth", response.payload.accessToken.toString())
- AuthDataStore().authToken = response.payload.accessToken
- AuthDataStore().refreshToken = response.payload.refreshToken
+
+ authDataStore.saveAuthToken(response.payload.accessToken)
+ authDataStore.saveRefreshToken(response.payload.refreshToken)
+ authDataStore.updateLogin(true)
+
Result.success(Unit)
} else {
Result.failure(RuntimeException("로그인 실패: ${response.result.message}"))
diff --git a/data/src/main/java/com/kusitms/data/repository/SignInRepositoryImpl.kt b/data/src/main/java/com/kusitms/data/repository/SignInRepositoryImpl.kt
index 47bb5542..f46c4581 100644
--- a/data/src/main/java/com/kusitms/data/repository/SignInRepositoryImpl.kt
+++ b/data/src/main/java/com/kusitms/data/repository/SignInRepositoryImpl.kt
@@ -21,7 +21,8 @@ import java.io.File
import javax.inject.Inject
class SignInRepositoryImpl @Inject constructor(
- private val kusitmsApi: KusitmsApi
+ private val kusitmsApi: KusitmsApi,
+ private val authDataStore: AuthDataStore
): SignInRepository {
override suspend fun getLoginMemberProfile(): Result {
return try {
@@ -36,7 +37,7 @@ class SignInRepositoryImpl @Inject constructor(
phoneNumber = response.payload.phoneNumber,
memberDetailExist = response.payload.memberDetailExist
)
- AuthDataStore().loginMemberProfile = profile
+ authDataStore.saveLoginMemberProfile(profile)
Result.success(profile)
}
} catch (e: Exception) {
diff --git a/domain/src/main/java/com/kusitms/domain/repository/AuthRepository.kt b/domain/src/main/java/com/kusitms/domain/repository/AuthRepository.kt
index df8cff85..07c22c2e 100644
--- a/domain/src/main/java/com/kusitms/domain/repository/AuthRepository.kt
+++ b/domain/src/main/java/com/kusitms/domain/repository/AuthRepository.kt
@@ -15,4 +15,6 @@ interface AuthRepository {
suspend fun getAuthToken() :Result
fun checkInternetConnection(): Boolean
+
+ suspend fun getIsLogin(): Result
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/kusitms/domain/usecase/signin/GetIsLoginUseCase.kt b/domain/src/main/java/com/kusitms/domain/usecase/signin/GetIsLoginUseCase.kt
new file mode 100644
index 00000000..ee83b185
--- /dev/null
+++ b/domain/src/main/java/com/kusitms/domain/usecase/signin/GetIsLoginUseCase.kt
@@ -0,0 +1,13 @@
+package com.kusitms.domain.usecase.signin
+
+import com.kusitms.domain.model.login.TokenModel
+import com.kusitms.domain.repository.AuthRepository
+import javax.inject.Inject
+
+class GetIsLoginUseCase @Inject constructor(
+ private val authRepository: AuthRepository
+) {
+ suspend operator fun invoke(): Result {
+ return authRepository.getIsLogin()
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1d97be99..e2e8f361 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -12,6 +12,9 @@ hilt-navigation= "1.0.0"
# compose material3
material3="1.1.1"
+#zxing
+zxing = "4.2.0"
+
# navigation
navigation-compose ="2.7.0-alpha01"
@@ -70,6 +73,9 @@ test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "a
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
+#zxing
+zxing = {group = "com.journeyapps", name="zxing-android-embedded", version.ref="zxing"}
+
# timber
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
@@ -104,6 +110,7 @@ lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-co
lifecycle-viewmodel= { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
# dataStore
dataStore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStore" }
+dataStore-core = { group = "androidx.datastore", name = "datastore-core", version.ref = "dataStore" }
# hilt
hilt-compile = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt= { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
@@ -155,7 +162,8 @@ camerax=[
"camera2",
"camera2-lifecycle",
"camera2-view",
- "mlkit"
+ "mlkit",
+ "zxing"
]
lifecycle = [
@@ -175,7 +183,7 @@ network = [
"okhttp3-logging-interceptor"
]
-dataStore = ["dataStore"]
+dataStore = ["dataStore", "dataStore-core"]
retrofitSerialization = [
"kotlinx-serialization-json",
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 0fda1cf7..3eaa5ba0 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
@@ -7,12 +7,15 @@ 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.AttendCheckModel
import com.kusitms.domain.model.home.AttendCurrentModel
import com.kusitms.domain.model.home.AttendInfoModel
import com.kusitms.domain.model.home.AttendModel
import com.kusitms.domain.usecase.home.*
+import com.kusitms.domain.usecase.signin.GetIsLoginUseCase
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.flow.*
import kotlinx.coroutines.launch
@@ -30,11 +33,12 @@ import javax.inject.Inject
@HiltViewModel
class AttendViewModel @Inject constructor(
- getAttendCurrentListUseCase: GetAttendCurrentListUseCase,
+ private val getAttendCurrentListUseCase: GetAttendCurrentListUseCase,
getAttendInfoUseCase: GetAttendInfoUseCase,
getAttendScoreUseCase: GetAttendScoreUseCase,
- postAttendCheckUseCase: PostAttendCheckUseCase,
- getAttendQrUseCase: GetAttendQrUseCase
+ getAttendQrUseCase: GetAttendQrUseCase,
+ private val getIsLoginUseCase: GetIsLoginUseCase,
+ private val PostAttendCheckUseCase: PostAttendCheckUseCase
):ViewModel() {
val attendListInit: StateFlow> = getAttendCurrentListUseCase()
@@ -61,6 +65,18 @@ class AttendViewModel @Inject constructor(
initialValue = AttendInfoModel(0, "", false, "", "")
)
+ private val _attendCheckModel = MutableStateFlow(
+ AttendCheckModel(curriculumId = upcomingAttend.value.curriculumId, text = "")
+ )
+ val attendCheckModel = _attendCheckModel.asStateFlow()
+
+ fun updateScannedQrCode(qrText: String) {
+ _attendCheckModel.value = _attendCheckModel.value.copy(text = qrText)
+ }
+
+ private val _snackbarEvent = MutableSharedFlow()
+ val snackbarEvent : SharedFlow = _snackbarEvent.asSharedFlow()
+
val attendScore = getAttendScoreUseCase().catch {
}.stateIn(
viewModelScope,
@@ -80,6 +96,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)
+ }
+ }
+ }
+
fun formatTime(timeString: String): String {
val inputFormat = SimpleDateFormat("a hh:mm", Locale.KOREAN)
val outputFormat = SimpleDateFormat("a h:mm", Locale.KOREAN)
@@ -129,4 +160,8 @@ class AttendViewModel @Inject constructor(
}
}
}
+
+ enum class AttendSnackBarEvent {
+ Attend_success, Attend_fail
+ }
}
\ No newline at end of file
diff --git a/presentation/src/main/java/com/kusitms/presentation/model/signIn/SplashViewModel.kt b/presentation/src/main/java/com/kusitms/presentation/model/signIn/SplashViewModel.kt
index 4b86d443..62b4601f 100644
--- a/presentation/src/main/java/com/kusitms/presentation/model/signIn/SplashViewModel.kt
+++ b/presentation/src/main/java/com/kusitms/presentation/model/signIn/SplashViewModel.kt
@@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.kusitms.domain.usecase.home.GetNetworkStatusUseCase
import com.kusitms.domain.usecase.signin.GetAuthTokenUseCase
+import com.kusitms.domain.usecase.signin.GetIsLoginUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -18,7 +19,8 @@ enum class TokenStatus { VALID, INVALID, DEFAULT, SERVER }
@HiltViewModel
class SplashViewModel @Inject constructor(
private val getAuthTokenUseCase: GetAuthTokenUseCase,
- private val getNetworkStatusUseCase: GetNetworkStatusUseCase
+ private val getNetworkStatusUseCase: GetNetworkStatusUseCase,
+ private val getIsLoginUseCase: GetIsLoginUseCase
): ViewModel() {
private val _tokenStatus = MutableStateFlow(TokenStatus.DEFAULT)
@@ -27,6 +29,9 @@ class SplashViewModel @Inject constructor(
private val _networkStatus = MutableStateFlow(true)
val networkStatus : StateFlow = _networkStatus
+ private val _isLogin = MutableStateFlow(true)
+ val isLogin : StateFlow = _isLogin
+
val snackbarHostState = SnackbarHostState()
fun showInvalidTokenMessage() {
@@ -58,7 +63,8 @@ class SplashViewModel @Inject constructor(
_networkStatus.value = getNetworkStatusUseCase()
if(_networkStatus.value) {
updateTokenStatus(TokenStatus.VALID)
- } else {
+ }
+ else {
updateTokenStatus(TokenStatus.SERVER)
showInvalidTokenMessage()
}
@@ -71,6 +77,15 @@ class SplashViewModel @Inject constructor(
updateTokenStatus(TokenStatus.INVALID)
showInvalidTokenMessage()
}
+ getIsLoginUseCase()
+ .onSuccess {
+ if (it != null) {
+ _isLogin.value = it
+ }
+ }
+ .onFailure {
+ _isLogin.value = false
+ }
}
}
}
\ No newline at end of file
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 84e61eaa..43a66677 100644
--- a/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt
+++ b/presentation/src/main/java/com/kusitms/presentation/navigation/NavGraph.kt
@@ -48,6 +48,7 @@ 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.attend.camera.CameraScreen
import com.kusitms.presentation.ui.home.profile.MyProfileScreen
import com.kusitms.presentation.ui.home.team.HomeTeamDetailScreen
import com.kusitms.presentation.ui.login.LoginScreen
@@ -167,6 +168,14 @@ fun MainNavigation() {
navController
)
}
+
+ kusitmsComposableWithAnimation(NavRoutes.CameraPreview.route) {
+ CameraScreen(
+ attendViewModel,
+ navController
+ )
+ }
+
kusitmsComposableWithAnimation(NavRoutes.SignInProfileComplete.route) {
SignInProfileComplete(
signInViewModel,
@@ -246,10 +255,6 @@ fun MainNavigation() {
}
}
- kusitmsComposableWithAnimation(NavRoutes.CameraPreview.route) {
- CameraPreview()
- }
-
//HomeScreen
kusitmsComposableWithAnimation(NavRoutes.HomeScreen.route) {
HomeScreen(
diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/CameraLayer.kt b/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/CameraLayer.kt
new file mode 100644
index 00000000..932e39b6
--- /dev/null
+++ b/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/CameraLayer.kt
@@ -0,0 +1,3 @@
+package com.kusitms.presentation.ui.ImageVector
+
+object CameraLayer
diff --git a/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/LeftArrow.kt b/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/LeftArrow.kt
index 8fc2f056..5909faf7 100644
--- a/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/LeftArrow.kt
+++ b/presentation/src/main/java/com/kusitms/presentation/ui/ImageVector/LeftArrow.kt
@@ -58,9 +58,3 @@ object LeftArrow {
alignment = Alignment.Center)
}
}
-
-@Preview(showBackground = true)
-@Composable
-fun previewArrow() {
- LeftArrow.DrawRightArrow()
-}
\ 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
index fbf50854..e7cf49aa 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,130 +1,273 @@
package com.kusitms.presentation.ui.home.attend.camera
+import android.Manifest
+import android.content.pm.PackageManager
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.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Button
+import androidx.compose.material.Text
import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
+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.Color
+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.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 com.google.zxing.*
+import com.google.zxing.common.HybridBinarizer
import com.kusitms.presentation.common.ui.theme.KusitmsColorPalette
+import com.kusitms.presentation.model.home.attend.AttendViewModel
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
-@androidx.compose.ui.tooling.preview.Preview
@Composable
-fun CameraPreviewWithOverlay() {
- Box {
- CameraPreview()
- Overlay()
+fun CameraScreen(
+ viewModel: AttendViewModel,
+ navController: NavController
+) {
+ 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()
+ }
+ CameraPreview(onQrCodeScanned = onQrCodeScanned)
+ }
+ )
+}
+
+@Composable
+fun CameraOverlay() {
+ Canvas(modifier = Modifier
+ .fillMaxSize()
+ .graphicsLayer(alpha = 0.99f)
+ ) {
+ val rectSize = Size(size.width * 0.568f, size.width * 0.568f)
+
+ drawPath(
+ createCornersPath(
+ left = (size.width - rectSize.width) / 2f,
+ top = (size.height - rectSize.width) / 2f,
+ right = (size.width - rectSize.width) / 2f + rectSize.height,
+ bottom = (size.height - rectSize.width) / 2f + rectSize.width,
+ cornerRadius = 40f,
+ cornerLength = 40f
+ ),
+ color = Color.White,
+ style = Stroke(width = 10f)
+ )
}
}
@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)
- )
+fun ComposablePermission(
+ permission: String,
+ onDenied: @Composable (requester: () -> Unit) -> Unit,
+ onGranted: @Composable () -> Unit
+) {
+ val ctx = LocalContext.current
+
+ // check initial state of permission, it may be already granted
+ var grantState by remember {
+ mutableStateOf(
+ ContextCompat.checkSelfPermission(
+ ctx,
+ permission
+ ) == PackageManager.PERMISSION_GRANTED
+ )
+ }
+ if (grantState) {
+ onGranted()
+ } else {
+ val launcher: ManagedActivityResultLauncher =
+ rememberLauncherForActivityResult(contract = ActivityResultContracts.RequestPermission()) {
+ grantState = it
}
- }
+ onDenied { launcher.launch(permission) }
}
}
+
+private fun createCornersPath(
+ left: Float,
+ top: Float,
+ right: Float,
+ bottom: Float,
+ cornerRadius: Float,
+ cornerLength: Float
+): Path {
+ val path = Path()
+
+ // top left
+ path.moveTo(left, (top + cornerRadius))
+ path.arcTo(
+ Rect(left = left, top = top, right = left + cornerRadius, bottom = top + cornerRadius),
+ 180f,
+ 90f,
+ true
+ )
+
+ path.moveTo(left + (cornerRadius / 2f), top)
+ path.lineTo(left + (cornerRadius / 2f) + cornerLength, top)
+
+ path.moveTo(left, top + (cornerRadius / 2f))
+ path.lineTo(left, top + (cornerRadius / 2f) + cornerLength)
+
+ // top right
+ path.moveTo(right - cornerRadius, top)
+ path.arcTo(
+ Rect(left = right - cornerRadius, top = top, right = right, bottom = top + cornerRadius),
+ 270f,
+ 90f,
+ true
+ )
+
+ path.moveTo(right - (cornerRadius / 2f), top)
+ path.lineTo(right - (cornerRadius / 2f) - cornerLength, top)
+
+ path.moveTo(right, top + (cornerRadius / 2f))
+ path.lineTo(right, top + (cornerRadius / 2f) + cornerLength)
+
+ // bottom left
+ path.moveTo(left, bottom - cornerRadius)
+ path.arcTo(
+ Rect(left = left, top = bottom - cornerRadius, right = left+cornerRadius, bottom = bottom),
+ 90f,
+ 90f,
+ true
+ )
+
+ path.moveTo(left + (cornerRadius / 2f), bottom)
+ path.lineTo(left + (cornerRadius / 2f) + cornerLength, bottom)
+
+ path.moveTo(left, bottom - (cornerRadius / 2f))
+ path.lineTo(left, bottom - (cornerRadius / 2f) - cornerLength)
+
+ // bottom right
+ path.moveTo(left, bottom - cornerRadius)
+ path.arcTo(
+ Rect(left = right - cornerRadius, top = bottom - cornerRadius, right = right, bottom = bottom),
+ 0f,
+ 90f,
+ true
+ )
+
+ path.moveTo(right - (cornerRadius / 2f), bottom)
+ path.lineTo(right - (cornerRadius / 2f) - cornerLength, bottom)
+
+ path.moveTo(right, bottom - (cornerRadius / 2f))
+ path.lineTo(right, bottom - (cornerRadius / 2f) - cornerLength)
+
+ return path
+}
+
@Composable
@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
-fun CameraPreview() {
- val context = LocalContext.current
+fun CameraPreview(onQrCodeScanned: (String) -> Unit) {
val lifecycleOwner = LocalLifecycleOwner.current
+ val context = LocalContext.current
+ val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
+ val previewView = remember { PreviewView(context) }
+ val preview = Preview.Builder().build().also {
+ it.setSurfaceProvider(previewView.surfaceProvider)
+ }
- 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))
- })
-
+ // QR 코드 스캐너 설정
val imageAnalysis = ImageAnalysis.Builder()
+ .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.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()
+ 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)
}
+ val result = reader.decode(binaryBitmap)
+ val qrText = result.text
+ onQrCodeScanned(qrText)
+ } catch (e: NotFoundException) {
+ // QR 코드를 찾지 못함
+ } finally {
+ imageProxy.close()
+ }
}
}
}
+ // 카메라 시작
+ LaunchedEffect(previewView) {
+ val cameraProvider = cameraProviderFuture.get()
+ cameraProvider.unbindAll() // 기존 바인딩 제거
+
+ try {
+ // 카메라와 분석기 바인딩
+ cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ CameraSelector.DEFAULT_BACK_CAMERA,
+ preview,
+ imageAnalysis
+ )
+ } catch (e: Exception) {
+ Log.e("CameraPreview", "카메라 시작 실패", e)
+ }
+ }
+
+ // Compose에 카메라 프리뷰 표시
+ AndroidView({ previewView }, modifier = Modifier.fillMaxSize())
+
+ // QR 코드 스캔 영역 표시 (CameraOverlay)
+ CameraOverlay()
}
+
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 d4bcc515..03d0f732 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
@@ -38,6 +38,7 @@ import kotlinx.coroutines.delay
fun SplashScreen(viewModel: SplashViewModel, navController: NavController) {
val tokenStatus by viewModel.tokenStatus.collectAsState()
val snackbarHostState = remember { viewModel.snackbarHostState }
+ val isLogin by viewModel.isLogin.collectAsState()
val snackbarMessage = when (tokenStatus) {
TokenStatus.INVALID -> "토큰 상태가 유효하지 않습니다"
@@ -63,6 +64,20 @@ fun SplashScreen(viewModel: SplashViewModel, navController: NavController) {
else -> Unit
}
}
+ LaunchedEffect(isLogin) {
+ when (isLogin) {
+ isLogin -> {
+
+ }
+ !isLogin -> {
+ delay(2000)
+ navController.navigate(NavRoutes.LogInScreen.route) {
+ popUpTo(NavRoutes.SplashScreen.route) { inclusive = true }
+ }
+ }
+ else -> Unit
+ }
+ }
Scaffold(
snackbarHost = { SnackbarHost(
diff --git a/presentation/src/main/res/drawable/ic_camera_layer.xml b/presentation/src/main/res/drawable/ic_camera_layer.xml
new file mode 100644
index 00000000..167d9a5d
--- /dev/null
+++ b/presentation/src/main/res/drawable/ic_camera_layer.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+