diff --git a/android/2023-emmsale/app/src/main/AndroidManifest.xml b/android/2023-emmsale/app/src/main/AndroidManifest.xml index 24f2124ee..50eee8eed 100644 --- a/android/2023-emmsale/app/src/main/AndroidManifest.xml +++ b/android/2023-emmsale/app/src/main/AndroidManifest.xml @@ -22,6 +22,9 @@ android:theme="@style/Theme.Emmsale" android:usesCleartextTraffic="true" tools:targetApi="33"> + + - diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/FeedResponse.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/FeedResponse.kt index b889c375c..090abc8af 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/FeedResponse.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/FeedResponse.kt @@ -11,6 +11,8 @@ data class FeedResponse( val id: Long, @SerialName("eventId") val eventId: Long, + @SerialName("eventName") + val eventName: String = "", @SerialName("title") val title: String, @SerialName("content") diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/MyPostResponse.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/MyPostResponse.kt deleted file mode 100644 index a0ab4e9e4..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/MyPostResponse.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.emmsale.data.apiModel.response - -import com.emmsale.data.apiModel.serializer.DateSerializer -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import java.time.LocalDate - -@Serializable -data class MyPostResponse( - @SerialName("postId") - val postId: Long, - @SerialName("memberId") - val memberId: Long, - @SerialName("eventId") - val eventId: Long, - @SerialName("eventName") - val eventName: String, - @SerialName("content") - val content: String, - @SerialName("updatedAt") - @Serializable(with = DateSerializer::class) - val updatedAt: LocalDate, -) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/RecruitmentResponse.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/RecruitmentResponse.kt index 7f38b58c4..6234e6846 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/RecruitmentResponse.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/apiModel/response/RecruitmentResponse.kt @@ -20,6 +20,8 @@ data class RecruitmentResponse( val member: MemberResponse, @SerialName("eventId") val eventId: Long, + @SerialName("eventName") + val eventName: String = "", ) @Serializable diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/FeedMapper.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/FeedMapper.kt index 246b5b7b7..d1615ea6f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/FeedMapper.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/FeedMapper.kt @@ -2,13 +2,17 @@ package com.emmsale.data.mapper import com.emmsale.BuildConfig import com.emmsale.data.apiModel.response.FeedResponse +import com.emmsale.data.model.Event import com.emmsale.data.model.Feed fun List.toData(): List = map { it.toData() } fun FeedResponse.toData(): Feed = Feed( id = id, - eventId = eventId, + event = Event( + id = eventId, + name = eventName, + ), title = title, content = content, writer = writer.toData(), diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/MyPostMapper.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/MyPostMapper.kt deleted file mode 100644 index b0dbbdbe3..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/MyPostMapper.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.emmsale.data.mapper - -import com.emmsale.data.apiModel.response.MyPostResponse -import com.emmsale.data.model.Event -import com.emmsale.data.model.Member -import com.emmsale.data.model.Recruitment - -fun List.toData(): List = map { it.toData() } -fun MyPostResponse.toData() = Recruitment( - id = postId, - writer = Member(), - event = Event(id = eventId, name = eventName), - content = content, - updatedDate = updatedAt, -) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/RecruitmentMapper.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/RecruitmentMapper.kt index 0918d7451..40d58aece 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/RecruitmentMapper.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/mapper/RecruitmentMapper.kt @@ -13,5 +13,8 @@ fun RecruitmentResponse.toData(): Recruitment = Recruitment( content = content, updatedDate = updatedAt, writer = member.toData(), - event = Event(id = eventId), + event = Event( + id = eventId, + name = eventName, + ), ) diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt index b2fd31b57..7c1b8ae0c 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/model/Feed.kt @@ -4,7 +4,7 @@ import java.time.LocalDateTime data class Feed( val id: Long = -1, - val eventId: Long = -1, + val event: Event = Event(), val title: String = "", val content: String = "", val writer: Member = Member(), diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultFeedRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultFeedRepository.kt index c0e3ed3ab..1cf45805d 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultFeedRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/concretes/DefaultFeedRepository.kt @@ -36,6 +36,12 @@ class DefaultFeedRepository @Inject constructor( .map(FeedResponse::toData) } + override suspend fun getMyFeeds(): ApiResponse> = withContext(dispatcher) { + feedService + .getMyFeeds() + .map { it.toData() } + } + override suspend fun deleteFeed( feedId: Long, ): ApiResponse = withContext(dispatcher) { diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/FeedRepository.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/FeedRepository.kt index 19c5250e4..0e0f65543 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/FeedRepository.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/repository/interfaces/FeedRepository.kt @@ -10,6 +10,8 @@ interface FeedRepository { suspend fun getFeed(feedId: Long): ApiResponse + suspend fun getMyFeeds(): ApiResponse> + suspend fun deleteFeed(feedId: Long): ApiResponse suspend fun uploadFeed( diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/FeedService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/FeedService.kt index fdedfb756..2e65e88ac 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/FeedService.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/FeedService.kt @@ -20,6 +20,9 @@ interface FeedService { @Query("event-id") eventId: Long, ): ApiResponse> + @GET("/feeds/my") + suspend fun getMyFeeds(): ApiResponse> + @GET("/feeds/{feedId}") suspend fun getFeed( @Path("feedId") feedId: Long, diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/MyPostService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/MyPostService.kt deleted file mode 100644 index 4750a0874..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/MyPostService.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.emmsale.data.service - -import com.emmsale.data.apiModel.response.MyPostResponse -import com.emmsale.data.common.retrofit.callAdapter.ApiResponse -import retrofit2.http.GET -import retrofit2.http.Query - -interface MyPostService { - @GET("/events/recruitment-posts") - suspend fun getMyPosts( - @Query("member-id") memberId: Long, - ): ApiResponse> -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/RecruitmentService.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/RecruitmentService.kt index 141304f5c..1d5cdc32a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/RecruitmentService.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/data/service/RecruitmentService.kt @@ -3,7 +3,6 @@ package com.emmsale.data.service import com.emmsale.data.apiModel.request.RecruitmentCreateRequest import com.emmsale.data.apiModel.request.RecruitmentDeleteRequest import com.emmsale.data.apiModel.request.RecruitmentReportCreateRequest -import com.emmsale.data.apiModel.response.MyPostResponse import com.emmsale.data.apiModel.response.RecruitmentReportResponse import com.emmsale.data.apiModel.response.RecruitmentResponse import com.emmsale.data.common.retrofit.callAdapter.ApiResponse @@ -31,7 +30,7 @@ interface RecruitmentService { @GET("/events/recruitment-posts") suspend fun getMemberRecruitments( @Query("member-id") memberId: Long, - ): ApiResponse> + ): ApiResponse> @POST("/events/{eventId}/recruitment-posts") suspend fun postRecruitment( diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/di/modules/service/ServiceModule.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/di/modules/service/ServiceModule.kt index 24a00e194..0bf1213d2 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/di/modules/service/ServiceModule.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/di/modules/service/ServiceModule.kt @@ -11,7 +11,6 @@ import com.emmsale.data.service.FeedService import com.emmsale.data.service.LoginService import com.emmsale.data.service.MemberService import com.emmsale.data.service.MessageRoomService -import com.emmsale.data.service.MyPostService import com.emmsale.data.service.NotificationService import com.emmsale.data.service.RecruitmentService import dagger.Module @@ -88,12 +87,6 @@ class ServiceModule { serviceFactory: ServiceFactory, ): BlockedMemberService = serviceFactory.create(BlockedMemberService::class.java) - @Provides - @Singleton - fun provideMyPostService( - serviceFactory: ServiceFactory, - ): MyPostService = serviceFactory.create(MyPostService::class.java) - @Provides @Singleton fun provideMessageRoomService( diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsFragment.kt new file mode 100644 index 000000000..4ccec922c --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsFragment.kt @@ -0,0 +1,55 @@ +package com.emmsale.presentation.ui.myFeedList + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentMyFeedsBinding +import com.emmsale.presentation.base.NetworkFragment +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration +import com.emmsale.presentation.ui.feedDetail.FeedDetailActivity +import com.emmsale.presentation.ui.myFeedList.recyclerView.MyFeedAdapter +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MyFeedsFragment : NetworkFragment(R.layout.fragment_my_feeds) { + + override val viewModel: MyFeedsViewModel by viewModels() + + private val myFeedAdapter: MyFeedAdapter by lazy { + MyFeedAdapter(onItemClick = ::navigateToDetail) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupDataBinding() + setupRecyclerView() + + observeMyFeeds() + } + + private fun setupDataBinding() { + binding.vm = viewModel + } + + private fun setupRecyclerView() { + binding.rvMyFeeds.apply { + adapter = myFeedAdapter + itemAnimator = null + addItemDecoration(DividerItemDecoration(context = context)) + } + } + + private fun observeMyFeeds() { + viewModel.myFeeds.observe(viewLifecycleOwner) { myFeeds -> + myFeedAdapter.submitList(myFeeds) + } + } + + private fun navigateToDetail(feedId: Long) { + FeedDetailActivity.startActivity( + requireContext(), + feedId = feedId, + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsViewModel.kt new file mode 100644 index 000000000..4214bd60d --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/MyFeedsViewModel.kt @@ -0,0 +1,37 @@ +package com.emmsale.presentation.ui.myFeedList + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.emmsale.data.model.Feed +import com.emmsale.data.repository.interfaces.FeedRepository +import com.emmsale.presentation.base.RefreshableViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Job +import javax.inject.Inject + +@HiltViewModel +class MyFeedsViewModel @Inject constructor( + private val feedRepository: FeedRepository, +) : RefreshableViewModel() { + + private val _myFeeds: MutableLiveData> = MutableLiveData(emptyList()) + val myFeeds: LiveData> = _myFeeds + + init { + fetchMyFeeds() + } + + private fun fetchMyFeeds(): Job = fetchData( + fetchData = { feedRepository.getMyFeeds() }, + onSuccess = { + _myFeeds.value = it.sortedByDescending { feed -> feed.createdAt } + }, + ) + + override fun refresh(): Job = refreshData( + refresh = { feedRepository.getMyFeeds() }, + onSuccess = { + _myFeeds.value = it.sortedByDescending { feed -> feed.createdAt } + }, + ) +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedAdapter.kt new file mode 100644 index 000000000..7655ea704 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedAdapter.kt @@ -0,0 +1,31 @@ +package com.emmsale.presentation.ui.myFeedList.recyclerView + +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.emmsale.data.model.Feed + +class MyFeedAdapter( + private val onItemClick: (recruitmentId: Long) -> Unit, +) : ListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Feed, + newItem: Feed, + ): Boolean = oldItem == newItem + + override fun areContentsTheSame( + oldItem: Feed, + newItem: Feed, + ): Boolean = oldItem.id == newItem.id + }, +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyFeedViewHolder { + return MyFeedViewHolder.create(parent, onItemClick) + } + + override fun onBindViewHolder(holder: MyFeedViewHolder, position: Int) { + holder.bind(getItem(position)) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedViewHolder.kt new file mode 100644 index 000000000..3382b77e8 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myFeedList/recyclerView/MyFeedViewHolder.kt @@ -0,0 +1,32 @@ +package com.emmsale.presentation.ui.myFeedList.recyclerView + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.emmsale.data.model.Feed +import com.emmsale.databinding.ItemMyFeedBinding + +class MyFeedViewHolder( + private val binding: ItemMyFeedBinding, + navigateToDetail: (feedId: Long) -> Unit, +) : RecyclerView.ViewHolder(binding.root) { + + init { + itemView.setOnClickListener { navigateToDetail(binding.feed!!.id) } + } + + fun bind(feed: Feed) { + binding.feed = feed + } + + companion object { + fun create( + parent: ViewGroup, + onItemClick: (feedId: Long) -> Unit, + ): MyFeedViewHolder { + val binding = + ItemMyFeedBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MyFeedViewHolder(binding, onItemClick) + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsActivity.kt new file mode 100644 index 000000000..80ec3d80a --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsActivity.kt @@ -0,0 +1,57 @@ +package com.emmsale.presentation.ui.myPostListPage + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.emmsale.R +import com.emmsale.databinding.ActivityMyPostsBinding +import com.emmsale.presentation.base.BaseActivity +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MyPostsActivity : BaseActivity(R.layout.activity_my_posts) { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setupFragmentStateAdapter() + setupToolbar() + setupMyPostsTabLayoutSelectedListener() + } + + private fun setupFragmentStateAdapter() { + binding.vpMyPosts.adapter = + MyPostsFragmentStateAdapter(this) + val tabNames = listOf( + getString(R.string.myposts_tab_feeds), + getString(R.string.myposts_tab_recruitments), + ) + TabLayoutMediator(binding.tablayoutMyposts, binding.vpMyPosts) { tab, position -> + tab.text = tabNames[position] + }.attach() + } + + private fun setupMyPostsTabLayoutSelectedListener() { + binding.tablayoutMyposts.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + binding.vpMyPosts.currentItem = tab.position + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + override fun onTabReselected(tab: TabLayout.Tab) {} + }) + } + + private fun setupToolbar() { + binding.tbMyPost.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } + } + + companion object { + fun startActivity(context: Context) { + val intent = Intent(context, MyPostsActivity::class.java) + context.startActivity(intent) + } + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsFragmentStateAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsFragmentStateAdapter.kt new file mode 100644 index 000000000..776bfd839 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myPostListPage/MyPostsFragmentStateAdapter.kt @@ -0,0 +1,23 @@ +package com.emmsale.presentation.ui.myPostListPage + +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.emmsale.presentation.ui.myFeedList.MyFeedsFragment +import com.emmsale.presentation.ui.myRecruitmentList.MyRecruitmentsFragment + +class MyPostsFragmentStateAdapter(activity: AppCompatActivity) : FragmentStateAdapter(activity) { + override fun getItemCount(): Int = TAB_SIZE + + override fun createFragment(position: Int): Fragment = when (position) { + MY_FEEDS_POSITION -> MyFeedsFragment() + MY_RECRUITMENT_POSITION -> MyRecruitmentsFragment() + else -> throw IllegalArgumentException("올바르지 않은 fragment position 입니다.") + } + + companion object { + private const val TAB_SIZE = 2 + private const val MY_FEEDS_POSITION = 0 + private const val MY_RECRUITMENT_POSITION = 1 + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt deleted file mode 100644 index 699a43f7e..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentActivity.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.emmsale.presentation.ui.myRecruitmentList - -import android.content.Context -import android.content.Intent -import android.os.Bundle -import androidx.activity.result.contract.ActivityResultContracts -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import com.emmsale.R -import com.emmsale.databinding.ActivityMyPostBinding -import com.emmsale.presentation.common.extension.showToast -import com.emmsale.presentation.ui.myRecruitmentList.recyclerView.MyRecruitmentAdapter -import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailActivity -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class MyRecruitmentActivity : AppCompatActivity() { - private val binding by lazy { ActivityMyPostBinding.inflate(layoutInflater) } - private val viewModel: MyRecruitmentViewModel by viewModels() - - private val myRecruitmentAdapter: MyRecruitmentAdapter by lazy { - MyRecruitmentAdapter(navigateToDetail = ::navigateToDetail) - } - - private val fetchByResultActivityLauncher = - registerForActivityResult( - ActivityResultContracts.StartActivityForResult(), - ) { result -> - if (result == null || result.resultCode != RESULT_OK) return@registerForActivityResult - viewModel.refresh() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - initRecyclerView() - setUpMyPosts() - initBackPressButton() - } - - private fun setUpMyPosts() { - viewModel.myRecruitments.observe(this) { myRecruitments -> - if (myRecruitments.isError) { - showToast(R.string.all_data_loading_failed_message) - } else { - myRecruitmentAdapter.submitList(myRecruitments.list) - } - } - } - - private fun initRecyclerView() { - binding.rvMyPost.layoutManager = LinearLayoutManager(this) - binding.rvMyPost.adapter = myRecruitmentAdapter - } - - private fun navigateToDetail(eventId: Long, recruitmentId: Long) { - RecruitmentDetailActivity.startActivity( - this, - eventId = eventId, - recruitmentId = recruitmentId, - ) - } - - private fun initBackPressButton() { - binding.tbMyPost.setNavigationOnClickListener { - finish() - } - } - - companion object { - fun startActivity(context: Context) { - val intent = Intent(context, MyRecruitmentActivity::class.java) - context.startActivity(intent) - } - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentViewModel.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentViewModel.kt index 3c6f0521c..c046a789f 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentViewModel.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentViewModel.kt @@ -1,47 +1,45 @@ package com.emmsale.presentation.ui.myRecruitmentList -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.emmsale.data.common.retrofit.callAdapter.Failure -import com.emmsale.data.common.retrofit.callAdapter.NetworkError -import com.emmsale.data.common.retrofit.callAdapter.Success -import com.emmsale.data.common.retrofit.callAdapter.Unexpected +import com.emmsale.data.model.Recruitment import com.emmsale.data.repository.interfaces.RecruitmentRepository import com.emmsale.data.repository.interfaces.TokenRepository +import com.emmsale.presentation.base.RefreshableViewModel import com.emmsale.presentation.common.livedata.NotNullLiveData import com.emmsale.presentation.common.livedata.NotNullMutableLiveData -import com.emmsale.presentation.common.viewModel.Refreshable -import com.emmsale.presentation.ui.myRecruitmentList.uiState.MyRecruitmentsUiState import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.launch +import kotlinx.coroutines.Job import javax.inject.Inject @HiltViewModel class MyRecruitmentViewModel @Inject constructor( private val tokenRepository: TokenRepository, private val recruitmentRepository: RecruitmentRepository, -) : ViewModel(), Refreshable { +) : RefreshableViewModel() { private val uid: Long by lazy { tokenRepository.getMyUid()!! } - private val _myRecruitments: NotNullMutableLiveData = - NotNullMutableLiveData(MyRecruitmentsUiState()) - val myRecruitments: NotNullLiveData = _myRecruitments + private val _myRecruitments: NotNullMutableLiveData> = + NotNullMutableLiveData(emptyList()) + val myRecruitments: NotNullLiveData> = _myRecruitments init { - refresh() + fetchMyRecruitments() } - override fun refresh() { - _myRecruitments.value = _myRecruitments.value.copy(isLoading = true) - viewModelScope.launch { - when (val result = recruitmentRepository.getMemberRecruitments(uid)) { - is Failure, NetworkError -> - _myRecruitments.value = - _myRecruitments.value.copy(isLoading = false, isError = true) - - is Success -> _myRecruitments.value = MyRecruitmentsUiState.from(result.data) - is Unexpected -> throw Throwable(result.error) - } - } + private fun fetchMyRecruitments() { + fetchData( + fetchData = { recruitmentRepository.getMemberRecruitments(uid) }, + onSuccess = { + _myRecruitments.value = + it.sortedByDescending { recruitment -> recruitment.updatedDate } + }, + ) } + + override fun refresh(): Job = refreshData( + refresh = { recruitmentRepository.getMemberRecruitments(uid) }, + onSuccess = { + _myRecruitments.value = + it.sortedByDescending { recruitment -> recruitment.updatedDate } + }, + ) } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentsFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentsFragment.kt new file mode 100644 index 000000000..274a55c68 --- /dev/null +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/MyRecruitmentsFragment.kt @@ -0,0 +1,57 @@ +package com.emmsale.presentation.ui.myRecruitmentList + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.viewModels +import com.emmsale.R +import com.emmsale.databinding.FragmentMyRecruitmentsBinding +import com.emmsale.presentation.base.NetworkFragment +import com.emmsale.presentation.common.recyclerView.DividerItemDecoration +import com.emmsale.presentation.ui.myRecruitmentList.recyclerView.MyRecruitmentAdapter +import com.emmsale.presentation.ui.recruitmentDetail.RecruitmentDetailActivity +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MyRecruitmentsFragment : + NetworkFragment(R.layout.fragment_my_recruitments) { + + override val viewModel: MyRecruitmentViewModel by viewModels() + + private val myRecruitmentAdapter: MyRecruitmentAdapter by lazy { + MyRecruitmentAdapter(onItemClick = ::navigateToDetail) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupBinding() + setupRecyclerView() + + observeMyRecruitments() + } + + private fun setupBinding() { + binding.vm = viewModel + } + + private fun setupRecyclerView() { + binding.rvMyPost.apply { + adapter = myRecruitmentAdapter + itemAnimator = null + addItemDecoration(DividerItemDecoration(context = context)) + } + } + + private fun observeMyRecruitments() { + viewModel.myRecruitments.observe(viewLifecycleOwner) { myRecruitments -> + myRecruitmentAdapter.submitList(myRecruitments) + } + } + + private fun navigateToDetail(eventId: Long, recruitmentId: Long) { + RecruitmentDetailActivity.startActivity( + requireContext(), + eventId = eventId, + recruitmentId = recruitmentId, + ) + } +} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentAdapter.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentAdapter.kt index 7a682837b..7798fec61 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentAdapter.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentAdapter.kt @@ -3,31 +3,29 @@ package com.emmsale.presentation.ui.myRecruitmentList.recyclerView import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter -import com.emmsale.presentation.ui.myRecruitmentList.uiState.MyRecruitmentUiState +import com.emmsale.data.model.Recruitment class MyRecruitmentAdapter( - private val navigateToDetail: (eventId: Long, recruitmentId: Long) -> Unit, -) : ListAdapter(diffUtil) { + private val onItemClick: (eventId: Long, recruitmentId: Long) -> Unit, +) : ListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: Recruitment, + newItem: Recruitment, + ): Boolean = oldItem == newItem + + override fun areContentsTheSame( + oldItem: Recruitment, + newItem: Recruitment, + ): Boolean = (oldItem.id == newItem.id && oldItem.event.id == newItem.event.id) + }, +) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyRecruitmentViewHolder { - return MyRecruitmentViewHolder.create(parent, navigateToDetail) + return MyRecruitmentViewHolder.create(parent, onItemClick) } override fun onBindViewHolder(holder: MyRecruitmentViewHolder, position: Int) { holder.bind(getItem(position)) } - - companion object { - val diffUtil = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: MyRecruitmentUiState, - newItem: MyRecruitmentUiState, - ): Boolean = oldItem == newItem - - override fun areContentsTheSame( - oldItem: MyRecruitmentUiState, - newItem: MyRecruitmentUiState, - ): Boolean = (oldItem.postId == newItem.postId && oldItem.eventId == newItem.eventId) - } - } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentViewHolder.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentViewHolder.kt index 957c2f6cf..6a99bf39a 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentViewHolder.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/recyclerView/MyRecruitmentViewHolder.kt @@ -3,35 +3,35 @@ package com.emmsale.presentation.ui.myRecruitmentList.recyclerView import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.emmsale.databinding.ItemMyPostBinding -import com.emmsale.presentation.ui.myRecruitmentList.uiState.MyRecruitmentUiState +import com.emmsale.data.model.Recruitment +import com.emmsale.databinding.ItemMyRecruitmentBinding class MyRecruitmentViewHolder( - private val binding: ItemMyPostBinding, + private val binding: ItemMyRecruitmentBinding, navigateToDetail: (eventId: Long, recruitmentId: Long) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { init { itemView.setOnClickListener { navigateToDetail( - binding.recruitment!!.eventId, - binding.recruitment!!.postId, + binding.recruitment!!.event.id, + binding.recruitment!!.id, ) } } - fun bind(myRecruitment: MyRecruitmentUiState) { + fun bind(myRecruitment: Recruitment) { binding.recruitment = myRecruitment } companion object { fun create( parent: ViewGroup, - navigateToDetail: (eventId: Long, recruitmentId: Long) -> Unit, + onItemClick: (eventId: Long, recruitmentId: Long) -> Unit, ): MyRecruitmentViewHolder { val binding = - ItemMyPostBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return MyRecruitmentViewHolder(binding, navigateToDetail) + ItemMyRecruitmentBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return MyRecruitmentViewHolder(binding, onItemClick) } } } diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentUiState.kt deleted file mode 100644 index 58b469d4b..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentUiState.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.emmsale.presentation.ui.myRecruitmentList.uiState - -import com.emmsale.data.model.Recruitment - -data class MyRecruitmentUiState( - val postId: Long = DEFAULT_ID, - val eventId: Long = DEFAULT_ID, - val eventName: String = "", - val content: String = "", - val updatedAt: String = "", -) { - companion object { - private const val DEFAULT_ID = -1L - - fun from(recruitment: Recruitment) = MyRecruitmentUiState( - postId = recruitment.id, - eventId = recruitment.event.id, - eventName = recruitment.event.name, - content = recruitment.content, - updatedAt = recruitment.updatedDate.toString(), - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentsUiState.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentsUiState.kt deleted file mode 100644 index 16fcc8309..000000000 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/myRecruitmentList/uiState/MyRecruitmentsUiState.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.emmsale.presentation.ui.myRecruitmentList.uiState - -import com.emmsale.data.model.Recruitment - -data class MyRecruitmentsUiState( - val list: List = listOf(), - val isLoading: Boolean = false, - val isError: Boolean = false, -) { - companion object { - fun from(recruitments: List) = MyRecruitmentsUiState( - list = recruitments.map { MyRecruitmentUiState.from(it) }, - isLoading = false, - isError = false, - ) - } -} diff --git a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt index a4ed9ee6a..aae32785b 100644 --- a/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt +++ b/android/2023-emmsale/app/src/main/java/com/emmsale/presentation/ui/setting/SettingFragment.kt @@ -15,7 +15,7 @@ import com.emmsale.presentation.common.views.WarningDialog import com.emmsale.presentation.ui.blockMemberList.BlockedMembersActivity import com.emmsale.presentation.ui.login.LoginActivity import com.emmsale.presentation.ui.myCommentList.MyCommentsActivity -import com.emmsale.presentation.ui.myRecruitmentList.MyRecruitmentActivity +import com.emmsale.presentation.ui.myPostListPage.MyPostsActivity import com.emmsale.presentation.ui.notificationConfig.NotificationConfigActivity import com.emmsale.presentation.ui.setting.uiState.SettingUiEvent import com.emmsale.presentation.ui.useTerm.UseTermWebViewActivity @@ -43,7 +43,7 @@ class SettingFragment : private fun setupDataBinding() { binding.viewModel = viewModel - binding.onWritingsButtonClick = { MyRecruitmentActivity.startActivity(requireContext()) } + binding.onWritingsButtonClick = { MyPostsActivity.startActivity(requireContext()) } binding.onWrittenCommentsButtonClick = { MyCommentsActivity.startActivity(requireContext()) } binding.onNotificationSettingButtonClick = diff --git a/android/2023-emmsale/app/src/main/res/layout/activity_my_post.xml b/android/2023-emmsale/app/src/main/res/layout/activity_my_posts.xml similarity index 50% rename from android/2023-emmsale/app/src/main/res/layout/activity_my_post.xml rename to android/2023-emmsale/app/src/main/res/layout/activity_my_posts.xml index a7975cdfa..c76fe8990 100644 --- a/android/2023-emmsale/app/src/main/res/layout/activity_my_post.xml +++ b/android/2023-emmsale/app/src/main/res/layout/activity_my_posts.xml @@ -1,8 +1,6 @@ + xmlns:app="http://schemas.android.com/apk/res-auto"> @@ -16,8 +14,7 @@ + android:background="@color/white"> - - - + app:layout_constraintEnd_toEndOf="parent" /> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/tablayout_myposts" + app:layout_constraintBottom_toBottomOf="parent" /> + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_my_feeds.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_my_feeds.xml new file mode 100644 index 000000000..96e22c378 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_my_feeds.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_my_recruitments.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_my_recruitments.xml new file mode 100644 index 000000000..45acf56c9 --- /dev/null +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_my_recruitments.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml b/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml index c2c0ecdf7..b80e5ad6a 100644 --- a/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml +++ b/android/2023-emmsale/app/src/main/res/layout/fragment_setting.xml @@ -94,12 +94,30 @@ app:layout_constraintTop_toBottomOf="@+id/tv_setting_member_name" tools:text="git1234567@github.com" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/2023-emmsale/app/src/main/res/layout/item_my_post.xml b/android/2023-emmsale/app/src/main/res/layout/item_my_recruitment.xml similarity index 60% rename from android/2023-emmsale/app/src/main/res/layout/item_my_post.xml rename to android/2023-emmsale/app/src/main/res/layout/item_my_recruitment.xml index df1afb568..2c13e7128 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_my_post.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_my_recruitment.xml @@ -7,29 +7,18 @@ + type="com.emmsale.data.model.Recruitment" /> + + - - - + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground"> - - - + diff --git a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml index 4d88041af..461f21888 100644 --- a/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml +++ b/android/2023-emmsale/app/src/main/res/layout/item_mycomments_comment.xml @@ -48,6 +48,8 @@ android:lineSpacingExtra="3dp" android:text="@{comment.content}" android:textSize="12sp" + android:ellipsize="end" + android:maxLines="5" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/tv_mycomments_event_name" app:layout_constraintTop_toBottomOf="@+id/tv_mycomments_event_name" diff --git a/android/2023-emmsale/app/src/main/res/values/strings.xml b/android/2023-emmsale/app/src/main/res/values/strings.xml index 33717a9cb..5ac9268c8 100644 --- a/android/2023-emmsale/app/src/main/res/values/strings.xml +++ b/android/2023-emmsale/app/src/main/res/values/strings.xml @@ -51,6 +51,9 @@ 오류 데이터를 불러올 수 없습니다. 알 수 없는 오류가 발생했습니다. + 이미지 권한 허용을 위해 설정창으로 이동하시겠습니까? + 이미지 권한이 거부되었어요 + 갤러리 종류를 선택하세요 업데이트 새로운 버전이 출시되었어요!\n지금 바로 커디 스토어로 이동하시겠어요? @@ -71,14 +74,6 @@ 복수 선택이 최대 4개까지 가능합니다. 선택할 수 있는 개수를 초과했어요! - %s ~ %s - 게시판 - 행사 공유 - 친구로부터 행사를 공유받았어요!\n커디에서 게시글을 구경하고 동행자를 모집해보세요. - 행사 구경하기! - 카카오톡 공유에 실패했어요 🥲 - 지원되는 브라우저가 없어요 🥲 - 3/4 교육 활동 교육 이력이 있나요? @@ -256,6 +251,13 @@ 상세정보 보기 탭 스크랩 불가 스크랩 해제 불가 + %s ~ %s + 게시판 + 행사 공유 + 친구로부터 행사를 공유받았어요!\n커디에서 게시글을 구경하고 동행자를 모집해보세요. + 행사 구경하기! + 카카오톡 공유에 실패했어요 🥲 + 지원되는 브라우저가 없어요 🥲 비용 장소 @@ -318,6 +320,7 @@ 차단 목록 작성한 글 오픈 프로필 등록 + 작성한 글 작성한 댓글 회원 정보를 불러오는 데 실패했어요 😥 회원 탈퇴하는 데 실패했어요 😥 @@ -463,9 +466,9 @@ 전체 삭제 전체 삭제 최근 검색어를 모두 삭제하시겠어요? - 이미지 권한 허용을 위해 설정창으로 이동하시겠습니까? - 이미지 권한이 거부되었어요 - 갤러리 종류를 선택하세요 + + 행사 게시판 + 함께해요 플레이스토어가 설치되지 않았거나 로그인하지 않았어요 😥