Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.poti.android.core.common.constant

object ImageConstants {
const val IMAGE_EXTENSION = "jpg"
const val IMAGE_CONTENT_TYPE = "image/jpeg"
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class S3UploadClient
annotation class FileUploadClient
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object NetworkModule {
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build()

@S3UploadClient
@FileUploadClient
@Provides
@Singleton
fun provideOkHttpClientForS3(): OkHttpClient = OkHttpClient.Builder().build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.poti.android.data.di
import com.poti.android.data.repository.ArtistRepositoryImpl
import com.poti.android.data.repository.AuthRepositoryImpl
import com.poti.android.data.repository.DeliveryRepositoryImpl
import com.poti.android.data.repository.FileUploadRepositoryImpl
import com.poti.android.data.repository.HomeRepositoryImpl
import com.poti.android.data.repository.ImageRepositoryImpl
import com.poti.android.data.repository.ParticipationRepositoryImpl
Expand All @@ -14,6 +15,7 @@ import com.poti.android.data.repository.UserRepositoryImpl
import com.poti.android.domain.repository.ArtistRepository
import com.poti.android.domain.repository.AuthRepository
import com.poti.android.domain.repository.DeliveryRepository
import com.poti.android.domain.repository.FileUploadRepository
import com.poti.android.domain.repository.HomeRepository
import com.poti.android.domain.repository.ImageRepository
import com.poti.android.domain.repository.ParticipationRepository
Expand Down Expand Up @@ -74,4 +76,8 @@ abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindReviewRepository(reviewRepositoryImpl: ReviewRepositoryImpl): ReviewRepository

@Binds
@Singleton
abstract fun bindFileUploadRepository(fileUploadRepositoryImpl: FileUploadRepositoryImpl): FileUploadRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.poti.android.data.local.datasource

import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.util.Size
import androidx.core.net.toUri
import com.poti.android.core.common.constant.ImageConstants.IMAGE_EXTENSION
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.UUID
import javax.inject.Inject
import kotlin.math.max

class FileLocalDataSource @Inject constructor(
@param:ApplicationContext private val context: Context,
) {
fun createImageFile(uriString: String): File {
val uri = uriString.toUri()

val directory = getDirectory()
val compressedImage = compressImage(uri, directory)

return compressedImage
}

fun clearDirectory() {
val directory = getDirectory()
directory.listFiles()?.forEach { it.delete() }
}

private fun getDirectory(): File = File(context.cacheDir, DIRECTORY).apply {
mkdirs()
}

private fun compressImage(
uri: Uri,
dir: File,
): File {
val source = ImageDecoder.createSource(context.contentResolver, uri)
val bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, _ ->
// 디코더 설정
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
decoder.isMutableRequired = true

// 리사이징 크기 설정
val targetSize = calculateTargetSize(info.size.width, info.size.height)
decoder.setTargetSize(targetSize.width, targetSize.height)
}

// bitmap을 리사이징(압축)한 jpeg byteArray 생성
val compressedImage = ByteArrayOutputStream().use { stream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, stream)
bitmap.recycle()
stream.toByteArray()
}

// 임시 파일 객체 생성
val tempFile = File(
dir,
"${UUID.randomUUID()}.$IMAGE_EXTENSION",
)

// 파일 객채에 리사이징 byteArray 덮어씌워 최종 이미지 파일 생성
tempFile.outputStream().use { stream ->
stream.write(compressedImage)
}

return tempFile
}

private fun calculateTargetSize(
width: Int,
height: Int,
): Size {
if (width <= MAX_WIDTH && height <= MAX_HEIGHT) return Size(width, height)

val ratio = max(width.toFloat() / MAX_WIDTH, height.toFloat() / MAX_HEIGHT)

return Size((width / ratio).toInt(), (height / ratio).toInt())
}

companion object {
private const val MAX_WIDTH = 1024
private const val MAX_HEIGHT = 1024
private const val QUALITY = 80
private const val DIRECTORY = "compressed"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.poti.android.data.remote.datasource

import com.poti.android.core.common.constant.ImageConstants.IMAGE_CONTENT_TYPE
import com.poti.android.data.di.FileUploadClient
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import javax.inject.Inject

class FileUploadRemoteDataSource @Inject constructor(
@param:FileUploadClient private val okHttpClient: OkHttpClient,
) {
suspend fun uploadImage(
uploadUrl: String,
file: File,
) = withContext(Dispatchers.IO) {
val requestBody = file.asRequestBody(IMAGE_CONTENT_TYPE.toMediaType())

val request = Request.Builder()
.url(uploadUrl)
.put(requestBody)
.header("Content-Type", IMAGE_CONTENT_TYPE)
.build()

okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IllegalStateException(
"File upload failed: ${response.code}",
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.poti.android.data.repository

import com.poti.android.core.network.util.HttpResponseHandler
import com.poti.android.data.local.datasource.FileLocalDataSource
import com.poti.android.data.remote.datasource.FileUploadRemoteDataSource
import com.poti.android.domain.repository.FileUploadRepository
import java.io.File
import javax.inject.Inject

class FileUploadRepositoryImpl @Inject constructor(
private val httpResponseHandler: HttpResponseHandler,
private val fileUploadRemoteDataSource: FileUploadRemoteDataSource,
private val fileLocalDataSource: FileLocalDataSource,
Comment thread
doyeon0307 marked this conversation as resolved.
) : FileUploadRepository {
override suspend fun uploadImage(
uploadUrl: String,
file: File,
): Result<Unit> = httpResponseHandler.safeApiCall {
fileUploadRemoteDataSource.uploadImage(uploadUrl, file)
}

override fun createImage(uriString: String): Result<File> {
try {
val file = fileLocalDataSource.createImageFile(uriString)
return Result.success(file)
} catch (exception: Throwable) {
return Result.failure(exception)
}
}

override fun clearDirectory(): Result<Unit> {
try {
fileLocalDataSource.clearDirectory()
return Result.success(Unit)
} catch (exception: Throwable) {
return Result.failure(exception)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.poti.android.data.repository

import com.poti.android.core.network.util.HttpResponseHandler
import com.poti.android.data.di.S3UploadClient
import com.poti.android.data.di.FileUploadClient
import com.poti.android.domain.model.image.PresignedUploadInfo
import com.poti.android.domain.repository.S3Repository
import okhttp3.MediaType.Companion.toMediaType
Expand All @@ -13,7 +13,7 @@ import javax.inject.Inject

class S3RepositoryImpl @Inject constructor(
private val httpResponseHandler: HttpResponseHandler,
@param:S3UploadClient private val okHttpClient: OkHttpClient,
@param:FileUploadClient private val okHttpClient: OkHttpClient,
) : S3Repository {
override suspend fun uploadImages(
uploadInfos: List<PresignedUploadInfo>,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.poti.android.domain.repository

import java.io.File

interface FileUploadRepository {
suspend fun uploadImage(
uploadUrl: String,
file: File,
): Result<Unit>

fun createImage(uriString: String): Result<File>

fun clearDirectory(): Result<Unit>
}
Comment thread
doyeon0307 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.poti.android.domain.usecase.image

import com.poti.android.core.common.constant.ImageConstants.IMAGE_EXTENSION
import com.poti.android.domain.model.image.PresignedUploadInfo
import com.poti.android.domain.repository.FileUploadRepository
import com.poti.android.domain.repository.ImageRepository
import java.io.File
import javax.inject.Inject
import kotlin.coroutines.cancellation.CancellationException

class UploadImagesUseCaseV2 @Inject constructor(
private val imageRepository: ImageRepository,
private val fileUploadRepository: FileUploadRepository,
) {
suspend operator fun invoke(
uploadType: String,
uriStrings: List<String>,
): Result<List<String>> {
try {
val uploadInfos = getUploadUrls(uploadType, uriStrings.size)
val (urls, fileNames) = uploadInfos.map { it.url to it.fileName }.unzip()
val files = createImages(uriStrings)

uploadImages(urls, files)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
return Result.success(fileNames)
} catch (t: Throwable) {
if (t is CancellationException) throw t
return Result.failure(t)
} finally {
fileUploadRepository.clearDirectory()
}
Comment thread
doyeon0307 marked this conversation as resolved.
}

private suspend fun getUploadUrls(
uploadType: String,
size: Int,
): List<PresignedUploadInfo> = imageRepository.getPresignedUrls(
type = uploadType,
extensions = List(size) { IMAGE_EXTENSION },
).getOrThrow()

private fun createImages(
uriStrings: List<String>,
): List<File> = uriStrings.map { uri ->
fileUploadRepository.createImage(uri).getOrThrow()
}

private suspend fun uploadImages(
urls: List<String>,
files: List<File>,
) {
if (urls.size != files.size) {
throw IllegalStateException("Upload URL count and file count must match")
}

for (i in urls.indices) {
fileUploadRepository.uploadImage(urls[i], files[i]).getOrThrow()
}
}
}