diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index 30e50480..40aeb355 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -30,6 +30,7 @@
+
@@ -40,6 +41,9 @@
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 00000000..44ca2d9b
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 0ad17cbd..8978d23d 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,4 +1,3 @@
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index bc85d4e6..4fbc54a0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -22,6 +22,7 @@ dependencies {
implementation(project(":feature:login"))
implementation(project(":feature:sign-up"))
implementation(project(":feature:main"))
+ implementation(project(":feature:qrcode-scan"))
implementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 70f3317c..a059ed34 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,9 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
suspend fun getOutingList(): Flow>
suspend fun getOutingCount(): Flow
diff --git a/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt b/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt
index da8180ce..5b71e538 100644
--- a/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt
+++ b/core/data/src/main/java/com/goms/data/repository/outing/OutingRepositoryImpl.kt
@@ -4,11 +4,16 @@ import com.goms.model.response.outing.CountResponse
import com.goms.model.response.outing.OutingResponse
import com.goms.network.datasource.outing.OutingDataSource
import kotlinx.coroutines.flow.Flow
+import java.util.UUID
import javax.inject.Inject
class OutingRepositoryImpl @Inject constructor(
private val remoteOutingDataSource: OutingDataSource
) : OutingRepository {
+ override suspend fun outing(outingUUID: UUID): Flow {
+ return remoteOutingDataSource.outing(outingUUID)
+ }
+
override suspend fun getOutingList(): Flow> {
return remoteOutingDataSource.getOutingList()
}
diff --git a/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt b/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt
index b722fcdf..8f8813e8 100644
--- a/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt
+++ b/core/design-system/src/main/java/com/goms/design_system/icon/GomsIcon.kt
@@ -91,6 +91,17 @@ fun QrScanIcon(
)
}
+@Composable
+fun QrScanGuideIcon(
+ modifier: Modifier = Modifier
+) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_qr_scan_guide),
+ contentDescription = "Qr Scan Guide Icon",
+ modifier = modifier
+ )
+}
+
@Composable
fun QrCreateIcon(
modifier: Modifier = Modifier
diff --git a/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml b/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml
new file mode 100644
index 00000000..e94db7fb
--- /dev/null
+++ b/core/design-system/src/main/res/drawable/ic_qr_scan_guide.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt b/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt
new file mode 100644
index 00000000..9543767e
--- /dev/null
+++ b/core/domain/src/main/java/com/goms/domain/outing/OutingUseCase.kt
@@ -0,0 +1,14 @@
+package com.goms.domain.outing
+
+import com.goms.data.repository.outing.OutingRepository
+import kotlinx.coroutines.flow.Flow
+import java.util.UUID
+import javax.inject.Inject
+
+class OutingUseCase @Inject constructor(
+ private val outingRepository: OutingRepository
+) {
+ suspend operator fun invoke(outingUUID: UUID) = kotlin.runCatching {
+ outingRepository.outing(outingUUID)
+ }
+}
\ No newline at end of file
diff --git a/core/network/src/main/java/com/goms/network/api/OutingAPI.kt b/core/network/src/main/java/com/goms/network/api/OutingAPI.kt
index b757be82..b52ec744 100644
--- a/core/network/src/main/java/com/goms/network/api/OutingAPI.kt
+++ b/core/network/src/main/java/com/goms/network/api/OutingAPI.kt
@@ -2,10 +2,19 @@ package com.goms.network.api
import com.goms.model.response.outing.CountResponse
import com.goms.model.response.outing.OutingResponse
+import retrofit2.Response
import retrofit2.http.GET
+import retrofit2.http.POST
+import retrofit2.http.Path
import retrofit2.http.Query
+import java.util.UUID
interface OutingAPI {
+ @POST("/api/v2/outing/{outingUUID}")
+ suspend fun outing(
+ @Path("outingUUID") outingUUID: UUID
+ ): Response
+
@GET("/api/v2/outing/")
suspend fun getOutingList(): List
diff --git a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt
index 772c7645..252e7486 100644
--- a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt
+++ b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSource.kt
@@ -3,8 +3,11 @@ package com.goms.network.datasource.outing
import com.goms.model.response.outing.CountResponse
import com.goms.model.response.outing.OutingResponse
import kotlinx.coroutines.flow.Flow
+import java.util.UUID
interface OutingDataSource {
+ suspend fun outing(outingUUID: UUID): Flow
+
suspend fun getOutingList(): Flow>
suspend fun getOutingCount(): Flow
diff --git a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt
index d2bb41e4..c33f2830 100644
--- a/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt
+++ b/core/network/src/main/java/com/goms/network/datasource/outing/OutingDataSourceImpl.kt
@@ -8,11 +8,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
+import java.util.UUID
import javax.inject.Inject
class OutingDataSourceImpl @Inject constructor(
private val outingAPI: OutingAPI
) : OutingDataSource {
+ override suspend fun outing(outingUUID: UUID): Flow = flow {
+ emit(
+ GomsApiHandler()
+ .httpRequest { outingAPI.outing(outingUUID) }
+ .sendRequest()
+ )
+ }.flowOn(Dispatchers.IO)
+
override suspend fun getOutingList(): Flow> = flow {
emit(
GomsApiHandler>()
diff --git a/feature/main/src/main/java/com/goms/main/MainScreen.kt b/feature/main/src/main/java/com/goms/main/MainScreen.kt
index 30b61292..4198ff3a 100644
--- a/feature/main/src/main/java/com/goms/main/MainScreen.kt
+++ b/feature/main/src/main/java/com/goms/main/MainScreen.kt
@@ -1,5 +1,6 @@
package com.goms.main
+import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -37,7 +38,8 @@ fun MainRoute(
viewModelStoreOwner: ViewModelStoreOwner,
onOutingStatusClick: () -> Unit,
onLateListClick: () -> Unit,
- onStudentManagementClick: () -> Unit
+ onStudentManagementClick: () -> Unit,
+ onQrcodeClick: () -> Unit
) {
MainViewModelProvider(viewModelStoreOwner = viewModelStoreOwner) { viewModel ->
val role by viewModel.role.collectAsStateWithLifecycle(initialValue = "")
@@ -55,6 +57,7 @@ fun MainRoute(
onOutingStatusClick = onOutingStatusClick,
onLateListClick = onLateListClick,
onStudentManagementClick = onStudentManagementClick,
+ onQrcodeClick = onQrcodeClick,
mainCallBack = {
viewModel.getProfile()
viewModel.getLateRankList()
@@ -74,6 +77,7 @@ fun MainScreen(
onOutingStatusClick: () -> Unit,
onLateListClick: () -> Unit,
onStudentManagementClick: () -> Unit,
+ onQrcodeClick: () -> Unit,
mainCallBack: () -> Unit
) {
LaunchedEffect(true) {
@@ -125,7 +129,9 @@ fun MainScreen(
.align(Alignment.BottomEnd)
.padding(end = 16.dp, bottom = 16.dp),
role = role
- ) {}
+ ) {
+ onQrcodeClick()
+ }
}
}
}
\ No newline at end of file
diff --git a/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt b/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt
index 823f7f6c..50edce12 100644
--- a/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt
+++ b/feature/main/src/main/java/com/goms/main/navigation/MainNavigation.kt
@@ -23,14 +23,16 @@ fun NavGraphBuilder.mainScreen(
viewModelStoreOwner: ViewModelStoreOwner,
onOutingStatusClick: () -> Unit,
onLateListClick: () -> Unit,
- onStudentManagementClick: () -> Unit
+ onStudentManagementClick: () -> Unit,
+ onQrcodeClick: () -> Unit
) {
composable(route = mainRoute) {
MainRoute(
viewModelStoreOwner = viewModelStoreOwner,
onOutingStatusClick = onOutingStatusClick,
onLateListClick = onLateListClick,
- onStudentManagementClick = onStudentManagementClick
+ onStudentManagementClick = onStudentManagementClick,
+ onQrcodeClick = onQrcodeClick
)
}
}
diff --git a/feature/qrcode-scan/.gitignore b/feature/qrcode-scan/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/qrcode-scan/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/qrcode-scan/build.gradle.kts b/feature/qrcode-scan/build.gradle.kts
new file mode 100644
index 00000000..2abe4ea7
--- /dev/null
+++ b/feature/qrcode-scan/build.gradle.kts
@@ -0,0 +1,22 @@
+import com.goms.goms_android_v2.libs
+
+plugins {
+ id("goms.android.feature")
+ id("goms.android.hilt")
+}
+
+android {
+ namespace = "com.goms.qrcode_scan"
+}
+
+dependencies {
+ add("implementation", libs.findLibrary("camera.core").get())
+ add("implementation", libs.findLibrary("camera.view").get())
+ add("implementation", libs.findLibrary("camera.camera2").get())
+ add("implementation", libs.findLibrary("camera.lifecycle").get())
+ add("implementation", libs.findLibrary("camera.extensions").get())
+
+ add("implementation", libs.findLibrary("mlkit").get())
+
+ add("implementation", libs.findLibrary("accompanist.permission").get())
+}
diff --git a/feature/qrcode-scan/consumer-rules.pro b/feature/qrcode-scan/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/feature/qrcode-scan/proguard-rules.pro b/feature/qrcode-scan/proguard-rules.pro
new file mode 100644
index 00000000..481bb434
--- /dev/null
+++ b/feature/qrcode-scan/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt b/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt
new file mode 100644
index 00000000..fba4f85f
--- /dev/null
+++ b/feature/qrcode-scan/src/androidTest/java/com/goms/qrcode_scan/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+package com.goms.qrcode_scan
+
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.ext.junit.runners.AndroidJUnit4
+
+import org.junit.Test
+import org.junit.runner.RunWith
+
+import org.junit.Assert.*
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+@RunWith(AndroidJUnit4::class)
+class ExampleInstrumentedTest {
+ @Test
+ fun useAppContext() {
+ // Context of the app under test.
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ assertEquals("com.goms.qrcode_scan.test", appContext.packageName)
+ }
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/AndroidManifest.xml b/feature/qrcode-scan/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..a5918e68
--- /dev/null
+++ b/feature/qrcode-scan/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt
new file mode 100644
index 00000000..b9f7b7ae
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/QrcodeScanScreen.kt
@@ -0,0 +1,80 @@
+package com.goms.qrcode_scan
+
+import android.Manifest
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import com.goms.design_system.theme.GomsTheme
+import com.goms.qrcode_scan.component.QrcodeScanGuide
+import com.goms.qrcode_scan.component.QrcodeScanPreview
+import com.goms.qrcode_scan.component.QrcodeScanTopBar
+import com.goms.qrcode_scan.viewmodel.QrcodeViewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.PermissionState
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+import com.google.accompanist.permissions.shouldShowRationale
+import java.util.UUID
+
+@OptIn(ExperimentalPermissionsApi::class)
+@Composable
+fun QrcodeScanRoute(
+ onPermissionBlock: () -> Unit,
+ viewModel: QrcodeViewModel = hiltViewModel(),
+) {
+ val cameraPermissionState: PermissionState = rememberPermissionState(Manifest.permission.CAMERA)
+
+ LaunchedEffect("getPermission") {
+ if (!cameraPermissionState.status.isGranted && !cameraPermissionState.status.shouldShowRationale) run {
+ cameraPermissionState.launchPermissionRequest()
+ }
+ }
+
+ if (cameraPermissionState.status.isGranted) {
+ QrcodeScanScreen(
+ onQrcodeScan = { qrcodeData ->
+ viewModel.outing(UUID.fromString(qrcodeData))
+ }
+ )
+ } else {
+ onPermissionBlock()
+ }
+}
+
+@androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
+@Composable
+fun QrcodeScanScreen(
+ onQrcodeScan: (String?) -> Unit
+) {
+ GomsTheme { _, _ ->
+ QrcodeScanPreview(
+ context = LocalContext.current,
+ onQrcodeScan = { qrcodeData ->
+ onQrcodeScan(qrcodeData)
+ }
+ )
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding(),
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ QrcodeScanTopBar()
+ Spacer(modifier = Modifier.height(224.dp))
+ QrcodeScanGuide()
+ }
+ }
+}
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt
new file mode 100644
index 00000000..5eacd038
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanGuide.kt
@@ -0,0 +1,19 @@
+package com.goms.qrcode_scan.component
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.tooling.preview.Preview
+import com.goms.design_system.theme.GomsTheme
+import com.goms.design_system.icon.QrScanGuideIcon
+@Composable
+fun QrcodeScanGuide() {
+ GomsTheme { _, _ ->
+ QrScanGuideIcon()
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun QrcodeScanGuidePreview() {
+ QrcodeScanGuide()
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt
new file mode 100644
index 00000000..9fd4d538
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanPreview.kt
@@ -0,0 +1,88 @@
+package com.goms.qrcode_scan.component
+
+import android.content.Context
+import android.util.Log
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import android.widget.Toast
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.view.LifecycleCameraController
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.core.app.ComponentActivity
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.LifecycleOwner
+import com.goms.design_system.theme.GomsTheme
+import com.goms.qrcode_scan.util.QrcodeScanner
+import java.util.concurrent.Executors
+
+@androidx.camera.core.ExperimentalGetImage
+@Composable
+fun QrcodeScanPreview(
+ context: Context,
+ onQrcodeScan: (String?) -> Unit
+) {
+ GomsTheme { _, _ ->
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ ) { innerPadding: PaddingValues ->
+ AndroidView({ context ->
+ val cameraExecutor = Executors.newSingleThreadExecutor()
+ val previewView = PreviewView(context).also {
+ it.scaleType = PreviewView.ScaleType.FILL_CENTER
+ }
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+ cameraProviderFuture.addListener({
+ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
+
+ val preview = Preview.Builder()
+ .build()
+ .also {
+ it.setSurfaceProvider(previewView.surfaceProvider)
+ }
+
+ val imageCapture = ImageCapture.Builder().build()
+
+ val imageAnalyzer = ImageAnalysis.Builder()
+ .build()
+ .also {
+ it.setAnalyzer(cameraExecutor, QrcodeScanner { qrcodeData ->
+ Toast.makeText(context, qrcodeData, Toast.LENGTH_SHORT).show()
+ onQrcodeScan(qrcodeData)
+ })
+ }
+
+ val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
+
+ try {
+ cameraProvider.unbindAll()
+
+ cameraProvider.bindToLifecycle(
+ context as ComponentActivity, cameraSelector, preview, imageCapture, imageAnalyzer)
+
+ } catch(exc: Exception) {
+ Log.d("qrcode", "binding failed")
+ }
+ }, ContextCompat.getMainExecutor(context))
+ previewView
+ },
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding))
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt
new file mode 100644
index 00000000..644568c4
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/component/QrcodeScanTopBar.kt
@@ -0,0 +1,46 @@
+package com.goms.qrcode_scan.component
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.goms.design_system.component.button.GomsBackButton
+import com.goms.design_system.icon.CloseIcon
+import com.goms.design_system.icon.GomsTextIcon
+import com.goms.design_system.theme.GomsTheme
+
+@Composable
+fun QrcodeScanTopBar(
+) {
+ GomsTheme { color, _ ->
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ GomsTextIcon(tint = color.BLACK)
+ Box(
+ modifier = Modifier.size(64.dp, 56.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ CloseIcon(tint = color.BLACK)
+ }
+ }
+ }
+}
+
+@Composable
+@Preview(showBackground = true)
+fun QrcodeScanTopBarPreview() {
+ QrcodeScanTopBar()
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt
new file mode 100644
index 00000000..87c0572e
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/navigation/QrcodeNavigation.kt
@@ -0,0 +1,23 @@
+package com.goms.qrcode_scan.navigation
+
+import androidx.navigation.NavController
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavOptions
+import androidx.navigation.compose.composable
+import com.goms.qrcode_scan.QrcodeScanRoute
+
+const val qrcodeScanRoute = "qrcode_scan_route"
+
+fun NavController.navigateToQrScan(navOptions: NavOptions? = null) {
+ this.navigate(qrcodeScanRoute, navOptions)
+}
+
+fun NavGraphBuilder.qrcodeScanScreen(
+ onPermissionBlock: () -> Unit
+) {
+ composable(route = qrcodeScanRoute) {
+ QrcodeScanRoute(
+ onPermissionBlock = onPermissionBlock
+ )
+ }
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt
new file mode 100644
index 00000000..3e1a7a21
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/util/QrcodeScanner.kt
@@ -0,0 +1,38 @@
+package com.goms.qrcode_scan.util
+
+import android.util.Log
+import androidx.camera.core.ImageAnalysis
+import androidx.camera.core.ImageProxy
+import com.google.mlkit.vision.barcode.BarcodeScannerOptions
+import com.google.mlkit.vision.barcode.BarcodeScanning
+import com.google.mlkit.vision.barcode.common.Barcode
+import com.google.mlkit.vision.common.InputImage
+
+class QrcodeScanner(
+ val qrcodeData: (String?) -> Unit
+) : ImageAnalysis.Analyzer {
+ @androidx.annotation.OptIn(androidx.camera.core.ExperimentalGetImage::class)
+ override fun analyze(imageProxy: ImageProxy) {
+ val options = BarcodeScannerOptions.Builder()
+ .setBarcodeFormats(Barcode.FORMAT_QR_CODE)
+ .build()
+
+ val scanner = BarcodeScanning.getClient(options)
+ val mediaImage = imageProxy.image
+ mediaImage?.let {
+ val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
+
+ scanner.process(image)
+ .addOnSuccessListener { barcodes ->
+ if (barcodes.isNotEmpty()) {
+ val qrCodeValue = barcodes[0].displayValue
+ qrcodeData(qrCodeValue)
+ }
+ }
+ .addOnFailureListener {
+ Log.d("qrcode","scan fail")
+ }
+ }
+ imageProxy.close()
+ }
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt
new file mode 100644
index 00000000..ab643af9
--- /dev/null
+++ b/feature/qrcode-scan/src/main/java/com/goms/qrcode_scan/viewmodel/QrcodeViewModel.kt
@@ -0,0 +1,41 @@
+package com.goms.qrcode_scan.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.goms.common.result.Result
+import com.goms.common.result.asResult
+import com.goms.domain.auth.LoginUseCase
+import com.goms.domain.auth.SaveTokenUseCase
+import com.goms.domain.outing.OutingUseCase
+import com.goms.model.request.auth.SignUpRequest
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.catch
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import java.util.UUID
+import javax.inject.Inject
+
+@HiltViewModel
+class QrcodeViewModel @Inject constructor(
+ private val outingUseCase: OutingUseCase
+) : ViewModel() {
+ private val _outingState = MutableStateFlow>(Result.Loading)
+ val outingState = _outingState.asStateFlow()
+
+ fun outing(outingUUID: UUID) = viewModelScope.launch {
+ outingUseCase(outingUUID = outingUUID)
+ .onSuccess {
+ it.catch { remoteError ->
+ _outingState.value = Result.Error(remoteError)
+ }.collect { result ->
+ _outingState.value = Result.Success(result)
+ }
+ }.onFailure {
+ _outingState.value = Result.Error(it)
+ }
+ }
+}
\ No newline at end of file
diff --git a/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt b/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt
new file mode 100644
index 00000000..be87efd1
--- /dev/null
+++ b/feature/qrcode-scan/src/test/java/com/goms/qrcode_scan/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+package com.goms.qrcode_scan
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class ExampleUnitTest {
+ @Test
+ fun addition_isCorrect() {
+ assertEquals(4, 2 + 2)
+ }
+}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 05810de4..f9a3a0ec 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,5 +1,6 @@
[versions]
#Define Versions
+accompanist = "0.31.2-alpha"
androidGradlePlugin = "8.1.4"
androidxActivity = "1.9.0-alpha01"
androidxAppCompat = "1.6.1"
@@ -22,6 +23,8 @@ androidxTestRules = "1.5.0"
androidxTestRunner = "1.5.2"
androidx-test-ext-junit = "1.1.5"
androidxWindowManager = "1.2.0"
+camera = "1.3.0-beta01"
+cameraRc = "1.3.0-beta01-rc01@aar"
coil = "2.4.0"
converter-moshi = "2.9.0"
firebaseBom = "31.2.0"
@@ -41,6 +44,7 @@ ksp = "1.8.10-1.0.9"
lint = "31.2.1"
lifecycle-runtime-ktx = "2.6.2"
material = "1.2.0-alpha02"
+mlkit = "17.0.3"
moshi = "1.15.0"
okhttp = "4.11.0"
org-jetbrains-kotlin-android = "1.8.10"
@@ -54,6 +58,7 @@ turbine = "1.0.0"
[libraries]
#Define Library
+accompanist-permission = { group = "com.google.accompanist", name = "accompanist-permissions", version.ref = "accompanist" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-browser = { group = "androidx.browser", name = "browser", version.ref = "androidxBrowser" }
@@ -83,6 +88,11 @@ androidx-test-ext = { group = "androidx.test.ext", name = "junit-ktx", version.r
androidx-test-rules = { group = "androidx.test", name = "rules", version.ref = "androidxTestRules" }
androidx-test-runner = { group = "androidx.test", name = "runner", version.ref = "androidxTestRunner" }
androidx-window-manager = { module = "androidx.window:window", version.ref = "androidxWindowManager" }
+camera-core = { group = "androidx.camera", name = "camera-core", version.ref = "camera" }
+camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camera" }
+camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "cameraRc" }
+camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camera" }
+camera-extensions = { group = "androidx.camera", name = "camera-extensions", version.ref = "camera" }
coil-kt = { group = "io.coil-kt", name = "coil", version.ref = "coil" }
coil-kt-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
coil-kt-svg = { group = "io.coil-kt", name = "coil-svg", version.ref = "coil" }
@@ -99,6 +109,7 @@ kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-cor
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinxDateTime" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
lint-api = { group = "com.android.tools.lint", name = "lint-api", version.ref = "lint" }
+mlkit = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkit" }
moshi = { group = "com.squareup.moshi", name = "moshi", version.ref = "moshi" }
okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
protobuf-kotlin-lite = { group = "com.google.protobuf", name = "protobuf-kotlin-lite", version.ref = "protobuf" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ebfd3d38..779683ce 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,3 +34,4 @@ include(":core:common")
include(":feature:login")
include(":feature:sign-up")
include(":feature:main")
+include(":feature:qrcode-scan")