diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..4baf206ff Binary files /dev/null and b/.DS_Store differ diff --git a/.idea/2023-naaga.iml b/.idea/2023-naaga.iml deleted file mode 100644 index d6ebd4805..000000000 --- a/.idea/2023-naaga.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 25553dd5d..000000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 000000000..f5c566453 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +![์ œ๋ชฉ](etc/images/header.png) + +## ๐Ÿ’Œ ๋‚˜์•„๊ฐ€๋กœ๋ถ€ํ„ฐ์˜ ์ดˆ๋Œ€์žฅ์ด ๋„์ฐฉํ–ˆ์Šต๋‹ˆ๋‹ค + +๋ฐ˜๋ณต๋˜๋Š” ์ธ์Šคํƒ€ ํ”ผ๋“œ๋ฅผ ๋ณด๊ณ , ๊ฒŒ์ž„์„ ํ•˜๋ฉฐ ๋ณด๋‚ด๋Š” ์ผ์ƒ์ด ์ง€๋ฃจํ•˜์ง€ ์•Š์œผ์‹œ๋‚˜์š”? +๊ณต๋ถ€์™€ ์ผ์— ์น˜์—ฌ ์‹ค๋‚ด์—์„œ ๋ณด๋‚ด๋Š” ์‹œ๊ฐ„์ด ๋งŽ์„ ํ…๋ฐ์š”. ์ž‘์€ ํ™”๋ฉด ์†์„ ๋ฒ—์–ด๋‚˜ ํ˜„์‹ค ์„ธ๊ณ„์˜ ๊ฒฝํ—˜์„ ํ•ด๋ณด๊ณ  ์‹ถ์ง€ ์•Š์œผ์‹ ๊ฐ€์š”? + +๊ทธ๋Ÿฐ ๋‹น์‹ ์„ โ€˜๋‚˜์•„๊ฐ€โ€™๋กœ ์ดˆ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. + +## ๐Ÿšถ๐Ÿป ์ถ”๋ฆฌ์™€ ๋ฐœ๊ฑธ์Œ์˜ ๋งŒ๋‚จ +๋‚˜์•„๊ฐ€๋Š” ํ˜„์‹ค ์„ธ๊ณ„๋ฅผ ๋ˆ„๋น„๋ฉฐ ์ง„ํ–‰๋˜๋Š” ์ถ”๋ฆฌ ๊ฒŒ์ž„์ž…๋‹ˆ๋‹ค. ๊ฒŒ์ž„์„ ์‹œ์ž‘ํ•˜๋ฉด, ๋‹น์‹  ์ฃผ๋ณ€ ์–ด๋”˜๊ฐ€์˜ ์‚ฌ์ง„์ด ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค. ์‚ฌ์ง„์ด ์•Œ์ญ๋‹ฌ์ญํ•˜์—ฌ ๊ทธ๊ณณ์ด ์–ด๋”˜์ง€ ์•Œ์•„๋งžํžˆ๊ธฐ ์–ด๋ ต๊ฒ ์ง€๋งŒ, ์šฐ์„  ๋ฐœ๊ฑธ์Œ์„ ์˜ฎ๊ฒจ๋ณด์„ธ์š”. + + + + +![์ƒ์„ธ ํŽ˜์ด์ง€](etc/images/service%20intro.png) diff --git a/android/app/build.gradle b/android/app/build.gradle index ebcd1b6e7..69b4b3179 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId "com.now.naaga" minSdk 28 targetSdk 33 - versionCode 9 - versionName "1.1.3" + versionCode 10 + versionName "1.1.4" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "com.now.naaga.HiltTestRunner" @@ -31,8 +31,15 @@ android { } buildTypes { + debug { + minifyEnabled true + firebaseCrashlytics { + mappingFileUploadEnabled false + } + } release { minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' ndk.debugSymbolLevel 'FULL' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f8a7b38c7..798fb8e3a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,14 +2,11 @@ - - - + + 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 34b5d465f..a56b07aa3 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 @@ -6,9 +6,6 @@ const val BEGIN_GO_SETTING = "GO_SETTING" const val BEGIN_GO_UPLOAD = "GO_UPLOAD" const val BEGIN_GO_MYPAGE = "GO_MYPAGE" -// LocationPermissionDialog -const val LOCATION_PERMISSION_OPEN_SETTING = "OPEN_SETTING" - // AdventureResultActivity const val RESULT_RESULT_RETURN = "RESULT_RETURN" @@ -23,7 +20,6 @@ const val ON_ADVENTURE_END_ADVENTURE = "END_ADVENTURE" // UploadActivity const val UPLOAD_OPEN_CAMERA = "OPEN_CAMERA" -const val UPLOAD_SET_COORDINATE = "SET_COORDINATE" // CameraPermissionDialog const val CAMERA_PERMISSION_OPEN_SETTING = "OPEN_SETTING" diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/AdventureResultMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/AdventureResultMapper.kt index f063a240e..91afff7f2 100644 --- a/android/app/src/main/java/com/now/naaga/data/mapper/AdventureResultMapper.kt +++ b/android/app/src/main/java/com/now/naaga/data/mapper/AdventureResultMapper.kt @@ -1,7 +1,7 @@ package com.now.naaga.data.mapper import com.now.domain.model.AdventureResult -import com.now.domain.model.AdventureResultType +import com.now.domain.model.type.AdventureResultType import com.now.naaga.data.remote.dto.AdventureResultDto import java.time.LocalDateTime diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/AuthMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/AuthMapper.kt index 0e0f99559..93206f0aa 100644 --- a/android/app/src/main/java/com/now/naaga/data/mapper/AuthMapper.kt +++ b/android/app/src/main/java/com/now/naaga/data/mapper/AuthMapper.kt @@ -1,7 +1,7 @@ package com.now.naaga.data.mapper -import com.now.domain.model.AuthPlatformType import com.now.domain.model.PlatformAuth +import com.now.domain.model.type.AuthPlatformType import com.now.naaga.data.remote.dto.PlatformAuthDto fun PlatformAuthDto.toDomain(): PlatformAuth { diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/LetterMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/LetterMapper.kt new file mode 100644 index 000000000..2ddb5f8c2 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/mapper/LetterMapper.kt @@ -0,0 +1,14 @@ +package com.now.naaga.data.mapper + +import com.now.domain.model.letter.Letter +import com.now.naaga.data.remote.dto.LetterDto + +fun LetterDto.toDomain(): Letter { + return Letter( + id = id, + player = player.toDomain(), + coordinate = coordinateDto.toDomain(), + message = message, + registerDate = registerDate, + ) +} diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/LetterPreviewMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/LetterPreviewMapper.kt new file mode 100644 index 000000000..6ef7bd289 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/mapper/LetterPreviewMapper.kt @@ -0,0 +1,11 @@ +package com.now.naaga.data.mapper + +import com.now.domain.model.letter.LetterPreview +import com.now.naaga.data.remote.dto.LetterPreviewDto + +fun LetterPreviewDto.toDomain(): LetterPreview { + return LetterPreview( + id = id, + coordinate = coordinateDto.toDomain(), + ) +} 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 c254fa327..f034d49b1 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 @@ -1,8 +1,10 @@ package com.now.naaga.data.mapper import com.now.domain.model.Place +import com.now.domain.model.PreferenceState import com.now.naaga.data.remote.dto.PlaceDto -import com.now.naaga.data.remote.dto.PostPlaceDto +import com.now.naaga.data.remote.dto.PreferenceStateDto +import com.now.naaga.data.remote.dto.post.PostPlaceDto fun Place.toDto(): PlaceDto { return PlaceDto( @@ -33,3 +35,7 @@ fun PostPlaceDto.toDomain(): Place { description = description, ) } + +fun PreferenceStateDto.toPreferenceState(): PreferenceState { + return PreferenceState.valueOf(this.type) +} diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/PlayerMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/PlayerMapper.kt index 9b92f0939..f4690239e 100644 --- a/android/app/src/main/java/com/now/naaga/data/mapper/PlayerMapper.kt +++ b/android/app/src/main/java/com/now/naaga/data/mapper/PlayerMapper.kt @@ -10,3 +10,11 @@ fun PlayerDto.toDomain(): Player { score = totalScore, ) } + +fun Player.toDto(): PlayerDto { + return PlayerDto( + id = id, + nickname = nickname, + totalScore = score, + ) +} diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/AdventureDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/AdventureDto.kt index 34219cc0b..60d62e0c7 100644 --- a/android/app/src/main/java/com/now/naaga/data/remote/dto/AdventureDto.kt +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/AdventureDto.kt @@ -3,7 +3,6 @@ package com.now.naaga.data.remote.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -// AdventureDto๋กœ ๋„ค์ด๋ฐ ์ˆ˜์ •ํ•ด์•ผ๋จ. ์ผ๋‹จ ํ˜„์žฌ AdventureDto๋ฅผ ์ง€์šธ ์ˆ˜ ์—†์–ด ์ด๋ ‡๊ฒŒ ๋‘์—ˆ์Œ @Serializable data class AdventureDto( @SerialName("id") diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterDto.kt new file mode 100644 index 000000000..f8e06f11c --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterDto.kt @@ -0,0 +1,18 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LetterDto( + @SerialName("id") + val id: Long, + @SerialName("player") + val player: PlayerDto, + @SerialName("coordinate") + val coordinateDto: CoordinateDto, + @SerialName("message") + val message: String, + @SerialName("registerDate") + val registerDate: String, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterPreviewDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterPreviewDto.kt new file mode 100644 index 000000000..2f736a822 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/LetterPreviewDto.kt @@ -0,0 +1,12 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class LetterPreviewDto( + @SerialName("id") + val id: Long, + @SerialName("coordinate") + val coordinateDto: CoordinateDto, +) 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/LikeCountDto.kt similarity index 60% rename from android/app/src/main/java/com/now/naaga/data/remote/dto/RefreshTokenDto.kt rename to android/app/src/main/java/com/now/naaga/data/remote/dto/LikeCountDto.kt index 6c6ed2410..99167bd63 100644 --- 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/LikeCountDto.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class RefreshTokenDto( - @SerialName("refreshToken") - val refreshToken: String, +data class LikeCountDto( + @SerialName("placeLikeCount") + val placeLikeCount: Int, ) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceDto.kt new file mode 100644 index 000000000..8ab0ed7b3 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceDto.kt @@ -0,0 +1,16 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PreferenceDto( + @SerialName("id") + val id: Int, + @SerialName("playerId") + val playerId: Int, + @SerialName("placeId") + val placeId: Int, + @SerialName("type") + val type: String, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceStateDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceStateDto.kt new file mode 100644 index 000000000..14fb7971c --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/PreferenceStateDto.kt @@ -0,0 +1,10 @@ +package com.now.naaga.data.remote.dto + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PreferenceStateDto( + @SerialName("type") + val type: String, +) diff --git a/android/app/src/main/java/com/now/naaga/data/remote/dto/post/PostLetterDto.kt b/android/app/src/main/java/com/now/naaga/data/remote/dto/post/PostLetterDto.kt new file mode 100644 index 000000000..6e92e4342 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/dto/post/PostLetterDto.kt @@ -0,0 +1,14 @@ +package com.now.naaga.data.remote.dto.post + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class PostLetterDto( + @SerialName("message") + val message: String, + @SerialName("latitude") + val latitude: Double, + @SerialName("longitude") + val longitude: Double, +) 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/post/PostPlaceDto.kt similarity index 83% rename from android/app/src/main/java/com/now/naaga/data/remote/dto/PostPlaceDto.kt rename to android/app/src/main/java/com/now/naaga/data/remote/dto/post/PostPlaceDto.kt index 149455438..79edacff0 100644 --- 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/post/PostPlaceDto.kt @@ -1,5 +1,6 @@ -package com.now.naaga.data.remote.dto +package com.now.naaga.data.remote.dto.post +import com.now.naaga.data.remote.dto.CoordinateDto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable 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 deleted file mode 100644 index c798eeef8..000000000 --- a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/RetrofitFactory.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.now.naaga.data.remote.retrofit - -import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import com.now.naaga.BuildConfig -import kotlinx.serialization.json.Json -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit - -object RetrofitFactory { - private const val BASE_URL = BuildConfig.BASE_URL - - val retrofit: Retrofit = Retrofit.Builder() - .baseUrl(BASE_URL) - .addConverterFactory(Json.asConverterFactory("application/json".toMediaType())) - .client(createOkHttpClient()) - .build() - - private fun createOkHttpClient(): OkHttpClient { - return OkHttpClient.Builder().apply { - addInterceptor(AuthInterceptor()) - addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }) - }.build() - } -} diff --git a/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/LetterService.kt b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/LetterService.kt new file mode 100644 index 000000000..3221ed3c0 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/remote/retrofit/service/LetterService.kt @@ -0,0 +1,35 @@ +package com.now.naaga.data.remote.retrofit.service + +import com.now.naaga.data.remote.dto.LetterDto +import com.now.naaga.data.remote.dto.LetterPreviewDto +import com.now.naaga.data.remote.dto.post.PostLetterDto +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.Path +import retrofit2.http.Query + +interface LetterService { + @POST("/letters") + suspend fun registerLetter( + @Body postLetterDto: PostLetterDto, + ): Response + + @GET("/letters/nearby") + suspend fun getNearbyLetters( + @Query("latitude") latitude: Double, + @Query("longitude") longitude: Double, + ): Response> + + @GET("/letters/{letterId}") + suspend fun getLetter( + @Path("letterId") letterId: Long, + ): Response + + @GET("/letterlogs") + suspend fun getInGameLetters( + @Query("gameId") gameId: Long, + @Query("logType") logType: String, + ): 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 0fb325c3b..48b0bbfd0 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,10 +1,15 @@ package com.now.naaga.data.remote.retrofit.service +import com.now.naaga.data.remote.dto.LikeCountDto import com.now.naaga.data.remote.dto.PlaceDto -import com.now.naaga.data.remote.dto.PostPlaceDto +import com.now.naaga.data.remote.dto.PreferenceDto +import com.now.naaga.data.remote.dto.PreferenceStateDto +import com.now.naaga.data.remote.dto.post.PostPlaceDto import okhttp3.MultipartBody import okhttp3.RequestBody import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.Multipart import retrofit2.http.POST @@ -31,4 +36,25 @@ interface PlaceService { @PartMap postData: HashMap, @Part imageFile: MultipartBody.Part, ): Response + + @GET("/places/{placeId}/likes/count") + suspend fun getLikeCount( + @Path("placeId") placedId: Int, + ): Response + + @DELETE("/places/{placeId}/likes/my") + suspend fun deletePreference( + @Path("placeId") placedId: Int, + ): Response + + @POST("/places/{placeId}/likes") + suspend fun postPreference( + @Path("placeId") placedId: Int, + @Body body: PreferenceStateDto, + ): Response + + @GET("/places/{placeId}/likes/my") + suspend fun getMyPreference( + @Path("placeId") placedId: Int, + ): 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 ff0815e2e..b9358ae7e 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 @@ -1,19 +1,19 @@ package com.now.naaga.data.repository import com.now.domain.model.Adventure -import com.now.domain.model.AdventureEndType import com.now.domain.model.AdventureResult import com.now.domain.model.AdventureStatus import com.now.domain.model.Coordinate import com.now.domain.model.Hint -import com.now.domain.model.OrderType -import com.now.domain.model.SortType +import com.now.domain.model.type.AdventureEndType +import com.now.domain.model.type.OrderType +import com.now.domain.model.type.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.FinishGameDto import com.now.naaga.data.remote.retrofit.service.AdventureService -import com.now.naaga.util.getValueOrThrow +import com.now.naaga.util.extension.getValueOrThrow class DefaultAdventureRepository( private val adventureService: AdventureService, 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 429643c00..fb3743fd3 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 @@ -5,7 +5,7 @@ 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.service.AuthService -import com.now.naaga.util.getValueOrThrow +import com.now.naaga.util.extension.getValueOrThrow import com.now.naaga.util.unlinkWithKakao import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers diff --git a/android/app/src/main/java/com/now/naaga/data/repository/DefaultLetterRepository.kt b/android/app/src/main/java/com/now/naaga/data/repository/DefaultLetterRepository.kt new file mode 100644 index 000000000..1b0d7b0ce --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/data/repository/DefaultLetterRepository.kt @@ -0,0 +1,33 @@ +package com.now.naaga.data.repository + +import com.now.domain.model.letter.Letter +import com.now.domain.model.letter.LetterPreview +import com.now.domain.model.type.LogType +import com.now.domain.repository.LetterRepository +import com.now.naaga.data.mapper.toDomain +import com.now.naaga.data.remote.dto.post.PostLetterDto +import com.now.naaga.data.remote.retrofit.service.LetterService +import com.now.naaga.util.extension.getValueOrThrow + +class DefaultLetterRepository( + private val letterService: LetterService, +) : LetterRepository { + override suspend fun postLetter(message: String, latitude: Double, longitude: Double): Letter { + val response = letterService.registerLetter(PostLetterDto(message, latitude, longitude)).getValueOrThrow() + return response.toDomain() + } + + override suspend fun fetchNearbyLetters(latitude: Double, longitude: Double): List { + val response = letterService.getNearbyLetters(latitude, longitude).getValueOrThrow() + return response.map { it.toDomain() } + } + + override suspend fun fetchLetter(letterId: Long): Letter { + val response = letterService.getLetter(letterId).getValueOrThrow() + return response.toDomain() + } + + override suspend fun fetchLetterLogs(gameId: Long, logType: LogType): List { + return letterService.getInGameLetters(gameId, logType.name).getValueOrThrow().map { it.toDomain() } + } +} 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 76a77d1b9..44734e19e 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 @@ -2,10 +2,13 @@ package com.now.naaga.data.repository import com.now.domain.model.Coordinate import com.now.domain.model.Place +import com.now.domain.model.PreferenceState import com.now.domain.repository.PlaceRepository import com.now.naaga.data.mapper.toDomain +import com.now.naaga.data.mapper.toPreferenceState +import com.now.naaga.data.remote.dto.PreferenceStateDto import com.now.naaga.data.remote.retrofit.service.PlaceService -import com.now.naaga.util.getValueOrThrow +import com.now.naaga.util.extension.getValueOrThrow import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -33,9 +36,8 @@ class DefaultPlaceRepository( name: String, description: String, coordinate: Coordinate, - image: String, + file: File, ): Place { - val file = File(image) val requestFile = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) val imagePart = MultipartBody.Part.createFormData( KEY_IMAGE_FILE, @@ -53,6 +55,25 @@ class DefaultPlaceRepository( return response.getValueOrThrow().toDomain() } + override suspend fun deletePreference(placeId: Int) { + placeService.deletePreference(placeId) + } + + override suspend fun getLikeCount(placeId: Int): Int { + val response = placeService.getLikeCount(placeId) + return response.getValueOrThrow().placeLikeCount + } + + override suspend fun getMyPreference(placeId: Int): PreferenceState { + val response = placeService.getMyPreference(placeId) + return response.getValueOrThrow().toPreferenceState() + } + + override suspend fun postPreference(placeId: Int, preferenceState: PreferenceState): PreferenceState { + val response = placeService.postPreference(placeId, PreferenceStateDto(preferenceState.name)) + return PreferenceState.valueOf(response.getValueOrThrow().type) + } + companion object { const val KEY_NAME = "name" const val KEY_DESCRIPTION = "description" 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 ac447c91b..c296c302a 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 @@ -4,7 +4,7 @@ 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.retrofit.service.RankService -import com.now.naaga.util.getValueOrThrow +import com.now.naaga.util.extension.getValueOrThrow class DefaultRankRepository( private val rankService: RankService, 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 7f1d70332..8fc82b266 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 @@ -4,7 +4,7 @@ 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.service.StatisticsService -import com.now.naaga.util.getValueOrThrow +import com.now.naaga.util.extension.getValueOrThrow class DefaultStatisticsRepository( private val statisticsService: StatisticsService, diff --git a/android/app/src/main/java/com/now/naaga/data/throwable/DataThrowable.kt b/android/app/src/main/java/com/now/naaga/data/throwable/DataThrowable.kt index 60172c5dd..a2abb66a4 100644 --- a/android/app/src/main/java/com/now/naaga/data/throwable/DataThrowable.kt +++ b/android/app/src/main/java/com/now/naaga/data/throwable/DataThrowable.kt @@ -19,10 +19,19 @@ sealed class DataThrowable(val code: Int, message: String) : Throwable(message) // http ์‘๋‹ต์ฝ”๋“œ 500๋ฒˆ๋Œ€, body๊ฐ€ null์ผ ๋•Œ์˜ ์—๋Ÿฌ class IllegalStateThrowable : DataThrowable(ILLEGAL_STATE_THROWABLE_CODE, ILLEGAL_STATE_THROWABLE_MESSAGE) + // 700๋ฒˆ๋Œ€ ์ชฝ์ง€ ๊ด€๋ จ ์—๋Ÿฌ + class LetterThrowable(code: Int, message: String) : DataThrowable(code, message) + + // IO Exception ์ผ ๊ฒฝ์šฐ์˜ ์˜ˆ์™ธ + class NetworkThrowable : DataThrowable(NETWORK_THROWABLE_CODE, NETWORK_THROWABLE_MESSAGE) + companion object { const val ILLEGAL_STATE_THROWABLE_CODE = 900 const val ILLEGAL_STATE_THROWABLE_MESSAGE = "์ž˜๋ชป๋œ ๊ฐ’์ž…๋‹ˆ๋‹ค." + const val NETWORK_THROWABLE_CODE = 1000 + const val NETWORK_THROWABLE_MESSAGE = "IOException์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค." + val hintThrowable = GameThrowable(455, "์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํžŒํŠธ๋ฅผ ๋ชจ๋‘ ์†Œ์ง„ํ–ˆ์Šต๋‹ˆ๋‹ค.") } } 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 index e7beb9286..a05423df3 100644 --- a/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt +++ b/android/app/src/main/java/com/now/naaga/di/RepositoryModule.kt @@ -2,17 +2,20 @@ package com.now.naaga.di import com.now.domain.repository.AdventureRepository import com.now.domain.repository.AuthRepository +import com.now.domain.repository.LetterRepository 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.LetterService 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.DefaultLetterRepository import com.now.naaga.data.repository.DefaultPlaceRepository import com.now.naaga.data.repository.DefaultRankRepository import com.now.naaga.data.repository.DefaultStatisticsRepository @@ -47,4 +50,9 @@ class RepositoryModule { @Provides fun provideStatisticsRepository(statisticsService: StatisticsService): StatisticsRepository = DefaultStatisticsRepository(statisticsService) + + @Singleton + @Provides + fun provideLetterRepository(letterService: LetterService): LetterRepository = + DefaultLetterRepository(letterService) } diff --git a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt index 266411eb9..340e12d02 100644 --- a/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt +++ b/android/app/src/main/java/com/now/naaga/di/ServiceModule.kt @@ -5,6 +5,7 @@ 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.LetterService 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 @@ -56,4 +57,8 @@ class ServiceModule { @Singleton @Provides fun provideAuthService(retrofit: Retrofit): AuthService = retrofit.create(AuthService::class.java) + + @Singleton + @Provides + fun provideLetterService(retrofit: Retrofit): LetterService = retrofit.create(LetterService::class.java) } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt new file mode 100644 index 000000000..57fe34965 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt @@ -0,0 +1,107 @@ +package com.now.naaga.presentation.adventuredetail + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.bumptech.glide.Glide +import com.google.android.material.tabs.TabLayoutMediator +import com.now.domain.model.AdventureResult +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.databinding.ActivityAdventureDetailBinding +import com.now.naaga.presentation.adventuredetail.viewpager.ViewPagerAdapter +import com.now.naaga.presentation.uimodel.model.LetterUiModel +import com.now.naaga.util.extension.repeatOnStarted +import com.now.naaga.util.extension.showSnackbarWithEvent +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { + private lateinit var binding: ActivityAdventureDetailBinding + private val viewModel: AdventureDetailViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityAdventureDetailBinding.inflate(layoutInflater) + setContentView(binding.root) + + val gameId = intent.getLongExtra(KET_GAME_ID, 0L) + + initView(gameId) + setClickListeners() + subscribe() + } + + private fun initView(gameId: Long) { + viewModel.fetchReadLetter(gameId) + viewModel.fetchWriteLetter(gameId) + viewModel.fetchAdventureResult(gameId) + } + + private fun setClickListeners() { + binding.ivAdventureDetailBack.setOnClickListener { finish() } + } + + private fun subscribe() { + repeatOnStarted { + viewModel.uiState.collect { adventureDetailUiState -> + when (adventureDetailUiState) { + is AdventureDetailUiState.Loading, is AdventureDetailUiState.Error -> Unit + is AdventureDetailUiState.Success -> initView(adventureDetailUiState) + } + } + } + repeatOnStarted { + viewModel.throwableFlow.collect { event -> + when (event) { + is AdventureDetailViewModel.Event.NetworkExceptionEvent -> showReRequestSnackbar() + is AdventureDetailViewModel.Event.LetterExceptionEvent -> showReRequestSnackbar() + is AdventureDetailViewModel.Event.GameExceptionEvent -> showReRequestSnackbar() + } + } + } + } + + private fun showReRequestSnackbar() { + binding.root.showSnackbarWithEvent( + message = getString(R.string.snackbar_action_re_request_message), + actionTitle = getString(R.string.snackbar_action__re_request_title), + ) { finish() } + } + + private fun initView(adventureDetailUiState: AdventureDetailUiState.Success) { + initViewPager(adventureDetailUiState.readLetters, adventureDetailUiState.writeLetters) + initImage(adventureDetailUiState.adventureResult) + } + + private fun initViewPager(readLetters: List, writeLetters: List) { + binding.vpAdventureDetail.adapter = ViewPagerAdapter(listOf(readLetters, writeLetters)) + + TabLayoutMediator(binding.tlAdventureDetail, binding.vpAdventureDetail) { tab, position -> + when (position) { + 0 -> tab.text = getString(R.string.adventure_detail_read_letter) + 1 -> tab.text = getString(R.string.adventure_detail_write_letter) + } + }.attach() + } + + private fun initImage(adventureResult: AdventureResult) { + Glide.with(binding.ivAdventureDetailPhoto) + .load(adventureResult.destination.image) + .error(R.drawable.ic_none_photo) + .into(binding.ivAdventureDetailPhoto) + } + + companion object { + private const val KET_GAME_ID = "GAME_ID" + + fun getIntentWithId(context: Context, gameId: Long): Intent { + return Intent(context, AdventureDetailActivity::class.java).apply { + putExtra(KET_GAME_ID, gameId) + } + } + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailUiState.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailUiState.kt new file mode 100644 index 000000000..3eb0d6cbd --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailUiState.kt @@ -0,0 +1,16 @@ +package com.now.naaga.presentation.adventuredetail + +import com.now.domain.model.AdventureResult +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +sealed interface AdventureDetailUiState { + object Loading : AdventureDetailUiState + + data class Success( + val readLetters: List, + val writeLetters: List, + val adventureResult: AdventureResult, + ) : AdventureDetailUiState + + object Error : AdventureDetailUiState +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt new file mode 100644 index 000000000..cf2eeb8c2 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt @@ -0,0 +1,115 @@ +package com.now.naaga.presentation.adventuredetail + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.now.domain.model.AdventureResult +import com.now.domain.model.letter.Letter +import com.now.domain.model.type.LogType +import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.LetterRepository +import com.now.naaga.data.throwable.DataThrowable +import com.now.naaga.presentation.uimodel.mapper.toUiModel +import com.now.naaga.presentation.uimodel.model.LetterUiModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch +import java.io.IOException +import javax.inject.Inject + +@HiltViewModel +class AdventureDetailViewModel @Inject constructor( + private val letterRepository: LetterRepository, + private val adventureRepository: AdventureRepository, +) : ViewModel() { + private val readLettersFlow = MutableSharedFlow>() + + private val writeLettersFlow = MutableSharedFlow>() + + private val adventureFlow = MutableSharedFlow() + + private val _uiState: MutableStateFlow = MutableStateFlow(AdventureDetailUiState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _throwableFlow = MutableSharedFlow() + val throwableFlow: SharedFlow = _throwableFlow.asSharedFlow() + + init { + viewModelScope.launch { + combine(readLettersFlow, writeLettersFlow, adventureFlow) { readLetters, writeLetters, adventureResult -> + AdventureDetailUiState.Success( + readLetters = getOpenLetterUiModels(readLetters), + writeLetters = getOpenLetterUiModels(writeLetters), + adventureResult = adventureResult, + ) + }.collectLatest { _uiState.value = it } + } + } + + private fun getOpenLetterUiModels(letters: List): List { + if (letters.isEmpty()) return listOf(LetterUiModel.DEFAULT_OPEN_LETTER) + return letters.map { it.toUiModel() } + } + + fun fetchReadLetter(gameId: Long) { + viewModelScope.launch { + runCatching { + letterRepository.fetchLetterLogs(gameId, LogType.READ) + }.onSuccess { + readLettersFlow.emit(it) + }.onFailure { + setThrowable(it) + } + } + } + + fun fetchWriteLetter(gameId: Long) { + viewModelScope.launch { + runCatching { + letterRepository.fetchLetterLogs(gameId, LogType.WRITE) + }.onSuccess { + writeLettersFlow.emit(it) + }.onFailure { + setThrowable(it) + } + } + } + + fun fetchAdventureResult(gameId: Long) { + viewModelScope.launch { + runCatching { + adventureRepository.fetchAdventureResult(gameId) + }.onSuccess { + adventureFlow.emit(it) + }.onFailure { + setThrowable(it) + } + } + } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> throwable(Event.NetworkExceptionEvent(throwable)) + is DataThrowable.LetterThrowable -> throwable(Event.LetterExceptionEvent(throwable)) + is DataThrowable.GameThrowable -> throwable(Event.GameExceptionEvent(throwable)) + } + } + + private fun throwable(event: Event) { + viewModelScope.launch { + _throwableFlow.emit(event) + } + } + + sealed class Event { + data class NetworkExceptionEvent(val throwable: Throwable) : Event() + data class LetterExceptionEvent(val throwable: Throwable) : Event() + data class GameExceptionEvent(val throwable: Throwable) : Event() + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterAdapter.kt new file mode 100644 index 000000000..81215f887 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterAdapter.kt @@ -0,0 +1,19 @@ +package com.now.naaga.presentation.adventuredetail.recyclerview + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +class LetterAdapter(private val letters: List) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LetterViewHolder { + return LetterViewHolder(parent) + } + + override fun onBindViewHolder(holder: LetterViewHolder, position: Int) { + holder.bind(letters[position]) + } + + override fun getItemCount(): Int { + return letters.size + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterViewHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterViewHolder.kt new file mode 100644 index 000000000..f251fe821 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterViewHolder.kt @@ -0,0 +1,20 @@ +package com.now.naaga.presentation.adventuredetail.recyclerview + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.R +import com.now.naaga.databinding.ItemLetterBinding +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +class LetterViewHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_letter, parent, false), +) { + private val binding: ItemLetterBinding = ItemLetterBinding.bind(itemView) + + fun bind(letter: LetterUiModel) { + binding.tvItemLetterNickname.text = letter.nickname + binding.tvItemLetterRegisterDate.text = letter.registerDate + binding.tvItemLetter.text = letter.message + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerAdapter.kt new file mode 100644 index 000000000..e6187505f --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerAdapter.kt @@ -0,0 +1,19 @@ +package com.now.naaga.presentation.adventuredetail.viewpager + +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +class ViewPagerAdapter(private val data: List>) : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewPagerHolder { + return ViewPagerHolder(parent) + } + + override fun onBindViewHolder(holder: ViewPagerHolder, position: Int) { + holder.bind(data[position]) + } + + override fun getItemCount(): Int { + return data.size + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerHolder.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerHolder.kt new file mode 100644 index 000000000..06a98ed93 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerHolder.kt @@ -0,0 +1,19 @@ +package com.now.naaga.presentation.adventuredetail.viewpager + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.now.naaga.R +import com.now.naaga.databinding.ItemViewPagerBinding +import com.now.naaga.presentation.adventuredetail.recyclerview.LetterAdapter +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +class ViewPagerHolder(parent: ViewGroup) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_view_pager, parent, false), +) { + private val binding: ItemViewPagerBinding = ItemViewPagerBinding.bind(itemView) + + fun bind(data: List) { + binding.rvItemViewPager.adapter = LetterAdapter(data) + } +} 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 20ed236f9..443adbd40 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 @@ -6,15 +6,19 @@ import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.now.domain.model.AdventureResult +import com.now.naaga.R +import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityAdventureHistoryBinding +import com.now.naaga.presentation.adventuredetail.AdventureDetailActivity import com.now.naaga.presentation.adventurehistory.recyclerview.AdventureHistoryAdapter +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class AdventureHistoryActivity : AppCompatActivity() { private lateinit var binding: ActivityAdventureHistoryBinding private val viewModel: AdventureHistoryViewModel by viewModels() - private val historyAdapter = AdventureHistoryAdapter() + private val historyAdapter = AdventureHistoryAdapter(::navigateDetail) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -38,6 +42,11 @@ class AdventureHistoryActivity : AppCompatActivity() { viewModel.adventureResults.observe(this) { adventureResults -> updateHistory(adventureResults) } + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } + } } private fun setClickListeners() { @@ -50,6 +59,11 @@ class AdventureHistoryActivity : AppCompatActivity() { historyAdapter.submitList(adventureResults) } + private fun navigateDetail(gameId: Long) { + val intent = AdventureDetailActivity.getIntentWithId(this, gameId) + startActivity(intent) + } + companion object { fun getIntent(context: Context): Intent { return Intent(context, AdventureHistoryActivity::class.java) 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 8a879729f..1c3721578 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 @@ -5,13 +5,14 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel 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.model.type.OrderType +import com.now.domain.model.type.SortType import com.now.domain.repository.AdventureRepository 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 java.io.IOException import javax.inject.Inject @HiltViewModel @@ -20,8 +21,8 @@ class AdventureHistoryViewModel @Inject constructor(private val adventureReposit private val _adventureResults = MutableLiveData>() val adventureResults: LiveData> = _adventureResults - private val _errorMessage = MutableLiveData() - val errorMessage: LiveData = _errorMessage + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable fun fetchHistories() { viewModelScope.launch { @@ -30,18 +31,15 @@ class AdventureHistoryViewModel @Inject constructor(private val adventureReposit }.onSuccess { results: List -> _adventureResults.value = results }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } - private fun setErrorMessage(throwable: DataThrowable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { - is PlayerThrowable -> { - _errorMessage.value = throwable.message - } - - else -> {} + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is PlayerThrowable -> { _throwable.value = throwable } } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryAdapter.kt b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryAdapter.kt index 4a4647d46..0cdfc5b85 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryAdapter.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/adventurehistory/recyclerview/AdventureHistoryAdapter.kt @@ -5,9 +5,13 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import com.now.domain.model.AdventureResult -class AdventureHistoryAdapter : ListAdapter(historyDiff) { +class AdventureHistoryAdapter( + private val onClick: (Long) -> Unit, +) : ListAdapter(historyDiff) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdventureHistoryViewHolder { - return AdventureHistoryViewHolder(AdventureHistoryViewHolder.getView(parent)) + return AdventureHistoryViewHolder(parent) { position -> + onClick(getItem(position).gameId) + } } override fun onBindViewHolder(holder: AdventureHistoryViewHolder, position: Int) { 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 5cc167318..9d75ae055 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 @@ -5,11 +5,23 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.now.domain.model.AdventureResult -import com.now.domain.model.AdventureResultType +import com.now.domain.model.type.AdventureResultType import com.now.naaga.R import com.now.naaga.databinding.ItemHistoryBinding -class AdventureHistoryViewHolder(private val binding: ItemHistoryBinding) : RecyclerView.ViewHolder(binding.root) { +class AdventureHistoryViewHolder( + parent: ViewGroup, + onClick: (Int) -> Unit, +) : RecyclerView.ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_history, parent, false), +) { + + private val binding: ItemHistoryBinding = ItemHistoryBinding.bind(itemView) + + init { + binding.root.setOnClickListener { onClick(adapterPosition) } + } + fun bind(adventureResult: AdventureResult) { binding.adventureResult = adventureResult Glide.with(binding.ivAdventureHistoryPhoto) @@ -38,10 +50,5 @@ class AdventureHistoryViewHolder(private val binding: ItemHistoryBinding) : Recy companion object { private const val DESTINATION_NAME_IN_FAILURE_CASE = "????" - - fun getView(parent: ViewGroup): ItemHistoryBinding { - val layoutInflater = LayoutInflater.from(parent.context) - return ItemHistoryBinding.inflate(layoutInflater, parent, false) - } } } 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 05a0c8dd7..42eaf8049 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 @@ -8,13 +8,14 @@ import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import com.bumptech.glide.Glide import com.now.domain.model.AdventureResult -import com.now.domain.model.AdventureResultType +import com.now.domain.model.type.AdventureResultType import com.now.naaga.R -import com.now.naaga.data.firebase.analytics.ADVENTURE_RESULT 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.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityAdventureResultBinding +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -52,11 +53,18 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul viewModel.adventureResult.observe(this) { adventureResult -> setResultType(adventureResult) setPhoto(adventureResult.destination.image) + viewModel.fetchPreference() } - viewModel.throwable.observe(this) { throwable -> - Toast.makeText(this, throwable.message, Toast.LENGTH_SHORT).show() - logServerError(ADVENTURE_RESULT, throwable.code, throwable.message.toString()) + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } + } + + viewModel.preference.observe(this) { + binding.customAdventureResultPreference.updatePreference(it.state) + binding.customAdventureResultPreference.likeCount = it.likeCount.value } } @@ -93,6 +101,10 @@ class AdventureResultActivity : AppCompatActivity(), AnalyticsDelegate by Defaul logClickEvent(getViewEntryName(it), RESULT_RESULT_RETURN) finish() } + + binding.customAdventureResultPreference.setPreferenceClickListener { + viewModel.changePreference(it) + } } companion object { 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 dadce03a4..ceb9ce1f7 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 @@ -5,29 +5,47 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.now.domain.model.AdventureResult +import com.now.domain.model.Preference +import com.now.domain.model.PreferenceCount +import com.now.domain.model.PreferenceState import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.PlaceRepository import com.now.domain.repository.RankRepository import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.data.throwable.DataThrowable.GameThrowable import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.async import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel class AdventureResultViewModel @Inject constructor( private val adventureRepository: AdventureRepository, private val rankRepository: RankRepository, + private val placeRepository: PlaceRepository, ) : ViewModel() { - private val _adventureResult = MutableLiveData() val adventureResult: LiveData = _adventureResult private val _myRank = MutableLiveData() val myRank: LiveData = _myRank + private val _preference = MutableLiveData() + val preference: LiveData = _preference + private val _throwable = MutableLiveData() val throwable: LiveData = _throwable + fun changePreference(newState: PreferenceState) { + _preference.value = preference.value?.select(newState) + if (preference.value?.state == PreferenceState.NONE) { + deletePreference() + return + } + postPreference() + } + fun fetchGameResult(adventureId: Long) { viewModelScope.launch { runCatching { @@ -35,7 +53,7 @@ class AdventureResultViewModel @Inject constructor( }.onSuccess { adventureResult -> _adventureResult.value = adventureResult }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } @@ -47,18 +65,66 @@ class AdventureResultViewModel @Inject constructor( }.onSuccess { rank -> _myRank.value = rank.rank }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } - private fun setErrorMessage(throwable: DataThrowable) { - when (throwable) { - is GameThrowable -> { - _throwable.value = throwable + fun fetchPreference() { + viewModelScope.launch { + runCatching { + val placeId = + requireNotNull(adventureResult.value) { "adventureResult๊ฐ€ null์ž…๋‹ˆ๋‹ค." }.destination.id.toInt() + val deferredLikeCount = async { placeRepository.getLikeCount(placeId) } + val deferredPreferenceState = async { placeRepository.getMyPreference(placeId) } + + Preference( + state = deferredPreferenceState.await(), + likeCount = PreferenceCount(deferredLikeCount.await()), + ) + }.onSuccess { + _preference.value = it + }.onFailure { + setThrowable(it) } + } + } - else -> {} + private fun postPreference() { + viewModelScope.launch { + runCatching { + placeRepository.postPreference( + requireNotNull(adventureResult.value) { "adventureResult๊ฐ€ null์ž…๋‹ˆ๋‹ค." }.destination.id.toInt(), + requireNotNull(preference.value) { "preference๊ฐ€ null์ž…๋‹ˆ๋‹ค." }.state, + ) + }.onSuccess { + // post ์‘๋‹ต์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™”๋Š”๋ฐ ๋‚ด๊ฐ€ ๋ณด๋‚ธ ๊ฒƒ๊ณผ ๋‹ค๋ฅธ๊ฒŒ ์˜จ ๊ฒฝ์šฐ. ์ฆ‰ ๋ง์ด ์•ˆ๋˜๋Š” ๊ฒฝ์šฐ + if (preference.value?.state != it) { + _preference.value = Preference(state = it) + } + }.onFailure { + _preference.value = preference.value?.revert() + setThrowable(it) + } + } + } + + private fun deletePreference() { + viewModelScope.launch { + val placeId = requireNotNull(adventureResult.value) { "adventureResult๊ฐ€ null์ž…๋‹ˆ๋‹ค." }.destination.id.toInt() + runCatching { + placeRepository.deletePreference(placeId) + }.onFailure { + _preference.value = preference.value?.revert() + setThrowable(it) + } + } + } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> _throwable.value = DataThrowable.NetworkThrowable() + is GameThrowable -> _throwable.value = throwable } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt new file mode 100644 index 000000000..3a34f0f77 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/adventureresult/PreferenceView.kt @@ -0,0 +1,84 @@ +package com.now.naaga.presentation.adventureresult + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import com.now.domain.model.PreferenceState +import com.now.naaga.R +import com.now.naaga.databinding.CustomPreferenceViewBinding + +class PreferenceView(context: Context, attrs: AttributeSet? = null) : ConstraintLayout(context, attrs) { + private val binding: CustomPreferenceViewBinding + private val layoutInflater = LayoutInflater.from(this.context) + private var preferenceClickListener: PreferenceClickListener? = null + var likeCount: Int = 0 + set(value) { + field = value + binding.tvPreferenceLikeCount.text = value.toString() + } + + init { + binding = CustomPreferenceViewBinding.inflate(layoutInflater, this, true) + setClickListeners() + context.theme.obtainStyledAttributes( + attrs, + R.styleable.PreferenceView, + 0, + 0, + ).apply { + val isLikeVisible = getBoolean(R.styleable.PreferenceView_isLikeVisible, true) + val isDislikeVisible = getBoolean(R.styleable.PreferenceView_isDislikeVisible, true) + val isLikeCountVisible = getBoolean(R.styleable.PreferenceView_isLikeCountVisible, true) + setComponentVisibility(isLikeVisible, isDislikeVisible, isLikeCountVisible) + } + } + + private fun setClickListeners() { + binding.ivPreferenceLike.setOnClickListener { + preferenceClickListener?.onClick(PreferenceState.LIKE) + } + binding.ivPreferenceDislike.setOnClickListener { + preferenceClickListener?.onClick(PreferenceState.DISLIKE) + } + } + + private fun setComponentVisibility( + isLikeVisible: Boolean, + isDislikeVisible: Boolean, + isLikeCountVisible: Boolean, + ) { + val setVisibility: (Boolean) -> Int = { isVisible: Boolean -> if (isVisible) View.VISIBLE else View.GONE } + binding.ivPreferenceLike.visibility = setVisibility(isLikeVisible) + binding.ivPreferenceDislike.visibility = setVisibility(isDislikeVisible) + binding.tvPreferenceLikeCount.visibility = setVisibility(isLikeCountVisible) + } + + fun setPreferenceClickListener(listener: PreferenceClickListener) { + preferenceClickListener = listener + } + + fun updatePreference(preferenceState: PreferenceState) { + when (preferenceState) { + PreferenceState.LIKE -> { + binding.ivPreferenceLike.isSelected = true + binding.ivPreferenceDislike.isSelected = false + } + + PreferenceState.DISLIKE -> { + binding.ivPreferenceLike.isSelected = false + binding.ivPreferenceDislike.isSelected = true + } + + PreferenceState.NONE -> { + binding.ivPreferenceLike.isSelected = false + binding.ivPreferenceDislike.isSelected = false + } + } + } + + fun interface PreferenceClickListener { + fun onClick(preferenceState: PreferenceState) + } +} 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 79f211617..e1987b1d5 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 @@ -22,12 +22,13 @@ 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.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.setting.SettingActivity import com.now.naaga.presentation.upload.UploadActivity +import com.now.naaga.util.extension.openSetting +import com.now.naaga.util.extension.showSnackbarWithEvent +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -43,18 +44,10 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default private val locationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> - when { - permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - Toast.makeText(this, getString(R.string.beginAdventure_precise_access), Toast.LENGTH_SHORT).show() - } - - permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { - Toast.makeText(this, getString(R.string.beginAdventure_approximate_access), Toast.LENGTH_SHORT) - .show() - } - - else -> { - Toast.makeText(this, getString(R.string.beginAdventure_denied_access), Toast.LENGTH_SHORT).show() + permissions.entries.forEach { + val isGranted = it.value + if (isGranted.not()) { + showPermissionSnackbar() } } } @@ -67,7 +60,6 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default startLoading() registerAnalytics(this.lifecycle) fetchInProgressAdventure() - requestLocationPermission() setClickListeners() subscribe() @@ -83,8 +75,10 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default viewModel.loading.observe(this) { loading -> setLoadingView(loading) } - viewModel.error.observe(this) { error: DataThrowable -> - Toast.makeText(this, error.message, Toast.LENGTH_SHORT).show() + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } } } @@ -102,17 +96,6 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default binding.lottieBeginAdventureLoading.visibility = View.GONE } - private fun requestLocationPermission() { - if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { - locationPermissionLauncher.launch( - arrayOf( - Manifest.permission.ACCESS_FINE_LOCATION, - Manifest.permission.ACCESS_COARSE_LOCATION, - ), - ) - } - } - private fun setClickListeners() { binding.btnBeginAdventureBegin.setOnClickListener { logClickEvent(getViewEntryName(it), BEGIN_BEGIN_ADVENTURE) @@ -132,9 +115,17 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default } } + private fun showPermissionSnackbar() { + binding.root.showSnackbarWithEvent( + message = getString(R.string.snackbar_location_message), + actionTitle = getString(R.string.snackbar_action_title), + action = { openSetting() }, + ) + } + private fun checkPermissionAndBeginAdventure() { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_DENIED) { - PermissionDialog(DialogType.LOCATION).show(supportFragmentManager) + locationPermissionLauncher.launch(locationPermissions) } else { checkLocationPermissionInStatusBar() } @@ -161,6 +152,11 @@ class BeginAdventureActivity : AppCompatActivity(), AnalyticsDelegate by Default } companion object { + private val locationPermissions = arrayOf( + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) + private const val GPS_TURN_ON_MESSAGE = "GPS ์„ค์ •์„ ์ผœ์ฃผ์„ธ์š”" fun getIntent(context: Context): Intent { 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 a1c45e2d3..50250a7c2 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 @@ -10,6 +10,7 @@ import com.now.domain.repository.AdventureRepository import com.now.naaga.data.throwable.DataThrowable import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -17,8 +18,8 @@ class BeginAdventureViewModel @Inject constructor(private val adventureRepositor private val _adventure = MutableLiveData() val adventure: LiveData = _adventure - private val _error = MutableLiveData() - val error: LiveData = _error + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable private val _loading = MutableLiveData(false) val loading: LiveData = _loading @@ -32,8 +33,14 @@ class BeginAdventureViewModel @Inject constructor(private val adventureRepositor _loading.value = false _adventure.value = it.firstOrNull() }.onFailure { - _error.value = it as DataThrowable + setThrowable(it) } } } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + } + } } 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 deleted file mode 100644 index 5d5bdeb18..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/DialogType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.now.naaga.presentation.common.dialog - -enum class DialogType { - LOCATION, CAMERA -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterReadDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterReadDialog.kt new file mode 100644 index 000000000..cd35bee26 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterReadDialog.kt @@ -0,0 +1,48 @@ +package com.now.naaga.presentation.common.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.now.naaga.databinding.DialogReadLetterBinding +import com.now.naaga.util.dpToPx +import com.now.naaga.util.getWidthProportionalToDevice + +class LetterReadDialog(private val content: String) : DialogFragment() { + private lateinit var binding: DialogReadLetterBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DialogReadLetterBinding.inflate(layoutInflater) + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + setContent() + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialog?.setCanceledOnTouchOutside(true) + setSize() + } + + private fun setSize() { + val dialogWidth = getWidthProportionalToDevice(requireContext(), WIDTH_RATE) + val dialogHeight = dpToPx(requireContext(), HEIGHT) + dialog?.window?.setLayout(dialogWidth, dialogHeight) + } + + private fun setContent() { + binding.letter = content + } + + companion object { + private const val WIDTH_RATE = 0.78f + private const val HEIGHT = 430 + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterSendDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterSendDialog.kt new file mode 100644 index 000000000..316b45d34 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/LetterSendDialog.kt @@ -0,0 +1,53 @@ +package com.now.naaga.presentation.common.dialog + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.now.naaga.databinding.DialogSendLetterBinding +import com.now.naaga.util.dpToPx +import com.now.naaga.util.getWidthProportionalToDevice + +class LetterSendDialog( + private val onClick: (String) -> Unit, +) : DialogFragment() { + private lateinit var binding: DialogSendLetterBinding + var message: String = "" + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + binding = DialogSendLetterBinding.inflate(layoutInflater) + dialog?.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + binding.dialog = this + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + dialog?.setCanceledOnTouchOutside(true) + setSize() + setClickListener() + } + + private fun setSize() { + val dialogWidth = getWidthProportionalToDevice(requireContext(), WIDTH_RATE) + val dialogHeight = dpToPx(requireContext(), HEIGHT) + dialog?.window?.setLayout(dialogWidth, dialogHeight) + } + + private fun setClickListener() { + binding.btnDialogLetterSubmit.setOnClickListener { onClick(message) } + } + + companion object { + const val TAG = "SEND_LETTER" + private const val WIDTH_RATE = 0.78f + private const val HEIGHT = 430 + } +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt b/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt deleted file mode 100644 index b541e51d7..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/common/dialog/PermissionDialog.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.now.naaga.presentation.common.dialog - -import android.annotation.SuppressLint -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.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.databinding.DialogPermissionBinding -import com.now.naaga.util.dpToPx -import com.now.naaga.util.getWidthProportionalToDevice - -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 = 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) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setSize() - binding.btnDialogPermissionSetting.setOnClickListener { - logClickEvent(requireContext().getViewEntryName(it), CAMERA_PERMISSION_OPEN_SETTING) - openSetting() - dismiss() - } - } - - private fun setSize() { - val dialogWidth = getWidthProportionalToDevice(requireContext(), WIDTH_RATE) - val dialogHeight = dpToPx(requireContext(), 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) - } - - fun show(manager: FragmentManager) { - show(manager, type.name) - } - - companion object { - const val WIDTH_RATE = 0.83f - const val HEIGHT = 400 - } -} 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 1e2f9f673..0f7aa81b7 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 @@ -5,15 +5,16 @@ 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 com.now.domain.model.AuthPlatformType +import com.now.domain.model.type.AuthPlatformType +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.throwable.DataThrowable import com.now.naaga.databinding.ActivityLoginBinding import com.now.naaga.presentation.beginadventure.BeginAdventureActivity +import com.now.naaga.util.extension.showToast import com.now.naaga.util.loginWithKakao import dagger.hilt.android.AndroidEntryPoint @@ -39,9 +40,10 @@ class LoginActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytics } } - viewModel.throwable.observe(this) { throwable -> - Toast.makeText(this, throwable.message, Toast.LENGTH_SHORT).show() - logServerError(LOGIN_AUTH, throwable.code, throwable.message.toString()) + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } } } 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 0637fb344..1cc6136f5 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 @@ -4,12 +4,13 @@ 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.model.type.AuthPlatformType import com.now.domain.repository.AuthRepository import com.now.naaga.data.throwable.DataThrowable import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -29,8 +30,14 @@ class LoginViewModel @Inject constructor( }.onSuccess { status -> _isLoginSucceed.value = status }.onFailure { - _throwable.value = it as DataThrowable + setThrowable(it) } } } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + } + } } 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 392bff895..516b77f63 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 @@ -3,18 +3,19 @@ package com.now.naaga.presentation.mypage 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.domain.model.Statistics +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.MYPAGE_GO_RESULTS -import com.now.naaga.data.firebase.analytics.MY_PAGE_STATISTICS +import com.now.naaga.data.throwable.DataThrowable 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 com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -63,9 +64,10 @@ class MyPageActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic val placesUiModels = places.map { it.toUiModel() } binding.customGridMypagePlaces.initContent(placesUiModels) } - viewModel.throwable.observe(this) { throwable -> - Toast.makeText(this, throwable.message, Toast.LENGTH_SHORT).show() - logServerError(MY_PAGE_STATISTICS, throwable.code, throwable.message.toString()) + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } } } 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 c7b8979a9..5a70439ea 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 @@ -4,19 +4,18 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.now.domain.model.OrderType import com.now.domain.model.Place import com.now.domain.model.Rank -import com.now.domain.model.SortType import com.now.domain.model.Statistics +import com.now.domain.model.type.OrderType +import com.now.domain.model.type.SortType import com.now.domain.repository.PlaceRepository import com.now.domain.repository.RankRepository import com.now.domain.repository.StatisticsRepository 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 java.io.IOException import javax.inject.Inject @HiltViewModel @@ -44,7 +43,7 @@ class MyPageViewModel @Inject constructor( }.onSuccess { rank -> _rank.value = rank }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } @@ -56,7 +55,7 @@ class MyPageViewModel @Inject constructor( }.onSuccess { statistics -> _statistics.value = statistics }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } @@ -68,22 +67,16 @@ class MyPageViewModel @Inject constructor( }.onSuccess { places -> _places.value = places }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } - private fun setErrorMessage(throwable: Throwable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { - is PlayerThrowable -> { - _throwable.value = throwable - } - - is PlaceThrowable -> { - _throwable.value = throwable - } - - else -> {} + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is DataThrowable.PlayerThrowable -> { _throwable.value = throwable } + is DataThrowable.PlaceThrowable -> { _throwable.value = throwable } } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/DistinctChildLiveData.kt b/android/app/src/main/java/com/now/naaga/presentation/onadventure/DistinctChildLiveData.kt deleted file mode 100644 index 18fc7c28d..000000000 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/DistinctChildLiveData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.now.naaga.presentation.onadventure - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import java.util.concurrent.atomic.AtomicReference - -class DistinctChildLiveData(private val parentLiveData: LiveData) : LiveData() { - private val oldData = AtomicReference() - - override fun observe(owner: LifecycleOwner, observer: Observer) { - parentLiveData.observe(owner) { newData: T -> - if (oldData.get() != newData) { - oldData.set(newData) - value = newData - } - super.observe(owner, observer) - } - } -} diff --git a/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaverMapSettingDelegate.kt b/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaverMapSettingDelegate.kt index 66ba2b07a..6f02b328a 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaverMapSettingDelegate.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/onadventure/NaverMapSettingDelegate.kt @@ -20,18 +20,23 @@ import com.naver.maps.map.widget.LocationButtonView import com.now.domain.model.Coordinate import com.now.domain.model.Direction import com.now.domain.model.Hint +import com.now.domain.model.letter.LetterPreview import com.now.naaga.R import com.now.naaga.util.dpToPx +import com.now.naaga.util.extension.setOnSingleClickListener interface NaverMapSettingDelegate : OnMapReadyCallback { val mapFragment: MapFragment val naverMap: NaverMap val locationSource: LocationSource val hintMarkers: MutableList + val letterMarkers: MutableList fun setNaverMap(activity: AppCompatActivity, @IdRes mapLayoutId: Int) fun addHintMarker(hint: Hint) fun addDestinationMarker(coordinate: Coordinate) + fun addLetter(letter: LetterPreview, action: (id: Long) -> Unit) + fun removeLetters() fun setOnMapReady(action: () -> Unit) } @@ -44,6 +49,7 @@ class DefaultNaverMapSettingDelegate() : NaverMapSettingDelegate, DefaultLifecyc override lateinit var naverMap: NaverMap // API๋ฅผ ํ˜ธ์ถœํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค override lateinit var locationSource: LocationSource // ๋„ค์ด๋ฒ„ ์ง€๋„ SDK์— ์œ„์น˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค override val hintMarkers: MutableList = mutableListOf() + override val letterMarkers: MutableList = mutableListOf() override fun setNaverMap(activity: AppCompatActivity, @IdRes mapLayoutId: Int) { this.activity = activity @@ -121,6 +127,35 @@ class DefaultNaverMapSettingDelegate() : NaverMapSettingDelegate, DefaultLifecyc } } + override fun addLetter(letter: LetterPreview, action: (id: Long) -> Unit) { + Marker().apply { + position = LatLng(letter.coordinate.latitude, letter.coordinate.longitude) + icon = selectLetterIcon(letter.isNearBy) + if (letter.isNearBy) { + setOnSingleClickListener { + action(letter.id) + } + } + map = naverMap + letterMarkers.add(this) + } + } + + private fun selectLetterIcon(isNearBy: Boolean): OverlayImage { + return if (isNearBy) { + OverlayImage.fromResource(R.drawable.ic_letter) + } else { + OverlayImage.fromResource(R.drawable.ic_letter_preview) + } + } + + override fun removeLetters() { + letterMarkers.forEach { letterMarker -> + letterMarker.map = null + } + letterMarkers.clear() + } + override fun setOnMapReady(action: () -> Unit) { this.action = action } 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 831188b5c..95acee13e 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 @@ -15,6 +15,7 @@ import com.now.domain.model.Adventure import com.now.domain.model.AdventureStatus import com.now.domain.model.Coordinate import com.now.domain.model.Hint +import com.now.domain.model.letter.LetterPreview import com.now.naaga.R import com.now.naaga.data.firebase.analytics.AnalyticsDelegate import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate @@ -26,12 +27,16 @@ 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.LetterReadDialog +import com.now.naaga.presentation.common.dialog.LetterSendDialog 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.extension.getParcelableCompat +import com.now.naaga.util.extension.showSnackbar +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -55,6 +60,15 @@ class OnAdventureActivity : setClickListeners() } + override fun onPause() { + super.onPause() + supportFragmentManager.fragments.forEach { fragment -> + if (TAGS.contains(fragment.tag)) { + supportFragmentManager.beginTransaction().remove(fragment).commit() + } + } + } + private var backPressedTime = 0L private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -66,8 +80,7 @@ class OnAdventureActivity : this@OnAdventureActivity, getString(R.string.OnAdventure_warning_back_pressed), Toast.LENGTH_SHORT, - ) - .show() + ).show() } } } @@ -89,6 +102,9 @@ class OnAdventureActivity : logClickEvent(getViewEntryName(it), ON_ADVENTURE_END_ADVENTURE) viewModel.endAdventure() } + binding.ivSendLetter.setOnClickListener { + LetterSendDialog(viewModel::sendLetter).show(supportFragmentManager, LetterSendDialog.TAG) + } } private fun subscribe() { @@ -106,14 +122,24 @@ class OnAdventureActivity : viewModel.lastHint.observe(this) { drawHintMarkers(listOf(it)) } - viewModel.remainingHintCount.observe(this) { - // binding.tvOnAdventureHintCount.text = it.toString() + viewModel.isSendLetterSuccess.observe(this) { + supportFragmentManager.findFragmentByTag(LetterSendDialog.TAG)?.onDestroyView() + when (it) { + true -> { binding.root.showSnackbar(getString(R.string.OnAdventure_send_letter_success)) } + false -> { binding.root.showSnackbar(getString(R.string.OnAdventure_send_letter_fail)) } + } + } + viewModel.letters.observe(this) { + drawLetters(it) } - viewModel.error.observe(this) { error: DataThrowable -> - logServerError(ON_ADVENTURE_GAME, error.code, error.message.toString()) - when (error.code) { + viewModel.letter.observe(this) { + showLetterReadDialog(it.message) + } + viewModel.throwable.observe(this) { throwable: DataThrowable -> + logServerError(ON_ADVENTURE_GAME, throwable.code, throwable.message.toString()) + when (throwable.code) { OnAdventureViewModel.NO_DESTINATION -> { - shortToast(error.message ?: return@observe) + showToast(throwable.message ?: return@observe) finish() } @@ -122,8 +148,11 @@ class OnAdventureActivity : shortSnackbar(getString(R.string.onAdventure_retry, remainingTryCount)) } - OnAdventureViewModel.TRY_COUNT_OVER -> shortToast(getString(R.string.onAdventure_try_count_over)) - else -> shortSnackbar(error.message ?: return@observe) + OnAdventureViewModel.TRY_COUNT_OVER -> showToast(getString(R.string.onAdventure_try_count_over)) + + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + + else -> shortSnackbar(throwable.message ?: return@observe) } } } @@ -167,6 +196,13 @@ class OnAdventureActivity : } } + private fun drawLetters(letters: List) { + removeLetters() + letters.forEach { letter -> + addLetter(letter, viewModel::getLetter) + } + } + private fun showGiveUpDialog() { val fragment: Fragment? = supportFragmentManager.findFragmentByTag(GIVE_UP) if (fragment == null) { @@ -186,7 +222,7 @@ class OnAdventureActivity : private fun showHintDialog() { NaagaAlertDialog.Builder().build( title = getString(R.string.hint_using_dialog_title), - description = getString(R.string.hint_using_dialog_description, viewModel.remainingHintCount.value), + description = getString(R.string.hint_using_dialog_description, viewModel.remainingHintCount), positiveText = getString(R.string.hint_using_dialog_continue), negativeText = getString(R.string.hint_using_dialog_give_up), positiveAction = { viewModel.openHint() }, @@ -204,12 +240,12 @@ class OnAdventureActivity : } } - private fun shortSnackbar(message: String) { - Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() + private fun showLetterReadDialog(content: String) { + LetterReadDialog(content).show(supportFragmentManager, LETTER) } - private fun shortToast(message: String) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + private fun shortSnackbar(message: String) { + Snackbar.make(binding.root, message, Snackbar.LENGTH_SHORT).show() } companion object { @@ -217,6 +253,8 @@ class OnAdventureActivity : private const val GIVE_UP = "GIVE_UP" private const val ADVENTURE = "ADVENTURE" private const val HINT = "HINT" + private const val LETTER = "LETTER" + private val TAGS = listOf(GIVE_UP, HINT, LETTER) fun getIntent(context: Context): Intent { return Intent(context, OnAdventureActivity::class.java) 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 3119d6753..a35f4ffc9 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,29 +3,40 @@ package com.now.naaga.presentation.onadventure import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData 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 import com.now.domain.model.Coordinate import com.now.domain.model.Hint import com.now.domain.model.RemainingTryCount +import com.now.domain.model.letter.LetterPreview +import com.now.domain.model.type.AdventureEndType import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.LetterRepository 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 com.now.naaga.presentation.uimodel.mapper.toUiModel +import com.now.naaga.presentation.uimodel.model.LetterUiModel import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel -class OnAdventureViewModel @Inject constructor(private val adventureRepository: AdventureRepository) : ViewModel() { +class OnAdventureViewModel @Inject constructor( + private val adventureRepository: AdventureRepository, + private val letterRepository: LetterRepository, +) : ViewModel() { private val _adventure = MutableLiveData() val adventure: LiveData = _adventure val hints = DisposableLiveData>(_adventure.map { it.hints }) - val remainingHintCount = DistinctChildLiveData(adventure.map { getRemainingHintCount() }) + val remainingHintCount: Int + get() = (RemainingTryCount(MAX_HINT_COUNT) - (adventure.value?.hints?.size ?: 0)).toInt() val myCoordinate = MutableLiveData() val startCoordinate = DisposableLiveData(myCoordinate) @@ -36,8 +47,28 @@ class OnAdventureViewModel @Inject constructor(private val adventureRepository: private val _lastHint = MutableLiveData() val lastHint: LiveData = _lastHint - private val _error = MutableLiveData() - val error: LiveData = _error + val letters: LiveData> = liveData { + while (true) { + myCoordinate.value?.let { coordinate -> + emit( + letterRepository.fetchNearbyLetters( + latitude = coordinate.latitude, + longitude = coordinate.longitude, + ).map { it.copy(isNearBy = coordinate.isNearBy(it.coordinate)) }, + ) + } ?: emit(emptyList()) + delay(5000) + } + } + + private val _letter = MutableLiveData() + val letter: LiveData = _letter + + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable + + private val _isSendLetterSuccess = MutableLiveData() + val isSendLetterSuccess: LiveData = _isSendLetterSuccess fun setAdventure(adventure: Adventure) { _adventure.value = adventure @@ -50,7 +81,7 @@ class OnAdventureViewModel @Inject constructor(private val adventureRepository: }.onSuccess { setAdventure(it) }.onFailure { - setError(it as DataThrowable) + setThrowable(it) } } } @@ -66,14 +97,14 @@ class OnAdventureViewModel @Inject constructor(private val adventureRepository: }.onSuccess { status: AdventureStatus -> _adventure.value = adventure.value?.copy(adventureStatus = status) }.onFailure { - setError(it as DataThrowable) + setThrowable(it) } } } fun openHint() { if (isAllHintsUsed()) { - setError(hintThrowable) + setThrowable(hintThrowable) return } viewModelScope.launch { @@ -86,18 +117,13 @@ class OnAdventureViewModel @Inject constructor(private val adventureRepository: _adventure.value = adventure.value?.copy(hints = ((adventure.value?.hints ?: listOf()) + hint)) _lastHint.value = hint }.onFailure { - setError(it as DataThrowable) + setThrowable(it) } } } private fun isAllHintsUsed(): Boolean { - return getRemainingHintCount() <= 0 - } - - private fun getRemainingHintCount(): Int { - val usedHintCount = adventure.value?.hints?.size ?: 0 - return (RemainingTryCount(MAX_HINT_COUNT) - usedHintCount).toInt() + return remainingHintCount <= 0 } fun endAdventure() { @@ -111,23 +137,50 @@ class OnAdventureViewModel @Inject constructor(private val adventureRepository: }.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) + setThrowable(it) + } + } + } + + private fun handleGameThrowable(throwable: GameThrowable) { + when (throwable.code) { + TRY_COUNT_OVER -> _adventure.value = adventure.value?.copy(adventureStatus = AdventureStatus.DONE) + NOT_ARRIVED -> { + val currentRemainingTryCount = adventure.value?.remainingTryCount ?: return + _adventure.value = adventure.value?.copy(remainingTryCount = currentRemainingTryCount - 1) + } + else -> { _throwable.value = throwable } + } + } + + fun getLetter(id: Long) { + viewModelScope.launch { + runCatching { + letterRepository.fetchLetter(id) + }.onSuccess { letter -> + _letter.value = letter.toUiModel() + }.onFailure { + setThrowable(it) + } + } + } + + fun sendLetter(message: String) { + viewModelScope.launch { + runCatching { + myCoordinate.value?.let { letterRepository.postLetter(message, it.latitude, it.longitude) } + }.onSuccess { + _isSendLetterSuccess.value = true + }.onFailure { } } } - private fun setError(throwable: DataThrowable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { - is GameThrowable -> _error.value = throwable - is UniversalThrowable -> _error.value = throwable - else -> {} + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is GameThrowable -> { handleGameThrowable(throwable) } + is UniversalThrowable -> _throwable.value = throwable } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/rank/RankActivity.kt index ad41103b9..98167504a 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 @@ -3,15 +3,16 @@ package com.now.naaga.presentation.rank 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.domain.model.Rank +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.RANK_RANK +import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityRankBinding import com.now.naaga.presentation.rank.recyclerview.RankAdapter +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -51,8 +52,9 @@ class RankActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsD updateRank(ranks) } viewModel.throwable.observe(this) { throwable -> - Toast.makeText(this, throwable.message, Toast.LENGTH_SHORT).show() - logServerError(RANK_RANK, throwable.code, throwable.message.toString()) + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } } } 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 277615c06..5996d0547 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 @@ -4,14 +4,14 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel 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.model.type.OrderType +import com.now.domain.model.type.SortType import com.now.domain.repository.RankRepository 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 java.io.IOException import javax.inject.Inject @HiltViewModel @@ -40,7 +40,7 @@ class RankViewModel @Inject constructor(private val rankRepository: RankReposito _myScore.value = rank.player.score _myRank.value = rank.rank }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } @@ -52,15 +52,15 @@ class RankViewModel @Inject constructor(private val rankRepository: RankReposito }.onSuccess { ranks -> _ranks.value = ranks }.onFailure { - setErrorMessage(it as DataThrowable) + setThrowable(it) } } } - private fun setErrorMessage(throwable: DataThrowable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { - is PlayerThrowable -> { _throwable.value = throwable } - else -> {} + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is DataThrowable.PlayerThrowable -> { _throwable.value = throwable } } } } 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 index e5b4efacb..13d720c3e 100644 --- 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 @@ -3,7 +3,6 @@ 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 @@ -11,6 +10,7 @@ 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 com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -30,19 +30,20 @@ class SettingActivity : AppCompatActivity() { private fun subscribe() { viewModel.isLoggedIn.observe(this) { isLoggedIn -> if (!isLoggedIn) { - shortToast(getString(R.string.setting_logout_message)) + showToast(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.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + WRONG_AUTH_ERROR_CODE -> showToast(getString(R.string.setting_wrong_error_message)) + EXPIRATION_AUTH_ERROR_CODE -> showToast(getString(R.string.setting_expiration_error_message)) + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } } } viewModel.withdrawalStatus.observe(this) { status -> if (status == true) { - shortToast(getString(R.string.setting_withdrawal_success_message)) + showToast(getString(R.string.setting_withdrawal_success_message)) navigateLogin() } } @@ -68,10 +69,6 @@ class SettingActivity : AppCompatActivity() { 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), 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 index 3da61a6b6..0ef7d90fd 100644 --- 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 @@ -6,9 +6,9 @@ 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 java.io.IOException import javax.inject.Inject @HiltViewModel @@ -26,7 +26,7 @@ class SettingViewModel @Inject constructor(private val authRepository: AuthRepos viewModelScope.launch { runCatching { authRepository.logout() } .onSuccess { _isLoggedIn.value = false } - .onFailure { setError(it as DataThrowable) } + .onFailure { setThrowable(it) } } } @@ -34,14 +34,14 @@ class SettingViewModel @Inject constructor(private val authRepository: AuthRepos viewModelScope.launch { runCatching { authRepository.withdrawalMember() } .onSuccess { _withdrawalStatus.value = true } - .onFailure { setError(it as DataThrowable) } + .onFailure { setThrowable(it) } } } - private fun setError(throwable: DataThrowable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { - is AuthorizationThrowable -> _throwable.value = throwable - else -> {} + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is DataThrowable.AuthorizationThrowable -> _throwable.value = throwable } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashActivity.kt index fee2f0d9c..19c7ccc9a 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 @@ -12,11 +12,12 @@ 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.data.throwable.DataThrowable import com.now.naaga.presentation.beginadventure.BeginAdventureActivity import com.now.naaga.presentation.common.dialog.NaagaAlertDialog import com.now.naaga.presentation.login.LoginActivity import com.now.naaga.util.extension.getPackageInfoCompat +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint @SuppressLint("CustomSplashScreen") @@ -81,8 +82,10 @@ class SplashActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic } startLoginActivity() } - viewModel.error.observe(this) { - logServerError(SPLASH_MY_PAGE_STATISTICS, it.code, it.message.toString()) + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + DataThrowable.NETWORK_THROWABLE_CODE -> { showToast(getString(R.string.network_error_message)) } + } } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/splash/SplashViewModel.kt index 4d57b6570..426f48f4d 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 @@ -8,6 +8,7 @@ import com.now.domain.repository.StatisticsRepository import com.now.naaga.data.throwable.DataThrowable import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.io.IOException import javax.inject.Inject @HiltViewModel @@ -16,8 +17,8 @@ class SplashViewModel @Inject constructor(private val statisticsRepository: Stat private val _isTokenValid = MutableLiveData() val isTokenValid: LiveData = _isTokenValid - private val _error = MutableLiveData() - val error: LiveData = _error + private val _throwable = MutableLiveData() + val throwable: LiveData = _throwable fun testTokenValid() { viewModelScope.launch { @@ -27,8 +28,15 @@ class SplashViewModel @Inject constructor(private val statisticsRepository: Stat _isTokenValid.value = true }.onFailure { _isTokenValid.value = false - _error.value = it as DataThrowable.AuthorizationThrowable + setThrowable(it) } } } + + private fun setThrowable(throwable: Throwable) { + when (throwable) { + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } + is DataThrowable.AuthorizationThrowable -> { _throwable.value = throwable } + } + } } diff --git a/android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/LetterMapper.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/LetterMapper.kt new file mode 100644 index 000000000..b58190dbb --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/LetterMapper.kt @@ -0,0 +1,12 @@ +package com.now.naaga.presentation.uimodel.mapper + +import com.now.domain.model.letter.Letter +import com.now.naaga.presentation.uimodel.model.LetterUiModel + +fun Letter.toUiModel(): LetterUiModel { + return LetterUiModel( + nickname = player.nickname, + registerDate = registerDate, + message = message, + ) +} diff --git a/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/LetterUiModel.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/LetterUiModel.kt new file mode 100644 index 000000000..08a2bf78e --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/LetterUiModel.kt @@ -0,0 +1,12 @@ +package com.now.naaga.presentation.uimodel.model + +data class LetterUiModel( + val nickname: String, + val registerDate: String, + val message: String, +) { + companion object { + private const val DEFAULT_MESSAGE = "์ชฝ์ง€๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค." + val DEFAULT_OPEN_LETTER = LetterUiModel("", "", DEFAULT_MESSAGE) + } +} 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 820949ecc..5ac5a77f1 100644 --- a/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt +++ b/android/app/src/main/java/com/now/naaga/presentation/upload/UploadActivity.kt @@ -6,16 +6,16 @@ import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.location.Location import android.net.Uri +import android.os.Build import android.os.Bundle import android.provider.MediaStore import android.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 com.google.android.gms.location.LocationServices import com.google.android.gms.tasks.CancellationToken import com.google.android.gms.tasks.CancellationTokenSource @@ -27,9 +27,14 @@ import com.now.naaga.data.firebase.analytics.DefaultAnalyticsDelegate import com.now.naaga.data.firebase.analytics.UPLOAD_OPEN_CAMERA import com.now.naaga.data.throwable.DataThrowable import com.now.naaga.databinding.ActivityUploadBinding -import com.now.naaga.presentation.common.dialog.DialogType -import com.now.naaga.presentation.common.dialog.PermissionDialog +import com.now.naaga.presentation.upload.UploadViewModel.Companion.FILE_EMPTY +import com.now.naaga.util.extension.openSetting +import com.now.naaga.util.extension.showSnackbarWithEvent +import com.now.naaga.util.extension.showToast import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.io.FileOutputStream +import java.time.LocalDateTime @AndroidEntryPoint class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() { @@ -47,20 +52,18 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions(), ) { permission: Map -> - permission.entries.forEach { entry -> - val isGranted = entry.value - if (isGranted.not()) { - when (entry.key) { - Manifest.permission.CAMERA -> { - PermissionDialog(DialogType.CAMERA).show(supportFragmentManager) - } - - Manifest.permission.ACCESS_FINE_LOCATION -> { - PermissionDialog(DialogType.LOCATION).show(supportFragmentManager) - } - } + + val keys = permission.entries.map { it.key } + val isStorageRequest = storagePermissions.any { keys.contains(it) } + if (isStorageRequest) { + if (permission.entries.map { it.value }.contains(false)) { + showPermissionSnackbar(getString(R.string.snackbar_storage_message)) + } else { + openCamera() } + return@registerForActivityResult } + showPermissionSnackbar(getString(R.string.snackbar_location_message)) } override fun onCreate(savedInstanceState: Bundle?) { @@ -72,7 +75,6 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic initViewModel() subscribe() registerAnalytics(this.lifecycle) - requestPermission() setCoordinate() setClickListeners() } @@ -90,7 +92,7 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic when (uploadStatus) { UploadStatus.SUCCESS -> { changeVisibility(binding.lottieUploadLoading, View.GONE) - shortToast(getString(R.string.upload_success_submit)) + showToast(getString(R.string.upload_success_submit)) finish() } @@ -100,47 +102,29 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic UploadStatus.FAIL -> { changeVisibility(binding.lottieUploadLoading, View.GONE) - shortToast(getString(R.string.upload_fail_submit)) + showToast(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)) - } + viewModel.throwable.observe(this) { throwable: DataThrowable -> + when (throwable.code) { + UploadViewModel.ERROR_STORE_PHOTO -> showToast(getString(R.string.upload_error_store_photo_message)) + UploadViewModel.ERROR_POST_BODY -> showToast(getString(R.string.upload_error_post_message)) + DataThrowable.NETWORK_THROWABLE_CODE -> showToast(getString(R.string.network_error_message)) } } } private fun changeVisibility(view: View, status: Int) { - when (status) { - View.VISIBLE -> { - view.visibility = status - } - - View.GONE -> { - view.visibility = status - } - } + view.visibility = status } - private fun requestPermission() { - val permissionsToRequest = mutableListOf() - requestPermissions.forEach { permission -> - if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { - permissionsToRequest.add(permission) - } - } - requestPermissionLauncher.launch(permissionsToRequest.toTypedArray()) + private fun showPermissionSnackbar(message: String) { + binding.root.showSnackbarWithEvent( + message = message, + actionTitle = getString(R.string.snackbar_action_title), + action = { openSetting() }, + ) } private fun setCoordinate() { @@ -151,6 +135,8 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic location.let { viewModel.setCoordinate(getCoordinate(location)) } } .addOnFailureListener { } + } else { + requestPermissionLauncher.launch(locationPermissions) } } @@ -180,30 +166,32 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private fun setClickListeners() { binding.ivUploadCameraIcon.setOnClickListener { logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) - checkCameraPermission() + requestStoragePermission() } binding.ivUploadPhoto.setOnClickListener { logClickEvent(getViewEntryName(it), UPLOAD_OPEN_CAMERA) - checkCameraPermission() + requestStoragePermission() } binding.ivUploadBack.setOnClickListener { finish() } binding.btnUploadSubmit.setOnClickListener { if (isFormValid().not()) { - shortToast(getString(R.string.upload_error_insufficient_info_message)) + showToast(getString(R.string.upload_error_insufficient_info_message)) } else { viewModel.postPlace() } } } - private fun checkCameraPermission() { - if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_DENIED) { - PermissionDialog(DialogType.CAMERA).show(supportFragmentManager) - } else { - openCamera() + private fun requestStoragePermission() { + val permissionToRequest = storagePermissions.toMutableList() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + return openCamera() } + + requestPermissionLauncher.launch(permissionToRequest.toTypedArray()) } private fun openCamera() { @@ -213,8 +201,21 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic private fun setImage(bitmap: Bitmap) { binding.ivUploadCameraIcon.visibility = View.GONE binding.ivUploadPhoto.setImageBitmap(bitmap) - val uri = getAbsolutePathFromUri(getImageUri(bitmap) ?: Uri.EMPTY) ?: "" - viewModel.setUri(uri) + val uri = getImageUri(bitmap) ?: Uri.EMPTY + val file = makeImageFile(uri) + viewModel.setFile(file) + } + + private fun makeImageFile(uri: Uri): File { + val bitmap = contentResolver.openInputStream(uri).use { + BitmapFactory.decodeStream(it) + } + val tempFile = File.createTempFile("image", ".jpeg", cacheDir) ?: FILE_EMPTY + + FileOutputStream(tempFile).use { + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it) + } + return tempFile } private fun getImageUri(bitmap: Bitmap): Uri? { @@ -230,35 +231,22 @@ class UploadActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalytic return null } - 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 shortToast(message: String) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - } - private fun isFormValid(): Boolean { return viewModel.isFormValid() } companion object { - private val requestPermissions = listOf( + private val storagePermissions = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + ) + + private val locationPermissions = arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION, - Manifest.permission.CAMERA, ) val contentValues = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, "ImageTitle") + put(MediaStore.Images.Media.DISPLAY_NAME, "ImageTitle ${LocalDateTime.now()}") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") } 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 5f7a460ac..17efad611 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 @@ -9,17 +9,19 @@ 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 com.now.naaga.util.singleliveevent.MutableSingleLiveData +import com.now.naaga.util.singleliveevent.SingleLiveData import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import java.io.File +import java.io.IOException import javax.inject.Inject @HiltViewModel class UploadViewModel @Inject constructor( private val placeRepository: PlaceRepository, ) : ViewModel() { - private var imageUri: String = URI_EMPTY + private var file = FILE_EMPTY val name = MutableLiveData() @@ -32,8 +34,8 @@ class UploadViewModel @Inject constructor( private val _coordinate = MutableLiveData() val coordinate: LiveData = _coordinate - fun setUri(uri: String) { - imageUri = uri + fun setFile(file: File) { + this.file = file } fun setCoordinate(coordinate: Coordinate) { @@ -41,7 +43,7 @@ class UploadViewModel @Inject constructor( } fun isFormValid(): Boolean { - return (imageUri != URI_EMPTY) && (_coordinate.value != null) && (name.value != null) + return (file != FILE_EMPTY) && (_coordinate.value != null) && (name.value != null) } fun postPlace() { @@ -53,30 +55,29 @@ class UploadViewModel @Inject constructor( name = name.value.toString(), description = "", coordinate = coordinate, - image = imageUri, + file = file, ) }.onSuccess { _successUpload.setValue(UploadStatus.SUCCESS) }.onFailure { _successUpload.setValue(UploadStatus.FAIL) - setError(it as DataThrowable) + setThrowable(it) } } } } - private fun setError(throwable: DataThrowable) { + private fun setThrowable(throwable: Throwable) { when (throwable) { + is IOException -> { _throwable.value = DataThrowable.NetworkThrowable() } is UniversalThrowable -> _throwable.value = throwable is PlaceThrowable -> _throwable.value = throwable - else -> {} } } companion object { - const val URI_EMPTY = "EMPTY" + val FILE_EMPTY = File("") - 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/extension/ClickListenerExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/ClickListenerExt.kt new file mode 100644 index 000000000..78864b689 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/extension/ClickListenerExt.kt @@ -0,0 +1,11 @@ +package com.now.naaga.util.extension + +import com.naver.maps.map.overlay.Overlay +import com.now.naaga.util.singleclickevent.OnSingleClickListener + +fun Overlay.setOnSingleClickListener(action: (overlay: Overlay) -> Unit) { + val clickListener = OnSingleClickListener { + action(it) + } + onClickListener = clickListener +} diff --git a/android/app/src/main/java/com/now/naaga/util/extension/ContextExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/ContextExt.kt new file mode 100644 index 000000000..881eb8df3 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/extension/ContextExt.kt @@ -0,0 +1,19 @@ +package com.now.naaga.util.extension + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.widget.Toast + +fun Context.openSetting() { + val appDetailsIntent = Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.parse("package:$packageName"), + ).addCategory(Intent.CATEGORY_DEFAULT) + startActivity(appDetailsIntent) +} + +fun Context.showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} diff --git a/android/app/src/main/java/com/now/naaga/util/extension/LifeCycleOwnerExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/LifeCycleOwnerExt.kt new file mode 100644 index 000000000..985936fce --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/extension/LifeCycleOwnerExt.kt @@ -0,0 +1,14 @@ +package com.now.naaga.util.extension + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +fun LifecycleOwner.repeatOnStarted(block: suspend CoroutineScope.() -> Unit) { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED, block) + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt b/android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt similarity index 92% rename from android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt rename to android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt index 2d71bb421..ff848f9dd 100644 --- a/android/app/src/main/java/com/now/naaga/util/ResponseUtil.kt +++ b/android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt @@ -1,4 +1,4 @@ -package com.now.naaga.util +package com.now.naaga.util.extension import com.now.naaga.data.throwable.DataThrowable import org.json.JSONObject @@ -39,6 +39,7 @@ fun Response.getValueOrThrow(): T { in 300..399 -> { throw DataThrowable.PlayerThrowable(code, message) } in 400..499 -> { throw DataThrowable.GameThrowable(code, message) } in 500..599 -> { throw DataThrowable.PlaceThrowable(code, message) } + in 700..799 -> { throw DataThrowable.LetterThrowable(code, message) } } } throw DataThrowable.IllegalStateThrowable() diff --git a/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt new file mode 100644 index 000000000..bf9dcf4ca --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt @@ -0,0 +1,16 @@ +package com.now.naaga.util.extension + +import android.view.View +import com.google.android.material.snackbar.BaseTransientBottomBar.ANIMATION_MODE_SLIDE +import com.google.android.material.snackbar.Snackbar + +fun View.showSnackbar(message: String) { + Snackbar.make(this, message, Snackbar.LENGTH_SHORT).setAnimationMode(ANIMATION_MODE_SLIDE).show() +} + +fun View.showSnackbarWithEvent(message: String, actionTitle: String, action: () -> Unit) { + Snackbar.make(this, message, Snackbar.LENGTH_SHORT) + .setAction(actionTitle) { + action() + }.setAnimationMode(ANIMATION_MODE_SLIDE).show() +} diff --git a/android/app/src/main/java/com/now/naaga/util/singleclickevent/OnSingleClickListener.kt b/android/app/src/main/java/com/now/naaga/util/singleclickevent/OnSingleClickListener.kt new file mode 100644 index 000000000..3ebac78b3 --- /dev/null +++ b/android/app/src/main/java/com/now/naaga/util/singleclickevent/OnSingleClickListener.kt @@ -0,0 +1,24 @@ +package com.now.naaga.util.singleclickevent + +import com.naver.maps.map.overlay.Overlay + +class OnSingleClickListener(private val onSingleClick: (overlay: Overlay) -> Unit) : Overlay.OnClickListener { + private var lastClickTime = DEFAULT_LAST_CLICK_TIME + + override fun onClick(overlay: Overlay): Boolean { + if (isValidate()) { + onSingleClick(overlay) + } + lastClickTime = System.currentTimeMillis() + return true + } + + private fun isValidate(): Boolean { + return System.currentTimeMillis() - lastClickTime > CLICK_INTERVAL_TIME + } + + companion object { + private const val DEFAULT_LAST_CLICK_TIME = 0L + private const val CLICK_INTERVAL_TIME = 1000L + } +} diff --git a/android/app/src/main/java/com/now/naaga/util/Event.kt b/android/app/src/main/java/com/now/naaga/util/singleliveevent/Event.kt similarity index 93% rename from android/app/src/main/java/com/now/naaga/util/Event.kt rename to android/app/src/main/java/com/now/naaga/util/singleliveevent/Event.kt index 66459f691..d7c478255 100644 --- a/android/app/src/main/java/com/now/naaga/util/Event.kt +++ b/android/app/src/main/java/com/now/naaga/util/singleliveevent/Event.kt @@ -1,4 +1,4 @@ -package com.now.naaga.util +package com.now.naaga.util.singleliveevent /** * Used as a wrapper for data that is exposed via a LiveData that represents an event. diff --git a/android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt b/android/app/src/main/java/com/now/naaga/util/singleliveevent/MutableSingleLiveData.kt similarity index 87% rename from android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt rename to android/app/src/main/java/com/now/naaga/util/singleliveevent/MutableSingleLiveData.kt index bb31a719d..255b52474 100644 --- a/android/app/src/main/java/com/now/naaga/util/MutableSingleLiveData.kt +++ b/android/app/src/main/java/com/now/naaga/util/singleliveevent/MutableSingleLiveData.kt @@ -1,4 +1,4 @@ -package com.now.naaga.util +package com.now.naaga.util.singleliveevent class MutableSingleLiveData : SingleLiveData { diff --git a/android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt b/android/app/src/main/java/com/now/naaga/util/singleliveevent/SingleLiveData.kt similarity index 94% rename from android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt rename to android/app/src/main/java/com/now/naaga/util/singleliveevent/SingleLiveData.kt index 30fb3b8a5..13a131742 100644 --- a/android/app/src/main/java/com/now/naaga/util/SingleLiveData.kt +++ b/android/app/src/main/java/com/now/naaga/util/singleliveevent/SingleLiveData.kt @@ -1,4 +1,4 @@ -package com.now.naaga.util +package com.now.naaga.util.singleliveevent import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.MutableLiveData diff --git a/android/app/src/main/res/drawable/ic_camera_dialog.xml b/android/app/src/main/res/drawable/ic_camera_dialog.xml deleted file mode 100644 index 3a97119e5..000000000 --- a/android/app/src/main/res/drawable/ic_camera_dialog.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/drawable/ic_dislike_selected.xml b/android/app/src/main/res/drawable/ic_dislike_selected.xml new file mode 100644 index 000000000..b8cae650f --- /dev/null +++ b/android/app/src/main/res/drawable/ic_dislike_selected.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_dislike_unselected.xml b/android/app/src/main/res/drawable/ic_dislike_unselected.xml new file mode 100644 index 000000000..1b77d2d71 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_dislike_unselected.xml @@ -0,0 +1,19 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_letter.xml b/android/app/src/main/res/drawable/ic_letter.xml new file mode 100644 index 000000000..05421b0fd --- /dev/null +++ b/android/app/src/main/res/drawable/ic_letter.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_letter_preview.xml b/android/app/src/main/res/drawable/ic_letter_preview.xml new file mode 100644 index 000000000..4d0547d38 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_letter_preview.xml @@ -0,0 +1,14 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_message_received.xml b/android/app/src/main/res/drawable/ic_letter_received.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_message_received.xml rename to android/app/src/main/res/drawable/ic_letter_received.xml diff --git a/android/app/src/main/res/drawable/ic_letter_send.xml b/android/app/src/main/res/drawable/ic_letter_send.xml new file mode 100644 index 000000000..242306678 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_letter_send.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_like_selected.xml b/android/app/src/main/res/drawable/ic_like_selected.xml new file mode 100644 index 000000000..7dc4f37f5 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_like_selected.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_like_unselected.xml b/android/app/src/main/res/drawable/ic_like_unselected.xml new file mode 100644 index 000000000..ec1cca9ce --- /dev/null +++ b/android/app/src/main/res/drawable/ic_like_unselected.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_location_dialog.xml b/android/app/src/main/res/drawable/ic_location_dialog.xml deleted file mode 100644 index ac4979756..000000000 --- a/android/app/src/main/res/drawable/ic_location_dialog.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - diff --git a/android/app/src/main/res/drawable/ic_message_send.xml b/android/app/src/main/res/drawable/ic_message_send.xml deleted file mode 100644 index 34cbfc6c7..000000000 --- a/android/app/src/main/res/drawable/ic_message_send.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/android/app/src/main/res/drawable/rect_radius_small.xml b/android/app/src/main/res/drawable/rect_radius_small.xml index dadcaab82..c3ca54021 100644 --- a/android/app/src/main/res/drawable/rect_radius_small.xml +++ b/android/app/src/main/res/drawable/rect_radius_small.xml @@ -1,5 +1,5 @@ - - - + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/rect_red_white_radius_small.xml b/android/app/src/main/res/drawable/rect_red_white_radius_small.xml new file mode 100644 index 000000000..959ac4a2c --- /dev/null +++ b/android/app/src/main/res/drawable/rect_red_white_radius_small.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/android/app/src/main/res/drawable/selector_dislike.xml b/android/app/src/main/res/drawable/selector_dislike.xml new file mode 100644 index 000000000..25323d742 --- /dev/null +++ b/android/app/src/main/res/drawable/selector_dislike.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/drawable/selector_like.xml b/android/app/src/main/res/drawable/selector_like.xml new file mode 100644 index 000000000..ccbab49c2 --- /dev/null +++ b/android/app/src/main/res/drawable/selector_like.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml new file mode 100644 index 000000000..6eb8d23c2 --- /dev/null +++ b/android/app/src/main/res/layout/activity_adventure_detail.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_adventure_result.xml b/android/app/src/main/res/layout/activity_adventure_result.xml index f103439c9..4a4ed4a69 100644 --- a/android/app/src/main/res/layout/activity_adventure_result.xml +++ b/android/app/src/main/res/layout/activity_adventure_result.xml @@ -11,228 +11,246 @@ - - - - - - - - - - - + + - - + + + android:layout_marginTop="40dp" + android:text="@string/adventureResult_destination_description" + android:textSize="32sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - - - - - - - + + + android:layout_marginTop="12dp" + android:textSize="30sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/iv_adventureResult_photo" + tools:text="๋ฃจํ„ฐํšŒ๊ด€" /> - - - - - - - + - + + + android:layout_marginHorizontal="32dp" + android:layout_marginTop="16dp" + android:background="@drawable/rect_radius_small" + android:backgroundTint="@color/white" + android:orientation="vertical" + android:paddingHorizontal="24dp" + android:paddingVertical="16dp" + app:layout_constraintBottom_toTopOf="@id/btn_adventureResult_return" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/custom_adventureResult_preference"> - - - + + + + + + + + + + - - - - + android:paddingVertical="@dimen/space_default_medium"> - + + + + + - + + - - + + + + + + + + + + - - - - - - - - + android:paddingVertical="@dimen/space_default_medium"> + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_on_adventure.xml b/android/app/src/main/res/layout/activity_on_adventure.xml index 40dca9a64..3b5eb6524 100644 --- a/android/app/src/main/res/layout/activity_on_adventure.xml +++ b/android/app/src/main/res/layout/activity_on_adventure.xml @@ -21,71 +21,71 @@ android:id="@+id/lottie_onAdventure_loading" android:layout_width="match_parent" android:layout_height="match_parent" - android:padding="80dp" - android:elevation="4dp" android:background="@color/main_gray_opacity_medium" - app:layout_constraintTop_toTopOf="parent" + android:elevation="4dp" + android:padding="80dp" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:lottie_rawRes="@raw/walking_loading" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lottie_autoPlay="true" app:lottie_loop="true" - app:lottie_autoPlay="true" /> + app:lottie_rawRes="@raw/walking_loading" /> + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintStart_toStartOf="parent"> + android:text="@string/onAdventure_to_destination" + android:textSize="24sp" /> + android:text="@string/onAdventure_meter" + android:textSize="36sp" /> @@ -102,12 +102,12 @@ + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/space_default_medium" + android:layout_marginBottom="@dimen/space_default_medium" + app:layout_constraintBottom_toTopOf="@id/ll_onAdventure_bottom" + app:layout_constraintStart_toStartOf="parent" /> + app:layout_constraintStart_toStartOf="@id/fcv_onAdventure_map" + app:layout_constraintTop_toTopOf="@id/fcv_onAdventure_map" /> + + + app:layout_constraintStart_toStartOf="parent"> + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" /> @@ -166,42 +177,42 @@ android:id="@+id/view_onAdventure_divider_show_photo" android:layout_width="1dp" android:layout_height="match_parent" - android:background="@color/white" - android:layout_marginVertical="12dp" /> + android:layout_marginVertical="12dp" + android:background="@color/white" /> + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" /> @@ -210,41 +221,41 @@ android:id="@+id/view_onAdventure_divider_search_direction" android:layout_width="1dp" android:layout_height="match_parent" - android:background="@color/white" - android:layout_marginVertical="12dp" /> + android:layout_marginVertical="12dp" + android:background="@color/white" /> + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toTopOf="parent" /> @@ -256,20 +267,20 @@ + android:text="@string/onAdventure_on_adventure" + android:textSize="20sp" + android:visibility="@{viewModel.isNearby() ? View.GONE : View.VISIBLE}" /> + android:background="@drawable/bg_yellow_button_thick" + android:text="@string/onAdventure_end_adventure" + android:textSize="20sp" + android:visibility="@{viewModel.isNearby() ? View.VISIBLE : View.GONE}" /> diff --git a/android/app/src/main/res/layout/custom_preference_view.xml b/android/app/src/main/res/layout/custom_preference_view.xml new file mode 100644 index 000000000..026f52f74 --- /dev/null +++ b/android/app/src/main/res/layout/custom_preference_view.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_permission.xml b/android/app/src/main/res/layout/dialog_permission.xml deleted file mode 100644 index d9de24a3c..000000000 --- a/android/app/src/main/res/layout/dialog_permission.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - diff --git a/android/app/src/main/res/layout/dialog_polaroid.xml b/android/app/src/main/res/layout/dialog_polaroid.xml index 977589642..ebe4e3d22 100644 --- a/android/app/src/main/res/layout/dialog_polaroid.xml +++ b/android/app/src/main/res/layout/dialog_polaroid.xml @@ -44,7 +44,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginVertical="20dp" - android:fontFamily="@font/pretendard_semibold" android:text="@string/polaroid_description" android:textColor="@color/black" android:textSize="28sp" diff --git a/android/app/src/main/res/layout/dialog_read_letter.xml b/android/app/src/main/res/layout/dialog_read_letter.xml new file mode 100644 index 000000000..e88520043 --- /dev/null +++ b/android/app/src/main/res/layout/dialog_read_letter.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/dialog_send_letter.xml b/android/app/src/main/res/layout/dialog_send_letter.xml new file mode 100644 index 000000000..474079950 --- /dev/null +++ b/android/app/src/main/res/layout/dialog_send_letter.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_letter.xml b/android/app/src/main/res/layout/item_letter.xml new file mode 100644 index 000000000..6e2801156 --- /dev/null +++ b/android/app/src/main/res/layout/item_letter.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/item_view_pager.xml b/android/app/src/main/res/layout/item_view_pager.xml new file mode 100644 index 000000000..fc63578aa --- /dev/null +++ b/android/app/src/main/res/layout/item_view_pager.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values/attrs.xml b/android/app/src/main/res/values/attrs.xml new file mode 100644 index 000000000..586569140 --- /dev/null +++ b/android/app/src/main/res/values/attrs.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 8fede51db..24a77863a 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - Naaga + ๋‚˜์•„๊ฐ€ ์œ„์น˜ ๊ถŒํ•œ ์š”์ฒญ์ด ํ—ˆ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค @@ -10,12 +10,6 @@ ๋‚˜์˜ ๋ชจํ—˜ ์ด์–ด ํ•˜๊ธฐ - - ์„ค์ •์œผ๋กœ ์ด๋™ํ•ด์š” - ๋‚˜์•„๊ฐ€ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”\n์œ„์น˜ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•ด์ฃผ์…”์•ผ ํ•ด์š” - ๋Œ€๋žต์ ์ธ ์œ„์น˜ ๊ถŒํ•œ๋งŒ ํ—ˆ์šฉ๋˜์—ˆ์–ด์š”\n๋‚˜์•„๊ฐ€ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š”\n์ •ํ™•ํ•œ ์œ„์น˜ ๊ถŒํ•œ์„\n๋ถ€์—ฌํ•ด์ฃผ์…”์•ผ ํ•ด์š” - ์žฅ์†Œ๋ฅผ ์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•ด์„œ๋Š”\n์นด๋ฉ”๋ผ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ด์š”! - ์‚ฌ์ง„์„ ๋กœ๋”ฉ ์ค‘์ด์—์š” ๋ชฉ์ ์ง€๋ฅผ ๋ฐ›์•„์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์–ด์š”! @@ -39,6 +33,8 @@ ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š” ์ง„ํ–‰์ค‘์ธ ๊ฒŒ์ž„์— ์ž…์žฅํ–ˆ์–ด์š”! ๋’ค๋กœ๊ฐ€๊ธฐ ๋ฒ„ํŠผ์„ ํ•œ๋ฒˆ ๋” ๋ˆ„๋ฅด๋ฉด ๊ฒŒ์ž„์—์„œ ๋‚˜๊ฐ€์ ธ์š”! + ์ชฝ์ง€ ๋“ฑ๋ก์— ์„ฑ๊ณตํ–ˆ์–ด์š”! + ์ชฝ์ง€ ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์–ด์š”.. ์˜ค๋Š˜์˜ ๋ชฉ์ ์ง€ @@ -64,7 +60,6 @@ ์žฅ์†Œ๋“ฑ๋ก์— ์„ฑ๊ณตํ–ˆ์–ด์š”!\n๋ฐ˜์˜๋˜๊ธฐ๊นŒ์ง€ ์•ฝ๊ฐ„์˜ ์‹œ๊ฐ„์ด ํ•„์š”ํ•ด์š”! ์žฅ์†Œ๋“ฑ๋ก์— ์‹คํŒจํ–ˆ์–ด์š”! ์‚ฌ์ง„์„ ์ €์žฅํ•˜๋Š”๋ฐ ๋ฌธ์ œ๊ฐ€ ์ƒ๊ฒผ์–ด์š”! ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”! - ๊ทผ๋ฐฉ์— ๋‹ค๋ฅธ ๋ฏธ์…˜์žฅ์†Œ๊ฐ€ ์กด์žฌํ•ด์„œ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์—†์–ด์š”! ์ „์†ก์— ์‹คํŒจํ–ˆ์–ด์š”! ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”! ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”. @@ -120,6 +115,10 @@ naaganow@gmail.com [๋‚˜์•„๊ฐ€์—๊ฒŒ ๋ฌธ์˜ํ•˜๊ธฐ] + + ์ฝ์€ ์ชฝ์ง€ + ๋“ฑ๋กํ•œ ์ชฝ์ง€ + ์ •๋ง ํšŒ์› ํƒˆํ‡ด๋ฅผ ํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ? ๋‚˜์•„๊ฐ€์™€ ํ•จ๊ป˜ ์ฆ๊ฑฐ์šด ๋ชจํ—˜์„ ๊ณ„์†ํ•ด๋ณด์„ธ์š”! @@ -137,4 +136,19 @@ ์ƒˆ๋กœ์šด ๋ฒ„์ „์ด ์ถœ์‹œ๋˜์—ˆ์–ด์š”!\n์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•ด์š”! ์—…๋ฐ์ดํŠธ ์ทจ์†Œ + + + ์œ„์น˜ ๊ถŒํ•œ์ด ํ•„์š”ํ•ด์š”! + ์ €์žฅ์†Œ ๊ถŒํ•œ์ด ํ•„์š”ํ•ด์š”! + ์ด๋™ํ•˜๊ธฐ + ๋‹ค์‹œ ์š”์ฒญํ•ด์ฃผ์„ธ์š”! + ๋‚˜๊ฐ€๊ธฐ + + + ์ด ๊ณณ์— ๋‚ด์šฉ์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”! + ์ „์†กํ•˜๊ธฐ + + + ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”. ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”! + diff --git a/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt b/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt new file mode 100644 index 000000000..47cf95d57 --- /dev/null +++ b/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt @@ -0,0 +1,164 @@ +package com.now.naaga + +import com.now.domain.model.AdventureResult +import com.now.domain.model.Coordinate +import com.now.domain.model.Place +import com.now.domain.model.Player +import com.now.domain.model.letter.Letter +import com.now.domain.model.type.AdventureResultType +import com.now.domain.model.type.LogType +import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.LetterRepository +import com.now.naaga.presentation.adventuredetail.AdventureDetailUiState +import com.now.naaga.presentation.adventuredetail.AdventureDetailViewModel +import com.now.naaga.presentation.uimodel.mapper.toUiModel +import io.mockk.coEvery +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertSame +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.setMain +import org.junit.Before +import org.junit.Test +import java.time.LocalDateTime + +class AdventureDetailViewModelTest { + private lateinit var vm: AdventureDetailViewModel + private lateinit var letterRepository: LetterRepository + private lateinit var adventureRepository: AdventureRepository + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + letterRepository = mockk() + adventureRepository = mockk() + vm = AdventureDetailViewModel(letterRepository, adventureRepository) + } + + @Test + fun `์ฝ์€ ์ชฝ์ง€๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋ฉด AdventureDetailUiState๋Š” Loading ์ƒํƒœ๋‹ค`() { + // given + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.READ) + } coAnswers { + fakeReadLetterLogs + } + + // when + vm.fetchReadLetter(1L) + + // then + assertSame(vm.uiState.value, AdventureDetailUiState.Loading) + } + + @Test + fun `์ž‘์„ฑํ•œ ์ชฝ์ง€๋งŒ ๋ถˆ๋Ÿฌ์˜ค๋ฉด AdventureDetailUiState๋Š” Loading ์ƒํƒœ๋‹ค`() { + // given + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.WRITE) + } coAnswers { + fakeWriteLetterLogs + } + + // when + vm.fetchWriteLetter(1L) + + // then + assertSame(vm.uiState.value, AdventureDetailUiState.Loading) + } + + @Test + fun `์ฝ์€ ์ชฝ์ง€์™€ ์ž‘์„ฑํ•œ ์ชฝ์ง€๋ฅผ ๋ชจ๋‘ ๋ถˆ๋Ÿฌ์™€๋„ AdventureDetailUiState์€ Loading ์ƒํƒœ๋‹ค`() { + // given + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.WRITE) + } coAnswers { + fakeWriteLetterLogs + } + + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.READ) + } coAnswers { + fakeReadLetterLogs + } + + // when + vm.fetchWriteLetter(1L) + vm.fetchReadLetter(1L) + + // then + assertSame(vm.uiState.value, AdventureDetailUiState.Loading) + } + + @Test + fun `์ฝ์€ ์ชฝ์ง€, ์ž‘์„ฑํ•œ ์ชฝ์ง€, ๊ฒŒ์ž„ ๊ฒฐ๊ณผ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉด AdventureDetailUiState์€ Success ์ƒํƒœ๋‹ค`() { + // given + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.WRITE) + } coAnswers { + fakeWriteLetterLogs + } + + coEvery { + letterRepository.fetchLetterLogs(1L, LogType.READ) + } coAnswers { + fakeReadLetterLogs + } + + coEvery { + adventureRepository.fetchAdventureResult(1L) + } coAnswers { + fakeAdventureResult + } + + // when + vm.fetchWriteLetter(1L) + vm.fetchReadLetter(1L) + vm.fetchAdventureResult(1L) + + // then + val actual = AdventureDetailUiState.Success( + readLetters = fakeReadLetterLogs.map { it.toUiModel() }, + writeLetters = fakeWriteLetterLogs.map { it.toUiModel() }, + adventureResult = fakeAdventureResult, + ) + assertEquals(vm.uiState.value, actual) + } + + private val fakeReadLetterLogs = listOf( + Letter( + id = 1L, + player = Player(1L, "krrong", 1234), + coordinate = Coordinate(123.0, 123.0), + message = "Hello im krrong", + registerDate = "now", + ), + ) + + private val fakeWriteLetterLogs = listOf( + Letter( + id = 1L, + player = Player(1L, "notKrrong", 1212), + coordinate = Coordinate(123.0, 123.0), + message = "i was krrong", + registerDate = "now", + ), + ) + + private val fakeAdventureResult = AdventureResult( + id = 1L, + gameId = 2L, + destination = Place(1L, "์ง‘", Coordinate(123.0, 37.0), "", ""), + resultType = AdventureResultType.FAIL, + score = 123, + playTime = 123, + distance = 123, + hintUses = 123, + tryCount = 1, + beginTime = LocalDateTime.now(), + endTime = LocalDateTime.now(), + ) +} diff --git a/android/app/src/test/java/com/now/naaga/AdventureHistoryViewModelTest.kt b/android/app/src/test/java/com/now/naaga/AdventureHistoryViewModelTest.kt index 4e4ea001d..b3b4c61b7 100644 --- a/android/app/src/test/java/com/now/naaga/AdventureHistoryViewModelTest.kt +++ b/android/app/src/test/java/com/now/naaga/AdventureHistoryViewModelTest.kt @@ -2,11 +2,11 @@ package com.now.naaga import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.now.domain.model.AdventureResult -import com.now.domain.model.AdventureResultType import com.now.domain.model.Coordinate -import com.now.domain.model.OrderType import com.now.domain.model.Place -import com.now.domain.model.SortType +import com.now.domain.model.type.AdventureResultType +import com.now.domain.model.type.OrderType +import com.now.domain.model.type.SortType import com.now.domain.repository.AdventureRepository import com.now.naaga.presentation.adventurehistory.AdventureHistoryViewModel import io.mockk.coEvery diff --git a/android/app/src/test/java/com/now/naaga/AdventureResultViewModelTest.kt b/android/app/src/test/java/com/now/naaga/AdventureResultViewModelTest.kt index 84335e10e..606fa944a 100644 --- a/android/app/src/test/java/com/now/naaga/AdventureResultViewModelTest.kt +++ b/android/app/src/test/java/com/now/naaga/AdventureResultViewModelTest.kt @@ -2,12 +2,13 @@ package com.now.naaga import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.now.domain.model.AdventureResult -import com.now.domain.model.AdventureResultType import com.now.domain.model.Coordinate import com.now.domain.model.Place import com.now.domain.model.Player import com.now.domain.model.Rank +import com.now.domain.model.type.AdventureResultType import com.now.domain.repository.AdventureRepository +import com.now.domain.repository.PlaceRepository import com.now.domain.repository.RankRepository import com.now.naaga.presentation.adventureresult.AdventureResultViewModel import io.mockk.coEvery @@ -30,6 +31,7 @@ class AdventureResultViewModelTest { private lateinit var vm: AdventureResultViewModel private lateinit var adventureRepository: AdventureRepository private lateinit var rankRepository: RankRepository + private lateinit var placeRepository: PlaceRepository @get:Rule val instantExecutorRule = InstantTaskExecutorRule() @@ -104,7 +106,8 @@ class AdventureResultViewModelTest { Dispatchers.setMain(UnconfinedTestDispatcher()) adventureRepository = mockk() rankRepository = mockk() - vm = AdventureResultViewModel(adventureRepository, rankRepository) + placeRepository = mockk() + vm = AdventureResultViewModel(adventureRepository, rankRepository, placeRepository) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/android/app/src/test/java/com/now/naaga/RankViewModelTest.kt b/android/app/src/test/java/com/now/naaga/RankViewModelTest.kt index 26b9dc935..3b4b349f3 100644 --- a/android/app/src/test/java/com/now/naaga/RankViewModelTest.kt +++ b/android/app/src/test/java/com/now/naaga/RankViewModelTest.kt @@ -1,10 +1,10 @@ package com.now.naaga import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.now.domain.model.OrderType import com.now.domain.model.Player import com.now.domain.model.Rank -import com.now.domain.model.SortType +import com.now.domain.model.type.OrderType +import com.now.domain.model.type.SortType import com.now.domain.repository.RankRepository import com.now.naaga.presentation.rank.RankViewModel import io.mockk.coEvery diff --git a/android/app/src/test/java/com/now/naaga/UploadViewModelTest.kt b/android/app/src/test/java/com/now/naaga/UploadViewModelTest.kt index c2caee1c8..6e939569a 100644 --- a/android/app/src/test/java/com/now/naaga/UploadViewModelTest.kt +++ b/android/app/src/test/java/com/now/naaga/UploadViewModelTest.kt @@ -1,110 +1,110 @@ -package com.now.naaga - -import android.text.Editable -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -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.presentation.upload.UploadStatus -import com.now.naaga.presentation.upload.UploadViewModel -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.mockk -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test - -class UploadViewModelTest { - private lateinit var viewModel: UploadViewModel - private lateinit var placeRepository: PlaceRepository - - // ๋ผ์ด๋ธŒ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•˜๋„๋ก ํ•จ - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - @OptIn(ExperimentalCoroutinesApi::class) - @Before - fun setup() { - Dispatchers.setMain(UnconfinedTestDispatcher()) - placeRepository = mockk() - viewModel = UploadViewModel(placeRepository) - } - - @OptIn(ExperimentalCoroutinesApi::class) - @After - fun tearDown() { - Dispatchers.resetMain() - } - - private fun String.toEditable(): Editable { - return Editable.Factory.getInstance().newEditable(this) - } - - @Test - fun `Coordinate์ด ์ž…๋ ฅ๋˜๋ฉด ๋ทฐ๋ชจ๋ธ์˜ Coordinate๋„ ๋ฐ”๋€๋‹ค`() { - // given - val coordinate = Coordinate(123.4567, 37.890) - - // when - viewModel.setCoordinate(coordinate) - - // then - assertEquals(Coordinate(123.4567, 37.890), viewModel.coordinate.value) - } - - @Test - fun `๋ฐ์ดํ„ฐ ์ „์†ก ์„ฑ๊ณต์‹œ successUpload๊ฐ€ UploadStatus SUCCESS๋‹ค`() { - // given - val coordinate = Coordinate(123.4567, 37.890) - viewModel.setCoordinate(coordinate) - - coEvery { - placeRepository.postPlace(any(), any(), any(), any()) - } returns ( - Place( - id = 1, - name = "krrong", - coordinate = Coordinate(37.1234, 127.1234), - image = "https://img.segye.com/content/image/2021/07/29/20210729513138.jpg", - description = "android", - ) - ) - - // when - viewModel.postPlace() - - // Assert: ๊ฒฐ๊ณผ ํ™•์ธ - assertEquals(UploadStatus.SUCCESS, viewModel.successUpload.getValue()) - } - - @Test - fun `๋ฐ์ดํ„ฐ ์ „์†ก ์‹คํŒจ์‹œ successUpload๊ฐ€ UploadStatus Fail์ด๊ณ  ๋ฐ˜ํ™˜๋œ throwable์ด ์ €์žฅ๋œ๋‹ค`() { - // given - val coordinate = Coordinate(123.4567, 37.890) - viewModel.setCoordinate(coordinate) - val placeThrowable = DataThrowable.PlaceThrowable(505, "Test Failure") - coEvery { placeRepository.postPlace(any(), any(), any(), any()) } throws placeThrowable - - // when - runBlocking { viewModel.postPlace() } - - // then - // placeRepository.postPlace๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - coVerify { placeRepository.postPlace(any(), any(), any(), any()) } - - // successUpload๊ฐ€ UploadStatus.Fail ์ธ์ง€ ํ™•์ธ - assertEquals(UploadStatus.FAIL, viewModel.successUpload.getValue()) - - // ์˜๋„ํ•œ throwable์ด ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ - assertEquals(placeThrowable, viewModel.throwable.value) - } -} +package com.now.naaga + +import android.text.Editable +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +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.presentation.upload.UploadStatus +import com.now.naaga.presentation.upload.UploadViewModel +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import junit.framework.TestCase.assertEquals +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class UploadViewModelTest { + private lateinit var viewModel: UploadViewModel + private lateinit var placeRepository: PlaceRepository + + // ๋ผ์ด๋ธŒ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ๋™์ž‘ํ•˜๋„๋ก ํ•จ + @get:Rule + val instantExecutorRule = InstantTaskExecutorRule() + + @OptIn(ExperimentalCoroutinesApi::class) + @Before + fun setup() { + Dispatchers.setMain(UnconfinedTestDispatcher()) + placeRepository = mockk() + viewModel = UploadViewModel(placeRepository) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @After + fun tearDown() { + Dispatchers.resetMain() + } + + private fun String.toEditable(): Editable { + return Editable.Factory.getInstance().newEditable(this) + } + + @Test + fun `Coordinate์ด ์ž…๋ ฅ๋˜๋ฉด ๋ทฐ๋ชจ๋ธ์˜ Coordinate๋„ ๋ฐ”๋€๋‹ค`() { + // given + val coordinate = Coordinate(123.4567, 37.890) + + // when + viewModel.setCoordinate(coordinate) + + // then + assertEquals(Coordinate(123.4567, 37.890), viewModel.coordinate.value) + } + + @Test + fun `๋ฐ์ดํ„ฐ ์ „์†ก ์„ฑ๊ณต์‹œ successUpload๊ฐ€ UploadStatus SUCCESS๋‹ค`() { + // given + val coordinate = Coordinate(123.4567, 37.890) + viewModel.setCoordinate(coordinate) + + coEvery { + placeRepository.postPlace(any(), any(), any(), any()) + } returns ( + Place( + id = 1, + name = "krrong", + coordinate = Coordinate(37.1234, 127.1234), + image = "https://img.segye.com/content/image/2021/07/29/20210729513138.jpg", + description = "android", + ) + ) + + // when + viewModel.postPlace() + + // Assert: ๊ฒฐ๊ณผ ํ™•์ธ + assertEquals(UploadStatus.SUCCESS, viewModel.successUpload.getValue()) + } + + @Test + fun `๋ฐ์ดํ„ฐ ์ „์†ก ์‹คํŒจ์‹œ successUpload๊ฐ€ UploadStatus Fail์ด๊ณ  ๋ฐ˜ํ™˜๋œ throwable์ด ์ €์žฅ๋œ๋‹ค`() { + // given + val coordinate = Coordinate(123.4567, 37.890) + viewModel.setCoordinate(coordinate) + val placeThrowable = DataThrowable.PlaceThrowable(505, "Test Failure") + coEvery { placeRepository.postPlace(any(), any(), any(), any()) } throws placeThrowable + + // when + runBlocking { viewModel.postPlace() } + + // then + // placeRepository.postPlace๊ฐ€ ์‹คํ–‰๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + coVerify { placeRepository.postPlace(any(), any(), any(), any()) } + + // successUpload๊ฐ€ UploadStatus.Fail ์ธ์ง€ ํ™•์ธ + assertEquals(UploadStatus.FAIL, viewModel.successUpload.getValue()) + + // ์˜๋„ํ•œ throwable์ด ์ €์žฅ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + assertEquals(placeThrowable, viewModel.throwable.value) + } +} diff --git a/android/domain/src/main/java/com/now/domain/model/AdventureResult.kt b/android/domain/src/main/java/com/now/domain/model/AdventureResult.kt index e5ed98c67..0909f211e 100644 --- a/android/domain/src/main/java/com/now/domain/model/AdventureResult.kt +++ b/android/domain/src/main/java/com/now/domain/model/AdventureResult.kt @@ -1,5 +1,6 @@ package com.now.domain.model +import com.now.domain.model.type.AdventureResultType import java.time.LocalDateTime data class AdventureResult( diff --git a/android/domain/src/main/java/com/now/domain/model/Coordinate.kt b/android/domain/src/main/java/com/now/domain/model/Coordinate.kt index 96230681d..2dbde816b 100644 --- a/android/domain/src/main/java/com/now/domain/model/Coordinate.kt +++ b/android/domain/src/main/java/com/now/domain/model/Coordinate.kt @@ -1,3 +1,25 @@ package com.now.domain.model -data class Coordinate(val latitude: Double, val longitude: Double) +import kotlin.math.acos +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin + +data class Coordinate( + val latitude: Double, + val longitude: Double, +) { + + fun isNearBy(other: Coordinate): Boolean { + val rad = Math.PI / 180 + val radLat1 = rad * other.latitude + val radLat2 = rad * latitude + val radDist = rad * (other.longitude - longitude) + + var distance = sin(radLat1) * sin(radLat2) + distance += cos(radLat1) * cos(radLat2) * cos(radDist) + val ret = 6371000.0 * acos(distance) + + return ret.roundToInt() <= 50 + } +} diff --git a/android/domain/src/main/java/com/now/domain/model/Place.kt b/android/domain/src/main/java/com/now/domain/model/Place.kt index bef235078..798d463cb 100644 --- a/android/domain/src/main/java/com/now/domain/model/Place.kt +++ b/android/domain/src/main/java/com/now/domain/model/Place.kt @@ -29,10 +29,6 @@ data class Place( return getDistance(coordinate) <= NEARBY_STANDARD } - fun isNearBy(distance: Int): Boolean { - return distance <= NEARBY_STANDARD - } - companion object { const val NEARBY_STANDARD = 70L } diff --git a/android/domain/src/main/java/com/now/domain/model/PlatformAuth.kt b/android/domain/src/main/java/com/now/domain/model/PlatformAuth.kt index 985af91bf..aa49294e7 100644 --- a/android/domain/src/main/java/com/now/domain/model/PlatformAuth.kt +++ b/android/domain/src/main/java/com/now/domain/model/PlatformAuth.kt @@ -1,5 +1,7 @@ package com.now.domain.model +import com.now.domain.model.type.AuthPlatformType + data class PlatformAuth( val token: String, val type: AuthPlatformType, diff --git a/android/domain/src/main/java/com/now/domain/model/Preference.kt b/android/domain/src/main/java/com/now/domain/model/Preference.kt new file mode 100644 index 000000000..d1f7dd4a5 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/Preference.kt @@ -0,0 +1,33 @@ +package com.now.domain.model + +data class Preference( + val state: PreferenceState = PreferenceState.NONE, + val preState: PreferenceState = PreferenceState.NONE, + val likeCount: PreferenceCount = PreferenceCount(0), + val preLikeCount: PreferenceCount = PreferenceCount(0), +) { + fun select(selectedState: PreferenceState): Preference { + val newState = state.select(selectedState) + return Preference( + state = newState, + preState = state, + likeCount = updateCount(newState), + preLikeCount = likeCount, + ) + } + + fun revert(): Preference { + return Preference( + state = preState, + likeCount = preLikeCount, + ) + } + + private fun updateCount(newState: PreferenceState): PreferenceCount { + return when (newState) { + PreferenceState.NONE -> if (state == PreferenceState.LIKE) likeCount.minus() else likeCount + PreferenceState.LIKE -> likeCount.plus() + PreferenceState.DISLIKE -> if (state == PreferenceState.LIKE) likeCount.minus() else likeCount + } + } +} diff --git a/android/domain/src/main/java/com/now/domain/model/PreferenceCount.kt b/android/domain/src/main/java/com/now/domain/model/PreferenceCount.kt new file mode 100644 index 000000000..36077e231 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/PreferenceCount.kt @@ -0,0 +1,17 @@ +package com.now.domain.model + +@JvmInline +value class PreferenceCount(val value: Int) { + init { + require(value >= 0) { "์„ ํ˜ธ๋„ ๊ฐœ์ˆ˜๊ฐ€ 0๋ณด๋‹ค ์ž‘์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." } + } + + fun plus(): PreferenceCount { + return PreferenceCount(value + 1) + } + + fun minus(): PreferenceCount { + val newValue = if (value == 0) 0 else (value - 1) + return PreferenceCount(newValue) + } +} diff --git a/android/domain/src/main/java/com/now/domain/model/PreferenceState.kt b/android/domain/src/main/java/com/now/domain/model/PreferenceState.kt new file mode 100644 index 000000000..6e4bee096 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/PreferenceState.kt @@ -0,0 +1,23 @@ +package com.now.domain.model + +enum class PreferenceState { + NONE { + override fun select(pref: PreferenceState): PreferenceState { + return pref + } + }, + LIKE { + override fun select(pref: PreferenceState): PreferenceState { + if (pref == LIKE) return NONE + return pref + } + }, + DISLIKE { + override fun select(pref: PreferenceState): PreferenceState { + if (pref == DISLIKE) return NONE + return pref + } + }, ; + + abstract fun select(pref: PreferenceState): PreferenceState +} diff --git a/android/domain/src/main/java/com/now/domain/model/RestTryCount.kt b/android/domain/src/main/java/com/now/domain/model/RestTryCount.kt deleted file mode 100644 index 40c1d7851..000000000 --- a/android/domain/src/main/java/com/now/domain/model/RestTryCount.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.now.domain.model - -@JvmInline -value class RestTryCount(private val value: Int) { - init { - require(value >= 0) { "์ž”์—ฌ ์‹œ๋„ ํšŸ์ˆ˜๋Š” 0๋ณด๋‹ค ์ž‘์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." } - } - - fun toInt(): Int = value - - operator fun minus(value: Int): RestTryCount { - var result = this.value - value - if (result < 0) result = 0 - return RestTryCount(result) - } -} diff --git a/android/domain/src/main/java/com/now/domain/model/letter/Letter.kt b/android/domain/src/main/java/com/now/domain/model/letter/Letter.kt new file mode 100644 index 000000000..55a7dbc67 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/letter/Letter.kt @@ -0,0 +1,12 @@ +package com.now.domain.model.letter + +import com.now.domain.model.Coordinate +import com.now.domain.model.Player + +data class Letter( + val id: Long, + val player: Player, + val coordinate: Coordinate, + val message: String, + val registerDate: String, +) diff --git a/android/domain/src/main/java/com/now/domain/model/letter/LetterPreview.kt b/android/domain/src/main/java/com/now/domain/model/letter/LetterPreview.kt new file mode 100644 index 000000000..c99c98b03 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/letter/LetterPreview.kt @@ -0,0 +1,9 @@ +package com.now.domain.model.letter + +import com.now.domain.model.Coordinate + +data class LetterPreview( + val id: Long, + val coordinate: Coordinate, + val isNearBy: Boolean = false, +) diff --git a/android/domain/src/main/java/com/now/domain/model/AdventureEndType.kt b/android/domain/src/main/java/com/now/domain/model/type/AdventureEndType.kt similarity index 63% rename from android/domain/src/main/java/com/now/domain/model/AdventureEndType.kt rename to android/domain/src/main/java/com/now/domain/model/type/AdventureEndType.kt index 9648ef813..a29c3d52d 100644 --- a/android/domain/src/main/java/com/now/domain/model/AdventureEndType.kt +++ b/android/domain/src/main/java/com/now/domain/model/type/AdventureEndType.kt @@ -1,4 +1,4 @@ -package com.now.domain.model +package com.now.domain.model.type enum class AdventureEndType { ARRIVED, diff --git a/android/domain/src/main/java/com/now/domain/model/AdventureResultType.kt b/android/domain/src/main/java/com/now/domain/model/type/AdventureResultType.kt similarity index 87% rename from android/domain/src/main/java/com/now/domain/model/AdventureResultType.kt rename to android/domain/src/main/java/com/now/domain/model/type/AdventureResultType.kt index 4a951596b..b8bdaa9b0 100644 --- a/android/domain/src/main/java/com/now/domain/model/AdventureResultType.kt +++ b/android/domain/src/main/java/com/now/domain/model/type/AdventureResultType.kt @@ -1,4 +1,4 @@ -package com.now.domain.model +package com.now.domain.model.type enum class AdventureResultType { SUCCESS, diff --git a/android/domain/src/main/java/com/now/domain/model/AuthPlatformType.kt b/android/domain/src/main/java/com/now/domain/model/type/AuthPlatformType.kt similarity index 87% rename from android/domain/src/main/java/com/now/domain/model/AuthPlatformType.kt rename to android/domain/src/main/java/com/now/domain/model/type/AuthPlatformType.kt index 03c12656b..23fa13489 100644 --- a/android/domain/src/main/java/com/now/domain/model/AuthPlatformType.kt +++ b/android/domain/src/main/java/com/now/domain/model/type/AuthPlatformType.kt @@ -1,4 +1,4 @@ -package com.now.domain.model +package com.now.domain.model.type enum class AuthPlatformType { KAKAO, diff --git a/android/domain/src/main/java/com/now/domain/model/type/LogType.kt b/android/domain/src/main/java/com/now/domain/model/type/LogType.kt new file mode 100644 index 000000000..bf7d6427d --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/model/type/LogType.kt @@ -0,0 +1,6 @@ +package com.now.domain.model.type + +enum class LogType { + READ, + WRITE, +} diff --git a/android/domain/src/main/java/com/now/domain/model/OrderType.kt b/android/domain/src/main/java/com/now/domain/model/type/OrderType.kt similarity index 62% rename from android/domain/src/main/java/com/now/domain/model/OrderType.kt rename to android/domain/src/main/java/com/now/domain/model/type/OrderType.kt index 3aed423e8..7660bfb29 100644 --- a/android/domain/src/main/java/com/now/domain/model/OrderType.kt +++ b/android/domain/src/main/java/com/now/domain/model/type/OrderType.kt @@ -1,4 +1,4 @@ -package com.now.domain.model +package com.now.domain.model.type enum class OrderType { ASCENDING, diff --git a/android/domain/src/main/java/com/now/domain/model/SortType.kt b/android/domain/src/main/java/com/now/domain/model/type/SortType.kt similarity index 56% rename from android/domain/src/main/java/com/now/domain/model/SortType.kt rename to android/domain/src/main/java/com/now/domain/model/type/SortType.kt index c4b3400f0..338bb472b 100644 --- a/android/domain/src/main/java/com/now/domain/model/SortType.kt +++ b/android/domain/src/main/java/com/now/domain/model/type/SortType.kt @@ -1,4 +1,4 @@ -package com.now.domain.model +package com.now.domain.model.type enum class SortType { TIME, diff --git a/android/domain/src/main/java/com/now/domain/repository/AdventureRepository.kt b/android/domain/src/main/java/com/now/domain/repository/AdventureRepository.kt index 9ce2746c1..d00538887 100644 --- a/android/domain/src/main/java/com/now/domain/repository/AdventureRepository.kt +++ b/android/domain/src/main/java/com/now/domain/repository/AdventureRepository.kt @@ -1,13 +1,13 @@ package com.now.domain.repository import com.now.domain.model.Adventure -import com.now.domain.model.AdventureEndType import com.now.domain.model.AdventureResult import com.now.domain.model.AdventureStatus import com.now.domain.model.Coordinate import com.now.domain.model.Hint -import com.now.domain.model.OrderType -import com.now.domain.model.SortType +import com.now.domain.model.type.AdventureEndType +import com.now.domain.model.type.OrderType +import com.now.domain.model.type.SortType interface AdventureRepository { suspend fun fetchMyAdventures(): List diff --git a/android/domain/src/main/java/com/now/domain/repository/LetterRepository.kt b/android/domain/src/main/java/com/now/domain/repository/LetterRepository.kt new file mode 100644 index 000000000..053cb1733 --- /dev/null +++ b/android/domain/src/main/java/com/now/domain/repository/LetterRepository.kt @@ -0,0 +1,19 @@ +package com.now.domain.repository + +import com.now.domain.model.letter.Letter +import com.now.domain.model.letter.LetterPreview +import com.now.domain.model.type.LogType + +interface LetterRepository { + suspend fun postLetter( + message: String, + latitude: Double, + longitude: Double, + ): Letter + + suspend fun fetchNearbyLetters(latitude: Double, longitude: Double): List + + suspend fun fetchLetter(letterId: Long): Letter + + suspend fun fetchLetterLogs(gameId: Long, logType: LogType): List +} diff --git a/android/domain/src/main/java/com/now/domain/repository/PlaceRepository.kt b/android/domain/src/main/java/com/now/domain/repository/PlaceRepository.kt index 46763c496..8d9113cef 100644 --- a/android/domain/src/main/java/com/now/domain/repository/PlaceRepository.kt +++ b/android/domain/src/main/java/com/now/domain/repository/PlaceRepository.kt @@ -2,6 +2,8 @@ package com.now.domain.repository import com.now.domain.model.Coordinate import com.now.domain.model.Place +import com.now.domain.model.PreferenceState +import java.io.File interface PlaceRepository { suspend fun fetchMyPlaces( @@ -17,6 +19,11 @@ interface PlaceRepository { name: String, description: String, coordinate: Coordinate, - image: String, + file: File, ): Place + + suspend fun postPreference(placeId: Int, preferenceState: PreferenceState): PreferenceState + suspend fun getMyPreference(placeId: Int): PreferenceState + suspend fun deletePreference(placeId: Int) + suspend fun getLikeCount(placeId: Int): Int } diff --git a/android/domain/src/test/java/com/now/domain/model/ClosedLetterTest.kt b/android/domain/src/test/java/com/now/domain/model/ClosedLetterTest.kt new file mode 100644 index 000000000..cf8afd130 --- /dev/null +++ b/android/domain/src/test/java/com/now/domain/model/ClosedLetterTest.kt @@ -0,0 +1,45 @@ +package com.now.domain.model + +import com.now.domain.model.letter.LetterPreview +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +internal class ClosedLetterTest { + private lateinit var myCoordinate: Coordinate + private lateinit var closeLetter: LetterPreview + private lateinit var distantLetter: LetterPreview + + @BeforeEach + fun setUp() { + myCoordinate = Coordinate(37.549335, 127.075816) + closeLetter = LetterPreview(1, Coordinate(37.549369, 127.076357)) + distantLetter = LetterPreview(2, Coordinate(37.549305, 127.077007)) + } + + @Test + fun `๋‚ด ์œ„์น˜์™€ ์ชฝ์ง€์™€์˜ ๊ฑฐ๋ฆฌ๊ฐ€ 50m ๋ณด๋‹ค ํฌ๋ฉด ๊ฐ’์ด false ์ด๋‹ค`() { + // given + val currentMyCoordinate = myCoordinate + val letter = distantLetter + + // when + letter.coordinate.isNearBy(currentMyCoordinate) + + // then + Assertions.assertThat(letter.isNearBy).isFalse + } + + @Test + fun `๋‚ด ์œ„์น˜์™€ ์ชฝ์ง€์™€์˜ ๊ฑฐ๋ฆฌ๊ฐ€ 50m ๋ณด๋‹ค ํฌ๋ฉด ๊ฐ’์ด true ์ด๋‹ค`() { + // given + val currentMyCoordinate = myCoordinate + val letter = closeLetter + + // when + letter.coordinate.isNearBy(currentMyCoordinate) + + // then + Assertions.assertThat(letter.isNearBy).isTrue + } +} diff --git a/android/gradlew.bat b/android/gradlew.bat index ac1b06f93..107acd32c 100644 --- a/android/gradlew.bat +++ b/android/gradlew.bat @@ -1,89 +1,89 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/gradlew.bat b/backend/gradlew.bat index 6689b85be..93e3f59f1 100644 --- a/backend/gradlew.bat +++ b/backend/gradlew.bat @@ -1,92 +1,92 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/backend/src/main/java/com/now/naaga/common/domain/BaseEntity.java b/backend/src/main/java/com/now/naaga/common/domain/BaseEntity.java index 4ee21d732..faa556bda 100644 --- a/backend/src/main/java/com/now/naaga/common/domain/BaseEntity.java +++ b/backend/src/main/java/com/now/naaga/common/domain/BaseEntity.java @@ -16,6 +16,7 @@ public abstract class BaseEntity { @Column(nullable = false, updatable = false) private LocalDateTime createdAt; + // TODO: 10/19/23 ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ์–ด๋””ํŒ… ์•ˆ๋˜๋Š” ๊ฒƒ ์ˆ˜์ • ํ•„์š” @LastModifiedBy private LocalDateTime updatedAt; diff --git a/backend/src/main/resources/security b/backend/src/main/resources/security index a262f1344..6aea9836a 160000 --- a/backend/src/main/resources/security +++ b/backend/src/main/resources/security @@ -1 +1 @@ -Subproject commit a262f1344e60f0f982a84684ff7de180494237cf +Subproject commit 6aea9836ab385f1eee8d5075f6dd98e800ff158e diff --git a/etc/images/google play store.png b/etc/images/google play store.png new file mode 100644 index 000000000..6313ad11d Binary files /dev/null and b/etc/images/google play store.png differ diff --git a/etc/images/header.png b/etc/images/header.png new file mode 100644 index 000000000..d637cedaa Binary files /dev/null and b/etc/images/header.png differ diff --git a/etc/images/service intro.png b/etc/images/service intro.png new file mode 100644 index 000000000..edd044876 Binary files /dev/null and b/etc/images/service intro.png differ