Skip to content

Commit

Permalink
Feat/#67 게시글 목록 기능 구현 (#79)
Browse files Browse the repository at this point in the history
* feat : 게시글 등록 모듈 생성

* feat : 게시글 등록 기능 UI 구현 완료

* feat : UI기능 전까지 완료

* feat : 커스텀 갤러리 기능 구현

* feat : 갤러리에서 선택한 이미지를 게시글에서 받도록 구현

* feat : 게시글 등록 API 연동 구현

* feat : StringResource 리팩토링

* feat : Dependency Graph 모듈 볼수 있는 플러그인 추가

* feat : 화면 회전시 앱이 안보이는 버그 수정

* feat : GetAlbumImagesUseCase로 네이밍 수정

* feat : GetAlbumImagesUseCase에서 ImagePagingInfo를 주도록 변경

* feat : GalleryViewModel Test 작성

* feat : 앱 난독화 적용

* test : RegisterPostViewModel 테스트 작성

* feat : core-ui 모듈 추가 및 PostTopicUiState core-ui로 이동

* refactor : Rebase 충돌 수정

* refactor : 모듈을 post에서 postList로 수정

* refactor : material3 버젼 업

* feat : 게시글 목록 화면 탭 UI 구현

* feat : Tab에 viewModel 상태 적용

* feat : 게시글 목록 UI 구현 완료

* feat : Date 계산 로직 도메인으로 이동

* feat : 사진 변경사항 적용

- 고정 크기로 변경
- 사진이 없을 경우 대처

* feat : 게시글 목록 가져오는 기능 Usecase 및 Repository 로직 구현

* feat : 게시글 목록 조회 Paging 기능 ViewModel 및 View에 적용

* feat : snapshotList를 쓰지 않도록 수정

- snapshotList는 페이징 적용 x

* feat : 에러 이벤트 처리 구현

* feat : UiModel 적용

* feat : 리뷰 반영
  • Loading branch information
chws0508 authored Apr 5, 2024
1 parent ce642c3 commit d5453f0
Show file tree
Hide file tree
Showing 23 changed files with 552 additions and 200 deletions.
28 changes: 17 additions & 11 deletions app/src/main/java/com/withpeace/withpeace/MainBottomNavigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import com.withpeace.withpeace.feature.home.navigation.HOME_ROUTE
import com.withpeace.withpeace.feature.home.navigation.navigateHome
import com.withpeace.withpeace.feature.mypage.navigation.MY_PAGE_ROUTE
import com.withpeace.withpeace.feature.mypage.navigation.navigateMyPage
import com.withpeace.withpeace.feature.postlist.navigation.POST_LIST_ROUTE
import com.withpeace.withpeace.navigation.POST_NESTED_ROUTE

@Composable
Expand All @@ -34,19 +33,25 @@ fun MainBottomBar(
) {
BottomTab.entries.forEach { tab ->
NavigationBarItem(
colors = NavigationBarItemDefaults.colors(
selectedTextColor = WithpeaceTheme.colors.SystemBlack,
unselectedTextColor = WithpeaceTheme.colors.SystemGray2,
indicatorColor = WithpeaceTheme.colors.SystemWhite,
),
colors =
NavigationBarItemDefaults.colors(
selectedTextColor = WithpeaceTheme.colors.SystemBlack,
unselectedTextColor = WithpeaceTheme.colors.SystemGray2,
indicatorColor = WithpeaceTheme.colors.SystemWhite,
),
selected = currentDestination.route == tab.route,
onClick = { navController.navigateToTabScreen(tab) },
icon = {
Image(
painter = painterResource(
id = if (currentDestination.route == tab.route) tab.iconSelectedResId
else tab.iconUnSelectedResId,
),
painter =
painterResource(
id =
if (currentDestination.route == tab.route) {
tab.iconSelectedResId
} else {
tab.iconUnSelectedResId
},
),
contentDescription = context.getString(tab.contentDescription),
)
},
Expand Down Expand Up @@ -96,7 +101,8 @@ enum class BottomTab(
iconSelectedResId = R.drawable.ic_bottom_my_page_select,
contentDescription = R.string.my_page,
MY_PAGE_ROUTE,
);
),
;

companion object {
operator fun contains(route: String): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.withpeace.withpeace.core.data.mapper

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

fun String.toLocalDateTime(): LocalDateTime = LocalDateTime.parse(
this,
DateTimeFormatter.ofPattern(SERVER_DATE_FORMAT),
)

private const val SERVER_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.domain.model.date.Date
import com.withpeace.withpeace.core.domain.model.post.Post
import com.withpeace.withpeace.core.network.di.response.post.PostResponse

fun PostResponse.toDomain() =
Post(
postId = postId,
title = title,
content = content,
postTopic = type.toDomain(),
createDate = Date(createDate.toLocalDateTime()),
postImageUrl = postImageUrl,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.domain.model.post.PostTopic
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.ECONOMY
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.FREEDOM
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.HOBBY
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.INFORMATION
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.LIVING
import com.withpeace.withpeace.core.network.di.response.post.PostTopicResponse.QUESTION

fun PostTopicResponse.toDomain(): PostTopic {
return when (this) {
FREEDOM -> PostTopic.FREEDOM
INFORMATION -> PostTopic.INFORMATION
QUESTION -> PostTopic.QUESTION
LIVING -> PostTopic.LIVING
HOBBY -> PostTopic.HOBBY
ECONOMY -> PostTopic.ECONOMY
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,111 @@ import com.skydoves.sandwich.messageOrNull
import com.skydoves.sandwich.suspendMapSuccess
import com.skydoves.sandwich.suspendOnError
import com.skydoves.sandwich.suspendOnException
import com.withpeace.withpeace.core.data.mapper.toDomain
import com.withpeace.withpeace.core.data.util.convertToFile
import com.withpeace.withpeace.core.domain.model.WithPeaceError
import com.withpeace.withpeace.core.domain.model.WithPeaceError.GeneralError
import com.withpeace.withpeace.core.domain.model.WithPeaceError.UnAuthorized
import com.withpeace.withpeace.core.domain.model.post.Post
import com.withpeace.withpeace.core.domain.model.post.PostTopic
import com.withpeace.withpeace.core.domain.model.post.RegisterPost
import com.withpeace.withpeace.core.domain.repository.PostRepository
import com.withpeace.withpeace.core.network.di.service.PostService
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import javax.inject.Inject

class DefaultPostRepository @Inject constructor(
@ApplicationContext private val context: Context,
private val postService: PostService,
) : PostRepository {
override fun registerPost(
post: RegisterPost,
onError: suspend (WithPeaceError) -> Unit,
): Flow<Long> =
flow {
val imageRequestBodies = getImageRequestBodies(post.images.urls)
val postRequestBodies = getPostRequestBodies(post)
postService.registerPost(postRequestBodies, imageRequestBodies)
.suspendMapSuccess {
emit(data.postId)
class DefaultPostRepository
@Inject
constructor(
@ApplicationContext private val context: Context,
private val postService: PostService,
) : PostRepository {
override fun getPosts(
postTopic: PostTopic,
pageIndex: Int,
pageSize: Int,
onError: suspend (WithPeaceError) -> Unit,
): Flow<List<Post>> =
flow {
postService.getPosts(
postTopic = postTopic.name,
pageIndex = pageIndex,
pageSize = pageSize,
).suspendMapSuccess {
emit(data.map { it.toDomain() })
}.suspendOnError {
if (statusCode.code == 401) onError(UnAuthorized())
else onError(GeneralError(statusCode.code, messageOrNull))
if (statusCode.code == 401) {
onError(
UnAuthorized(
statusCode.code,
message = null,
),
)
} else {
onError(GeneralError(statusCode.code, messageOrNull))
}
}.suspendOnException {
onError(GeneralError(message = messageOrNull))
}
}
}.flowOn(Dispatchers.IO)

override fun registerPost(
post: RegisterPost,
onError: suspend (WithPeaceError) -> Unit,
): Flow<Long> =
flow {
val imageRequestBodies = getImageRequestBodies(post.images.urls)
val postRequestBodies = getPostRequestBodies(post)
postService.registerPost(postRequestBodies, imageRequestBodies)
.suspendMapSuccess {
emit(data.postId)
}.suspendOnError {
if (statusCode.code == 401) {
onError(UnAuthorized())
} else {
onError(GeneralError(statusCode.code, messageOrNull))
}
}.suspendOnException {
onError(GeneralError(message = messageOrNull))
}
}.flowOn(Dispatchers.IO)

private fun getImageRequestBodies(
imageUris: List<String>,
): List<MultipartBody.Part> {
val imageFiles = imageUris.map { Uri.parse(it).convertToFile(context) }
return imageFiles.map { file ->
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData(
IMAGES_COLUMN,
file.name,
requestFile,
)
private fun getImageRequestBodies(imageUris: List<String>): List<MultipartBody.Part> {
val imageFiles = imageUris.map { Uri.parse(it).convertToFile(context) }
return imageFiles.map { file ->
val requestFile = file.asRequestBody("image/*".toMediaTypeOrNull())
MultipartBody.Part.createFormData(
IMAGES_COLUMN,
file.name,
requestFile,
)
}
}
}

private fun getPostRequestBodies(post: RegisterPost): HashMap<String, RequestBody> {
return HashMap<String, RequestBody>().apply {
set(
TYPE_COLUMN,
post.topic.toString().toRequestBody("application/json".toMediaTypeOrNull()),
)
set(TITLE_COLUMN, post.title.toRequestBody("application/json".toMediaTypeOrNull()))
set(CONTENT_COLUMN, post.content.toRequestBody("application/json".toMediaTypeOrNull()))
private fun getPostRequestBodies(post: RegisterPost): HashMap<String, RequestBody> {
return HashMap<String, RequestBody>().apply {
set(
TYPE_COLUMN,
post.topic.toString().toRequestBody("application/json".toMediaTypeOrNull()),
)
set(TITLE_COLUMN, post.title.toRequestBody("application/json".toMediaTypeOrNull()))
set(CONTENT_COLUMN, post.content.toRequestBody("application/json".toMediaTypeOrNull()))
}
}
}

companion object {
const val TITLE_COLUMN = "title"
const val CONTENT_COLUMN = "content"
const val TYPE_COLUMN = "type"
const val IMAGES_COLUMN = "images"
companion object {
const val TITLE_COLUMN = "title"
const val CONTENT_COLUMN = "content"
const val TYPE_COLUMN = "type"
const val IMAGES_COLUMN = "images"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.withpeace.withpeace.core.domain.model.post

import androidx.paging.PagingConfig
import androidx.paging.PagingSource

data class PostPageInfo(
val pageSize: Int,
val enablePlaceholders: Boolean = true,
val pagingSource: PagingSource<Int, Post>,
) {
val pagingConfig =
PagingConfig(
pageSize = pageSize,
enablePlaceholders = enablePlaceholders,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.withpeace.withpeace.core.domain.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.withpeace.withpeace.core.domain.model.WithPeaceError
import com.withpeace.withpeace.core.domain.model.post.Post
import com.withpeace.withpeace.core.domain.model.post.PostTopic
import com.withpeace.withpeace.core.domain.repository.PostRepository
import kotlinx.coroutines.flow.firstOrNull

data class PostPagingSource(
private val postRepository: PostRepository,
private val postTopic: PostTopic,
private val pageSize: Int,
private val onError: suspend (WithPeaceError) -> Unit,
) : PagingSource<Int, Post>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Post> {
return try {
val currentPage = params.key ?: STARTING_PAGE_INDEX
val data = postRepository.getPosts(
postTopic = postTopic,
pageIndex = currentPage,
pageSize = params.loadSize,
onError = onError,
).firstOrNull() ?: emptyList()
val endOfPaginationReached = data.isEmpty()
val prevKey = if (currentPage == STARTING_PAGE_INDEX) null else currentPage - 1
val nextKey =
if (endOfPaginationReached) null else currentPage + (params.loadSize / pageSize)
LoadResult.Page(data, prevKey, nextKey)
} catch (exception: Exception) {
LoadResult.Error(exception)
}
}

override fun getRefreshKey(state: PagingState<Int, Post>): Int? {
return state.anchorPosition
}

companion object {
const val STARTING_PAGE_INDEX = 0
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
package com.withpeace.withpeace.core.domain.repository

import com.withpeace.withpeace.core.domain.model.WithPeaceError
import com.withpeace.withpeace.core.domain.model.post.Post
import com.withpeace.withpeace.core.domain.model.post.PostTopic
import com.withpeace.withpeace.core.domain.model.post.RegisterPost
import kotlinx.coroutines.flow.Flow

interface PostRepository {
fun getPosts(
postTopic: PostTopic,
pageIndex: Int,
pageSize: Int,
onError: suspend (WithPeaceError) -> Unit,
): Flow<List<Post>>

fun registerPost(
post: RegisterPost,
onError: suspend (WithPeaceError) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.withpeace.withpeace.core.domain.usecase

import com.withpeace.withpeace.core.domain.model.WithPeaceError
import com.withpeace.withpeace.core.domain.model.post.PostPageInfo
import com.withpeace.withpeace.core.domain.model.post.PostTopic
import com.withpeace.withpeace.core.domain.paging.PostPagingSource
import com.withpeace.withpeace.core.domain.repository.PostRepository
import javax.inject.Inject

class GetPostsUseCase @Inject constructor(
private val postRepository: PostRepository,
) {
operator fun invoke(
postTopic: PostTopic,
pageSize: Int,
onError: suspend (WithPeaceError) -> Unit,
): PostPageInfo = PostPageInfo(
pageSize = pageSize,
pagingSource = PostPagingSource(
postRepository = postRepository,
postTopic = postTopic,
pageSize = pageSize,
onError = onError,
),
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.withpeace.withpeace.core.network.di.response
package com.withpeace.withpeace.core.network.di.response.post

import kotlinx.serialization.Serializable

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.withpeace.withpeace.core.network.di.response.post

import kotlinx.serialization.Serializable

@Serializable
data class PostResponse(
val postId: Long,
val title: String,
val content: String,
val type: PostTopicResponse,
val postImageUrl: String? = null,
val createDate: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.withpeace.withpeace.core.network.di.response.post;

enum class PostTopicResponse {
FREEDOM,
INFORMATION,
QUESTION,
LIVING,
HOBBY,
ECONOMY
}
Loading

0 comments on commit d5453f0

Please sign in to comment.