diff --git a/app/src/main/java/com/keyme/app/ui/KeymeApp.kt b/app/src/main/java/com/keyme/app/ui/KeymeApp.kt index 12997fd..efc5f3f 100644 --- a/app/src/main/java/com/keyme/app/ui/KeymeApp.kt +++ b/app/src/main/java/com/keyme/app/ui/KeymeApp.kt @@ -53,15 +53,26 @@ fun KeymeApp() { appState.onBackClick() appState.navigate(DailyKeymeTestDestination) }, + nestedGraphs = { + takeKeymeTestGraph( + onBackClick = appState::onBackClick, + onTestSolved = { + appState.onBackClick() + appState.navigate(DailyKeymeTestDestination) + }, + ) + }, ) dailyKeymeTestGraph( navigateToTakeKeymeTest = { appState.navigate(TakeKeymeTestDestination, it) }, nestedGraphs = { takeKeymeTestGraph( onBackClick = appState::onBackClick, + onTestSolved = appState::onBackClick, ) }, ) + // takeKeymeTestGraph -> 문제 풀이, 결과 확인 myProfileGraph( navigateToQuestionResult = { question -> diff --git a/data/src/main/java/com/keyme/data/local/entity/UserAuthEntity.kt b/data/src/main/java/com/keyme/data/local/entity/UserAuthEntity.kt index 370aed6..43d09bb 100644 --- a/data/src/main/java/com/keyme/data/local/entity/UserAuthEntity.kt +++ b/data/src/main/java/com/keyme/data/local/entity/UserAuthEntity.kt @@ -5,11 +5,11 @@ import androidx.room.PrimaryKey @Entity(tableName = "userAuth") data class UserAuthEntity( - @PrimaryKey(autoGenerate = false) val id: Int, + val id: Int = 0, val nickname: String?, val profileImage: String?, val profileThumbnail: String?, val friendCode: String?, val onboardingTestResultId: Int?, - val accessToken: String, + @PrimaryKey val accessToken: String, ) diff --git a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingNavigation.kt b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingNavigation.kt index 6820857..d2e0c6e 100644 --- a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingNavigation.kt +++ b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingNavigation.kt @@ -13,6 +13,7 @@ object OnboardingDestination : KeymeNavigationDestination { fun NavGraphBuilder.onboardingGraph( navigateToOnboardingKeymeTest: (testId: Int) -> Unit, navigateToMyDaily: () -> Unit, + nestedGraphs: NavGraphBuilder.() -> Unit, ) { navigation( route = OnboardingDestination.route, @@ -24,5 +25,6 @@ fun NavGraphBuilder.onboardingGraph( navigateToMyDaily = navigateToMyDaily, ) } + nestedGraphs() } } diff --git a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingScreen.kt b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingScreen.kt index 3c5d954..ae92948 100644 --- a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingScreen.kt +++ b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier @@ -35,6 +34,7 @@ import com.keyme.presentation.onboarding.guide.Guide03Screen import com.keyme.presentation.onboarding.guide.Guide04Screen import com.keyme.presentation.onboarding.nickname.NicknameScreen import com.keyme.presentation.onboarding.signin.SignInScreen +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @Composable @@ -57,7 +57,6 @@ fun OnboardingScreen( ) { val coroutineScope = rememberCoroutineScope() val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.animation_signin_background)) - val localOnboardingState by viewModel.userAuthState.collectAsState() val pagerState = rememberPagerState(initialPage = 0) val onboardingSteps = listOf( @@ -69,15 +68,16 @@ fun OnboardingScreen( OnboardingStepsEnum.GUIDE_04, ) - LaunchedEffect(localOnboardingState) { - localOnboardingState.let { - when { - it?.accessToken == null -> pagerState.scrollToPage(OnboardingStepsEnum.KAKAO_SIGN_IN.ordinal) - it.nickname == null -> pagerState.scrollToPage(OnboardingStepsEnum.NICKNAME.ordinal) - it.onboardingTestResultId == null -> pagerState.scrollToPage(OnboardingStepsEnum.GUIDE_01.ordinal) - else -> navigateToMyDaily.invoke() + LaunchedEffect(key1 = Unit) { + viewModel.userAuthState + .collectLatest { + when { + it?.accessToken == null -> pagerState.scrollToPage(OnboardingStepsEnum.KAKAO_SIGN_IN.ordinal) + it.nickname.isNullOrBlank() -> pagerState.scrollToPage(OnboardingStepsEnum.NICKNAME.ordinal) + it.onboardingTestResultId == null -> pagerState.scrollToPage(OnboardingStepsEnum.GUIDE_01.ordinal) + else -> navigateToMyDaily.invoke() + } } - } } when (pagerState.currentPage) { diff --git a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingViewModel.kt b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingViewModel.kt index 54cce61..f7e0d18 100644 --- a/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingViewModel.kt +++ b/presentation/src/main/java/com/keyme/presentation/onboarding/OnboardingViewModel.kt @@ -103,7 +103,8 @@ class OnboardingViewModel @Inject constructor( originalUrl: String, thumbnailUrl: String, ) { - if (uploadProfileImageState.value == null) return + // todo 프로필 사진 선택 +// if (uploadProfileImageState.value == null) return apiCall(apiRequest = { updateMemberUseCase.invoke( nickname = nickname, diff --git a/presentation/src/main/java/com/keyme/presentation/onboarding/nickname/NicknameScreen.kt b/presentation/src/main/java/com/keyme/presentation/onboarding/nickname/NicknameScreen.kt index f977d69..30c3412 100644 --- a/presentation/src/main/java/com/keyme/presentation/onboarding/nickname/NicknameScreen.kt +++ b/presentation/src/main/java/com/keyme/presentation/onboarding/nickname/NicknameScreen.kt @@ -2,7 +2,6 @@ package com.keyme.presentation.onboarding.nickname import android.annotation.SuppressLint import android.net.Uri -import android.util.Base64 import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -28,7 +27,6 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -86,16 +84,6 @@ fun NicknameScreen( ActivityResultContracts.GetContent(), ) { uri -> uri?.let { selectedImage = uri } } - LaunchedEffect(key1 = uploadProfileImageState) { - uploadProfileImageState?.let { - viewModel.updateMember( - nickname = nickname, - originalUrl = it.originalUrl, - thumbnailUrl = it.thumbnailUrl, - ) - } - } - Column( modifier = Modifier .fillMaxSize() @@ -140,7 +128,13 @@ fun NicknameScreen( nickname = nickname, isNicknameValidated = verifyNicknameState?.valid ?: false, selectedImage = selectedImage, - uploadProfileImage = viewModel::uploadProfileImage, + uploadProfileImage = { + viewModel.updateMember( + nickname = nickname, + originalUrl = "", + thumbnailUrl = "", + ) + }, ) Spacer(modifier = Modifier.size(54.dp)) @@ -374,24 +368,24 @@ fun NextButton( uploadProfileImage: (String) -> Unit, ) { val context = LocalContext.current - val contentResolver = context.contentResolver KeymeTextButton( text = "다음", onClick = { if (nickname.isBlank() || !isNicknameValidated) { Toast.makeText(context, "닉네임을 확인해주세요", Toast.LENGTH_SHORT).show() - } else if (selectedImage == null) { - Toast.makeText(context, "프로필 사진을 선택해주세요", Toast.LENGTH_SHORT).show() - } else { - run { - val inputStream = contentResolver.openInputStream(selectedImage) - val imageBytes = inputStream?.readBytes() - val imageString = Base64.encodeToString(imageBytes, Base64.DEFAULT) - inputStream?.close() - - uploadProfileImage.invoke(imageString) - } + } + // todo 프로필 사진 선택 +// else if (selectedImage == null) { +// Toast.makeText(context, "프로필 사진을 선택해주세요", Toast.LENGTH_SHORT).show() +// } + else { + // todo 프로필 사진 선택 +// val imageString = ImageUploadUtil.getProfileImageString(context, selectedImage) +// imageString?.let { +// uploadProfileImage.invoke(imageString) +// } + uploadProfileImage("") } }, modifier = Modifier diff --git a/presentation/src/main/java/com/keyme/presentation/takekeymetest/TakeKeymeTestRoute.kt b/presentation/src/main/java/com/keyme/presentation/takekeymetest/TakeKeymeTestRoute.kt index f11841d..7423ae1 100644 --- a/presentation/src/main/java/com/keyme/presentation/takekeymetest/TakeKeymeTestRoute.kt +++ b/presentation/src/main/java/com/keyme/presentation/takekeymetest/TakeKeymeTestRoute.kt @@ -19,6 +19,7 @@ object TakeKeymeTestDestination : KeymeNavigationDestination { fun NavGraphBuilder.takeKeymeTestGraph( onBackClick: () -> Unit, + onTestSolved: () -> Unit, ) { composable( route = "${TakeKeymeTestDestination.route}/{${TakeKeymeTestDestination.testIdArg}}", @@ -31,6 +32,7 @@ fun NavGraphBuilder.takeKeymeTestGraph( TakeKeymeTestRoute( onBackClick = onBackClick, onCloseClick = { onBackClick() }, + onTestSolved = { onTestSolved() }, ) } } @@ -39,6 +41,7 @@ fun NavGraphBuilder.takeKeymeTestGraph( fun TakeKeymeTestRoute( takeKeymeTestViewModel: TakeKeymeTestViewModel = hiltViewModel(), onBackClick: () -> Unit, + onTestSolved: () -> Unit, onCloseClick: () -> Unit, ) { val loadTestUrl = takeKeymeTestViewModel.keymeTestUrl @@ -59,7 +62,9 @@ fun TakeKeymeTestRoute( KeymeTestResultScreen( myCharacter = myCharacter, testResult = keymeTestResult, - onCloseClick = onCloseClick, + onCloseClick = { + onTestSolved() + }, ) } } else { diff --git a/presentation/src/main/java/com/keyme/presentation/utils/ImageUploadUtil.kt b/presentation/src/main/java/com/keyme/presentation/utils/ImageUploadUtil.kt new file mode 100644 index 0000000..3dedd04 --- /dev/null +++ b/presentation/src/main/java/com/keyme/presentation/utils/ImageUploadUtil.kt @@ -0,0 +1,66 @@ +package com.keyme.presentation.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Base64 +import timber.log.Timber +import java.io.ByteArrayOutputStream + +object ImageUploadUtil { + + fun getProfileImageString(context: Context, uri: Uri): String? { + val resized = resize(context, uri, 360) + val result = resized?.let { + val compressed = compress(it) + Timber.d("><> compressed: ${compressed.byteCount}") + getStringImage(compressed) + } + + return result + } + + private fun resize(context: Context, uri: Uri, resizePx: Int): Bitmap? { + val options = BitmapFactory.Options() + + BitmapFactory.decodeStream(context.contentResolver.openInputStream(uri), null, options) + var width = options.outWidth + var height = options.outHeight + var samplesize = 1 + while (width > resizePx || height > resizePx) { + width /= 2 + height /= 2 + samplesize *= 2 + } + options.inSampleSize = samplesize + + val resized = BitmapFactory.decodeStream( + context.contentResolver.openInputStream(uri), + null, + options, + ) + + return resized?.let { + Bitmap.createScaledBitmap(resized, width, height, true) + } + } + + private fun compress(bitmap: Bitmap): Bitmap { + val baos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos) + + return BitmapFactory.decodeByteArray( + baos.toByteArray(), + 0, + baos.toByteArray().size, + ) + } + + private fun getStringImage(bitmap: Bitmap): String { + val baos = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) + val imageBytes = baos.toByteArray() + return Base64.encodeToString(imageBytes, Base64.DEFAULT) + } +}