From 824a8d58e334d3a316fee2ae0180d82d7976f842 Mon Sep 17 00:00:00 2001 From: Seungmin <39687846+peter-j0y@users.noreply.github.com> Date: Thu, 15 Aug 2024 22:57:29 +0900 Subject: [PATCH] =?UTF-8?q?=ED=94=84=EB=A1=9C=ED=95=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EA=B8=B0=EB=B3=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=84=B8=ED=8C=85=20=EB=B0=8F=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/res/xml/path_provider.xml | 2 +- .../account/AccountDataSourceImpl.kt | 27 +- .../data/datasource/account/AccountService.kt | 14 +- .../data/model/account/ChangeMyInfoRequest.kt | 6 - .../data/model/account/UserInfoResponse.kt | 14 + .../data/repository/AccountRepositoryImpl.kt | 9 +- .../domain/datasource/AccountDataSource.kt | 3 + .../domain/model/account/UserInfo.kt | 7 + .../domain/repository/AccountRepository.kt | 5 +- .../GetChallengePreviewsByTypeUseCase.kt | 2 +- .../usecase/GetFollowingsPostsUseCase.kt | 2 +- .../domain/usecase/GetMyUidUseCase.kt | 2 +- .../domain/usecase/LikePostUseCase.kt | 2 +- .../domain/usecase/UploadPostUseCase.kt | 2 +- .../domain/usecase/community/FollowUseCase.kt | 2 +- .../community/RemoveFollowerUseCase.kt | 2 +- .../usecase/community/SendCommentUseCase.kt | 2 +- .../usecase/community/UnFollowUseCase.kt | 2 +- .../running/GetRunningFollowerUseCase.kt | 2 +- .../usecase/running/RunningFinishUseCase.kt | 2 +- .../usecase/running/RunningStartUseCase.kt | 2 +- .../domain/usecase/running/SendLikeUseCase.kt | 2 +- gradle/libs.versions.toml | 2 +- .../screens/mypage/MyPageScreen.kt | 6 +- .../mypage/editprofile/EditProfileScreen.kt | 361 +++++++++--------- .../editprofile/EditProfileViewModel.kt | 44 ++- .../mypage/editprofile/UserInfoUiState.kt | 7 + .../screens/setting/SettingScreen.kt | 2 + .../whyranoid/presentation/util/extensions.kt | 9 +- .../presentation/viewmodel/SplashViewModel.kt | 4 +- .../main/res/drawable/ic_default_profile.xml | 25 ++ 31 files changed, 347 insertions(+), 226 deletions(-) delete mode 100644 data/src/main/java/com/whyranoid/data/model/account/ChangeMyInfoRequest.kt create mode 100644 data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt create mode 100644 domain/src/main/java/com/whyranoid/domain/model/account/UserInfo.kt create mode 100644 presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/UserInfoUiState.kt create mode 100644 presentation/src/main/res/drawable/ic_default_profile.xml diff --git a/app/src/main/res/xml/path_provider.xml b/app/src/main/res/xml/path_provider.xml index 5b5e8874..e637cf57 100644 --- a/app/src/main/res/xml/path_provider.xml +++ b/app/src/main/res/xml/path_provider.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt b/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt index 45f0210e..97bc1f16 100644 --- a/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt +++ b/data/src/main/java/com/whyranoid/data/datasource/account/AccountDataSourceImpl.kt @@ -1,11 +1,16 @@ package com.whyranoid.data.datasource.account import com.whyranoid.data.getResult -import com.whyranoid.data.model.account.ChangeMyInfoRequest import com.whyranoid.data.model.account.SignUpRequest import com.whyranoid.data.model.account.toLoginData +import com.whyranoid.data.model.account.toUserInfo import com.whyranoid.domain.datasource.AccountDataSource import com.whyranoid.domain.model.account.LoginData +import com.whyranoid.domain.model.account.UserInfo +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody +import java.io.File class AccountDataSourceImpl(private val accountService: AccountService) : AccountDataSource { override suspend fun signUp( @@ -54,12 +59,18 @@ class AccountDataSourceImpl(private val accountService: AccountService) : Accoun override suspend fun changeMyInfo(walkieId: Long, nickName: String, profileUrl: String?): Result { return kotlin.runCatching { + var imagePart: MultipartBody.Part? = null + + if (profileUrl != null) { + val file = File(profileUrl) + val fileBody = RequestBody.create(MediaType.parse("image/*"), file) + imagePart = MultipartBody.Part.createFormData("profileImg", file.name, fileBody) + } + val response = accountService.changeMyInfo( walkieId, - ChangeMyInfoRequest( - profileImg = profileUrl ?: "", - nickname = nickName - ) + imagePart, + nickName ) if (response.isSuccessful) { return Result.success(true) @@ -68,4 +79,10 @@ class AccountDataSourceImpl(private val accountService: AccountService) : Accoun } } } + + override suspend fun getUserInfo(walkieId: Long): Result { + return kotlin.runCatching { + accountService.getMyInfo(walkieId).getResult { it.toUserInfo() } + } + } } diff --git a/data/src/main/java/com/whyranoid/data/datasource/account/AccountService.kt b/data/src/main/java/com/whyranoid/data/datasource/account/AccountService.kt index fc7bc7e8..d9af3b92 100644 --- a/data/src/main/java/com/whyranoid/data/datasource/account/AccountService.kt +++ b/data/src/main/java/com/whyranoid/data/datasource/account/AccountService.kt @@ -1,16 +1,18 @@ package com.whyranoid.data.datasource.account import com.whyranoid.data.API -import com.whyranoid.data.model.account.ChangeMyInfoRequest import com.whyranoid.data.model.account.ChangeMyInfoResponse import com.whyranoid.data.model.account.LoginDataResponse import com.whyranoid.data.model.account.NickCheckResponse import com.whyranoid.data.model.account.SignUpRequest import com.whyranoid.data.model.account.SignUpResponse +import com.whyranoid.data.model.account.UserInfoResponse +import okhttp3.MultipartBody import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Query interface AccountService { @@ -31,7 +33,13 @@ interface AccountService { @POST(API.WalkingControl.MY) suspend fun changeMyInfo( - @Query("walkieId") id: Long, - @Body myInfoRequest: ChangeMyInfoRequest + @Part("walkieId") id: Long, + @Part profileImg: MultipartBody.Part?, + @Part("nickname") nickName: String, ): Response + + @GET(API.WalkingControl.MY) + suspend fun getMyInfo( + @Query("walkieId") id: Long + ): Response } diff --git a/data/src/main/java/com/whyranoid/data/model/account/ChangeMyInfoRequest.kt b/data/src/main/java/com/whyranoid/data/model/account/ChangeMyInfoRequest.kt deleted file mode 100644 index 76f3beda..00000000 --- a/data/src/main/java/com/whyranoid/data/model/account/ChangeMyInfoRequest.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.whyranoid.data.model.account - -data class ChangeMyInfoRequest ( - val profileImg: String?, - val nickname: String, -) \ No newline at end of file diff --git a/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt b/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt new file mode 100644 index 00000000..ae354f3e --- /dev/null +++ b/data/src/main/java/com/whyranoid/data/model/account/UserInfoResponse.kt @@ -0,0 +1,14 @@ +package com.whyranoid.data.model.account + +import com.whyranoid.domain.model.account.UserInfo + +data class UserInfoResponse ( + val nickname: String, + val profileImg: String? +) + +fun UserInfoResponse.toUserInfo() = UserInfo( + name = "", // TODO 수정 + nickname = nickname, + profileImg = profileImg +) \ No newline at end of file diff --git a/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt b/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt index ee1fd41d..e3a466c8 100644 --- a/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt +++ b/data/src/main/java/com/whyranoid/data/repository/AccountRepositoryImpl.kt @@ -4,6 +4,7 @@ import android.util.Log import com.whyranoid.data.AccountDataStore import com.whyranoid.domain.datasource.AccountDataSource import com.whyranoid.domain.model.account.Sex +import com.whyranoid.domain.model.account.UserInfo import com.whyranoid.domain.repository.AccountRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -14,13 +15,13 @@ class AccountRepositoryImpl( ) : AccountRepository { override val authId: Flow = accountDataStore.authId - override val uId: Flow = accountDataStore.uId + override val walkieId: Flow = accountDataStore.uId override val userName: Flow = accountDataStore.userName override val nickName: Flow = accountDataStore.nickName override val profileUrl: Flow = accountDataStore.profileUrl override suspend fun getUID(): Long { - return requireNotNull(uId.first()) + return requireNotNull(walkieId.first()) } override suspend fun signUp( @@ -102,4 +103,8 @@ class AccountRepositoryImpl( return Result.failure(Exception("마이페이지 정보 수정 실패")) } } + + override suspend fun getUserInfo(walkieId: Long): Result { + return accountDataSource.getUserInfo(walkieId) + } } diff --git a/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt b/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt index f2671a75..22ad5805 100644 --- a/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt +++ b/domain/src/main/java/com/whyranoid/domain/datasource/AccountDataSource.kt @@ -1,6 +1,7 @@ package com.whyranoid.domain.datasource import com.whyranoid.domain.model.account.LoginData +import com.whyranoid.domain.model.account.UserInfo interface AccountDataSource { suspend fun signUp( @@ -16,4 +17,6 @@ interface AccountDataSource { suspend fun signIn(authorId: String): Result suspend fun changeMyInfo(walkieId: Long, nickName: String, profileUrl: String?): Result + + suspend fun getUserInfo(walkieId: Long): Result } diff --git a/domain/src/main/java/com/whyranoid/domain/model/account/UserInfo.kt b/domain/src/main/java/com/whyranoid/domain/model/account/UserInfo.kt new file mode 100644 index 00000000..7ff45087 --- /dev/null +++ b/domain/src/main/java/com/whyranoid/domain/model/account/UserInfo.kt @@ -0,0 +1,7 @@ +package com.whyranoid.domain.model.account + +data class UserInfo ( + val name: String, + val profileImg: String?, + val nickname: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/whyranoid/domain/repository/AccountRepository.kt b/domain/src/main/java/com/whyranoid/domain/repository/AccountRepository.kt index eb4dc6b7..e89907e2 100644 --- a/domain/src/main/java/com/whyranoid/domain/repository/AccountRepository.kt +++ b/domain/src/main/java/com/whyranoid/domain/repository/AccountRepository.kt @@ -1,12 +1,13 @@ package com.whyranoid.domain.repository import com.whyranoid.domain.model.account.Sex +import com.whyranoid.domain.model.account.UserInfo import kotlinx.coroutines.flow.Flow interface AccountRepository { val authId: Flow - val uId: Flow + val walkieId: Flow val userName: Flow val nickName: Flow val profileUrl: Flow @@ -33,4 +34,6 @@ interface AccountRepository { suspend fun checkNickName(nickName: String): Result> suspend fun changeMyInfo(walkieId: Long, nickName: String, profileUrl: String?): Result + + suspend fun getUserInfo(walkieId: Long): Result } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/GetChallengePreviewsByTypeUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/GetChallengePreviewsByTypeUseCase.kt index a610fe9e..2c4604a2 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/GetChallengePreviewsByTypeUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/GetChallengePreviewsByTypeUseCase.kt @@ -12,7 +12,7 @@ class GetChallengePreviewsByTypeUseCase( ) { suspend operator fun invoke(type: ChallengeType): List { return challengeRepository.getChallengePreviewsByType( - accountRepository.uId.first()?.toInt() ?: -1, type + accountRepository.walkieId.first()?.toInt() ?: -1, type ) } } \ No newline at end of file diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/GetFollowingsPostsUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/GetFollowingsPostsUseCase.kt index b93e9783..4132f493 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/GetFollowingsPostsUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/GetFollowingsPostsUseCase.kt @@ -10,7 +10,7 @@ class GetFollowingsPostsUseCase( private val postRepository: PostRepository, ) { suspend operator fun invoke(): Result> { - val myUid = requireNotNull(accountRepository.uId.first()) + val myUid = requireNotNull(accountRepository.walkieId.first()) return postRepository.getMyFollowingsPost(myUid) } } \ No newline at end of file diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/GetMyUidUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/GetMyUidUseCase.kt index d666d6f3..6ebf8cf2 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/GetMyUidUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/GetMyUidUseCase.kt @@ -7,6 +7,6 @@ class GetMyUidUseCase( private val accountRepository: AccountRepository, ) { suspend operator fun invoke(): Result { - return runCatching { requireNotNull(accountRepository.uId.firstOrNull()) } + return runCatching { requireNotNull(accountRepository.walkieId.firstOrNull()) } } } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/LikePostUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/LikePostUseCase.kt index c853ee3a..47e01cd7 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/LikePostUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/LikePostUseCase.kt @@ -10,7 +10,7 @@ class LikePostUseCase( ) { suspend operator fun invoke(postId: Long): Result { - val uid = requireNotNull(accountRepository.uId.first()) + val uid = requireNotNull(accountRepository.walkieId.first()) return communityRepository.likePost(postId, uid) } } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/UploadPostUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/UploadPostUseCase.kt index 93f37791..6f728602 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/UploadPostUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/UploadPostUseCase.kt @@ -15,7 +15,7 @@ class UploadPostUseCase @Inject constructor( history: String, imagePath: String, ): Result { - return accountRepository.uId.first()?.let { uid -> + return accountRepository.walkieId.first()?.let { uid -> postRepository.uploadPost(uid, content, colorMode, history, imagePath) } ?: kotlin.run { Result.failure(Exception("Account Error")) diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/community/FollowUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/community/FollowUseCase.kt index 90c82003..9b5a744d 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/community/FollowUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/community/FollowUseCase.kt @@ -10,7 +10,7 @@ class FollowUseCase( ) { suspend operator fun invoke(otherUId: Long): Result { return runCatching { - val uid = requireNotNull(accountRepository.uId.first()) + val uid = requireNotNull(accountRepository.walkieId.first()) followRepository.follow(uid, otherUId).getOrThrow() } } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/community/RemoveFollowerUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/community/RemoveFollowerUseCase.kt index fa650c8a..ffbd4762 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/community/RemoveFollowerUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/community/RemoveFollowerUseCase.kt @@ -10,7 +10,7 @@ class RemoveFollowerUseCase( ) { suspend operator fun invoke(otherUId: Long): Result { return runCatching { - val uid = requireNotNull(accountRepository.uId.first()) + val uid = requireNotNull(accountRepository.walkieId.first()) followRepository.unfollow(otherUId, uid).getOrThrow() } } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/community/SendCommentUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/community/SendCommentUseCase.kt index 78ad3698..cd70ee5d 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/community/SendCommentUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/community/SendCommentUseCase.kt @@ -13,7 +13,7 @@ class SendCommentUseCase( postId: Long, content: String, ): Result { - val commenterId = accountRepository.uId.first() ?: return Result.failure(Exception("uid is null")) + val commenterId = accountRepository.walkieId.first() ?: return Result.failure(Exception("uid is null")) return postRepository.sendComment( postId, commenterId, diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/community/UnFollowUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/community/UnFollowUseCase.kt index f9da79b7..1df92466 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/community/UnFollowUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/community/UnFollowUseCase.kt @@ -10,7 +10,7 @@ class UnFollowUseCase( ) { suspend operator fun invoke(otherUId: Long): Result { return runCatching { - val uid = requireNotNull(accountRepository.uId.first()) + val uid = requireNotNull(accountRepository.walkieId.first()) followRepository.unfollow(uid, otherUId).getOrThrow() } } diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/running/GetRunningFollowerUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/running/GetRunningFollowerUseCase.kt index 37f08cb4..b2aea021 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/running/GetRunningFollowerUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/running/GetRunningFollowerUseCase.kt @@ -17,7 +17,7 @@ class GetRunningFollowerUseCase( var followings = listOf() var runningFollowings = listOf() kotlin.runCatching { - id = requireNotNull(accountRepository.uId.first()) + id = requireNotNull(accountRepository.walkieId.first()) } return callbackFlow { while (true) { diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningFinishUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningFinishUseCase.kt index 06177d90..0fa0edbb 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningFinishUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningFinishUseCase.kt @@ -9,7 +9,7 @@ class RunningFinishUseCase( private val runningRepository: RunningRepository, ) { suspend operator fun invoke(): Result { - accountRepository.uId.first()?.let { id -> + accountRepository.walkieId.first()?.let { id -> return runningRepository.finishRunning(id) } return Result.failure(Exception("ID 정보 없음")) diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningStartUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningStartUseCase.kt index 169abf90..d2d74120 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningStartUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/running/RunningStartUseCase.kt @@ -9,7 +9,7 @@ class RunningStartUseCase( private val runningRepository: RunningRepository, ) { suspend operator fun invoke(): Result { - accountRepository.uId.first()?.let { id -> + accountRepository.walkieId.first()?.let { id -> return runningRepository.startRunning(id) } return Result.failure(Exception("ID 정보 없음")) diff --git a/domain/src/main/java/com/whyranoid/domain/usecase/running/SendLikeUseCase.kt b/domain/src/main/java/com/whyranoid/domain/usecase/running/SendLikeUseCase.kt index 4c19c2d6..c11b6dce 100644 --- a/domain/src/main/java/com/whyranoid/domain/usecase/running/SendLikeUseCase.kt +++ b/domain/src/main/java/com/whyranoid/domain/usecase/running/SendLikeUseCase.kt @@ -10,7 +10,7 @@ class SendLikeUseCase( ) { suspend operator fun invoke(receiverId: Long): Result { return kotlin.runCatching { - val uid = requireNotNull(accountRepository.uId.first()) + val uid = requireNotNull(accountRepository.walkieId.first()) runningRepository.sendLike(uid, receiverId).onSuccess { return Result.success(Unit) }.onFailure { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8c02ccce..331d51f4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidx-core = "1.13.1" androidx-appcompat = "1.6.1" androidx-constraintlayout = "2.1.4" naver-maps-android-sdk = "3.16.2" -hilt = "2.44" +hilt = "2.48.1" javax-inject = "1" android-material = "1.8.0" diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt index 5e338ffd..4ad14fb2 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/MyPageScreen.kt @@ -48,6 +48,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -82,7 +83,7 @@ fun MyPageScreen( var uid by rememberSaveable { mutableStateOf(null) } LaunchedEffect(Unit) { - val myUid = requireNotNull(viewModel.accountRepository.uId.first()) + val myUid = requireNotNull(viewModel.accountRepository.walkieId.first()) uid = myUid viewModel.getUserDetail(myUid, null) viewModel.getUserBadges(myUid) @@ -196,9 +197,10 @@ fun UserPageContent( AsyncImage( model = userDetail.user.imageUrl, contentDescription = "유저 프로필 이미지", + contentScale = ContentScale.Crop, modifier = Modifier .clip(shape = CircleShape) - .size(70.dp), + .size(64.dp), ) Spacer(modifier = Modifier.width(20.dp)) diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt index b13c5701..884670ed 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileScreen.kt @@ -1,6 +1,9 @@ package com.whyranoid.presentation.screens.mypage.editprofile import android.Manifest +import android.app.Activity +import android.content.Intent +import android.util.Log import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -55,7 +58,7 @@ import androidx.core.content.FileProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import coil.compose.AsyncImage -import com.whyranoid.domain.util.EMPTY +import coil.request.ImageRequest import com.whyranoid.presentation.R import com.whyranoid.presentation.component.button.CircularIconButton import com.whyranoid.presentation.component.button.WalkieBottomSheetButton @@ -67,55 +70,56 @@ import com.whyranoid.presentation.util.createImageFile import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel -import java.util.Objects @OptIn(ExperimentalMaterialApi::class) @Composable fun EditProfileScreen(navController: NavController) { val viewModel = koinViewModel() val walkieId = viewModel.walkieId.collectAsStateWithLifecycle(initialValue = 0L) - val name = viewModel.name.collectAsStateWithLifecycle(initialValue = String.EMPTY) - val nick = viewModel.nick.collectAsStateWithLifecycle(initialValue = String.EMPTY) - val context = LocalContext.current - LaunchedEffect(viewModel.profileImg) { - viewModel.profileImg.collectLatest { - it?.let { url -> viewModel.setProfileUrl(url) } - } - } + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() val file = context.createImageFile() val uri = FileProvider.getUriForFile( - Objects.requireNonNull(context), - context.packageName + ".provider", file + context, + "com.whyranoid.walkie.provider", + file ) - val cameraLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicture()) { - viewModel.setProfileUrl(uri.toString()) - } + val cameraLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.TakePicture()) { success -> + if (success) { + viewModel.setProfileUrl(uri.toString()) + viewModel.isChangeButtonEnabled.value = true + } + } val cameraPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { if (it) { - cameraLauncher.launch(uri) + uri?.let { uri -> + cameraLauncher.launch(uri) + } } else { // 권한 거부시 } } val galleryLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { - it?.let { uri -> viewModel.setProfileUrl(uri.toString()) } + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + viewModel.setProfileUrl(result.data?.data.toString()) + viewModel.isChangeButtonEnabled.value = true + } } val bottomSheetState = rememberModalBottomSheetState( initialValue = ModalBottomSheetValue.Hidden ) - val coroutineScope = rememberCoroutineScope() - BackHandler(enabled = bottomSheetState.isVisible) { coroutineScope.launch { bottomSheetState.hide() @@ -143,6 +147,9 @@ fun EditProfileScreen(navController: NavController) { buttonText = "새 프로필 사진 찍기", onClick = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + coroutineScope.launch { + bottomSheetState.hide() + } } ) @@ -151,7 +158,14 @@ fun EditProfileScreen(navController: NavController) { WalkieBottomSheetButton( buttonText = "앨범에서 프로필 사진 가져오기", onClick = { - galleryLauncher.launch("image/*") + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "image/*" + } + galleryLauncher.launch(intent) + coroutineScope.launch { + bottomSheetState.hide() + } } ) @@ -161,6 +175,9 @@ fun EditProfileScreen(navController: NavController) { buttonText = "현재 프로필 사진 삭제", onClick = { viewModel.setProfileUrl(null) + coroutineScope.launch { + bottomSheetState.hide() + } } ) } @@ -168,8 +185,6 @@ fun EditProfileScreen(navController: NavController) { ) { EditProfileContent( walkieId = walkieId.value ?: 0L, - name = name.value.orEmpty(), - nick = nick.value.orEmpty(), bottomSheetState = bottomSheetState, viewModel = viewModel, ) { @@ -184,8 +199,6 @@ fun EditProfileScreen(navController: NavController) { @Composable fun EditProfileContent( walkieId: Long, - name: String, - nick: String, bottomSheetState: ModalBottomSheetState, viewModel: EditProfileViewModel, popBackStack: () -> Unit @@ -197,20 +210,10 @@ fun EditProfileContent( val focusManager = LocalFocusManager.current val keyboardHeight = WindowInsets.ime.getBottom(LocalDensity.current) val isDuplicateNickName by viewModel.isDuplicateNickName.collectAsStateWithLifecycle() - val currentProfileImg by viewModel.currentProfileUrl.collectAsStateWithLifecycle() - var isChangeEnabled by remember { mutableStateOf(false) } - - var initialProfileImg by remember { mutableStateOf(null) } - LaunchedEffect(viewModel.profileImg) { - viewModel.profileImg.collectLatest { - initialProfileImg = it - } - } + val userInfoUiState by viewModel.userInfoUiState.collectAsStateWithLifecycle() - LaunchedEffect(currentProfileImg) { - if (currentProfileImg != initialProfileImg) { - isChangeEnabled = true - } + LaunchedEffect(Unit) { + viewModel.getUserInfo() } LaunchedEffect(viewModel.isMyInfoChanged) { @@ -221,7 +224,7 @@ fun EditProfileContent( } LaunchedEffect(isDuplicateNickName) { - if (isDuplicateNickName == false) isChangeEnabled = true + if (isDuplicateNickName == false) viewModel.isChangeButtonEnabled.value = true } LaunchedEffect(keyboardHeight) { @@ -230,161 +233,169 @@ fun EditProfileContent( } } - Column( - modifier = Modifier - .fillMaxSize() - .padding(start = 16.dp, end = 16.dp, top = 16.dp) - .verticalScroll(scrollState) - ) { - Box( - Modifier.fillMaxSize() - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = "프로필 편집", - style = WalkieTypography.Title - ) - Icon( - imageVector = Icons.Filled.Close, - contentDescription = "close button", - modifier = Modifier - .size(24.dp) - .align(Alignment.CenterStart) - .clickable { - popBackStack() - }, - ) - } - - Spacer(modifier = Modifier.height(48.dp)) + if (userInfoUiState != null) { + val name by remember { mutableStateOf(userInfoUiState?.name) } + var nickname by remember { mutableStateOf(userInfoUiState?.nickname) } + val profileImg = userInfoUiState?.profileUrl - Box( + Column( modifier = Modifier - .align(Alignment.CenterHorizontally) - .size(90.dp), + .fillMaxSize() + .padding(start = 16.dp, end = 16.dp, top = 16.dp) + .verticalScroll(scrollState) ) { - AsyncImage( - model = currentProfileImg, - contentDescription = "프로필 이미지", - contentScale = ContentScale.Crop, - modifier = Modifier - .matchParentSize() - .clip(shape = CircleShape) - ) - - CircularIconButton( - modifier = Modifier.align(Alignment.BottomEnd), - onClick = { - coroutineScope.launch { - bottomSheetState.show() - } - }, - contentDescription = "프로필 편집", - icon = ImageVector.vectorResource(id = R.drawable.ic_edit_icon), - tint = Color(0xFF999999), - backgroundColor = Color(0xFFEEEEEE), - iconSize = 24.dp, - buttonSize = 30.dp, - ) - } + Box( + Modifier.fillMaxSize() + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = "프로필 편집", + style = WalkieTypography.Title + ) + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "close button", + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterStart) + .clickable { + popBackStack() + }, + ) + } - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(48.dp)) - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = "프로필 사진 변경", - style = WalkieTypography.Body1_SemiBold - ) + Box( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .size(90.dp), + ) { + AsyncImage( + model = ImageRequest.Builder(context) + .data(profileImg) + .fallback(R.drawable.ic_default_profile) + .error(R.drawable.ic_default_profile) + .build(), + onError = { + Log.d("sm.shin", "error: ${it.result.throwable.message}") + }, + contentDescription = "프로필 이미지", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(90.dp) + .clip(shape = CircleShape) + ) - Spacer(modifier = Modifier.height(25.dp)) + CircularIconButton( + modifier = Modifier.align(Alignment.BottomEnd), + onClick = { + coroutineScope.launch { + bottomSheetState.show() + } + }, + contentDescription = "프로필 편집", + icon = ImageVector.vectorResource(id = R.drawable.ic_edit_icon), + tint = Color(0xFF999999), + backgroundColor = Color(0xFFEEEEEE), + iconSize = 24.dp, + buttonSize = 30.dp, + ) + } - Text( - modifier = Modifier.align(Alignment.Start), - text = "이름", - style = WalkieTypography.Body1 - ) + Spacer(modifier = Modifier.height(22.dp)) - Spacer(modifier = Modifier.height(10.dp)) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = "프로필 사진 변경", + style = WalkieTypography.Body1_SemiBold + ) - WalkieTextField( - text = name, - readOnly = true, - focusRequester = focusRequester, - ) + Spacer(modifier = Modifier.height(25.dp)) - Spacer(modifier = Modifier.height(20.dp)) + Text( + modifier = Modifier.align(Alignment.Start), + text = "이름", + style = WalkieTypography.Body1 + ) - Text( - modifier = Modifier.align(Alignment.Start), - text = "워키닉네임", - style = WalkieTypography.Body1 - ) + Spacer(modifier = Modifier.height(10.dp)) - Spacer(modifier = Modifier.height(10.dp)) + WalkieTextField( + text = name.orEmpty(), + readOnly = true, + focusRequester = focusRequester, + ) - var nickName by remember { mutableStateOf(nick) } - var isEditMode by remember { mutableStateOf(false) } + Spacer(modifier = Modifier.height(20.dp)) - LaunchedEffect(nick) { - nickName = nick - } + Text( + modifier = Modifier.align(Alignment.Start), + text = "워키닉네임", + style = WalkieTypography.Body1 + ) - val nickNameRegex = Regex("[a-zA-Z0-9_.]{0,29}") - - WalkieTextField( - modifier = Modifier, - focusRequester = focusRequester, - text = nickName, - isEnabled = isEditMode, - isValidValue = isDuplicateNickName?.not(), - trailings = { - if (isEditMode.not()) { - Image( - painter = painterResource(id = R.drawable.ic_edit_icon), - contentDescription = "edit icon", - modifier = Modifier - .clickable { - focusRequester.requestFocus() - isEditMode = true - } - ) - } else { - Text( - text = "확인", - style = WalkieTypography.Caption.copy( - fontWeight = FontWeight.Bold - ), - modifier = Modifier - .clickable { - viewModel.checkDuplicateNickName(nickName) - // 성공 시 editMode 해제 - focusManager.clearFocus() - } - ) - } + Spacer(modifier = Modifier.height(10.dp)) + + var isEditMode by remember { mutableStateOf(false) } + + val nickNameRegex = Regex("[a-zA-Z0-9_.]{0,29}") + + WalkieTextField( + modifier = Modifier, + focusRequester = focusRequester, + text = nickname.orEmpty(), + isEnabled = isEditMode, + isValidValue = isDuplicateNickName?.not(), + trailings = { + if (isEditMode.not()) { + Image( + painter = painterResource(id = R.drawable.ic_edit_icon), + contentDescription = "edit icon", + modifier = Modifier + .clickable { + focusRequester.requestFocus() + isEditMode = true + } + ) + } else { + Text( + text = "확인", + style = WalkieTypography.Caption.copy( + fontWeight = FontWeight.Bold + ), + modifier = Modifier + .clickable { + viewModel.checkDuplicateNickName(nickname.orEmpty()) + // 성공 시 editMode 해제 + focusManager.clearFocus() + } + ) + } - }, - onValueChange = { - if (it.matches(nickNameRegex)) { - nickName = it - } else { - SingleToast.show(context, "닉네임은 30자 이내로 영문,숫자,마침표,_만 입력해주세요.") + }, + onValueChange = { + if (it.matches(nickNameRegex)) { + nickname = it + } else { + SingleToast.show(context, "닉네임은 30자 이내로 영문,숫자,마침표,_만 입력해주세요.") + } } - } - ) + ) - Spacer(modifier = Modifier.height(40.dp)) + Spacer(modifier = Modifier.height(40.dp)) - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - WalkiePositiveButton( - text = "변경", - isEnabled = isChangeEnabled, - onClicked = { - viewModel.changeMyInfo(walkieId, nickName, currentProfileImg) - } - ) + WalkiePositiveButton( + text = "변경", + isEnabled = viewModel.isChangeButtonEnabled.value, + onClicked = { + viewModel.changeMyInfo(walkieId, nickname.orEmpty(), profileImg) + } + ) - Spacer(modifier = Modifier.height(20.dp)) + Spacer(modifier = Modifier.height(20.dp)) + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt index 32d0f426..8a879433 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/EditProfileViewModel.kt @@ -1,5 +1,6 @@ package com.whyranoid.presentation.screens.mypage.editprofile +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.whyranoid.domain.repository.AccountRepository @@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -21,13 +23,29 @@ class EditProfileViewModel( private val _isMyInfoChanged = MutableSharedFlow() val isMyInfoChanged = _isMyInfoChanged.asSharedFlow() - private val _currentProfileUrl = MutableStateFlow(null) - val currentProfileUrl = _currentProfileUrl.asStateFlow() + private val _userInfoUiState = MutableStateFlow(null) + val userInfoUiState = _userInfoUiState.asStateFlow() - val name = accountRepository.userName - val nick = accountRepository.nickName - val profileImg = accountRepository.profileUrl - val walkieId = accountRepository.uId + val isChangeButtonEnabled = mutableStateOf(false) + + val walkieId = accountRepository.walkieId + + fun getUserInfo() { + viewModelScope.launch(Dispatchers.IO) { + val walkieId = walkieId.firstOrNull() ?: return@launch + + accountRepository.getUserInfo(walkieId) + .onSuccess { userInfo -> + _userInfoUiState.update { + UserInfoUiState( + userInfo.name, + userInfo.nickname, + userInfo.profileImg + ) + } + } + } + } fun checkDuplicateNickName(nickName: String) = viewModelScope.launch(Dispatchers.IO) { accountRepository.checkNickName(nickName) @@ -37,12 +55,16 @@ class EditProfileViewModel( .onFailure { _isDuplicateNickName.update { null } } } - fun changeMyInfo(walkieId: Long, nickName: String, profileUrl: String?) = viewModelScope.launch(Dispatchers.IO) { - accountRepository.changeMyInfo(walkieId, nickName, profileUrl) - .onSuccess { _isMyInfoChanged.emit(true) } - } + fun changeMyInfo(walkieId: Long, nickName: String, profileUrl: String?) = + viewModelScope.launch(Dispatchers.IO) { + accountRepository.changeMyInfo(walkieId, nickName, profileUrl) + .onSuccess { + _isMyInfoChanged.emit(true) + _userInfoUiState.update { it?.copy(nickname = nickName, profileUrl = profileUrl) } + } + } fun setProfileUrl(url: String?) { - _currentProfileUrl.update { url } + _userInfoUiState.update { it?.copy(profileUrl = url) } } } diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/UserInfoUiState.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/UserInfoUiState.kt new file mode 100644 index 00000000..65e52360 --- /dev/null +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/mypage/editprofile/UserInfoUiState.kt @@ -0,0 +1,7 @@ +package com.whyranoid.presentation.screens.mypage.editprofile + +data class UserInfoUiState( + val name: String, + val nickname: String, + val profileUrl: String? +) \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt index 7a184bd8..6492deee 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/screens/setting/SettingScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController @@ -98,6 +99,7 @@ fun ProfileSection( AsyncImage( model = user.imageUrl, contentDescription = "Profile Image", + contentScale = ContentScale.Crop, modifier = Modifier .size(64.dp) .clip(CircleShape) diff --git a/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt b/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt index d86de8dc..a7bd5935 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/util/extensions.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build +import android.os.Environment import android.provider.Settings import androidx.annotation.RequiresApi import java.io.File @@ -80,11 +81,11 @@ fun Activity.openStatusBar() { fun Context.createImageFile(): File { val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - val imageFileName = "JPEG_" + timeStamp + "_" - val image = File.createTempFile( + val storageDir: File? = getExternalFilesDir(Environment.DIRECTORY_PICTURES) + val imageFileName = "JPEG_${timeStamp}_" + return File.createTempFile( imageFileName, ".jpg", - externalCacheDir + storageDir ) - return image } \ No newline at end of file diff --git a/presentation/src/main/java/com/whyranoid/presentation/viewmodel/SplashViewModel.kt b/presentation/src/main/java/com/whyranoid/presentation/viewmodel/SplashViewModel.kt index 16338938..284be9ad 100644 --- a/presentation/src/main/java/com/whyranoid/presentation/viewmodel/SplashViewModel.kt +++ b/presentation/src/main/java/com/whyranoid/presentation/viewmodel/SplashViewModel.kt @@ -24,7 +24,7 @@ class SplashViewModel(private val accountRepository: AccountRepository) : ViewMo _splashState.value = if (isSignedIn()) SplashState.SignedInState else SplashState.SignInState - accountRepository.uId.collect { + accountRepository.walkieId.collect { if (it == null) { _splashState.value = SplashState.SignInState } @@ -37,7 +37,7 @@ class SplashViewModel(private val accountRepository: AccountRepository) : ViewMo } private suspend fun isSignedIn(): Boolean { - return accountRepository.uId.first() != null + return accountRepository.walkieId.first() != null } } diff --git a/presentation/src/main/res/drawable/ic_default_profile.xml b/presentation/src/main/res/drawable/ic_default_profile.xml new file mode 100644 index 00000000..eb718bbe --- /dev/null +++ b/presentation/src/main/res/drawable/ic_default_profile.xml @@ -0,0 +1,25 @@ + + + + + + + + + +