Skip to content

Commit

Permalink
Merge pull request #78 from LanPet-dev/feat/upload_resource
Browse files Browse the repository at this point in the history
Feat/upload resource
  • Loading branch information
hyeseonpark authored Jan 18, 2025
2 parents 2be41db + 9afce0c commit 755aebb
Show file tree
Hide file tree
Showing 14 changed files with 384 additions and 96 deletions.
52 changes: 52 additions & 0 deletions core/common/src/main/java/com/lanpet/core/common/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ package com.lanpet.core.common
import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.widget.Toast
import androidx.compose.foundation.layout.WindowInsets
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
Expand Down Expand Up @@ -104,3 +108,51 @@ fun Context.toast(
) {
Toast.makeText(this, message, duration).show()
}

fun List<Uri>.toByteArrayList(context: Context): List<ByteArray> =
this.mapNotNull { uri ->
uri.toCompressedByteArray(context)
}

fun Uri.toCompressedByteArray(
context: Context,
targetWidth: Int = 800,
targetHeight: Int = 800,
compressFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 80,
): ByteArray? =
try {
context.contentResolver.openInputStream(this)?.use { inputStream ->
val originalBitmap = BitmapFactory.decodeStream(inputStream)

val originalWidth = originalBitmap.width
val originalHeight = originalBitmap.height

val aspectRatio = originalWidth.toFloat() / originalHeight
val newWidth: Int
val newHeight: Int
if (aspectRatio > 1) {
newWidth = targetWidth
newHeight = (targetWidth / aspectRatio).toInt()
} else {
newHeight = targetHeight
newWidth = (targetHeight * aspectRatio).toInt()
}

val resizedBitmap =
Bitmap.createScaledBitmap(
originalBitmap,
newWidth,
newHeight,
true,
)

ByteArrayOutputStream().use { outputStream ->
resizedBitmap.compress(compressFormat, quality, outputStream)
outputStream.toByteArray()
}
}
} catch (e: IOException) {
e.printStackTrace()
null
}
10 changes: 10 additions & 0 deletions core/di/src/main/java/com/lanpet/core/di/ApiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import com.lanpet.data.service.FreeBoardApiClient
import com.lanpet.data.service.FreeBoardApiService
import com.lanpet.data.service.ProfileApiClient
import com.lanpet.data.service.ProfileApiService
import com.lanpet.data.service.S3UploadApiClient
import com.lanpet.data.service.S3UploadApiService
import com.lanpet.data.service.interceptors.RefreshTokenInterceptor
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -59,4 +61,12 @@ object ApiModule {
fun provideFreeBoardApiUrl(
@Named("BaseApiUrl") baseUrl: String,
): String = baseUrl + "sarangbangs/"

@Singleton
@Provides
fun provideS3UploadApiClient(authStateHolder: AuthStateHolder): S3UploadApiClient = S3UploadApiClient(authStateHolder)

@Singleton
@Provides
fun provideS3UploadApiService(s3UploadApiClient: S3UploadApiClient): S3UploadApiService = s3UploadApiClient.getService()
}
10 changes: 10 additions & 0 deletions core/di/src/main/java/com/lanpet/core/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package com.lanpet.core.di
import com.lanpet.data.repository.AccountRepositoryImpl
import com.lanpet.data.repository.FreeBoardRepositoryImpl
import com.lanpet.data.repository.ProfileRepositoryImpl
import com.lanpet.data.repository.S3UploadRepositoryImpl
import com.lanpet.data.service.AccountApiService
import com.lanpet.data.service.FreeBoardApiService
import com.lanpet.data.service.ProfileApiService
import com.lanpet.data.service.S3UploadApiService
import com.lanpet.data.service.localdb.AuthDatabase
import com.lanpet.domain.repository.AccountRepository
import com.lanpet.domain.repository.FreeBoardRepository
import com.lanpet.domain.repository.ProfileRepository
import com.lanpet.domain.repository.S3UploadRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
Expand Down Expand Up @@ -43,4 +46,11 @@ class RepositoryModule {
profileService,
authDatabase,
)

@Singleton
@Provides
fun provideS3UploadRepository(s3UploadApiService: S3UploadApiService): S3UploadRepository =
S3UploadRepositoryImpl(
s3UploadApiService,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ fun AppNavigation(modifier: Modifier = Modifier) {
onNavigateToFreeBoardWriteFreeBoard = {
navController.navigateToFreeBoardWriteScreen()
},
onNavigateToFreeBoardDetail = { postId, profileId ->
navController.navigateToFreeBoardDetailScreen(postId, profileId)
onNavigateToFreeBoardDetail = { postId, profileId, navOptions ->
navController.navigateToFreeBoardDetailScreen(postId, profileId, navOptions)
},
)
wikiNavGraph()
Expand Down
1 change: 1 addition & 0 deletions data/repository/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.kotlin.android)
alias(libs.plugins.convention.lib.build)
alias(libs.plugins.convention.hilt)
alias(libs.plugins.convention.lib.retrofit)
}

android {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.lanpet.data.repository

import com.lanpet.data.service.S3UploadApiService
import com.lanpet.domain.repository.S3UploadRepository
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.RequestBody.Companion.toRequestBody
import javax.inject.Inject

class S3UploadRepositoryImpl
@Inject
constructor(
private val s3UploadApiService: S3UploadApiService,
) : S3UploadRepository {
override fun uploadImageResource(
url: String,
byteArray: ByteArray,
): Flow<Unit> =
flow {
val requestBody = byteArray.toRequestBody("image/jpeg".toMediaTypeOrNull())
val res = s3UploadApiService.uploadImage(url = url, body = requestBody)
emit(res)
}.flowOn(Dispatchers.IO)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.lanpet.data.service

import com.google.gson.GsonBuilder
import com.lanpet.core.manager.AuthStateHolder
import com.lanpet.domain.model.AuthState
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Inject

/**
* S3UploadApiClient는 S3 업로드를 위한 API 클라이언트입니다.
* 현재 페이지에서 사용하는 `baseUrl`은 S3UploadApiClient 빌드에만 사용되며,
* 실제 API 통신에는 사용되지 않습니다.
*/
class S3UploadApiClient
@Inject
constructor(
private val authStateHolder: AuthStateHolder,
private val baseUrl: String = "https://lanpet-resource.s3.ap-northeast-2.amazonaws.com/",
) {
private val headerInterceptor =
Interceptor { chain ->
if (authStateHolder.authState.value !is AuthState.Loading &&
authStateHolder.authState.value !is AuthState.Success
) {
throw SecurityException("x-access-token is required")
}

val request =
chain
.request()
.newBuilder()
.addHeader("Content-Type", "image/jpeg")
.build()
chain.proceed(request)
}

private val gson =
GsonBuilder()
.create()

private val okHttpClient =
OkHttpClient
.Builder()
.addInterceptor(headerInterceptor)
.build()

private fun apiService() =
Retrofit
.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(S3UploadApiService::class.java)

fun getService(): S3UploadApiService = apiService()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.lanpet.data.service

import okhttp3.RequestBody
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.PUT
import retrofit2.http.Url

interface S3UploadApiService {
@PUT
suspend fun uploadImage(
@Url url: String,
@Header("Content-Type") contentType: String = "image/jpeg",
@Body body: RequestBody,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.lanpet.domain.repository

import kotlinx.coroutines.flow.Flow

interface S3UploadRepository {
fun uploadImageResource(
url: String,
byteArray: ByteArray,
): Flow<Unit>
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.lanpet.domain.usecase.freeboard

import com.lanpet.domain.repository.FreeBoardRepository
import com.lanpet.domain.repository.S3UploadRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapConcat
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import javax.inject.Inject

class UploadImageResourceUseCase
@Inject
constructor(
private val freeBoardRepository: FreeBoardRepository,
private val s3UploadRepository: S3UploadRepository,
) {
@OptIn(FlowPreview::class)
operator fun invoke(
sarangbangId: String,
imageList: List<ByteArray>,
) = freeBoardRepository
.getResourceUploadUrl(sarangbangId, imageList.size)
.flatMapConcat { urlItems ->
flow {
coroutineScope {
val results = urlItems.items.mapIndexed { index, url ->
async {
s3UploadRepository
.uploadImageResource(
url = url,
byteArray = imageList[index]
)
.first()
}
}
delay(2000)
emit(results.awaitAll())
}
}
}
.flowOn(Dispatchers.IO)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.lanpet.free.navigation
import android.os.Build
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.lanpet.core.auth.LocalAuthManager
Expand All @@ -14,15 +15,17 @@ import kotlinx.serialization.Serializable
fun NavGraphBuilder.freeNavGraph(
onNavigateUp: () -> Unit,
onNavigateToFreeBoardWriteFreeBoard: () -> Unit,
onNavigateToFreeBoardDetail: (postId: String, profileId: String) -> Unit,
onNavigateToFreeBoardDetail: (postId: String, profileId: String, navOptions: NavOptions?) -> Unit,
) {
navigation<FreeBoardBaseRoute>(
startDestination = FreeBoard,
) {
composable<FreeBoard> {
FreeBoardScreen(
onNavigateToFreeBoardWrite = onNavigateToFreeBoardWriteFreeBoard,
onNavigateToFreeBoardDetail = onNavigateToFreeBoardDetail,
onNavigateToFreeBoardDetail = { postId, profileId ->
onNavigateToFreeBoardDetail(postId, profileId, null)
},
)
}
composable<FreeBoardDetail> {
Expand Down Expand Up @@ -53,6 +56,7 @@ fun NavGraphBuilder.freeNavGraph(
composable<FreeBoardWrite> {
FreeBoardWriteScreen(
onNavigateUp = onNavigateUp,
onNavigateToFreeBoardDetail = onNavigateToFreeBoardDetail,
)
}
}
Expand Down Expand Up @@ -83,10 +87,22 @@ fun NavController.navigateToFreeBoardScreen() {
fun NavController.navigateToFreeBoardDetailScreen(
postId: String,
profileId: String,
navOptions: NavOptions? = null,
) {
val defaultNavOptions =
NavOptions
.Builder()
.setLaunchSingleTop(true)
.apply {
navOptions?.let { options ->
setPopUpTo(options.popUpToId, options.isPopUpToInclusive())
}
}.build()

navigate(
FreeBoardDetail(postId = postId, profileId = profileId),
) {}
route = FreeBoardDetail(postId = postId, profileId = profileId),
navOptions = defaultNavOptions,
)
}

fun NavController.navigateToFreeBoardWriteScreen() {
Expand Down
Loading

0 comments on commit 755aebb

Please sign in to comment.