Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/#111 행사 상세 화면 구현 #191

Merged
merged 7 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion android/2023-emmsale/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
<activity
android:name=".presentation.ui.onboarding.OnboardingActivity"
android:exported="true" />
<activity
android:name=".presentation.eventdetail.EventDetailActivity"
android:exported="false" />
<activity
android:name=".presentation.ui.login.LoginActivity"
android:exported="true"
Expand Down Expand Up @@ -53,4 +56,4 @@
</service>
</application>

</manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.emmsale.data.eventdetail

data class EventDetail(
val id: Long,
val name: String,
val status: String,
val location: String,
val startDate: String,
val endDate: String,
val informationUrl: String,
val tags: List<String>,
val imageUrl: String,
val remainingDays: Int,
val type: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.emmsale.data.eventdetail

import com.emmsale.data.common.ApiResult

interface EventDetailRepository {
suspend fun fetchEventDetail(eventId: Long): ApiResult<EventDetail>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

컨벤션에 따르면 반환값이 있으므로 getXxx 네이밍이 맞습니당ㅠㅠ
viewModel에서는 데이터를 불러오는 로직에 반환값이 없으므로 fetchXxx를 사용합니다.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이전에 토마스 PR에도 남긴 리뷰입니다.
위에 남겨주신 리뷰를 조금 더 구체적으로 작성한 예시입니다.

스크린샷 2023-08-02 오후 5 37 27

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 사실 다시 고려해 봐야할 것 같아요. viewModel 에서만 fetch를 쓰고 서버에서는 get 을 쓰는게 좀 어색합니다. fetch 가 좀더 서버에서 쓰이는 언어같은데... 일단은 놔두고 다음 회의 때 한번 더 정했으면 좋겠습니다.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.emmsale.data.eventdetail

import com.emmsale.data.common.ApiResult
import com.emmsale.data.common.handleApi
import com.emmsale.data.eventdetail.dto.EventDetailApiModel

class EventDetailRepositoryImpl(
private val eventDetailService: EventDetailService,
) : EventDetailRepository {

override suspend fun fetchEventDetail(eventId: Long): ApiResult<EventDetail> {
val response = eventDetailService.fetchEventDetail(eventId)
return handleApi(
response = response,
mapToDomain = EventDetailApiModel::toData,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.emmsale.data.eventdetail

import com.emmsale.data.eventdetail.dto.EventDetailApiModel
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path

interface EventDetailService {
@GET("events/{eventId}")
suspend fun fetchEventDetail(
@Path("eventId") eventId: Long,
): Response<EventDetailApiModel>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.emmsale.data.eventdetail.dto

import com.emmsale.data.eventdetail.EventDetail
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class EventDetailApiModel(
@SerialName("id")
val id: Long,
@SerialName("name")
val name: String,
@SerialName("status")
val status: String,
@SerialName("location")
val location: String,
@SerialName("startDate")
val startDate: String,
@SerialName("endDate")
val endDate: String,
@SerialName("informationUrl")
val informationUrl: String,
@SerialName("tags")
val tags: List<String>,
@SerialName("imageUrl")
val imageUrl: String,
@SerialName("remainingDays")
val remainingDays: Int,
@SerialName("type")
val type: String,
) {
fun toData(): EventDetail = EventDetail(
id = id,
name = name,
status = status,
location = location,
startDate = startDate,
endDate = endDate,
informationUrl = informationUrl,
tags = tags,
imageUrl = imageUrl,
remainingDays = remainingDays,
type = type,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ interface MemberService {

@POST("/members")
suspend fun updateMember(@Body member: MemberApiModel): Response<Unit>
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import com.emmsale.data.activity.ActivityRepository
import com.emmsale.data.activity.ActivityRepositoryImpl
import com.emmsale.data.event.EventRepository
import com.emmsale.data.event.EventRepositoryImpl
import com.emmsale.data.eventdetail.EventDetailRepository
import com.emmsale.data.eventdetail.EventDetailRepositoryImpl
import com.emmsale.data.fcmToken.FcmTokenRepository
import com.emmsale.data.fcmToken.FcmTokenRepositoryImpl
import com.emmsale.data.login.LoginRepository
Expand Down Expand Up @@ -35,4 +37,7 @@ class RepositoryContainer(
val fcmTokenRepository: FcmTokenRepository by lazy {
FcmTokenRepositoryImpl(fcmTokenService = serviceContainer.fcmTokenService)
}
val eventDetailRepository: EventDetailRepository by lazy {
EventDetailRepositoryImpl(eventDetailService = serviceContainer.eventDetailService)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.emmsale.di
import com.emmsale.data.activity.ActivityService
import com.emmsale.data.common.ServiceFactory
import com.emmsale.data.event.EventService
import com.emmsale.data.eventdetail.EventDetailService
import com.emmsale.data.fcmToken.FcmTokenService
import com.emmsale.data.login.LoginService
import com.emmsale.data.member.MemberService
Expand All @@ -13,4 +14,5 @@ class ServiceContainer(serviceFactory: ServiceFactory) {
val memberService: MemberService by lazy { serviceFactory.create(MemberService::class.java) }
val eventService: EventService by lazy { serviceFactory.create(EventService::class.java) }
val fcmTokenService: FcmTokenService by lazy { serviceFactory.create(FcmTokenService::class.java) }
val eventDetailService: EventDetailService by lazy { serviceFactory.create(EventDetailService::class.java) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import com.emmsale.data.common.ServiceFactory
import com.emmsale.di.RepositoryContainer
import com.emmsale.di.ServiceContainer
import com.emmsale.di.SharedPreferenceContainer
import com.emmsale.presentation.common.firebase.analytics.Kerdy.initFirebaseAnalytics
import com.emmsale.presentation.common.firebase.analytics.KerdyAnalytics.initFirebaseAnalytics
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -16,7 +16,7 @@ class KerdyApplication : Application() {
super.onCreate()
repositoryContainer = RepositoryContainer(
serviceContainer = ServiceContainer(ServiceFactory()),
preferenceContainer = SharedPreferenceContainer(this)
preferenceContainer = SharedPreferenceContainer(this),
)
applicationScope.launch {
repositoryContainer.tokenRepository.getToken()?.let(::initFirebaseAnalytics)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.emmsale.presentation.common.firebase.analytics
package com.emmsale.presentation.common.firebase.analytics // ktlint-disable filename

import com.emmsale.data.token.Token
import com.google.firebase.analytics.FirebaseAnalytics
Expand All @@ -7,7 +7,7 @@ import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.analytics.ktx.logEvent
import com.google.firebase.ktx.Firebase

object Kerdy {
object KerdyAnalytics {
private val firebaseAnalytics: FirebaseAnalytics = Firebase.analytics

fun initFirebaseAnalytics(token: Token) {
Expand All @@ -18,4 +18,3 @@ object Kerdy {
firebaseAnalytics.logEvent(event, parameters)
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.emmsale.presentation.eventdetail

import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.emmsale.databinding.ActivityEventDetailBinding
import com.emmsale.presentation.ui.eventdetail.EventDetailFragmentStateAdpater
import com.emmsale.presentation.ui.eventdetail.EventDetailViewModel
import com.emmsale.presentation.ui.eventdetail.EventTag
import com.emmsale.presentation.ui.eventdetail.uistate.EventDetailUiState
import com.google.android.material.tabs.TabLayoutMediator

class EventDetailActivity : AppCompatActivity() {
private lateinit var binding: ActivityEventDetailBinding
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private val binding: ActivityEventDetailBinding by lazy { ActivityEventDetailBinding.inflate(layoutInflater) }
이렇게 var를 val로 바꿔도 괜찮을 것 같습니다 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

바꿨어용~~~

private val viewModel: EventDetailViewModel by viewModels { EventDetailViewModel.factory }
private val eventId: Long by lazy {
intent.getLongExtra(EVENT_ID_KEY, DEFAULT_EVENT_ID).apply {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply는 왜 있는 건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실수요 ㅎㅎ

}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setUpBinding()
setUpEventDetail()
setBackPress()
setUpParticipation()
viewModel.fetchEventDetail(1)
}

private fun initFragmentStateAdapter(informationUrl: String, imageUrl: String) {
binding.vpEventdetail.adapter =
EventDetailFragmentStateAdpater(this, eventId, informationUrl, imageUrl)
TabLayoutMediator(binding.tablayoutEventdetail, binding.vpEventdetail) { tab, position ->
when (position) {
INFORMATION_TAB_POSITION -> tab.text = "상세 정보"
COMMENT_TAB_POSITION -> tab.text = "댓글"
PARTICIPANT_TAB_POSITION -> tab.text = "같이가요"
Comment on lines +36 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

문자열 리소스로 추출하지 않은 거 가슴이 아프네요ㅠㅠ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 한번에 하자고 하셔서... ㅠㅠ

}
}.attach()
}

private fun setUpBinding() {
binding = ActivityEventDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
}

private fun setUpEventDetail() {
viewModel.eventDetail.observe(this) { eventDetailUiState ->
when (eventDetailUiState) {
is EventDetailUiState.Success -> {
binding.eventDetail = eventDetailUiState
addTagChip(eventDetailUiState.tags)
initFragmentStateAdapter(
eventDetailUiState.informationUrl,
eventDetailUiState.imageUrl,
)
}

else -> showToastMessage("행사 받아오기 실패")
}
}
}

private fun setUpParticipation() {
viewModel.participation.observe(this) { success ->
if (success) {
showToastMessage("참여 성공")
} else {
showToastMessage("참여 실패")
}
}
}

private fun addTagChip(tags: List<String>) {
tags.forEach { binding.chipgroupEvendetailTags.addView(createChip(it)) }
}

private fun createChip(tag: String) = EventTag(this).apply {
text = tag
}

private fun showToastMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}

private fun setBackPress() {
binding.ivEventdetailBackpress.setOnClickListener {
finish()
}
}

companion object {
private const val EVENT_ID_KEY = "EVENT_ID_KEY"
private const val DEFAULT_EVENT_ID = 1L
private const val INFORMATION_TAB_POSITION = 0
private const val COMMENT_TAB_POSITION = 1
private const val PARTICIPANT_TAB_POSITION = 2

fun startActivity(context: Context, eventId: Long) {
val intent = Intent(context, EventDetailActivity::class.java)
intent.putExtra(EVENT_ID_KEY, eventId)
context.startActivity(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.emmsale.presentation.ui.eventdetail

import EventParticipantFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.emmsale.presentation.ui.eventdetail.comment.EventCommentFragment
import com.emmsale.presentation.ui.eventdetail.information.EventInfoFragment

class EventDetailFragmentStateAdpater(
fragmentActivity: FragmentActivity,
private val eventId: Long,
private val informationUrl: String,
private val imageUrl: String,
) : FragmentStateAdapter(fragmentActivity) {

override fun getItemCount(): Int = EVENT_DETAIL_TAB_COUNT

override fun createFragment(position: Int): Fragment {
return when (position) {
INFORMATION_TAB -> EventInfoFragment.create(informationUrl, imageUrl)
COMMENT_TAB -> EventCommentFragment.create(eventId)
PARTICIPANT_TAB -> EventParticipantFragment.create(eventId)
else -> throw IllegalArgumentException("알수없는 ViewPager 오류입니다.")
}
}

companion object {
private const val INFORMATION_TAB = 0
private const val COMMENT_TAB = 1
private const val PARTICIPANT_TAB = 2
private const val EVENT_DETAIL_TAB_COUNT = 3
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.emmsale.presentation.ui.eventdetail

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.emmsale.data.common.ApiError
import com.emmsale.data.common.ApiException
import com.emmsale.data.common.ApiSuccess
import com.emmsale.data.eventdetail.EventDetailRepository
import com.emmsale.presentation.KerdyApplication
import com.emmsale.presentation.common.ViewModelFactory
import com.emmsale.presentation.ui.eventdetail.uistate.EventDetailUiState
import kotlinx.coroutines.launch

class EventDetailViewModel(
private val eventDetailRepository: EventDetailRepository,
) : ViewModel() {

private val _eventDetail: MutableLiveData<EventDetailUiState> =
MutableLiveData<EventDetailUiState>()
val eventDetail: LiveData<EventDetailUiState>
get() = _eventDetail

private val _participation: MutableLiveData<Boolean> = MutableLiveData()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

타입이 Boolean이라면 participation이라는 명사보다 isParticipated 같은 동사구가 국룰 아닌가요? ❓

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 필요 없는 변수라 지웠습니다~ 제가 참가자 목록이랑 함께 작업하다 보니 섞인게 많아요 ㅎㅎ

val participation: LiveData<Boolean>
get() = _participation

fun fetchEventDetail(id: Long) {
viewModelScope.launch {
when (val result = eventDetailRepository.fetchEventDetail(id)) {
is ApiSuccess -> _eventDetail.postValue(
EventDetailUiState.from(
result.data,
),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

한 줄로 작성하셔도 될 것 같아요~!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵!

)

is ApiError -> _eventDetail.postValue(EventDetailUiState.Error)
is ApiException -> _eventDetail.postValue(EventDetailUiState.Error)
}
}
}

companion object {
val factory = ViewModelFactory {
EventDetailViewModel(
eventDetailRepository = KerdyApplication.repositoryContainer.eventDetailRepository,
)
}
}
}
Loading