diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index aa08a8a71..798fb8e3a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -70,6 +70,10 @@
android:name=".presentation.setting.SettingActivity"
android:screenOrientation="portrait"
android:exported="false" />
+
diff --git a/android/app/src/main/java/com/now/naaga/data/mapper/OpenLetterMapper.kt b/android/app/src/main/java/com/now/naaga/data/mapper/OpenLetterMapper.kt
new file mode 100644
index 000000000..f21120598
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/data/mapper/OpenLetterMapper.kt
@@ -0,0 +1,14 @@
+package com.now.naaga.data.mapper
+
+import com.now.domain.model.letter.OpenLetter
+import com.now.naaga.data.remote.dto.OpenLetterDto
+
+fun OpenLetterDto.toDomain(): OpenLetter {
+ return OpenLetter(
+ id = id,
+ player = player.toDomain(),
+ coordinate = coordinateDto.toDomain(),
+ message = message,
+ registerDate = registerDate,
+ )
+}
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
index b11e8ee1b..558325544 100644
--- 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
@@ -25,6 +25,6 @@ class DefaultLetterRepository(
}
override suspend fun fetchLetterLogs(gameId: Long, logType: LogType): List {
- TODO("Not yet implemented")
+ return letterService.getInGameLetters(gameId, logType.name).getValueOrThrow().map { it.toDomain() }
}
}
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 a3beebd37..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,6 +19,9 @@ 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)
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..950a6700c
--- /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.OpenLetterUiModel
+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..722897e00
--- /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.OpenLetterUiModel
+
+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..74cff1dd1
--- /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.OpenLetter
+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.OpenLetterUiModel
+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(OpenLetterUiModel.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..02c530369
--- /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.OpenLetterUiModel
+
+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..59397c036
--- /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.OpenLetterUiModel
+
+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: OpenLetterUiModel) {
+ 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..bc798a627
--- /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.OpenLetterUiModel
+
+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..32f82fd29
--- /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.OpenLetterUiModel
+
+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 075036120..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
@@ -9,6 +9,7 @@ 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
@@ -17,7 +18,7 @@ import dagger.hilt.android.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)
@@ -58,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/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 3f4b0768b..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
@@ -9,7 +9,19 @@ 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/uimodel/mapper/OpenLetterMapper.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/OpenLetterMapper.kt
new file mode 100644
index 000000000..9ac140c3b
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/OpenLetterMapper.kt
@@ -0,0 +1,12 @@
+package com.now.naaga.presentation.uimodel.mapper
+
+import com.now.domain.model.letter.OpenLetter
+import com.now.naaga.presentation.uimodel.model.OpenLetterUiModel
+
+fun OpenLetter.toUiModel(): OpenLetterUiModel {
+ return OpenLetterUiModel(
+ nickname = player.nickname,
+ registerDate = registerDate,
+ message = message,
+ )
+}
diff --git a/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/OpenLetterUiModel.kt b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/OpenLetterUiModel.kt
new file mode 100644
index 000000000..65b21dc63
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/OpenLetterUiModel.kt
@@ -0,0 +1,12 @@
+package com.now.naaga.presentation.uimodel.model
+
+data class OpenLetterUiModel(
+ val nickname: String,
+ val registerDate: String,
+ val message: String,
+) {
+ companion object {
+ private const val DEFAULT_MESSAGE = "쪽지가 없습니다."
+ val DEFAULT_OPEN_LETTER = OpenLetterUiModel("", "", DEFAULT_MESSAGE)
+ }
+}
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/extension/ResponseExt.kt b/android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt
index 6a395d35b..ff848f9dd 100644
--- a/android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt
+++ b/android/app/src/main/java/com/now/naaga/util/extension/ResponseExt.kt
@@ -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
index 13ba67de4..a39c872bf 100644
--- a/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt
+++ b/android/app/src/main/java/com/now/naaga/util/extension/ViewExt.kt
@@ -10,3 +10,7 @@ fun View.showSnackbarWithEvent(message: String, actionTitle: String, action: ()
action()
}.setAnimationMode(ANIMATION_MODE_SLIDE).show()
}
+
+fun View.showSnackbar(message: String) {
+ Snackbar.make(this, message, Snackbar.LENGTH_SHORT).setAnimationMode(ANIMATION_MODE_SLIDE).show()
+}
diff --git a/android/app/src/main/res/drawable/rect_red_white_radius_small.xml b/android/app/src/main/res/drawable/rect_red_white_radius_small.xml
index 779dcce22..959ac4a2c 100644
--- 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
@@ -1,6 +1,6 @@
-
+
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/item_letter.xml b/android/app/src/main/res/layout/item_letter.xml
new file mode 100644
index 000000000..9fc2d81a5
--- /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/strings.xml b/android/app/src/main/res/values/strings.xml
index 127eb9333..981df49ef 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -113,6 +113,10 @@
naaganow@gmail.com
[나아가에게 문의하기]
+
+ 읽은 쪽지
+ 등록한 쪽지
+
정말 회원 탈퇴를 하시겠습니까?
나아가와 함께 즐거운 모험을 계속해보세요!
@@ -135,6 +139,8 @@
위치 권한이 필요해요!
저장소 권한이 필요해요!
이동하기
+ 다시 요청해주세요!
+ 나가기
이 곳에 내용을 작성해주세요!
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..5c73f192c
--- /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.OpenLetter
+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(
+ OpenLetter(
+ id = 1L,
+ player = Player(1L, "krrong", 1234),
+ coordinate = Coordinate(123.0, 123.0),
+ message = "Hello im krrong",
+ registerDate = "now",
+ ),
+ )
+
+ private val fakeWriteLetterLogs = listOf(
+ OpenLetter(
+ 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(),
+ )
+}