-
Notifications
You must be signed in to change notification settings - Fork 2
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
The head ref may contain hidden characters: "Feature/#111-\uD589\uC0AC_\uC0C1\uC138_\uD654\uBA74_\uAD6C\uD604"
Changes from 5 commits
ce3d385
c255760
3c65341
929c209
01c294b
bf38402
57ad1a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
} |
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 |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. apply는 왜 있는 건가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 문자열 리소스로 추출하지 않은 거 가슴이 아프네요ㅠㅠ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 타입이 Boolean이라면 participation이라는 명사보다 isParticipated 같은 동사구가 국룰 아닌가요? ❓ There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 한 줄로 작성하셔도 될 것 같아요~! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
컨벤션에 따르면 반환값이 있으므로 getXxx 네이밍이 맞습니당ㅠㅠ
viewModel에서는 데이터를 불러오는 로직에 반환값이 없으므로 fetchXxx를 사용합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이전에 토마스 PR에도 남긴 리뷰입니다.
위에 남겨주신 리뷰를 조금 더 구체적으로 작성한 예시입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분도 사실 다시 고려해 봐야할 것 같아요. viewModel 에서만 fetch를 쓰고 서버에서는 get 을 쓰는게 좀 어색합니다. fetch 가 좀더 서버에서 쓰이는 언어같은데... 일단은 놔두고 다음 회의 때 한번 더 정했으면 좋겠습니다.