diff --git a/.github/workflows/android_ci.yml b/.github/workflows/android_ci.yml index 3a6961504..8d9401e41 100644 --- a/.github/workflows/android_ci.yml +++ b/.github/workflows/android_ci.yml @@ -2,7 +2,7 @@ name: NAAGA ANDROID CI on: pull_request: - branches: [ dev ] + branches: [ dev_android ] paths: - 'android/**' diff --git a/.github/workflows/backend_dev_merge_workflow.yml b/.github/workflows/backend_dev_merge_workflow.yml index 11aaf056d..867e515ac 100644 --- a/.github/workflows/backend_dev_merge_workflow.yml +++ b/.github/workflows/backend_dev_merge_workflow.yml @@ -2,7 +2,8 @@ name: NAAGA BACKEND MERGE CI on: push: branches: - - dev + - dev_backend + jobs: deploy: runs-on: naaga diff --git a/.github/workflows/backend_dev_pr_workflow.yml b/.github/workflows/backend_dev_pr_workflow.yml index cd38b349c..36ba39d66 100644 --- a/.github/workflows/backend_dev_pr_workflow.yml +++ b/.github/workflows/backend_dev_pr_workflow.yml @@ -3,7 +3,7 @@ name: NAAGA BACKEND PULL REQUEST CI on: pull_request: branches: - - dev + - dev_backend paths: - backend/** @@ -52,3 +52,16 @@ jobs: - name: Test with Gradle run: ./gradlew test + + - name: 테스트 결과를 PR에 코멘트로 등록합니다 + uses: EnricoMi/publish-unit-test-result-action@v1 + if: always() + with: + files: 'backend/build/test-results/test/TEST-*.xml' + + - name: 테스트 실패 시, 실패한 코드 라인에 Check 코멘트를 등록합니다 + uses: mikepenz/action-junit-report@v3 + if: always() + with: + report_paths: '**/build/test-results/test/TEST-*.xml' + token: ${{ github.token }} diff --git a/.github/workflows/backend_prod_merge_workflow.yml b/.github/workflows/backend_prod_merge_workflow.yml index 69cb6861a..9615d5c08 100644 --- a/.github/workflows/backend_prod_merge_workflow.yml +++ b/.github/workflows/backend_prod_merge_workflow.yml @@ -4,7 +4,9 @@ on: push: branches: - main - + paths: + - backend/** + jobs: deploy: runs-on: naaga @@ -38,3 +40,22 @@ jobs: cd /home/ubuntu/prod chmod +x ./deploy_prod.sh sudo ./deploy_prod.sh + + sync-dev_backend: + needs: deploy + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: main + + - name: merge main -> dev_backend + uses: devmasx/merge-branch@master + with: + type: now + from_branch: main + target_branch: dev_backend + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/backend_rel_merge_workflow.yml b/.github/workflows/backend_rel_merge_workflow.yml new file mode 100644 index 000000000..3ac3a3e3b --- /dev/null +++ b/.github/workflows/backend_rel_merge_workflow.yml @@ -0,0 +1,60 @@ +name: NAAGA BACKEND RELEASE SERVER MERGE CI/CD + +on: + push: + branches: + - release/** + paths: + - backend/** + +jobs: + deploy: + runs-on: naaga + steps: + - name: change permission + run: | + sudo chown -R ubuntu:ubuntu /home/ubuntu/actions-runner/naaga/2023-naaga/2023-naaga + + - name: checkout + uses: actions/checkout@v3 + with: + submodules: true + ssh-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: project remove + run: | + sudo rm -rf /home/ubuntu/prod/2023-naaga + + - name: project copy + run: | + sudo cp -r /home/ubuntu/actions-runner/naaga/2023-naaga/2023-naaga /home/ubuntu/prod + + - name: build + run: | + cd /home/ubuntu/prod/2023-naaga/backend + sudo chmod +x ./gradlew + sudo ./gradlew clean bootJar + + - name: transfer & run + run: | + cd /home/ubuntu/prod + chmod +x ./deploy_prod.sh + sudo ./deploy_prod.sh + + sync-dev_backend: + needs: deploy + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + ref: main + + - name: merge release -> dev_backend + uses: devmasx/merge-branch@master + with: + type: now + target_branch: dev_backend + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..0a5d4cc38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.idea \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index cd7f67f95..bbe975504 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -5,6 +5,8 @@ plugins { id 'kotlin-parcelize' id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' + id 'kotlin-kapt' + id 'com.google.dagger.hilt.android' } Properties properties = new Properties() @@ -18,10 +20,11 @@ android { applicationId "com.now.naaga" minSdk 28 targetSdk 33 - versionCode 4 - versionName "1.0.1" + versionCode 5 + versionName "1.1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "com.now.naaga.HiltTestRunner" buildConfigField "String", "BASE_URL", properties["BASE_URL"] buildConfigField "String", "KAKAO_NATIVE_APP_KEY", properties["KAKAO_NATIVE_APP_KEY"] resValue "string", "kakao_redirection_scheme", properties["kakao_redirection_scheme"] @@ -31,6 +34,8 @@ android { release { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + + ndk.debugSymbolLevel 'FULL' } } compileOptions { @@ -43,15 +48,20 @@ android { dataBinding { enable = true } + testOptions { + unitTests { + includeAndroidResources = true + } + } } dependencies { - implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(path: ':domain') + implementation 'androidx.test:runner:1.5.2' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -88,6 +98,7 @@ dependencies { // coroutines implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.2") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") // splash implementation "androidx.core:core-splashscreen:1.0.0" @@ -96,6 +107,7 @@ dependencies { implementation(platform('com.google.firebase:firebase-bom:32.2.0')) implementation "com.google.firebase:firebase-analytics-ktx" implementation("com.google.firebase:firebase-crashlytics-ktx") + implementation("com.google.firebase:firebase-config-ktx") // kakao Login implementation "com.kakao.sdk:v2-user:2.15.0" @@ -105,4 +117,26 @@ dependencies { // lottie implementation "com.airbnb.android:lottie:6.1.0" + + // ViewModel Test + testImplementation("androidx.arch.core:core-testing:2.2.0") + + // Robolectric + testImplementation("org.robolectric:robolectric:4.9") + + // hilt + implementation "com.google.dagger:hilt-android:2.44" + kapt "com.google.dagger:hilt-compiler:2.44" + + implementation "androidx.activity:activity-ktx:1.7.2" + + // For Robolectric tests. + testImplementation 'com.google.dagger:hilt-android-testing:2.44' + // ...with Kotlin. + kaptTest 'com.google.dagger:hilt-android-compiler:2.44' +} + +// Allow references to generated code +kapt { + correctErrorTypes true } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 513434637..f8a7b38c7 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -30,8 +30,12 @@ + android:screenOrientation="portrait"> + + + + + - - - - - - + android:exported="false" /> + diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 000000000..27f053269 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/java/com/now/naaga/NaagaApplication.kt b/android/app/src/main/java/com/now/naaga/NaagaApplication.kt index 31015517a..e68c27296 100644 --- a/android/app/src/main/java/com/now/naaga/NaagaApplication.kt +++ b/android/app/src/main/java/com/now/naaga/NaagaApplication.kt @@ -5,7 +5,9 @@ import androidx.appcompat.app.AppCompatDelegate import com.kakao.sdk.common.KakaoSdk import com.now.naaga.data.local.AuthDataSource import com.now.naaga.data.local.DefaultAuthDataSource +import dagger.hilt.android.HiltAndroidApp +@HiltAndroidApp class NaagaApplication : Application() { override fun onCreate() { diff --git a/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ButtonNames.kt b/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ButtonNames.kt index e3a115fce..34b5d465f 100644 --- a/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ButtonNames.kt +++ b/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ButtonNames.kt @@ -2,7 +2,7 @@ package com.now.naaga.data.firebase.analytics // BeginAdventureActivity const val BEGIN_BEGIN_ADVENTURE = "BEGIN_ADVENTURE" -const val BEGIN_GO_RANK = "GO_RANK" +const val BEGIN_GO_SETTING = "GO_SETTING" const val BEGIN_GO_UPLOAD = "GO_UPLOAD" const val BEGIN_GO_MYPAGE = "GO_MYPAGE" diff --git a/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ServerErrorNames.kt b/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ServerErrorNames.kt index 9fd47360a..ea7e7acac 100644 --- a/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ServerErrorNames.kt +++ b/android/app/src/main/java/com/now/naaga/data/firebase/analytics/ServerErrorNames.kt @@ -6,3 +6,4 @@ const val ADVENTURE_RESULT = "ADVENTURE_RESULT" const val LOGIN_AUTH = "LOGIN" const val MY_PAGE_STATISTICS = "MY_PAGE" const val RANK_RANK = "RANK" +const val SPLASH_MY_PAGE_STATISTICS = "SPLASH_BUT_MY_PAGE_API" diff --git a/android/app/src/main/java/com/now/naaga/data/local/AuthDataSource.kt b/android/app/src/main/java/com/now/naaga/data/local/AuthDataSource.kt index e0a07683e..0616b9227 100644 --- a/android/app/src/main/java/com/now/naaga/data/local/AuthDataSource.kt +++ b/android/app/src/main/java/com/now/naaga/data/local/AuthDataSource.kt @@ -5,4 +5,5 @@ interface AuthDataSource { fun setAccessToken(newToken: String) fun getRefreshToken(): String? fun setRefreshToken(newToken: String) + fun resetToken() } diff --git a/android/app/src/main/java/com/now/naaga/data/local/DefaultAuthDataSource.kt b/android/app/src/main/java/com/now/naaga/data/local/DefaultAuthDataSource.kt index 9b9757482..f7a2ef711 100644 --- a/android/app/src/main/java/com/now/naaga/data/local/DefaultAuthDataSource.kt +++ b/android/app/src/main/java/com/now/naaga/data/local/DefaultAuthDataSource.kt @@ -19,7 +19,8 @@ class DefaultAuthDataSource(context: Context) : AuthDataSource { ) override fun getAccessToken(): String? { - return authPreference.getString(ACCESS_TOKEN, null) + val accessToken = authPreference.getString(ACCESS_TOKEN, null) ?: return null + return BEARER + accessToken } override fun setAccessToken(newToken: String) { @@ -27,16 +28,24 @@ class DefaultAuthDataSource(context: Context) : AuthDataSource { } override fun getRefreshToken(): String? { - return authPreference.getString(REFRESH_TOKEN, null) + val refreshToken = authPreference.getString(REFRESH_TOKEN, null) ?: return null + return BEARER + refreshToken } override fun setRefreshToken(newToken: String) { authPreference.edit().putString(REFRESH_TOKEN, newToken).apply() } + override fun resetToken() { + authPreference.edit().putString(ACCESS_TOKEN, DEFAULT_TOKEN).apply() + authPreference.edit().putString(REFRESH_TOKEN, DEFAULT_TOKEN).apply() + } + companion object { + private const val DEFAULT_TOKEN = "" private const val AUTH_ENCRYPTED_PREFERENCE = "AUTH_ENCRYPTED_PREFERENCE" private const val ACCESS_TOKEN = "ACCESS_TOKEN" private const val REFRESH_TOKEN = "REFRESH_TOKEN" + private const val BEARER = "Bearer " } } diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/PlaceMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/PlaceMapper.kt index 7f10ab138..c254fa327 100644 --- a/android/app/src/main/java/com/now/naaga/data/mapper/PlaceMapper.kt +++ b/android/app/src/main/java/com/now/naaga/data/mapper/PlaceMapper.kt @@ -2,6 +2,7 @@ package com.now.naaga.data.mapper import com.now.domain.model.Place import com.now.naaga.data.remote.dto.PlaceDto +import com.now.naaga.data.remote.dto.PostPlaceDto fun Place.toDto(): PlaceDto { return PlaceDto( @@ -22,3 +23,13 @@ fun PlaceDto.toDomain(): Place { description = description, ) } + +fun PostPlaceDto.toDomain(): Place { + return Place( + id = id, + name = name, + coordinate = coordinate.toDomain(), + image = imageUrl, + description = description, + ) +} diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/PostPlaceDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/PostPlaceDto.kt new file mode 100644 index 000000000..149455438 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/PostPlaceDto.kt @@ -0,0 +1,20 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PostPlaceDto( + @SerialName("id") + val id: Long, + @SerialName("name") + val name: String, + @SerialName("coordinate") + val coordinate: CoordinateDto, + @SerialName("imageUrl") + val imageUrl: String, + @SerialName("description") + val description: String, + @SerialName("registeredPlayerId") + val registeredPlayerId: Long, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/RefreshTokenDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/RefreshTokenDto.kt new file mode 100644 index 000000000..6c6ed2410 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/RefreshTokenDto.kt @@ -0,0 +1,10 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class RefreshTokenDto( + @SerialName("refreshToken") + val refreshToken: String, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/AuthInterceptor.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/AuthInterceptor.kt index 6848c6653..bfe90be5a 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/AuthInterceptor.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/AuthInterceptor.kt @@ -6,6 +6,7 @@ import com.now.naaga.BuildConfig import com.now.naaga.NaagaApplication import com.now.naaga.data.remote.dto.FailureDto import com.now.naaga.data.remote.dto.NaagaAuthDto +import com.now.naaga.data.throwable.DataThrowable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -13,6 +14,7 @@ import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import org.json.JSONObject @@ -22,72 +24,64 @@ class AuthInterceptor : Interceptor { private val client = OkHttpClient.Builder().build() override fun intercept(chain: Interceptor.Chain): Response { - if (isLoginRequest(chain.request())) { - return chain.proceed(chain.request()) - } + val accessToken = getAccessToken() ?: return chain.proceed(chain.request()) - val headerAddedRequest = chain.request().newBuilder().addHeader(AUTH_KEY, getAccessToken()).build() - val response = chain.proceed(headerAddedRequest) + val headerAddedRequest = chain.request().newBuilder().addHeader(AUTH_KEY, accessToken).build() + val response: Response = chain.proceed(headerAddedRequest) if (response.code == 401) { - val failureDto = response.getDto() - when (failureDto.code) { - 101 -> throw IllegalStateException(AUTH_ERROR.format(headerAddedRequest.url.toString())) - 102 -> { - response.close() - return reRequest(chain) - } - - 103 -> throw IllegalStateException(REFRESH_EXPIRED) - } + val newAccessToken = getRefreshedToken(accessToken).getOrElse { return response } + return chain.proceed(chain.request().newBuilder().addHeader(AUTH_KEY, newAccessToken).build()) } return response } - private fun isLoginRequest(request: Request): Boolean { - val path: String = request.url.encodedPath.substringAfter(BuildConfig.BASE_URL) - return path == AUTH_PATH - } + private fun getRefreshedToken(accessToken: String): Result { + val requestBody = createRefreshRequestBody() + val request = createRefreshRequest(requestBody, accessToken) - private fun reRequest(chain: Interceptor.Chain): Response { - val token: String = getRefreshedToken() - val request = chain.request().newBuilder().addHeader(AUTH_KEY, token).build() - return chain.proceed(request) + val auth: NaagaAuthDto = requestRefresh(request).getOrElse { + return Result.failure(it) + } + storeToken(auth.accessToken, auth.refreshToken) + return Result.success(auth.accessToken) } - private fun getRefreshedToken(): String { - val body = JSONObject() + private fun createRefreshRequestBody(): RequestBody { + return JSONObject() .put(AUTH_REFRESH_KEY, getRefreshToken()) .toString() .toRequestBody(contentType = "application/json".toMediaType()) + } - val request = Request.Builder() + private fun createRefreshRequest(requestBody: RequestBody, accessToken: String): Request { + return Request.Builder() .url(BuildConfig.BASE_URL + AUTH_REFRESH_PATH) - .post(body) - .addHeader(AUTH_KEY, getAccessToken()) + .post(requestBody) + .addHeader(AUTH_KEY, accessToken) .build() - - val auth = requestRefresh(request) - storeToken(auth.accessToken, auth.refreshToken) - return auth.accessToken } - private fun requestRefresh(request: Request): NaagaAuthDto { + private fun requestRefresh(request: Request): Result { val response: Response = runBlocking { withContext(Dispatchers.IO) { client.newCall(request).execute() } } if (response.isSuccessful) { - return response.getDto() + return Result.success(response.getDto()) + } + val failedResponse = response.getDto() + if (failedResponse.code == 101) { + return Result.failure(DataThrowable.AuthorizationThrowable(failedResponse.code, failedResponse.message)) } - throw IllegalStateException(REFRESH_FAILURE) + return Result.failure(IllegalStateException(REFRESH_FAILURE)) } - private fun getAccessToken(): String { - return NaagaApplication.authDataSource.getAccessToken() ?: throw IllegalStateException(NO_ACCESS_TOKEN) + private fun getAccessToken(): String? { + return NaagaApplication.authDataSource.getAccessToken() } private fun getRefreshToken(): String { - return NaagaApplication.authDataSource.getRefreshToken() ?: throw IllegalStateException(NO_REFRESH_TOKEN) + return requireNotNull(NaagaApplication.authDataSource.getRefreshToken()) { NO_REFRESH_TOKEN } } private fun storeToken(accessToken: String, refreshToken: String) { @@ -107,9 +101,6 @@ class AuthInterceptor : Interceptor { const val AUTH_PATH = "auth" const val AUTH_REFRESH_PATH = "auth/refresh" - const val AUTH_ERROR = "%s에서 인증 오류 발생" - const val REFRESH_EXPIRED = "리프레시 토큰 만료" - const val NO_ACCESS_TOKEN = "엑세스 토큰이 없습니다" const val NO_REFRESH_TOKEN = "리프레시 토큰이 없습니다" const val REFRESH_FAILURE = "토큰 리프레시 실패" } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitFactory.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitFactory.kt index 2aef2ac2a..c798eeef8 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitFactory.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitFactory.kt @@ -2,11 +2,10 @@ package com.now.naaga.data.remote.retrofit import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory import com.now.naaga.BuildConfig -import com.now.naaga.NaagaApplication import kotlinx.serialization.json.Json -import okhttp3.Interceptor import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit object RetrofitFactory { @@ -18,20 +17,10 @@ object RetrofitFactory { .client(createOkHttpClient()) .build() - private fun createInterceptor(): Interceptor = Interceptor { chain -> - val token: String = NaagaApplication.authDataSource.getAccessToken() ?: "" - with(chain) { - val newRequest = request().newBuilder() - .addHeader("Authorization", "Bearer $token") - .addHeader("Content-Type", "application/json") - .build() - proceed(newRequest) - } - } - private fun createOkHttpClient(): OkHttpClient { return OkHttpClient.Builder().apply { - addInterceptor(createInterceptor()) + addInterceptor(AuthInterceptor()) + addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) }.build() } } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitUtil.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitUtil.kt deleted file mode 100644 index e40c26f76..000000000 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitUtil.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.now.naaga.data.remote.retrofit - -import com.now.naaga.data.throwable.DataThrowable.AuthorizationThrowable -import com.now.naaga.data.throwable.DataThrowable.GameThrowable -import com.now.naaga.data.throwable.DataThrowable.IllegalStateThrowable -import com.now.naaga.data.throwable.DataThrowable.PlaceThrowable -import com.now.naaga.data.throwable.DataThrowable.PlayerThrowable -import com.now.naaga.data.throwable.DataThrowable.UniversalThrowable -import org.json.JSONObject -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response - -fun Response.codeIn400s(): Boolean { - return this.code() in 400..499 -} - -fun Response.codeIn500s(): Boolean { - return this.code() in 500..599 -} - -fun Call.fetchResponse( - onSuccess: (T) -> Unit, - onFailure: (Throwable) -> Unit, -) { - enqueue( - object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - val body = response.body() ?: return onFailure(IllegalStateThrowable()) - onSuccess(body) - } - - if (response.codeIn500s()) { - return onFailure(IllegalStateThrowable()) - } - - if (response.codeIn400s()) { - val errorResponse = response.errorBody()?.string() - val jsonObject = errorResponse?.let { JSONObject(it) } - val code = jsonObject?.getInt("code") ?: 0 - val message = jsonObject?.getString("message") ?: "" - - when (code) { - in 100..199 -> { onFailure(AuthorizationThrowable(code, message)) } - in 200..299 -> { onFailure(UniversalThrowable(code, message)) } - in 300..399 -> { onFailure(PlayerThrowable(code, message)) } - in 400..499 -> { onFailure(GameThrowable(code, message)) } - in 500..599 -> { onFailure(PlaceThrowable(code, message)) } - } - } - } - - override fun onFailure(call: Call, t: Throwable) { - onFailure(IllegalStateThrowable()) - } - }, - ) -} diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/ServicePool.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/ServicePool.kt deleted file mode 100644 index 74ca786a3..000000000 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/ServicePool.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.now.naaga.data.remote.retrofit - -import com.now.naaga.data.remote.retrofit.RetrofitFactory.retrofit -import com.now.naaga.data.remote.retrofit.service.AdventureService -import com.now.naaga.data.remote.retrofit.service.AuthService -import com.now.naaga.data.remote.retrofit.service.PlaceService -import com.now.naaga.data.remote.retrofit.service.RankService -import com.now.naaga.data.remote.retrofit.service.StatisticsService - -object ServicePool { - val adventureService = retrofit.create(AdventureService::class.java) - val rankService = retrofit.create(RankService::class.java) - val statisticsService = retrofit.create(StatisticsService::class.java) - val placeService = retrofit.create(PlaceService::class.java) - val authService = retrofit.create(AuthService::class.java) -} diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AdventureService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AdventureService.kt index 96ac30141..b21316daf 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AdventureService.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/AdventureService.kt @@ -6,7 +6,7 @@ import com.now.naaga.data.remote.dto.AdventureStatusDto import com.now.naaga.data.remote.dto.CoordinateDto import com.now.naaga.data.remote.dto.FinishGameDto import com.now.naaga.data.remote.dto.HintDto -import retrofit2.Call +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH @@ -16,49 +16,49 @@ import retrofit2.http.Query interface AdventureService { @GET("/games") - fun getMyGames(): Call> + suspend fun getMyGames(): Response> @GET("/games/{gameId}") - fun getGame( + suspend fun getGame( @Path("gameId") gameId: Long, - ): Call + ): Response @GET("/games") - fun getGamesByStatus( + suspend fun getGamesByStatus( @Query("status") status: String, - ): Call> + ): Response> @POST("/games") - fun beginGame( + suspend fun beginGame( @Body coordinateDto: CoordinateDto, - ): Call + ): Response @PATCH("/games/{gameId}") - fun endGame( + suspend fun endGame( @Path("gameId") gameId: Long, @Body finishGameDto: FinishGameDto, - ): Call + ): Response @GET("/games/{gameId}/result") - fun getGameResult( + suspend fun getGameResult( @Path("gameId") gameId: Long, - ): Call + ): Response @GET("/games/results") - fun getMyGameResults( + suspend fun getMyGameResults( @Query("sort-by") sortBy: String, @Query("order") order: String, - ): Call> + ): Response> @GET("/games/{gameId}/hints/{hintId}") - fun getHint( + suspend fun getHint( @Path("gameId") gameId: Long, @Path("hintId") hingId: Long, - ): Call + ): Response @POST("/games/{gameId}/hints") - fun requestHint( + suspend fun requestHint( @Path("gameId") gameId: Long, @Body coordinateDto: CoordinateDto, - ): Call + ): Response } 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 73fdde4e0..7308ce262 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,13 +2,20 @@ 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 retrofit2.Call +import retrofit2.Response import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.POST interface AuthService { @POST("/auth") - fun requestAuth( + suspend fun requestAuth( @Body platformAuthDto: PlatformAuthDto, - ): Call + ): Response + + @DELETE("/auth/unlink") + suspend fun withdrawalMember(): Response + + @DELETE("/auth") + suspend fun requestLogout(): Response } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/PlaceService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/PlaceService.kt index 35e395eb1..0fb325c3b 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/PlaceService.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/PlaceService.kt @@ -1,34 +1,34 @@ package com.now.naaga.data.remote.retrofit.service import com.now.naaga.data.remote.dto.PlaceDto +import com.now.naaga.data.remote.dto.PostPlaceDto import okhttp3.MultipartBody import okhttp3.RequestBody -import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST import retrofit2.http.Part +import retrofit2.http.PartMap import retrofit2.http.Path import retrofit2.http.Query interface PlaceService { @GET("/places") - fun getMyPlace( + suspend fun getMyPlace( @Query("sort-by") sort: String, @Query("order") order: String, - ): Call> + ): Response> @GET("/places/{placeId}") - fun getPlace( + suspend fun getPlace( @Path("placeId") placeId: Long, - ): Call + ): Response @Multipart - @POST("/places") - fun registerPlace( - @Part name: RequestBody, - @Part description: RequestBody, - @Part coordinate: RequestBody, + @POST("/temporary-places") + suspend fun registerPlace( + @PartMap postData: HashMap, @Part imageFile: MultipartBody.Part, - ): Call + ): Response } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/RankService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/RankService.kt index bcf36d65b..cf35b3032 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/RankService.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/RankService.kt @@ -1,17 +1,17 @@ package com.now.naaga.data.remote.retrofit.service import com.now.naaga.data.remote.dto.RankDto -import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.Query interface RankService { @GET("/ranks") - fun getAllRanks( + suspend fun getAllRanks( @Query("sort-by") sortBy: String, @Query("order") order: String, - ): Call> + ): Response> @GET("/ranks/my") - fun getMyRank(): Call + suspend fun getMyRank(): Response } diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/StatisticsService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/StatisticsService.kt index 5d95028a9..0682a0d62 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/StatisticsService.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/StatisticsService.kt @@ -1,10 +1,10 @@ package com.now.naaga.data.remote.retrofit.service import com.now.naaga.data.remote.dto.StatisticsDto -import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET interface StatisticsService { @GET("/statistics/my") - fun getMyStatistics(): Call + suspend fun getMyStatistics(): Response } diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultAdventureRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultAdventureRepository.kt index c4546626a..ff0815e2e 100644 --- a/android/app/src/main/java/com/now/naaga/data/repository/DefaultAdventureRepository.kt +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultAdventureRepository.kt @@ -11,127 +11,63 @@ import com.now.domain.model.SortType import com.now.domain.repository.AdventureRepository import com.now.naaga.data.mapper.toDomain import com.now.naaga.data.mapper.toDto -import com.now.naaga.data.remote.dto.AdventureDto import com.now.naaga.data.remote.dto.FinishGameDto -import com.now.naaga.data.remote.retrofit.ServicePool -import com.now.naaga.data.remote.retrofit.fetchResponse +import com.now.naaga.data.remote.retrofit.service.AdventureService +import com.now.naaga.util.getValueOrThrow -class DefaultAdventureRepository : AdventureRepository { - override fun fetchMyAdventures(callback: (Result>) -> Unit) { - val call = ServicePool.adventureService.getMyGames() - call.fetchResponse( - onSuccess = { adventureDtos: List -> - callback(Result.success(adventureDtos.map { it.toDomain() })) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) +class DefaultAdventureRepository( + private val adventureService: AdventureService, +) : AdventureRepository { + override suspend fun fetchMyAdventures(): List { + val response = adventureService.getMyGames().getValueOrThrow() + return response.map { it.toDomain() } } - override fun fetchAdventure(adventureId: Long, callback: (Result) -> Unit) { - val call = ServicePool.adventureService.getGame(adventureId) - call.fetchResponse( - onSuccess = { adventureDto -> - callback(Result.success(adventureDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun fetchAdventure(adventureId: Long): Adventure { + val response = adventureService.getGame(adventureId).getValueOrThrow() + return response.toDomain() } - override fun fetchAdventureByStatus(status: AdventureStatus, callback: (Result>) -> Unit) { - val call = ServicePool.adventureService.getGamesByStatus(status.name) - call.fetchResponse( - onSuccess = { adventureDtos: List -> - callback(Result.success(adventureDtos.map { it.toDomain() })) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun fetchAdventureByStatus(status: AdventureStatus): List { + val response = adventureService.getGamesByStatus(status.name).getValueOrThrow() + return response.map { it.toDomain() } } - override fun beginAdventure(coordinate: Coordinate, callback: (Result) -> Unit) { - val call = ServicePool.adventureService.beginGame(coordinate.toDto()) - call.fetchResponse( - onSuccess = { adventureDto -> - callback(Result.success(adventureDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun beginAdventure(coordinate: Coordinate): Adventure { + val response = adventureService.beginGame(coordinate.toDto()).getValueOrThrow() + return response.toDomain() } - override fun endGame( + override suspend fun endGame( adventureId: Long, endType: AdventureEndType, coordinate: Coordinate, - callback: (Result) -> Unit, - ) { + ): AdventureStatus { val finishGameDto = FinishGameDto(endType.name, coordinate.toDto()) - val call = ServicePool.adventureService.endGame(adventureId, finishGameDto) - call.fetchResponse( - onSuccess = { adventureStatusDto -> - callback(Result.success(AdventureStatus.getStatus(adventureStatusDto.gameStatus))) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + val response = adventureService.endGame(adventureId, finishGameDto).getValueOrThrow() + return AdventureStatus.getStatus(response.gameStatus) } - override fun fetchAdventureResult(adventureId: Long, callback: (Result) -> Unit) { - val call = ServicePool.adventureService.getGameResult(adventureId) - call.fetchResponse( - onSuccess = { adventureResultDto -> - callback(Result.success(adventureResultDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun fetchAdventureResult(adventureId: Long): AdventureResult { + val response = adventureService.getGameResult(adventureId).getValueOrThrow() + return response.toDomain() } - override fun fetchMyAdventureResults( + override suspend fun fetchMyAdventureResults( sortBy: SortType, order: OrderType, - callback: (Result>) -> Unit, - ) { - val call = ServicePool.adventureService.getMyGameResults(sortBy.name, order.name) - call.fetchResponse( - onSuccess = { adventureResultDtos -> - callback(Result.success(adventureResultDtos.map { it.toDomain() })) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + ): List { + val response = adventureService.getMyGameResults(sortBy.name, order.name).getValueOrThrow() + return response.map { it.toDomain() } } - override fun fetchHint(adventureId: Long, hintId: Long, callback: (Result) -> Unit) { - val call = ServicePool.adventureService.getHint(adventureId, hintId) - call.fetchResponse( - onSuccess = { hintDto -> - callback(Result.success(hintDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun fetchHint(adventureId: Long, hintId: Long): Hint { + val response = adventureService.getHint(adventureId, hintId).getValueOrThrow() + return response.toDomain() } - override fun makeHint(adventureId: Long, coordinate: Coordinate, callback: (Result) -> Unit) { - val call = ServicePool.adventureService.requestHint(adventureId, coordinate.toDto()) - call.fetchResponse( - onSuccess = { hintDto -> - callback(Result.success(hintDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun makeHint(adventureId: Long, coordinate: Coordinate): Hint { + val response = adventureService.requestHint(adventureId, coordinate.toDto()).getValueOrThrow() + return response.toDomain() } } 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 09bf224a5..429643c00 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,22 +4,42 @@ 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.retrofit.ServicePool -import com.now.naaga.data.remote.retrofit.fetchResponse +import com.now.naaga.data.remote.retrofit.service.AuthService +import com.now.naaga.util.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) : AuthRepository { - override fun getToken( - platformAuth: PlatformAuth, - callback: (Result) -> Unit, - ) { - val call = ServicePool.authService.requestAuth(platformAuth.toDto()) - call.fetchResponse( - onSuccess = { naagaAuthDto -> +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) - callback(Result.success(true)) - }, - onFailure = { callback(Result.failure(it)) }, - ) + return@withContext true + } + return@withContext false + } + } + + override suspend fun logout() { + withContext(dispatcher) { + val response = authService.requestLogout() + authDataSource.resetToken() + response.getValueOrThrow() + } + } + + override suspend fun withdrawalMember() { + authService.withdrawalMember() + unlinkWithKakao() } } diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultPlaceRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultPlaceRepository.kt index 16571c10c..76a77d1b9 100644 --- a/android/app/src/main/java/com/now/naaga/data/repository/DefaultPlaceRepository.kt +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultPlaceRepository.kt @@ -4,68 +4,60 @@ import com.now.domain.model.Coordinate import com.now.domain.model.Place import com.now.domain.repository.PlaceRepository import com.now.naaga.data.mapper.toDomain -import com.now.naaga.data.mapper.toDto -import com.now.naaga.data.remote.dto.PlaceDto -import com.now.naaga.data.remote.retrofit.ServicePool.placeService -import com.now.naaga.data.remote.retrofit.fetchResponse -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json +import com.now.naaga.data.remote.retrofit.service.PlaceService +import com.now.naaga.util.getValueOrThrow import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody +import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import java.io.File -class DefaultPlaceRepository : PlaceRepository { - override fun fetchMyPlaces( +class DefaultPlaceRepository( + private val placeService: PlaceService, +) : PlaceRepository { + override suspend fun fetchMyPlaces( sortBy: String, order: String, - callback: (Result>) -> Unit, - ) { - val call = placeService.getMyPlace(sortBy, order) - call.fetchResponse( - onSuccess = { placeDtos: List -> - callback(Result.success(placeDtos.map { it.toDomain() })) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + ): List { + val response = placeService.getMyPlace(sortBy, order) + return response.getValueOrThrow().map { it.toDomain() } } - override fun fetchPlace(placeId: Long, callback: (Result) -> Unit) { - val call = placeService.getPlace(placeId) - call.fetchResponse( - onSuccess = { placeDto -> - callback(Result.success(placeDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun fetchPlace(placeId: Long): Place { + val response = placeService.getPlace(placeId) + return response.getValueOrThrow().toDomain() } - // TODO : 업로드 기능 구현 시 fetchResponse로 변경해야 함 - override fun postPlace( + override suspend fun postPlace( name: String, description: String, coordinate: Coordinate, image: String, - callback: (Result) -> Unit, - ) { + ): Place { val file = File(image) val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) val imagePart = MultipartBody.Part.createFormData( - "imageFile", + KEY_IMAGE_FILE, file.name, requestFile, ) - val namePart = name.toRequestBody("text/plain".toMediaTypeOrNull()) - val descriptionPart = description.toRequestBody("text/plain".toMediaTypeOrNull()) - val coordinatePart = - Json.encodeToString(coordinate.toDto()).toRequestBody("application/json".toMediaTypeOrNull()) + val postData = HashMap() + postData[KEY_NAME] = name.toRequestBody("text/plain".toMediaTypeOrNull()) + postData[KEY_DESCRIPTION] = description.toRequestBody("text/plain".toMediaTypeOrNull()) + postData[KEY_LATITUDE] = coordinate.latitude.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + postData[KEY_LONGITUDE] = coordinate.longitude.toString().toRequestBody("text/plain".toMediaTypeOrNull()) + + val response = placeService.registerPlace(postData, imagePart) + return response.getValueOrThrow().toDomain() + } - val call = placeService.registerPlace(namePart, descriptionPart, coordinatePart, imagePart) + companion object { + const val KEY_NAME = "name" + const val KEY_DESCRIPTION = "description" + const val KEY_LATITUDE = "latitude" + const val KEY_LONGITUDE = "longitude" + const val KEY_IMAGE_FILE = "imageFile" } } diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultRankRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultRankRepository.kt index 006c6d636..ac447c91b 100644 --- a/android/app/src/main/java/com/now/naaga/data/repository/DefaultRankRepository.kt +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultRankRepository.kt @@ -3,36 +3,22 @@ package com.now.naaga.data.repository import com.now.domain.model.Rank import com.now.domain.repository.RankRepository import com.now.naaga.data.mapper.toDomain -import com.now.naaga.data.remote.dto.RankDto -import com.now.naaga.data.remote.retrofit.ServicePool.rankService -import com.now.naaga.data.remote.retrofit.fetchResponse +import com.now.naaga.data.remote.retrofit.service.RankService +import com.now.naaga.util.getValueOrThrow -class DefaultRankRepository : RankRepository { - override fun getAllRanks( +class DefaultRankRepository( + private val rankService: RankService, +) : RankRepository { + override suspend fun getAllRanks( sortBy: String, order: String, - callback: (Result>) -> Unit, - ) { - val call = rankService.getAllRanks(sortBy, order) - call.fetchResponse( - onSuccess = { rankDtos: List -> - callback(Result.success(rankDtos.map { it.toDomain() })) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + ): List { + val response = rankService.getAllRanks(sortBy, order) + return response.getValueOrThrow().map { it.toDomain() } } - override fun getMyRank(callback: (Result) -> Unit) { - val call = rankService.getMyRank() - call.fetchResponse( - onSuccess = { rankDto -> - callback(Result.success(rankDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) + override suspend fun getMyRank(): Rank { + val response = rankService.getMyRank() + return response.getValueOrThrow().toDomain() } } diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultStatisticsRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultStatisticsRepository.kt index c983f9779..7f1d70332 100644 --- a/android/app/src/main/java/com/now/naaga/data/repository/DefaultStatisticsRepository.kt +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultStatisticsRepository.kt @@ -3,19 +3,14 @@ package com.now.naaga.data.repository import com.now.domain.model.Statistics import com.now.domain.repository.StatisticsRepository import com.now.naaga.data.mapper.toDomain -import com.now.naaga.data.remote.retrofit.ServicePool.statisticsService -import com.now.naaga.data.remote.retrofit.fetchResponse +import com.now.naaga.data.remote.retrofit.service.StatisticsService +import com.now.naaga.util.getValueOrThrow -class DefaultStatisticsRepository : StatisticsRepository { - override fun getMyStatistics(callback: (Result) -> Unit) { - val call = statisticsService.getMyStatistics() - call.fetchResponse( - onSuccess = { statisticsDto -> - callback(Result.success(statisticsDto.toDomain())) - }, - onFailure = { - callback(Result.failure(it)) - }, - ) +class DefaultStatisticsRepository( + private val statisticsService: StatisticsService, +) : StatisticsRepository { + override suspend fun getMyStatistics(): Statistics { + val response = statisticsService.getMyStatistics() + return response.getValueOrThrow().toDomain() } } diff --git a/android/app/src/main/java/com/now/naaga/di/DataSourceModule.kt b/android/app/src/main/java/com/now/naaga/di/DataSourceModule.kt new file mode 100644 index 000000000..4765ecf64 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/di/DataSourceModule.kt @@ -0,0 +1,19 @@ +package com.now.naaga.di + +import android.content.Context +import com.now.naaga.data.local.AuthDataSource +import com.now.naaga.data.local.DefaultAuthDataSource +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class DataSourceModule { + @Singleton + @Provides + fun provideAuthDatasource(@ApplicationContext context: Context): AuthDataSource = DefaultAuthDataSource(context) +} diff --git a/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt b/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt new file mode 100644 index 000000000..e7beb9286 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt @@ -0,0 +1,50 @@ +package com.now.naaga.di + +import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.AuthRepository +import com.now.domain.repository.PlaceRepository +import com.now.domain.repository.RankRepository +import com.now.domain.repository.StatisticsRepository +import com.now.naaga.data.local.AuthDataSource +import com.now.naaga.data.remote.retrofit.service.AdventureService +import com.now.naaga.data.remote.retrofit.service.AuthService +import com.now.naaga.data.remote.retrofit.service.PlaceService +import com.now.naaga.data.remote.retrofit.service.RankService +import com.now.naaga.data.remote.retrofit.service.StatisticsService +import com.now.naaga.data.repository.DefaultAdventureRepository +import com.now.naaga.data.repository.DefaultAuthRepository +import com.now.naaga.data.repository.DefaultPlaceRepository +import com.now.naaga.data.repository.DefaultRankRepository +import com.now.naaga.data.repository.DefaultStatisticsRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class RepositoryModule { + @Singleton + @Provides + fun provideRankRepository(rankService: RankService): RankRepository = DefaultRankRepository(rankService) + + @Singleton + @Provides + fun provideAuthRepository(authDataSource: AuthDataSource, authService: AuthService): AuthRepository = + DefaultAuthRepository(authDataSource, authService) + + @Singleton + @Provides + fun provideAdventureRepository(adventureService: AdventureService): AdventureRepository = + DefaultAdventureRepository(adventureService) + + @Singleton + @Provides + fun providePlaceRepository(placeService: PlaceService): PlaceRepository = DefaultPlaceRepository(placeService) + + @Singleton + @Provides + fun provideStatisticsRepository(statisticsService: StatisticsService): StatisticsRepository = + DefaultStatisticsRepository(statisticsService) +} 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 new file mode 100644 index 000000000..266411eb9 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt @@ -0,0 +1,59 @@ +package com.now.naaga.di + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import com.now.naaga.BuildConfig +import com.now.naaga.data.remote.retrofit.AuthInterceptor +import com.now.naaga.data.remote.retrofit.service.AdventureService +import com.now.naaga.data.remote.retrofit.service.AuthService +import com.now.naaga.data.remote.retrofit.service.PlaceService +import com.now.naaga.data.remote.retrofit.service.RankService +import com.now.naaga.data.remote.retrofit.service.StatisticsService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +class ServiceModule { + private val BASE_URL = BuildConfig.BASE_URL + + @Singleton + @Provides + fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().apply { + addInterceptor(AuthInterceptor()) + }.build() + + @Singleton + @Provides + fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit = Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) + .client(okHttpClient) + .build() + + @Singleton + @Provides + fun provideRankService(retrofit: Retrofit): RankService = retrofit.create(RankService::class.java) + + @Singleton + @Provides + fun provideStatisticsService(retrofit: Retrofit): StatisticsService = retrofit.create(StatisticsService::class.java) + + @Singleton + @Provides + fun provideAdventureService(retrofit: Retrofit): AdventureService = retrofit.create(AdventureService::class.java) + + @Singleton + @Provides + fun providePlaceService(retrofit: Retrofit): PlaceService = retrofit.create(PlaceService::class.java) + + @Singleton + @Provides + fun provideAuthService(retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryActivity.kt index e4e776905..20ed236f9 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryActivity.kt @@ -3,15 +3,17 @@ package com.now.naaga.presentation.adventurehistory import android.content.Context import android.content.Intent import android.os.Bundle +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider import com.now.domain.model.AdventureResult import com.now.naaga.databinding.ActivityAdventureHistoryBinding import com.now.naaga.presentation.adventurehistory.recyclerview.AdventureHistoryAdapter +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class AdventureHistoryActivity : AppCompatActivity() { private lateinit var binding: ActivityAdventureHistoryBinding - private lateinit var viewModel: AdventureHistoryViewModel + private val viewModel: AdventureHistoryViewModel by viewModels() private val historyAdapter = AdventureHistoryAdapter() override fun onCreate(savedInstanceState: Bundle?) { @@ -19,18 +21,12 @@ class AdventureHistoryActivity : AppCompatActivity() { binding = ActivityAdventureHistoryBinding.inflate(layoutInflater) setContentView(binding.root) - initViewModel() initRecyclerView() viewModel.fetchHistories() subscribe() setClickListeners() } - private fun initViewModel() { - viewModel = ViewModelProvider(this, AdventureHistoryViewModel.Factory)[AdventureHistoryViewModel::class.java] - binding.lifecycleOwner = this - } - private fun initRecyclerView() { binding.rvAdventureHistoryVisitedPlaces.apply { adapter = historyAdapter diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryViewModel.kt index 5fceb76c8..8a879729f 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/AdventureHistoryViewModel.kt @@ -3,16 +3,20 @@ package com.now.naaga.presentation.adventurehistory import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.now.domain.model.AdventureResult import com.now.domain.model.OrderType import com.now.domain.model.SortType import com.now.domain.repository.AdventureRepository -import com.now.naaga.data.repository.DefaultAdventureRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.PlayerThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class AdventureHistoryViewModel(private val adventureRepository: AdventureRepository) : ViewModel() { +@HiltViewModel +class AdventureHistoryViewModel @Inject constructor(private val adventureRepository: AdventureRepository) : + ViewModel() { private val _adventureResults = MutableLiveData>() val adventureResults: LiveData> = _adventureResults @@ -20,28 +24,24 @@ class AdventureHistoryViewModel(private val adventureRepository: AdventureReposi val errorMessage: LiveData = _errorMessage fun fetchHistories() { - adventureRepository.fetchMyAdventureResults(SortType.TIME, OrderType.DESCENDING) { result -> - result - .onSuccess { _adventureResults.value = it } - .onFailure { setErrorMessage(it as DataThrowable) } + viewModelScope.launch { + runCatching { + adventureRepository.fetchMyAdventureResults(SortType.TIME, OrderType.DESCENDING) + }.onSuccess { results: List -> + _adventureResults.value = results + }.onFailure { + setErrorMessage(it as DataThrowable) + } } } private fun setErrorMessage(throwable: DataThrowable) { when (throwable) { - is PlayerThrowable -> { _errorMessage.value = throwable.message } - else -> {} - } - } - - companion object { - val Factory = AdventureHistoryFactory(DefaultAdventureRepository()) - - class AdventureHistoryFactory(private val adventureRepository: AdventureRepository) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return AdventureHistoryViewModel(adventureRepository) as T + is PlayerThrowable -> { + _errorMessage.value = throwable.message } + + else -> {} } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryViewHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryViewHolder.kt index 8110faa47..5cc167318 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryViewHolder.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryViewHolder.kt @@ -22,8 +22,8 @@ class AdventureHistoryViewHolder(private val binding: ItemHistoryBinding) : Recy private fun setSuccessOrFailure(resultType: AdventureResultType) { when (resultType) { - AdventureResultType.SUCCESS -> binding.ivAdventureHistoryStamp.setImageResource(R.drawable.ic_success) - AdventureResultType.FAIL -> binding.ivAdventureHistoryStamp.setImageResource(R.drawable.ic_fail) + AdventureResultType.SUCCESS -> binding.ivAdventureHistoryStamp.setImageResource(R.drawable.ic_success_label) + AdventureResultType.FAIL -> binding.ivAdventureHistoryStamp.setImageResource(R.drawable.ic_fail_label) AdventureResultType.NONE -> {} } } 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 258636ac0..05a0c8dd7 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 @@ -4,8 +4,8 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider import com.bumptech.glide.Glide import com.now.domain.model.AdventureResult import com.now.domain.model.AdventureResultType @@ -15,10 +15,12 @@ import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.RESULT_RESULT_RETURN import com.now.naaga.databinding.ActivityAdventureResultBinding +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityAdventureResultBinding - private lateinit var viewModel: AdventureResultViewModel + private val viewModel: AdventureResultViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -42,7 +44,6 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul } private fun initViewModel() { - viewModel = ViewModelProvider(this, AdventureResultViewModel.Factory)[AdventureResultViewModel::class.java] binding.viewModel = viewModel binding.lifecycleOwner = this } @@ -70,12 +71,12 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul } private fun setSuccessTypeView(destinationName: String) { - binding.ivAdventureResultStamp.setImageResource(R.drawable.ic_success) + binding.ivAdventureResultStamp.setImageResource(R.drawable.ic_success_label) binding.tvAdventureResultDestination.text = destinationName } private fun setFailTypeView() { - binding.ivAdventureResultStamp.setImageResource(R.drawable.ic_fail) + binding.ivAdventureResultStamp.setImageResource(R.drawable.ic_fail_label) binding.tvAdventureResultDestination.text = getString(R.string.adventureResult_fail_destination_name) } 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 0b469b41c..dadce03a4 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 @@ -3,16 +3,18 @@ package com.now.naaga.presentation.adventureresult import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.now.domain.model.AdventureResult import com.now.domain.repository.AdventureRepository import com.now.domain.repository.RankRepository -import com.now.naaga.data.repository.DefaultAdventureRepository -import com.now.naaga.data.repository.DefaultRankRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.GameThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class AdventureResultViewModel( +@HiltViewModel +class AdventureResultViewModel @Inject constructor( private val adventureRepository: AdventureRepository, private val rankRepository: RankRepository, ) : ViewModel() { @@ -27,44 +29,36 @@ class AdventureResultViewModel( val throwable: LiveData = _throwable fun fetchGameResult(adventureId: Long) { - adventureRepository.fetchAdventureResult( - adventureId, - callback = { result -> - result - .onSuccess { adventureResult -> _adventureResult.value = adventureResult } - .onFailure { setErrorMessage(it as DataThrowable) } - }, - ) + viewModelScope.launch { + runCatching { + adventureRepository.fetchAdventureResult(adventureId) + }.onSuccess { adventureResult -> + _adventureResult.value = adventureResult + }.onFailure { + setErrorMessage(it as DataThrowable) + } + } } fun fetchMyRank() { - rankRepository.getMyRank( - callback = { result -> - result - .onSuccess { rank -> _myRank.value = rank.rank } - .onFailure { setErrorMessage(it as DataThrowable) } - }, - ) + viewModelScope.launch { + runCatching { + rankRepository.getMyRank() + }.onSuccess { rank -> + _myRank.value = rank.rank + }.onFailure { + setErrorMessage(it as DataThrowable) + } + } } private fun setErrorMessage(throwable: DataThrowable) { when (throwable) { - is GameThrowable -> { _throwable.value = throwable } - else -> {} - } - } - - companion object { - val Factory = AdventureResultFactory(DefaultAdventureRepository(), DefaultRankRepository()) - - class AdventureResultFactory( - private val adventureRepository: AdventureRepository, - private val rankRepository: RankRepository, - ) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return AdventureResultViewModel(adventureRepository, rankRepository) as T + is GameThrowable -> { + _throwable.value = throwable } + + else -> {} } } } 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 6b42cffed..79f211617 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 @@ -10,26 +10,30 @@ import android.provider.Settings import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider import com.now.domain.model.AdventureStatus import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.BEGIN_BEGIN_ADVENTURE import com.now.naaga.data.firebase.analytics.BEGIN_GO_MYPAGE -import com.now.naaga.data.firebase.analytics.BEGIN_GO_RANK +import com.now.naaga.data.firebase.analytics.BEGIN_GO_SETTING import com.now.naaga.data.firebase.analytics.BEGIN_GO_UPLOAD import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityBeginAdventureBinding -import com.now.naaga.presentation.beginadventure.LocationPermissionDialog.Companion.TAG_LOCATION_DIALOG +import com.now.naaga.presentation.common.dialog.DialogType +import com.now.naaga.presentation.common.dialog.PermissionDialog import com.now.naaga.presentation.mypage.MyPageActivity import com.now.naaga.presentation.onadventure.OnAdventureActivity -import com.now.naaga.presentation.rank.RankActivity +import com.now.naaga.presentation.setting.SettingActivity +import com.now.naaga.presentation.upload.UploadActivity +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityBeginAdventureBinding - private lateinit var viewModel: BeginAdventureViewModel + private val viewModel: BeginAdventureViewModel by viewModels() private val onAdventureActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ -> @@ -62,7 +66,6 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default startLoading() registerAnalytics(this.lifecycle) - initViewModel() fetchInProgressAdventure() requestLocationPermission() setClickListeners() @@ -72,10 +75,6 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default binding.lifecycleOwner = this } - private fun initViewModel() { - viewModel = ViewModelProvider(this, BeginAdventureViewModel.Factory)[BeginAdventureViewModel::class.java] - } - private fun fetchInProgressAdventure() { viewModel.fetchAdventure(AdventureStatus.IN_PROGRESS) } @@ -115,28 +114,27 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default } private fun setClickListeners() { - binding.clBeginAdventureBegin.setOnClickListener { + binding.btnBeginAdventureBegin.setOnClickListener { logClickEvent(getViewEntryName(it), BEGIN_BEGIN_ADVENTURE) checkPermissionAndBeginAdventure() } - binding.ivBeginAdventureRank.setOnClickListener { - logClickEvent(getViewEntryName(it), BEGIN_GO_RANK) - startActivity(RankActivity.getIntent(this)) - } - binding.ivBeginAdventureUpload.setOnClickListener { + binding.btnBeginAdventureUpload.setOnClickListener { logClickEvent(getViewEntryName(it), BEGIN_GO_UPLOAD) - Toast.makeText(this, getString(R.string.beginAdventure_features_not_ready), Toast.LENGTH_SHORT).show() -// startActivity(UploadActivity.getIntent(this)) + startActivity(UploadActivity.getIntent(this)) } - binding.ivBeginAdventureMypage.setOnClickListener { + binding.btnBeginAdventureMyPage.setOnClickListener { logClickEvent(getViewEntryName(it), BEGIN_GO_MYPAGE) startActivity(MyPageActivity.getIntent(this)) } + binding.ivBeginAdventureSetting.setOnClickListener { + logClickEvent(getViewEntryName(it), BEGIN_GO_SETTING) + startActivity(SettingActivity.getIntent(this)) + } } private fun checkPermissionAndBeginAdventure() { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { - LocationPermissionDialog().show(supportFragmentManager, TAG_LOCATION_DIALOG) + PermissionDialog(DialogType.LOCATION).show(supportFragmentManager) } else { checkLocationPermissionInStatusBar() } diff --git a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureViewModel.kt index 1a43660f1..a1c45e2d3 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/beginadventure/BeginAdventureViewModel.kt @@ -3,14 +3,17 @@ package com.now.naaga.presentation.beginadventure import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.now.domain.model.Adventure import com.now.domain.model.AdventureStatus import com.now.domain.repository.AdventureRepository -import com.now.naaga.data.repository.DefaultAdventureRepository import com.now.naaga.data.throwable.DataThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class BeginAdventureViewModel(private val adventureRepository: AdventureRepository) : ViewModel() { +@HiltViewModel +class BeginAdventureViewModel @Inject constructor(private val adventureRepository: AdventureRepository) : ViewModel() { private val _adventure = MutableLiveData() val adventure: LiveData = _adventure @@ -22,26 +25,14 @@ class BeginAdventureViewModel(private val adventureRepository: AdventureReposito fun fetchAdventure(adventureStatus: AdventureStatus) { _loading.value = true - adventureRepository.fetchAdventureByStatus(adventureStatus) { result -> - _loading.value = false - result - .onSuccess { - _adventure.value = it.firstOrNull() - } - .onFailure { - _error.value = it as DataThrowable - } - } - } - - companion object { - val Factory = BeginAdventureViewModelFactory(DefaultAdventureRepository()) - - class BeginAdventureViewModelFactory( - private val adventureRepository: AdventureRepository, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return BeginAdventureViewModel(adventureRepository) as T + viewModelScope.launch { + runCatching { + adventureRepository.fetchAdventureByStatus(adventureStatus) + }.onSuccess { + _loading.value = false + _adventure.value = it.firstOrNull() + }.onFailure { + _error.value = it as DataThrowable } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/DialogType.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/DialogType.kt new file mode 100644 index 000000000..5d5bdeb18 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/DialogType.kt @@ -0,0 +1,5 @@ +package com.now.naaga.presentation.common.dialog + +enum class DialogType { + LOCATION, CAMERA +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaagaAlertDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/NaagaAlertDialog.kt similarity index 89% rename from android/app/src/main/java/com/now/naaga/presentation/onadventure/NaagaAlertDialog.kt rename to android/app/src/main/java/com/now/naaga/presentation/common/dialog/NaagaAlertDialog.kt index 1169f4501..a83e31bf1 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaagaAlertDialog.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/NaagaAlertDialog.kt @@ -1,4 +1,4 @@ -package com.now.naaga.presentation.onadventure +package com.now.naaga.presentation.common.dialog import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -20,7 +20,7 @@ class NaagaAlertDialog private constructor() : DialogFragment() { private lateinit var positiveAction: () -> Unit private lateinit var negativeAction: () -> Unit - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = DialogNaagaAlertBinding.inflate(layoutInflater) return binding.root } @@ -62,15 +62,8 @@ class NaagaAlertDialog private constructor() : DialogFragment() { binding.tvAlertDialogNegative.text = negativeText } - fun setPositiveButton(action: () -> Unit) { - positiveAction = action - } - - fun setNegativeButton(action: () -> Unit) { - negativeAction = action - } - class Builder() { + private var isCancelable = true fun build( title: String, @@ -87,8 +80,14 @@ class NaagaAlertDialog private constructor() : DialogFragment() { this.negativeText = negativeText this.positiveAction = positiveAction this.negativeAction = negativeAction + this.isCancelable = this@Builder.isCancelable } } + + fun setCancelable(isCancelable: Boolean): Builder { + this.isCancelable = isCancelable + return this + } } companion object { diff --git a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/LocationPermissionDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt similarity index 53% rename from android/app/src/main/java/com/now/naaga/presentation/beginadventure/LocationPermissionDialog.kt rename to android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt index f6ad86cbc..b541e51d7 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/beginadventure/LocationPermissionDialog.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt @@ -1,8 +1,7 @@ -package com.now.naaga.presentation.beginadventure +package com.now.naaga.presentation.common.dialog -import android.Manifest.permission.ACCESS_COARSE_LOCATION +import android.annotation.SuppressLint import android.content.Intent -import android.content.pm.PackageManager.PERMISSION_GRANTED import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.net.Uri @@ -11,29 +10,53 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat.checkSelfPermission +import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate +import com.now.naaga.data.firebase.analytics.CAMERA_PERMISSION_OPEN_SETTING import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate -import com.now.naaga.data.firebase.analytics.LOCATION_PERMISSION_OPEN_SETTING -import com.now.naaga.databinding.DialogLocationPermissionBinding +import com.now.naaga.databinding.DialogPermissionBinding import com.now.naaga.util.dpToPx import com.now.naaga.util.getWidthProportionalToDevice -class LocationPermissionDialog : DialogFragment(), AnalyticsDelegate by DefaultAnalyticsDelegate() { - private lateinit var binding: DialogLocationPermissionBinding +class PermissionDialog(private val type: DialogType) : + DialogFragment(), + AnalyticsDelegate by DefaultAnalyticsDelegate() { + private lateinit var binding: DialogPermissionBinding + @SuppressLint("UseCompatLoadingForDrawables") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, ): View { - binding = DialogLocationPermissionBinding.inflate(layoutInflater) + binding = DialogPermissionBinding.inflate(layoutInflater) dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + initView() return binding.root } + private fun initView() { + binding.btnDialogPermissionSetting.text = getString(R.string.permissionDialog_setting) + when (type) { + DialogType.LOCATION -> { + binding.ivDialogPermissionIcon.setImageDrawable( + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_location_dialog), + ) + binding.tvDialogPermissionDescription.text = getString(R.string.permissionDialog_location_description) + } + + DialogType.CAMERA -> { + binding.ivDialogPermissionIcon.setImageDrawable( + AppCompatResources.getDrawable(requireContext(), R.drawable.ic_camera_dialog), + ) + binding.tvDialogPermissionDescription.text = getString(R.string.permissionDialog_camera_description) + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) registerAnalytics(this.lifecycle) @@ -41,10 +64,9 @@ class LocationPermissionDialog : DialogFragment(), AnalyticsDelegate by DefaultA override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - checkPermission() setSize() - binding.btnDialogLocationSetting.setOnClickListener { - logClickEvent(requireContext().getViewEntryName(it), LOCATION_PERMISSION_OPEN_SETTING) + binding.btnDialogPermissionSetting.setOnClickListener { + logClickEvent(requireContext().getViewEntryName(it), CAMERA_PERMISSION_OPEN_SETTING) openSetting() dismiss() } @@ -64,27 +86,12 @@ class LocationPermissionDialog : DialogFragment(), AnalyticsDelegate by DefaultA startActivity(appDetailsIntent) } - private fun checkPermission() { - if (checkSelfPermission(requireContext(), ACCESS_COARSE_LOCATION) == PERMISSION_GRANTED) { - setDescription(true) - return - } - setDescription(false) - } - - private fun setDescription(isApproximateAccessGranted: Boolean) { - val description: String = if (isApproximateAccessGranted) { - getString(R.string.locationDialog_approximate_description) - } else { - getString(R.string.locationDialog_description) - } - - binding.tvDialogLocationDescription.text = description + fun show(manager: FragmentManager) { + show(manager, type.name) } companion object { const val WIDTH_RATE = 0.83f const val HEIGHT = 400 - const val TAG_LOCATION_DIALOG = "LOCATION" } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/PolaroidDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PolaroidDialog.kt similarity index 97% rename from android/app/src/main/java/com/now/naaga/presentation/onadventure/PolaroidDialog.kt rename to android/app/src/main/java/com/now/naaga/presentation/common/dialog/PolaroidDialog.kt index c781a384d..2a88ab1ad 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/PolaroidDialog.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PolaroidDialog.kt @@ -1,4 +1,4 @@ -package com.now.naaga.presentation.onadventure +package com.now.naaga.presentation.common.dialog import android.graphics.Color import android.graphics.drawable.ColorDrawable diff --git a/android/app/src/main/java/com/now/naaga/presentation/login/LoginActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/login/LoginActivity.kt index e81fd810d..1e2f9f673 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/login/LoginActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/login/LoginActivity.kt @@ -2,25 +2,25 @@ package com.now.naaga.presentation.login import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK +import android.content.Intent.FLAG_ACTIVITY_NEW_TASK import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.WindowInsetsControllerCompat -import androidx.lifecycle.ViewModelProvider import com.now.domain.model.AuthPlatformType -import com.now.naaga.NaagaApplication -import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.LOGIN_AUTH -import com.now.naaga.data.repository.DefaultAuthRepository import com.now.naaga.databinding.ActivityLoginBinding import com.now.naaga.presentation.beginadventure.BeginAdventureActivity import com.now.naaga.util.loginWithKakao +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class LoginActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityLoginBinding - private lateinit var viewModel: LoginViewModel + private val viewModel: LoginViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -28,15 +28,8 @@ class LoginActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytics binding.lifecycleOwner = this setContentView(binding.root) registerAnalytics(this.lifecycle) - initViewModel() subscribe() setClickListeners() - setStatusBar() - } - - private fun initViewModel() { - val factory = LoginViewModelFactory(DefaultAuthRepository(NaagaApplication.authDataSource)) - viewModel = ViewModelProvider(this, factory)[LoginViewModel::class.java] } private fun subscribe() { @@ -58,13 +51,6 @@ class LoginActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytics } } - private fun setStatusBar() { - window.apply { - statusBarColor = getColor(R.color.white) - WindowInsetsControllerCompat(this, this.decorView).isAppearanceLightStatusBars = true - } - } - private fun navigateHome() { val intent = BeginAdventureActivity.getIntent(this) startActivity(intent) @@ -75,5 +61,12 @@ class LoginActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytics fun getIntent(context: Context): Intent { return Intent(context, LoginActivity::class.java) } + + fun getIntentWithTop(context: Context): Intent { + return Intent(context, LoginActivity::class.java).apply { + addFlags(FLAG_ACTIVITY_NEW_TASK) + addFlags(FLAG_ACTIVITY_CLEAR_TASK) + } + } } } 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 788c29670..0637fb344 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 @@ -3,12 +3,17 @@ package com.now.naaga.presentation.login import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.now.domain.model.AuthPlatformType import com.now.domain.model.PlatformAuth import com.now.domain.repository.AuthRepository import com.now.naaga.data.throwable.DataThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class LoginViewModel( +@HiltViewModel +class LoginViewModel @Inject constructor( private val authRepository: AuthRepository, ) : ViewModel() { private val _isLoginSucceed = MutableLiveData() @@ -18,10 +23,14 @@ class LoginViewModel( val throwable: LiveData = _throwable fun signIn(token: String, platformType: AuthPlatformType) { - authRepository.getToken(PlatformAuth(token, platformType)) { result -> - result - .onSuccess { _isLoginSucceed.value = it } - .onFailure { _throwable.value = it as DataThrowable } + viewModelScope.launch { + runCatching { + authRepository.getToken(PlatformAuth(token, platformType)) + }.onSuccess { status -> + _isLoginSucceed.value = status + }.onFailure { + _throwable.value = it as DataThrowable + } } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModelFactory.kt b/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModelFactory.kt deleted file mode 100644 index 12b5a8f5b..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/login/LoginViewModelFactory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.now.naaga.presentation.login - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.now.domain.repository.AuthRepository - -class LoginViewModelFactory( - private val authRepository: AuthRepository, -) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { - return LoginViewModel(authRepository) as T - } else { - throw IllegalArgumentException() - } - } -} 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 355a02b41..392bff895 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 @@ -4,18 +4,23 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider +import com.now.domain.model.Statistics import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.MYPAGE_GO_RESULTS import com.now.naaga.data.firebase.analytics.MY_PAGE_STATISTICS import com.now.naaga.databinding.ActivityMyPageBinding import com.now.naaga.presentation.adventurehistory.AdventureHistoryActivity +import com.now.naaga.presentation.mypage.statistics.MyPageStatisticsAdapter +import com.now.naaga.presentation.uimodel.model.StatisticsUiModel +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityMyPageBinding - private lateinit var viewModel: MyPageViewModel + private val viewModel: MyPageViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -29,7 +34,6 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } private fun initViewModel() { - viewModel = ViewModelProvider(this, MyPageViewModel.Factory)[MyPageViewModel::class.java] binding.viewModel = viewModel binding.lifecycleOwner = this } @@ -38,7 +42,7 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic binding.ivMypageBack.setOnClickListener { finish() } - binding.btnMypageAdventureResults.setOnClickListener { + binding.btnMypageAdventureHistory.setOnClickListener { logClickEvent(getViewEntryName(it), MYPAGE_GO_RESULTS) val intent = AdventureHistoryActivity.getIntent(this) startActivity(intent) @@ -53,11 +57,11 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private fun subscribe() { viewModel.statistics.observe(this) { statistics -> - binding.customGridMypageStatistics.initContent(statistics.toUiModel(this)) + initRecyclerView(statistics) } viewModel.places.observe(this) { places -> - val placesUiModel = places.map { it.toUiModel() } - binding.customGridMypagePlaces.initContent(placesUiModel) + val placesUiModels = places.map { it.toUiModel() } + binding.customGridMypagePlaces.initContent(placesUiModels) } viewModel.throwable.observe(this) { throwable -> Toast.makeText(this, throwable.message, Toast.LENGTH_SHORT).show() @@ -65,6 +69,16 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } } + private fun initRecyclerView(statistics: Statistics) { + val statisticsUiModels = mutableListOf( + StatisticsUiModel.getSuccessAdventureStatisticsModel(statistics.successCount), + StatisticsUiModel.getFailAdventureStatisticsModel(statistics.failureCount), + StatisticsUiModel.getAllAdventureStatisticsModel(statistics.adventureCount), + ) + val adapter = MyPageStatisticsAdapter(statisticsUiModels) + binding.rvMypageStatistics.adapter = adapter + } + companion object { fun getIntent(context: Context): Intent { return Intent(context, MyPageActivity::class.java) diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageAdapter.kt deleted file mode 100644 index 79672c0c8..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageAdapter.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.now.naaga.presentation.mypage - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -class MyPageAdapter( - private val contents: List, -) : RecyclerView.Adapter() { - - override fun getItemCount(): Int { - return contents.size - } - - override fun getItemViewType(position: Int): Int { - return when (contents[position].viewType) { - MyPageViewType.PLACES -> MyPageViewType.PLACES.ordinal - MyPageViewType.STATISTICS -> MyPageViewType.STATISTICS.ordinal - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - MyPageViewType.PLACES.ordinal -> MyPagePlaceViewHolder(MyPagePlaceViewHolder.getView(parent)) - MyPageViewType.STATISTICS.ordinal -> MyPageStatisticsViewHolder(MyPageStatisticsViewHolder.getView(parent)) - else -> throw IllegalArgumentException() - } - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is MyPagePlaceViewHolder -> holder.bind(contents[position] as MyPagePlaceUiModel) - is MyPageStatisticsViewHolder -> holder.bind(contents[position] as MyPageStatisticsUiModel) - } - } -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageCustomGrid.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageCustomGrid.kt deleted file mode 100644 index 620920495..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageCustomGrid.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.now.naaga.presentation.mypage - -import android.content.Context -import android.util.AttributeSet -import android.view.LayoutInflater -import androidx.constraintlayout.widget.ConstraintLayout -import com.now.naaga.R -import com.now.naaga.databinding.CustomMypageGridBinding -import com.now.naaga.databinding.CustomMypageGridEmptyBinding - -class MyPageCustomGrid(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) { - private lateinit var layoutInflater: LayoutInflater - private lateinit var notEmptybinding: CustomMypageGridBinding - private lateinit var emptyBinding: CustomMypageGridEmptyBinding - private lateinit var adapter: MyPageAdapter - - private fun initNotEmptyView() { - layoutInflater = LayoutInflater.from(context) - notEmptybinding = CustomMypageGridBinding.inflate(layoutInflater, this, true) - } - - private fun initEmptyView() { - layoutInflater = LayoutInflater.from(context) - emptyBinding = CustomMypageGridEmptyBinding.inflate(layoutInflater, this, true) - } - - fun initContent(data: List) { - if (data.isNotEmpty()) { - initNotEmptyView() - makeAdapter(data) - notEmptybinding.tvMypageItemTitle.text = data.first().viewType.text - notEmptybinding.rvMypageItemContent.adapter = adapter - } else { - initEmptyView() - emptyBinding.tvMypageEmptyItemTitle.text = context.getString(R.string.mypage_empty_item_title) - emptyBinding.tvMypageEmptyDescription.text = context.getString(R.string.mypage_empty_description) - } - } - - private fun makeAdapter(data: List) { - adapter = if (data.first().viewType == MyPageViewType.PLACES) { - val lastIndex = if (data.size > END_PLACE_INDEX) END_PLACE_INDEX else data.size - MyPageAdapter(data.subList(START_PLACE_INDEX, lastIndex)) - } else { - MyPageAdapter(data) - } - } - - companion object { - const val START_PLACE_INDEX = 0 - const val END_PLACE_INDEX = 3 - } -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageItemUiModel.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageItemUiModel.kt deleted file mode 100644 index 02f2c1430..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageItemUiModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.now.naaga.presentation.mypage - -abstract class MyPageItemUiModel(val viewType: MyPageViewType) - -data class MyPagePlaceUiModel( - val image: String, - val description: String, -) : MyPageItemUiModel(MyPageViewType.PLACES) - -data class MyPageStatisticsUiModel( - val value: Int, - val unit: String, - val description: String, -) : MyPageItemUiModel(MyPageViewType.STATISTICS) - -enum class MyPageViewType(val text: String) { - STATISTICS("Statistics"), - PLACES("My Places"), -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageMapper.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageMapper.kt index afc20423d..7910a3aeb 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageMapper.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageMapper.kt @@ -1,44 +1,7 @@ package com.now.naaga.presentation.mypage -import android.content.Context import com.now.domain.model.Place -import com.now.domain.model.Statistics -import com.now.naaga.R - -fun Statistics.toUiModel(context: Context): List { - return listOf( - MyPageStatisticsUiModel( - adventureCount, - context.getString(R.string.mypage_count), - context.getString(R.string.mypage_total_adventure), - ), - MyPageStatisticsUiModel( - successCount, - context.getString(R.string.mypage_count), - context.getString(R.string.mypage_adventure_success), - ), - MyPageStatisticsUiModel( - failureCount, - context.getString(R.string.mypage_count), - context.getString(R.string.mypage_adventure_failure), - ), - MyPageStatisticsUiModel( - totalPlayTime, - context.getString(R.string.mypage_minute), - context.getString(R.string.mypage_total_play_time), - ), - MyPageStatisticsUiModel( - totalDistance, - context.getString(R.string.mypage_meter), - context.getString(R.string.mypage_total_adventure_distance), - ), - MyPageStatisticsUiModel( - totalHintUses, - context.getString(R.string.mypage_number), - context.getString(R.string.mypage_total_hint_uses), - ), - ) -} +import com.now.naaga.presentation.uimodel.model.MyPagePlaceUiModel fun Place.toUiModel(): MyPagePlaceUiModel { return MyPagePlaceUiModel( diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewHolder.kt deleted file mode 100644 index 69bf03b26..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/mypage/MyPageViewHolder.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.now.naaga.presentation.mypage - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.now.naaga.databinding.RvMypageItemPlaceBinding -import com.now.naaga.databinding.RvMypageItemStatisticsBinding - -class MyPagePlaceViewHolder(private val binding: RvMypageItemPlaceBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(data: MyPagePlaceUiModel) { - binding.model = data - Glide.with(binding.ivMypageItem).load(data.image).into(binding.ivMypageItem) - binding.ivMypageItem.clipToOutline = true - } - - companion object { - fun getView(parent: ViewGroup): RvMypageItemPlaceBinding { - val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context) - return RvMypageItemPlaceBinding.inflate(layoutInflater, parent, false) - } - } -} - -class MyPageStatisticsViewHolder(private val binding: RvMypageItemStatisticsBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(data: MyPageStatisticsUiModel) { - binding.model = data - - // 숫자가 1자리일 때 36, 2자리일 때 32 ... 이렇게 4단위로 내려가도록 계산함. - // 단 10자리 부터는 0혹은 음수가 나올 수 있음 - val fontSize = (10 - (data.value / 10)) * 4 - binding.tvMypageItemValue.textSize = fontSize.toFloat() - } - - companion object { - fun getView(parent: ViewGroup): RvMypageItemStatisticsBinding { - val layoutInflater: LayoutInflater = LayoutInflater.from(parent.context) - return RvMypageItemStatisticsBinding.inflate(layoutInflater, parent, false) - } - } -} 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 12cabaa4c..c7b8979a9 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 @@ -3,7 +3,7 @@ package com.now.naaga.presentation.mypage import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.now.domain.model.OrderType import com.now.domain.model.Place import com.now.domain.model.Rank @@ -12,14 +12,15 @@ import com.now.domain.model.Statistics import com.now.domain.repository.PlaceRepository import com.now.domain.repository.RankRepository import com.now.domain.repository.StatisticsRepository -import com.now.naaga.data.repository.DefaultPlaceRepository -import com.now.naaga.data.repository.DefaultRankRepository -import com.now.naaga.data.repository.DefaultStatisticsRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.PlaceThrowable import com.now.naaga.data.throwable.DataThrowable.PlayerThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class MyPageViewModel( +@HiltViewModel +class MyPageViewModel @Inject constructor( private val rankRepository: RankRepository, private val statisticsRepository: StatisticsRepository, private val placeRepository: PlaceRepository, @@ -37,48 +38,52 @@ class MyPageViewModel( val throwable: LiveData = _throwable fun fetchRank() { - rankRepository.getMyRank { result: Result -> - result - .onSuccess { rank -> _rank.value = rank } - .onFailure { setErrorMessage(it as DataThrowable) } + viewModelScope.launch { + runCatching { + rankRepository.getMyRank() + }.onSuccess { rank -> + _rank.value = rank + }.onFailure { + setErrorMessage(it as DataThrowable) + } } } fun fetchStatistics() { - statisticsRepository.getMyStatistics { result: Result -> - result - .onSuccess { statistics -> _statistics.value = statistics } - .onFailure { setErrorMessage(it as DataThrowable) } + viewModelScope.launch { + runCatching { + statisticsRepository.getMyStatistics() + }.onSuccess { statistics -> + _statistics.value = statistics + }.onFailure { + setErrorMessage(it as DataThrowable) + } } } fun fetchPlaces() { - placeRepository.fetchMyPlaces(SortType.TIME.name, OrderType.DESCENDING.name) { result -> - result - .onSuccess { places -> _places.value = places } - .onFailure { setErrorMessage(it as DataThrowable) } + viewModelScope.launch { + runCatching { + placeRepository.fetchMyPlaces(SortType.TIME.name, OrderType.DESCENDING.name) + }.onSuccess { places -> + _places.value = places + }.onFailure { + setErrorMessage(it as DataThrowable) + } } } private fun setErrorMessage(throwable: Throwable) { when (throwable) { - is PlayerThrowable -> { _throwable.value = throwable } - is PlaceThrowable -> { _throwable.value = throwable } - else -> {} - } - } - - companion object { - val Factory = MyPageFactory(DefaultRankRepository(), DefaultStatisticsRepository(), DefaultPlaceRepository()) + is PlayerThrowable -> { + _throwable.value = throwable + } - class MyPageFactory( - private val rankRepository: RankRepository, - private val statisticsRepository: StatisticsRepository, - private val placeRepository: PlaceRepository, - ) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return MyPageViewModel(rankRepository, statisticsRepository, placeRepository) as T + is PlaceThrowable -> { + _throwable.value = throwable } + + else -> {} } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPageCustomGrid.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPageCustomGrid.kt new file mode 100644 index 000000000..1b96fbfb6 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPageCustomGrid.kt @@ -0,0 +1,34 @@ +package com.now.naaga.presentation.mypage.place + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.constraintlayout.widget.ConstraintLayout +import com.now.naaga.databinding.CustomMypageGridBinding +import com.now.naaga.databinding.CustomMypageGridEmptyBinding +import com.now.naaga.presentation.uimodel.model.MyPagePlaceUiModel + +class MyPageCustomGrid(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) { + private lateinit var layoutInflater: LayoutInflater + private lateinit var placeBinding: CustomMypageGridBinding + private lateinit var emptyBinding: CustomMypageGridEmptyBinding + + private fun initPlaceBinding() { + layoutInflater = LayoutInflater.from(context) + placeBinding = CustomMypageGridBinding.inflate(layoutInflater, this, true) + } + + private fun initEmptyBinding() { + layoutInflater = LayoutInflater.from(context) + emptyBinding = CustomMypageGridEmptyBinding.inflate(layoutInflater, this, true) + } + + fun initContent(data: List) { + if (data.isNotEmpty()) { + initPlaceBinding() + placeBinding.rvMypageItemContent.adapter = MyPagePlaceAdapter(data) + } else { + initEmptyBinding() + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceAdapter.kt new file mode 100644 index 000000000..1721e3bec --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceAdapter.kt @@ -0,0 +1,22 @@ +package com.now.naaga.presentation.mypage.place + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.presentation.uimodel.model.MyPagePlaceUiModel + +class MyPagePlaceAdapter( + private val contents: List, +) : RecyclerView.Adapter() { + + override fun getItemCount(): Int { + return contents.size + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyPagePlaceViewHolder { + return MyPagePlaceViewHolder(parent) + } + + override fun onBindViewHolder(holder: MyPagePlaceViewHolder, position: Int) { + holder.bind(contents[position]) + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceViewHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceViewHolder.kt new file mode 100644 index 000000000..8a7af3d24 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/place/MyPagePlaceViewHolder.kt @@ -0,0 +1,27 @@ +package com.now.naaga.presentation.mypage.place + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.now.naaga.R +import com.now.naaga.databinding.RvMypageItemPlaceBinding +import com.now.naaga.presentation.uimodel.model.MyPagePlaceUiModel + +class MyPagePlaceViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate( + R.layout.rv_mypage_item_place, + parent, + false, + ), +) { + private val binding = RvMypageItemPlaceBinding.bind(itemView) + + fun bind(data: MyPagePlaceUiModel) { + binding.apply { + model = data + Glide.with(ivMypageItem).load(data.image).into(ivMypageItem) + ivMypageItem.clipToOutline = true + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsAdapter.kt new file mode 100644 index 000000000..b7260a77e --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsAdapter.kt @@ -0,0 +1,20 @@ +package com.now.naaga.presentation.mypage.statistics + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.presentation.uimodel.model.StatisticsUiModel + +class MyPageStatisticsAdapter(private val statisticsUiModels: List) : + RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyPageStatisticsViewHolder { + return MyPageStatisticsViewHolder(parent) + } + + override fun onBindViewHolder(holder: MyPageStatisticsViewHolder, position: Int) { + holder.bind(statisticsUiModels[position]) + } + + override fun getItemCount(): Int { + return statisticsUiModels.size + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsViewHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsViewHolder.kt new file mode 100644 index 000000000..20669b013 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/mypage/statistics/MyPageStatisticsViewHolder.kt @@ -0,0 +1,27 @@ +package com.now.naaga.presentation.mypage.statistics + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.R +import com.now.naaga.databinding.RvMypageItemAdventureBinding +import com.now.naaga.presentation.uimodel.model.StatisticsUiModel + +class MyPageStatisticsViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.rv_mypage_item_adventure, parent, false), +) { + private val binding = RvMypageItemAdventureBinding.bind(itemView) + + fun bind(statisticsUiModels: StatisticsUiModel) { + binding.apply { + ivMypageItemIcon.setImageDrawable( + AppCompatResources.getDrawable(itemView.context, statisticsUiModels.icon), + ) + tvMypageItemAdventureCount.text = statisticsUiModels.count.toString() + tvMypageItemAdventureTitle.text = statisticsUiModels.detail + vMypageItemIconBackground.background = + AppCompatResources.getDrawable(itemView.context, statisticsUiModels.background) + } + } +} 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 94d139bb9..831188b5c 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 @@ -6,10 +6,10 @@ 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 import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider import com.google.android.material.snackbar.Snackbar import com.now.domain.model.Adventure import com.now.domain.model.AdventureStatus @@ -26,17 +26,21 @@ import com.now.naaga.data.firebase.analytics.ON_ADVENTURE_SHOW_POLAROID import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityOnAdventureBinding import com.now.naaga.presentation.adventureresult.AdventureResultActivity +import com.now.naaga.presentation.common.dialog.NaagaAlertDialog +import com.now.naaga.presentation.common.dialog.PolaroidDialog import com.now.naaga.presentation.uimodel.mapper.toDomain import com.now.naaga.presentation.uimodel.mapper.toUi import com.now.naaga.presentation.uimodel.model.AdventureUiModel -import com.now.naaga.util.getParcelableCompat +import com.now.naaga.util.extension.getParcelableCompat +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class OnAdventureActivity : AppCompatActivity(), NaverMapSettingDelegate by DefaultNaverMapSettingDelegate(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityOnAdventureBinding - private lateinit var viewModel: OnAdventureViewModel + private val viewModel: OnAdventureViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { setNaverMap(this, R.id.fcv_onAdventure_map) @@ -69,15 +73,15 @@ class OnAdventureActivity : } private fun setClickListeners() { - binding.ivOnAdventureGiveUp.setOnClickListener { + binding.clOnAdventureStop.setOnClickListener { logClickEvent(getViewEntryName(it), ON_ADVENTURE_SHOW_GIVE_UP) showGiveUpDialog() } - binding.ivOnAdventurePhoto.setOnClickListener { + binding.clOnAdventureShowPhoto.setOnClickListener { logClickEvent(getViewEntryName(it), ON_ADVENTURE_SHOW_POLAROID) showPolaroidDialog() } - binding.ivOnAdventureHint.setOnClickListener { + binding.clOnAdventureSearchDirection.setOnClickListener { logClickEvent(getViewEntryName(it), ON_ADVENTURE_SHOW_HINT) showHintDialog() } @@ -97,12 +101,13 @@ class OnAdventureActivity : viewModel.hints.observe(this) { hints -> drawHintMarkers(hints) binding.lottieOnAdventureLoading.visibility = View.GONE + showPolaroidDialog() } viewModel.lastHint.observe(this) { drawHintMarkers(listOf(it)) } viewModel.remainingHintCount.observe(this) { - binding.tvOnAdventureHintCount.text = it.toString() + // binding.tvOnAdventureHintCount.text = it.toString() } viewModel.error.observe(this) { error: DataThrowable -> logServerError(ON_ADVENTURE_GAME, error.code, error.message.toString()) @@ -124,7 +129,6 @@ class OnAdventureActivity : } private fun initViewModel() { - viewModel = ViewModelProvider(this, OnAdventureViewModel.Factory)[OnAdventureViewModel::class.java] binding.viewModel = viewModel binding.lifecycleOwner = this } 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 66ce642ed..3119d6753 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 @@ -3,8 +3,8 @@ package com.now.naaga.presentation.onadventure import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.map +import androidx.lifecycle.viewModelScope import com.now.domain.model.Adventure import com.now.domain.model.AdventureEndType import com.now.domain.model.AdventureStatus @@ -12,13 +12,16 @@ import com.now.domain.model.Coordinate import com.now.domain.model.Hint import com.now.domain.model.RemainingTryCount import com.now.domain.repository.AdventureRepository -import com.now.naaga.data.repository.DefaultAdventureRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.Companion.hintThrowable import com.now.naaga.data.throwable.DataThrowable.GameThrowable import com.now.naaga.data.throwable.DataThrowable.UniversalThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class OnAdventureViewModel(private val adventureRepository: AdventureRepository) : ViewModel() { +@HiltViewModel +class OnAdventureViewModel @Inject constructor(private val adventureRepository: AdventureRepository) : ViewModel() { private val _adventure = MutableLiveData() val adventure: LiveData = _adventure val hints = DisposableLiveData>(_adventure.map { it.hints }) @@ -41,22 +44,30 @@ class OnAdventureViewModel(private val adventureRepository: AdventureRepository) } fun beginAdventure(currentCoordinate: Coordinate) { - adventureRepository.beginAdventure(currentCoordinate) { result: Result -> - result - .onSuccess { setAdventure(it) } - .onFailure { setError(it as DataThrowable) } + viewModelScope.launch { + runCatching { + adventureRepository.beginAdventure(currentCoordinate) + }.onSuccess { + setAdventure(it) + }.onFailure { + setError(it as DataThrowable) + } } } fun giveUpAdventure() { - adventureRepository.endGame( - adventureId = adventure.value?.id ?: return, - endType = AdventureEndType.GIVE_UP, - coordinate = myCoordinate.value ?: return, - ) { result: Result -> - result - .onSuccess { _adventure.value = adventure.value?.copy(adventureStatus = it) } - .onFailure { setError(it as DataThrowable) } + viewModelScope.launch { + runCatching { + adventureRepository.endGame( + adventureId = adventure.value?.id ?: throw IllegalStateException(ADVENTURE_IS_NULL), + endType = AdventureEndType.GIVE_UP, + coordinate = myCoordinate.value ?: throw IllegalStateException(MY_COORDINATE_IS_NULL), + ) + }.onSuccess { status: AdventureStatus -> + _adventure.value = adventure.value?.copy(adventureStatus = status) + }.onFailure { + setError(it as DataThrowable) + } } } @@ -65,16 +76,18 @@ class OnAdventureViewModel(private val adventureRepository: AdventureRepository) setError(hintThrowable) return } - adventureRepository.makeHint( - adventureId = adventure.value?.id ?: return, - coordinate = myCoordinate.value ?: return, - ) { result: Result -> - result - .onSuccess { hint -> - _adventure.value = adventure.value?.copy(hints = ((adventure.value?.hints ?: listOf()) + hint)) - _lastHint.value = hint - } - .onFailure { setError(it as DataThrowable) } + viewModelScope.launch { + runCatching { + adventureRepository.makeHint( + adventureId = adventure.value?.id ?: throw IllegalStateException(ADVENTURE_IS_NULL), + coordinate = myCoordinate.value ?: throw IllegalStateException(MY_COORDINATE_IS_NULL), + ) + }.onSuccess { hint: Hint -> + _adventure.value = adventure.value?.copy(hints = ((adventure.value?.hints ?: listOf()) + hint)) + _lastHint.value = hint + }.onFailure { + setError(it as DataThrowable) + } } } @@ -88,25 +101,25 @@ class OnAdventureViewModel(private val adventureRepository: AdventureRepository) } fun endAdventure() { - adventureRepository.endGame( - adventureId = adventure.value?.id ?: return, - endType = AdventureEndType.ARRIVED, - coordinate = myCoordinate.value ?: return, - ) { result: Result -> - result - .onSuccess { _adventure.value = adventure.value?.copy(adventureStatus = it) } - .onFailure { - when ((it as DataThrowable).code) { - NOT_ARRIVED -> { - val currentRemainingTryCount = adventure.value?.remainingTryCount ?: return@onFailure - _adventure.value = adventure.value?.copy(remainingTryCount = currentRemainingTryCount - 1) - } - - TRY_COUNT_OVER -> - _adventure.value = adventure.value?.copy(adventureStatus = AdventureStatus.DONE) + viewModelScope.launch { + runCatching { + adventureRepository.endGame( + adventureId = adventure.value?.id ?: throw IllegalStateException(ADVENTURE_IS_NULL), + endType = AdventureEndType.ARRIVED, + coordinate = myCoordinate.value ?: throw IllegalStateException(MY_COORDINATE_IS_NULL), + ) + }.onSuccess { + _adventure.value = adventure.value?.copy(adventureStatus = it) + }.onFailure { + when ((it as DataThrowable).code) { + TRY_COUNT_OVER -> _adventure.value = adventure.value?.copy(adventureStatus = AdventureStatus.DONE) + NOT_ARRIVED -> { + val currentRemainingTryCount = adventure.value?.remainingTryCount ?: return@onFailure + _adventure.value = adventure.value?.copy(remainingTryCount = currentRemainingTryCount - 1) } - setError(it) } + setError(it) + } } } @@ -123,12 +136,8 @@ class OnAdventureViewModel(private val adventureRepository: AdventureRepository) const val NO_DESTINATION = 406 const val NOT_ARRIVED = 415 const val TRY_COUNT_OVER = 416 - val Factory = ViewModelFactory(DefaultAdventureRepository()) - - class ViewModelFactory(private val adventureRepository: AdventureRepository) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return OnAdventureViewModel(adventureRepository) as T - } - } + private const val ERROR_PREFIX = "[ERROR] OnAdventureViewModel:" + private const val ADVENTURE_IS_NULL = "$ERROR_PREFIX adventure가 널입니다." + private const val MY_COORDINATE_IS_NULL = "$ERROR_PREFIX myCoordinate가 널입니다." } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt index b0b1ab43d..ad41103b9 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt @@ -4,18 +4,20 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.ViewModelProvider import com.now.domain.model.Rank import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.RANK_RANK import com.now.naaga.databinding.ActivityRankBinding import com.now.naaga.presentation.rank.recyclerview.RankAdapter +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class RankActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityRankBinding - private lateinit var viewModel: RankViewModel + private val viewModel: RankViewModel by viewModels() private val rankAdapter = RankAdapter() @@ -33,7 +35,6 @@ class RankActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsD } private fun initViewModel() { - viewModel = ViewModelProvider(this, RankViewModel.Factory)[RankViewModel::class.java] binding.viewModel = viewModel binding.lifecycleOwner = this } diff --git a/android/app/src/main/java/com/now/naaga/presentation/rank/RankViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/rank/RankViewModel.kt index 4c739e757..277615c06 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/rank/RankViewModel.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/rank/RankViewModel.kt @@ -3,16 +3,19 @@ package com.now.naaga.presentation.rank import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.now.domain.model.OrderType import com.now.domain.model.Rank import com.now.domain.model.SortType import com.now.domain.repository.RankRepository -import com.now.naaga.data.repository.DefaultRankRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.PlayerThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class RankViewModel(private val rankRepository: RankRepository) : ViewModel() { +@HiltViewModel +class RankViewModel @Inject constructor(private val rankRepository: RankRepository) : ViewModel() { private val _myName = MutableLiveData() val myName: LiveData = _myName @@ -29,29 +32,29 @@ class RankViewModel(private val rankRepository: RankRepository) : ViewModel() { val throwable: LiveData = _throwable fun fetchMyRank() { - rankRepository.getMyRank( - callback = { result -> - result - .onSuccess { rank -> - _myName.value = rank.player.nickname - _myScore.value = rank.player.score - _myRank.value = rank.rank - } - .onFailure { setErrorMessage(it as DataThrowable) } - }, - ) + viewModelScope.launch { + runCatching { + rankRepository.getMyRank() + }.onSuccess { rank -> + _myName.value = rank.player.nickname + _myScore.value = rank.player.score + _myRank.value = rank.rank + }.onFailure { + setErrorMessage(it as DataThrowable) + } + } } fun fetchRanks() { - rankRepository.getAllRanks( - SortType.RANK.name, - OrderType.ASCENDING.name, - callback = { result -> - result - .onSuccess { ranks -> _ranks.value = ranks } - .onFailure { setErrorMessage(it as DataThrowable) } - }, - ) + viewModelScope.launch { + runCatching { + rankRepository.getAllRanks(SortType.RANK.name, OrderType.ASCENDING.name) + }.onSuccess { ranks -> + _ranks.value = ranks + }.onFailure { + setErrorMessage(it as DataThrowable) + } + } } private fun setErrorMessage(throwable: DataThrowable) { @@ -60,15 +63,4 @@ class RankViewModel(private val rankRepository: RankRepository) : ViewModel() { else -> {} } } - - companion object { - val Factory = RankFactory(DefaultRankRepository()) - - class RankFactory(private val rankRepository: RankRepository) : - ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return RankViewModel(rankRepository) as T - } - } - } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/setting/SettingActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/setting/SettingActivity.kt new file mode 100644 index 000000000..e5b4efacb --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/setting/SettingActivity.kt @@ -0,0 +1,120 @@ +package com.now.naaga.presentation.setting + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.widget.Toast +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.now.naaga.R +import com.now.naaga.data.throwable.DataThrowable +import com.now.naaga.databinding.ActivitySettingBinding +import com.now.naaga.presentation.common.dialog.NaagaAlertDialog +import com.now.naaga.presentation.login.LoginActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SettingActivity : AppCompatActivity() { + private lateinit var binding: ActivitySettingBinding + private val viewModel: SettingViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySettingBinding.inflate(layoutInflater) + setContentView(binding.root) + binding.lifecycleOwner = this + subscribe() + setClickListeners() + } + + private fun subscribe() { + viewModel.isLoggedIn.observe(this) { isLoggedIn -> + if (!isLoggedIn) { + shortToast(getString(R.string.setting_logout_message)) + startActivity(LoginActivity.getIntentWithTop(this)) + } + } + viewModel.throwable.observe(this) { error: DataThrowable -> + when (error.code) { + WRONG_AUTH_ERROR_CODE -> shortToast(getString(R.string.setting_wrong_error_message)) + EXPIRATION_AUTH_ERROR_CODE -> shortToast(getString(R.string.setting_expiration_error_message)) + } + } + viewModel.withdrawalStatus.observe(this) { status -> + if (status == true) { + shortToast(getString(R.string.setting_withdrawal_success_message)) + navigateLogin() + } + } + } + + private fun setClickListeners() { + binding.tvSettingLogout.setOnClickListener { + showLogoutDialog() + } + binding.ivSettingBack.setOnClickListener { + finish() + } + binding.tvSettingUnlink.setOnClickListener { + showWithdrawalDialog() + } + binding.tvSettingInquiry.setOnClickListener { + sendEmail() + } + } + + private fun navigateLogin() { + startActivity(LoginActivity.getIntentWithTop(this)) + finish() + } + + private fun shortToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + + private fun showWithdrawalDialog() { + NaagaAlertDialog.Builder().build( + title = getString(R.string.withdrawal_dialog_title), + description = getString(R.string.withdrawal_dialog_description), + positiveText = getString(R.string.withdrawal_dialog_negative), + negativeText = getString(R.string.withdrawal_dialog_positive), + positiveAction = { }, + negativeAction = { viewModel.withdrawalMember() }, + ).show(supportFragmentManager, WITHDRAWAL) + } + + private fun showLogoutDialog() { + NaagaAlertDialog.Builder().build( + title = getString(R.string.logout_dialog_title), + description = getString(R.string.logout_dialog_description), + positiveText = getString(R.string.logout_dialog_positive_text), + negativeText = getString(R.string.logout_dialog_negative_text), + positiveAction = { }, + negativeAction = { viewModel.logout() }, + ).show(supportFragmentManager, LOGOUT) + } + + private fun sendEmail() { + val intent = Intent(Intent.ACTION_SEND) + intent.apply { + type = INTENT_TYPE + val emails = arrayOf(getString(R.string.setting_question_email)) + putExtra(Intent.EXTRA_EMAIL, emails) + putExtra(Intent.EXTRA_SUBJECT, getString(R.string.setting_question_email_title)) + startActivity(this) + } + } + + companion object { + private const val LOGOUT = "LOGOUT" + private const val WITHDRAWAL = "WITHDRAWAL" + private const val INTENT_TYPE = "plain/text" + + private const val WRONG_AUTH_ERROR_CODE = 101 + private const val EXPIRATION_AUTH_ERROR_CODE = 102 + + fun getIntent(context: Context): Intent { + return Intent(context, SettingActivity::class.java) + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/setting/SettingViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/setting/SettingViewModel.kt new file mode 100644 index 000000000..3da61a6b6 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/setting/SettingViewModel.kt @@ -0,0 +1,47 @@ +package com.now.naaga.presentation.setting + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.now.domain.repository.AuthRepository +import com.now.naaga.data.throwable.DataThrowable +import com.now.naaga.data.throwable.DataThrowable.AuthorizationThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingViewModel @Inject constructor(private val authRepository: AuthRepository) : ViewModel() { + private val _isLoggedIn = MutableLiveData(true) + val isLoggedIn: LiveData = _isLoggedIn + + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable + + private val _withdrawalStatus = MutableLiveData() + val withdrawalStatus: LiveData = _withdrawalStatus + + fun logout() { + viewModelScope.launch { + runCatching { authRepository.logout() } + .onSuccess { _isLoggedIn.value = false } + .onFailure { setError(it as DataThrowable) } + } + } + + fun withdrawalMember() { + viewModelScope.launch { + runCatching { authRepository.withdrawalMember() } + .onSuccess { _withdrawalStatus.value = true } + .onFailure { setError(it as DataThrowable) } + } + } + + private fun setError(throwable: DataThrowable) { + when (throwable) { + is AuthorizationThrowable -> _throwable.value = throwable + else -> {} + } + } +} 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 0ee04011b..fee2f0d9c 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 @@ -1,36 +1,105 @@ package com.now.naaga.presentation.splash +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri import android.os.Bundle +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.lifecycle.ViewModelProvider +import com.google.firebase.ktx.Firebase +import com.google.firebase.remoteconfig.ktx.remoteConfig +import com.google.firebase.remoteconfig.ktx.remoteConfigSettings import com.now.naaga.R +import com.now.naaga.data.firebase.analytics.AnalyticsDelegate +import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate +import com.now.naaga.data.firebase.analytics.SPLASH_MY_PAGE_STATISTICS +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.util.extension.getPackageInfoCompat +import dagger.hilt.android.AndroidEntryPoint + +@SuppressLint("CustomSplashScreen") +@AndroidEntryPoint +class SplashActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { + private val viewModel: SplashViewModel by viewModels() -class SplashActivity : AppCompatActivity() { - private lateinit var viewModel: SplashViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - initViewModel() - viewModel.fetchInProgressAdventure() + registerAnalytics(this.lifecycle) + updateCheck() subscribe() - installSplashScreen() setContentView(R.layout.activity_splash) } - private fun initViewModel() { - viewModel = ViewModelProvider(this, SplashViewModel.Factory)[SplashViewModel::class.java] + private fun updateCheck() { + val remoteConfig = Firebase.remoteConfig + val configSettings = remoteConfigSettings { + minimumFetchIntervalInSeconds = 3600 + } + remoteConfig.setConfigSettingsAsync(configSettings) + remoteConfig.setDefaultsAsync(mapOf(MIN_VERSION to DEFAULT_VERSION)) + val curVersion = packageManager.getPackageInfoCompat(packageName).versionName + + remoteConfig.fetchAndActivate().addOnCompleteListener { + if (it.isSuccessful) { + val minVersion = remoteConfig.getString(MIN_VERSION) + if (minVersion > curVersion) { + showUpdateDialog() + } else { + viewModel.testTokenValid() + } + } + } + } + + private fun showUpdateDialog() { + NaagaAlertDialog.Builder() + .setCancelable(false) + .build( + title = getString(R.string.confirm_dialog_title), + description = getString(R.string.confirm_dialog_description), + positiveText = getString(R.string.confirm_dialog_positive_text), + negativeText = getString(R.string.confirm_dialog_negative_text), + positiveAction = ::navigateToPlayStore, + negativeAction = ::finish, + ).show(supportFragmentManager, TAG_CONFIRM_DIALOG) + } + + private fun navigateToPlayStore() { + val uri = PLAY_STORE_URI + packageName + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(uri)) + startActivity(intent) + finish() } private fun subscribe() { - viewModel.adventureStatus.observe(this) { - startNextActivity() + viewModel.isTokenValid.observe(this) { isTokenValid: Boolean -> + if (isTokenValid) { + startBeginAdventureActivity() + return@observe + } + startLoginActivity() + } + viewModel.error.observe(this) { + logServerError(SPLASH_MY_PAGE_STATISTICS, it.code, it.message.toString()) } } - private fun startNextActivity() { - val intent = LoginActivity.getIntent(this) - startActivity(intent) + private fun startBeginAdventureActivity() { + startActivity(BeginAdventureActivity.getIntent(this)) + finish() + } + + private fun startLoginActivity() { + startActivity(LoginActivity.getIntent(this)) finish() } + + companion object { + const val TAG_CONFIRM_DIALOG = "CONFIRM" + const val MIN_VERSION = "version" + const val PLAY_STORE_URI = "market://details?id=" + const val DEFAULT_VERSION = "0.0.0" + } } 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 37d240952..4d57b6570 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 @@ -3,42 +3,31 @@ package com.now.naaga.presentation.splash import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.now.domain.model.Adventure -import com.now.domain.model.AdventureStatus -import com.now.domain.repository.AdventureRepository -import com.now.naaga.data.repository.DefaultAdventureRepository +import androidx.lifecycle.viewModelScope +import com.now.domain.repository.StatisticsRepository +import com.now.naaga.data.throwable.DataThrowable +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class SplashViewModel(private val adventureRepository: AdventureRepository) : ViewModel() { - private val _adventure = MutableLiveData() - val adventure: LiveData = _adventure +@HiltViewModel +class SplashViewModel @Inject constructor(private val statisticsRepository: StatisticsRepository) : ViewModel() { - private val _adventureStatus = MutableLiveData() - val adventureStatus: LiveData = _adventureStatus + private val _isTokenValid = MutableLiveData() + val isTokenValid: LiveData = _isTokenValid - fun fetchInProgressAdventure() { - adventureRepository.fetchAdventureByStatus(AdventureStatus.IN_PROGRESS) { result: Result> -> - result - .onSuccess { fetchAdventure(it) } - .onFailure { _adventureStatus.value = AdventureStatus.NONE } - } - } - - private fun fetchAdventure(adventures: List) { - if (adventures.isNotEmpty()) { - _adventure.value = adventures.first() - _adventureStatus.value = adventures.first().adventureStatus - } else { - _adventureStatus.value = AdventureStatus.NONE - } - } - - companion object { - val Factory = ViewModelFactory(DefaultAdventureRepository()) + private val _error = MutableLiveData() + val error: LiveData = _error - class ViewModelFactory(private val adventureRepository: AdventureRepository) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return SplashViewModel(adventureRepository) as T + fun testTokenValid() { + viewModelScope.launch { + runCatching { + statisticsRepository.getMyStatistics() + }.onSuccess { + _isTokenValid.value = true + }.onFailure { + _isTokenValid.value = false + _error.value = it as DataThrowable.AuthorizationThrowable } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/MyPagePlaceUiModel.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/MyPagePlaceUiModel.kt new file mode 100644 index 000000000..0483c1fdf --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/MyPagePlaceUiModel.kt @@ -0,0 +1,6 @@ +package com.now.naaga.presentation.uimodel.model + +data class MyPagePlaceUiModel( + val image: String, + val name: String, +) diff --git a/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/StatisticsUiModel.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/StatisticsUiModel.kt new file mode 100644 index 000000000..771993aca --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/StatisticsUiModel.kt @@ -0,0 +1,43 @@ +package com.now.naaga.presentation.uimodel.model + +import androidx.annotation.DrawableRes +import com.now.naaga.R + +data class StatisticsUiModel( + @DrawableRes val icon: Int, + @DrawableRes val background: Int, + val detail: String, + val count: Int, +) { + companion object { + fun getSuccessAdventureStatisticsModel(count: Int): StatisticsUiModel { + return StatisticsUiModel( + R.drawable.ic_success_adventure, + R.drawable.oval_orange_gradient, + SUCCESS_DETAIL, + count, + ) + } + + fun getFailAdventureStatisticsModel(count: Int): StatisticsUiModel { + return StatisticsUiModel( + R.drawable.ic_fail_adventure, + R.drawable.oval_yellow_gradient, + FAIL_DETAIL, + count, + ) + } + fun getAllAdventureStatisticsModel(count: Int): StatisticsUiModel { + return StatisticsUiModel( + R.drawable.ic_all_adventure, + R.drawable.oval_blue_gradient, + ALL_DETAIL, + count, + ) + } + + private const val SUCCESS_DETAIL = "성공 모험" + private const val FAIL_DETAIL = "실패 모험" + private const val ALL_DETAIL = "전체 모험" + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/upload/CameraPermissionDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/upload/CameraPermissionDialog.kt deleted file mode 100644 index 9a8c7ba43..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/upload/CameraPermissionDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.now.naaga.presentation.upload - -import android.content.Intent -import android.graphics.Color -import android.graphics.drawable.ColorDrawable -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import com.now.naaga.data.firebase.analytics.AnalyticsDelegate -import com.now.naaga.data.firebase.analytics.CAMERA_PERMISSION_OPEN_SETTING -import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate -import com.now.naaga.databinding.DialogCameraPermissionBinding -import com.now.naaga.presentation.beginadventure.LocationPermissionDialog -import com.now.naaga.util.dpToPx -import com.now.naaga.util.getWidthProportionalToDevice - -class CameraPermissionDialog : DialogFragment(), AnalyticsDelegate by DefaultAnalyticsDelegate() { - private lateinit var binding: DialogCameraPermissionBinding - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle?, - ): View { - binding = DialogCameraPermissionBinding.inflate(layoutInflater) - dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - return binding.root - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - registerAnalytics(this.lifecycle) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setSize() - binding.btnDialogLocationSetting.setOnClickListener { - logClickEvent(requireContext().getViewEntryName(it), CAMERA_PERMISSION_OPEN_SETTING) - openSetting() - dismiss() - } - } - - private fun setSize() { - val dialogWidth = getWidthProportionalToDevice(requireContext(), LocationPermissionDialog.WIDTH_RATE) - val dialogHeight = dpToPx(requireContext(), LocationPermissionDialog.HEIGHT) - dialog?.window?.setLayout(dialogWidth, dialogHeight) - } - - private fun openSetting() { - val appDetailsIntent = Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.parse("package:${requireContext().packageName}"), - ).addCategory(Intent.CATEGORY_DEFAULT) - startActivity(appDetailsIntent) - } - - companion object { - const val TAG_CAMERA_DIALOG = "CAMERA" - } -} 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 69efd99d1..820949ecc 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 @@ -7,30 +7,34 @@ import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap import android.location.Location -import android.location.LocationManager import android.net.Uri import android.os.Bundle import android.provider.MediaStore import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider +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.now.domain.model.Coordinate +import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.UPLOAD_OPEN_CAMERA -import com.now.naaga.data.firebase.analytics.UPLOAD_SET_COORDINATE -import com.now.naaga.data.repository.DefaultPlaceRepository +import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityUploadBinding -import com.now.naaga.presentation.beginadventure.LocationPermissionDialog -import com.now.naaga.presentation.beginadventure.LocationPermissionDialog.Companion.TAG_LOCATION_DIALOG -import com.now.naaga.presentation.upload.CameraPermissionDialog.Companion.TAG_CAMERA_DIALOG +import com.now.naaga.presentation.common.dialog.DialogType +import com.now.naaga.presentation.common.dialog.PermissionDialog +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { private lateinit var binding: ActivityUploadBinding - private lateinit var viewModel: UploadViewModel + private val viewModel: UploadViewModel by viewModels() private val cameraLauncher = registerForActivityResult( ActivityResultContracts.TakePicturePreview(), @@ -48,11 +52,11 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic if (isGranted.not()) { when (entry.key) { Manifest.permission.CAMERA -> { - CameraPermissionDialog().show(supportFragmentManager, TAG_CAMERA_DIALOG) + PermissionDialog(DialogType.CAMERA).show(supportFragmentManager) } Manifest.permission.ACCESS_FINE_LOCATION -> { - LocationPermissionDialog().show(supportFragmentManager, TAG_LOCATION_DIALOG) + PermissionDialog(DialogType.LOCATION).show(supportFragmentManager) } } } @@ -66,6 +70,7 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic setContentView(binding.root) initViewModel() + subscribe() registerAnalytics(this.lifecycle) requestPermission() setCoordinate() @@ -73,12 +78,59 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } private fun initViewModel() { - val repository = DefaultPlaceRepository() - val factory = UploadFactory(application, repository) - viewModel = ViewModelProvider(this, factory)[UploadViewModel::class.java] binding.lifecycleOwner = this binding.viewModel = viewModel - setClickListeners() + } + + private fun subscribe() { + viewModel.coordinate.observe(this) { + binding.lottieUploadLoading.visibility = View.GONE + } + viewModel.successUpload.observe(this) { uploadStatus -> + when (uploadStatus) { + UploadStatus.SUCCESS -> { + changeVisibility(binding.lottieUploadLoading, View.GONE) + shortToast(getString(R.string.upload_success_submit)) + finish() + } + + UploadStatus.PENDING -> { + changeVisibility(binding.lottieUploadLoading, View.VISIBLE) + } + + UploadStatus.FAIL -> { + changeVisibility(binding.lottieUploadLoading, View.GONE) + shortToast(getString(R.string.upload_fail_submit)) + } + } + } + viewModel.throwable.observe(this) { error: DataThrowable -> + when (error.code) { + UploadViewModel.ERROR_STORE_PHOTO -> { + shortToast(getString(R.string.upload_error_store_photo_message)) + } + + UploadViewModel.ALREADY_EXISTS_NEARBY -> { + shortToast(getString(R.string.upload_error_already_exists_nearby_message)) + } + + UploadViewModel.ERROR_POST_BODY -> { + shortToast(getString(R.string.upload_error_post_message)) + } + } + } + } + + private fun changeVisibility(view: View, status: Int) { + when (status) { + View.VISIBLE -> { + view.visibility = status + } + + View.GONE -> { + view.visibility = status + } + } } private fun requestPermission() { @@ -93,13 +145,23 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private fun setCoordinate() { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager - val location = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER) + val fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) + fusedLocationClient.getCurrentLocation(PRIORITY_HIGH_ACCURACY, createCancellationToken()) + .addOnSuccessListener { location -> + location.let { viewModel.setCoordinate(getCoordinate(location)) } + } + .addOnFailureListener { } + } + } - if (location != null) { - val coordinate = getCoordinate(location) - binding.tvUploadPhotoCoordinate.text = coordinate.toText() - viewModel.setCoordinate(coordinate) + private fun createCancellationToken(): CancellationToken { + return object : CancellationToken() { + override fun onCanceledRequested(p0: OnTokenCanceledListener): CancellationToken { + return CancellationTokenSource().token + } + + override fun isCancellationRequested(): Boolean { + return false } } } @@ -120,21 +182,16 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) checkCameraPermission() } - binding.ivUploadPhoto.setOnClickListener { logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) checkCameraPermission() } - binding.ivUploadPhotoCoordinate.setOnClickListener { - logClickEvent(getViewEntryName(it), UPLOAD_SET_COORDINATE) - checkLocationPermission() - } - binding.ivUploadClose.setOnClickListener { + binding.ivUploadBack.setOnClickListener { finish() } binding.btnUploadSubmit.setOnClickListener { if (isFormValid().not()) { - Toast.makeText(this, "모든 정보를 입력해주세요.", Toast.LENGTH_SHORT).show() + shortToast(getString(R.string.upload_error_insufficient_info_message)) } else { viewModel.postPlace() } @@ -143,7 +200,7 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private fun checkCameraPermission() { if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) { - CameraPermissionDialog().show(supportFragmentManager, TAG_CAMERA_DIALOG) + PermissionDialog(DialogType.CAMERA).show(supportFragmentManager) } else { openCamera() } @@ -153,18 +210,10 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic cameraLauncher.launch(null) } - private fun checkLocationPermission() { - if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { - LocationPermissionDialog().show(supportFragmentManager, TAG_LOCATION_DIALOG) - } else { - setCoordinate() - } - } - private fun setImage(bitmap: Bitmap) { binding.ivUploadCameraIcon.visibility = View.GONE binding.ivUploadPhoto.setImageBitmap(bitmap) - val uri = getImageUri(bitmap) ?: Uri.EMPTY + val uri = getAbsolutePathFromUri(getImageUri(bitmap) ?: Uri.EMPTY) ?: "" viewModel.setUri(uri) } @@ -181,28 +230,24 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic return null } - private fun isFormValid(): Boolean { - return (isEmptyPhoto() || isEmptyTitle() || isEmptyCoordinate() || isEmptyDescription()).not() - } - - private fun isEmptyPhoto(): Boolean { - return viewModel.hasUri().not() - } - - private fun isEmptyTitle(): Boolean { - return viewModel.title.value == null - } - - private fun isEmptyCoordinate(): Boolean { - return viewModel.hasCoordinate().not() + private fun getAbsolutePathFromUri(uri: Uri): String? { + val projection = arrayOf(MediaStore.Images.Media.DATA) + val cursor = applicationContext.contentResolver.query(uri, projection, null, null, null) + cursor?.use { + val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) + if (it.moveToFirst()) { + return it.getString(columnIndex) + } + } + return null } - private fun isEmptyDescription(): Boolean { - return viewModel.description.value == null + private fun shortToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } - private fun Coordinate.toText(): String { - return "$latitude, $longitude" + private fun isFormValid(): Boolean { + return viewModel.isFormValid() } companion object { @@ -217,6 +262,8 @@ 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/UploadFactory.kt b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadFactory.kt deleted file mode 100644 index c322500e5..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadFactory.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.now.naaga.presentation.upload - -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.now.domain.repository.PlaceRepository - -class UploadFactory( - private val application: Application, - private val placeRepository: PlaceRepository, -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(UploadViewModel::class.java)) { - return UploadViewModel(application, placeRepository) as T - } else { - throw java.lang.IllegalArgumentException() - } - } -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadStatus.kt b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadStatus.kt new file mode 100644 index 000000000..34f98afd9 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadStatus.kt @@ -0,0 +1,5 @@ +package com.now.naaga.presentation.upload + +enum class UploadStatus { + SUCCESS, PENDING, FAIL +} 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 ae6e63ed6..5f7a460ac 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 @@ -1,81 +1,83 @@ package com.now.naaga.presentation.upload -import android.app.Application -import android.content.Context -import android.net.Uri -import android.provider.MediaStore -import android.text.Editable import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.now.domain.model.Coordinate -import com.now.domain.model.Place import com.now.domain.repository.PlaceRepository +import com.now.naaga.data.throwable.DataThrowable +import com.now.naaga.data.throwable.DataThrowable.PlaceThrowable +import com.now.naaga.data.throwable.DataThrowable.UniversalThrowable +import com.now.naaga.util.MutableSingleLiveData +import com.now.naaga.util.SingleLiveData +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject -class UploadViewModel( - private val application: Application, +@HiltViewModel +class UploadViewModel @Inject constructor( private val placeRepository: PlaceRepository, ) : ViewModel() { - private var imageUri: Uri = Uri.EMPTY - private var coordinate = DEFAULT_COORDINATE + private var imageUri: String = URI_EMPTY - private val _name = MutableLiveData() - val title: LiveData = _name + val name = MutableLiveData() - private val _description = MutableLiveData() - val description: LiveData = _description + private val _successUpload = MutableSingleLiveData() + val successUpload: SingleLiveData = _successUpload - fun setTitle(editTitle: Editable) { - _name.value = editTitle.toString() - } + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable - fun setDescription(editTitle: Editable) { - _description.value = editTitle.toString() - } + private val _coordinate = MutableLiveData() + val coordinate: LiveData = _coordinate - fun setUri(uri: Uri) { + fun setUri(uri: String) { imageUri = uri } fun setCoordinate(coordinate: Coordinate) { - this.coordinate = coordinate - } - - fun hasUri(): Boolean { - return imageUri != Uri.EMPTY + _coordinate.value = coordinate } - fun hasCoordinate(): Boolean { - return coordinate != DEFAULT_COORDINATE + fun isFormValid(): Boolean { + return (imageUri != URI_EMPTY) && (_coordinate.value != null) && (name.value != null) } fun postPlace() { - placeRepository.postPlace( - name = _name.value.toString(), - description = _description.value.toString(), - coordinate = coordinate, - image = getAbsolutePathFromUri(application.applicationContext, imageUri) ?: "", - callback = { result: Result -> - result - .onSuccess { } - .onFailure { } - }, - ) + _coordinate.value?.let { coordinate -> + _successUpload.setValue(UploadStatus.PENDING) + viewModelScope.launch { + runCatching { + placeRepository.postPlace( + name = name.value.toString(), + description = "", + coordinate = coordinate, + image = imageUri, + ) + }.onSuccess { + _successUpload.setValue(UploadStatus.SUCCESS) + }.onFailure { + _successUpload.setValue(UploadStatus.FAIL) + setError(it as DataThrowable) + } + } + } } - private fun getAbsolutePathFromUri(context: Context, uri: Uri): String? { - val projection = arrayOf(MediaStore.Images.Media.DATA) - val cursor = context.contentResolver.query(uri, projection, null, null, null) - cursor?.use { - val columnIndex = it.getColumnIndexOrThrow(MediaStore.Images.Media.DATA) - if (it.moveToFirst()) { - return it.getString(columnIndex) - } + private fun setError(throwable: DataThrowable) { + when (throwable) { + is UniversalThrowable -> _throwable.value = throwable + is PlaceThrowable -> _throwable.value = throwable + else -> {} } - return null } companion object { - val DEFAULT_COORDINATE = Coordinate(-1.0, -1.0) + const val URI_EMPTY = "EMPTY" + + const val ALREADY_EXISTS_NEARBY = 505 + const val ERROR_STORE_PHOTO = 215 + const val ERROR_POST_BODY = 205 } } diff --git a/android/app/src/main/java/com/now/naaga/util/Event.kt b/android/app/src/main/java/com/now/naaga/util/Event.kt new file mode 100644 index 000000000..66459f691 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/Event.kt @@ -0,0 +1,27 @@ +package com.now.naaga.util + +/** + * Used as a wrapper for data that is exposed via a LiveData that represents an event. + */ +open class Event(private val content: T) { + + var hasBeenHandled = false + private set // Allow external read but not write + + /** + * Returns the content and prevents its use again. + */ + fun getContentIfNotHandled(): T? { + return if (hasBeenHandled) { + null + } else { + hasBeenHandled = true + content + } + } + + /** + * Returns the content, even if it's already been handled. + */ + fun peekContent(): T = content +} diff --git a/android/app/src/main/java/com/now/naaga/util/KakaoLoginUtil.kt b/android/app/src/main/java/com/now/naaga/util/KakaoLoginUtil.kt index 20e78b956..e98de10fe 100644 --- a/android/app/src/main/java/com/now/naaga/util/KakaoLoginUtil.kt +++ b/android/app/src/main/java/com/now/naaga/util/KakaoLoginUtil.kt @@ -7,9 +7,11 @@ import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient -private const val KAKAO_LOGIN_LOG_TAG = "kakao login" +const val KAKAO_LOGIN_LOG_TAG = "kakao login" private const val KAKAO_LOGIN_FAIL_MESSAGE = "카카오계정으로 로그인 실패" private const val KAKAO_LOGIN_SUCCESS_MESSAGE = "카카오계정으로 로그인 성공" +private const val KAKAO_UNLINK_FAIL_MESSAGE = "연결 끊기 실패" +private const val KAKAO_UNLINK_SUCCESS_MESSAGE = "연결 끊기 성공. SDK에서 토큰 삭제 됨" private fun getLoginCallback(doNextAction: (token: String) -> Unit): (OAuthToken?, Throwable?) -> Unit { val callback: (OAuthToken?, Throwable?) -> Unit = { token, error -> @@ -50,3 +52,13 @@ fun loginWithKakao(context: Context, doNextAction: (token: String) -> Unit) { UserApiClient.instance.loginWithKakaoAccount(context, callback = callback) } } + +fun unlinkWithKakao() { + UserApiClient.instance.unlink { error -> + if (error != null) { + Log.e(KAKAO_LOGIN_LOG_TAG, KAKAO_UNLINK_FAIL_MESSAGE, error) + } else { + Log.i(KAKAO_LOGIN_LOG_TAG, KAKAO_UNLINK_SUCCESS_MESSAGE) + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt b/android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt new file mode 100644 index 000000000..bb31a719d --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt @@ -0,0 +1,16 @@ +package com.now.naaga.util + +class MutableSingleLiveData : SingleLiveData { + + constructor() : super() + + constructor(value: T) : super(value) + + public override fun postValue(value: T) { + super.postValue(value) + } + + public override fun setValue(value: T) { + super.setValue(value) + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt b/android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt new file mode 100644 index 000000000..2d71bb421 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt @@ -0,0 +1,45 @@ +package com.now.naaga.util + +import com.now.naaga.data.throwable.DataThrowable +import org.json.JSONObject +import retrofit2.Response + +private fun Response.codeIn400s(): Boolean { + return this.code() in 400..499 +} + +private fun Response.codeIn500s(): Boolean { + return this.code() in 500..599 +} + +private fun Response.isDelete(): Boolean { + return this.raw().request.method == "DELETE" +} + +@Suppress("UNCHECKED_CAST") +fun Response.getValueOrThrow(): T { + if (this.isSuccessful) { + if (this.isDelete()) { return Unit as T } + return this.body() ?: throw DataThrowable.IllegalStateThrowable() + } + + if (codeIn500s()) { + throw DataThrowable.IllegalStateThrowable() + } + + if (codeIn400s()) { + val errorResponse = errorBody()?.string() + val jsonObject = errorResponse?.let { JSONObject(it) } + val code = jsonObject?.getInt("code") ?: 0 + val message = jsonObject?.getString("message") ?: "" + + when (code) { + in 100..199 -> { throw DataThrowable.AuthorizationThrowable(code, message) } + in 200..299 -> { throw DataThrowable.UniversalThrowable(code, message) } + in 300..399 -> { throw DataThrowable.PlayerThrowable(code, message) } + in 400..499 -> { throw DataThrowable.GameThrowable(code, message) } + in 500..599 -> { throw DataThrowable.PlaceThrowable(code, message) } + } + } + throw DataThrowable.IllegalStateThrowable() +} diff --git a/android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt b/android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt new file mode 100644 index 000000000..30fb3b8a5 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt @@ -0,0 +1,33 @@ +package com.now.naaga.util + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.MutableLiveData + +abstract class SingleLiveData { + + private val liveData = MutableLiveData>() + + protected constructor() + + protected constructor(value: T) { + liveData.value = Event(value) + } + + protected open fun setValue(value: T) { + liveData.value = Event(value) + } + + protected open fun postValue(value: T) { + liveData.postValue(Event(value)) + } + + fun getValue() = liveData.value?.peekContent() + + fun observe(owner: LifecycleOwner, onResult: (T) -> Unit) { + liveData.observe(owner) { it.getContentIfNotHandled()?.let(onResult) } + } + + fun observePeek(owner: LifecycleOwner, onResult: (T) -> Unit) { + liveData.observe(owner) { onResult(it.peekContent()) } + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/IntentExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/IntentExt.kt similarity index 88% rename from android/app/src/main/java/com/now/naaga/util/IntentExt.kt rename to android/app/src/main/java/com/now/naaga/util/extension/IntentExt.kt index 31ebe4025..3da311f84 100644 --- a/android/app/src/main/java/com/now/naaga/util/IntentExt.kt +++ b/android/app/src/main/java/com/now/naaga/util/extension/IntentExt.kt @@ -1,4 +1,4 @@ -package com.now.naaga.util +package com.now.naaga.util.extension import android.content.Intent import android.os.Build diff --git a/android/app/src/main/java/com/now/naaga/util/extension/PackageManagerExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/PackageManagerExt.kt new file mode 100644 index 000000000..6137b8bd8 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/extension/PackageManagerExt.kt @@ -0,0 +1,14 @@ +package com.now.naaga.util.extension + +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build + +fun PackageManager.getPackageInfoCompat(packageName: String, flags: Int = 0): PackageInfo { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags.toLong())) + } else { + @Suppress("DEPRECATION") + getPackageInfo(packageName, flags) + } +} diff --git a/android/app/src/main/naaga_icon-playstore.png b/android/app/src/main/naaga_icon-playstore.png index 0447b6389..8af0b09a5 100644 Binary files a/android/app/src/main/naaga_icon-playstore.png and b/android/app/src/main/naaga_icon-playstore.png differ diff --git a/android/app/src/main/res/drawable/bg_blue_button.xml b/android/app/src/main/res/drawable/bg_blue_button.xml new file mode 100644 index 000000000..5b0c8246b --- /dev/null +++ b/android/app/src/main/res/drawable/bg_blue_button.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/bg_blue_gradient.xml b/android/app/src/main/res/drawable/bg_blue_gradient.xml new file mode 100644 index 000000000..faa6e80d8 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_blue_gradient.xml @@ -0,0 +1,16 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/bg_yellow_button.xml b/android/app/src/main/res/drawable/bg_yellow_button.xml new file mode 100644 index 000000000..8b17b9fa9 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_yellow_button.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/bg_yellow_button_thick.xml b/android/app/src/main/res/drawable/bg_yellow_button_thick.xml new file mode 100644 index 000000000..1c2fc7b43 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_yellow_button_thick.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_all_adventure.xml b/android/app/src/main/res/drawable/ic_all_adventure.xml new file mode 100644 index 000000000..5566dd1a9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_all_adventure.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_building.xml b/android/app/src/main/res/drawable/ic_building.xml new file mode 100644 index 000000000..d8ded2560 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_building.xml @@ -0,0 +1,11 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_building_full.xml b/android/app/src/main/res/drawable/ic_building_full.xml new file mode 100644 index 000000000..976e5adda --- /dev/null +++ b/android/app/src/main/res/drawable/ic_building_full.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_camera_dialog.xml b/android/app/src/main/res/drawable/ic_camera_dialog.xml index 1898378a7..3a97119e5 100644 --- a/android/app/src/main/res/drawable/ic_camera_dialog.xml +++ b/android/app/src/main/res/drawable/ic_camera_dialog.xml @@ -8,14 +8,14 @@ android:fillColor="#E3C6ED"/> + android:fillColor="#FACB10"/> + android:fillColor="#F2B70C"/> + android:fillColor="#F2B70C"/> diff --git a/android/app/src/main/res/drawable/ic_compass.xml b/android/app/src/main/res/drawable/ic_compass.xml new file mode 100644 index 000000000..f14824ac7 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_compass.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_drop.xml b/android/app/src/main/res/drawable/ic_drop.xml deleted file mode 100644 index ef96c1ffb..000000000 --- a/android/app/src/main/res/drawable/ic_drop.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_fail_adventure.xml b/android/app/src/main/res/drawable/ic_fail_adventure.xml new file mode 100644 index 000000000..054e4be54 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_fail_adventure.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_fail.xml b/android/app/src/main/res/drawable/ic_fail_label.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_fail.xml rename to android/app/src/main/res/drawable/ic_fail_label.xml diff --git a/android/app/src/main/res/drawable/ic_flag.xml b/android/app/src/main/res/drawable/ic_flag.xml new file mode 100644 index 000000000..88383d664 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_flag.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_hint.xml b/android/app/src/main/res/drawable/ic_hint.xml deleted file mode 100644 index 481db8bee..000000000 --- a/android/app/src/main/res/drawable/ic_hint.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..356597e10 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_location_dialog.xml b/android/app/src/main/res/drawable/ic_location_dialog.xml index b56ea58ea..ac4979756 100644 --- a/android/app/src/main/res/drawable/ic_location_dialog.xml +++ b/android/app/src/main/res/drawable/ic_location_dialog.xml @@ -1,15 +1,18 @@ - - - + android:width="144dp" + android:height="144dp" + android:viewportWidth="144" + android:viewportHeight="144"> + + + + diff --git a/android/app/src/main/res/drawable/ic_logo_purple.xml b/android/app/src/main/res/drawable/ic_logo_purple.xml deleted file mode 100644 index a7e2a6f47..000000000 --- a/android/app/src/main/res/drawable/ic_logo_purple.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_marker_east.xml b/android/app/src/main/res/drawable/ic_marker_east.xml index 06cbf22fb..7dc51c1cb 100644 --- a/android/app/src/main/res/drawable/ic_marker_east.xml +++ b/android/app/src/main/res/drawable/ic_marker_east.xml @@ -1,27 +1,18 @@ + android:width="122dp" + android:height="85dp" + android:viewportWidth="122" + android:viewportHeight="85"> - - - - - - - + android:pathData="M16.15,0L105.3,0A16.15,16.15 0,0 1,121.45 16.15L121.45,16.15A16.15,16.15 0,0 1,105.3 32.3L16.15,32.3A16.15,16.15 0,0 1,0 16.15L0,16.15A16.15,16.15 0,0 1,16.15 0z" + android:fillColor="#0B0726"/> + android:pathData="M25.15,15.87H35.2V17.14H25.15V15.87ZM29.38,14.2H30.97V16.36H29.38V14.2ZM26.36,13.69H34.06V14.95H26.36V13.69ZM26.36,11.13H34V12.37H27.94V14.5H26.36V11.13ZM30.14,17.69C32.54,17.69 33.99,18.42 33.99,19.73C33.99,21.04 32.54,21.77 30.14,21.77C27.74,21.77 26.27,21.04 26.27,19.73C26.27,18.42 27.74,17.69 30.14,17.69ZM30.14,18.89C28.65,18.89 27.89,19.17 27.89,19.73C27.89,20.31 28.65,20.56 30.14,20.56C31.63,20.56 32.38,20.31 32.38,19.73C32.38,19.17 31.63,18.89 30.14,18.89ZM38.28,11.7H39.46V12.29C39.46,13.57 38.72,14.8 37.01,15.22L36.36,14.05C37.66,13.75 38.28,12.99 38.28,12.29V11.7ZM38.75,11.7H39.87V12.27C39.87,12.9 40.48,13.62 41.66,13.91L41.19,15.07C39.45,14.67 38.75,13.43 38.75,12.27V11.7ZM42.51,11.7H43.62V12.27C43.62,13.44 42.93,14.68 41.19,15.07L40.72,13.91C41.9,13.62 42.51,12.91 42.51,12.27V11.7ZM42.94,11.7H44.12V12.29C44.12,13.02 44.73,13.77 46.04,14.05L45.38,15.22C43.68,14.8 42.94,13.61 42.94,12.29V11.7ZM36.84,11.03H40.97V12.29H36.84V11.03ZM41.42,11.03H45.53V12.29H41.42V11.03ZM36.17,15.97H46.23V17.23H36.17V15.97ZM40.4,15.07H41.99V16.97H40.4V15.07ZM37.26,18.12H45.02V21.76H43.42V19.38H37.26V18.12ZM51.65,14.74H53.48V16.02H51.65V14.74ZM55.24,10.65H56.76V21.76H55.24V10.65ZM52.97,10.81H54.47V21.25H52.97V10.81ZM49.71,11.43C51.18,11.43 52.13,12.95 52.13,15.46C52.13,17.98 51.18,19.5 49.71,19.5C48.24,19.5 47.31,17.98 47.31,15.46C47.31,12.95 48.24,11.43 49.71,11.43ZM49.71,12.93C49.11,12.93 48.76,13.77 48.76,15.46C48.76,17.16 49.11,17.99 49.71,17.99C50.33,17.99 50.68,17.16 50.68,15.46C50.68,13.77 50.33,12.93 49.71,12.93ZM63.15,17.39H64.35V17.76C64.35,19.44 63.6,20.98 61.86,21.6L61.08,20.44C62.54,19.95 63.15,18.84 63.15,17.76V17.39ZM63.56,17.39H64.7V17.76C64.7,18.84 65.24,19.97 66.59,20.53L65.96,21.71C64.19,21.05 63.56,19.39 63.56,17.76V17.39ZM67.2,17.39H68.34V17.76C68.34,19.3 67.68,21.03 65.96,21.71L65.3,20.53C66.65,19.92 67.2,18.75 67.2,17.76V17.39ZM67.58,17.39H68.78V17.76C68.78,18.91 69.38,19.98 70.84,20.44L70.06,21.6C68.3,21 67.58,19.51 67.58,17.76V17.39ZM68.28,10.66H69.89V16.93H68.28V10.66ZM63.84,11.22C65.57,11.22 66.86,12.33 66.86,13.89C66.86,15.43 65.57,16.54 63.84,16.54C62.12,16.54 60.82,15.43 60.82,13.89C60.82,12.33 62.12,11.22 63.84,11.22ZM63.84,12.54C63,12.54 62.38,13.03 62.38,13.89C62.38,14.73 63,15.22 63.84,15.22C64.68,15.22 65.3,14.73 65.3,13.89C65.3,13.03 64.68,12.54 63.84,12.54ZM74.74,11.41C76.4,11.41 77.61,12.95 77.61,15.37C77.61,17.82 76.4,19.36 74.74,19.36C73.07,19.36 71.87,17.82 71.87,15.37C71.87,12.95 73.07,11.41 74.74,11.41ZM74.74,12.87C73.94,12.87 73.4,13.74 73.4,15.37C73.4,17.03 73.94,17.92 74.74,17.92C75.56,17.92 76.08,17.03 76.08,15.37C76.08,13.74 75.56,12.87 74.74,12.87ZM79.43,10.63H81.02V21.78H79.43V10.63ZM77.14,14.64H80.1V15.93H77.14V14.64ZM84.95,16.29H86.53V19.41H84.95V16.29ZM88.98,16.29H90.58V19.41H88.98V16.29ZM82.74,19.18H92.8V20.47H82.74V19.18ZM87.76,11.19C90.15,11.19 91.91,12.36 91.91,14.17C91.91,15.99 90.15,17.15 87.76,17.15C85.36,17.15 83.61,15.99 83.61,14.17C83.61,12.36 85.36,11.19 87.76,11.19ZM87.76,12.43C86.21,12.43 85.18,13.07 85.18,14.17C85.18,15.27 86.21,15.91 87.76,15.91C89.31,15.91 90.34,15.27 90.34,14.17C90.34,13.07 89.31,12.43 87.76,12.43ZM94.59,18.42L94.19,12.46H95.89L95.5,18.42H94.59ZM94.29,20.69V19.15H95.81V20.69H94.29Z" + android:fillColor="#ffffff"/> + android:pathData="M59.38,56.92L59.06,52.04C59.03,51.68 59.07,51.31 59.16,50.96L59.22,50.72C59.4,50.05 59.82,49.48 60.4,49.12C60.64,48.97 60.9,48.86 61.17,48.79L62.1,48.56C62.39,48.49 62.67,48.38 62.94,48.25L63.39,48.03C64.02,47.72 64.52,47.2 64.8,46.56C64.99,46.12 65.07,45.65 65.04,45.17L65,44.54C64.96,43.95 64.73,43.38 64.35,42.91C63.92,42.39 63.33,42.03 62.67,41.9L62.46,41.85C62.01,41.76 61.54,41.76 61.09,41.85L60.94,41.88C60.13,42.05 59.43,42.51 58.96,43.17C58.63,43.64 58.43,44.19 58.38,44.76L58.3,45.81C58.27,46.12 58.19,46.42 58.05,46.7C57.73,47.33 57.15,47.8 56.45,47.97L56.3,48.01C55.89,48.11 55.46,48.12 55.04,48.03L54.66,47.95C54.3,47.88 53.95,47.73 53.63,47.52C52.97,47.09 52.51,46.41 52.34,45.64L52.29,45.4C52.23,45.08 52.2,44.75 52.23,44.43L52.34,42.96L52.41,42.62C52.6,41.71 52.94,40.84 53.42,40.05C53.74,39.5 54.14,39 54.58,38.54L54.76,38.36C55.19,37.92 55.65,37.53 56.15,37.18L56.17,37.17C56.81,36.72 57.51,36.35 58.24,36.07C58.92,35.81 59.63,35.63 60.35,35.52L60.56,35.49C61.17,35.4 61.79,35.38 62.41,35.43L62.46,35.43C63.21,35.48 63.94,35.63 64.64,35.88C65.19,36.07 65.71,36.32 66.2,36.61L66.65,36.88C67.2,37.21 67.71,37.59 68.19,38.02C68.7,38.48 69.17,39 69.58,39.56L69.63,39.63C69.98,40.1 70.28,40.61 70.52,41.13C70.94,42.02 71.22,42.97 71.33,43.94L71.35,44.04C71.43,44.77 71.44,45.5 71.36,46.23L71.33,46.47C71.25,47.18 71.09,47.88 70.86,48.54C70.57,49.36 70.17,50.15 69.67,50.85C69.29,51.38 68.86,51.87 68.38,52.31L68.22,52.45C67.74,52.89 67.22,53.29 66.66,53.63L65.71,54.23C65.51,54.36 65.4,54.58 65.41,54.82L65.53,56.65C65.57,57.12 65.51,57.58 65.36,58.02L65.3,58.21C65.16,58.62 64.93,59 64.61,59.3C64.27,59.63 63.85,59.86 63.39,59.97L63.18,60.03C62.77,60.13 62.33,60.14 61.91,60.05L61.89,60.05C61.43,59.96 61.01,59.76 60.64,59.48C60.28,59.19 59.98,58.82 59.79,58.39L59.71,58.22C59.52,57.81 59.41,57.37 59.38,56.92Z" + android:fillColor="#0B0726"/> + diff --git a/android/app/src/main/res/drawable/ic_marker_north.xml b/android/app/src/main/res/drawable/ic_marker_north.xml index deb9205a4..3382c191b 100644 --- a/android/app/src/main/res/drawable/ic_marker_north.xml +++ b/android/app/src/main/res/drawable/ic_marker_north.xml @@ -1,27 +1,18 @@ + android:width="122dp" + android:height="85dp" + android:viewportWidth="122" + android:viewportHeight="85"> - - - - - - - + android:pathData="M16.15,0.16L105.3,0.16A16.15,16.15 0,0 1,121.45 16.31L121.45,16.31A16.15,16.15 0,0 1,105.3 32.46L16.15,32.46A16.15,16.15 0,0 1,0 16.31L0,16.31A16.15,16.15 0,0 1,16.15 0.16z" + android:fillColor="#0B0726"/> + android:pathData="M29.35,17.06H30.93V18.74H29.35V17.06ZM25.13,16.1H35.18V17.36H25.13V16.1ZM26.25,18.29H33.97V21.92H32.37V19.55H26.25V18.29ZM26.42,11.08H27.99V12.07H32.32V11.08H33.91V15.38H26.42V11.08ZM27.99,13.25V14.13H32.32V13.25H27.99ZM38.28,11.86H39.46V12.45C39.46,13.73 38.72,14.96 37.01,15.38L36.36,14.21C37.66,13.91 38.28,13.15 38.28,12.45V11.86ZM38.75,11.86H39.87V12.43C39.87,13.06 40.48,13.78 41.66,14.07L41.19,15.23C39.45,14.83 38.75,13.59 38.75,12.43V11.86ZM42.51,11.86H43.62V12.43C43.62,13.6 42.93,14.84 41.19,15.23L40.72,14.07C41.9,13.78 42.51,13.07 42.51,12.43V11.86ZM42.94,11.86H44.12V12.45C44.12,13.18 44.73,13.93 46.04,14.21L45.38,15.38C43.68,14.96 42.94,13.77 42.94,12.45V11.86ZM36.84,11.19H40.97V12.45H36.84V11.19ZM41.42,11.19H45.53V12.45H41.42V11.19ZM36.17,16.13H46.23V17.39H36.17V16.13ZM40.4,15.23H41.99V17.13H40.4V15.23ZM37.26,18.28H45.02V21.92H43.42V19.54H37.26V18.28ZM51.65,14.9H53.48V16.18H51.65V14.9ZM55.24,10.81H56.76V21.92H55.24V10.81ZM52.97,10.97H54.47V21.41H52.97V10.97ZM49.71,11.59C51.18,11.59 52.13,13.11 52.13,15.62C52.13,18.14 51.18,19.66 49.71,19.66C48.24,19.66 47.31,18.14 47.31,15.62C47.31,13.11 48.24,11.59 49.71,11.59ZM49.71,13.09C49.11,13.09 48.76,13.93 48.76,15.62C48.76,17.32 49.11,18.15 49.71,18.15C50.33,18.15 50.68,17.32 50.68,15.62C50.68,13.93 50.33,13.09 49.71,13.09ZM63.15,17.55H64.35V17.92C64.35,19.6 63.6,21.14 61.86,21.76L61.08,20.6C62.54,20.11 63.15,19 63.15,17.92V17.55ZM63.56,17.55H64.7V17.92C64.7,19 65.24,20.13 66.59,20.69L65.96,21.87C64.19,21.21 63.56,19.55 63.56,17.92V17.55ZM67.2,17.55H68.34V17.92C68.34,19.46 67.68,21.19 65.96,21.87L65.3,20.69C66.65,20.08 67.2,18.91 67.2,17.92V17.55ZM67.58,17.55H68.78V17.92C68.78,19.07 69.38,20.14 70.84,20.6L70.06,21.76C68.3,21.16 67.58,19.67 67.58,17.92V17.55ZM68.28,10.82H69.89V17.09H68.28V10.82ZM63.84,11.38C65.57,11.38 66.86,12.49 66.86,14.05C66.86,15.59 65.57,16.7 63.84,16.7C62.12,16.7 60.82,15.59 60.82,14.05C60.82,12.49 62.12,11.38 63.84,11.38ZM63.84,12.7C63,12.7 62.38,13.19 62.38,14.05C62.38,14.89 63,15.38 63.84,15.38C64.68,15.38 65.3,14.89 65.3,14.05C65.3,13.19 64.68,12.7 63.84,12.7ZM74.74,11.57C76.4,11.57 77.61,13.11 77.61,15.53C77.61,17.98 76.4,19.52 74.74,19.52C73.07,19.52 71.87,17.98 71.87,15.53C71.87,13.11 73.07,11.57 74.74,11.57ZM74.74,13.03C73.94,13.03 73.4,13.9 73.4,15.53C73.4,17.19 73.94,18.08 74.74,18.08C75.56,18.08 76.08,17.19 76.08,15.53C76.08,13.9 75.56,13.03 74.74,13.03ZM79.43,10.79H81.02V21.94H79.43V10.79ZM77.14,14.8H80.1V16.09H77.14V14.8ZM84.95,16.45H86.53V19.57H84.95V16.45ZM88.98,16.45H90.58V19.57H88.98V16.45ZM82.74,19.34H92.8V20.63H82.74V19.34ZM87.76,11.35C90.15,11.35 91.91,12.52 91.91,14.33C91.91,16.15 90.15,17.31 87.76,17.31C85.36,17.31 83.61,16.15 83.61,14.33C83.61,12.52 85.36,11.35 87.76,11.35ZM87.76,12.59C86.21,12.59 85.18,13.23 85.18,14.33C85.18,15.43 86.21,16.07 87.76,16.07C89.31,16.07 90.34,15.43 90.34,14.33C90.34,13.23 89.31,12.59 87.76,12.59ZM94.59,18.58L94.19,12.62H95.89L95.5,18.58H94.59ZM94.29,20.85V19.31H95.81V20.85H94.29Z" + android:fillColor="#ffffff"/> + android:pathData="M59.38,57.08L59.06,52.2C59.03,51.84 59.07,51.47 59.16,51.12L59.22,50.88C59.4,50.21 59.82,49.64 60.4,49.28C60.64,49.13 60.9,49.02 61.17,48.95L62.1,48.72C62.39,48.65 62.67,48.54 62.94,48.41L63.39,48.19C64.02,47.88 64.52,47.36 64.8,46.72C64.99,46.28 65.07,45.81 65.04,45.33L65,44.7C64.96,44.11 64.73,43.54 64.35,43.07C63.92,42.55 63.33,42.19 62.67,42.06L62.46,42.01C62.01,41.92 61.54,41.92 61.09,42.01L60.94,42.04C60.13,42.21 59.43,42.67 58.96,43.33C58.63,43.8 58.43,44.35 58.38,44.92L58.3,45.97C58.27,46.28 58.19,46.58 58.05,46.86C57.73,47.5 57.15,47.96 56.45,48.13L56.3,48.17C55.89,48.27 55.46,48.28 55.04,48.19L54.66,48.11C54.3,48.04 53.95,47.89 53.63,47.68C52.97,47.25 52.51,46.57 52.34,45.8L52.29,45.56C52.23,45.24 52.2,44.91 52.23,44.59L52.34,43.12L52.41,42.78C52.6,41.87 52.94,41 53.42,40.21C53.74,39.66 54.14,39.16 54.58,38.7L54.76,38.52C55.19,38.08 55.65,37.69 56.15,37.34L56.17,37.33C56.81,36.88 57.51,36.51 58.24,36.23C58.92,35.97 59.63,35.79 60.35,35.68L60.56,35.65C61.17,35.56 61.79,35.54 62.41,35.59L62.46,35.59C63.21,35.64 63.94,35.79 64.64,36.04C65.19,36.23 65.71,36.48 66.2,36.77L66.65,37.04C67.2,37.37 67.71,37.75 68.19,38.18C68.7,38.64 69.17,39.16 69.58,39.72L69.63,39.79C69.98,40.26 70.28,40.77 70.52,41.29C70.94,42.18 71.22,43.13 71.33,44.1L71.35,44.2C71.43,44.93 71.44,45.66 71.36,46.39L71.33,46.63C71.25,47.34 71.09,48.04 70.86,48.7C70.57,49.52 70.17,50.31 69.67,51.01C69.29,51.54 68.86,52.03 68.38,52.47L68.22,52.61C67.74,53.05 67.22,53.45 66.66,53.79L65.71,54.39C65.51,54.52 65.4,54.74 65.41,54.98L65.53,56.81C65.57,57.28 65.51,57.74 65.36,58.18L65.3,58.37C65.16,58.78 64.93,59.16 64.61,59.46C64.27,59.79 63.85,60.02 63.39,60.13L63.18,60.19C62.77,60.29 62.33,60.3 61.91,60.21L61.89,60.21C61.43,60.12 61.01,59.92 60.64,59.64C60.28,59.35 59.98,58.98 59.79,58.55L59.71,58.38C59.52,57.97 59.41,57.53 59.38,57.08Z" + android:fillColor="#0B0726"/> + diff --git a/android/app/src/main/res/drawable/ic_marker_south.xml b/android/app/src/main/res/drawable/ic_marker_south.xml index 2ab524e65..3fc91584f 100644 --- a/android/app/src/main/res/drawable/ic_marker_south.xml +++ b/android/app/src/main/res/drawable/ic_marker_south.xml @@ -1,27 +1,18 @@ + android:width="122dp" + android:height="85dp" + android:viewportWidth="122" + android:viewportHeight="85"> - - - - - - - + android:pathData="M16.15,0L105.3,0A16.15,16.15 0,0 1,121.45 16.15L121.45,16.15A16.15,16.15 0,0 1,105.3 32.3L16.15,32.3A16.15,16.15 0,0 1,0 16.15L0,16.15A16.15,16.15 0,0 1,16.15 0z" + android:fillColor="#0B0726"/> + android:pathData="M32.29,10.65H33.88V16.91H32.29V10.65ZM33.45,12.99H35.36V14.28H33.45V12.99ZM26.65,17.41H33.88V21.64H26.65V17.41ZM32.32,18.66H28.22V20.38H32.32V18.66ZM25.58,11.17H27.17V15.72H25.58V11.17ZM25.58,15.04H26.51C28.03,15.04 29.67,14.93 31.42,14.59L31.6,15.88C29.79,16.25 28.09,16.35 26.51,16.35H25.58V15.04ZM38.28,11.7H39.46V12.29C39.46,13.57 38.72,14.8 37.01,15.22L36.36,14.05C37.66,13.75 38.28,12.99 38.28,12.29V11.7ZM38.75,11.7H39.87V12.27C39.87,12.9 40.48,13.62 41.66,13.91L41.19,15.07C39.45,14.67 38.75,13.43 38.75,12.27V11.7ZM42.51,11.7H43.62V12.27C43.62,13.44 42.93,14.68 41.19,15.07L40.72,13.91C41.9,13.62 42.51,12.91 42.51,12.27V11.7ZM42.94,11.7H44.12V12.29C44.12,13.02 44.73,13.77 46.04,14.05L45.38,15.22C43.68,14.8 42.94,13.61 42.94,12.29V11.7ZM36.84,11.03H40.97V12.29H36.84V11.03ZM41.42,11.03H45.53V12.29H41.42V11.03ZM36.17,15.97H46.23V17.23H36.17V15.97ZM40.4,15.07H41.99V16.97H40.4V15.07ZM37.26,18.12H45.02V21.76H43.42V19.38H37.26V18.12ZM51.65,14.74H53.48V16.02H51.65V14.74ZM55.24,10.65H56.76V21.76H55.24V10.65ZM52.97,10.81H54.47V21.25H52.97V10.81ZM49.71,11.43C51.18,11.43 52.13,12.95 52.13,15.46C52.13,17.98 51.18,19.5 49.71,19.5C48.24,19.5 47.31,17.98 47.31,15.46C47.31,12.95 48.24,11.43 49.71,11.43ZM49.71,12.93C49.11,12.93 48.76,13.77 48.76,15.46C48.76,17.16 49.11,17.99 49.71,17.99C50.33,17.99 50.68,17.16 50.68,15.46C50.68,13.77 50.33,12.93 49.71,12.93ZM63.15,17.39H64.35V17.76C64.35,19.44 63.6,20.98 61.86,21.6L61.08,20.44C62.54,19.95 63.15,18.84 63.15,17.76V17.39ZM63.56,17.39H64.7V17.76C64.7,18.84 65.24,19.97 66.59,20.53L65.96,21.71C64.19,21.05 63.56,19.39 63.56,17.76V17.39ZM67.2,17.39H68.34V17.76C68.34,19.3 67.68,21.03 65.96,21.71L65.3,20.53C66.65,19.92 67.2,18.75 67.2,17.76V17.39ZM67.58,17.39H68.78V17.76C68.78,18.91 69.38,19.98 70.84,20.44L70.06,21.6C68.3,21 67.58,19.51 67.58,17.76V17.39ZM68.28,10.66H69.89V16.93H68.28V10.66ZM63.84,11.22C65.57,11.22 66.86,12.33 66.86,13.89C66.86,15.43 65.57,16.54 63.84,16.54C62.12,16.54 60.82,15.43 60.82,13.89C60.82,12.33 62.12,11.22 63.84,11.22ZM63.84,12.54C63,12.54 62.38,13.03 62.38,13.89C62.38,14.73 63,15.22 63.84,15.22C64.68,15.22 65.3,14.73 65.3,13.89C65.3,13.03 64.68,12.54 63.84,12.54ZM74.74,11.41C76.4,11.41 77.61,12.95 77.61,15.37C77.61,17.82 76.4,19.36 74.74,19.36C73.07,19.36 71.87,17.82 71.87,15.37C71.87,12.95 73.07,11.41 74.74,11.41ZM74.74,12.87C73.94,12.87 73.4,13.74 73.4,15.37C73.4,17.03 73.94,17.92 74.74,17.92C75.56,17.92 76.08,17.03 76.08,15.37C76.08,13.74 75.56,12.87 74.74,12.87ZM79.43,10.63H81.02V21.78H79.43V10.63ZM77.14,14.64H80.1V15.93H77.14V14.64ZM84.95,16.29H86.53V19.41H84.95V16.29ZM88.98,16.29H90.58V19.41H88.98V16.29ZM82.74,19.18H92.8V20.47H82.74V19.18ZM87.76,11.19C90.15,11.19 91.91,12.36 91.91,14.17C91.91,15.99 90.15,17.15 87.76,17.15C85.36,17.15 83.61,15.99 83.61,14.17C83.61,12.36 85.36,11.19 87.76,11.19ZM87.76,12.43C86.21,12.43 85.18,13.07 85.18,14.17C85.18,15.27 86.21,15.91 87.76,15.91C89.31,15.91 90.34,15.27 90.34,14.17C90.34,13.07 89.31,12.43 87.76,12.43ZM94.59,18.42L94.19,12.46H95.89L95.5,18.42H94.59ZM94.29,20.69V19.15H95.81V20.69H94.29Z" + android:fillColor="#ffffff"/> + android:pathData="M59.38,56.92L59.06,52.04C59.03,51.68 59.07,51.31 59.16,50.96L59.22,50.72C59.4,50.05 59.82,49.48 60.4,49.12C60.64,48.97 60.9,48.86 61.17,48.79L62.1,48.56C62.39,48.49 62.67,48.38 62.94,48.25L63.39,48.03C64.02,47.72 64.52,47.2 64.8,46.56C64.99,46.12 65.07,45.65 65.04,45.17L65,44.54C64.96,43.95 64.73,43.38 64.35,42.91C63.92,42.39 63.33,42.03 62.67,41.9L62.46,41.85C62.01,41.76 61.54,41.76 61.09,41.85L60.94,41.88C60.13,42.05 59.43,42.51 58.96,43.17C58.63,43.64 58.43,44.19 58.38,44.76L58.3,45.81C58.27,46.12 58.19,46.42 58.05,46.7C57.73,47.33 57.15,47.8 56.45,47.97L56.3,48.01C55.89,48.11 55.46,48.12 55.04,48.03L54.66,47.95C54.3,47.88 53.95,47.73 53.63,47.52C52.97,47.09 52.51,46.41 52.34,45.64L52.29,45.4C52.23,45.08 52.2,44.75 52.23,44.43L52.34,42.96L52.41,42.62C52.6,41.71 52.94,40.84 53.42,40.05C53.74,39.5 54.14,39 54.58,38.54L54.76,38.36C55.19,37.92 55.65,37.53 56.15,37.18L56.17,37.17C56.81,36.72 57.51,36.35 58.24,36.07C58.92,35.81 59.63,35.63 60.35,35.52L60.56,35.49C61.17,35.4 61.79,35.38 62.41,35.43L62.46,35.43C63.21,35.48 63.94,35.63 64.64,35.88C65.19,36.07 65.71,36.32 66.2,36.61L66.65,36.88C67.2,37.21 67.71,37.59 68.19,38.02C68.7,38.48 69.17,39 69.58,39.56L69.63,39.63C69.98,40.1 70.28,40.61 70.52,41.13C70.94,42.02 71.22,42.97 71.33,43.94L71.35,44.04C71.43,44.77 71.44,45.5 71.36,46.23L71.33,46.47C71.25,47.18 71.09,47.88 70.86,48.54C70.57,49.36 70.17,50.15 69.67,50.85C69.29,51.38 68.86,51.87 68.38,52.31L68.22,52.45C67.74,52.89 67.22,53.29 66.66,53.63L65.71,54.23C65.51,54.36 65.4,54.58 65.41,54.82L65.53,56.65C65.57,57.12 65.51,57.58 65.36,58.02L65.3,58.21C65.16,58.62 64.93,59 64.61,59.3C64.27,59.63 63.85,59.86 63.39,59.97L63.18,60.03C62.77,60.13 62.33,60.14 61.91,60.05L61.89,60.05C61.43,59.96 61.01,59.76 60.64,59.48C60.28,59.19 59.98,58.82 59.79,58.39L59.71,58.22C59.52,57.81 59.41,57.37 59.38,56.92Z" + android:fillColor="#0B0726"/> + diff --git a/android/app/src/main/res/drawable/ic_marker_west.xml b/android/app/src/main/res/drawable/ic_marker_west.xml index ba35acdc1..e7f9e0364 100644 --- a/android/app/src/main/res/drawable/ic_marker_west.xml +++ b/android/app/src/main/res/drawable/ic_marker_west.xml @@ -1,27 +1,18 @@ + android:width="122dp" + android:height="85dp" + android:viewportWidth="122" + android:viewportHeight="85"> - - - - - - - + android:pathData="M16.15,0.16L105.3,0.16A16.15,16.15 0,0 1,121.45 16.31L121.45,16.31A16.15,16.15 0,0 1,105.3 32.46L16.15,32.46A16.15,16.15 0,0 1,0 16.31L0,16.31A16.15,16.15 0,0 1,16.15 0.16z" + android:fillColor="#0B0726"/> + android:pathData="M30.74,14.27H33.67V15.56H30.74V14.27ZM27.71,11.66H29.01V13.27C29.01,16.06 28.06,18.64 25.97,19.69L24.98,18.4C26.87,17.53 27.71,15.38 27.71,13.27V11.66ZM28.05,11.66H29.32V13.27C29.32,15.38 30.13,17.41 31.96,18.22L31.01,19.49C28.93,18.49 28.05,16.04 28.05,13.27V11.66ZM32.87,10.79H34.46V21.94H32.87V10.79ZM38.28,11.86H39.46V12.45C39.46,13.73 38.72,14.96 37.01,15.38L36.36,14.21C37.66,13.91 38.28,13.15 38.28,12.45V11.86ZM38.75,11.86H39.87V12.43C39.87,13.06 40.48,13.78 41.66,14.07L41.19,15.23C39.45,14.83 38.75,13.59 38.75,12.43V11.86ZM42.51,11.86H43.62V12.43C43.62,13.6 42.93,14.84 41.19,15.23L40.72,14.07C41.9,13.78 42.51,13.07 42.51,12.43V11.86ZM42.94,11.86H44.12V12.45C44.12,13.18 44.73,13.93 46.04,14.21L45.38,15.38C43.68,14.96 42.94,13.77 42.94,12.45V11.86ZM36.84,11.19H40.97V12.45H36.84V11.19ZM41.42,11.19H45.53V12.45H41.42V11.19ZM36.17,16.13H46.23V17.39H36.17V16.13ZM40.4,15.23H41.99V17.13H40.4V15.23ZM37.26,18.28H45.02V21.92H43.42V19.54H37.26V18.28ZM51.65,14.9H53.48V16.18H51.65V14.9ZM55.24,10.81H56.76V21.92H55.24V10.81ZM52.97,10.97H54.47V21.41H52.97V10.97ZM49.71,11.59C51.18,11.59 52.13,13.11 52.13,15.62C52.13,18.14 51.18,19.66 49.71,19.66C48.24,19.66 47.31,18.14 47.31,15.62C47.31,13.11 48.24,11.59 49.71,11.59ZM49.71,13.09C49.11,13.09 48.76,13.93 48.76,15.62C48.76,17.32 49.11,18.15 49.71,18.15C50.33,18.15 50.68,17.32 50.68,15.62C50.68,13.93 50.33,13.09 49.71,13.09ZM63.15,17.55H64.35V17.92C64.35,19.6 63.6,21.14 61.86,21.76L61.08,20.6C62.54,20.11 63.15,19 63.15,17.92V17.55ZM63.56,17.55H64.7V17.92C64.7,19 65.24,20.13 66.59,20.69L65.96,21.87C64.19,21.21 63.56,19.55 63.56,17.92V17.55ZM67.2,17.55H68.34V17.92C68.34,19.46 67.68,21.19 65.96,21.87L65.3,20.69C66.65,20.08 67.2,18.91 67.2,17.92V17.55ZM67.58,17.55H68.78V17.92C68.78,19.07 69.38,20.14 70.84,20.6L70.06,21.76C68.3,21.16 67.58,19.67 67.58,17.92V17.55ZM68.28,10.82H69.89V17.09H68.28V10.82ZM63.84,11.38C65.57,11.38 66.86,12.49 66.86,14.05C66.86,15.59 65.57,16.7 63.84,16.7C62.12,16.7 60.82,15.59 60.82,14.05C60.82,12.49 62.12,11.38 63.84,11.38ZM63.84,12.7C63,12.7 62.38,13.19 62.38,14.05C62.38,14.89 63,15.38 63.84,15.38C64.68,15.38 65.3,14.89 65.3,14.05C65.3,13.19 64.68,12.7 63.84,12.7ZM74.74,11.57C76.4,11.57 77.61,13.11 77.61,15.53C77.61,17.98 76.4,19.52 74.74,19.52C73.07,19.52 71.87,17.98 71.87,15.53C71.87,13.11 73.07,11.57 74.74,11.57ZM74.74,13.03C73.94,13.03 73.4,13.9 73.4,15.53C73.4,17.19 73.94,18.08 74.74,18.08C75.56,18.08 76.08,17.19 76.08,15.53C76.08,13.9 75.56,13.03 74.74,13.03ZM79.43,10.79H81.02V21.94H79.43V10.79ZM77.14,14.8H80.1V16.09H77.14V14.8ZM84.95,16.45H86.53V19.57H84.95V16.45ZM88.98,16.45H90.58V19.57H88.98V16.45ZM82.74,19.34H92.8V20.63H82.74V19.34ZM87.76,11.35C90.15,11.35 91.91,12.52 91.91,14.33C91.91,16.15 90.15,17.31 87.76,17.31C85.36,17.31 83.61,16.15 83.61,14.33C83.61,12.52 85.36,11.35 87.76,11.35ZM87.76,12.59C86.21,12.59 85.18,13.23 85.18,14.33C85.18,15.43 86.21,16.07 87.76,16.07C89.31,16.07 90.34,15.43 90.34,14.33C90.34,13.23 89.31,12.59 87.76,12.59ZM94.59,18.58L94.19,12.62H95.89L95.5,18.58H94.59ZM94.29,20.85V19.31H95.81V20.85H94.29Z" + android:fillColor="#ffffff"/> + android:pathData="M59.38,57.08L59.06,52.2C59.03,51.84 59.07,51.47 59.16,51.12L59.22,50.88C59.4,50.21 59.82,49.64 60.4,49.28C60.64,49.13 60.9,49.02 61.17,48.95L62.1,48.72C62.39,48.65 62.67,48.54 62.94,48.41L63.39,48.19C64.02,47.88 64.52,47.36 64.8,46.72C64.99,46.28 65.07,45.81 65.04,45.33L65,44.7C64.96,44.11 64.73,43.54 64.35,43.07C63.92,42.55 63.33,42.19 62.67,42.06L62.46,42.01C62.01,41.92 61.54,41.92 61.09,42.01L60.94,42.04C60.13,42.21 59.43,42.67 58.96,43.33C58.63,43.8 58.43,44.35 58.38,44.92L58.3,45.97C58.27,46.28 58.19,46.58 58.05,46.86C57.73,47.5 57.15,47.96 56.45,48.13L56.3,48.17C55.89,48.27 55.46,48.28 55.04,48.19L54.66,48.11C54.3,48.04 53.95,47.89 53.63,47.68C52.97,47.25 52.51,46.57 52.34,45.8L52.29,45.56C52.23,45.24 52.2,44.91 52.23,44.59L52.34,43.12L52.41,42.78C52.6,41.87 52.94,41 53.42,40.21C53.74,39.66 54.14,39.16 54.58,38.7L54.76,38.52C55.19,38.08 55.65,37.69 56.15,37.34L56.17,37.33C56.81,36.88 57.51,36.51 58.24,36.23C58.92,35.97 59.63,35.79 60.35,35.68L60.56,35.65C61.17,35.56 61.79,35.54 62.41,35.59L62.46,35.59C63.21,35.64 63.94,35.79 64.64,36.04C65.19,36.23 65.71,36.48 66.2,36.77L66.65,37.04C67.2,37.37 67.71,37.75 68.19,38.18C68.7,38.64 69.17,39.16 69.58,39.72L69.63,39.79C69.98,40.26 70.28,40.77 70.52,41.29C70.94,42.18 71.22,43.13 71.33,44.1L71.35,44.2C71.43,44.93 71.44,45.66 71.36,46.39L71.33,46.63C71.25,47.34 71.09,48.04 70.86,48.7C70.57,49.52 70.17,50.31 69.67,51.01C69.29,51.54 68.86,52.03 68.38,52.47L68.22,52.61C67.74,53.05 67.22,53.45 66.66,53.79L65.71,54.39C65.51,54.52 65.4,54.74 65.41,54.98L65.53,56.81C65.57,57.28 65.51,57.74 65.36,58.18L65.3,58.37C65.16,58.78 64.93,59.16 64.61,59.46C64.27,59.79 63.85,60.02 63.39,60.13L63.18,60.19C62.77,60.29 62.33,60.3 61.91,60.21L61.89,60.21C61.43,60.12 61.01,59.92 60.64,59.64C60.28,59.35 59.98,58.98 59.79,58.55L59.71,58.38C59.52,57.97 59.41,57.53 59.38,57.08Z" + android:fillColor="#0B0726"/> + diff --git a/android/app/src/main/res/drawable/ic_message_received.xml b/android/app/src/main/res/drawable/ic_message_received.xml new file mode 100644 index 000000000..712fe8b99 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_message_received.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_message_send.xml b/android/app/src/main/res/drawable/ic_message_send.xml new file mode 100644 index 000000000..34cbfc6c7 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_message_send.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_mypage.xml b/android/app/src/main/res/drawable/ic_mypage.xml deleted file mode 100644 index 04771b46a..000000000 --- a/android/app/src/main/res/drawable/ic_mypage.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/drawable/ic_naaga_logo.xml b/android/app/src/main/res/drawable/ic_naaga_logo.xml new file mode 100644 index 000000000..e2548c1e3 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_naaga_logo.xml @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_setting.xml b/android/app/src/main/res/drawable/ic_setting.xml new file mode 100644 index 000000000..691f41129 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_setting.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/ic_star.xml b/android/app/src/main/res/drawable/ic_star.xml new file mode 100644 index 000000000..b68ee9a6b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_star.xml @@ -0,0 +1,910 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_stop.xml b/android/app/src/main/res/drawable/ic_stop.xml new file mode 100644 index 000000000..d2745ce60 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_stop.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_success.xml b/android/app/src/main/res/drawable/ic_success.xml deleted file mode 100644 index c606aee66..000000000 --- a/android/app/src/main/res/drawable/ic_success.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/ic_success_adventure.xml b/android/app/src/main/res/drawable/ic_success_adventure.xml new file mode 100644 index 000000000..c16f083ae --- /dev/null +++ b/android/app/src/main/res/drawable/ic_success_adventure.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_success_label.xml b/android/app/src/main/res/drawable/ic_success_label.xml new file mode 100644 index 000000000..a9eb30db8 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_success_label.xml @@ -0,0 +1,6 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_upload.xml b/android/app/src/main/res/drawable/ic_upload.xml deleted file mode 100644 index fefac2583..000000000 --- a/android/app/src/main/res/drawable/ic_upload.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/android/app/src/main/res/drawable/naaga_icon_background.xml b/android/app/src/main/res/drawable/naaga_icon_background.xml new file mode 100644 index 000000000..e009ebe7e --- /dev/null +++ b/android/app/src/main/res/drawable/naaga_icon_background.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/naaga_icon_foreground.xml b/android/app/src/main/res/drawable/naaga_icon_foreground.xml new file mode 100644 index 000000000..d1cc549ee --- /dev/null +++ b/android/app/src/main/res/drawable/naaga_icon_foreground.xml @@ -0,0 +1,733 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/oval_blue_gradient.xml b/android/app/src/main/res/drawable/oval_blue_gradient.xml new file mode 100644 index 000000000..ff3354c75 --- /dev/null +++ b/android/app/src/main/res/drawable/oval_blue_gradient.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/res/drawable/oval_orange_gradient.xml b/android/app/src/main/res/drawable/oval_orange_gradient.xml new file mode 100644 index 000000000..120a3e511 --- /dev/null +++ b/android/app/src/main/res/drawable/oval_orange_gradient.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/res/drawable/oval_yellow_gradient.xml b/android/app/src/main/res/drawable/oval_yellow_gradient.xml new file mode 100644 index 000000000..fa763a897 --- /dev/null +++ b/android/app/src/main/res/drawable/oval_yellow_gradient.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/res/drawable/rect_main_gradient.xml b/android/app/src/main/res/drawable/rect_main_gradient.xml deleted file mode 100644 index 8d1b47b1c..000000000 --- a/android/app/src/main/res/drawable/rect_main_gradient.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/rect_radius_small_white.xml b/android/app/src/main/res/drawable/rect_radius_small_white.xml deleted file mode 100644 index ed6ce6653..000000000 --- a/android/app/src/main/res/drawable/rect_radius_small_white.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/font/main_font.otf b/android/app/src/main/res/font/main_font.otf new file mode 100644 index 000000000..7bf60ffe2 Binary files /dev/null and b/android/app/src/main/res/font/main_font.otf differ diff --git a/android/app/src/main/res/layout/activity_adventure_history.xml b/android/app/src/main/res/layout/activity_adventure_history.xml index dcc468b50..8d5d45c38 100644 --- a/android/app/src/main/res/layout/activity_adventure_history.xml +++ b/android/app/src/main/res/layout/activity_adventure_history.xml @@ -13,33 +13,30 @@ android:id="@+id/iv_adventureHistory_back" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="24dp" - android:layout_marginTop="24dp" + android:layout_marginStart="8dp" + android:layout_marginTop="16dp" android:paddingHorizontal="16dp" android:paddingVertical="12dp" + android:src="@drawable/ic_arrow" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - android:src="@drawable/ic_arrow" /> + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="@+id/iv_adventureHistory_back" /> + app:layout_constraintTop_toTopOf="parent" /> + tools:src="@drawable/ic_success_label" /> @@ -87,9 +84,8 @@ android:id="@+id/tv_adventureResult_timeTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:fontFamily="@font/pretendard_bold" android:text="@string/adventureResult_time_description" - android:textColor="@color/black" + android:textColor="@color/main_gray" android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -99,16 +95,14 @@ android:id="@+id/tv_adventureResult_playTime" android:layout_width="0dp" android:layout_height="wrap_content" - android:text="@{@string/adventureResult_play_time_value(viewModel.adventureResult.playTime)}" - android:fontFamily="@font/pretendard_bold" - android:textColor="@color/white" - android:textSize="20sp" + android:layout_marginStart="100dp" + android:background="@drawable/bg_blue_gradient" android:gravity="end" - android:background="@drawable/rect_radius_small" - android:backgroundTint="@color/main_navy" - android:layout_marginStart="40dp" android:paddingVertical="4dp" android:paddingEnd="@dimen/space_default_medium" + android:text="@{@string/adventureResult_play_time_value(viewModel.adventureResult.playTime)}" + android:textColor="@color/white" + android:textSize="20sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/tv_adventureResult_timeTitle" @@ -132,9 +126,8 @@ - - - - - - - - - - - -