diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt index b9ac914f..2bde022e 100644 --- a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/di/RepositoryModule.kt @@ -1,9 +1,11 @@ package ac.dnd.bookkeeping.android.data.di import ac.dnd.bookkeeping.android.data.repository.authentication.MockAuthenticationRepository +import ac.dnd.bookkeeping.android.data.repository.file.MockFileRepository import ac.dnd.bookkeeping.android.data.repository.member.MockMemberRepository import ac.dnd.bookkeeping.android.data.repository.sociallogin.KakaoLoginRepositoryImpl import ac.dnd.bookkeeping.android.domain.repository.AuthenticationRepository +import ac.dnd.bookkeeping.android.domain.repository.FileRepository import ac.dnd.bookkeeping.android.domain.repository.KakaoLoginRepository import ac.dnd.bookkeeping.android.domain.repository.MemberRepository import dagger.Binds @@ -28,6 +30,12 @@ internal abstract class RepositoryModule { memberRepository: MockMemberRepository ): MemberRepository + @Binds + @Singleton + abstract fun bindsFileRepository( + fileRepository: MockFileRepository + ): FileRepository + @Binds @Singleton abstract fun bindsKakaoLoginRepository( diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt new file mode 100644 index 00000000..66b66f3a --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/api/FileApi.kt @@ -0,0 +1,73 @@ +package ac.dnd.bookkeeping.android.data.remote.network.api + +import ac.dnd.bookkeeping.android.data.remote.network.di.AuthHttpClient +import ac.dnd.bookkeeping.android.data.remote.network.di.NoAuthHttpClient +import ac.dnd.bookkeeping.android.data.remote.network.environment.BaseUrlProvider +import ac.dnd.bookkeeping.android.data.remote.network.environment.ErrorMessageMapper +import ac.dnd.bookkeeping.android.data.remote.network.model.file.GetPreSignedUrlRes +import ac.dnd.bookkeeping.android.data.remote.network.util.convert +import android.net.Uri +import android.webkit.MimeTypeMap +import io.ktor.client.HttpClient +import io.ktor.client.request.forms.formData +import io.ktor.client.request.forms.submitFormWithBinaryData +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.http.Headers +import io.ktor.http.HttpHeaders +import java.io.File +import javax.inject.Inject + + +class FileApi @Inject constructor( + @NoAuthHttpClient private val noAuthClient: HttpClient, + @AuthHttpClient private val client: HttpClient, + private val baseUrlProvider: BaseUrlProvider, + private val errorMessageMapper: ErrorMessageMapper +) { + private val baseUrl: String + get() = baseUrlProvider.get() + + suspend fun getPreSignedUrl( + fileName: String + ): Result { + return client.get("$baseUrl/api/v1/files/presigned") { + parameter("fileName", fileName) + }.convert(errorMessageMapper::map) + } + + suspend fun upload( + preSignedUrl: String, + imageUri: String, + fileName: String? = null + ): Result { + val image = Uri.parse(imageUri)?.path ?: let { + return Result.failure(IllegalArgumentException("Invalid imageUri")) + } + val file = File(image) + val name = fileName ?: file.name + val contentType = getContentType(file.path) + + return noAuthClient.submitFormWithBinaryData( + url = preSignedUrl, + formData = formData { + append( + "image", + file.readBytes(), + Headers.build { + contentType?.let { append(HttpHeaders.ContentType, it) } + append(HttpHeaders.ContentDisposition, "filename=$name") + } + ) + } + ).convert(errorMessageMapper::map) + } + + private fun getContentType( + url: String + ): String? { + return MimeTypeMap.getFileExtensionFromUrl(url)?.let { fileExtension -> + MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension) + } + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt new file mode 100644 index 00000000..deaa6bcc --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/remote/network/model/file/GetPreSignedUrlRes.kt @@ -0,0 +1,21 @@ +package ac.dnd.bookkeeping.android.data.remote.network.model.file + +import ac.dnd.bookkeeping.android.data.remote.mapper.DataMapper +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetPreSignedUrlRes( + @SerialName("preSignedUrl") + val preSignedUrl: String, + @SerialName("uploadFileUrl") + val uploadFileUrl: String +) : DataMapper { + override fun toDomain(): PreSignedUrl { + return PreSignedUrl( + preSignedUrl = preSignedUrl, + uploadFileUrl = uploadFileUrl + ) + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt new file mode 100644 index 00000000..1429a110 --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/MockFileRepository.kt @@ -0,0 +1,36 @@ +package ac.dnd.bookkeeping.android.data.repository.file + +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import ac.dnd.bookkeeping.android.domain.repository.FileRepository +import kotlinx.coroutines.delay +import javax.inject.Inject + +class MockFileRepository @Inject constructor() : FileRepository { + override suspend fun getPreSignedUrl( + fileName: String + ): Result { + randomShortDelay() + return Result.success( + PreSignedUrl( + preSignedUrl = "https://example.com", + uploadFileUrl = "https://example.com" + ) + ) + } + + override suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result { + randomLongDelay() + return Result.success(Unit) + } + + private suspend fun randomShortDelay() { + delay(LongRange(100, 500).random()) + } + + private suspend fun randomLongDelay() { + delay(LongRange(500, 2000).random()) + } +} diff --git a/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt new file mode 100644 index 00000000..3d7b473c --- /dev/null +++ b/data/src/main/kotlin/ac/dnd/bookkeeping/android/data/repository/file/RealFileRepository.kt @@ -0,0 +1,29 @@ +package ac.dnd.bookkeeping.android.data.repository.file + +import ac.dnd.bookkeeping.android.data.remote.local.SharedPreferencesManager +import ac.dnd.bookkeeping.android.data.remote.network.api.FileApi +import ac.dnd.bookkeeping.android.data.remote.network.util.toDomain +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl +import ac.dnd.bookkeeping.android.domain.repository.FileRepository +import javax.inject.Inject + +class RealFileRepository @Inject constructor( + private val fileApi: FileApi, + private val sharedPreferencesManager: SharedPreferencesManager +) : FileRepository { + override suspend fun getPreSignedUrl( + fileName: String + ): Result { + return fileApi.getPreSignedUrl(fileName).toDomain() + } + + override suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result { + return fileApi.upload( + preSignedUrl = preSignedUrl, + imageUri = imageUri + ) + } +} diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt new file mode 100644 index 00000000..043aad03 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/model/file/PreSignedUrl.kt @@ -0,0 +1,6 @@ +package ac.dnd.bookkeeping.android.domain.model.file + +data class PreSignedUrl( + val preSignedUrl: String, + val uploadFileUrl: String +) diff --git a/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt new file mode 100644 index 00000000..af80f801 --- /dev/null +++ b/domain/src/main/kotlin/ac/dnd/bookkeeping/android/domain/repository/FileRepository.kt @@ -0,0 +1,14 @@ +package ac.dnd.bookkeeping.android.domain.repository + +import ac.dnd.bookkeeping.android.domain.model.file.PreSignedUrl + +interface FileRepository { + suspend fun getPreSignedUrl( + fileName: String + ): Result + + suspend fun upload( + preSignedUrl: String, + imageUri: String + ): Result +}