Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.acon.acon.core.common.utils

import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

private val yyyyMMddFormatter by lazy {
DateTimeFormatter.ofPattern("yyyyMMdd")
}

/**
* [LocalDate]를 `yyyyMMdd` 형식으로 변환
*/
fun LocalDate.toyyyyMMdd(): String {
return format(yyyyMMddFormatter)
}

/**
* yyyyMMdd을 [LocalDate]로 변환.
* 파싱 실패 시 null 반환
* ```
* "20250915".toLocalDate() // == LocalDate.of(2025, 9, 15)
* ```
*/
fun String.toLocalDate(): LocalDate? {
return try {
LocalDate.parse(this, yyyyMMddFormatter)
} catch (_: DateTimeParseException) {
null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import com.acon.core.data.dto.request.profile.UpdateProfileRequest
import com.acon.core.data.dto.response.profile.ProfileResponse
import com.acon.core.data.dto.response.profile.SavedSpotResponse
import com.acon.core.data.dto.response.profile.SavedSpotsResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.Query

interface ProfileApi {

@GET("/api/v1/members/me")
suspend fun getProfile() : ProfileResponse

@PATCH("/api/v1/members/me")
suspend fun updateProfile(updateProfileRequest: UpdateProfileRequest)
suspend fun updateProfile(@Body updateProfileRequest: UpdateProfileRequest)

@GET("/api/v1/nickname/validate")
suspend fun validateNickname(nickname: String)
suspend fun validateNickname(@Query("nickname") nickname: String)

@GET("/api/v1/saved-spots")
suspend fun getSavedSpots() : SavedSpotsResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.acon.core.data.api.remote.noauth

import com.acon.core.data.dto.request.GetPresignedUrlRequest
import com.acon.core.data.dto.response.PresignedUrlResponse
import com.acon.core.data.dto.response.app.ShouldUpdateResponse
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Query

interface AconAppNoAuthApi {
Expand All @@ -10,4 +14,9 @@ interface AconAppNoAuthApi {
@Query("version") currentVersion: String,
@Query("platform") platform: String = "android"
): ShouldUpdateResponse

@POST("/api/v1/images/presigned-url")
suspend fun getPresignedUrl(
@Body request: GetPresignedUrlRequest
): PresignedUrlResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.acon.core.data.api.remote.noauth

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

interface FileUploadApi {
@PUT
suspend fun uploadFile(
@Url url: String,
@Body body: RequestBody
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,25 @@ package com.acon.core.data.datasource.remote

import com.acon.core.data.dto.response.app.ShouldUpdateResponse
import com.acon.core.data.api.remote.noauth.AconAppNoAuthApi
import com.acon.core.data.api.remote.noauth.FileUploadApi
import com.acon.core.data.dto.request.GetPresignedUrlRequest
import com.acon.core.data.dto.response.PresignedUrlResponse
import okhttp3.RequestBody
import javax.inject.Inject

class AconAppRemoteDataSource @Inject constructor(
private val aconAppNoAuthApi: AconAppNoAuthApi
private val aconAppNoAuthApi: AconAppNoAuthApi,
private val fileUploadApi: FileUploadApi,
) {
suspend fun fetchShouldUpdateApp(currentVersion: String): ShouldUpdateResponse {
return aconAppNoAuthApi.fetchShouldUpdateApp(currentVersion)
}

suspend fun getPresignedUrl(request: GetPresignedUrlRequest): PresignedUrlResponse {
return aconAppNoAuthApi.getPresignedUrl(request)
}

suspend fun uploadFile(presignedUrl: String, body: RequestBody) {
fileUploadApi.uploadFile(presignedUrl, body)
}
}
9 changes: 9 additions & 0 deletions core/data/src/main/kotlin/com/acon/core/data/di/ApiModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.acon.core.data.api.remote.auth.SpotAuthApi
import com.acon.core.data.api.remote.noauth.SpotNoAuthApi
import com.acon.core.data.api.remote.auth.UploadAuthApi
import com.acon.core.data.api.remote.auth.UserAuthApi
import com.acon.core.data.api.remote.noauth.FileUploadApi
import com.acon.core.data.api.remote.noauth.UserNoAuthApi
import dagger.Module
import dagger.Provides
Expand Down Expand Up @@ -113,4 +114,12 @@ internal object ApiModule {
): AconAppNoAuthApi {
return retrofit.create(AconAppNoAuthApi::class.java)
}

@Singleton
@Provides
fun providesFileUploadApi(
@NoAuth retrofit: Retrofit
): FileUploadApi {
return retrofit.create(FileUploadApi::class.java)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.acon.core.data.dto.request

import com.acon.acon.core.model.type.ImageType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class GetPresignedUrlRequest(
@SerialName("imageType") val imageType: ImageType,
@SerialName("originalFileName") val fileName: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,21 @@ package com.acon.core.data.dto.request.profile
import com.acon.acon.core.model.model.profile.BirthDateStatus
import com.acon.acon.core.model.model.profile.Profile
import com.acon.acon.core.model.model.profile.ProfileImageStatus
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UpdateProfileRequest(
val nickname: String,
val birthDate: String?,
val image: String?
@SerialName("nickname") val nickname: String,
@SerialName("birthDate") val birthDate: String?,
@SerialName("profileImage") val image: String?
)

fun Profile.toUpdateProfileRequest() : UpdateProfileRequest {
val requestNickname = nickname
val requestBirthDate: String? = when(birthDate) {
is BirthDateStatus.Specified -> with((birthDate as BirthDateStatus.Specified).date) {
"$year.$month.$dayOfMonth"
"$year.${monthValue.toString().padStart(2, '0')}.$dayOfMonth"
}
BirthDateStatus.NotSpecified -> null
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.acon.core.data.dto.response

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

@Serializable
data class PresignedUrlResponse(
@SerialName("fileUrl") val fileUrl: String,
@SerialName("preSignedUrl") val presignedUrl: String
)
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
package com.acon.core.data.repository

import android.content.Context
import androidx.core.net.toUri
import com.acon.acon.core.model.model.profile.Profile
import com.acon.acon.core.model.model.profile.ProfileImageStatus
import com.acon.acon.core.model.model.profile.SavedSpot
import com.acon.acon.core.model.type.ImageType
import com.acon.acon.domain.error.profile.UpdateProfileError
import com.acon.acon.domain.error.profile.ValidateNicknameError
import com.acon.acon.domain.repository.ProfileRepository
import com.acon.core.data.api.remote.noauth.FileUploadApi
import com.acon.core.data.datasource.local.ProfileLocalDataSource
import com.acon.core.data.datasource.remote.AconAppRemoteDataSource
import com.acon.core.data.datasource.remote.ProfileRemoteDataSource
import com.acon.core.data.dto.request.GetPresignedUrlRequest
import com.acon.core.data.dto.request.profile.toUpdateProfileRequest
import com.acon.core.data.error.runCatchingWith
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody.Companion.toRequestBody
import javax.inject.Inject

class ProfileRepositoryImpl @Inject constructor(
private val profileRemoteDataSource: ProfileRemoteDataSource,
private val profileLocalDataSource: ProfileLocalDataSource
private val profileLocalDataSource: ProfileLocalDataSource,
private val aconAppRemoteDataSource: AconAppRemoteDataSource,
@ApplicationContext private val context: Context
) : ProfileRepository {

override fun getProfile(): Flow<Result<Profile>> {
Expand Down Expand Up @@ -45,9 +57,34 @@ class ProfileRepositoryImpl @Inject constructor(

override suspend fun updateProfile(newProfile: Profile): Result<Unit> {
return runCatchingWith(UpdateProfileError()) {
profileRemoteDataSource.updateProfile(newProfile.toUpdateProfileRequest())
val profileToUpdate: Profile

profileLocalDataSource.cacheProfile(newProfile)
val imageStatus = newProfile.image
if (imageStatus is ProfileImageStatus.Custom) {
if (imageStatus.url.startsWith("content://")) {
val presignedUrlResponse = aconAppRemoteDataSource.getPresignedUrl(GetPresignedUrlRequest(
imageType = ImageType.PROFILE,
fileName = imageStatus.url
))
val inputStream = context.contentResolver.openInputStream(imageStatus.url.toUri())
val requestBody = inputStream?.readBytes()?.toRequestBody("image/jpeg".toMediaTypeOrNull())
requestBody?.let {
aconAppRemoteDataSource.uploadFile(presignedUrlResponse.presignedUrl, it)
}

profileToUpdate = newProfile.copy(
image = ProfileImageStatus.Custom(presignedUrlResponse.fileUrl)
)
} else {
profileToUpdate = newProfile
}
} else {
profileToUpdate = newProfile
}

profileRemoteDataSource.updateProfile(profileToUpdate.toUpdateProfileRequest())

profileLocalDataSource.cacheProfile(profileToUpdate)

Unit
}
Expand Down
Loading