diff --git a/app/src/main/java/org/sopt/certi/core/util/StringExt.kt b/app/src/main/java/org/sopt/certi/core/util/StringExt.kt index fbf62a73..5d0eb6d8 100644 --- a/app/src/main/java/org/sopt/certi/core/util/StringExt.kt +++ b/app/src/main/java/org/sopt/certi/core/util/StringExt.kt @@ -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 @@ -21,3 +22,13 @@ fun String.toLocalDateOrNull(): LocalDate? = fun String.dateString(): String { return this.padStart(2, '0') } + +fun String.toDateData(): DateData { + val parts = this.split("-") + + return DateData( + year = parts[0].toInt(), + month = parts[1].toInt(), + day = parts[2].toInt() + ) +} diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todomain/image/PresignedDtoMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todomain/image/PresignedDtoMapper.kt new file mode 100644 index 00000000..3a469eae --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todomain/image/PresignedDtoMapper.kt @@ -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 +) diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/PersonalInfoDtoMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/PersonalInfoDtoMapper.kt new file mode 100644 index 00000000..fca45b2f --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todomain/user/PersonalInfoDtoMapper.kt @@ -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 ?: "" +) diff --git a/app/src/main/java/org/sopt/certi/data/mapper/todto/user/PersonalInfoMapper.kt b/app/src/main/java/org/sopt/certi/data/mapper/todto/user/PersonalInfoMapper.kt new file mode 100644 index 00000000..b5d2444b --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/mapper/todto/user/PersonalInfoMapper.kt @@ -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 +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasource/S3DataSource.kt b/app/src/main/java/org/sopt/certi/data/remote/datasource/S3DataSource.kt new file mode 100644 index 00000000..4b80db52 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasource/S3DataSource.kt @@ -0,0 +1,7 @@ +package org.sopt.certi.data.remote.datasource + +import android.net.Uri + +interface S3DataSource { + suspend fun uploadImageToS3(presignedUrl: String, imageUri: Uri) +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasource/UserRemoteDataSource.kt b/app/src/main/java/org/sopt/certi/data/remote/datasource/UserRemoteDataSource.kt index d9eec685..5f81c568 100644 --- a/app/src/main/java/org/sopt/certi/data/remote/datasource/UserRemoteDataSource.kt +++ b/app/src/main/java/org/sopt/certi/data/remote/datasource/UserRemoteDataSource.kt @@ -3,9 +3,12 @@ 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 @@ -13,4 +16,7 @@ interface UserRemoteDataSource { suspend fun modifyInterestedJobList(jobNameList: ModifyInterestedJobRequestDto): NullableApiResponse suspend fun getUserTrack(): ApiResponse suspend fun getMyPageInfo(): ApiResponse + suspend fun getPersonalInfo(): ApiResponse + suspend fun putPersonalInfo(request: PutPersonalInfoRequestDto): NullableApiResponse + suspend fun getPresignedUrl(): ApiResponse } diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/S3DataSourceImpl.kt b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/S3DataSourceImpl.kt new file mode 100644 index 00000000..3cf51a8f --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/S3DataSourceImpl.kt @@ -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()}") + } + } +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/UserRemoteDataSourceImpl.kt b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/UserRemoteDataSourceImpl.kt index b7709613..295d6968 100644 --- a/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/UserRemoteDataSourceImpl.kt +++ b/app/src/main/java/org/sopt/certi/data/remote/datasourceimpl/UserRemoteDataSourceImpl.kt @@ -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 @@ -28,4 +31,13 @@ class UserRemoteDataSourceImpl @Inject constructor( override suspend fun getMyPageInfo(): ApiResponse = userService.getMyPageInfo() + + override suspend fun getPersonalInfo(): ApiResponse = + userService.getPersonalInfo() + + override suspend fun putPersonalInfo(request: PutPersonalInfoRequestDto): NullableApiResponse = + userService.putPersonalInfo(request) + + override suspend fun getPresignedUrl(): ApiResponse = + userService.getPresignedUrl() } diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/request/PutPersonalInfoRequestDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/request/PutPersonalInfoRequestDto.kt new file mode 100644 index 00000000..6149ebd9 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/request/PutPersonalInfoRequestDto.kt @@ -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 +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/response/GetPersonalInfoResponseDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/response/GetPersonalInfoResponseDto.kt new file mode 100644 index 00000000..f8546165 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/response/GetPersonalInfoResponseDto.kt @@ -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? +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/dto/response/PresignedResponseDto.kt b/app/src/main/java/org/sopt/certi/data/remote/dto/response/PresignedResponseDto.kt new file mode 100644 index 00000000..42506ab5 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/dto/response/PresignedResponseDto.kt @@ -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 +) diff --git a/app/src/main/java/org/sopt/certi/data/remote/service/S3Service.kt b/app/src/main/java/org/sopt/certi/data/remote/service/S3Service.kt new file mode 100644 index 00000000..848dae60 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/remote/service/S3Service.kt @@ -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 { + @PUT + suspend fun uploadImage( + @Url presignedUrl: String, + @Body file: RequestBody + ): Response +} diff --git a/app/src/main/java/org/sopt/certi/data/remote/service/UserService.kt b/app/src/main/java/org/sopt/certi/data/remote/service/UserService.kt index d6ff09ff..0184e42c 100644 --- a/app/src/main/java/org/sopt/certi/data/remote/service/UserService.kt +++ b/app/src/main/java/org/sopt/certi/data/remote/service/UserService.kt @@ -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 { @@ -28,4 +32,13 @@ interface UserService { @GET("/api/v1/user/mypage") suspend fun getMyPageInfo(): ApiResponse + + @GET("/api/v1/user/pinfo") + suspend fun getPersonalInfo(): ApiResponse + + @PUT("/api/v1/user/pinfo") + suspend fun putPersonalInfo(@Body request: PutPersonalInfoRequestDto): NullableApiResponse + + @GET("/api/v1/user/presigned-url") + suspend fun getPresignedUrl(): ApiResponse } diff --git a/app/src/main/java/org/sopt/certi/data/repositoryimpl/S3RepositoryImpl.kt b/app/src/main/java/org/sopt/certi/data/repositoryimpl/S3RepositoryImpl.kt new file mode 100644 index 00000000..a35d120b --- /dev/null +++ b/app/src/main/java/org/sopt/certi/data/repositoryimpl/S3RepositoryImpl.kt @@ -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 { + return runCatching { + s3DataSource.uploadImageToS3(presignedUrl, imageUri.toUri()) + } + } +} diff --git a/app/src/main/java/org/sopt/certi/data/repositoryimpl/UserRepositoryImpl.kt b/app/src/main/java/org/sopt/certi/data/repositoryimpl/UserRepositoryImpl.kt index 654d47bf..2a773aab 100644 --- a/app/src/main/java/org/sopt/certi/data/repositoryimpl/UserRepositoryImpl.kt +++ b/app/src/main/java/org/sopt/certi/data/repositoryimpl/UserRepositoryImpl.kt @@ -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 @@ -48,4 +52,24 @@ class UserRepositoryImpl @Inject constructor( .getOrThrow() .toDomain() } + + override suspend fun getPersonalInfo(): Result = safeApiCall { + userRemoteDataSource.getPersonalInfo() + .handleApiResponse() + .getOrThrow() + .toDomain() + } + + override suspend fun putPersonalInfo(request: PersonalInfo): Result = safeApiCall { + userRemoteDataSource.putPersonalInfo(request.toDto()) + .handleNullableApiResponse() + .getOrThrow() + } + + override suspend fun getPresignedUrl(): Result = safeApiCall { + userRemoteDataSource.getPresignedUrl() + .handleApiResponse() + .getOrThrow() + .toDomain() + } } diff --git a/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt b/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt index 1be3fabb..7585d394 100644 --- a/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt +++ b/app/src/main/java/org/sopt/certi/di/DataSourceModule.kt @@ -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 @@ -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 @@ -68,4 +70,8 @@ abstract class DataSourceModule { @Binds @Singleton abstract fun bindsPreCertEditDataSource(preCertEditRemoteDataSourceImpl: PreCertEditRemoteDataSourceImpl): PreCertEditRemoteDataSource + + @Binds + @Singleton + abstract fun bindsS3DataSource(s3DataSourceImpl: S3DataSourceImpl): S3DataSource } diff --git a/app/src/main/java/org/sopt/certi/di/NetworkModule.kt b/app/src/main/java/org/sopt/certi/di/NetworkModule.kt index 0ddafee3..9f4a13eb 100644 --- a/app/src/main/java/org/sopt/certi/di/NetworkModule.kt +++ b/app/src/main/java/org/sopt/certi/di/NetworkModule.kt @@ -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) @@ -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() + } } diff --git a/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt b/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt index 025a7fbb..25b7b9dd 100644 --- a/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/certi/di/RepositoryModule.kt @@ -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 @@ -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 @@ -67,4 +69,8 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindPreCertEditRepository(PreCertEditRepositoryImpl: PreCertEditRepositoryImpl): PreCertEditRepository + + @Binds + @Singleton + abstract fun bindS3Repository(s3RepositoryImpl: S3RepositoryImpl): S3Repository } diff --git a/app/src/main/java/org/sopt/certi/di/ServiceModule.kt b/app/src/main/java/org/sopt/certi/di/ServiceModule.kt index fd7c3f9b..4b121c4f 100644 --- a/app/src/main/java/org/sopt/certi/di/ServiceModule.kt +++ b/app/src/main/java/org/sopt/certi/di/ServiceModule.kt @@ -14,8 +14,10 @@ import org.sopt.certi.data.remote.service.DummyService import org.sopt.certi.data.remote.service.HomeService import org.sopt.certi.data.remote.service.PreCertEditService import org.sopt.certi.data.remote.service.PreCertService +import org.sopt.certi.data.remote.service.S3Service import org.sopt.certi.data.remote.service.UserService import retrofit2.Retrofit +import javax.inject.Named @Module @InstallIn(SingletonComponent::class) @@ -69,4 +71,10 @@ object ServiceModule { @Singleton fun preCertEditService(retrofit: Retrofit): PreCertEditService = retrofit.create(PreCertEditService::class.java) + + @Provides + @Singleton + fun provideS3Service(@Named("S3Retrofit") retrofit: Retrofit): S3Service { + return retrofit.create(S3Service::class.java) + } } diff --git a/app/src/main/java/org/sopt/certi/domain/model/DateData.kt b/app/src/main/java/org/sopt/certi/domain/model/DateData.kt index 8041921e..4fa2545c 100644 --- a/app/src/main/java/org/sopt/certi/domain/model/DateData.kt +++ b/app/src/main/java/org/sopt/certi/domain/model/DateData.kt @@ -5,12 +5,7 @@ data class DateData( val month: Int? = null, val day: Int? = null ) { - val yearText: String get() = year?.toString() ?: "" - val monthText: String get() = month?.let { "%02d".format(it) } ?: "" - val dayText: String get() = day?.let { "%02d".format(it) } ?: "" - val isAllEmpty: Boolean get() = year == null && month == null && day == null val isComplete: Boolean get() = year != null && month != null && day != null - val isValid: Boolean get() = isAllEmpty || isComplete } diff --git a/app/src/main/java/org/sopt/certi/domain/model/image/PresignedData.kt b/app/src/main/java/org/sopt/certi/domain/model/image/PresignedData.kt new file mode 100644 index 00000000..0c139e10 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/model/image/PresignedData.kt @@ -0,0 +1,6 @@ +package org.sopt.certi.domain.model.image + +data class PresignedData( + val presignedUrl: String, + val publicUrl: String +) diff --git a/app/src/main/java/org/sopt/certi/domain/model/user/UserProfile.kt b/app/src/main/java/org/sopt/certi/domain/model/user/PersonalInfo.kt similarity index 76% rename from app/src/main/java/org/sopt/certi/domain/model/user/UserProfile.kt rename to app/src/main/java/org/sopt/certi/domain/model/user/PersonalInfo.kt index 4e26d750..b0dd49b3 100644 --- a/app/src/main/java/org/sopt/certi/domain/model/user/UserProfile.kt +++ b/app/src/main/java/org/sopt/certi/domain/model/user/PersonalInfo.kt @@ -2,10 +2,10 @@ package org.sopt.certi.domain.model.user import org.sopt.certi.domain.model.DateData -data class UserProfile( +data class PersonalInfo( val name: String, val nickname: String, val email: String, val birth: DateData, - val profileImageUrl: String? + val profileImageUrl: String ) diff --git a/app/src/main/java/org/sopt/certi/domain/repository/S3Repository.kt b/app/src/main/java/org/sopt/certi/domain/repository/S3Repository.kt new file mode 100644 index 00000000..9625ea1b --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/repository/S3Repository.kt @@ -0,0 +1,5 @@ +package org.sopt.certi.domain.repository + +interface S3Repository { + suspend fun uploadImage(presignedUrl: String, imageUri: String): Result +} diff --git a/app/src/main/java/org/sopt/certi/domain/repository/UserRepository.kt b/app/src/main/java/org/sopt/certi/domain/repository/UserRepository.kt index 8dc0552e..e2a90c45 100644 --- a/app/src/main/java/org/sopt/certi/domain/repository/UserRepository.kt +++ b/app/src/main/java/org/sopt/certi/domain/repository/UserRepository.kt @@ -1,7 +1,9 @@ package org.sopt.certi.domain.repository +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 interface UserRepository { suspend fun checkNicknameValidation(keyword: String): Result @@ -9,4 +11,7 @@ interface UserRepository { suspend fun modifyInterestedJobList(jobNameList: List): Result suspend fun getUserTrack(): Result suspend fun getMyPageInfo(): Result + suspend fun getPersonalInfo(): Result + suspend fun putPersonalInfo(request: PersonalInfo): Result + suspend fun getPresignedUrl(): Result } diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/image/GetPresignedUrlUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/image/GetPresignedUrlUseCase.kt new file mode 100644 index 00000000..0607d039 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/image/GetPresignedUrlUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.certi.domain.usecase.image + +import org.sopt.certi.domain.model.image.PresignedData +import org.sopt.certi.domain.repository.UserRepository +import javax.inject.Inject + +class GetPresignedUrlUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(): Result = userRepository.getPresignedUrl() +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/image/UploadImageToS3UseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/image/UploadImageToS3UseCase.kt new file mode 100644 index 00000000..912fb13b --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/image/UploadImageToS3UseCase.kt @@ -0,0 +1,12 @@ +package org.sopt.certi.domain.usecase.image + +import org.sopt.certi.domain.repository.S3Repository +import javax.inject.Inject + +class UploadImageToS3UseCase @Inject constructor( + private val s3Repository: S3Repository +) { + suspend operator fun invoke(presignedUrl: String, imageUri: String): Result { + return s3Repository.uploadImage(presignedUrl, imageUri) + } +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/user/GetPersonalInfoUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/user/GetPersonalInfoUseCase.kt new file mode 100644 index 00000000..af7fc61e --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/user/GetPersonalInfoUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.certi.domain.usecase.user + +import org.sopt.certi.domain.model.user.PersonalInfo +import org.sopt.certi.domain.repository.UserRepository +import javax.inject.Inject + +class GetPersonalInfoUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(): Result = userRepository.getPersonalInfo() +} diff --git a/app/src/main/java/org/sopt/certi/domain/usecase/user/PutPersonalInfoUseCase.kt b/app/src/main/java/org/sopt/certi/domain/usecase/user/PutPersonalInfoUseCase.kt new file mode 100644 index 00000000..0d865246 --- /dev/null +++ b/app/src/main/java/org/sopt/certi/domain/usecase/user/PutPersonalInfoUseCase.kt @@ -0,0 +1,11 @@ +package org.sopt.certi.domain.usecase.user + +import org.sopt.certi.domain.model.user.PersonalInfo +import org.sopt.certi.domain.repository.UserRepository +import javax.inject.Inject + +class PutPersonalInfoUseCase @Inject constructor( + private val userRepository: UserRepository +) { + suspend operator fun invoke(request: PersonalInfo): Result = userRepository.putPersonalInfo(request) +} diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoScreen.kt b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoScreen.kt index 34e925ef..74b0fa42 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoScreen.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoScreen.kt @@ -6,15 +6,18 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.union +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager @@ -57,7 +60,10 @@ fun EditPersonalInfoRoute( onEmailChange = viewModel::onEmailChange, onBirthChange = viewModel::onBirthChange, onSaveClick = viewModel::onSaveClick, - modifier = Modifier.padding(padding) + modifier = Modifier.padding( + top = padding.calculateTopPadding(), + bottom = 0.dp + ) ) } @@ -80,9 +86,11 @@ fun EditPersonalInfoScreen( Column( modifier = modifier .fillMaxSize() + .windowInsetsPadding( + WindowInsets.navigationBars.union(WindowInsets.ime) + ) .verticalScroll(rememberScrollState()) .noRippleClickable { focusManager.clearFocus() } - .imePadding() .padding(horizontal = screenWidthDp(20.dp)), verticalArrangement = Arrangement.spacedBy(screenHeightDp(24.dp)), horizontalAlignment = Alignment.CenterHorizontally @@ -139,21 +147,17 @@ fun EditPersonalInfoScreen( @Preview(showBackground = true) @Composable private fun EditPersonalInfoPreview() { - val viewModel = remember { EditPersonalInfoViewModel() } - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val nickNameValidType by viewModel.nickNameValidTypeUiState.collectAsStateWithLifecycle() - CERTITheme { EditPersonalInfoScreen( - uiState = uiState, - nickNameValidType = nickNameValidType, - onSaveClick = viewModel::onSaveClick, - onProfileUriChange = viewModel::onProfileUriChange, - onNickNameChange = viewModel::onNickNameChange, - onNickNameCheckButtonClick = viewModel::onNickNameCheckButtonClick, - onNameChange = viewModel::onNameChange, - onEmailChange = viewModel::onEmailChange, - onBirthChange = viewModel::onBirthChange, + uiState = EditPersonalInfoUiState(), + nickNameValidType = NickNameValidType.VALID, + onSaveClick = {}, + onProfileUriChange = {}, + onNickNameChange = {}, + onNickNameCheckButtonClick = {}, + onNameChange = {}, + onEmailChange = {}, + onBirthChange = {}, modifier = Modifier .fillMaxSize() .background(CertiTheme.colors.white) diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoViewModel.kt b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoViewModel.kt index 4589ec5d..5ea54b23 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoViewModel.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/EditPersonalInfoViewModel.kt @@ -10,92 +10,133 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.sopt.certi.domain.model.DateData -import org.sopt.certi.domain.model.user.UserProfile +import org.sopt.certi.domain.model.user.PersonalInfo +import org.sopt.certi.domain.usecase.image.GetPresignedUrlUseCase +import org.sopt.certi.domain.usecase.image.UploadImageToS3UseCase +import org.sopt.certi.domain.usecase.user.CheckNicknameValidationUseCase +import org.sopt.certi.domain.usecase.user.GetPersonalInfoUseCase +import org.sopt.certi.domain.usecase.user.PutPersonalInfoUseCase import org.sopt.certi.presentation.type.NickNameValidType import org.sopt.certi.presentation.ui.editpersonalinfo.state.EditPersonalInfoUiState +import timber.log.Timber import javax.inject.Inject @HiltViewModel -class EditPersonalInfoViewModel @Inject constructor() : ViewModel() { +class EditPersonalInfoViewModel @Inject constructor( + private val getPersonalInfoUseCase: GetPersonalInfoUseCase, + private val checkNicknameValidationUseCase: CheckNicknameValidationUseCase, + private val getPresignedUrlUseCase: GetPresignedUrlUseCase, + private val uploadImageToS3UseCase: UploadImageToS3UseCase, + private val putPersonalInfoUseCase: PutPersonalInfoUseCase +) : ViewModel() { private val _uiState = MutableStateFlow(EditPersonalInfoUiState()) val uiState = _uiState.asStateFlow() private val _nickNameValidTypeUiState = MutableStateFlow(NickNameValidType.DEFAULT) val nickNameValidTypeUiState = _nickNameValidTypeUiState.asStateFlow() - private var _originalUserProfile: UserProfile? = null + private var _originalPersonalInfo: PersonalInfo? = null init { loadPersonalInfoData() } - private fun loadPersonalInfoData() { - viewModelScope.launch { - val profileData = UserProfile( - nickname = "nick", - name = "name", - email = "certification@gmail.com", - birth = DateData(2004, 5, 31), - profileImageUrl = null - ) - - _originalUserProfile = profileData - _uiState.update { - EditPersonalInfoUiState( - nickname = profileData.nickname, - name = profileData.name, - email = profileData.email, - birth = profileData.birth, - profileUri = profileData.profileImageUrl?.toUri() - ) + private fun loadPersonalInfoData() = viewModelScope.launch { + getPersonalInfoUseCase() + .onSuccess { result -> + _originalPersonalInfo = result + _uiState.update { + it.copy( + name = result.name, + nickname = result.nickname, + email = result.email, + birth = result.birth, + profileUri = if (result.profileImageUrl.isNotEmpty()) result.profileImageUrl.toUri() else null + ) + } + } + .onFailure { error -> + Timber.e(error, "LoadPersonalInfoData Failed") } - } } private fun updateSaveButtonState() { - val original = _originalUserProfile ?: return - + val original = _originalPersonalInfo ?: return val current = _uiState.value val isNameChanged = current.name != original.name val isEmailChanged = current.email != original.email val isProfileChanged = current.profileUri?.toString() != original.profileImageUrl - val isContentChanged = ( - current.isNicknameChanged || - isNameChanged || - isEmailChanged || - current.isBirthChanged || - isProfileChanged - ) && current.birth.isValid + val isContentChanged = current.isNicknameChanged || isNameChanged || isEmailChanged || current.isBirthChanged || isProfileChanged val isNicknameValid = _nickNameValidTypeUiState.value in setOf(NickNameValidType.VALID, NickNameValidType.DEFAULT) _uiState.update { - it.copy(isSaveButtonEnabled = isContentChanged && isNicknameValid) + it.copy(isSaveButtonEnabled = isContentChanged && isNicknameValid && current.birth.isValid) } } - fun onSaveClick() { - viewModelScope.launch { - _nickNameValidTypeUiState.value = NickNameValidType.DEFAULT - val savedState = _uiState.value - _uiState.update { - it.copy( - isNicknameChanged = false, - isBirthChanged = false, - isSaveButtonEnabled = false - ) - } - _originalUserProfile = UserProfile( - name = savedState.name, - nickname = savedState.nickname, - email = savedState.email, - birth = savedState.birth, - profileImageUrl = savedState.profileUri?.toString() // 서버 url로 나중에 수정 + fun onSaveClick() = viewModelScope.launch { + try { + val currentState = _uiState.value + val original = _originalPersonalInfo ?: return@launch + + val finalProfileImageUrl = getFinalProfileImageUrl(currentState, original) + + val request = PersonalInfo( + name = currentState.name, + nickname = currentState.nickname, + email = currentState.email, + birth = currentState.birth, + profileImageUrl = finalProfileImageUrl ) + + putPersonalInfoUseCase(request) + .onSuccess { + _nickNameValidTypeUiState.value = NickNameValidType.DEFAULT + _uiState.update { + it.copy( + isNicknameChanged = false, + isBirthChanged = false, + isSaveButtonEnabled = false + ) + } + _originalPersonalInfo = request + } + .onFailure { + Timber.d("정보 수정 실패") + } + } catch (e: Exception) { + Timber.e(e, "정보 수정 실패") } } + private suspend fun getFinalProfileImageUrl( + currentState: EditPersonalInfoUiState, + original: PersonalInfo + ): String { + val currentUriString = currentState.profileUri?.toString() ?: "" + val originalUrlString = original.profileImageUrl + + val isProfileChanged = currentUriString != originalUrlString + + return if (isProfileChanged) { + if (currentState.profileUri != null) { + uploadImageAndGetPublicUrl(currentState.profileUri) + } else { + "" + } + } else { + originalUrlString + } + } + + private suspend fun uploadImageAndGetPublicUrl(uri: Uri): String { + val presignedInfo = getPresignedUrlUseCase().getOrThrow() + uploadImageToS3UseCase(presignedInfo.presignedUrl, uri.toString()).getOrThrow() + return presignedInfo.publicUrl + } + fun onProfileUriChange(uri: Uri?) { _uiState.update { it.copy(profileUri = uri) } updateSaveButtonState() @@ -105,31 +146,38 @@ class EditPersonalInfoViewModel @Inject constructor() : ViewModel() { _uiState.update { it.copy( nickname = nickname, - isNicknameChanged = _originalUserProfile?.nickname != null && nickname != _originalUserProfile?.nickname + isNicknameChanged = _originalPersonalInfo?.nickname != null && nickname != _originalPersonalInfo?.nickname ) } - val original = _originalUserProfile ?: return + val original = _originalPersonalInfo ?: return _nickNameValidTypeUiState.update { when { nickname.isEmpty() -> NickNameValidType.EMPTY - nickname.contains("시발") -> NickNameValidType.INVALID nickname == original.nickname -> NickNameValidType.DEFAULT else -> NickNameValidType.UNCHECKED } } + updateSaveButtonState() } - fun onNickNameCheckButtonClick() { - _nickNameValidTypeUiState.update { - when { - (_nickNameValidTypeUiState.value == NickNameValidType.UNCHECKED) -> NickNameValidType.VALID - else -> NickNameValidType.DUPLICATE + fun onNickNameCheckButtonClick() = viewModelScope.launch { + checkNicknameValidationUseCase(_uiState.value.nickname) + .onSuccess { + _nickNameValidTypeUiState.value = NickNameValidType.VALID + updateSaveButtonState() + } + .onFailure { throwable -> + val msg = throwable.message + + _nickNameValidTypeUiState.value = when { + msg!!.contains("이미 존재하는 닉네임입니다.") -> NickNameValidType.DUPLICATE + msg.contains("닉네임에 비속어를 포함할 수 없습니다.") -> NickNameValidType.INVALID + else -> NickNameValidType.DEFAULT + } } - } - updateSaveButtonState() } fun onNameChange(name: String) { @@ -146,7 +194,7 @@ class EditPersonalInfoViewModel @Inject constructor() : ViewModel() { _uiState.update { it.copy( birth = birth, - isBirthChanged = _originalUserProfile?.birth != null && birth != _originalUserProfile?.birth + isBirthChanged = _originalPersonalInfo?.birth != null && birth != _originalPersonalInfo?.birth ) } updateSaveButtonState() diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/DateInputField.kt b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/DateInputField.kt index 8e69e9a4..0580825a 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/DateInputField.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/DateInputField.kt @@ -119,7 +119,7 @@ fun DateInputField( DateDropdown( placeholder = "YYYY", items = yearList, - value = value.yearText, + value = value.year?.toString() ?: "", onValueChange = { updateDate(newYear = it.toIntOrNull()) }, backgroundColor = inputFieldBackgroundColor, initialScrollItem = currentDate.year.toString(), @@ -128,7 +128,7 @@ fun DateInputField( DateDropdown( placeholder = "MM", items = monthList, - value = value.monthText, + value = value.month?.let { "%02d".format(it) } ?: "", onValueChange = { updateDate(newMonth = it.toIntOrNull()) }, backgroundColor = inputFieldBackgroundColor, modifier = Modifier.widthIn(min = screenWidthDp(76.dp), max = screenWidthDp(94.dp)) @@ -136,7 +136,7 @@ fun DateInputField( DateDropdown( placeholder = "DD", items = dayList, - value = value.dayText, + value = value.day?.let { "%02d".format(it) } ?: "", onValueChange = { updateDate(newDay = it.toIntOrNull()) }, backgroundColor = inputFieldBackgroundColor, modifier = Modifier.widthIn(min = screenWidthDp(76.dp), max = screenWidthDp(94.dp)) diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/EditPersonalInfoTextField.kt b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/EditPersonalInfoTextField.kt index e1e4ffde..810826ad 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/EditPersonalInfoTextField.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/EditPersonalInfoTextField.kt @@ -19,9 +19,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.certi.core.util.screenWidthDp @@ -40,6 +43,21 @@ fun EditPersonalInfoTextField( imeAction: ImeAction = ImeAction.Next ) { val focusManager = LocalFocusManager.current + var textFieldValueState by remember { + mutableStateOf( + TextFieldValue( + text = value, + selection = TextRange(value.length) + ) + ) + } + + if (value != textFieldValueState.text) { + textFieldValueState = textFieldValueState.copy( + text = value, + selection = TextRange(value.length) + ) + } Column( modifier = modifier, @@ -51,12 +69,16 @@ fun EditPersonalInfoTextField( color = CertiTheme.colors.gray600 ) BasicTextField( - value = value, + value = textFieldValueState, onValueChange = { newValue -> - if (newValue.contains("\n") || newValue.contains("\r")) { - return@BasicTextField + val filteredText = newValue.text.replace("\n", "").replace("\r", "") + if (filteredText != newValue.text) { + textFieldValueState = newValue.copy(text = filteredText) + onValueChange(filteredText) + } else { + textFieldValueState = newValue + onValueChange(newValue.text) } - onValueChange(newValue) }, modifier = Modifier .fillMaxWidth() @@ -67,7 +89,15 @@ fun EditPersonalInfoTextField( shape = RoundedCornerShape(8.dp) ) .background(CertiTheme.colors.gray0) - .padding(screenWidthDp(12.dp)), + .padding(screenWidthDp(12.dp)) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + val text = textFieldValueState.text + textFieldValueState = textFieldValueState.copy( + selection = TextRange(text.length) + ) + } + }, textStyle = CertiTheme.typography.caption.regular_14.copy( color = CertiTheme.colors.black ), diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/PersonalInfoProfileImage.kt b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/PersonalInfoProfileImage.kt index 703fecf0..f4f9f719 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/PersonalInfoProfileImage.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/editpersonalinfo/component/PersonalInfoProfileImage.kt @@ -22,7 +22,6 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage import org.sopt.certi.R import org.sopt.certi.core.util.noRippleClickable import org.sopt.certi.core.util.screenWidthDp @@ -46,23 +45,12 @@ fun PersonalInfoProfileImage( Box( modifier = modifier ) { - if (selectedImageUri == null) { - MyPageProfileImage( - imageUrl = "", - modifier = Modifier - .padding(screenWidthDp(2.dp)) - .size(screenWidthDp(100.dp)) - ) - } else { - AsyncImage( - model = selectedImageUri, - contentDescription = null, - modifier = Modifier - .padding(screenWidthDp(2.dp)) - .size(screenWidthDp(100.dp)) - .clip(CircleShape) - ) - } + MyPageProfileImage( + imageUri = selectedImageUri, + modifier = Modifier + .padding(screenWidthDp(2.dp)) + .size(screenWidthDp(100.dp)) + ) Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_pencil_24), diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainScreen.kt b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainScreen.kt index 242fce65..e7f9a529 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainScreen.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainScreen.kt @@ -12,7 +12,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.sopt.certi.R import org.sopt.certi.core.state.UiState @@ -37,6 +40,10 @@ fun MyPageMainRoute( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + viewModel.loadMyPageData() + } + when (val state = uiState.myPageInfoLoadState) { is UiState.Success -> { MyPageMainScreen( @@ -70,7 +77,7 @@ fun MyPageMainScreen( name = uiState.nickname, email = uiState.email, jobList = uiState.jobs, - profileImageUrl = uiState.profileImageUrl + profileImageUri = if (uiState.profileImageUrl.isNotBlank())uiState.profileImageUrl.toUri() else null ) LazyColumn( modifier = Modifier diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainViewModel.kt b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainViewModel.kt index ace92e3d..bd97de83 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainViewModel.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/MyPageMainViewModel.kt @@ -19,11 +19,7 @@ class MyPageMainViewModel@Inject constructor( private val _uiState = MutableStateFlow(MyPageUiSate()) val uiState = _uiState.asStateFlow() - init { - loadMyPageData() - } - - private fun loadMyPageData() = viewModelScope.launch { + fun loadMyPageData() = viewModelScope.launch { myPageUseCase() .onSuccess { result -> _uiState.update { it.copy(myPageInfoLoadState = UiState.Success(result)) } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfile.kt b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfile.kt index 66cf7ea2..1af45230 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfile.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfile.kt @@ -1,5 +1,6 @@ package org.sopt.certi.presentation.ui.mypage.component +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth @@ -22,7 +23,7 @@ fun MyPageProfile( name: String, email: String, jobList: List, - profileImageUrl: String, + profileImageUri: Uri?, modifier: Modifier = Modifier ) { Column( @@ -33,7 +34,7 @@ fun MyPageProfile( horizontalAlignment = Alignment.CenterHorizontally ) { MyPageProfileImage( - imageUrl = profileImageUrl, + imageUri = profileImageUri, modifier = Modifier.size(screenWidthDp(80.dp)) ) Text( @@ -65,7 +66,7 @@ private fun MyPageProfilePreview() { name = "김서티", email = "certification@gmail.com", jobList = listOf("경영/사무", "무역/유통", "마케팅/광고/홍보"), - profileImageUrl = "" + profileImageUri = null ) } } diff --git a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfileImage.kt b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfileImage.kt index 4382e26a..e0ca606e 100644 --- a/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfileImage.kt +++ b/app/src/main/java/org/sopt/certi/presentation/ui/mypage/component/MyPageProfileImage.kt @@ -1,5 +1,6 @@ package org.sopt.certi.presentation.ui.mypage.component +import android.net.Uri import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.shape.CircleShape @@ -19,7 +20,7 @@ import org.sopt.certi.ui.theme.CertiTheme @Composable fun MyPageProfileImage( - imageUrl: String, + imageUri: Uri?, modifier: Modifier = Modifier ) { Box( @@ -36,7 +37,7 @@ fun MyPageProfileImage( AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(imageUrl) + .data(imageUri) .crossfade(true) .build(), contentDescription = null,