Skip to content
11 changes: 11 additions & 0 deletions app/src/main/java/org/sopt/certi/core/util/StringExt.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.sopt.certi.core.util

import org.sopt.certi.domain.model.DateData
import java.time.LocalDate
import java.time.format.DateTimeFormatter

Expand All @@ -17,3 +18,13 @@ fun String.toLocalDateOrMin(): LocalDate =
fun String.toLocalDateOrNull(): LocalDate? =
runCatching { LocalDate.parse(this, DateFormatters.dotDate) }
.getOrElse { null }

fun String.toDateData(): DateData {
val parts = this.split("-")

return DateData(
year = parts[0].toInt(),
month = parts[1].toInt(),
day = parts[2].toInt()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.sopt.certi.data.mapper.todomain.image

import org.sopt.certi.data.remote.dto.response.PresignedResponseDto
import org.sopt.certi.domain.model.image.PresignedData

fun PresignedResponseDto.toDomain(): PresignedData = PresignedData(
presignedUrl = preSignedURL,
publicUrl = publicURL
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.sopt.certi.data.mapper.todomain.user

import org.sopt.certi.core.util.toDateData
import org.sopt.certi.data.remote.dto.response.GetPersonalInfoResponseDto
import org.sopt.certi.domain.model.DateData
import org.sopt.certi.domain.model.user.PersonalInfo

fun GetPersonalInfoResponseDto.toDomain(): PersonalInfo = PersonalInfo(
name = name,
nickname = nickName,
email = email,
birth = birthDate?.toDateData() ?: DateData(),
profileImageUrl = profileImageURL ?: ""
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.sopt.certi.data.mapper.todto.user

import org.sopt.certi.data.remote.dto.request.PutPersonalInfoRequestDto
import org.sopt.certi.domain.model.user.PersonalInfo

fun PersonalInfo.toDto(): PutPersonalInfoRequestDto = PutPersonalInfoRequestDto(
name = name,
email = email,
nickName = nickname,
birthDate = if (!birth.isComplete) "" else "${birth.year}.${birth.month?.let { "%02d".format(it) }}.${birth.day?.let { "%02d".format(it) }}",
publicURL = profileImageUrl
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.certi.data.remote.datasource

import android.net.Uri

interface S3DataSource {
suspend fun uploadImageToS3(presignedUrl: String, imageUri: Uri)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@ package org.sopt.certi.data.remote.datasource
import org.sopt.certi.data.remote.dto.base.ApiResponse
import org.sopt.certi.data.remote.dto.base.NullableApiResponse
import org.sopt.certi.data.remote.dto.request.ModifyInterestedJobRequestDto
import org.sopt.certi.data.remote.dto.request.PutPersonalInfoRequestDto
import org.sopt.certi.data.remote.dto.response.GetInterestJobListResponseDto
import org.sopt.certi.data.remote.dto.response.GetPersonalInfoResponseDto
import org.sopt.certi.data.remote.dto.response.GetMyPageResponseDto
import org.sopt.certi.data.remote.dto.response.GetUserTrackResponseDto
import org.sopt.certi.data.remote.dto.response.PresignedResponseDto

interface UserRemoteDataSource {
suspend fun checkNicknameValidation(keyword: String): NullableApiResponse<Unit>
suspend fun getInterestedJobList(): ApiResponse<GetInterestJobListResponseDto>
suspend fun modifyInterestedJobList(jobNameList: ModifyInterestedJobRequestDto): NullableApiResponse<Unit>
suspend fun getUserTrack(): ApiResponse<GetUserTrackResponseDto>
suspend fun getMyPageInfo(): ApiResponse<GetMyPageResponseDto>
suspend fun getPersonalInfo(): ApiResponse<GetPersonalInfoResponseDto>
suspend fun putPersonalInfo(request: PutPersonalInfoRequestDto): NullableApiResponse<Unit>
suspend fun getPresignedUrl(): ApiResponse<PresignedResponseDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sopt.certi.data.remote.datasourceimpl

import android.content.Context
import android.net.Uri
import dagger.hilt.android.qualifiers.ApplicationContext
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import org.sopt.certi.data.remote.datasource.S3DataSource
import org.sopt.certi.data.remote.service.S3Service
import javax.inject.Inject

class S3DataSourceImpl @Inject constructor(
private val s3Service: S3Service,
@ApplicationContext private val context: Context
) : S3DataSource {
override suspend fun uploadImageToS3(presignedUrl: String, imageUri: Uri) {
val inputStream = context.contentResolver.openInputStream(imageUri)
val byteArray = inputStream?.readBytes() ?: throw IllegalStateException("이미지를 읽을 수 없습니다.")
inputStream.close()

val requestBody = byteArray.toRequestBody("image/*".toMediaTypeOrNull())

val response = s3Service.uploadImage(presignedUrl, requestBody)

if (!response.isSuccessful) {
throw Exception("S3 업로드 실패: ${response.code()}")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import org.sopt.certi.data.remote.datasource.UserRemoteDataSource
import org.sopt.certi.data.remote.dto.base.ApiResponse
import org.sopt.certi.data.remote.dto.base.NullableApiResponse
import org.sopt.certi.data.remote.dto.request.ModifyInterestedJobRequestDto
import org.sopt.certi.data.remote.dto.request.PutPersonalInfoRequestDto
import org.sopt.certi.data.remote.dto.response.GetInterestJobListResponseDto
import org.sopt.certi.data.remote.dto.response.GetMyPageResponseDto
import org.sopt.certi.data.remote.dto.response.GetPersonalInfoResponseDto
import org.sopt.certi.data.remote.dto.response.GetUserTrackResponseDto
import org.sopt.certi.data.remote.dto.response.PresignedResponseDto
import org.sopt.certi.data.remote.service.UserService
import javax.inject.Inject

Expand All @@ -28,4 +31,13 @@ class UserRemoteDataSourceImpl @Inject constructor(

override suspend fun getMyPageInfo(): ApiResponse<GetMyPageResponseDto> =
userService.getMyPageInfo()

override suspend fun getPersonalInfo(): ApiResponse<GetPersonalInfoResponseDto> =
userService.getPersonalInfo()

override suspend fun putPersonalInfo(request: PutPersonalInfoRequestDto): NullableApiResponse<Unit> =
userService.putPersonalInfo(request)

override suspend fun getPresignedUrl(): ApiResponse<PresignedResponseDto> =
userService.getPresignedUrl()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.sopt.certi.data.remote.dto.request

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class PutPersonalInfoRequestDto(
@SerialName("name")
val name: String,
@SerialName("email")
val email: String,
@SerialName("nickName")
val nickName: String,
@SerialName("birthDate")
val birthDate: String,
@SerialName("publicURL")
val publicURL: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.sopt.certi.data.remote.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GetPersonalInfoResponseDto(
@SerialName("nickName")
val nickName: String,
@SerialName("name")
val name: String,
@SerialName("email")
val email: String,
@SerialName("birthDate")
val birthDate: String?,
@SerialName("profileImageURL")
val profileImageURL: String?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.sopt.certi.data.remote.dto.response

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class PresignedResponseDto(
@SerialName("preSignedURL")
val preSignedURL: String,
@SerialName("publicURL")
val publicURL: String
)
15 changes: 15 additions & 0 deletions app/src/main/java/org/sopt/certi/data/remote/service/S3Service.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.sopt.certi.data.remote.service

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

interface S3Service {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[3] 왜 이름을 S3로 하셨나요 변수나 함수이름에 숫자 넣기 귀찮을텐디

@PUT
suspend fun uploadImage(
@Url presignedUrl: String,
@Body file: RequestBody
): Response<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package org.sopt.certi.data.remote.service
import org.sopt.certi.data.remote.dto.base.ApiResponse
import org.sopt.certi.data.remote.dto.base.NullableApiResponse
import org.sopt.certi.data.remote.dto.request.ModifyInterestedJobRequestDto
import org.sopt.certi.data.remote.dto.request.PutPersonalInfoRequestDto
import org.sopt.certi.data.remote.dto.response.GetInterestJobListResponseDto
import org.sopt.certi.data.remote.dto.response.GetMyPageResponseDto
import org.sopt.certi.data.remote.dto.response.GetPersonalInfoResponseDto
import org.sopt.certi.data.remote.dto.response.GetUserTrackResponseDto
import org.sopt.certi.data.remote.dto.response.PresignedResponseDto
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Query

interface UserService {
Expand All @@ -28,4 +32,13 @@ interface UserService {

@GET("/api/v1/user/mypage")
suspend fun getMyPageInfo(): ApiResponse<GetMyPageResponseDto>

@GET("/api/v1/user/pinfo")
suspend fun getPersonalInfo(): ApiResponse<GetPersonalInfoResponseDto>

@PUT("/api/v1/user/pinfo")
suspend fun putPersonalInfo(@Body request: PutPersonalInfoRequestDto): NullableApiResponse<Unit>

@GET("/api/v1/user/presigned-url")
suspend fun getPresignedUrl(): ApiResponse<PresignedResponseDto>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.certi.data.repositoryimpl

import androidx.core.net.toUri
import org.sopt.certi.data.remote.datasource.S3DataSource
import org.sopt.certi.domain.repository.S3Repository
import javax.inject.Inject

class S3RepositoryImpl @Inject constructor(
private val s3DataSource: S3DataSource
) : S3Repository {
override suspend fun uploadImage(presignedUrl: String, imageUri: String): Result<Unit> {
return runCatching {
s3DataSource.uploadImageToS3(presignedUrl, imageUri.toUri())
}
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package org.sopt.certi.data.repositoryimpl

import org.sopt.certi.data.mapper.todomain.image.toDomain
import org.sopt.certi.data.mapper.todomain.user.toDomain
import org.sopt.certi.data.mapper.todto.user.toDto
import org.sopt.certi.data.remote.datasource.UserRemoteDataSource
import org.sopt.certi.data.remote.dto.request.ModifyInterestedJobRequestDto
import org.sopt.certi.data.remote.util.HttpResponseHandler.handleApiResponse
import org.sopt.certi.data.remote.util.HttpResponseHandler.handleNullableApiResponse
import org.sopt.certi.data.remote.util.safeApiCall
import org.sopt.certi.domain.model.image.PresignedData
import org.sopt.certi.domain.model.user.InterestedJobListData
import org.sopt.certi.domain.model.user.MyPageInfo
import org.sopt.certi.domain.model.user.PersonalInfo
import org.sopt.certi.domain.repository.UserRepository
import javax.inject.Inject

Expand Down Expand Up @@ -48,4 +52,24 @@ class UserRepositoryImpl @Inject constructor(
.getOrThrow()
.toDomain()
}

override suspend fun getPersonalInfo(): Result<PersonalInfo> = safeApiCall {
userRemoteDataSource.getPersonalInfo()
.handleApiResponse()
.getOrThrow()
.toDomain()
}

override suspend fun putPersonalInfo(request: PersonalInfo): Result<Unit> = safeApiCall {
userRemoteDataSource.putPersonalInfo(request.toDto())
.handleNullableApiResponse()
.getOrThrow()
}

override suspend fun getPresignedUrl(): Result<PresignedData> = safeApiCall {
userRemoteDataSource.getPresignedUrl()
.handleApiResponse()
.getOrThrow()
.toDomain()
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/sopt/certi/di/DataSourceModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.sopt.certi.data.remote.datasource.DummyRemoteDataSource
import org.sopt.certi.data.remote.datasource.HomeRemoteDataSource
import org.sopt.certi.data.remote.datasource.PreCertEditRemoteDataSource
import org.sopt.certi.data.remote.datasource.PreCertRemoteDataSource
import org.sopt.certi.data.remote.datasource.S3DataSource
import org.sopt.certi.data.remote.datasourceimpl.AcquisitionRemoteDataSourceImpl
import org.sopt.certi.data.remote.datasource.UserRemoteDataSource
import org.sopt.certi.data.remote.datasourceimpl.ActivityRemoteDataSourceImpl
Expand All @@ -24,6 +25,7 @@ import org.sopt.certi.data.remote.datasourceimpl.DummyRemoteDataSourceImpl
import org.sopt.certi.data.remote.datasourceimpl.HomeRemoteDataSourceImpl
import org.sopt.certi.data.remote.datasourceimpl.PreCertEditRemoteDataSourceImpl
import org.sopt.certi.data.remote.datasourceimpl.PreCertRemoteDataSourceImpl
import org.sopt.certi.data.remote.datasourceimpl.S3DataSourceImpl
import org.sopt.certi.data.remote.datasourceimpl.UserRemoteDataSourceImpl

@Module
Expand Down Expand Up @@ -68,4 +70,8 @@ abstract class DataSourceModule {
@Binds
@Singleton
abstract fun bindsPreCertEditDataSource(preCertEditRemoteDataSourceImpl: PreCertEditRemoteDataSourceImpl): PreCertEditRemoteDataSource

@Binds
@Singleton
abstract fun bindsS3DataSource(s3DataSourceImpl: S3DataSourceImpl): S3DataSource
}
26 changes: 26 additions & 0 deletions app/src/main/java/org/sopt/certi/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import okhttp3.logging.HttpLoggingInterceptor
import org.sopt.certi.BuildConfig
import org.sopt.certi.core.network.TokenManager
import retrofit2.Retrofit
import javax.inject.Named

@Module
@InstallIn(SingletonComponent::class)
Expand Down Expand Up @@ -96,4 +97,29 @@ object NetworkModule {
json.asConverterFactory(requireNotNull("application/json".toMediaTypeOrNull()))
)
.build()

@Provides
@Singleton
@Named("S3Client") // 이름표 붙임
fun providesS3OkHttpClient(
loggingInterceptor: HttpLoggingInterceptor
): OkHttpClient =
OkHttpClient.Builder().apply {
connectTimeout(20, TimeUnit.SECONDS)
writeTimeout(20, TimeUnit.SECONDS)
readTimeout(20, TimeUnit.SECONDS)
addInterceptor(loggingInterceptor)
}.build()

@Provides
@Singleton
@Named("S3Retrofit")
fun provideS3Retrofit(
@Named("S3Client") okHttpClient: OkHttpClient
): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.build()
}
}
6 changes: 6 additions & 0 deletions app/src/main/java/org/sopt/certi/di/RepositoryModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.sopt.certi.data.repositoryimpl.DummyRepositoryImpl
import org.sopt.certi.data.repositoryimpl.HomeRepositoryImpl
import org.sopt.certi.data.repositoryimpl.PreCertEditRepositoryImpl
import org.sopt.certi.data.repositoryimpl.PreCertRepositoryImpl
import org.sopt.certi.data.repositoryimpl.S3RepositoryImpl
import org.sopt.certi.domain.repository.AcquisitionRepository
import org.sopt.certi.data.repositoryimpl.UserRepositoryImpl
import org.sopt.certi.domain.repository.ActivityRepository
Expand All @@ -23,6 +24,7 @@ import org.sopt.certi.domain.repository.DummyRepository
import org.sopt.certi.domain.repository.HomeRepository
import org.sopt.certi.domain.repository.PreCertEditRepository
import org.sopt.certi.domain.repository.PreCertRepository
import org.sopt.certi.domain.repository.S3Repository
import org.sopt.certi.domain.repository.UserRepository
import javax.inject.Singleton

Expand Down Expand Up @@ -67,4 +69,8 @@ abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindPreCertEditRepository(PreCertEditRepositoryImpl: PreCertEditRepositoryImpl): PreCertEditRepository

@Binds
@Singleton
abstract fun bindS3Repository(s3RepositoryImpl: S3RepositoryImpl): S3Repository
}
Loading