From 520a766c7a3fdbbea7add6b23742d90e147e9fbd Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Mon, 16 Oct 2023 22:12:24 +0900
Subject: [PATCH 01/24] =?UTF-8?q?feat:=20OpenLetter=20=EB=A7=A4=ED=8D=BC?=
 =?UTF-8?q?=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../com/now/naaga/data/mapper/OpenLetterMapper.kt  | 14 ++++++++++++++
 1 file changed, 14 insertions(+)
 create mode 100644 android/app/src/main/java/com/now/naaga/data/mapper/OpenLetterMapper.kt

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

From a8912df8f9fcdb0ce5d7a782f37e034f94d7015b Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Mon, 16 Oct 2023 22:25:00 +0900
Subject: [PATCH 02/24] =?UTF-8?q?feat:=20AdventureDetailViewModel=20?=
 =?UTF-8?q?=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../repository/DefaultLetterRepository.kt     |  2 +-
 .../AdventureDetailViewModel.kt               | 51 +++++++++++++++++++
 2 files changed, 52 insertions(+), 1 deletion(-)
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt

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<OpenLetter> {
-        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/presentation/adventuredetail/AdventureDetailViewModel.kt b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt
new file mode 100644
index 000000000..b52405f26
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailViewModel.kt
@@ -0,0 +1,51 @@
+package com.now.naaga.presentation.adventuredetail
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.now.domain.model.letter.OpenLetter
+import com.now.domain.model.type.LogType
+import com.now.domain.repository.LetterRepository
+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
+class AdventureDetailViewModel @Inject constructor(private val letterRepository: LetterRepository) : ViewModel() {
+    private val _readLetters = MutableLiveData<List<OpenLetter>>()
+    val readLetters: LiveData<List<OpenLetter>> = _readLetters
+
+    private val _writeLetters = MutableLiveData<List<OpenLetter>>()
+    val writeLetters: LiveData<List<OpenLetter>> = _writeLetters
+
+    private val _throwable = MutableLiveData<DataThrowable>()
+    val throwable: LiveData<DataThrowable> = _throwable
+
+    fun fetchLetterLogs(gameId: Long, logType: LogType) {
+        viewModelScope.launch {
+            runCatching {
+                letterRepository.fetchLetterLogs(gameId, logType)
+            }.onSuccess {
+                setLetters(logType, it)
+            }.onFailure {
+                setThrowable(it)
+            }
+        }
+    }
+
+    private fun setLetters(logType: LogType, letters: List<OpenLetter>) {
+        when (logType) {
+            LogType.READ -> _readLetters.value = letters
+            LogType.WRITE -> _writeLetters.value = letters
+        }
+    }
+
+    private fun setThrowable(throwable: Throwable) {
+        when (throwable) {
+            is IOException -> { TODO("_throwable.value = DataThrowable.NetworkThrowable") }
+        }
+    }
+}

From 04cc1a9ae95c1db0bfb9c05cb977a70f5bc367c7 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Mon, 16 Oct 2023 22:25:11 +0900
Subject: [PATCH 03/24] =?UTF-8?q?test:=20AdventureDetailViewModel=20?=
 =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../now/naaga/AdventureDetailViewModelTest.kt | 92 +++++++++++++++++++
 1 file changed, 92 insertions(+)
 create mode 100644 android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt

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..4ba7f38c4
--- /dev/null
+++ b/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt
@@ -0,0 +1,92 @@
+package com.now.naaga
+
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import com.now.domain.model.Coordinate
+import com.now.domain.model.Player
+import com.now.domain.model.letter.OpenLetter
+import com.now.domain.model.type.LogType
+import com.now.domain.repository.LetterRepository
+import com.now.naaga.presentation.adventuredetail.AdventureDetailViewModel
+import io.mockk.coEvery
+import io.mockk.mockk
+import junit.framework.TestCase.assertEquals
+import junit.framework.TestCase.assertTrue
+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.Rule
+import org.junit.Test
+
+class AdventureDetailViewModelTest {
+    private lateinit var vm: AdventureDetailViewModel
+    private lateinit var letterRepository: LetterRepository
+
+    @get:Rule
+    val instantExecutorRule = InstantTaskExecutorRule()
+
+    @OptIn(ExperimentalCoroutinesApi::class)
+    @Before
+    fun setup() {
+        Dispatchers.setMain(UnconfinedTestDispatcher())
+        letterRepository = mockk()
+        vm = AdventureDetailViewModel(letterRepository)
+    }
+
+    @Test
+    fun `읽은 쪽지를 불러올 때 작성한 쪽지는 불러오지 않는다`() {
+        // given
+        coEvery {
+            letterRepository.fetchLetterLogs(1L, LogType.READ)
+        } coAnswers {
+            fakeReadLetterLogs
+        }
+
+        // when
+        vm.fetchLetterLogs(1L, LogType.READ)
+
+        // then
+        assertTrue(vm.readLetters.isInitialized)
+        assertEquals(vm.readLetters.getOrAwaitValue(), fakeReadLetterLogs)
+        assertEquals(vm.writeLetters.isInitialized, false)
+    }
+
+    @Test
+    fun `작성한 쪽지를 불러올 때 읽은 쪽지는 불러오지 않는다`() {
+        // given
+        coEvery {
+            letterRepository.fetchLetterLogs(1L, LogType.WRITE)
+        } coAnswers {
+            fakeWriteLetterLogs
+        }
+
+        // when
+        vm.fetchLetterLogs(1L, LogType.WRITE)
+
+        // then
+        assertEquals(vm.readLetters.isInitialized, false)
+        assertTrue(vm.writeLetters.isInitialized)
+        assertEquals(vm.writeLetters.getOrAwaitValue(), fakeWriteLetterLogs)
+    }
+
+    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",
+        ),
+    )
+}

From 82c3344e4770f0fa9349955faf14f7949033064d Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Mon, 16 Oct 2023 22:28:12 +0900
Subject: [PATCH 04/24] =?UTF-8?q?feat:=20LetterThrowable=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../main/java/com/now/naaga/data/throwable/DataThrowable.kt    | 3 +++
 .../presentation/adventuredetail/AdventureDetailViewModel.kt   | 1 +
 .../src/main/java/com/now/naaga/util/extension/ResponseExt.kt  | 1 +
 3 files changed, 5 insertions(+)

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..dfec68f37 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)
+
     companion object {
         const val ILLEGAL_STATE_THROWABLE_CODE = 900
         const val ILLEGAL_STATE_THROWABLE_MESSAGE = "잘못된 값입니다."
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
index b52405f26..8654465d3 100644
--- 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
@@ -46,6 +46,7 @@ class AdventureDetailViewModel @Inject constructor(private val letterRepository:
     private fun setThrowable(throwable: Throwable) {
         when (throwable) {
             is IOException -> { TODO("_throwable.value = DataThrowable.NetworkThrowable") }
+            is DataThrowable.LetterThrowable -> { _throwable.value = throwable }
         }
     }
 }
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 <T> Response<T>.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()

From 81c7c24b19d06526e15258287d76a21951f181d2 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:31:48 +0900
Subject: [PATCH 05/24] =?UTF-8?q?feat:=20=EC=9D=BD=EC=9D=80=20=ED=8E=B8?=
 =?UTF-8?q?=EC=A7=80,=20=EB=93=B1=EB=A1=9D=ED=95=9C=20=ED=8E=B8=EC=A7=80?=
 =?UTF-8?q?=EB=A5=BC=20=EC=9D=BD=EC=9D=84=20=EC=88=98=20=EC=9E=88=EB=8A=94?=
 =?UTF-8?q?=20=EB=B7=B0=ED=8E=98=EC=9D=B4=EC=A0=80=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                | 28 ++++++++
 .../recyclerview/LetterAdapter.kt             | 19 +++++
 .../recyclerview/LetterViewHolder.kt          | 20 ++++++
 .../viewpager/ViewPagerAdapter.kt             | 19 +++++
 .../viewpager/ViewPagerHolder.kt              | 19 +++++
 .../uimodel/mapper/OpenLetterMapper.kt        | 12 ++++
 .../uimodel/model/OpenLetterUiModel.kt        |  7 ++
 .../res/layout/activity_adventure_detail.xml  | 71 +++++++++++++++++++
 .../app/src/main/res/layout/item_letter.xml   | 58 +++++++++++++++
 .../src/main/res/layout/item_view_pager.xml   | 27 +++++++
 10 files changed, 280 insertions(+)
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterAdapter.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/recyclerview/LetterViewHolder.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerAdapter.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/viewpager/ViewPagerHolder.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/uimodel/mapper/OpenLetterMapper.kt
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/uimodel/model/OpenLetterUiModel.kt
 create mode 100644 android/app/src/main/res/layout/activity_adventure_detail.xml
 create mode 100644 android/app/src/main/res/layout/item_letter.xml
 create mode 100644 android/app/src/main/res/layout/item_view_pager.xml

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..b5d9e2aac
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailActivity.kt
@@ -0,0 +1,28 @@
+package com.now.naaga.presentation.adventuredetail
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import com.now.naaga.databinding.ActivityAdventureDetailBinding
+import com.now.naaga.presentation.adventuredetail.viewpager.ViewPagerAdapter
+import com.now.naaga.presentation.uimodel.model.OpenLetterUiModel
+
+class AdventureDetailActivity : AppCompatActivity() {
+    private lateinit var binding: ActivityAdventureDetailBinding
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        binding = ActivityAdventureDetailBinding.inflate(layoutInflater)
+        setContentView(binding.root)
+
+        initViewPager()
+    }
+
+    private fun initViewPager() {
+        binding.vpAdventureDetail.adapter = ViewPagerAdapter(
+            listOf(
+                List(10) { OpenLetterUiModel("krrong", "today", "meesage") },
+                List(10) { OpenLetterUiModel("krrong", "today", "meesage") },
+            ),
+        )
+    }
+}
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<OpenLetterUiModel>) : RecyclerView.Adapter<LetterViewHolder>() {
+    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<List<OpenLetterUiModel>>) : RecyclerView.Adapter<ViewPagerHolder>() {
+    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<OpenLetterUiModel>) {
+        binding.rvItemViewPager.adapter = LetterAdapter(data)
+    }
+}
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..08a286b89
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/uimodel/model/OpenLetterUiModel.kt
@@ -0,0 +1,7 @@
+package com.now.naaga.presentation.uimodel.model
+
+data class OpenLetterUiModel(
+    val nickname: String,
+    val registerDate: String,
+    val message: String,
+)
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..56a425b8a
--- /dev/null
+++ b/android/app/src/main/res/layout/activity_adventure_detail.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:context=".presentation.adventuredetail.AdventureDetailActivity">
+
+        <ImageView
+            android:id="@+id/iv_adventure_detail_back"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginStart="8dp"
+            android:layout_marginTop="16dp"
+            android:paddingHorizontal="20dp"
+            android:paddingVertical="@dimen/space_default_medium"
+            android:src="@drawable/ic_arrow"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+        <TextView
+            android:id="@+id/tv_adventure_detail_place_title"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="루터회관"
+            android:textSize="28sp"
+            app:layout_constraintBottom_toBottomOf="@id/iv_adventure_detail_back"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="@id/iv_adventure_detail_back" />
+
+        <ImageView
+            android:id="@+id/iv_adventure_detail_photo"
+            android:layout_width="250dp"
+            android:layout_height="250dp"
+            android:layout_marginTop="@dimen/space_default_large"
+            android:background="@drawable/rect_radius_small"
+            android:scaleType="centerCrop"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/tv_adventure_detail_place_title"
+            app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"
+            tools:srcCompat="@tools:sample/avatars" />
+
+<!--        <com.google.android.material.tabs.TabLayout-->
+<!--            android:id="@+id/tl_adventure_detail"-->
+<!--            android:layout_width="0dp"-->
+<!--            android:layout_height="wrap_content"-->
+<!--            app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"-->
+<!--            app:layout_constraintEnd_toEndOf="parent"-->
+<!--            app:layout_constraintStart_toStartOf="parent"-->
+<!--            app:layout_constraintTop_toBottomOf="@id/iv_adventure_detail_photo" />-->
+
+        <androidx.viewpager2.widget.ViewPager2
+            android:id="@+id/vp_adventure_detail"
+            android:layout_width="0dp"
+            android:layout_height="0dp"
+            android:orientation="horizontal"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/iv_adventure_detail_photo" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginHorizontal="@dimen/space_default_large"
+        android:layout_marginTop="@dimen/space_default_medium"
+        android:background="@drawable/rect_red_white_radius_small">
+
+        <TextView
+            android:id="@+id/tv_item_letter_nickname"
+            android:layout_width="0dp"
+            android:layout_height="20dp"
+            android:layout_marginStart="@dimen/space_default_large"
+            android:layout_marginTop="@dimen/space_default_medium"
+            android:textColor="@color/main_dark_blue"
+            android:textSize="12sp"
+            app:layout_constraintEnd_toStartOf="@id/tv_item_letter_register_date"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="krrong" />
+
+        <TextView
+            android:id="@+id/tv_item_letter_register_date"
+            android:layout_width="0dp"
+            android:layout_height="20dp"
+            android:layout_marginTop="@dimen/space_default_medium"
+            android:layout_marginEnd="@dimen/space_default_large"
+            android:gravity="end"
+            android:textColor="@color/main_dark_blue"
+            android:textSize="12sp"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toEndOf="@id/tv_item_letter_nickname"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:text="2023-07-03" />
+
+        <TextView
+            android:id="@+id/tv_item_letter"
+            android:layout_width="0dp"
+            android:layout_height="40dp"
+            android:layout_marginBottom="@dimen/space_default_large"
+            android:gravity="center"
+            android:textColor="@color/main_dark_blue"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="@id/tv_item_letter_register_date"
+            app:layout_constraintStart_toStartOf="@id/tv_item_letter_nickname"
+            app:layout_constraintTop_toBottomOf="@id/tv_item_letter_nickname"
+            tools:text="여기 뷰 미쳤다!" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>
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 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <data>
+
+    </data>
+
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/rv_item_view_pager"
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
+            android:orientation="vertical"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="parent"
+            app:layout_constraintTop_toTopOf="parent"
+            tools:listitem="@layout/item_letter" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

From 96c762f86ac64fd016e5dafca8c3df84d59939a8 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:36:56 +0900
Subject: [PATCH 06/24] =?UTF-8?q?feat:=20=EB=B7=B0=ED=8E=98=EC=9D=B4?=
 =?UTF-8?q?=EC=A0=80,=20=ED=83=AD=EB=A0=88=EC=9D=B4=EC=95=84=EC=9B=83=20?=
 =?UTF-8?q?=EC=97=B0=EA=B2=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                |  8 ++++++++
 .../res/layout/activity_adventure_detail.xml  | 20 +++++++++----------
 2 files changed, 18 insertions(+), 10 deletions(-)

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
index b5d9e2aac..1e74ab32c 100644
--- 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
@@ -2,6 +2,7 @@ package com.now.naaga.presentation.adventuredetail
 
 import android.os.Bundle
 import androidx.appcompat.app.AppCompatActivity
+import com.google.android.material.tabs.TabLayoutMediator
 import com.now.naaga.databinding.ActivityAdventureDetailBinding
 import com.now.naaga.presentation.adventuredetail.viewpager.ViewPagerAdapter
 import com.now.naaga.presentation.uimodel.model.OpenLetterUiModel
@@ -24,5 +25,12 @@ class AdventureDetailActivity : AppCompatActivity() {
                 List(10) { OpenLetterUiModel("krrong", "today", "meesage") },
             ),
         )
+
+        TabLayoutMediator(binding.tlAdventureDetail, binding.vpAdventureDetail) { tab, position ->
+            when (position) {
+                0 -> tab.text = "읽은 편지"
+                1 -> tab.text = "등록한 편지"
+            }
+        }.attach()
     }
 }
diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml
index 56a425b8a..c7e8b403a 100644
--- a/android/app/src/main/res/layout/activity_adventure_detail.xml
+++ b/android/app/src/main/res/layout/activity_adventure_detail.xml
@@ -45,17 +45,17 @@
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/tv_adventure_detail_place_title"
-            app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"
+            app:layout_constraintBottom_toTopOf="@id/tl_adventure_detail"
             tools:srcCompat="@tools:sample/avatars" />
 
-<!--        <com.google.android.material.tabs.TabLayout-->
-<!--            android:id="@+id/tl_adventure_detail"-->
-<!--            android:layout_width="0dp"-->
-<!--            android:layout_height="wrap_content"-->
-<!--            app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"-->
-<!--            app:layout_constraintEnd_toEndOf="parent"-->
-<!--            app:layout_constraintStart_toStartOf="parent"-->
-<!--            app:layout_constraintTop_toBottomOf="@id/iv_adventure_detail_photo" />-->
+        <com.google.android.material.tabs.TabLayout
+            android:id="@+id/tl_adventure_detail"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toBottomOf="@id/iv_adventure_detail_photo" />
 
         <androidx.viewpager2.widget.ViewPager2
             android:id="@+id/vp_adventure_detail"
@@ -65,7 +65,7 @@
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toBottomOf="@id/iv_adventure_detail_photo" />
+            app:layout_constraintTop_toBottomOf="@id/tl_adventure_detail" />
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 </layout>

From f05ca63efb68d23736e1cbfa07ac04fed0c63652 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:39:25 +0900
Subject: [PATCH 07/24] =?UTF-8?q?feat:=20radius=EB=A5=BC=20=ED=8C=8C?=
 =?UTF-8?q?=EC=9D=BC=EB=AA=85=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80?=
 =?UTF-8?q?=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../app/src/main/res/drawable/rect_red_white_radius_small.xml   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 @@
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
     android:shape="rectangle">
-    <corners android:radius="@dimen/radius_medium" />
+    <corners android:radius="@dimen/radius_small" />
     <solid android:color="@color/red_white" />
 </shape>

From aa544f973fd1ae39934850c78f43de9c4281ded7 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:40:37 +0900
Subject: [PATCH 08/24] =?UTF-8?q?refactor:=20=EB=B7=B0=20=EB=A7=88?=
 =?UTF-8?q?=EC=A7=84,=20=EB=B0=B0=EA=B2=BD=20=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 android/app/src/main/res/layout/activity_adventure_detail.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml
index c7e8b403a..1696af176 100644
--- a/android/app/src/main/res/layout/activity_adventure_detail.xml
+++ b/android/app/src/main/res/layout/activity_adventure_detail.xml
@@ -42,16 +42,18 @@
             android:layout_marginTop="@dimen/space_default_large"
             android:background="@drawable/rect_radius_small"
             android:scaleType="centerCrop"
+            app:layout_constraintBottom_toTopOf="@id/tl_adventure_detail"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
             app:layout_constraintTop_toBottomOf="@id/tv_adventure_detail_place_title"
-            app:layout_constraintBottom_toTopOf="@id/tl_adventure_detail"
             tools:srcCompat="@tools:sample/avatars" />
 
         <com.google.android.material.tabs.TabLayout
             android:id="@+id/tl_adventure_detail"
             android:layout_width="0dp"
             android:layout_height="wrap_content"
+            android:layout_marginHorizontal="4dp"
+            android:background="@drawable/rect_red_white_radius_small"
             app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"

From 3e30f2b0efae7e6d058e07937d594d1f25037f16 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:41:18 +0900
Subject: [PATCH 09/24] =?UTF-8?q?refactor:=20text=EB=A5=BC=20tools?=
 =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 android/app/src/main/res/layout/activity_adventure_detail.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml
index 1696af176..d8c4f7ae5 100644
--- a/android/app/src/main/res/layout/activity_adventure_detail.xml
+++ b/android/app/src/main/res/layout/activity_adventure_detail.xml
@@ -28,12 +28,12 @@
             android:id="@+id/tv_adventure_detail_place_title"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:text="루터회관"
             android:textSize="28sp"
             app:layout_constraintBottom_toBottomOf="@id/iv_adventure_detail_back"
             app:layout_constraintEnd_toEndOf="parent"
             app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintTop_toTopOf="@id/iv_adventure_detail_back" />
+            app:layout_constraintTop_toTopOf="@id/iv_adventure_detail_back"
+            tools:text="루터회관" />
 
         <ImageView
             android:id="@+id/iv_adventure_detail_photo"

From 33f74259b862d5f9dbc859c7cbeb7cd908e00dac Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 02:57:20 +0900
Subject: [PATCH 10/24] =?UTF-8?q?refactor:=20=EB=A7=88=EC=A7=84=20?=
 =?UTF-8?q?=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 android/app/src/main/res/layout/activity_adventure_detail.xml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/android/app/src/main/res/layout/activity_adventure_detail.xml b/android/app/src/main/res/layout/activity_adventure_detail.xml
index d8c4f7ae5..6eb8d23c2 100644
--- a/android/app/src/main/res/layout/activity_adventure_detail.xml
+++ b/android/app/src/main/res/layout/activity_adventure_detail.xml
@@ -53,6 +53,7 @@
             android:layout_width="0dp"
             android:layout_height="wrap_content"
             android:layout_marginHorizontal="4dp"
+            android:layout_marginTop="@dimen/space_default_medium"
             android:background="@drawable/rect_red_white_radius_small"
             app:layout_constraintBottom_toTopOf="@id/vp_adventure_detail"
             app:layout_constraintEnd_toEndOf="parent"

From 22781e54932360635df19a45520f217d057546d9 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 11:53:11 +0900
Subject: [PATCH 11/24] =?UTF-8?q?feat:=20=EC=9D=BD=EC=9D=80=20=EC=AA=BD?=
 =?UTF-8?q?=EC=A7=80,=20=EB=93=B1=EB=A1=9D=ED=95=9C=20=EC=AA=BD=EC=A7=80?=
 =?UTF-8?q?=EB=A5=BC=20flow=EB=A1=9C=20combine?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                | 52 ++++++++++++++--
 .../adventuredetail/AdventureDetailUiState.kt | 14 +++++
 .../AdventureDetailViewModel.kt               | 59 ++++++++++++++-----
 3 files changed, 103 insertions(+), 22 deletions(-)
 create mode 100644 android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailUiState.kt

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
index 1e74ab32c..ce70f90e6 100644
--- 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
@@ -1,28 +1,58 @@
 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 androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
 import com.google.android.material.tabs.TabLayoutMediator
+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 dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.launch
 
-class AdventureDetailActivity : AppCompatActivity() {
+@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)
 
-        initViewPager()
+        initView()
+        subscribe()
     }
 
-    private fun initViewPager() {
+    private fun initView() {
+        viewModel.fetchReadLetter(1L)
+        viewModel.fetchWriteLetter(1L)
+    }
+
+    private fun subscribe() {
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                viewModel.uiState.collect { adventureDetailUiState ->
+                    when (adventureDetailUiState) {
+                        is AdventureDetailUiState.Loading, is AdventureDetailUiState.Error -> Unit
+                        is AdventureDetailUiState.Success -> initViewPager(adventureDetailUiState)
+                    }
+                }
+            }
+        }
+    }
+
+    private fun initViewPager(adventureDetailUiState: AdventureDetailUiState.Success) {
         binding.vpAdventureDetail.adapter = ViewPagerAdapter(
             listOf(
-                List(10) { OpenLetterUiModel("krrong", "today", "meesage") },
-                List(10) { OpenLetterUiModel("krrong", "today", "meesage") },
+                adventureDetailUiState.readLetters,
+                adventureDetailUiState.writeLetters,
             ),
         )
 
@@ -33,4 +63,14 @@ class AdventureDetailActivity : AppCompatActivity() {
             }
         }.attach()
     }
+
+    companion object {
+        private const val GAME_ID = "GAME_ID"
+
+        fun getIntentWithId(context: Context, gameId: Long): Intent {
+            return Intent(context, AdventureDetailActivity::class.java).apply {
+                putExtra(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..496ccff37
--- /dev/null
+++ b/android/app/src/main/java/com/now/naaga/presentation/adventuredetail/AdventureDetailUiState.kt
@@ -0,0 +1,14 @@
+package com.now.naaga.presentation.adventuredetail
+
+import com.now.naaga.presentation.uimodel.model.OpenLetterUiModel
+
+sealed interface AdventureDetailUiState {
+    object Loading : AdventureDetailUiState
+
+    data class Success(
+        val readLetters: List<OpenLetterUiModel>,
+        val writeLetters: List<OpenLetterUiModel>,
+    ) : 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
index 8654465d3..c0dd90a13 100644
--- 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
@@ -8,45 +8,72 @@ import com.now.domain.model.letter.OpenLetter
 import com.now.domain.model.type.LogType
 import com.now.domain.repository.LetterRepository
 import com.now.naaga.data.throwable.DataThrowable
+import com.now.naaga.presentation.uimodel.mapper.toUiModel
 import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+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) : ViewModel() {
-    private val _readLetters = MutableLiveData<List<OpenLetter>>()
-    val readLetters: LiveData<List<OpenLetter>> = _readLetters
+class AdventureDetailViewModel @Inject constructor(
+    private val letterRepository: LetterRepository,
+) : ViewModel() {
+    private val readLettersFlow = MutableSharedFlow<List<OpenLetter>>()
 
-    private val _writeLetters = MutableLiveData<List<OpenLetter>>()
-    val writeLetters: LiveData<List<OpenLetter>> = _writeLetters
+    private val writeLettersFlow = MutableSharedFlow<List<OpenLetter>>()
+
+    private val _uiState: MutableStateFlow<AdventureDetailUiState> = MutableStateFlow(AdventureDetailUiState.Loading)
+    val uiState: StateFlow<AdventureDetailUiState> = _uiState.asStateFlow()
 
     private val _throwable = MutableLiveData<DataThrowable>()
     val throwable: LiveData<DataThrowable> = _throwable
 
-    fun fetchLetterLogs(gameId: Long, logType: LogType) {
+    init {
+        viewModelScope.launch {
+            combine(readLettersFlow, writeLettersFlow) { readLetters, writeLetters ->
+                AdventureDetailUiState.Success(
+                    readLetters = readLetters.map { it.toUiModel() },
+                    writeLetters = writeLetters.map { it.toUiModel() },
+                )
+            }.collectLatest { _uiState.value = it }
+        }
+    }
+
+    fun fetchReadLetter(gameId: Long) {
         viewModelScope.launch {
             runCatching {
-                letterRepository.fetchLetterLogs(gameId, logType)
+                letterRepository.fetchLetterLogs(gameId, LogType.READ)
             }.onSuccess {
-                setLetters(logType, it)
-            }.onFailure {
-                setThrowable(it)
+                readLettersFlow.emit(it)
             }
         }
     }
 
-    private fun setLetters(logType: LogType, letters: List<OpenLetter>) {
-        when (logType) {
-            LogType.READ -> _readLetters.value = letters
-            LogType.WRITE -> _writeLetters.value = letters
+    fun fetchWriteLetter(gameId: Long) {
+        viewModelScope.launch {
+            runCatching {
+                letterRepository.fetchLetterLogs(gameId, LogType.WRITE)
+            }.onSuccess {
+                writeLettersFlow.emit(it)
+            }
         }
     }
 
     private fun setThrowable(throwable: Throwable) {
         when (throwable) {
-            is IOException -> { TODO("_throwable.value = DataThrowable.NetworkThrowable") }
-            is DataThrowable.LetterThrowable -> { _throwable.value = throwable }
+            is IOException -> {
+                TODO("_throwable.value = DataThrowable.NetworkThrowable")
+            }
+
+            is DataThrowable.LetterThrowable -> {
+                _throwable.value = throwable
+            }
         }
     }
 }

From 97e847847520e30a2540adbfbd652c268d91874f Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 12:19:16 +0900
Subject: [PATCH 12/24] =?UTF-8?q?feat:=20=EB=AA=A8=ED=97=98=20=EA=B8=B0?=
 =?UTF-8?q?=EB=A1=9D=20=EC=83=81=EC=84=B8=20=ED=8E=98=EC=9D=B4=EC=A7=80?=
 =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98=EB=8A=94=20=EA=B8=B0?=
 =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureHistoryActivity.kt               |  8 +++++++-
 .../recyclerview/AdventureHistoryAdapter.kt   |  8 ++++++--
 .../AdventureHistoryViewHolder.kt             | 19 +++++++++++++------
 3 files changed, 26 insertions(+), 9 deletions(-)

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..f9c4a48ca 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
@@ -7,6 +7,7 @@ import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
 import com.now.domain.model.AdventureResult
 import com.now.naaga.databinding.ActivityAdventureHistoryBinding
+import com.now.naaga.presentation.adventuredetail.AdventureDetailActivity
 import com.now.naaga.presentation.adventurehistory.recyclerview.AdventureHistoryAdapter
 import dagger.hilt.android.AndroidEntryPoint
 
@@ -14,7 +15,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)
@@ -50,6 +51,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<AdventureResult, AdventureHistoryViewHolder>(historyDiff) {
+class AdventureHistoryAdapter(
+    private val onClick: (Long) -> Unit,
+) : ListAdapter<AdventureResult, AdventureHistoryViewHolder>(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)
-        }
     }
 }

From f81a799195eeed4d25878aef308e3b03039e1efe Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 12:21:18 +0900
Subject: [PATCH 13/24] =?UTF-8?q?feat:=20=EB=92=A4=EB=A1=9C=EA=B0=80?=
 =?UTF-8?q?=EA=B8=B0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../presentation/adventuredetail/AdventureDetailActivity.kt  | 5 +++++
 1 file changed, 5 insertions(+)

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
index ce70f90e6..158a2da3f 100644
--- 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
@@ -27,6 +27,7 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
         setContentView(binding.root)
 
         initView()
+        setClickListeners()
         subscribe()
     }
 
@@ -35,6 +36,10 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
         viewModel.fetchWriteLetter(1L)
     }
 
+    private fun setClickListeners() {
+        binding.ivAdventureDetailBack.setOnClickListener { finish() }
+    }
+
     private fun subscribe() {
         lifecycleScope.launch {
             repeatOnLifecycle(Lifecycle.State.STARTED) {

From 4c5d6964f9041038e916c40509b4e4ba5a8f304c Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 12:26:50 +0900
Subject: [PATCH 14/24] =?UTF-8?q?feat:=20=EB=84=98=EA=B2=A8=EC=A4=80=20?=
 =?UTF-8?q?=EA=B2=8C=EC=9E=84=20id=EB=A5=BC=20=EA=B0=80=EC=A0=B8=EC=98=A4?=
 =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../adventuredetail/AdventureDetailActivity.kt     | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

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
index 158a2da3f..3bbf52905 100644
--- 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
@@ -26,14 +26,16 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
         binding = ActivityAdventureDetailBinding.inflate(layoutInflater)
         setContentView(binding.root)
 
-        initView()
+        val gameId = intent.getLongExtra(KET_GAME_ID, 0L)
+
+        initView(gameId)
         setClickListeners()
         subscribe()
     }
 
-    private fun initView() {
-        viewModel.fetchReadLetter(1L)
-        viewModel.fetchWriteLetter(1L)
+    private fun initView(gameId: Long) {
+        viewModel.fetchReadLetter(gameId)
+        viewModel.fetchWriteLetter(gameId)
     }
 
     private fun setClickListeners() {
@@ -70,11 +72,11 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
     }
 
     companion object {
-        private const val GAME_ID = "GAME_ID"
+        private const val KET_GAME_ID = "GAME_ID"
 
         fun getIntentWithId(context: Context, gameId: Long): Intent {
             return Intent(context, AdventureDetailActivity::class.java).apply {
-                putExtra(GAME_ID, gameId)
+                putExtra(KET_GAME_ID, gameId)
             }
         }
     }

From 01c0d09fd73ed69ba65dbcc80d2fd1c30e0fc840 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 12:46:15 +0900
Subject: [PATCH 15/24] =?UTF-8?q?feat:=20=EA=B2=8C=EC=9E=84=20id=EB=A1=9C?=
 =?UTF-8?q?=20=EA=B2=8C=EC=9E=84=20=EA=B2=B0=EA=B3=BC=EB=A5=BC=20=EA=B0=80?=
 =?UTF-8?q?=EC=A0=B8=EC=99=80=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8A=94=20?=
 =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                | 28 +++++++++++++------
 .../adventuredetail/AdventureDetailUiState.kt |  2 ++
 .../AdventureDetailViewModel.kt               | 18 +++++++++++-
 3 files changed, 39 insertions(+), 9 deletions(-)

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
index 3bbf52905..9f54d0e3a 100644
--- 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
@@ -8,11 +8,15 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.repeatOnLifecycle
+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 dagger.hilt.android.AndroidEntryPoint
 import kotlinx.coroutines.launch
 
@@ -36,6 +40,7 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
     private fun initView(gameId: Long) {
         viewModel.fetchReadLetter(gameId)
         viewModel.fetchWriteLetter(gameId)
+        viewModel.fetchAdventureResult(gameId)
     }
 
     private fun setClickListeners() {
@@ -48,20 +53,20 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
                 viewModel.uiState.collect { adventureDetailUiState ->
                     when (adventureDetailUiState) {
                         is AdventureDetailUiState.Loading, is AdventureDetailUiState.Error -> Unit
-                        is AdventureDetailUiState.Success -> initViewPager(adventureDetailUiState)
+                        is AdventureDetailUiState.Success -> initView(adventureDetailUiState)
                     }
                 }
             }
         }
     }
 
-    private fun initViewPager(adventureDetailUiState: AdventureDetailUiState.Success) {
-        binding.vpAdventureDetail.adapter = ViewPagerAdapter(
-            listOf(
-                adventureDetailUiState.readLetters,
-                adventureDetailUiState.writeLetters,
-            ),
-        )
+    private fun initView(adventureDetailUiState: AdventureDetailUiState.Success) {
+        initViewPager(adventureDetailUiState.readLetters, adventureDetailUiState.writeLetters)
+        initImage(adventureDetailUiState.adventureResult)
+    }
+
+    private fun initViewPager(readLetters: List<OpenLetterUiModel>, writeLetters: List<OpenLetterUiModel>) {
+        binding.vpAdventureDetail.adapter = ViewPagerAdapter(listOf(readLetters, writeLetters))
 
         TabLayoutMediator(binding.tlAdventureDetail, binding.vpAdventureDetail) { tab, position ->
             when (position) {
@@ -71,6 +76,13 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
         }.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"
 
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
index 496ccff37..722897e00 100644
--- 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
@@ -1,5 +1,6 @@
 package com.now.naaga.presentation.adventuredetail
 
+import com.now.domain.model.AdventureResult
 import com.now.naaga.presentation.uimodel.model.OpenLetterUiModel
 
 sealed interface AdventureDetailUiState {
@@ -8,6 +9,7 @@ sealed interface AdventureDetailUiState {
     data class Success(
         val readLetters: List<OpenLetterUiModel>,
         val writeLetters: List<OpenLetterUiModel>,
+        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
index c0dd90a13..fbf937229 100644
--- 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
@@ -4,8 +4,10 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 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
@@ -23,11 +25,14 @@ import javax.inject.Inject
 @HiltViewModel
 class AdventureDetailViewModel @Inject constructor(
     private val letterRepository: LetterRepository,
+    private val adventureRepository: AdventureRepository,
 ) : ViewModel() {
     private val readLettersFlow = MutableSharedFlow<List<OpenLetter>>()
 
     private val writeLettersFlow = MutableSharedFlow<List<OpenLetter>>()
 
+    private val adventureFlow = MutableSharedFlow<AdventureResult>()
+
     private val _uiState: MutableStateFlow<AdventureDetailUiState> = MutableStateFlow(AdventureDetailUiState.Loading)
     val uiState: StateFlow<AdventureDetailUiState> = _uiState.asStateFlow()
 
@@ -36,10 +41,11 @@ class AdventureDetailViewModel @Inject constructor(
 
     init {
         viewModelScope.launch {
-            combine(readLettersFlow, writeLettersFlow) { readLetters, writeLetters ->
+            combine(readLettersFlow, writeLettersFlow, adventureFlow) { readLetters, writeLetters, adventureResult ->
                 AdventureDetailUiState.Success(
                     readLetters = readLetters.map { it.toUiModel() },
                     writeLetters = writeLetters.map { it.toUiModel() },
+                    adventureResult = adventureResult,
                 )
             }.collectLatest { _uiState.value = it }
         }
@@ -65,6 +71,16 @@ class AdventureDetailViewModel @Inject constructor(
         }
     }
 
+    fun fetchAdventureResult(gameId: Long) {
+        viewModelScope.launch {
+            runCatching {
+                adventureRepository.fetchAdventureResult(gameId)
+            }.onSuccess {
+                adventureFlow.emit(it)
+            }
+        }
+    }
+
     private fun setThrowable(throwable: Throwable) {
         when (throwable) {
             is IOException -> {

From 4df847175a8f4304271d9ee35939ef7383a18f31 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 12:49:01 +0900
Subject: [PATCH 16/24] =?UTF-8?q?refactor:=20string=20resource=ED=99=94?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../presentation/adventuredetail/AdventureDetailActivity.kt   | 4 ++--
 android/app/src/main/res/values/strings.xml                   | 4 ++++
 2 files changed, 6 insertions(+), 2 deletions(-)

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
index 9f54d0e3a..448e2e575 100644
--- 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
@@ -70,8 +70,8 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
 
         TabLayoutMediator(binding.tlAdventureDetail, binding.vpAdventureDetail) { tab, position ->
             when (position) {
-                0 -> tab.text = "읽은 편지"
-                1 -> tab.text = "등록한 편지"
+                0 -> tab.text = getString(R.string.adventure_detail_read_letter)
+                1 -> tab.text = getString(R.string.adventure_detail_write_letter)
             }
         }.attach()
     }
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index fcf8cdddc..00d7de1ac 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -114,6 +114,10 @@
     <string name="setting_question_email">naaganow@gmail.com</string>
     <string name="setting_question_email_title">[나아가에게 문의하기]</string>
 
+    <!-- AdventureDetailActivity -->
+    <string name="adventure_detail_read_letter">읽은 편지</string>
+    <string name="adventure_detail_write_letter">등록한 편지</string>
+
     <!-- WithdrawalDialog -->
     <string name="withdrawal_dialog_title">정말 회원 탈퇴를 하시겠습니까?</string>
     <string name="withdrawal_dialog_description">나아가와 함께 즐거운 모험을 계속해보세요!</string>

From 94fced68f79c4a2b48de4af39c4697a447ad931f Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 14:48:46 +0900
Subject: [PATCH 17/24] =?UTF-8?q?refactor:=20string=20=EB=B3=80=EA=B2=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 android/app/src/main/res/values/strings.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 00d7de1ac..c3c27b0bb 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -115,8 +115,8 @@
     <string name="setting_question_email_title">[나아가에게 문의하기]</string>
 
     <!-- AdventureDetailActivity -->
-    <string name="adventure_detail_read_letter">읽은 편지</string>
-    <string name="adventure_detail_write_letter">등록한 편지</string>
+    <string name="adventure_detail_read_letter">읽은 쪽지</string>
+    <string name="adventure_detail_write_letter">등록한 쪽지</string>
 
     <!-- WithdrawalDialog -->
     <string name="withdrawal_dialog_title">정말 회원 탈퇴를 하시겠습니까?</string>

From 0c29e97bc1b809589335e4647e57675d02a64c87 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 15:11:14 +0900
Subject: [PATCH 18/24] =?UTF-8?q?feat:=20=EB=93=B1=EB=A1=9D=EB=90=9C=20?=
 =?UTF-8?q?=EC=AA=BD=EC=A7=80=EA=B0=80=20=EC=97=86=EB=8A=94=20=EA=B2=BD?=
 =?UTF-8?q?=EC=9A=B0=20=EC=AA=BD=EC=A7=80=EA=B0=80=20=EC=97=86=EB=8B=A4?=
 =?UTF-8?q?=EB=8A=94=20=EB=82=B4=EC=9A=A9=EC=9D=84=20=EB=B3=B4=EC=97=AC?=
 =?UTF-8?q?=EC=A3=BC=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../adventuredetail/AdventureDetailViewModel.kt        | 10 ++++++++--
 .../presentation/uimodel/model/OpenLetterUiModel.kt    | 10 +++++++++-
 2 files changed, 17 insertions(+), 3 deletions(-)

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
index fbf937229..c7b678665 100644
--- 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
@@ -11,6 +11,7 @@ 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
@@ -43,14 +44,19 @@ class AdventureDetailViewModel @Inject constructor(
         viewModelScope.launch {
             combine(readLettersFlow, writeLettersFlow, adventureFlow) { readLetters, writeLetters, adventureResult ->
                 AdventureDetailUiState.Success(
-                    readLetters = readLetters.map { it.toUiModel() },
-                    writeLetters = writeLetters.map { it.toUiModel() },
+                    readLetters = getOpenLetterUiModels(readLetters),
+                    writeLetters = getOpenLetterUiModels(writeLetters),
                     adventureResult = adventureResult,
                 )
             }.collectLatest { _uiState.value = it }
         }
     }
 
+    private fun getOpenLetterUiModels(letters: List<OpenLetter>): List<OpenLetterUiModel> {
+        if (letters.isEmpty()) return listOf(OpenLetterUiModel.getDefault())
+        return letters.map { it.toUiModel() }
+    }
+
     fun fetchReadLetter(gameId: Long) {
         viewModelScope.launch {
             runCatching {
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
index 08a286b89..e8df08386 100644
--- 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
@@ -4,4 +4,12 @@ data class OpenLetterUiModel(
     val nickname: String,
     val registerDate: String,
     val message: String,
-)
+) {
+    companion object {
+        private const val DEFAULT_MESSAGE = "쪽지가 없습니다."
+
+        fun getDefault(): OpenLetterUiModel {
+            return OpenLetterUiModel("", "", DEFAULT_MESSAGE)
+        }
+    }
+}

From 9b2c5fddb14beeddc74b3bb1264ed16ae0b20c15 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 15:40:19 +0900
Subject: [PATCH 19/24] =?UTF-8?q?feat:=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?=
 =?UTF-8?q?=EA=B0=80=20=EC=97=86=EB=8A=94=20=EC=8A=A4=EB=82=B5=EB=B0=94=20?=
 =?UTF-8?q?=ED=99=95=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../app/src/main/java/com/now/naaga/util/extension/ViewExt.kt | 4 ++++
 1 file changed, 4 insertions(+)

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

From c7e75894fe28cad56df73e45b5e011bf83f8cf62 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 15:47:59 +0900
Subject: [PATCH 20/24] =?UTF-8?q?feat:=20=EC=98=88=EC=83=81=20=EA=B0=80?=
 =?UTF-8?q?=EB=8A=A5=ED=95=9C=20=EC=98=88=EC=99=B8=20=ED=95=B8=EB=93=A4?=
 =?UTF-8?q?=EB=A7=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                | 19 +++++++++++
 .../AdventureDetailViewModel.kt               | 34 +++++++++++++------
 android/app/src/main/res/values/strings.xml   |  2 ++
 3 files changed, 45 insertions(+), 10 deletions(-)

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
index 448e2e575..1f1b21554 100644
--- 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
@@ -17,6 +17,7 @@ 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.showSnackbarWithEvent
 import dagger.hilt.android.AndroidEntryPoint
 import kotlinx.coroutines.launch
 
@@ -58,6 +59,24 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
                 }
             }
         }
+        lifecycleScope.launch {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                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) {
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
index c7b678665..84efa37c2 100644
--- 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
@@ -1,7 +1,5 @@
 package com.now.naaga.presentation.adventuredetail
 
-import androidx.lifecycle.LiveData
-import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import com.now.domain.model.AdventureResult
@@ -15,7 +13,9 @@ 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
@@ -37,8 +37,8 @@ class AdventureDetailViewModel @Inject constructor(
     private val _uiState: MutableStateFlow<AdventureDetailUiState> = MutableStateFlow(AdventureDetailUiState.Loading)
     val uiState: StateFlow<AdventureDetailUiState> = _uiState.asStateFlow()
 
-    private val _throwable = MutableLiveData<DataThrowable>()
-    val throwable: LiveData<DataThrowable> = _throwable
+    private val _throwableFlow = MutableSharedFlow<Event>()
+    val throwableFlow: SharedFlow<Event> = _throwableFlow.asSharedFlow()
 
     init {
         viewModelScope.launch {
@@ -63,6 +63,8 @@ class AdventureDetailViewModel @Inject constructor(
                 letterRepository.fetchLetterLogs(gameId, LogType.READ)
             }.onSuccess {
                 readLettersFlow.emit(it)
+            }.onFailure {
+                setThrowable(it)
             }
         }
     }
@@ -73,6 +75,8 @@ class AdventureDetailViewModel @Inject constructor(
                 letterRepository.fetchLetterLogs(gameId, LogType.WRITE)
             }.onSuccess {
                 writeLettersFlow.emit(it)
+            }.onFailure {
+                setThrowable(it)
             }
         }
     }
@@ -83,19 +87,29 @@ class AdventureDetailViewModel @Inject constructor(
                 adventureRepository.fetchAdventureResult(gameId)
             }.onSuccess {
                 adventureFlow.emit(it)
+            }.onFailure {
+                setThrowable(it)
             }
         }
     }
 
     private fun setThrowable(throwable: Throwable) {
         when (throwable) {
-            is IOException -> {
-                TODO("_throwable.value = DataThrowable.NetworkThrowable")
-            }
+            is IOException -> throwable(Event.NetworkExceptionEvent(throwable))
+            is DataThrowable.LetterThrowable -> throwable(Event.LetterExceptionEvent(throwable))
+            is DataThrowable.GameThrowable -> throwable(Event.GameExceptionEvent(throwable))
+        }
+    }
 
-            is DataThrowable.LetterThrowable -> {
-                _throwable.value = 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/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index c3c27b0bb..97cb5ad7c 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -140,6 +140,8 @@
     <string name="snackbar_location_message">위치 권한이 필요해요!</string>
     <string name="snackbar_storage_message">저장소 권한이 필요해요!</string>
     <string name="snackbar_action_title">이동하기</string>
+    <string name="snackbar_action_re_request_message">다시 요청해주세요!</string>
+    <string name="snackbar_action__re_request_title">나가기</string>
 
     <!-- SendLetterDialog -->
     <string name="send_letter_dialog_hint">이 곳에 내용을 작성해주세요!</string>

From 0e9a035100c6b2b9a75975c2ebd520e84bee633c Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 15:50:05 +0900
Subject: [PATCH 21/24] =?UTF-8?q?refactor:=20repeatOnStarted=20=ED=99=95?=
 =?UTF-8?q?=EC=9E=A5=ED=95=A8=EC=88=98=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../AdventureDetailActivity.kt                | 31 +++++++------------
 .../naaga/util/extension/LifeCycleOwnerExt.kt | 14 +++++++++
 2 files changed, 26 insertions(+), 19 deletions(-)
 create mode 100644 android/app/src/main/java/com/now/naaga/util/extension/LifeCycleOwnerExt.kt

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
index 1f1b21554..950a6700c 100644
--- 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
@@ -5,9 +5,6 @@ import android.content.Intent
 import android.os.Bundle
 import androidx.activity.viewModels
 import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.lifecycleScope
-import androidx.lifecycle.repeatOnLifecycle
 import com.bumptech.glide.Glide
 import com.google.android.material.tabs.TabLayoutMediator
 import com.now.domain.model.AdventureResult
@@ -17,9 +14,9 @@ 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
-import kotlinx.coroutines.launch
 
 @AndroidEntryPoint
 class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by DefaultAnalyticsDelegate() {
@@ -49,24 +46,20 @@ class AdventureDetailActivity : AppCompatActivity(), AnalyticsDelegate by Defaul
     }
 
     private fun subscribe() {
-        lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.uiState.collect { adventureDetailUiState ->
-                    when (adventureDetailUiState) {
-                        is AdventureDetailUiState.Loading, is AdventureDetailUiState.Error -> Unit
-                        is AdventureDetailUiState.Success -> initView(adventureDetailUiState)
-                    }
+        repeatOnStarted {
+            viewModel.uiState.collect { adventureDetailUiState ->
+                when (adventureDetailUiState) {
+                    is AdventureDetailUiState.Loading, is AdventureDetailUiState.Error -> Unit
+                    is AdventureDetailUiState.Success -> initView(adventureDetailUiState)
                 }
             }
         }
-        lifecycleScope.launch {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                viewModel.throwableFlow.collect { event ->
-                    when (event) {
-                        is AdventureDetailViewModel.Event.NetworkExceptionEvent -> showReRequestSnackbar()
-                        is AdventureDetailViewModel.Event.LetterExceptionEvent -> showReRequestSnackbar()
-                        is AdventureDetailViewModel.Event.GameExceptionEvent -> showReRequestSnackbar()
-                    }
+        repeatOnStarted {
+            viewModel.throwableFlow.collect { event ->
+                when (event) {
+                    is AdventureDetailViewModel.Event.NetworkExceptionEvent -> showReRequestSnackbar()
+                    is AdventureDetailViewModel.Event.LetterExceptionEvent -> showReRequestSnackbar()
+                    is AdventureDetailViewModel.Event.GameExceptionEvent -> showReRequestSnackbar()
                 }
             }
         }
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)
+    }
+}

From 3fac1bc0b5237a970c35fa3976fd6425b86fb0f0 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 16:17:13 +0900
Subject: [PATCH 22/24] =?UTF-8?q?feat:=20=EC=95=A1=ED=8B=B0=EB=B9=84?=
 =?UTF-8?q?=ED=8B=B0=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 android/app/src/main/AndroidManifest.xml | 4 ++++
 1 file changed, 4 insertions(+)

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" />
+        <activity
+            android:name=".presentation.adventuredetail.AdventureDetailActivity"
+            android:screenOrientation="portrait"
+            android:exported="false" />
         <activity
             android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
             android:exported="true">

From 6d0c43846b21e186fc6699d1d674fa033d77c7b2 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 16:37:26 +0900
Subject: [PATCH 23/24] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?=
 =?UTF-8?q?=EC=88=98=EC=A0=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../now/naaga/AdventureDetailViewModelTest.kt | 106 +++++++++++++++---
 1 file changed, 89 insertions(+), 17 deletions(-)

diff --git a/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt b/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt
index 4ba7f38c4..5c73f192c 100644
--- a/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt
+++ b/android/app/src/test/java/com/now/naaga/AdventureDetailViewModelTest.kt
@@ -1,41 +1,45 @@
 package com.now.naaga
 
-import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+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.assertTrue
+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.Rule
 import org.junit.Test
+import java.time.LocalDateTime
 
 class AdventureDetailViewModelTest {
     private lateinit var vm: AdventureDetailViewModel
     private lateinit var letterRepository: LetterRepository
-
-    @get:Rule
-    val instantExecutorRule = InstantTaskExecutorRule()
+    private lateinit var adventureRepository: AdventureRepository
 
     @OptIn(ExperimentalCoroutinesApi::class)
     @Before
     fun setup() {
         Dispatchers.setMain(UnconfinedTestDispatcher())
         letterRepository = mockk()
-        vm = AdventureDetailViewModel(letterRepository)
+        adventureRepository = mockk()
+        vm = AdventureDetailViewModel(letterRepository, adventureRepository)
     }
 
     @Test
-    fun `읽은 쪽지를 불러올 때 작성한 쪽지는 불러오지 않는다`() {
+    fun `읽은 쪽지만 불러오면 AdventureDetailUiState는 Loading 상태다`() {
         // given
         coEvery {
             letterRepository.fetchLetterLogs(1L, LogType.READ)
@@ -44,16 +48,14 @@ class AdventureDetailViewModelTest {
         }
 
         // when
-        vm.fetchLetterLogs(1L, LogType.READ)
+        vm.fetchReadLetter(1L)
 
         // then
-        assertTrue(vm.readLetters.isInitialized)
-        assertEquals(vm.readLetters.getOrAwaitValue(), fakeReadLetterLogs)
-        assertEquals(vm.writeLetters.isInitialized, false)
+        assertSame(vm.uiState.value, AdventureDetailUiState.Loading)
     }
 
     @Test
-    fun `작성한 쪽지를 불러올 때 읽은 쪽지는 불러오지 않는다`() {
+    fun `작성한 쪽지만 불러오면 AdventureDetailUiState는 Loading 상태다`() {
         // given
         coEvery {
             letterRepository.fetchLetterLogs(1L, LogType.WRITE)
@@ -62,12 +64,68 @@ class AdventureDetailViewModelTest {
         }
 
         // when
-        vm.fetchLetterLogs(1L, LogType.WRITE)
+        vm.fetchWriteLetter(1L)
 
         // then
-        assertEquals(vm.readLetters.isInitialized, false)
-        assertTrue(vm.writeLetters.isInitialized)
-        assertEquals(vm.writeLetters.getOrAwaitValue(), fakeWriteLetterLogs)
+        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(
@@ -89,4 +147,18 @@ class AdventureDetailViewModelTest {
             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(),
+    )
 }

From 5d6abee4b00674424329264b35ae5da07dba60e6 Mon Sep 17 00:00:00 2001
From: krrong <khk9664@naver.com>
Date: Tue, 17 Oct 2023 22:22:55 +0900
Subject: [PATCH 24/24] =?UTF-8?q?refactor:=20=EB=94=94=ED=8F=B4=ED=8A=B8?=
 =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EC=83=9D=EC=84=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../presentation/adventuredetail/AdventureDetailViewModel.kt | 2 +-
 .../naaga/presentation/uimodel/model/OpenLetterUiModel.kt    | 5 +----
 2 files changed, 2 insertions(+), 5 deletions(-)

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
index 84efa37c2..74cff1dd1 100644
--- 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
@@ -53,7 +53,7 @@ class AdventureDetailViewModel @Inject constructor(
     }
 
     private fun getOpenLetterUiModels(letters: List<OpenLetter>): List<OpenLetterUiModel> {
-        if (letters.isEmpty()) return listOf(OpenLetterUiModel.getDefault())
+        if (letters.isEmpty()) return listOf(OpenLetterUiModel.DEFAULT_OPEN_LETTER)
         return letters.map { it.toUiModel() }
     }
 
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
index e8df08386..65b21dc63 100644
--- 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
@@ -7,9 +7,6 @@ data class OpenLetterUiModel(
 ) {
     companion object {
         private const val DEFAULT_MESSAGE = "쪽지가 없습니다."
-
-        fun getDefault(): OpenLetterUiModel {
-            return OpenLetterUiModel("", "", DEFAULT_MESSAGE)
-        }
+        val DEFAULT_OPEN_LETTER = OpenLetterUiModel("", "", DEFAULT_MESSAGE)
     }
 }