diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1d306d60..dbdfe748 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,10 @@ + + diff --git a/data/src/main/java/com/cheocharm/data/error/ErrorData.kt b/data/src/main/java/com/cheocharm/data/error/ErrorData.kt index afda20cc..0ddd28cc 100644 --- a/data/src/main/java/com/cheocharm/data/error/ErrorData.kt +++ b/data/src/main/java/com/cheocharm/data/error/ErrorData.kt @@ -14,4 +14,5 @@ sealed class ErrorData( data class RefreshAccessTokenUnavailable(override val message: String) : ErrorData(message) data class SearchGroupUnavailable(override val message: String) : ErrorData(message) data class JoinGroupUnavailable(override val message: String) : ErrorData(message) + data class GroupCreateUnavailable(override val message: String) : ErrorData(message) } diff --git a/data/src/main/java/com/cheocharm/data/error/ErrorMapper.kt b/data/src/main/java/com/cheocharm/data/error/ErrorMapper.kt index 18a50230..25156247 100644 --- a/data/src/main/java/com/cheocharm/data/error/ErrorMapper.kt +++ b/data/src/main/java/com/cheocharm/data/error/ErrorMapper.kt @@ -12,4 +12,5 @@ internal fun ErrorData.toDomain(): Error = when (this) { is ErrorData.RefreshAccessTokenUnavailable -> Error.RefreshAccessTokenUnavailable(message) is ErrorData.SearchGroupUnavailable -> Error.SearchGroupUnavailable(message) is ErrorData.JoinGroupUnavailable -> Error.JoinGroupUnavailable(message) + is ErrorData.GroupCreateUnavailable -> Error.GroupCreateUnavailable(message) } diff --git a/data/src/main/java/com/cheocharm/data/repository/GroupRepositoryImpl.kt b/data/src/main/java/com/cheocharm/data/repository/GroupRepositoryImpl.kt index 4cfac9eb..1bccd00c 100644 --- a/data/src/main/java/com/cheocharm/data/repository/GroupRepositoryImpl.kt +++ b/data/src/main/java/com/cheocharm/data/repository/GroupRepositoryImpl.kt @@ -5,6 +5,8 @@ import com.cheocharm.data.error.ErrorData import com.cheocharm.data.error.toDomain import com.cheocharm.data.source.GroupRemoteDataSource import com.cheocharm.domain.model.Group +import com.cheocharm.domain.model.GroupJoin +import com.cheocharm.domain.model.group.GroupCreateRequest import com.cheocharm.domain.repository.GroupRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject @@ -17,7 +19,7 @@ class GroupRepositoryImpl @Inject constructor( return groupRemoteDataSource.fetchGroupSearchList(searchGroupName) } - override suspend fun joinGroup(groupName: String): Result { + override suspend fun joinGroup(groupName: String): Result { val result = groupRemoteDataSource.joinGroup(groupName) return when (val exception = result.exceptionOrNull()) { is ErrorData -> Result.failure(exception.toDomain()) @@ -25,4 +27,13 @@ class GroupRepositoryImpl @Inject constructor( else -> Result.failure(exception) } } + + override suspend fun createGroup(groupCreateRequest: GroupCreateRequest): Result { + val result = groupRemoteDataSource.createGroup(groupCreateRequest) + return when (val exception = result.exceptionOrNull()) { + is ErrorData -> Result.failure(exception.toDomain()) + null -> result + else -> Result.failure(exception) + } + } } diff --git a/data/src/main/java/com/cheocharm/data/source/GroupRemoteDataSource.kt b/data/src/main/java/com/cheocharm/data/source/GroupRemoteDataSource.kt index 61718f52..ff1c9dc0 100644 --- a/data/src/main/java/com/cheocharm/data/source/GroupRemoteDataSource.kt +++ b/data/src/main/java/com/cheocharm/data/source/GroupRemoteDataSource.kt @@ -2,11 +2,15 @@ package com.cheocharm.data.source import androidx.paging.PagingData import com.cheocharm.domain.model.Group +import com.cheocharm.domain.model.GroupJoin +import com.cheocharm.domain.model.group.GroupCreateRequest import kotlinx.coroutines.flow.Flow interface GroupRemoteDataSource { fun fetchGroupSearchList(searchGroupName: String): Flow> - suspend fun joinGroup(groupName: String): Result + suspend fun joinGroup(groupName: String): Result + + suspend fun createGroup(groupCreateRequest: GroupCreateRequest): Result } diff --git a/domain/src/main/java/com/cheocharm/domain/model/Error.kt b/domain/src/main/java/com/cheocharm/domain/model/Error.kt index 74d19b83..231adccc 100644 --- a/domain/src/main/java/com/cheocharm/domain/model/Error.kt +++ b/domain/src/main/java/com/cheocharm/domain/model/Error.kt @@ -14,4 +14,5 @@ sealed class Error( data class RefreshAccessTokenUnavailable(override val message: String) : Error() data class SearchGroupUnavailable(override val message: String) : Error() data class JoinGroupUnavailable(override val message: String) : Error() + data class GroupCreateUnavailable(override val message: String) : Error() } diff --git a/domain/src/main/java/com/cheocharm/domain/model/GroupJoin.kt b/domain/src/main/java/com/cheocharm/domain/model/GroupJoin.kt new file mode 100644 index 00000000..cc683fb9 --- /dev/null +++ b/domain/src/main/java/com/cheocharm/domain/model/GroupJoin.kt @@ -0,0 +1,6 @@ +package com.cheocharm.domain.model + +data class GroupJoin( + val alreadyJoin: Boolean, + val status: String +) diff --git a/domain/src/main/java/com/cheocharm/domain/model/group/GroupCreateRequest.kt b/domain/src/main/java/com/cheocharm/domain/model/group/GroupCreateRequest.kt new file mode 100644 index 00000000..32ef9795 --- /dev/null +++ b/domain/src/main/java/com/cheocharm/domain/model/group/GroupCreateRequest.kt @@ -0,0 +1,10 @@ +package com.cheocharm.domain.model.group + +import java.io.File + +data class GroupCreateRequest( + val groupImage: File, + val groupName: String, + val groupBio: String, + val changeStatus: Boolean +) diff --git a/domain/src/main/java/com/cheocharm/domain/repository/GroupRepository.kt b/domain/src/main/java/com/cheocharm/domain/repository/GroupRepository.kt index dfef6d25..56fcc8b6 100644 --- a/domain/src/main/java/com/cheocharm/domain/repository/GroupRepository.kt +++ b/domain/src/main/java/com/cheocharm/domain/repository/GroupRepository.kt @@ -2,11 +2,15 @@ package com.cheocharm.domain.repository import androidx.paging.PagingData import com.cheocharm.domain.model.Group +import com.cheocharm.domain.model.GroupJoin +import com.cheocharm.domain.model.group.GroupCreateRequest import kotlinx.coroutines.flow.Flow interface GroupRepository { fun searchGroup(searchGroupName: String): Flow> - suspend fun joinGroup(groupName: String): Result + suspend fun joinGroup(groupName: String): Result + + suspend fun createGroup(groupCreateRequest: GroupCreateRequest): Result } diff --git a/domain/src/main/java/com/cheocharm/domain/usecase/group/GroupCreateUseCase.kt b/domain/src/main/java/com/cheocharm/domain/usecase/group/GroupCreateUseCase.kt new file mode 100644 index 00000000..eee1f9ed --- /dev/null +++ b/domain/src/main/java/com/cheocharm/domain/usecase/group/GroupCreateUseCase.kt @@ -0,0 +1,13 @@ +package com.cheocharm.domain.usecase.group + +import com.cheocharm.domain.model.group.GroupCreateRequest +import com.cheocharm.domain.repository.GroupRepository +import javax.inject.Inject + +class GroupCreateUseCase @Inject constructor( + private val groupRepository: GroupRepository +) { + suspend operator fun invoke(groupCreateRequest: GroupCreateRequest): Result { + return groupRepository.createGroup(groupCreateRequest) + } +} diff --git a/domain/src/main/java/com/cheocharm/domain/usecase/group/JoinGroupUseCase.kt b/domain/src/main/java/com/cheocharm/domain/usecase/group/JoinGroupUseCase.kt index 9f0bf413..651f9251 100644 --- a/domain/src/main/java/com/cheocharm/domain/usecase/group/JoinGroupUseCase.kt +++ b/domain/src/main/java/com/cheocharm/domain/usecase/group/JoinGroupUseCase.kt @@ -1,12 +1,13 @@ package com.cheocharm.domain.usecase.group +import com.cheocharm.domain.model.GroupJoin import com.cheocharm.domain.repository.GroupRepository import javax.inject.Inject class JoinGroupUseCase @Inject constructor( private val groupRepository: GroupRepository ) { - suspend operator fun invoke(groupName: String): Result { + suspend operator fun invoke(groupName: String): Result { return groupRepository.joinGroup(groupName) } } diff --git a/presentation/build.gradle b/presentation/build.gradle index 2c49d378..8b7cbceb 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -80,4 +80,7 @@ dependencies { // paging implementation "androidx.paging:paging-runtime:$pagingVersion" + + // circle imageview + implementation 'de.hdodenhof:circleimageview:3.1.0' } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateActivity.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateActivity.kt new file mode 100644 index 00000000..bc90d971 --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateActivity.kt @@ -0,0 +1,36 @@ +package com.cheocharm.presentation.ui.group + +import android.content.Context +import android.graphics.Rect +import android.view.MotionEvent +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import com.cheocharm.presentation.R +import com.cheocharm.presentation.base.BaseActivity +import com.cheocharm.presentation.databinding.ActivityGroupCreateBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GroupCreateActivity : + BaseActivity(R.layout.activity_group_create) { + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + hideKeyboardWhenOutsideTouched(ev) + return super.dispatchTouchEvent(ev) + } + + private fun hideKeyboardWhenOutsideTouched(ev: MotionEvent?) { + if (ev?.action == MotionEvent.ACTION_DOWN) { + val v = currentFocus + if (v is EditText) { + val rect = Rect() + v.getGlobalVisibleRect(rect) + if (!rect.contains(ev.rawX.toInt(), ev.rawY.toInt())) { + v.clearFocus() + val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(v.windowToken, 0) + } + } + } + } +} diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateFragment.kt new file mode 100644 index 00000000..9d5a9d1a --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateFragment.kt @@ -0,0 +1,118 @@ +package com.cheocharm.presentation.ui.group + +import android.app.Activity +import android.content.Intent +import android.graphics.ImageDecoder +import android.os.Build +import android.os.Bundle +import android.provider.MediaStore +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged +import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.findNavController +import com.cheocharm.presentation.R +import com.cheocharm.presentation.base.BaseFragment +import com.cheocharm.presentation.common.EventObserver +import com.cheocharm.presentation.databinding.FragmentGroupCreateBinding +import com.cheocharm.presentation.util.UriUtil +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GroupCreateFragment : + BaseFragment(R.layout.fragment_group_create) { + + private val groupCreateViewModel: GroupCreateViewModel by activityViewModels() + private lateinit var galleryImageLauncher: ActivityResultLauncher + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.viewmodel = groupCreateViewModel + + initButtons() + initObservers() + initGalleryLauncher() + } + + private fun initButtons() { + binding.toolbarGroupCreate.setOnMenuItemClickListener { + when (it.itemId) { + R.id.location_confirm -> { + if (groupCreateViewModel.isGroupEnabled.value == true) + groupCreateViewModel.requestGroupCreate() + true + } + else -> { + false + } + } + } + binding.toolbarGroupCreate.setNavigationOnClickListener { + requireActivity().finish() + } + binding.ivGroupCreateGroup.setOnClickListener { + selectGroupImage() + } + binding.etGroupCreateName.doOnTextChanged { text, start, before, count -> + groupCreateViewModel.setGroupName(text.toString()) + groupCreateViewModel.checkGroupNameVerified() + groupCreateViewModel.checkGroupEnabled() + } + binding.etGroupCreateBio.doOnTextChanged { text, start, before, count -> + groupCreateViewModel.setGroupBio(text.toString()) + groupCreateViewModel.checkGroupEnabled() + } + } + + private fun initObservers() { + groupCreateViewModel.isGoToSearchEnabled.observe(viewLifecycleOwner, EventObserver { + //TODO: 검색화면으로 넘어가기 +// findNavController().navigate(R.id.action_groupCreateFragment_to_groupCreateSearchFragment) + }) + } + + private fun initGalleryLauncher() { + galleryImageLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> + if (activityResult.resultCode == Activity.RESULT_OK && activityResult.data != null) { + val imageUri = activityResult.data?.data + runCatching { + imageUri?.let { uri -> + val bitmap = if (Build.VERSION.SDK_INT < 28) { + MediaStore.Images.Media.getBitmap( + requireActivity().contentResolver, uri + ) + } else { + val source = ImageDecoder.createSource( + requireActivity().contentResolver, + uri + ) + ImageDecoder.decodeBitmap(source) + } + val file = UriUtil.getFileFromUri(requireActivity(), uri) + groupCreateViewModel.setGroupImage(file) + binding.ivGroupCreateGroup.setImageBitmap(bitmap) + binding.tvGroupCreateImage.isVisible = false + binding.ivGroupCreateImage.isVisible = false + } + }.onFailure { + Log.e("SignUpProfileFragment", "${it.message}") + } + } else if (activityResult.resultCode == Activity.RESULT_CANCELED) { + Toast.makeText(requireActivity(), "사진 선택 취소", Toast.LENGTH_SHORT).show() + } + } + } + + private fun selectGroupImage() { + val intent = Intent(Intent.ACTION_PICK).apply { + type = "image/*" + } + galleryImageLauncher.launch(intent) + } +} diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateSearchFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateSearchFragment.kt new file mode 100644 index 00000000..f75acacf --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateSearchFragment.kt @@ -0,0 +1,15 @@ +package com.cheocharm.presentation.ui.group + +import androidx.fragment.app.activityViewModels +import com.cheocharm.presentation.R +import com.cheocharm.presentation.base.BaseFragment +import com.cheocharm.presentation.databinding.FragmentGroupCreateSearchBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class GroupCreateSearchFragment : + BaseFragment(R.layout.fragment_group_create_search) { + + private val groupCreateViewModel by activityViewModels() + +} diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateViewModel.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateViewModel.kt new file mode 100644 index 00000000..34af4077 --- /dev/null +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/group/GroupCreateViewModel.kt @@ -0,0 +1,103 @@ +package com.cheocharm.presentation.ui.group + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.cheocharm.domain.model.group.GroupCreateRequest +import com.cheocharm.domain.model.group.GroupCreateSearchResult +import com.cheocharm.domain.usecase.group.GroupCreateUseCase +import com.cheocharm.presentation.common.Event +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import java.io.File +import java.util.regex.Pattern +import javax.inject.Inject + +@HiltViewModel +class GroupCreateViewModel @Inject constructor( + private val groupCreateUseCase: GroupCreateUseCase +) : ViewModel() { + + private val _groupImage = MutableLiveData() + val groupImage: LiveData + get() = _groupImage + + private val _groupName = MutableLiveData() + val groupName: LiveData + get() = _groupName + + private val _groupBio = MutableLiveData() + val groupBio: LiveData + get() = _groupBio + + private var isGroupNameVerified = false + + val isSearchEnabled = MutableLiveData() + + private val _isGroupEnabled = MutableLiveData(false) + val isGroupEnabled: LiveData + get() = _isGroupEnabled + + private val _isGoToSearchEnabled = MutableLiveData>() + val isGoToSearchEnabled: LiveData> + get() = _isGoToSearchEnabled + + // search + private val _searchMemberName = MutableLiveData() + val searchMemberName: LiveData + get() = _searchMemberName + + private val _searchResultList = MutableLiveData>() + val searchResultList: LiveData> + get() = _searchResultList + + private val _inviteMemberList = MutableLiveData>() + val inviteMemberList: LiveData> + get() = _inviteMemberList + + fun setGroupImage(groupImage: File) { + _groupImage.value = groupImage + } + + fun setGroupName(name: String) { + _groupName.value = name + } + + fun setGroupBio(bio: String) { + _groupBio.value = bio + } + + fun checkGroupNameVerified() { + val pattern = Pattern.compile("^[가-힣a-zA-Z0-9]{2,12}$") + isGroupNameVerified = pattern.matcher(groupName.value ?: return).find() == true + } + + fun checkGroupEnabled() { + _isGroupEnabled.value = groupImage.value != null && groupName.value.isNullOrEmpty() + .not() && groupBio.value.isNullOrEmpty().not() && isGroupNameVerified + } + + fun requestGroupCreate() { + if (isGroupEnabled.value != true) return + + val image = groupImage.value ?: return + val name = groupName.value ?: return + val bio = groupBio.value ?: return + + viewModelScope.launch { + groupCreateUseCase.invoke( + GroupCreateRequest( + image, + name, + bio, + isSearchEnabled.value ?: false + ) + ) + .onSuccess { _isGoToSearchEnabled.value = Event(Unit) } + .onFailure { + // TODO: 그룹 셍성 실패 토스트 띄우기 + } + } + } +} diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt index 821c8623..9df89905 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchFragment.kt @@ -1,5 +1,6 @@ package com.cheocharm.presentation.ui.search +import android.content.Intent import android.os.Bundle import android.view.View import android.view.inputmethod.EditorInfo @@ -11,6 +12,7 @@ import com.cheocharm.presentation.R import com.cheocharm.presentation.databinding.ActivityMainBinding import com.cheocharm.presentation.databinding.FragmentSearchBinding import com.cheocharm.presentation.ui.MainActivity +import com.cheocharm.presentation.ui.group.GroupCreateActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint @@ -27,6 +29,7 @@ class SearchFragment : BaseFragment(R.layout.fragment_sea mainActivityBinding = (activity as MainActivity).getBinding() initEditTexts() + initButtons() initObservers() } @@ -49,6 +52,13 @@ class SearchFragment : BaseFragment(R.layout.fragment_sea } } + private fun initButtons() { + binding.btnSearchGroupCreate.setOnClickListener { + val intent = Intent(requireActivity(), GroupCreateActivity::class.java) + startActivity(intent) + } + } + private fun initObservers() { } diff --git a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchViewModel.kt b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchViewModel.kt index 12d0ff5c..967a465d 100644 --- a/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchViewModel.kt +++ b/presentation/src/main/java/com/cheocharm/presentation/ui/search/SearchViewModel.kt @@ -51,8 +51,9 @@ class SearchViewModel @Inject constructor( viewModelScope.launch { selectedGroup.value?.name?.let { joinGroupUseCase.invoke(it) - .onSuccess { - _searchGroupJoinBottom.value = Event(Unit) + .onSuccess { groupJoin -> + if (groupJoin.alreadyJoin.not()) _searchGroupJoinBottom.value = Event(Unit) + else setToastMessage(groupJoin.status) }.onFailure { throwable -> when (throwable) { is Error.JoinGroupUnavailable -> setToastMessage(throwable.message) diff --git a/presentation/src/main/res/color/switch_track.xml b/presentation/src/main/res/color/switch_track.xml new file mode 100644 index 00000000..8dfcf497 --- /dev/null +++ b/presentation/src/main/res/color/switch_track.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/presentation/src/main/res/drawable/bg_cursor.xml b/presentation/src/main/res/drawable/bg_cursor.xml index 01a99f38..4956f243 100644 --- a/presentation/src/main/res/drawable/bg_cursor.xml +++ b/presentation/src/main/res/drawable/bg_cursor.xml @@ -1,5 +1,5 @@ - + diff --git a/presentation/src/main/res/drawable/bg_outlined_radius_10dp.xml b/presentation/src/main/res/drawable/bg_outlined_radius_10dp.xml new file mode 100644 index 00000000..1cb93815 --- /dev/null +++ b/presentation/src/main/res/drawable/bg_outlined_radius_10dp.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/presentation/src/main/res/layout/activity_group_create.xml b/presentation/src/main/res/layout/activity_group_create.xml new file mode 100644 index 00000000..649a1913 --- /dev/null +++ b/presentation/src/main/res/layout/activity_group_create.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/fragment_group_create.xml b/presentation/src/main/res/layout/fragment_group_create.xml new file mode 100644 index 00000000..91dc4362 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_group_create.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/layout/fragment_group_create_search.xml b/presentation/src/main/res/layout/fragment_group_create_search.xml new file mode 100644 index 00000000..7d480fa6 --- /dev/null +++ b/presentation/src/main/res/layout/fragment_group_create_search.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/presentation/src/main/res/navigation/nav_group.xml b/presentation/src/main/res/navigation/nav_group.xml new file mode 100644 index 00000000..de3cf92e --- /dev/null +++ b/presentation/src/main/res/navigation/nav_group.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/presentation/src/main/res/values/dimens.xml b/presentation/src/main/res/values/dimens.xml index bc9960e9..843fd90a 100644 --- a/presentation/src/main/res/values/dimens.xml +++ b/presentation/src/main/res/values/dimens.xml @@ -3,6 +3,7 @@ 5dp 8dp 12dp + 14dp 16dp 18dp 20dp @@ -45,4 +46,8 @@ 120dp 10dp 15dp + + 37dp + 2dp + 120dp diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 3edb8562..a5e863fd 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -63,4 +63,11 @@ 그룹 가입하기 %s의 그룹장에게\n가입요청이 전달되었습니다. 가입이 승인되면 홈에서\n가입된 그룹을 볼 수 있어요! + + 그룹 만들기 + 그룹 이름 입력 + 그룹의 소개글을 간략하게 적어주세요. + 검색 비허용 + 그룹이 검색결과로 뜨지 않습니다.\n오직 그룹장의 초대로 멤버가 들어올 수 있습니다. + 사진 추가 diff --git a/presentation/src/main/res/values/themes.xml b/presentation/src/main/res/values/themes.xml index a9730e03..29f37866 100644 --- a/presentation/src/main/res/values/themes.xml +++ b/presentation/src/main/res/values/themes.xml @@ -36,4 +36,9 @@ @color/grey_dark @drawable/bg_cursor + + diff --git a/remote/src/main/java/com/cheocharm/remote/api/GroupApi.kt b/remote/src/main/java/com/cheocharm/remote/api/GroupApi.kt index 997cd516..ce9db3f3 100644 --- a/remote/src/main/java/com/cheocharm/remote/api/GroupApi.kt +++ b/remote/src/main/java/com/cheocharm/remote/api/GroupApi.kt @@ -1,20 +1,52 @@ package com.cheocharm.remote.api import com.cheocharm.remote.model.BaseResponse +import com.cheocharm.remote.model.request.GroupCreateDto +import com.cheocharm.remote.model.response.group.GroupJoinResponse +import com.cheocharm.remote.model.response.group.GroupMemberResponse +import com.cheocharm.remote.model.response.group.GroupResponse import com.cheocharm.remote.model.response.group.GroupSearchResponse -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.http.* interface GroupApi { @GET("group") suspend fun fetchGroupSearchList( @Query("page") page: Int, - @Query("searchName") searchName: String + @Query("groupName") searchName: String, + @Query("cursorId") cursorId: Int ): BaseResponse + @Multipart + @POST("group") + suspend fun createGroup( + @Part("dto") dto: GroupCreateDto, + @Part file: MultipartBody.Part + ): BaseResponse + @POST("group/join") - suspend fun joinGroup(@Body body: HashMap): BaseResponse + suspend fun joinGroup(@Body body: HashMap): BaseResponse + + @PATCH("group/join") + suspend fun modifyJoinGroup(@Body body: RequestBody): BaseResponse + + @POST("group/invite") + suspend fun inviteGroup(@Body body: RequestBody): BaseResponse + + @PATCH("group/status") + suspend fun modifyGroupStatus(@Body body: RequestBody): BaseResponse + + @PATCH("group/exit") + suspend fun exitGroup(@Body body: RequestBody): BaseResponse + + @PATCH("group/chief") + suspend fun modifyChief(@Body body: RequestBody): BaseResponse + + @GET("group/mygroup") + suspend fun fetchMyGroup(): BaseResponse> + + @GET("group/member") + suspend fun fetchGroupMember(@Query("groupId") groupId: Int): BaseResponse> } diff --git a/remote/src/main/java/com/cheocharm/remote/mapper/GroupMapper.kt b/remote/src/main/java/com/cheocharm/remote/mapper/GroupMapper.kt index d1fcdc01..ab5aff89 100644 --- a/remote/src/main/java/com/cheocharm/remote/mapper/GroupMapper.kt +++ b/remote/src/main/java/com/cheocharm/remote/mapper/GroupMapper.kt @@ -1,8 +1,12 @@ package com.cheocharm.remote.mapper import com.cheocharm.domain.model.Group +import com.cheocharm.domain.model.GroupJoin import com.cheocharm.domain.model.GroupMember import com.cheocharm.domain.model.GroupSearch +import com.cheocharm.domain.model.group.GroupCreateRequest +import com.cheocharm.remote.model.request.GroupCreateDto +import com.cheocharm.remote.model.response.group.GroupJoinResponse import com.cheocharm.remote.model.response.group.GroupSearchResponse // remote -> domain @@ -15,3 +19,13 @@ internal fun GroupSearchResponse.toDomain(): GroupSearch { } return GroupSearch(hasNextPage, groupResultList) } + +// remote -> domain +internal fun GroupJoinResponse.toDomain(): GroupJoin { + return GroupJoin(alreadyJoin, status) +} + +// domain -> remote +internal fun GroupCreateRequest.toDto(): GroupCreateDto { + return GroupCreateDto(groupName, groupBio, changeStatus) +} diff --git a/remote/src/main/java/com/cheocharm/remote/model/request/GroupCreateDto.kt b/remote/src/main/java/com/cheocharm/remote/model/request/GroupCreateDto.kt new file mode 100644 index 00000000..ebc55f76 --- /dev/null +++ b/remote/src/main/java/com/cheocharm/remote/model/request/GroupCreateDto.kt @@ -0,0 +1,7 @@ +package com.cheocharm.remote.model.request + +data class GroupCreateDto( + val groupName: String, + val bio: String, + val changeStatus: Boolean +) diff --git a/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupJoinResponse.kt b/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupJoinResponse.kt new file mode 100644 index 00000000..f5c14181 --- /dev/null +++ b/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupJoinResponse.kt @@ -0,0 +1,6 @@ +package com.cheocharm.remote.model.response.group + +data class GroupJoinResponse( + val alreadyJoin: Boolean, + val status: String +) diff --git a/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupMemberResponse.kt b/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupMemberResponse.kt new file mode 100644 index 00000000..5ef1cd13 --- /dev/null +++ b/remote/src/main/java/com/cheocharm/remote/model/response/group/GroupMemberResponse.kt @@ -0,0 +1,8 @@ +package com.cheocharm.remote.model.response.group + +data class GroupMemberResponse( + val userName: String, + val userImageUrl: String, + val userId: Int, + val invitationStatus: String +) diff --git a/remote/src/main/java/com/cheocharm/remote/source/GroupRemoteDataSourceImpl.kt b/remote/src/main/java/com/cheocharm/remote/source/GroupRemoteDataSourceImpl.kt index d1acb7ba..2e0b212b 100644 --- a/remote/src/main/java/com/cheocharm/remote/source/GroupRemoteDataSourceImpl.kt +++ b/remote/src/main/java/com/cheocharm/remote/source/GroupRemoteDataSourceImpl.kt @@ -6,8 +6,15 @@ import androidx.paging.PagingData import com.cheocharm.data.error.ErrorData import com.cheocharm.data.source.GroupRemoteDataSource import com.cheocharm.domain.model.Group +import com.cheocharm.domain.model.GroupJoin +import com.cheocharm.domain.model.group.GroupCreateRequest import com.cheocharm.remote.api.GroupApi +import com.cheocharm.remote.mapper.toDomain +import com.cheocharm.remote.mapper.toDto import kotlinx.coroutines.flow.Flow +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody import java.net.UnknownHostException import javax.inject.Inject @@ -21,15 +28,42 @@ class GroupRemoteDataSourceImpl @Inject constructor( }.flow } - override suspend fun joinGroup(groupName: String): Result { + override suspend fun joinGroup(groupName: String): Result { val result = runCatching { groupApi.joinGroup(hashMapOf("groupName" to groupName)) } return when (val exception = result.exceptionOrNull()) { null -> { val response = result.getOrNull() ?: return Result.failure(Throwable(NullPointerException())) - if (response.statusCode == 200) Result.success(Unit) - else Result.failure(ErrorData.JoinGroupUnavailable(response.message)) + Result.success( + response.data?.toDomain() ?: return Result.failure( + ErrorData.JoinGroupUnavailable(response.message) + ) + ) + } + is UnknownHostException -> Result.failure(ErrorData.NetworkUnavailable) + else -> Result.failure(exception) + } + } + + override suspend fun createGroup(groupCreateRequest: GroupCreateRequest): Result { + val groupCreateDto = groupCreateRequest.toDto() + val fileRequestBody = MultipartBody.Part.createFormData( + "file", + groupCreateRequest.groupImage.name, + groupCreateRequest.groupImage.asRequestBody("image/*".toMediaType()) + ) + + val result = runCatching { groupApi.createGroup(groupCreateDto, fileRequestBody) } + return when (val exception = result.exceptionOrNull()) { + null -> { + val response = + result.getOrNull() ?: return Result.failure(Throwable(NullPointerException())) + Result.success( + if (response.statusCode != 200) return Result.failure( + ErrorData.GroupCreateUnavailable(response.message) + ) else response.data ?: "" + ) } is UnknownHostException -> Result.failure(ErrorData.NetworkUnavailable) else -> Result.failure(exception) diff --git a/remote/src/main/java/com/cheocharm/remote/source/GroupSearchPagingSource.kt b/remote/src/main/java/com/cheocharm/remote/source/GroupSearchPagingSource.kt index 63ef4c69..1a7fa5a5 100644 --- a/remote/src/main/java/com/cheocharm/remote/source/GroupSearchPagingSource.kt +++ b/remote/src/main/java/com/cheocharm/remote/source/GroupSearchPagingSource.kt @@ -22,7 +22,7 @@ class GroupSearchPagingSource( override suspend fun load(params: LoadParams): LoadResult { val start = params.key ?: 0 - val result = runCatching { groupApi.fetchGroupSearchList(start, groupSearchName) } + val result = runCatching { groupApi.fetchGroupSearchList(start, groupSearchName, 0) } return when (val exception = result.exceptionOrNull()) { null -> {