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 @@
-