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(), + ) +}