diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 798fb8e3a..db51f9120 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,8 +5,12 @@ - + { - val requestBody = createRefreshRequestBody() - val request = createRefreshRequest(requestBody, accessToken) - - val auth: NaagaAuthDto = requestRefresh(request).getOrElse { - return Result.failure(it) - } - storeToken(auth.accessToken, auth.refreshToken) - return Result.success(BEARER + auth.accessToken) - } - - private fun createRefreshRequestBody(): RequestBody { - return JSONObject() - .put(AUTH_REFRESH_KEY, getRefreshToken()) - .toString() - .toRequestBody(contentType = "application/json".toMediaType()) + private fun Response.isTokenInvalid(): Boolean { + return this.code == 401 } - private fun createRefreshRequest(requestBody: RequestBody, accessToken: String): Request { - return Request.Builder() - .url(BuildConfig.BASE_URL + AUTH_REFRESH_PATH) - .post(requestBody) + private fun Request.putToken(accessToken: String): Request { + return this.newBuilder() .addHeader(AUTH_KEY, accessToken) .build() } - private fun requestRefresh(request: Request): Result { - val response: Response = runBlocking { - withContext(Dispatchers.IO) { client.newCall(request).execute() } - } - if (response.isSuccessful) { - return Result.success(response.getDto()) - } - val failedResponse = response.getDto() - if (failedResponse.code == 101) { - return Result.failure(DataThrowable.AuthorizationThrowable(failedResponse.code, failedResponse.message)) - } - return Result.failure(IllegalStateException(REFRESH_FAILURE)) - } - - private fun getAccessToken(): String? { - return NaagaApplication.authDataSource.getAccessToken() - } - - private fun getRefreshToken(): String { - return requireNotNull(NaagaApplication.authDataSource.getRefreshToken()) { NO_REFRESH_TOKEN } - } - - private fun storeToken(accessToken: String, refreshToken: String) { - NaagaApplication.authDataSource.setAccessToken(accessToken) - NaagaApplication.authDataSource.setRefreshToken(refreshToken) - } - - private inline fun Response.getDto(): T { - val responseObject = JsonParser.parseString(body?.string()).asJsonObject - return gson.fromJson(responseObject, T::class.java) - } - companion object { private const val AUTH_KEY = "Authorization" - private const val AUTH_REFRESH_KEY = "refreshToken" - - private const val AUTH_REFRESH_PATH = "/auth/refresh" - - private const val BEARER = "Bearer " - - private const val NO_REFRESH_TOKEN = "리프레시 토큰이 없습니다" - private const val REFRESH_FAILURE = "토큰 리프레시 실패" } } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AuthService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AuthService.kt index 7308ce262..f2f12f790 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AuthService.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AuthService.kt @@ -2,9 +2,11 @@ package com.now.naaga.data.remote.retrofit.service import com.now.naaga.data.remote.dto.NaagaAuthDto import com.now.naaga.data.remote.dto.PlatformAuthDto +import com.now.naaga.data.remote.dto.post.RefreshTokenDto import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE +import retrofit2.http.Header import retrofit2.http.POST interface AuthService { @@ -14,8 +16,17 @@ interface AuthService { ): Response @DELETE("/auth/unlink") - suspend fun withdrawalMember(): Response + suspend fun withdrawalMember( + @Header("Authorization") accessToken: String, + ): Response @DELETE("/auth") - suspend fun requestLogout(): Response + suspend fun requestLogout( + @Header("Authorization") accessToken: String, + ): Response + + @POST("/auth/refresh") + suspend fun requestRefresh( + @Body refreshToken: RefreshTokenDto, + ): Response } diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultAuthRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultAuthRepository.kt index fb3743fd3..bec081541 100644 --- a/android/app/src/main/java/com/now/naaga/data/repository/DefaultAuthRepository.kt +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultAuthRepository.kt @@ -4,42 +4,55 @@ import com.now.domain.model.PlatformAuth import com.now.domain.repository.AuthRepository import com.now.naaga.data.local.AuthDataSource import com.now.naaga.data.mapper.toDto +import com.now.naaga.data.remote.dto.post.RefreshTokenDto import com.now.naaga.data.remote.retrofit.service.AuthService import com.now.naaga.util.extension.getValueOrThrow import com.now.naaga.util.unlinkWithKakao -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class DefaultAuthRepository( private val authDataSource: AuthDataSource, private val authService: AuthService, - private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : AuthRepository { - override suspend fun getToken(platformAuth: PlatformAuth): Boolean { - return withContext(dispatcher) { - val response = authService.requestAuth(platformAuth.toDto()) - runCatching { - val naagaAuthDto = response.getValueOrThrow() - authDataSource.setAccessToken(naagaAuthDto.accessToken) - authDataSource.setRefreshToken(naagaAuthDto.refreshToken) - return@withContext true - } - return@withContext false - } + override suspend fun logIn(platformAuth: PlatformAuth): Boolean { + val response = authService.requestAuth(platformAuth.toDto()) + val naagaAuthDto = response.getValueOrThrow() + storeToken(naagaAuthDto.accessToken, naagaAuthDto.refreshToken) + return true } override suspend fun logout() { - withContext(dispatcher) { - val response = authService.requestLogout() - authDataSource.resetToken() - response.getValueOrThrow() - } + val response = authService.requestLogout(getAccessToken()!!) + response.getValueOrThrow() + authDataSource.resetToken() } override suspend fun withdrawalMember() { - authService.withdrawalMember() + val response = authService.withdrawalMember(getAccessToken()!!) + response.getValueOrThrow() unlinkWithKakao() } + + override fun getAccessToken(): String? { + return authDataSource.getAccessToken() + } + + private fun getRefreshToken(): String { + return requireNotNull(authDataSource.getRefreshToken()) { NO_REFRESH_TOKEN } + } + + private fun storeToken(accessToken: String, refreshToken: String) { + authDataSource.setAccessToken(accessToken) + authDataSource.setRefreshToken(refreshToken) + } + + override suspend fun refreshAccessToken() { + val response = authService.requestRefresh(RefreshTokenDto(getRefreshToken())) + val naagaAuthDto = response.getValueOrThrow() + storeToken(naagaAuthDto.accessToken, naagaAuthDto.refreshToken) + } + + companion object { + private const val NO_REFRESH_TOKEN = "리프레시 토큰이 없습니다" + } } diff --git a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt index 340e12d02..94acdb298 100644 --- a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt +++ b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt @@ -1,6 +1,7 @@ package com.now.naaga.di import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.now.domain.repository.AuthRepository import com.now.naaga.BuildConfig import com.now.naaga.data.remote.retrofit.AuthInterceptor import com.now.naaga.data.remote.retrofit.service.AdventureService @@ -17,8 +18,17 @@ import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import retrofit2.Retrofit +import javax.inject.Qualifier import javax.inject.Singleton +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthRetrofit + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class CommonRetrofit + @Module @InstallIn(SingletonComponent::class) class ServiceModule { @@ -26,39 +36,53 @@ class ServiceModule { @Singleton @Provides - fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().apply { - addInterceptor(AuthInterceptor()) + fun provideOkHttpClient(authRepository: AuthRepository): OkHttpClient = OkHttpClient.Builder().apply { + addInterceptor(AuthInterceptor(authRepository)) }.build() + @AuthRetrofit @Singleton @Provides - fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() + fun provideAuthRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) .client(okHttpClient) .build() + @CommonRetrofit + @Singleton + @Provides + fun provideNormalRetrofit(): Retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .build() + @Singleton @Provides - fun provideRankService(retrofit: Retrofit): RankService = retrofit.create(RankService::class.java) + fun provideRankService(@AuthRetrofit retrofit: Retrofit): RankService = retrofit.create(RankService::class.java) @Singleton @Provides - fun provideStatisticsService(retrofit: Retrofit): StatisticsService = retrofit.create(StatisticsService::class.java) + fun provideStatisticsService(@AuthRetrofit retrofit: Retrofit): StatisticsService = retrofit.create( + StatisticsService::class.java, + ) @Singleton @Provides - fun provideAdventureService(retrofit: Retrofit): AdventureService = retrofit.create(AdventureService::class.java) + fun provideAdventureService(@AuthRetrofit retrofit: Retrofit): AdventureService = retrofit.create( + AdventureService::class.java, + ) @Singleton @Provides - fun providePlaceService(retrofit: Retrofit): PlaceService = retrofit.create(PlaceService::class.java) + fun providePlaceService(@AuthRetrofit retrofit: Retrofit): PlaceService = retrofit.create(PlaceService::class.java) @Singleton @Provides - fun provideAuthService(retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) + fun provideAuthService(@CommonRetrofit retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) @Singleton @Provides - fun provideLetterService(retrofit: Retrofit): LetterService = retrofit.create(LetterService::class.java) + fun provideLetterService(@AuthRetrofit retrofit: Retrofit): LetterService = + retrofit.create(LetterService::class.java) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt index 57fe34965..1dd17b2ce 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt @@ -15,7 +15,7 @@ import com.now.naaga.databinding.ActivityAdventureDetailBinding import com.now.naaga.presentation.adventuredetail.viewpager.ViewPagerAdapter import com.now.naaga.presentation.uimodel.model.LetterUiModel import com.now.naaga.util.extension.repeatOnStarted -import com.now.naaga.util.extension.showSnackbarWithEvent +import com.now.naaga.util.extension.showShortSnackbarWithEvent import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -66,7 +66,7 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul } private fun showReRequestSnackbar() { - binding.root.showSnackbarWithEvent( + binding.root.showShortSnackbarWithEvent( message = getString(R.string.snackbar_action_re_request_message), actionTitle = getString(R.string.snackbar_action__re_request_title), ) { finish() } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultActivity.kt index 42eaf8049..73c2c6687 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultActivity.kt @@ -58,13 +58,12 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul viewModel.throwable.observe(this) { throwable: DataThrowable -> when (throwable.code) { - DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + DataThrowable.NETWORK_THROWABLE_CODE -> showToast(getString(R.string.network_error_message)) } } viewModel.preference.observe(this) { - binding.customAdventureResultPreference.updatePreference(it.state) - binding.customAdventureResultPreference.likeCount = it.likeCount.value + binding.customAdventureResultPreference.updatePreference(it) } } @@ -102,7 +101,7 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul finish() } - binding.customAdventureResultPreference.setPreferenceClickListener { + binding.customAdventureResultPreference.setPreferenceClickListener(CLICK_INTERVAL_TIME) { viewModel.changePreference(it) } } @@ -110,6 +109,7 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul companion object { private const val GAME_ID = "GAME_ID" private const val MESSAGE_IN_RESULT_TYPE_NONE = "네트워크에 문제가 생겼습니다." + private const val CLICK_INTERVAL_TIME = 500L fun getIntentWithGameId(context: Context, gameId: Long): Intent { return Intent(context, AdventureResultActivity::class.java).apply { diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultViewModel.kt index ceb9ce1f7..d0b6c4df3 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/AdventureResultViewModel.kt @@ -97,11 +97,6 @@ class AdventureResultViewModel @Inject constructor( requireNotNull(adventureResult.value) { "adventureResult가 null입니다." }.destination.id.toInt(), requireNotNull(preference.value) { "preference가 null입니다." }.state, ) - }.onSuccess { - // post 응답이 성공적으로 왔는데 내가 보낸 것과 다른게 온 경우. 즉 말이 안되는 경우 - if (preference.value?.state != it) { - _preference.value = Preference(state = it) - } }.onFailure { _preference.value = preference.value?.revert() setThrowable(it) diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt index 3a34f0f77..6a6c95414 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt @@ -5,6 +5,7 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.view.View import androidx.constraintlayout.widget.ConstraintLayout +import com.now.domain.model.Preference import com.now.domain.model.PreferenceState import com.now.naaga.R import com.now.naaga.databinding.CustomPreferenceViewBinding @@ -13,10 +14,12 @@ class PreferenceView(context: Context, attrs: AttributeSet? = null) : Constraint private val binding: CustomPreferenceViewBinding private val layoutInflater = LayoutInflater.from(this.context) private var preferenceClickListener: PreferenceClickListener? = null - var likeCount: Int = 0 + private var lastClickTime = 0L + private var clickIntervalTime = 0L + private var myPreference: Preference = Preference() set(value) { field = value - binding.tvPreferenceLikeCount.text = value.toString() + binding.tvPreferenceLikeCount.text = value.likeCount.value.toString() } init { @@ -37,10 +40,18 @@ class PreferenceView(context: Context, attrs: AttributeSet? = null) : Constraint private fun setClickListeners() { binding.ivPreferenceLike.setOnClickListener { - preferenceClickListener?.onClick(PreferenceState.LIKE) + singleClick(PreferenceState.LIKE) } binding.ivPreferenceDislike.setOnClickListener { - preferenceClickListener?.onClick(PreferenceState.DISLIKE) + singleClick(PreferenceState.DISLIKE) + } + } + + private fun singleClick(state: PreferenceState) { + val current = System.currentTimeMillis() + if (current - lastClickTime > clickIntervalTime) { + lastClickTime = current + preferenceClickListener?.onClick(state) } } @@ -55,12 +66,14 @@ class PreferenceView(context: Context, attrs: AttributeSet? = null) : Constraint binding.tvPreferenceLikeCount.visibility = setVisibility(isLikeCountVisible) } - fun setPreferenceClickListener(listener: PreferenceClickListener) { + fun setPreferenceClickListener(clickIntervalTime: Long = 0, listener: PreferenceClickListener) { + this.clickIntervalTime = clickIntervalTime preferenceClickListener = listener } - fun updatePreference(preferenceState: PreferenceState) { - when (preferenceState) { + fun updatePreference(preference: Preference) { + myPreference = preference + when (preference.state) { PreferenceState.LIKE -> { binding.ivPreferenceLike.isSelected = true binding.ivPreferenceDislike.isSelected = false diff --git a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureActivity.kt index e1987b1d5..38ed14097 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureActivity.kt @@ -27,7 +27,7 @@ import com.now.naaga.presentation.onadventure.OnAdventureActivity import com.now.naaga.presentation.setting.SettingActivity import com.now.naaga.presentation.upload.UploadActivity import com.now.naaga.util.extension.openSetting -import com.now.naaga.util.extension.showSnackbarWithEvent +import com.now.naaga.util.extension.showShortSnackbarWithEvent import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @@ -116,7 +116,7 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default } private fun showPermissionSnackbar() { - binding.root.showSnackbarWithEvent( + binding.root.showShortSnackbarWithEvent( message = getString(R.string.snackbar_location_message), actionTitle = getString(R.string.snackbar_action_title), action = { openSetting() }, diff --git a/android/app/src/main/java/com/now/naaga/presentation/custom/GameButton.kt b/android/app/src/main/java/com/now/naaga/presentation/custom/GameButton.kt new file mode 100644 index 000000000..6e1636774 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/custom/GameButton.kt @@ -0,0 +1,153 @@ +package com.now.naaga.presentation.custom + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Point +import android.graphics.RectF +import android.util.AttributeSet +import android.view.MotionEvent +import androidx.annotation.ColorInt +import androidx.appcompat.widget.AppCompatButton +import androidx.core.content.ContextCompat +import com.now.naaga.R + +class GameButton(context: Context, attrs: AttributeSet? = null) : AppCompatButton(context, attrs) { + private val radius: Float + private var isClicked: Boolean = false + private var clickAction: OnClickListener? = null + + @ColorInt + private val mainColor: Int + + @ColorInt + val firstShadowColor: Int + + @ColorInt + val middleColor: Int + + @ColorInt + val secondShadowColor: Int + + @ColorInt + private val bottomColor: Int + + init { + context.theme.obtainStyledAttributes( + attrs, + R.styleable.GameButton, + 0, + 0, + ).apply { + radius = getDimensionPixelSize(R.styleable.GameButton_radius, 0).toFloat() + val gameButtonColor = GameButtonColor.getColor(getInteger(R.styleable.GameButton_buttonColor, 0)) + mainColor = ContextCompat.getColor(context, gameButtonColor.mainColor) + firstShadowColor = ContextCompat.getColor(context, gameButtonColor.firstShadowColor) + middleColor = ContextCompat.getColor(context, gameButtonColor.middleColor) + secondShadowColor = ContextCompat.getColor(context, gameButtonColor.secondShadowColor) + bottomColor = ContextCompat.getColor(context, gameButtonColor.bottomColor) + recycle() + } + } + + private fun getPaint(color: Int) = Paint().apply { + this.color = color + } + + private val ripplePaint = Paint().apply { + this.color = ContextCompat.getColor(this@GameButton.context, R.color.custom_button_ripple) + } + + override fun onDraw(canvas: Canvas) { + setBackgroundColor(Color.TRANSPARENT) + drawButton(canvas) + if (isClicked) drawRipple(canvas) + super.onDraw(canvas) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + isClicked = true + invalidate() + } + + MotionEvent.ACTION_UP -> { + isClicked = false + invalidate() + if (isClickInsideButton(Point(event.x.toInt(), event.y.toInt()))) { + clickAction?.onClick(this) + } + } + } + return true + } + + override fun setOnClickListener(l: OnClickListener?) { + clickAction = l + } + + private fun isClickInsideButton(point: Point): Boolean { + val isXInside = point.x in 0..this.width + val isYInside = point.y in 0..this.height + return isXInside && isYInside + } + + private fun drawRipple(canvas: Canvas) { + canvas.drawRoundRect(getBottomRect(), radius, radius, ripplePaint) + } + + private fun drawButton(canvas: Canvas) { + with(canvas) { + drawRoundRect(getBottomRect(), radius, radius, getPaint(bottomColor)) + drawRoundRect(getSecondShadowRect(), radius, radius, getPaint(secondShadowColor)) + drawRoundRect(getMiddleRect(), radius, radius, getPaint(middleColor)) + drawRoundRect(getFirstShadowRect(), radius, radius, getPaint(firstShadowColor)) + drawRoundRect(getMainRect(), radius, radius, getPaint(mainColor)) + } + } + + // 161 x 84 (0,0) + private fun getBottomRect(): RectF { + return RectF(0f, 0f, width.toFloat(), height.toFloat()) + } + + // 160 x 79 (0,0) + private fun getSecondShadowRect(): RectF { + return RectF(0f, 0f, width.toFloat(), (height * 0.94).toFloat()) + } + + // 158 x 77 (1,1) + private fun getMiddleRect(): RectF { + val middleWidth = (width * 0.987).toFloat() + val middleHeight = (height * 0.916).toFloat() + val start = (width * 0.006).toFloat() + val end = start + middleWidth + val top = (height * 0.01).toFloat() + val bottom = top + middleHeight + return RectF(start, top, end, bottom) + } + + // 155 x 72 (2,2) + private fun getFirstShadowRect(): RectF { + val width = (this.width * 0.96).toFloat() + val height = (this.height * 0.86).toFloat() + val start = (this.width * 0.018).toFloat() + val end = start + width + val top = (this.height * 0.02).toFloat() + val bottom = top + height + return RectF(start, top, end, bottom) + } + + // 154 x 70 (2,2) + private fun getMainRect(): RectF { + val width = (this.width * 0.96).toFloat() + val height = (this.height * 0.83).toFloat() + val start = (this.width * 0.018).toFloat() + val end = start + width + val top = (this.height * 0.02).toFloat() + val bottom = top + height + return RectF(start, top, end, bottom) + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/custom/GameButtonColor.kt b/android/app/src/main/java/com/now/naaga/presentation/custom/GameButtonColor.kt new file mode 100644 index 000000000..16368bd2f --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/custom/GameButtonColor.kt @@ -0,0 +1,33 @@ +package com.now.naaga.presentation.custom + +import androidx.annotation.ColorRes +import com.now.naaga.R + +enum class GameButtonColor( + @ColorRes val mainColor: Int, + @ColorRes val firstShadowColor: Int, + @ColorRes val middleColor: Int, + @ColorRes val secondShadowColor: Int, + @ColorRes val bottomColor: Int, +) { + YELLOW( + R.color.custom_button_yellow_main, + R.color.custom_button_yellow_first_shadow, + R.color.custom_button_yellow_middle, + R.color.custom_button_yellow_second_shadow, + R.color.custom_button_yellow_bottom, + ), + BLUE( + R.color.custom_button_blue_main, + R.color.custom_button_blue_first_shadow, + R.color.custom_button_blue_middle, + R.color.custom_button_blue_second_shadow, + R.color.custom_button_blue_bottom, + ), ; + + companion object { + fun getColor(ordinal: Int): GameButtonColor { + return values().find { it.ordinal == ordinal } ?: YELLOW + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModel.kt index 1cc6136f5..90f681a96 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModel.kt @@ -26,7 +26,7 @@ class LoginViewModel @Inject constructor( fun signIn(token: String, platformType: AuthPlatformType) { viewModelScope.launch { runCatching { - authRepository.getToken(PlatformAuth(token, platformType)) + authRepository.logIn(PlatformAuth(token, platformType)) }.onSuccess { status -> _isLoginSucceed.value = status }.onFailure { diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt index 516b77f63..1a4ead2d9 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageActivity.kt @@ -51,9 +51,7 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } private fun fetchData() { - viewModel.fetchRank() - viewModel.fetchStatistics() - viewModel.fetchPlaces() + viewModel.fetchData() } private fun subscribe() { diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt index 5a70439ea..447efe3da 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewModel.kt @@ -14,6 +14,7 @@ import com.now.domain.repository.RankRepository import com.now.domain.repository.StatisticsRepository import com.now.naaga.data.throwable.DataThrowable import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject @@ -36,36 +37,16 @@ class MyPageViewModel @Inject constructor( private val _throwable = MutableLiveData() val throwable: LiveData = _throwable - fun fetchRank() { + fun fetchData() { viewModelScope.launch { runCatching { - rankRepository.getMyRank() - }.onSuccess { rank -> - _rank.value = rank - }.onFailure { - setThrowable(it) - } - } - } + val statistics = statisticsRepository.getMyStatistics() + val rank = async { rankRepository.getMyRank() } + val places = async { placeRepository.fetchMyPlaces(SortType.TIME.name, OrderType.DESCENDING.name) } - fun fetchStatistics() { - viewModelScope.launch { - runCatching { - statisticsRepository.getMyStatistics() - }.onSuccess { statistics -> _statistics.value = statistics - }.onFailure { - setThrowable(it) - } - } - } - - fun fetchPlaces() { - viewModelScope.launch { - runCatching { - placeRepository.fetchMyPlaces(SortType.TIME.name, OrderType.DESCENDING.name) - }.onSuccess { places -> - _places.value = places + _rank.value = rank.await() + _places.value = places.await() }.onFailure { setThrowable(it) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureActivity.kt index 95acee13e..d08162f49 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureActivity.kt @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -76,11 +75,7 @@ class OnAdventureActivity : finish() } else { backPressedTime = System.currentTimeMillis() - Toast.makeText( - this@OnAdventureActivity, - getString(R.string.OnAdventure_warning_back_pressed), - Toast.LENGTH_SHORT, - ).show() + showToast(getString(R.string.OnAdventure_warning_back_pressed)) } } } @@ -103,7 +98,7 @@ class OnAdventureActivity : viewModel.endAdventure() } binding.ivSendLetter.setOnClickListener { - LetterSendDialog(viewModel::sendLetter).show(supportFragmentManager, LetterSendDialog.TAG) + LetterSendDialog(::registerLetter).show(supportFragmentManager, LetterSendDialog.TAG) } } @@ -145,7 +140,7 @@ class OnAdventureActivity : OnAdventureViewModel.NOT_ARRIVED -> { val remainingTryCount: Int = viewModel.adventure.value?.remainingTryCount?.toInt() ?: 0 - shortSnackbar(getString(R.string.onAdventure_retry, remainingTryCount)) + showToast(getString(R.string.onAdventure_retry, remainingTryCount)) } OnAdventureViewModel.TRY_COUNT_OVER -> showToast(getString(R.string.onAdventure_try_count_over)) @@ -177,7 +172,7 @@ class OnAdventureActivity : return } - Toast.makeText(this, getString(R.string.OnAdventure_continue_adventure), Toast.LENGTH_SHORT).show() + showToast(getString(R.string.OnAdventure_continue_adventure)) viewModel.setAdventure(existingAdventure) } @@ -203,6 +198,14 @@ class OnAdventureActivity : } } + private fun registerLetter(message: String) { + if (message.isNotEmpty()) { + viewModel.sendLetter(message) + } else { + showToast(getString(R.string.send_letter_dialog_warning)) + } + } + private fun showGiveUpDialog() { val fragment: Fragment? = supportFragmentManager.findFragmentByTag(GIVE_UP) if (fragment == null) { diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureViewModel.kt index a35f4ffc9..f902c648f 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/onadventure/OnAdventureViewModel.kt @@ -144,12 +144,18 @@ class OnAdventureViewModel @Inject constructor( private fun handleGameThrowable(throwable: GameThrowable) { when (throwable.code) { - TRY_COUNT_OVER -> _adventure.value = adventure.value?.copy(adventureStatus = AdventureStatus.DONE) + TRY_COUNT_OVER -> { + _adventure.value = adventure.value?.copy(adventureStatus = AdventureStatus.DONE) + _throwable.value = throwable + } NOT_ARRIVED -> { val currentRemainingTryCount = adventure.value?.remainingTryCount ?: return _adventure.value = adventure.value?.copy(remainingTryCount = currentRemainingTryCount - 1) + _throwable.value = throwable + } + else -> { + _throwable.value = throwable } - else -> { _throwable.value = throwable } } } @@ -172,14 +178,15 @@ class OnAdventureViewModel @Inject constructor( }.onSuccess { _isSendLetterSuccess.value = true }.onFailure { + setThrowable(it) } } } private fun setThrowable(throwable: Throwable) { when (throwable) { - is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } - is GameThrowable -> { handleGameThrowable(throwable) } + is IOException -> _throwable.value = DataThrowable.NetworkThrowable() + is GameThrowable -> handleGameThrowable(throwable) is UniversalThrowable -> _throwable.value = throwable } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt index 19c7ccc9a..88c4c9639 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt @@ -16,6 +16,7 @@ import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.presentation.beginadventure.BeginAdventureActivity import com.now.naaga.presentation.common.dialog.NaagaAlertDialog import com.now.naaga.presentation.login.LoginActivity +import com.now.naaga.presentation.splash.SplashViewModel.Companion.EXPIRATION_AUTH_ERROR_CODE import com.now.naaga.util.extension.getPackageInfoCompat import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @@ -84,7 +85,8 @@ class SplashActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } viewModel.throwable.observe(this) { throwable: DataThrowable -> when (throwable.code) { - DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + DataThrowable.NETWORK_THROWABLE_CODE -> showToast(getString(R.string.network_error_message)) + EXPIRATION_AUTH_ERROR_CODE -> showToast(getString(R.string.splash_re_login_message)) } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt index 426f48f4d..abde2566d 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt @@ -35,8 +35,21 @@ class SplashViewModel @Inject constructor(private val statisticsRepository: Stat private fun setThrowable(throwable: Throwable) { when (throwable) { - is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } - is DataThrowable.AuthorizationThrowable -> { _throwable.value = throwable } + is IOException -> { + if (isAuthorizationThrowable(throwable)) { + _throwable.value = DataThrowable.AuthorizationThrowable(EXPIRATION_AUTH_ERROR_CODE, "") + } + } } } + + private fun isAuthorizationThrowable(throwable: Throwable): Boolean { + if (throwable.message == null) return false + return throwable.message!!.contains(AUTHORIZATION_THROWABLE) + } + + companion object { + const val EXPIRATION_AUTH_ERROR_CODE = 102 + private const val AUTHORIZATION_THROWABLE = "AuthorizationThrowable" + } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt index 5ac5a77f1..ebd8f7b54 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt @@ -6,12 +6,12 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap -import android.graphics.BitmapFactory import android.location.Location import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore +import android.provider.Settings import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels @@ -20,6 +20,7 @@ import com.google.android.gms.location.LocationServices import com.google.android.gms.tasks.CancellationToken import com.google.android.gms.tasks.CancellationTokenSource import com.google.android.gms.tasks.OnTokenCanceledListener +import com.google.android.material.snackbar.Snackbar import com.now.domain.model.Coordinate import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate @@ -27,7 +28,7 @@ import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.UPLOAD_OPEN_CAMERA import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityUploadBinding -import com.now.naaga.presentation.upload.UploadViewModel.Companion.FILE_EMPTY +import com.now.naaga.util.BitmapBuilder import com.now.naaga.util.extension.openSetting import com.now.naaga.util.extension.showSnackbarWithEvent import com.now.naaga.util.extension.showToast @@ -40,43 +41,57 @@ import java.time.LocalDateTime class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityUploadBinding private val viewModel: UploadViewModel by viewModels() - - private val cameraLauncher = registerForActivityResult( - ActivityResultContracts.TakePicturePreview(), - ) { bitmap -> - if (bitmap != null) { - setImage(bitmap) + private var imageUri: Uri? = null + private val cameraLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> + if (!success) return@registerForActivityResult + if (imageUri == null) { + showToast(getString(R.string.upload_image_orientation_error_message)) + return@registerForActivityResult } + setImage(requireNotNull(imageUri) { "imageUri가 null입니다" }) } - private val requestPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestMultiplePermissions(), - ) { permission: Map -> + private val storagePermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val isGranted: Boolean = permissions.values.all { it } + if (!isGranted) { + showStoragePermissionSnackBar() + return@registerForActivityResult + } + openCamera() + } - val keys = permission.entries.map { it.key } - val isStorageRequest = storagePermissions.any { keys.contains(it) } - if (isStorageRequest) { - if (permission.entries.map { it.value }.contains(false)) { - showPermissionSnackbar(getString(R.string.snackbar_storage_message)) - } else { - openCamera() + private val locationPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + val isGranted: Boolean = permissions.values.all { it } + if (!isGranted) { + showLocationPermissionSnackBar() + return@registerForActivityResult } - return@registerForActivityResult + setCoordinate() } - showPermissionSnackbar(getString(R.string.snackbar_location_message)) + + private val isStoragePermissionGranted: Boolean + get() = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + storagePermissionsBelow28.all { checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED } + } else { + true + } + + private val locationSettingLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + setCoordinate() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityUploadBinding.inflate(layoutInflater) setContentView(binding.root) initViewModel() subscribe() - registerAnalytics(this.lifecycle) - setCoordinate() setClickListeners() + locationPermissionLauncher.launch(locationPermissions) + registerAnalytics(this.lifecycle) } private fun initViewModel() { @@ -115,29 +130,88 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } } + private fun setClickListeners() { + binding.ivUploadCameraIcon.setOnClickListener { + logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) + openCameraWithPermission() + } + binding.ivUploadPhoto.setOnClickListener { + logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) + openCameraWithPermission() + } + binding.ivUploadBack.setOnClickListener { + finish() + } + binding.btnUploadSubmit.setOnClickListener { + if (isFormValid().not()) { + showToast(getString(R.string.upload_error_insufficient_info_message)) + } else { + viewModel.postPlace() + } + } + } + private fun changeVisibility(view: View, status: Int) { view.visibility = status } - private fun showPermissionSnackbar(message: String) { + private fun showStoragePermissionSnackBar() { binding.root.showSnackbarWithEvent( - message = message, + message = getString(R.string.snackbar_storage_message), actionTitle = getString(R.string.snackbar_action_title), + length = Snackbar.LENGTH_LONG, action = { openSetting() }, ) } + private fun showLocationPermissionSnackBar() { + binding.root.showSnackbarWithEvent( + message = getString(R.string.snackbar_location_message), + actionTitle = getString(R.string.snackbar_action_title), + length = Snackbar.LENGTH_INDEFINITE, + action = { + val appDetailsIntent = getSettingIntent() + locationSettingLauncher.launch(appDetailsIntent) + }, + ) + } + + private fun getSettingIntent() = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:$packageName"), + ).addCategory(Intent.CATEGORY_DEFAULT) + + private fun setImage(uri: Uri) { + binding.ivUploadCameraIcon.visibility = View.GONE + val bitmap = getBitmap(uri) + binding.ivUploadPhoto.setImageBitmap(bitmap) + val file = saveFile(bitmap) + viewModel.setFile(file) + } + + private fun getBitmap(uri: Uri) = BitmapBuilder(uri, contentResolver) + .addScaling(RESIZE) + .setProperRotate() + .build() + + private fun saveFile(bitmap: Bitmap): File { + val tempFile = File.createTempFile(System.currentTimeMillis().toString(), ".jpeg", cacheDir) + FileOutputStream(tempFile).use { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) + } + return tempFile + } + private fun setCoordinate() { - if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - fusedLocationClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, createCancellationToken()) - .addOnSuccessListener { location -> - location.let { viewModel.setCoordinate(getCoordinate(location)) } - } - .addOnFailureListener { } - } else { - requestPermissionLauncher.launch(locationPermissions) + if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { + showLocationPermissionSnackBar() + return } + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + fusedLocationClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, createCancellationToken()) + .addOnSuccessListener { location -> + location.let { viewModel.setCoordinate(getCoordinate(location)) } + }.addOnFailureListener { } } private fun createCancellationToken(): CancellationToken { @@ -163,72 +237,28 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic return (number * 10_000).toLong().toDouble() / 10_000 } - private fun setClickListeners() { - binding.ivUploadCameraIcon.setOnClickListener { - logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) - requestStoragePermission() - } - binding.ivUploadPhoto.setOnClickListener { - logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) - requestStoragePermission() - } - binding.ivUploadBack.setOnClickListener { - finish() - } - binding.btnUploadSubmit.setOnClickListener { - if (isFormValid().not()) { - showToast(getString(R.string.upload_error_insufficient_info_message)) - } else { - viewModel.postPlace() - } + private fun openCameraWithPermission() { + if (!isStoragePermissionGranted) { + storagePermissionLauncher.launch(storagePermissionsBelow28) + return } - } - - private fun requestStoragePermission() { - val permissionToRequest = storagePermissions.toMutableList() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - return openCamera() - } - - requestPermissionLauncher.launch(permissionToRequest.toTypedArray()) + openCamera() } private fun openCamera() { - cameraLauncher.launch(null) - } - - private fun setImage(bitmap: Bitmap) { - binding.ivUploadCameraIcon.visibility = View.GONE - binding.ivUploadPhoto.setImageBitmap(bitmap) - val uri = getImageUri(bitmap) ?: Uri.EMPTY - val file = makeImageFile(uri) - viewModel.setFile(file) - } - - private fun makeImageFile(uri: Uri): File { - val bitmap = contentResolver.openInputStream(uri).use { - BitmapFactory.decodeStream(it) + imageUri = createImageUri().getOrElse { + Snackbar.make(binding.root, getString(R.string.upload_retry_message), Snackbar.LENGTH_SHORT).show() + return } - val tempFile = File.createTempFile("image", ".jpeg", cacheDir) ?: FILE_EMPTY - - FileOutputStream(tempFile).use { - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) - } - return tempFile + cameraLauncher.launch(imageUri) } - private fun getImageUri(bitmap: Bitmap): Uri? { - val resolver = applicationContext.contentResolver - resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) - ?.let { imageUri -> - val outputStream = resolver.openOutputStream(imageUri) - outputStream?.use { stream -> - bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream) - } - return imageUri - } - return null + private fun createImageUri(): Result { + val imageUri: Uri = contentResolver.insert( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + contentValues, + ) ?: return Result.failure(IllegalStateException("이미지 uri를 가져오지 못했습니다.")) + return Result.success(imageUri) } private fun isFormValid(): Boolean { @@ -236,7 +266,10 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } companion object { - private val storagePermissions = arrayOf( + private const val PRIORITY_HIGH_ACCURACY = 100 + private const val RESIZE = 500 + private val storagePermissionsBelow28 = arrayOf( + Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE, ) @@ -250,8 +283,6 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") } - const val PRIORITY_HIGH_ACCURACY = 100 - fun getIntent(context: Context): Intent { return Intent(context, UploadActivity::class.java) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadViewModel.kt index 17efad611..94d3f29d7 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadViewModel.kt @@ -21,7 +21,7 @@ import javax.inject.Inject class UploadViewModel @Inject constructor( private val placeRepository: PlaceRepository, ) : ViewModel() { - private var file = FILE_EMPTY + private var file: File? = null val name = MutableLiveData() @@ -43,7 +43,7 @@ class UploadViewModel @Inject constructor( } fun isFormValid(): Boolean { - return (file != FILE_EMPTY) && (_coordinate.value != null) && (name.value != null) + return (file != null) && (_coordinate.value != null) && (name.value != null) } fun postPlace() { @@ -55,7 +55,7 @@ class UploadViewModel @Inject constructor( name = name.value.toString(), description = "", coordinate = coordinate, - file = file, + file = file!!, ) }.onSuccess { _successUpload.setValue(UploadStatus.SUCCESS) diff --git a/android/app/src/main/java/com/now/naaga/util/BitmapBuilder.kt b/android/app/src/main/java/com/now/naaga/util/BitmapBuilder.kt new file mode 100644 index 000000000..dbe8508ec --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/BitmapBuilder.kt @@ -0,0 +1,79 @@ +package com.now.naaga.util + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.BitmapFactory.Options +import android.graphics.Matrix +import android.net.Uri +import androidx.exifinterface.media.ExifInterface + +class BitmapBuilder( + private val imageUri: Uri, + private val contentResolver: ContentResolver, +) { + private var sampleSize: Int = 1 + private var isProperRotate: Boolean = false + + fun addScaling(resize: Int): BitmapBuilder { + val options = BitmapFactory.Options() + BitmapFactory.decodeStream(contentResolver.openInputStream(imageUri), null, options) + + var width = options.outWidth + var height = options.outHeight + var sampleSize = 1 + while (true) { + if (width / 2 < resize || height / 2 < resize) break + width /= 2 + height /= 2 + sampleSize *= 2 + } + + this.sampleSize = sampleSize + + return this + } + + fun setProperRotate(): BitmapBuilder { + isProperRotate = true + return this + } + + fun build(): Bitmap { + val bitmap = getBitmapFromUri(Options().apply { inSampleSize = sampleSize }) + if (isProperRotate) { + return getRotatedBitmap(bitmap) + } + return bitmap + } + + private fun getBitmapFromUri(option: Options?): Bitmap { + return BitmapFactory.decodeStream( + contentResolver.openInputStream(imageUri), + null, + option, + ) ?: throw IllegalStateException("비트맵 생성에 실패했습니다.") + } + + private fun getRotatedBitmap(bitmap: Bitmap): Bitmap { + val orientation = getImageOrientation() + if (orientation == 0) return bitmap + + val matrix = Matrix() + matrix.setRotate(orientation.toFloat(), (bitmap.width / 2).toFloat(), (bitmap.height / 2).toFloat()) + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true) + } + + private fun getImageOrientation(): Int { + val inputStream = requireNotNull(contentResolver.openInputStream(imageUri)) { "Uri로 InputStream을 여는데 실패했습니다." } + val exif = ExifInterface(inputStream) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1) + + return when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> 90 + ExifInterface.ORIENTATION_ROTATE_180 -> 180 + ExifInterface.ORIENTATION_ROTATE_270 -> 270 + else -> 0 + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt index bf9dcf4ca..808c712df 100644 --- a/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt +++ b/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt @@ -4,13 +4,20 @@ import android.view.View import com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_SLIDE import com.google.android.material.snackbar.Snackbar -fun View.showSnackbar(message: String) { - Snackbar.make(this, message, Snackbar.LENGTH_SHORT).setAnimationMode(ANIMATION_MODE_SLIDE).show() +fun View.showShortSnackbarWithEvent(message: String, actionTitle: String, action: () -> Unit) { + Snackbar.make(this, message, Snackbar.LENGTH_SHORT) + .setAction(actionTitle) { + action() + }.setAnimationMode(ANIMATION_MODE_SLIDE).show() } -fun View.showSnackbarWithEvent(message: String, actionTitle: String, action: () -> Unit) { - Snackbar.make(this, message, Snackbar.LENGTH_SHORT) +fun View.showSnackbarWithEvent(message: String, actionTitle: String, length: Int, action: () -> Unit) { + Snackbar.make(this, message, length) .setAction(actionTitle) { action() }.setAnimationMode(ANIMATION_MODE_SLIDE).show() } + +fun View.showSnackbar(message: String) { + Snackbar.make(this, message, Snackbar.LENGTH_SHORT).setAnimationMode(ANIMATION_MODE_SLIDE).show() +} diff --git a/android/app/src/main/res/color/selector_main_gray_purple.xml b/android/app/src/main/res/color/selector_main_gray_purple.xml deleted file mode 100644 index 8cfae0a47..000000000 --- a/android/app/src/main/res/color/selector_main_gray_purple.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rect_radius_small.xml b/android/app/src/main/res/drawable/rect_radius_small.xml index c3ca54021..dadcaab82 100644 --- a/android/app/src/main/res/drawable/rect_radius_small.xml +++ b/android/app/src/main/res/drawable/rect_radius_small.xml @@ -1,5 +1,5 @@ - - - + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rect_red_white_radius_small.xml b/android/app/src/main/res/drawable/rect_red_white_radius_small.xml deleted file mode 100644 index 959ac4a2c..000000000 --- a/android/app/src/main/res/drawable/rect_red_white_radius_small.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml index 6eb8d23c2..ff4b33fa7 100644 --- a/android/app/src/main/res/layout/activity_adventure_detail.xml +++ b/android/app/src/main/res/layout/activity_adventure_detail.xml @@ -54,7 +54,10 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="4dp" android:layout_marginTop="@dimen/space_default_medium" - android:background="@drawable/rect_red_white_radius_small" + android:background="@drawable/rect_radius_small" + android:backgroundTint="@color/secondary" + app:tabTextColor="@color/on_secondary" + app:tabSelectedTextColor="@color/on_primary" app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/android/app/src/main/res/layout/activity_adventure_result.xml b/android/app/src/main/res/layout/activity_adventure_result.xml index 4a4ed4a69..b150e82df 100644 --- a/android/app/src/main/res/layout/activity_adventure_result.xml +++ b/android/app/src/main/res/layout/activity_adventure_result.xml @@ -81,7 +81,7 @@ android:layout_marginHorizontal="32dp" android:layout_marginTop="16dp" android:background="@drawable/rect_radius_small" - android:backgroundTint="@color/white" + android:backgroundTint="@color/primary" android:orientation="vertical" android:paddingHorizontal="24dp" android:paddingVertical="16dp" @@ -101,7 +101,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/adventureResult_time_description" - android:textColor="@color/main_gray" + android:textColor="@color/on_secondary" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -131,6 +131,7 @@ android:id="@+id/divider" android:layout_width="match_parent" android:layout_height="2dp" + android:backgroundTint="@color/on_primary" android:background="?android:attr/listDivider" /> + android:backgroundTint="@color/on_primary" /> + android:background="?android:attr/listDivider" + android:backgroundTint="@color/on_primary" /> - - + app:layout_constraintStart_toStartOf="parent" + tools:text="모험 시작" /> - - - - + android:paddingBottom="20dp" + android:visibility="@{viewModel.isNearby() ? View.VISIBLE : View.GONE}" + app:radius="8dp" + app:buttonColor="yellow" /> diff --git a/android/app/src/main/res/layout/activity_upload.xml b/android/app/src/main/res/layout/activity_upload.xml index ab5a7ada0..7058e7f08 100644 --- a/android/app/src/main/res/layout/activity_upload.xml +++ b/android/app/src/main/res/layout/activity_upload.xml @@ -61,7 +61,7 @@ android:layout_height="300dp" android:layout_marginTop="@dimen/space_default_large" android:background="@drawable/rect_radius_small" - android:backgroundTint="@color/white" + android:backgroundTint="@color/primary" android:scaleType="centerCrop" app:layout_constraintBottom_toTopOf="@id/tv_upload_title" app:layout_constraintEnd_toEndOf="parent" @@ -117,19 +117,20 @@ android:textSize="24sp" app:layout_constraintEnd_toEndOf="@id/v_upload_divide_line_1" app:layout_constraintStart_toStartOf="@id/v_upload_divide_line_1" - app:layout_constraintTop_toBottomOf="@id/tv_upload_title"/> + app:layout_constraintTop_toBottomOf="@id/tv_upload_title" /> - diff --git a/android/app/src/main/res/layout/custom_mypage_grid.xml b/android/app/src/main/res/layout/custom_mypage_grid.xml index c6367edb2..eb2f4e72c 100644 --- a/android/app/src/main/res/layout/custom_mypage_grid.xml +++ b/android/app/src/main/res/layout/custom_mypage_grid.xml @@ -14,7 +14,7 @@ android:layout_marginStart="18dp" android:layout_marginTop="10dp" android:text="@string/mypage_place_title" - android:textColor="@color/black" + android:textColor="@color/on_primary" android:textSize="20sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/android/app/src/main/res/layout/custom_mypage_grid_empty.xml b/android/app/src/main/res/layout/custom_mypage_grid_empty.xml index 3c01f51f6..e2e48cabb 100644 --- a/android/app/src/main/res/layout/custom_mypage_grid_empty.xml +++ b/android/app/src/main/res/layout/custom_mypage_grid_empty.xml @@ -18,7 +18,7 @@ android:layout_marginStart="18dp" android:layout_marginTop="10dp" android:text="@string/mypage_place_title" - android:textColor="@color/black" + android:textColor="@color/on_primary" android:textSize="20sp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -31,7 +31,7 @@ android:paddingTop="@dimen/space_default_medium" android:paddingBottom="@dimen/space_default_large" android:text="@string/mypage_empty_description" - android:textColor="@color/main_gray" + android:textColor="@color/on_secondary" android:textSize="16sp" app:layout_constraintTop_toBottomOf="@id/tv_mypage_empty_item_title" /> diff --git a/android/app/src/main/res/layout/dialog_polaroid.xml b/android/app/src/main/res/layout/dialog_polaroid.xml index ebe4e3d22..a274afbcd 100644 --- a/android/app/src/main/res/layout/dialog_polaroid.xml +++ b/android/app/src/main/res/layout/dialog_polaroid.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/rect_radius_large" - android:backgroundTint="@color/red_white"> + android:backgroundTint="@color/secondary"> + android:background="@drawable/rect_radius_small" + android:backgroundTint="@color/secondary"> + android:background="@drawable/rect_radius_small" + android:backgroundTint="@color/secondary"> @@ -39,7 +39,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginTop="4dp" - android:textColor="@color/black" + android:textColor="@color/on_primary" android:textSize="16sp" android:ellipsize="end" android:maxLines="1" @@ -68,7 +68,7 @@ android:layout_height="wrap_content" android:layout_marginEnd="12dp" android:layout_marginBottom="12dp" - android:textColor="@color/main_gray" + android:textColor="@color/on_secondary" android:textSize="12sp" android:text="@{adventureResult.beginTime.toLocalDate().toString()}" app:layout_constraintBottom_toBottomOf="parent" diff --git a/android/app/src/main/res/layout/item_letter.xml b/android/app/src/main/res/layout/item_letter.xml index 6e2801156..244899d7a 100644 --- a/android/app/src/main/res/layout/item_letter.xml +++ b/android/app/src/main/res/layout/item_letter.xml @@ -12,7 +12,8 @@ android:layout_height="wrap_content" android:layout_marginHorizontal="@dimen/space_default_large" android:layout_marginTop="@dimen/space_default_medium" - android:background="@drawable/rect_red_white_radius_small"> + android:background="@drawable/rect_radius_small" + android:backgroundTint="@color/secondary" > + + + #FF000000 + #FFFFFFFF + #BDBDBD + #80989898 + + #0B0726 + #F6BF0C + #10C1FD + #E93394 + + + #232036 + #413E54 + #CCCCCC + #FFFFFFFF + + + + #4D000000 + + #FFD50C + #E3A40A + #FFB70A + #FFEE9F + #B68A21 + + + #1BCCFF + #009DDE + #0CB4F9 + #AFEDFF + #0D3A85 + diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml index 2577d80c9..ed0efa736 100644 --- a/android/app/src/main/res/values-night/themes.xml +++ b/android/app/src/main/res/values-night/themes.xml @@ -2,13 +2,16 @@ - \ No newline at end of file + diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml index 586569140..9cf31cffc 100644 --- a/android/app/src/main/res/values/attrs.xml +++ b/android/app/src/main/res/values/attrs.xml @@ -5,4 +5,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index 4d0ba9299..6ed756c11 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,25 +1,36 @@ - #ff0000 + #FF000000 #FFFFFFFF - #D4A5E4 - #E3C6ED - #F6EBF9 - #FFA5E4DB #BDBDBD - #616161 #80989898 - #E4A5A5 - - #0D0C4A - #E9D1F1 - - #EAE0E0 #0B0726 #F6BF0C #10C1FD #E93394 + + #FFFFFFFF + #EAE0E0 + #616161 + #FF000000 + + + + #4D000000 + + #FFD50C + #E3A40A + #FFB70A + #FFEE9F + #B68A21 + + + #1BCCFF + #009DDE + #0CB4F9 + #AFEDFF + #0D3A85 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 24a77863a..f7a5824d2 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -62,6 +62,12 @@ 사진을 저장하는데 문제가 생겼어요! 다시 시도해주세요! 전송에 실패했어요! 다시 시도해주세요! 모든 정보를 입력해주세요. + 사진 가져오기에 실패했습니다 + 실패했습니다. 사진을 세로로 찍어보세요! + 다시 시도해 주세요! + + + 인증 정보가 만료 되었어요!\n다시 로그인 해주세요! 모험 기록 @@ -105,7 +111,7 @@ "성공적으로 회원 탈퇴 되었습니다." - "인증 정보가 잘 못 되었어요!" + "인증 정보가 잘못 되었어요!" "인증 정보가 만료 되었어요!" 로그아웃 되었습니다. 설정 @@ -146,9 +152,11 @@ 이 곳에 내용을 작성해주세요! + 내용을 1자 이상 입력해주세요! 전송하기 문제가 발생했어요. 다시 시도해주세요! + diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml index f36421ca7..7fe43a01e 100644 --- a/android/app/src/main/res/values/themes.xml +++ b/android/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ -