Skip to content

Commit

Permalink
Fix/#77 커스텀 갤러리 이미지 validation 추가 (#81)
Browse files Browse the repository at this point in the history
* refactor: image 정보로 mimeType, size 추가

* feat: 타입, 사이즈 검증 로직 추가

* refactor: 리뷰반영
  • Loading branch information
rhkrwngud445 authored Apr 8, 2024
1 parent d5453f0 commit de1a3e7
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.withpeace.withpeace.core.data.mapper

import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.imagestorage.ImageInfoEntity

fun ImageInfoEntity.toDomain(): ImageInfo {
return ImageInfo(
uri = imageUri.toString(),
mimeType = mimeType,
byteSize = byteSize,
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.withpeace.withpeace.core.data.repository

import com.withpeace.withpeace.core.data.mapper.toDomain
import com.withpeace.withpeace.core.domain.model.image.ImageFolder
import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.domain.repository.ImageRepository
import com.withpeace.withpeace.core.imagestorage.ImageDataSource
import kotlinx.coroutines.Dispatchers
Expand All @@ -21,12 +23,12 @@ class DefaultImageRepository @Inject constructor(
}
}

override suspend fun getImages(page: Int, loadSize: Int, folderName: String?): List<String> =
override suspend fun getImages(page: Int, loadSize: Int, folderName: String?): List<ImageInfo> =
withContext(Dispatchers.IO) {
imageDataSource.getImages(
page = page,
loadSize = loadSize,
folder = folderName,
).map { it.toString() }
).map { it.toDomain() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.withpeace.withpeace.core.domain.model.image

data class ImageInfo(
val uri: String,
val mimeType: String,
val byteSize: Long,
) {
fun isUploadType(): Boolean {
val imageMimeType = mimeType.split("/").last().lowercase() // image/png
return uploadType.contains(imageMimeType)
}

fun isSizeOver(): Boolean {
return byteSize.bytesToMegabytes() > MAX_MEGA_BYTE_SIZE
}

private fun Long.bytesToMegabytes(): Double {
return this / (BYTE_TO_KB_UNIT * KB_TO_MB_UNIT)
}

companion object {
private val uploadType = arrayOf(
"jpg", "png", "webp", "jpeg",
)
private const val BYTE_TO_KB_UNIT = 1024.0
private const val KB_TO_MB_UNIT = 1024.0
private const val MAX_MEGA_BYTE_SIZE = 10
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import androidx.paging.PagingSource
data class ImagePagingInfo(
val pageSize: Int,
val enablePlaceholders: Boolean = true,
val pagingSource: PagingSource<Int, String>,
val pagingSource: PagingSource<Int, ImageInfo>,
) {
val pagingConfig = PagingConfig(
pageSize = pageSize,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ package com.withpeace.withpeace.core.domain.paging

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.domain.repository.ImageRepository

data class ImagePagingSource(
private val imageRepository: ImageRepository,
private val folderName: String?,
) : PagingSource<Int, String>() {
) : PagingSource<Int, ImageInfo>() {

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, String> {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ImageInfo> {
return try {
val currentPage = params.key ?: STARTING_PAGE_INDEX
val data = imageRepository.getImages(
Expand All @@ -27,7 +28,7 @@ data class ImagePagingSource(
}
}

override fun getRefreshKey(state: PagingState<Int, String>): Int? {
override fun getRefreshKey(state: PagingState<Int, ImageInfo>): Int? {
return state.anchorPosition?.let { achorPosition ->
state.closestPageToPosition(achorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(achorPosition)?.nextKey?.minus(1)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.withpeace.withpeace.core.domain.repository

import com.withpeace.withpeace.core.domain.model.image.ImageFolder
import com.withpeace.withpeace.core.domain.model.image.ImageInfo

interface ImageRepository {

Expand All @@ -10,5 +11,5 @@ interface ImageRepository {
page: Int,
loadSize: Int,
folderName: String?,
): List<String>
): List<ImageInfo>
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,24 @@ class DefaultImageDataSource(
page: Int,
loadSize: Int,
folder: String?,
): List<Uri> {
val imageUris = mutableListOf<Uri>()
val pagingImagesQuery = context.getPagingImagesQuery((page - 1) * loadSize, loadSize, folder)
): List<ImageInfoEntity> {
val imageInfo = mutableListOf<ImageInfoEntity>()
val pagingImagesQuery =
context.getPagingImagesQuery((page - 1) * loadSize, loadSize, folder)
pagingImagesQuery.use { cursor ->
val nameIndex = cursor.getColumnIndex(Images.ImageColumns.MIME_TYPE)
val sizeIndex = cursor.getColumnIndex(Images.ImageColumns.SIZE)
val idColumn = cursor.getColumnIndexOrThrow(Images.Media._ID)
while (cursor.moveToNext()) {
val uri = ContentUris.withAppendedId(uriExternal, cursor.getLong(idColumn))
imageUris.add(uri)
val mimeType = cursor.getString(nameIndex)
val size = cursor.getLong(sizeIndex)

imageInfo.add(ImageInfoEntity(uri, mimeType, size))
}
cursor.close()
}
return imageUris
return imageInfo
}

override suspend fun getFolders(): List<ImageFolderEntity> {
Expand Down Expand Up @@ -72,6 +78,8 @@ class DefaultImageDataSource(
val projection = arrayOf(
Images.ImageColumns.DATA,
Images.Media._ID,
Images.ImageColumns.MIME_TYPE,
Images.ImageColumns.SIZE,
)
val selection = folder?.let { "${Images.Media.DATA} LIKE ?" }
val selectionArgs = folder?.let { arrayOf("%$folder%") }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface ImageDataSource {
page: Int,
loadSize: Int,
folder: String?,
):List<Uri>
):List<ImageInfoEntity>

suspend fun getFolders(): List<ImageFolderEntity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.withpeace.withpeace.core.imagestorage

import android.net.Uri

data class ImageInfoEntity(
val imageUri: Uri,
val mimeType: String,
val byteSize: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import com.withpeace.withpeace.core.designsystem.theme.WithpeaceTheme
import com.withpeace.withpeace.core.designsystem.ui.WithPeaceBackButtonTopAppBar
import com.withpeace.withpeace.core.designsystem.ui.WithPeaceCompleteButton
import com.withpeace.withpeace.core.domain.model.image.ImageFolder
import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.domain.model.image.LimitedImages
import com.withpeace.withpeace.feature.gallery.R.drawable
import com.withpeace.withpeace.feature.gallery.R.string
Expand Down Expand Up @@ -83,7 +84,9 @@ fun GalleryRoute(
LaunchedEffect(key1 = null) {
viewModel.sideEffect.collectLatest {
when (it) {
GallerySideEffect.SelectImageFail -> onShowSnackBar(noMoreImageMessage)
GallerySideEffect.SelectImageNoMore -> onShowSnackBar(noMoreImageMessage)
GallerySideEffect.SelectImageNoApplyType -> onShowSnackBar("지원하지 않는 파일 형식입니다.")
GallerySideEffect.SelectImageOverSize -> onShowSnackBar("10MB 이하의 이미지만 업로드 가능합니다.")
}
}
}
Expand All @@ -95,8 +98,8 @@ fun GalleryScreen(
onCompleteRegisterImages: (List<String>) -> Unit = {},
allFolders: List<ImageFolder>,
onSelectFolder: (ImageFolder?) -> Unit = {},
onSelectImage: (String) -> Unit = {},
pagingImages: LazyPagingItems<String>,
onSelectImage: (ImageInfo) -> Unit = {},
pagingImages: LazyPagingItems<ImageInfo>,
selectedImageList: LimitedImages,
selectedFolder: ImageFolder?,
) {
Expand Down Expand Up @@ -217,9 +220,9 @@ fun FolderList(
@Composable
fun ImageList(
modifier: Modifier = Modifier,
pagingImages: LazyPagingItems<String>,
pagingImages: LazyPagingItems<ImageInfo>,
selectedImageList: LimitedImages,
onSelectImage: (String) -> Unit,
onSelectImage: (ImageInfo) -> Unit,
) {
LazyVerticalGrid(
modifier = modifier,
Expand All @@ -230,22 +233,22 @@ fun ImageList(
items(
pagingImages.itemCount,
) { index ->
val uriString = pagingImages[index] ?: throw IllegalStateException("uri가 존재하지 않음")
val imageInfo = pagingImages[index] ?: throw IllegalStateException("uri가 존재하지 않음")

Box(
modifier = Modifier
.aspectRatio(1f)
.clickable {
onSelectImage(uriString)
onSelectImage(imageInfo)
},
) {
GlideImage(
modifier = Modifier.align(Alignment.Center),
imageModel = { Uri.parse(uriString) },
imageModel = { Uri.parse(imageInfo.uri) },
imageOptions = ImageOptions(contentScale = ContentScale.Crop),
previewPlaceholder = R.drawable.ic_backarrow_right,
)
if (selectedImageList.contains(uriString)) {
if (selectedImageList.contains(imageInfo.uri)) {
Box(
modifier = Modifier
.fillMaxSize()
Expand Down Expand Up @@ -279,7 +282,7 @@ private fun GalleryScreenPreview() {
},
pagingImages = flowOf(
PagingData.from(
List(10) { "" },
List(10) { ImageInfo("", "", 1L) },
sourceLoadStates =
LoadStates(
refresh = LoadState.NotLoading(false),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.withpeace.withpeace.feature.gallery

sealed interface GallerySideEffect {
data object SelectImageFail : GallerySideEffect
data object SelectImageNoMore : GallerySideEffect
data object SelectImageNoApplyType : GallerySideEffect
data object SelectImageOverSize : GallerySideEffect
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.paging.Pager
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.withpeace.withpeace.core.domain.model.image.ImageFolder
import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.domain.model.image.LimitedImages
import com.withpeace.withpeace.core.domain.usecase.GetAlbumImagesUseCase
import com.withpeace.withpeace.core.domain.usecase.GetAllFoldersUseCase
Expand Down Expand Up @@ -61,7 +62,7 @@ class GalleryViewModel @Inject constructor(
}
}

private suspend fun getImagePagingData(folderName:String):PagingData<String>{
private suspend fun getImagePagingData(folderName:String):PagingData<ImageInfo>{
val imagePagingInfo = getAlbumImagesUseCase(folderName)
return Pager(
config = imagePagingInfo.pagingConfig,
Expand All @@ -75,14 +76,25 @@ class GalleryViewModel @Inject constructor(
_selectedFolder.value = imageFolder
}

fun onSelectImage(uriString: String) {
fun onSelectImage(imageInfo: ImageInfo) {
when {
selectedImages.value.contains(uriString) -> _selectedImages.update {
it.deleteImage(uriString)
selectedImages.value.contains(imageInfo.uri) -> _selectedImages.update {
it.deleteImage(imageInfo.uri)
}

selectedImages.value.canAddImage() -> _selectedImages.update { it.addImage(uriString) }
else -> viewModelScope.launch { _sideEffect.send(GallerySideEffect.SelectImageFail) }
selectedImages.value.canAddImage() -> {
if (!imageInfo.isUploadType()) {
viewModelScope.launch { _sideEffect.send(GallerySideEffect.SelectImageNoApplyType) }
return
}
if (imageInfo.isSizeOver()) {
viewModelScope.launch { _sideEffect.send(GallerySideEffect.SelectImageOverSize) }
return
}
_selectedImages.update { it.addImage(imageInfo.uri) }
}

else -> viewModelScope.launch { _sideEffect.send(GallerySideEffect.SelectImageNoMore) }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.paging.testing.asSnapshot
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import com.withpeace.withpeace.core.domain.model.image.ImageFolder
import com.withpeace.withpeace.core.domain.model.image.ImageInfo
import com.withpeace.withpeace.core.domain.model.image.ImagePagingInfo
import com.withpeace.withpeace.core.domain.model.image.LimitedImages
import com.withpeace.withpeace.core.domain.usecase.GetAlbumImagesUseCase
Expand Down Expand Up @@ -101,7 +102,7 @@ class GalleryViewModelTest {
} returns ImagePagingInfo(
pageSize = 30,
enablePlaceholders = false,
pagingSource = emptyList<String>().asPagingSourceFactory().invoke(),
pagingSource = emptyList<ImageInfo>().asPagingSourceFactory().invoke(),
)
// when & then
val actual = viewModel.images.getFullScrollItems()
Expand All @@ -118,7 +119,9 @@ class GalleryViewModelTest {
representativeImageUri = "test",
imageCount = 10,
)
val testImages = List(100) { "testUri" }
val testImages = List(100) { ImageInfo(
"uri","type",0L
)}
coEvery {
getAlbumImagesUseCase(testFolder.folderName)
} returns ImagePagingInfo(
Expand All @@ -140,11 +143,13 @@ class GalleryViewModelTest {
// given
savedStateHandle = SavedStateHandle()
viewModel = viewModel()
val testImage = "test"
val testImage = ImageInfo(
"uri","images/png",10
)
// when
viewModel.onSelectImage(testImage)
// then
val actual = viewModel.selectedImages.value.contains(testImage)
val actual = viewModel.selectedImages.value.contains(testImage.uri)
assertThat(actual).isTrue()
}

Expand All @@ -153,12 +158,14 @@ class GalleryViewModelTest {
// given
savedStateHandle = SavedStateHandle()
viewModel = viewModel()
val testImage = "test"
val testImage = ImageInfo(
"uri","type",0L
)
// when
viewModel.onSelectImage(testImage)
viewModel.onSelectImage(testImage)
// then
val actual = viewModel.selectedImages.value.contains(testImage)
val actual = viewModel.selectedImages.value.contains(testImage.uri)
assertThat(actual).isFalse()
}

Expand All @@ -169,12 +176,14 @@ class GalleryViewModelTest {
mapOf(GALLERY_IMAGE_LIMIT_ARGUMENT to 0), //최대 이미지 개수 0
)
viewModel = viewModel()
val testImage = "test"
val testImage = ImageInfo(
"uri","type",0L
)
// when && then
viewModel.sideEffect.test {
viewModel.onSelectImage(testImage)
val actual = awaitItem()
assertThat(actual).isEqualTo(GallerySideEffect.SelectImageFail)
assertThat(actual).isEqualTo(GallerySideEffect.SelectImageNoMore)
}
}
}

0 comments on commit de1a3e7

Please sign in to comment.