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