diff --git a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailActivity.kt b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailActivity.kt index b9a9f3688..e5045e8bb 100644 --- a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailActivity.kt +++ b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailActivity.kt @@ -6,12 +6,14 @@ import android.os.Bundle import android.view.MenuItem import android.widget.Toast import androidx.activity.viewModels +import androidx.annotation.StringRes import com.created.team201.R import com.created.team201.databinding.ActivityStudyDetailBinding import com.created.team201.presentation.common.BindingActivity import com.created.team201.presentation.profile.ProfileActivity import com.created.team201.presentation.studyDetail.adapter.StudyParticipantsAdapter import com.created.team201.presentation.studyDetail.model.PeriodFormat +import com.created.team201.presentation.studyDetail.model.StudyDetailUIModel import com.created.team201.presentation.studyManagement.StudyManagementActivity class StudyDetailActivity : @@ -32,6 +34,8 @@ class StudyDetailActivity : initStudyDetailInformation() observeStudyDetailParticipants() observeStartStudy() + observeCanStartStudy() + observeParticipantsCount() } private fun initViewModel() { @@ -44,12 +48,13 @@ class StudyDetailActivity : setSupportActionBar(binding.tbStudyDetailAppBar) supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayShowTitleEnabled(false) + supportActionBar?.setHomeActionContentDescription(R.string.toolbar_back_text) supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_back) } private fun validateStudyId() { if (studyId == NON_EXISTENCE_STUDY_ID) { - Toast.makeText(this, "스터디를 찾을 수 없습니다.", Toast.LENGTH_SHORT).show() + showToast(R.string.study_detail_notify_invalid_study) finish() } } @@ -60,7 +65,12 @@ class StudyDetailActivity : } private fun initStudyDetailInformation() { - studyDetailViewModel.fetchStudyDetail(studyId) + studyDetailViewModel.fetchStudyDetail(studyId) { + if (studyDetailViewModel.study.value == StudyDetailUIModel.INVALID_STUDY_DETAIL) { + showToast(R.string.study_detail_notify_invalid_study) + } + finish() + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -76,10 +86,11 @@ class StudyDetailActivity : } } - fun convertPeriodOfCountFormat(periodOfCount: String?): String { + fun convertPeriodOfCountFormat(periodOfCount: String): String { + if (periodOfCount == "") return "" val stringRes = - PeriodFormat.valueOf(periodOfCount?.last() ?: DEFAULT_PERIOD_SYMBOL).res - return getString(stringRes, periodOfCount?.dropLast(STRING_LAST_INDEX)?.toInt()) + PeriodFormat.valueOf(periodOfCount.last()).res + return getString(stringRes, periodOfCount.dropLast(STRING_LAST_INDEX).toInt()) } fun initMainButtonOnClick(isMaster: Boolean) { @@ -96,12 +107,8 @@ class StudyDetailActivity : } override fun onAcceptApplicantClick(memberId: Long) { - if (studyDetailViewModel.isFullMember.value == true) { - Toast.makeText( - this, - getString(R.string.study_detail_do_not_accept_member_anymore), - Toast.LENGTH_SHORT, - ).show() + if (studyDetailViewModel.isFullMember.value) { + showToast(R.string.study_detail_do_not_accept_member_anymore) return } studyDetailViewModel.acceptApplicant(studyId, memberId) @@ -111,6 +118,19 @@ class StudyDetailActivity : startActivity(ProfileActivity.getIntent(this, memberId)) } + private fun observeParticipantsCount() { + studyDetailViewModel.studyMemberCount.observe(this) { + if (studyDetailViewModel.state.value is StudyDetailState.Master) { + binding.btnStudyDetailMain.text = + getString( + R.string.study_detail_button_start_study, + studyDetailViewModel.studyMemberCount.value, + studyDetailViewModel.study.value.peopleCount, + ) + } + } + } + private fun observeStartStudy() { studyDetailViewModel.isStartStudy.observe(this) { isStartStudy -> if (isStartStudy) { @@ -127,11 +147,19 @@ class StudyDetailActivity : } } + private fun observeCanStartStudy() { + studyDetailViewModel.canStudyStart.observe(this) { cantStartStudy -> + binding.btnStudyDetailMain.isEnabled = cantStartStudy + } + } + + private fun showToast(@StringRes stringRes: Int) = + Toast.makeText(this, getString(stringRes), Toast.LENGTH_SHORT).show() + companion object { private const val FIRST_PAGE = 1 private const val ROLE_INDEX_STUDY_MASTER = 0 private const val NON_EXISTENCE_STUDY_ID = 0L - private const val DEFAULT_PERIOD_SYMBOL = 'd' private const val STRING_LAST_INDEX = 1 private const val KEY_STUDY_ID = "KEY_STUDY_ID" fun getIntent(context: Context, studyId: Long): Intent = diff --git a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailState.kt b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailState.kt index c660a3623..cbd407300 100644 --- a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailState.kt +++ b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailState.kt @@ -16,12 +16,12 @@ sealed class StudyDetailState( @DrawableRes val subButtonSrc: Int, ) { - object Master : StudyDetailState( + data class Master(private val canStartStudy: Boolean) : StudyDetailState( appBarTitle = R.string.study_detail_app_bar_study_master_title, mainButtonText = R.string.study_detail_button_start_study, mainButtonTextColor = R.color.white, subButtonSrc = R.drawable.ic_edit_20, - mainButtonIsEnabled = true, + mainButtonIsEnabled = canStartStudy, ) object Member : diff --git a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailViewModel.kt b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailViewModel.kt index 0006e2169..a9aaeb9e8 100644 --- a/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailViewModel.kt +++ b/android/app/src/main/java/com/created/team201/presentation/studyDetail/StudyDetailViewModel.kt @@ -1,7 +1,6 @@ package com.created.team201.presentation.studyDetail import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope @@ -14,35 +13,52 @@ import com.created.team201.data.remote.NetworkServiceModule import com.created.team201.data.repository.StudyDetailRepositoryImpl import com.created.team201.presentation.studyDetail.model.StudyDetailUIModel import com.created.team201.presentation.studyDetail.model.StudyMemberUIModel +import com.created.team201.util.NonNullLiveData +import com.created.team201.util.NonNullMutableLiveData import kotlinx.coroutines.launch class StudyDetailViewModel private constructor( private val studyDetailRepository: StudyDetailRepository, ) : ViewModel() { - private val _study: MutableLiveData = MutableLiveData() - val study: LiveData get() = _study - private val _studyParticipants: MutableLiveData> = MutableLiveData() - val studyParticipants: LiveData> get() = _studyParticipants - private val _state: MutableLiveData = - MutableLiveData(StudyDetailState.Nothing) + private val _study: NonNullMutableLiveData = + NonNullMutableLiveData(StudyDetailUIModel.INVALID_STUDY_DETAIL) + val study: NonNullLiveData get() = _study + + private val _studyParticipants: NonNullMutableLiveData> = + NonNullMutableLiveData(listOf()) + val studyParticipants: NonNullLiveData> get() = _studyParticipants + + private val _state: NonNullMutableLiveData = + NonNullMutableLiveData(StudyDetailState.Nothing) val state: LiveData get() = _state - private val _isStartStudy: MutableLiveData = MutableLiveData(false) - val isStartStudy: LiveData get() = _isStartStudy - private val _isFullMember: MutableLiveData = MutableLiveData(false) - val isFullMember: LiveData get() = _isFullMember - fun fetchStudyDetail(studyId: Long) { + private val _isStartStudy: NonNullMutableLiveData = NonNullMutableLiveData(false) + val isStartStudy: NonNullLiveData get() = _isStartStudy + + private val _isFullMember: NonNullMutableLiveData = NonNullMutableLiveData(false) + val isFullMember: NonNullLiveData get() = _isFullMember + + private val _canStudyStart: NonNullMutableLiveData = NonNullMutableLiveData(false) + val canStudyStart: NonNullLiveData get() = _canStudyStart + + private val _studyMemberCount: NonNullMutableLiveData = NonNullMutableLiveData(0) + val studyMemberCount: NonNullLiveData get() = _studyMemberCount + + fun fetchStudyDetail(studyId: Long, notifyInvalidStudy: () -> Unit) { viewModelScope.launch { runCatching { - val studyDetail = studyDetailRepository.getStudyDetail(studyId).toUIModel() - studyDetail + studyDetailRepository.getStudyDetail(studyId).toUIModel() }.onSuccess { _study.value = it _studyParticipants.value = it.studyMembers - _isFullMember.value = it.peopleCount == _studyParticipants.value!!.size - _state.value = it.role.toStudyDetailState() + _isFullMember.value = it.peopleCount == _studyParticipants.value.size + _state.value = it.role.toStudyDetailState(it.canStartStudy) + _studyMemberCount.value = it.memberCount + _canStudyStart.value = it.canStartStudy if (it.role == Role.MASTER) fetchApplicants(studyId) + }.onFailure { + notifyInvalidStudy() } } } @@ -63,10 +79,10 @@ class StudyDetailViewModel private constructor( studyDetailRepository.getStudyApplicants(studyId) }.onSuccess { members -> _studyParticipants.value = - _studyParticipants.value?.plus( + _studyParticipants.value.plus( members.map { it.toUIModel( - study.value?.studyMasterId ?: 0L, + study.value.studyMasterId, true, ) }, @@ -90,11 +106,13 @@ class StudyDetailViewModel private constructor( runCatching { studyDetailRepository.acceptApplicant(studyId, memberId) }.onFailure { // 204 No Content가 onFailure로 가는 현상이 있습니다. - val studyParticipants = _studyParticipants.value ?: listOf() + val studyParticipants = _studyParticipants.value val acceptedMember = - studyParticipants.find { it.id == memberId } ?: StudyMemberUIModel.DUMMY + studyParticipants.find { it.id == memberId } ?: return@launch _studyParticipants.value = studyParticipants.minus(acceptedMember) + acceptedMember.copy(isApplicant = false) + _canStudyStart.value = StudyDetail.canStartStudy(studyParticipants.size) + _studyMemberCount.value = _studyMemberCount.value.plus(1) } } } @@ -109,7 +127,8 @@ class StudyDetailViewModel private constructor( startDate = this.startAt, period = this.totalRoundCount.toString(), cycle = this.periodOfRound, - applicantCount = this.members.count(), + memberCount = this.members.size, + canStartStudy = StudyDetail.canStartStudy(this.numberOfCurrentMembers), studyMembers = this.members.map { it.toUIModel(this.studyMasterId, isApplicant = false) }, ) @@ -124,8 +143,8 @@ class StudyDetailViewModel private constructor( tier = this.tier, ) - private fun Role.toStudyDetailState(): StudyDetailState = when (this) { - Role.MASTER -> StudyDetailState.Master + private fun Role.toStudyDetailState(canStartStudy: Boolean): StudyDetailState = when (this) { + Role.MASTER -> StudyDetailState.Master(canStartStudy) Role.MEMBER -> StudyDetailState.Member Role.APPLICANT -> StudyDetailState.Applicant Role.NOTHING -> StudyDetailState.Nothing diff --git a/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyDetailUIModel.kt b/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyDetailUIModel.kt index e5b6de155..aaefa539b 100644 --- a/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyDetailUIModel.kt +++ b/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyDetailUIModel.kt @@ -12,6 +12,24 @@ data class StudyDetailUIModel( val startDate: String, val period: String, val cycle: String, - val applicantCount: Int, + val memberCount: Int, + val canStartStudy: Boolean, val studyMembers: List, -) +) { + companion object { + val INVALID_STUDY_DETAIL = StudyDetailUIModel( + studyMasterId = 0, + isMaster = false, + title = "", + introduction = "", + peopleCount = 0, + role = Role.NOTHING, + startDate = "", + period = "", + cycle = "", + memberCount = 0, + canStartStudy = false, + studyMembers = listOf(), + ) + } +} diff --git a/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyMemberUIModel.kt b/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyMemberUIModel.kt index 3a2bb446a..bd499b0e3 100644 --- a/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyMemberUIModel.kt +++ b/android/app/src/main/java/com/created/team201/presentation/studyDetail/model/StudyMemberUIModel.kt @@ -8,16 +8,4 @@ data class StudyMemberUIModel( val name: String, val successRate: Int, val tier: Int, -) { - companion object { - val DUMMY = StudyMemberUIModel( - id = 0L, - isMaster = true, - isApplicant = false, - tier = 3, - name = "bandal", - successRate = 90, - profileImageUrl = "https://opgg-com-image.akamaized.net/attach/images/20200321020018.373875.jpg", - ) - } -} +) diff --git a/android/app/src/main/res/layout/activity_study_detail.xml b/android/app/src/main/res/layout/activity_study_detail.xml index 93b5b7d2e..96261ffea 100644 --- a/android/app/src/main/res/layout/activity_study_detail.xml +++ b/android/app/src/main/res/layout/activity_study_detail.xml @@ -169,7 +169,7 @@ android:orientation="vertical" android:overScrollMode="never" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" - app:layout_constraintBottom_toTopOf="@id/btn_study_detail_main_not_study_master" + app:layout_constraintBottom_toTopOf="@id/btn_study_detail_main" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tv_study_detail_study_people_title" @@ -190,7 +190,7 @@ android:padding="16dp" android:src="@{context.getDrawable(viewModel.state.subButtonSrc)}" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toStartOf="@+id/btn_study_detail_main_not_study_master" + app:layout_constraintEnd_toStartOf="@id/btn_study_detail_main" app:layout_constraintHorizontal_bias="0.5" app:layout_constraintHorizontal_chainStyle="spread" app:layout_constraintHorizontal_weight="1" @@ -198,7 +198,7 @@ tools:src="@drawable/ic_dm" /> + app:layout_constraintStart_toEndOf="@id/btn_study_detail_sub" + tools:text="@string/study_detail_study_accept_waiting" /> diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 847abf65b..f1e970706 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -72,12 +72,13 @@ 참여하기(%d/%d) 시작하기(%d/%d) 스터디장에게 문의하기 - 스터디장의 수락을 기다리고있어요. - 스터디 시작을 기다리고있어요. + 수락을 기다리고 있어요 + 시작을 기다리고 있어요 더이상 스터디 멤버를 받을 수 없습니다. %d일 %d주 %s회차 + 스터디를 찾을 수 없습니다. 스터디 성공률 %d%% diff --git a/android/domain/src/main/java/com/created/domain/model/StudyDetail.kt b/android/domain/src/main/java/com/created/domain/model/StudyDetail.kt index 4e599535e..55dce5f93 100644 --- a/android/domain/src/main/java/com/created/domain/model/StudyDetail.kt +++ b/android/domain/src/main/java/com/created/domain/model/StudyDetail.kt @@ -15,4 +15,11 @@ data class StudyDetail( val introduction: String, val members: List, val rounds: List, -) +) { + companion object { + private const val START_MEMBER_CONDITION = 2 + + fun canStartStudy(numberOfCurrentMembers: Int): Boolean = + numberOfCurrentMembers >= START_MEMBER_CONDITION + } +}