Skip to content

Commit

Permalink
Feature/profile image (#47)
Browse files Browse the repository at this point in the history
* 프로필 이미지 업로드 UI, UseCase구현

* image upload api 구현(미완)

* file provider 추가

* image upload api 수정

* 이미지 저장되지 않는 현상 수정
  • Loading branch information
leesa-l authored Oct 3, 2024
1 parent 298591a commit b8e3e68
Show file tree
Hide file tree
Showing 22 changed files with 427 additions and 51 deletions.
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@
android:value="androidx.startup" />
</provider>

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.dkin.chevit.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

<activity
android:name=".MainActivity"
android:exported="true"
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/res/xml/file_paths.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--Context.getFilesDir()-->
<files-path name="files" path="." />

<!--getCacheDir()-->
<cache-path name="cache" path="." />

<!--Environment.getExternalStorageDirectory()-->
<external-path name="external" path="." />

<!--Context.getExternalFilesDir(null)-->
<external-files-path name="external_files" path="."/>

<!--Context.getExternalCacheDir()-->
<external-cache-path name="external_cache" path="."/>

<!--only available on API 21+ devices.-->
<!--Context.getExternalMediaDirs()-->
<external-media-path name="external_media" path="."/>
</paths>
27 changes: 27 additions & 0 deletions data/src/debug/java/com/dkin/chevit/data/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter
import retrofit2.Retrofit
import javax.inject.Named

@Module
@InstallIn(SingletonComponent::class)
Expand Down Expand Up @@ -86,6 +87,20 @@ internal object NetworkModule {
.addInterceptor(tokenInterceptor)
.build()

@Provides
@Singleton
@Named("Pure")
fun providePureOkHttpClient(
httpLoggingInterceptor: HttpLoggingInterceptor,
chuckerInterceptor: ChuckerInterceptor,
) = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addInterceptor(httpLoggingInterceptor)
.addInterceptor(chuckerInterceptor)
.build()

@Provides
@Singleton
fun provideRetrofit(
Expand All @@ -96,4 +111,16 @@ internal object NetworkModule {
.addConverterFactory(jsonConverter)
.baseUrl(BuildConfig.API_URL)
.build()

@Provides
@Singleton
@Named("Pure")
fun providePureRetrofit(
@Named("Pure") okHttpClient: OkHttpClient,
@JsonConverter jsonConverter: Converter.Factory,
): Retrofit = Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(jsonConverter)
.baseUrl(BuildConfig.API_URL)
.build()
}
8 changes: 8 additions & 0 deletions data/src/main/java/com/dkin/chevit/data/di/RetrofitModule.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dkin.chevit.data.di

import com.dkin.chevit.data.remote.AuthAPI
import com.dkin.chevit.data.remote.ImageAPI
import com.dkin.chevit.data.remote.NotificationAPI
import com.dkin.chevit.data.remote.PlanAPI
import com.dkin.chevit.data.remote.ServiceAPI
Expand All @@ -10,6 +11,7 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
import retrofit2.Retrofit
import javax.inject.Named

@Module
@InstallIn(SingletonComponent::class)
Expand Down Expand Up @@ -37,4 +39,10 @@ internal object RetrofitModule {
fun providePlanAPI(
retrofit: Retrofit
): PlanAPI = retrofit.create(PlanAPI::class.java)

@Provides
@Singleton
fun provideImageAPI(
@Named("Pure") retrofit: Retrofit
): ImageAPI = retrofit.create(ImageAPI::class.java)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package com.dkin.chevit.data.di.usecase

import com.dkin.chevit.domain.base.CoroutineDispatcherProvider
import com.dkin.chevit.domain.repository.AuthRepository
import com.dkin.chevit.domain.usecase.auth.GetProfileImageDataUseCase
import com.dkin.chevit.domain.usecase.auth.GetUserStateUseCase
import com.dkin.chevit.domain.usecase.auth.GetUserUseCase
import com.dkin.chevit.domain.usecase.auth.SignOutUseCase
import com.dkin.chevit.domain.usecase.auth.SignUpUserUseCase
import com.dkin.chevit.domain.usecase.auth.UpdateUserUseCase
import com.dkin.chevit.domain.usecase.auth.UploadProfileImageUseCase
import com.dkin.chevit.domain.usecase.auth.WithDrawUserUseCase
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -69,4 +71,22 @@ internal object AuthUseCaseModule {
coroutineDispatcherProvider,
authRepository
)

@Provides
fun provideGetProfileImageDataUseCase(
coroutineDispatcherProvider: CoroutineDispatcherProvider,
authRepository: AuthRepository,
) = GetProfileImageDataUseCase(
coroutineDispatcherProvider,
authRepository
)

@Provides
fun provideUploadProfileImageUseCase(
coroutineDispatcherProvider: CoroutineDispatcherProvider,
authRepository: AuthRepository,
) = UploadProfileImageUseCase(
coroutineDispatcherProvider,
authRepository
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dkin.chevit.data.model.request

import com.dkin.chevit.data.DataModel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class ProfileImageUploadPayload(
@SerialName("fileSize") val fileSize: Int,
@SerialName("mimeType") val mimeType: String,
) : DataModel
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.dkin.chevit.data.model.response

import com.dkin.chevit.data.DataModel
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class ProfileImageUploadResponse(
@SerialName("uploadMethod") val uploadMethod: String = "",
@SerialName("uploadURL") val uploadURL: String = "",
@SerialName("imageURL") val imageURL: String = "",
) : DataModel
11 changes: 11 additions & 0 deletions data/src/main/java/com/dkin/chevit/data/remote/AuthAPI.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.dkin.chevit.data.remote

import com.dkin.chevit.data.model.request.ProfileImageUploadPayload
import com.dkin.chevit.data.model.request.SignUpPayload
import com.dkin.chevit.data.model.request.UpdateUserPayload
import com.dkin.chevit.data.model.request.ValidationNicknamePayload
import com.dkin.chevit.data.model.response.ProfileImageUploadResponse
import com.dkin.chevit.data.model.response.UserResponse
import com.dkin.chevit.domain.base.None
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.Body
import retrofit2.http.DELETE
import retrofit2.http.GET
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Part
import retrofit2.http.Url

/**
* 유저 정보 및 인증 관련 API 모음
Expand All @@ -29,4 +37,7 @@ internal interface AuthAPI {

@DELETE("deleteUser")
suspend fun deleteUser(): Response<Unit>

@POST("getProfileUploadURL")
suspend fun getProfileUploadURL(@Body body: ProfileImageUploadPayload): ProfileImageUploadResponse
}
14 changes: 14 additions & 0 deletions data/src/main/java/com/dkin/chevit/data/remote/ImageAPI.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.dkin.chevit.data.remote

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

internal interface ImageAPI {
@PUT
suspend fun uploadProfileImage(
@Url url: String,
@Body file: RequestBody
)
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
package com.dkin.chevit.data.repository

import com.dkin.chevit.data.model.request.ProfileImageUploadPayload
import com.dkin.chevit.data.model.request.SignUpPayload
import com.dkin.chevit.data.model.request.UpdateUserPayload
import com.dkin.chevit.data.model.response.toUser
import com.dkin.chevit.data.remote.AuthAPI
import com.dkin.chevit.data.remote.ImageAPI
import com.dkin.chevit.domain.base.None
import com.dkin.chevit.domain.model.ProfileImageData
import com.dkin.chevit.domain.model.UserState
import com.dkin.chevit.domain.repository.AuthRepository
import com.google.firebase.auth.FirebaseAuth
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File
import javax.inject.Inject

internal class AuthRepositoryImpl @Inject constructor(
private val authAPI: AuthAPI,
private val auth: FirebaseAuth
private val imageAPI: ImageAPI,
private val auth: FirebaseAuth,
) : AuthRepository {
override suspend fun getUserState(): UserState {
return runCatching {
Expand Down Expand Up @@ -43,4 +53,29 @@ internal class AuthRepositoryImpl @Inject constructor(
auth.signOut()
return getUserState()
}

override suspend fun getProfileUploadURL(fileSize: Int): ProfileImageData {
val result = authAPI.getProfileUploadURL(
ProfileImageUploadPayload(
fileSize = fileSize,
mimeType = "image/jpeg"
)
)
return ProfileImageData(
uploadMethod = result.uploadMethod,
uploadURL = result.uploadURL,
uploadHeaders = "{\"Content-Type\": [\"image/jpeg\"]}",
imageURL = result.imageURL,
)
}

override suspend fun uploadProfileImage(
uploadURL: String,
uploadMethod: String,
uploadHeaders: String,
file: File
) {
val requestFile: RequestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull())
return imageAPI.uploadProfileImage(uploadURL, requestFile)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.dkin.chevit.domain.model

import com.dkin.chevit.domain.base.DomainModel

data class ProfileImageData(
val uploadMethod: String,
val uploadURL: String,
val uploadHeaders: String,
val imageURL: String
) : DomainModel
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package com.dkin.chevit.domain.model
import com.dkin.chevit.domain.base.DomainModel

sealed interface UserState : DomainModel {
object Guest : UserState
data object Guest : UserState

object NotRegister : UserState
data object NotRegister : UserState

data class User(
val id: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.dkin.chevit.domain.repository

import com.dkin.chevit.domain.base.None
import com.dkin.chevit.domain.model.ProfileImageData
import com.dkin.chevit.domain.model.UserState
import java.io.File

interface AuthRepository {
suspend fun getUserState(): UserState
Expand All @@ -12,4 +15,8 @@ interface AuthRepository {
suspend fun signOutUser(): UserState

suspend fun withDrawUser(): UserState

suspend fun getProfileUploadURL(fileSize: Int): ProfileImageData

suspend fun uploadProfileImage(uploadURL: String, uploadMethod: String, uploadHeaders: String, file: File)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.dkin.chevit.domain.usecase.auth

import com.dkin.chevit.domain.base.CoroutineDispatcherProvider
import com.dkin.chevit.domain.base.IOUseCase
import com.dkin.chevit.domain.model.ProfileImageData
import com.dkin.chevit.domain.repository.AuthRepository

class GetProfileImageDataUseCase(
coroutineDispatcherProvider: CoroutineDispatcherProvider,
private val authRepository: AuthRepository,
) : IOUseCase<GetProfileImageDataUseCase.Param, ProfileImageData>(coroutineDispatcherProvider = coroutineDispatcherProvider) {
override suspend fun execute(params: Param): ProfileImageData {
return authRepository.getProfileUploadURL(params.fileSize)
}

@JvmInline
value class Param(val fileSize: Int)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.dkin.chevit.domain.usecase.auth

import com.dkin.chevit.domain.base.CoroutineDispatcherProvider
import com.dkin.chevit.domain.base.IOUseCase
import com.dkin.chevit.domain.base.None
import com.dkin.chevit.domain.repository.AuthRepository
import java.io.File

class UploadProfileImageUseCase(
coroutineDispatcherProvider: CoroutineDispatcherProvider,
private val authRepository: AuthRepository,
) : IOUseCase<UploadProfileImageUseCase.Param, None>(coroutineDispatcherProvider = coroutineDispatcherProvider) {
override suspend fun execute(params: Param): None {
authRepository.uploadProfileImage(
uploadURL = params.uploadURL,
uploadMethod = params.uploadMethod,
uploadHeaders = params.uploadHeaders,
file = params.file
)
return None
}

data class Param(
val uploadURL: String,
val uploadMethod: String,
val uploadHeaders: String,
val file: File
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ private fun HomeStable(
.crossfade(true)
.build(),
contentDescription = "",
contentScale = ContentScale.Fit,
contentScale = ContentScale.FillBounds,
error = painterResource(id = R.drawable.ic_profile_empty)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ fun UserContents(
.crossfade(true)
.build(),
contentDescription = "",
contentScale = ContentScale.Fit,
contentScale = ContentScale.FillBounds,
error = painterResource(id = drawable.ic_profile_empty)
)
}
Expand Down
Loading

0 comments on commit b8e3e68

Please sign in to comment.