diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/ImageInfoMapper.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/ImageInfoMapper.kt new file mode 100644 index 00000000..f4c389b9 --- /dev/null +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/mapper/ImageInfoMapper.kt @@ -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, + ) +} diff --git a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultImageRepository.kt b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultImageRepository.kt index c0b184f1..dbae6624 100644 --- a/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultImageRepository.kt +++ b/core/data/src/main/kotlin/com/withpeace/withpeace/core/data/repository/DefaultImageRepository.kt @@ -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 @@ -21,12 +23,12 @@ class DefaultImageRepository @Inject constructor( } } - override suspend fun getImages(page: Int, loadSize: Int, folderName: String?): List = + override suspend fun getImages(page: Int, loadSize: Int, folderName: String?): List = withContext(Dispatchers.IO) { imageDataSource.getImages( page = page, loadSize = loadSize, folder = folderName, - ).map { it.toString() } + ).map { it.toDomain() } } } diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImageInfo.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImageInfo.kt new file mode 100644 index 00000000..e03d47f4 --- /dev/null +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImageInfo.kt @@ -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 + } +} diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImagePagingInfo.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImagePagingInfo.kt index f869e787..5e67271a 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImagePagingInfo.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/model/image/ImagePagingInfo.kt @@ -6,7 +6,7 @@ import androidx.paging.PagingSource data class ImagePagingInfo( val pageSize: Int, val enablePlaceholders: Boolean = true, - val pagingSource: PagingSource, + val pagingSource: PagingSource, ) { val pagingConfig = PagingConfig( pageSize = pageSize, diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/paging/ImagePagingSource.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/paging/ImagePagingSource.kt index eb0fc7a8..00f03b9a 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/paging/ImagePagingSource.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/paging/ImagePagingSource.kt @@ -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() { +) : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { + override suspend fun load(params: LoadParams): LoadResult { return try { val currentPage = params.key ?: STARTING_PAGE_INDEX val data = imageRepository.getImages( @@ -27,7 +28,7 @@ data class ImagePagingSource( } } - override fun getRefreshKey(state: PagingState): Int? { + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { achorPosition -> state.closestPageToPosition(achorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(achorPosition)?.nextKey?.minus(1) diff --git a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/ImageRepository.kt b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/ImageRepository.kt index 6562d51a..41e79885 100644 --- a/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/ImageRepository.kt +++ b/core/domain/src/main/java/com/withpeace/withpeace/core/domain/repository/ImageRepository.kt @@ -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 { @@ -10,5 +11,5 @@ interface ImageRepository { page: Int, loadSize: Int, folderName: String?, - ): List + ): List } diff --git a/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/DefaultImageDataSource.kt b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/DefaultImageDataSource.kt index f1fc3573..9befb7e4 100644 --- a/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/DefaultImageDataSource.kt +++ b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/DefaultImageDataSource.kt @@ -28,18 +28,24 @@ class DefaultImageDataSource( page: Int, loadSize: Int, folder: String?, - ): List { - val imageUris = mutableListOf() - val pagingImagesQuery = context.getPagingImagesQuery((page - 1) * loadSize, loadSize, folder) + ): List { + val imageInfo = mutableListOf() + 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 { @@ -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%") } diff --git a/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageDataSource.kt b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageDataSource.kt index 0112923f..8c097bcb 100644 --- a/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageDataSource.kt +++ b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageDataSource.kt @@ -7,7 +7,7 @@ interface ImageDataSource { page: Int, loadSize: Int, folder: String?, - ):List + ):List suspend fun getFolders(): List } diff --git a/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageInfoEntity.kt b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageInfoEntity.kt new file mode 100644 index 00000000..f001f16c --- /dev/null +++ b/core/imagestorage/src/main/java/com/withpeace/withpeace/core/imagestorage/ImageInfoEntity.kt @@ -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, +) \ No newline at end of file diff --git a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryScreen.kt b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryScreen.kt index 30f55a11..d2979cd6 100644 --- a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryScreen.kt +++ b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryScreen.kt @@ -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 @@ -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 이하의 이미지만 업로드 가능합니다.") } } } @@ -95,8 +98,8 @@ fun GalleryScreen( onCompleteRegisterImages: (List) -> Unit = {}, allFolders: List, onSelectFolder: (ImageFolder?) -> Unit = {}, - onSelectImage: (String) -> Unit = {}, - pagingImages: LazyPagingItems, + onSelectImage: (ImageInfo) -> Unit = {}, + pagingImages: LazyPagingItems, selectedImageList: LimitedImages, selectedFolder: ImageFolder?, ) { @@ -217,9 +220,9 @@ fun FolderList( @Composable fun ImageList( modifier: Modifier = Modifier, - pagingImages: LazyPagingItems, + pagingImages: LazyPagingItems, selectedImageList: LimitedImages, - onSelectImage: (String) -> Unit, + onSelectImage: (ImageInfo) -> Unit, ) { LazyVerticalGrid( modifier = modifier, @@ -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() @@ -279,7 +282,7 @@ private fun GalleryScreenPreview() { }, pagingImages = flowOf( PagingData.from( - List(10) { "" }, + List(10) { ImageInfo("", "", 1L) }, sourceLoadStates = LoadStates( refresh = LoadState.NotLoading(false), diff --git a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GallerySideEffect.kt b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GallerySideEffect.kt index f29e13a6..ed0ed903 100644 --- a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GallerySideEffect.kt +++ b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GallerySideEffect.kt @@ -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 } diff --git a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryViewModel.kt b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryViewModel.kt index a48cc2d3..ccd49c7a 100644 --- a/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryViewModel.kt +++ b/feature/gallery/src/main/java/com/withpeace/withpeace/feature/gallery/GalleryViewModel.kt @@ -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 @@ -61,7 +62,7 @@ class GalleryViewModel @Inject constructor( } } - private suspend fun getImagePagingData(folderName:String):PagingData{ + private suspend fun getImagePagingData(folderName:String):PagingData{ val imagePagingInfo = getAlbumImagesUseCase(folderName) return Pager( config = imagePagingInfo.pagingConfig, @@ -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) } } } diff --git a/feature/gallery/src/test/java/com/withpeace/withpeace/feature/gallery/GalleryViewModelTest.kt b/feature/gallery/src/test/java/com/withpeace/withpeace/feature/gallery/GalleryViewModelTest.kt index db4a863a..7e7dae98 100644 --- a/feature/gallery/src/test/java/com/withpeace/withpeace/feature/gallery/GalleryViewModelTest.kt +++ b/feature/gallery/src/test/java/com/withpeace/withpeace/feature/gallery/GalleryViewModelTest.kt @@ -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 @@ -101,7 +102,7 @@ class GalleryViewModelTest { } returns ImagePagingInfo( pageSize = 30, enablePlaceholders = false, - pagingSource = emptyList().asPagingSourceFactory().invoke(), + pagingSource = emptyList().asPagingSourceFactory().invoke(), ) // when & then val actual = viewModel.images.getFullScrollItems() @@ -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( @@ -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() } @@ -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() } @@ -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) } } }