마이페이지 - 프로필 사진 변경하기

변현섭·2023년 9월 4일
0
post-thumbnail

이번 포스팅에서는 마이페이지에서 본인의 프로필 사진을 변경할 수 있는 기능을 추가해보도록 하겠습니다. 오늘 다룰 기능은 마이페이지의 기능 중 가장 난이도가 높은 부분임과 동시에, 반드시 구현할 수 있어야 할 중요한 기능이 아닐까 싶습니다. 이번 포스팅의 내용을 제대로 이해하기 위해서는 (백엔드 측에서) AWS S3를 사용할 수 있어야 합니다. S3 사용법이 궁금하신 분들은 아래의 링크를 참조해주세요.
>> AWS S3 사용법

4. 프로필 변경하기

1) 백엔드

① UserController에 아래의 API를 추가한다.

/**
 * 유저 프로필 사진 변경
 */
@PatchMapping("/profile")
public BaseResponse<String> modifyProfile(@RequestPart(value = "image", required = false) MultipartFile multipartFile) {
    try {
        Long userId = jwtService.getUserIdx();
        return new BaseResponse<>(userService.modifyProfile(userId, multipartFile));
    } catch (BaseException exception) {
        return new BaseResponse<>(exception.getStatus());
    }
}

/**
 * 기본 프로필 사진 적용
 */
@PatchMapping("/noProfile")
public BaseResponse<String> modifyNoProfile() {
    try {
        Long userId = jwtService.getUserIdx();
        return new BaseResponse<>(userService.modifyNoProfile(userId));
    } catch (BaseException exception) {
        return new BaseResponse<>(exception.getStatus());
    }
}

② UserService에 아래의 메서드를 추가한다.

/**
 *  유저 프로필 변경
 */
@Transactional
public String modifyProfile(Long userId, MultipartFile multipartFile) throws BaseException {
    try {
        User user = utilService.findByUserIdWithValidation(userId);
        Profile profile = profileRepository.findProfileById(userId).orElse(null);
        if(profile == null) { // 프로필이 미등록된 사용자가 변경을 요청하는 경우
            GetS3Res getS3Res;
            if(multipartFile != null) {
                getS3Res = s3Service.uploadSingleFile(multipartFile);
                profileService.saveProfile(getS3Res, user);
            }
        }
        else { // 프로필이 등록된 사용자가 변경을 요청하는 경우
            // 1. 버킷에서 삭제
            profileService.deleteProfile(profile);
            // 2. Profile Repository에서 삭제
            profileService.deleteProfileById(userId);
            if(multipartFile != null) {
                GetS3Res getS3Res = s3Service.uploadSingleFile(multipartFile);
                profileService.saveProfile(getS3Res, user);
            }
        }
        return "프로필 수정이 완료되었습니다.";
    } catch (BaseException exception) {
        throw new BaseException(exception.getStatus());
    }
}

/**
 *  기본 프로필 적용
 */
@Transactional
public String modifyNoProfile(Long userId) throws BaseException {
    try {
        User user = utilService.findByUserIdWithValidation(userId);
        Profile profile = profileRepository.findProfileById(userId).orElse(null);
        if(profile != null) { // 프로필이 미등록된 사용자가 변경을 요청하는 경우
            // 1. 버킷에서 삭제
            profileService.deleteProfile(profile);
            // 2. Profile Repository에서 삭제
            profileService.deleteProfileById(userId);
        }
        return "기본 프로필이 적용됩니다.";
    } catch (BaseException exception) {
        throw new BaseException(exception.getStatus());
    }
}

2) 프론트엔드

① mypage 패키지 하위로, ProfileActivity를 생성한다.

② activity_profile.xml 파일에 아래의 내용을 입력한다.

<?xml version="1.0" encoding="utf-8"?>
<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/main_border"
    tools:context=".mypage.ProfileActivity">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:layout_marginBottom="70dp"
            android:src="@drawable/mypage"/>

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ImageView
                android:id="@+id/profile"
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:src="@drawable/profile"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>

        <Button
            android:id="@+id/newProfileBtn"
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:layout_marginTop="70dp"
            android:layout_marginHorizontal="40dp"
            android:background="@color/skyBlue"
            android:text="프로필 변경"
            android:textSize="35sp" />

        <Button
            android:id="@+id/noProfileBtn"
            android:layout_width="match_parent"
            android:layout_height="70dp"
            android:layout_marginVertical="50dp"
            android:layout_marginHorizontal="40dp"
            android:background="@color/skyBlue"
            android:text="기본 이미지로 설정"
            android:textSize="35sp" />

    </LinearLayout>

</ScrollView>
  • 이미지를 클릭하면 프로필 이미지를 선택할 수 있다.
  • 프로필 변경 버튼을 누르면 본인의 프로필이 선택된 이미지로 설정된다. 아무 이미지도 선택하지 않았을 경우, 이미지를 선택해달라는 토스트 메시지를 띄운다.
  • 기본 이미지로 설정하면, 기본 프로필이 적용된다.

③ 마이페이지에서 프로필 변경 버튼을 클릭했을 때, 프로필 변경 페이지로 전환되도록 MyPageFragment의 onCreateView에 아래의 내용을 추가한다.

lateinit var profileImage : ImageView
...
override fun onCreateView(
...
    val profile = view.findViewById<Button>(R.id.profileBtn)
    profile.setOnClickListener {
        val intent = Intent(requireActivity(), ProfileActivity::class.java)
        startActivity(intent)
    }

④ MyPageApi 인터페이스에 아래의 API를 추가한다.

@Multipart
@PATCH("/users/profile")
suspend fun modifyProfile(
    @Header("Authorization") accessToken : String,
    @Part image : MultipartBody.Part?
): BaseResponse<String>

@PATCH("/users/noProfile")
suspend fun modifyNoProfile(
    @Header("Authorization") accessToken : String
): BaseResponse<String>

⑤ 프로필 이미지를 불러오기 위해서는 Glide 라이브러리를 사용해야 한다. 아래의 의존성을 Module 수준의 build.gradle의 dependencies에 추가하자.

implementation("com.github.bumptech.glide:glide:4.12.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.12.0")

⑥ Module 수준의 build.gradle의 defaultConfig에 아래의 내용을 추가한다.

multiDexEnabled = true

⑦ ProfileActivity를 아래와 같이 수정한다.

class ProfileActivity : AppCompatActivity() {

    private var selectedImageUri: Uri? = null
    lateinit var profileUrl : String

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_profile)

        val newProfileBtn = findViewById<Button>(R.id.newProfileBtn)
        val profileImage = findViewById<ImageView>(R.id.profile)
        val noProfileBtn = findViewById<Button>(R.id.noProfileBtn)

        CoroutineScope(Dispatchers.IO).launch {
            val response = getUserInfo(FirebaseAuthUtils.getUid())
            if (response.isSuccess) {
                if(response.result?.imgUrl != null) {
                    this@ProfileActivity.profileUrl = response.result?.imgUrl
                    val profileUri = Uri.parse(profileUrl)
                    runOnUiThread {
                        Glide.with(this@ProfileActivity)
                            .load(profileUri)
                            .into(profileImage)
                    }
                }

            } else {
                Log.d("ProfileActivity", "유저의 정보를 불러오지 못함")
            }
        }

        val getAction = registerForActivityResult(
            ActivityResultContracts.GetContent(),
            ActivityResultCallback { uri ->
                profileImage.setImageURI(uri)
                selectedImageUri = uri
            }
        )

        profileImage.setOnClickListener {
            getAction.launch("image/*")
        }

        noProfileBtn.setOnClickListener {
            getAccessToken { accessToken ->
                if (accessToken.isNotEmpty()) {
                    CoroutineScope(Dispatchers.IO).launch {
                        val response = modifyNoProfile(accessToken)
                        if (response.isSuccess) {
                            withContext(Dispatchers.Main) {
                                Toast.makeText(this@ProfileActivity, "기본 프로필이 적용됩니다", Toast.LENGTH_SHORT).show()
                                val intent = Intent(this@ProfileActivity, MainActivity::class.java)
                                startActivity(intent)
                            }
                        } else {
                            Log.d("ProfileActivity", "프로필 변경 실패")
                            val message = response.message
                            Log.d("ProfileActivity", message)
                            withContext(Dispatchers.Main) {
                                Toast.makeText(this@ProfileActivity, message, Toast.LENGTH_SHORT).show()
                            }
                        }
                    }
                } else {
                    Log.e("ProfileActivity", "Invalid Token")
                }
            }
        }

        newProfileBtn.setOnClickListener {
            if (selectedImageUri != null) {
                getAccessToken { accessToken ->
                    if (accessToken.isNotEmpty()) {
                        Log.d("ProfileActivity", selectedImageUri.toString())
                        CoroutineScope(Dispatchers.IO).launch {
                            val response = modifyProfile(accessToken, selectedImageUri)
                            Log.d("ProfileActivity", response.result.toString())
                            if (response.isSuccess) {
                                withContext(Dispatchers.Main) {
                                    Toast.makeText(this@ProfileActivity, "프로필 변경이 완료되었습니다", Toast.LENGTH_SHORT).show()
                                    val intent = Intent(this@ProfileActivity, MainActivity::class.java)
                                    startActivity(intent)
                                }
                            } else {
                                Log.d("ProfileActivity", "프로필 변경 실패")
                                val message = response.message
                                Log.d("ProfileActivity", message)
                                withContext(Dispatchers.Main) {
                                    Toast.makeText(this@ProfileActivity, message, Toast.LENGTH_SHORT).show()
                                }
                            }
                        }
                    } else {
                        Log.e("ProfileActivity", "Invalid Token")
                    }
                }
            } else {
                Toast.makeText(this@ProfileActivity, "새로운 이미지를 선택해주세요.", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private suspend fun getUserInfo(uid: String): BaseResponse<GetUserRes> {
        return RetrofitInstance.myPageApi.getUserInfo(uid)
    }

    private suspend fun modifyProfile(accessToken : String, uri: Uri?): BaseResponse<String> {
        // Create a RequestBody from the image file
        val imagePath = getImagePathFromUri(uri!!)
        val imageFile = File(imagePath)
        Log.d("ProfileActivity", "path : " + imagePath.toString())
        Log.d("ProfileActivity", "file : " + imageFile.toString())

        val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), imageFile)
        val imagePart = MultipartBody.Part.createFormData("image", imageFile.name, requestFile)

        return RetrofitInstance.myPageApi.modifyProfile(accessToken, imagePart)
    }

    private suspend fun modifyNoProfile(accessToken : String) : BaseResponse<String> {
        return RetrofitInstance.myPageApi.modifyNoProfile(accessToken)
    }

    private fun getAccessToken(callback: (String) -> Unit) {
        val postListener = object : ValueEventListener {
            override fun onDataChange(dataSnapshot: DataSnapshot) {
                val data = dataSnapshot.getValue(com.chrome.chattingapp.authentication.UserInfo::class.java)
                val accessToken = data?.accessToken ?: ""
                callback(accessToken)
            }

            override fun onCancelled(databaseError: DatabaseError) {
                Log.w("ProfileActivity", "onCancelled", databaseError.toException())
            }
        }

        FirebaseRef.userInfo.child(FirebaseAuthUtils.getUid()).addListenerForSingleValueEvent(postListener)
    }

    private fun getImagePathFromUri(uri: Uri): String? {
        val projection = arrayOf(MediaStore.Images.Media.DATA)
        val cursor = contentResolver.query(uri, projection, null, null, null)
        cursor?.moveToFirst()
        val columnIndex = cursor?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        val imagePath = cursor?.getString(columnIndex!!)
        cursor?.close()
        return imagePath
    }
}

이제 코드를 실행시켜보면, 프로필 이미지를 클릭해 휴대폰의 사진을 불러올 수 있으며, 프로필 변경 버튼을 눌러 해당 사진을 본인의 프로필 사진으로 등록할 수 있다. 또한 기본 이미지로 설정하여 No Profile로 변경할 수도 있다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글