multipart/form-data 형식의 API

쿵ㅇ양·2024년 2월 16일
0

Android

목록 보기
18/30
post-thumbnail

api에 대해 자신감이 붙고 팀원들에게 로컬에서 대이터를 저장하고 주고 받는것보다 서버에 데이터를 저장하고 받는게 편하다고 아주 오만한(?) 말을 하자마자 이 api를 만났다...ㅎ
프로필 이미지를 수정하고 그 이미지를 불러오기 위한 Api!!

서버에서 s3를 이용해 이미지를 저장하고 업로드하는데
그 이미지를 내가 patch api를 이용해서 보내주는거!!

사실 api에 대해 빠삭하게 알고있다면 충분히 어렵지 않게 할 수 있었을 것 같지만
api에 대해 잘 모르는 상태였어서 더 어려웠던 것 같다..

그래서!! 내가 짜야하는 코드는!!

이미지 수정 버튼은 총 세개


이 중에서 가장 중요한건!! 앨범에서 이미지 추가하는거!!
따라서 앨범에서 이미지 추가하는것만 할 줄 안다면 나머지도 구현가능!!

Api 인터페이스 정의!!!!!

처음 이용해보는 색다른 형식!!! 구글링해도 잘 없어서 고생했다..

1. 이미지 파일을 file 키를 통해 multipart/form-data 형식으로 요청 본문에 첨부
(키는 image 로 주심)
2. Request Body 바디에 application/json 형식으로 이미지 타입도 같이 보내줌!
(키는 body 로 주심)
여기서 주의할점은 지금까지는 Request Body는 @Body라고 선언했다면 여기서는 multipart이기때문에 @Part로 선언!!

@Multipart
@PATCH("주소")
    fun patchProfileImage(@Path(value = "profile-id") profileId: Long, @Part("body") body: RequestBody, @Part image :MultipartBody.Part?):Call<PatchProfileImage>

우선, 갤러리 열기!! : 갤러리 접근 권한 부터 받아야함

여기서 권한이 계속 거부됐다고 떠서 몇시간을 헤맸는데 알고보니..
이때 쓰고 있던 TargetSdk가 33이었는데!! 33부터는

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

흔히 구글링하면 나오는 이게 아닌...

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

이거였다는거..!!

그래서 우리는 걍 맘편히 32로 낮춤..ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

32로 낮추고 권한 받아 갤러리를 열면 너무 잘열림..!!!! 허무하지만

우선 클래스 바로 밑에 getResult 변수 선언

갤러리 활동의 결과를 기다리고 처리하는 데 사용함

lateinit var getResult: ActivityResultLauncher<Intent>

갤러리로 이동하는 함수

private fun goGallery() {

        // 현재 기기에 설정된 쓰기 권한을 가져오기 위한 변수
        var writePermission = ContextCompat.checkSelfPermission(
            requireContext(),
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE
        )

// 현재 기기에 설정된 읽기 권한을 가져오기 위한 변수
        var readPermission = ContextCompat.checkSelfPermission(
            requireContext(),
            android.Manifest.permission.READ_EXTERNAL_STORAGE
        )

// 읽기 권한과 쓰기 권한에 대해서 설정이 되어있지 않다면
        if (writePermission == PackageManager.PERMISSION_DENIED || readPermission == PackageManager.PERMISSION_DENIED) {
            // 읽기, 쓰기 권한을 요청합니다.
            Log.d("go!!", "su")
            ActivityCompat.requestPermissions(
                requireActivity(),
                arrayOf(
                    android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    android.Manifest.permission.READ_EXTERNAL_STORAGE
                ),
                1
            )
        }
// 위 경우가 아니라면 권한에 대해서 설정이 되어 있으므로
        else {
            var state = Environment.getExternalStorageState()

            // 갤러리를 열어서 파일을 선택할 수 있도록
            if (TextUtils.equals(state, Environment.MEDIA_MOUNTED)) {
                val intent = Intent(Intent.ACTION_PICK)
                intent.type = "image/*"
                
    //갤러리를 열기 위해 사용되는 Intent를 launch() 메서드를 통해 실행
   //다른 활동(갤러리)을 시작하고 해당 활동이 완료될 때까지 기다린 후 결과를 처리할 수 있게함
                getResult.launch(intent)
            }
        }
    }

갤러리에서 선택한 파일 Uri를 통해 이미지 파일의 실제 경로를 가져오기

파일 시스템 경로 대신 content:// 형식의 URI를 사용하여 파일에 접근
이러한 URI는 다른 앱의 데이터에도 접근할 수 있도록 보안 및 권한 관리를 용이하게 함
=>
but 실제 파일 시스템의 경로가 필요할 때가 있는데,,
그때가 지금!! 이 함수는 주어진 URI를 기반으로 실제 파일 시스템 경로를 결정
->URI에 대한 실제 파일 시스템 경로를 문자열로 반환해주는 함수임!!

만약 Xiaomi 디바이스인 경우, uri.path.toString()를 통해 간단히 URI의 경로를 반환하고, 그 외의 경우에는 MediaStore를 사용하여 URI에 대한 실제 파일 경로를 가져옴

이때 MediaStore는 Android의 미디어 컨텐츠를 관리하는 프레임워크로, 여기서는 이미지 데이터를 쿼리하여 파일의 실제 경로를 얻기 위해 활용한당

    private fun getRealPathFromURI(uri: Uri): String {
        val buildName = Build.MANUFACTURER
        if (buildName.equals("Xiaomi")) {
            return uri.path.toString()
        }

        var columnIndex = 0
        val proj = arrayOf(MediaStore.Images.Media.DATA)
        var cursor = requireActivity().contentResolver.query(uri, proj, null, null, null)

        if (cursor!!.moveToFirst()) {
            columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
        }

        return cursor.getString(columnIndex)
    }

이미지 수정 Api 호출 ver1. 앨범에서 이미지 설정

getRealPathFromURI 메서드에서 얻은 실제 경로를

  1. @Part Request Body
{
    "profile_image_type": "CHARACTER"
}

업데이트할 프로필 이미지에 대한 정보를 포함하는 JSON 객체를 생성.
이 정보에는 프로필 이미지의 유형을 나타내는 profile_image_type 필드가 포함됨

이 JSON 객체를 서버에 전송하기 위해 HTTP 요청의 본문으로 사용될 RequestBody를 생성합니다. 이 때, application/json 미디어 타입을 사용

  1. @Part 이미지 파일을 file 키를 통해 multipart/form-data 형식으로 요청 본문에 첨부
private fun patchProfileImage(profileId: Long, type: String, filPath : String) {
        
        // 업데이트할 프로필 이미지에 대한 정보를 JSON 객체에 담음
        val json = JSONObject()
        
        //서버에서 준 형식으로 
        json.put("profile_image_type", type)

        val mediaType = "application/json; charset=utf-8".toMediaType()
        val requestBody = json.toString().toRequestBody(mediaType)

         //로그는 맞게 찍히는지 모두 찍어봄..ㅋㅋㅋ
        Log.d("경로4",json.toString())
        Log.d("경로5",mediaType.toString())
        Log.d("경로6",requestBody.toString())

        
         // <사용자가 선택한 이미지 파일을 읽어와서 Multipart 요청 생성과정>
        
        //지정된 파일 경로를 가지고 새로운 File 객체를 생성
        //서버에서 이미지를 File로 받음
        val file = File(filePath)
        
 //미디어 타입을 나타내는 MediaType을 생성하고, 이를 사용하여 파일을 RequestBody로 변환
        val requestFile = "image/*".toMediaTypeOrNull()?.let { RequestBody.create(it, file) }
        
  //Multipart 요청을 생성 createFormData() 메서드는 MultipartBody.Part 객체 생성
  //requestFile!!는 앞에서 생성한 RequestBody 객체를 전달
  //파일이름을 file.name이라고 안적고 임의로 적어서 계속 호출 실패함..
        val body: MultipartBody.Part =
            MultipartBody.Part.createFormData("image", file.name, requestFile!!)
        
        Log.d("경로1", filePath)
        Log.d("경로2", requestFile.toString())
        Log.d("경로3", body.toString())


        val call: Call<PatchProfileImage> =
           
       // Retrofit을 사용하여 서버로 프로필 이미지 업데이트 요청을 전송      
       RetrofitClient.mainProfile.patchProfileImage(profileId, requestBody, body)
        call.enqueue(object : Callback<PatchProfileImage> {
            override fun onResponse(
                call: Call<PatchProfileImage>,
                response: Response<PatchProfileImage>
            ) {
                if (response.isSuccessful) {
                    val responseBody = response.body() // 응답 본문 가져오기
                    if (responseBody != null) {
                        Log.d("서버 응답 본문", responseBody.toString()) // 응답 본문 출력
                    } else {
                        Log.d("서버 응답 본문", "응답 본문이 비어있습니다.")
                    }
                } else {
                    Log.d("서버 응답 오류", "서버 응답이 실패했습니다.")
                }
            }

            override fun onFailure(call: Call<PatchProfileImage>, t: Throwable) {
                // 통신 실패 처리
                Log.e("통신 실패", "요청 실패: ${t.message}", t)
            }
        })
    }

이미지 수정 Api 호출 ver2. 기본이미지 설정 & 캐릭터로 설정

이건 ver1을 이해했다면 아주아주 쉬움!!
여기서는 넘겨주는 이미지 파일이 없으니깐!! 그 자리에 널로 보내주면 됨~

private fun patchProfileImage2(profileId: Long, type: String) {
        val json = JSONObject()
        json.put("profile_image_type", type)

        val mediaType = "application/json; charset=utf-8".toMediaType()
        val requestBody = json.toString().toRequestBody(mediaType)

      
        Log.d("경로4",json.toString())
        Log.d("경로5",mediaType.toString())
        Log.d("경로6",requestBody.toString())


        val call: Call<PatchProfileImage> =
            RetrofitClient.mainProfile.patchProfileImage(profileId, requestBody, null)
        call.enqueue(object : Callback<PatchProfileImage> {
            override fun onResponse(
                call: Call<PatchProfileImage>,
                response: Response<PatchProfileImage>
            ) {
                if (response.isSuccessful) {
                    val responseBody = response.body() // 응답 본문 가져오기
                    if (responseBody != null) {
                        Log.d("서버 응답 본문", responseBody.toString()) // 응답 본문 출력
                    } else {
                        Log.d("서버 응답 본문", "응답 본문이 비어있습니다.")
                        Log.d("서버", response.message())
                    }
                } else {
                    val errorBody = response.errorBody()?.string() ?: "No error body"
                    //Log.d("서버 응답 오류", "서버 응답이 실패했습니다.")
                    Log.d("서버 응답 오류", "서버 응답이 실패했습니다. 오류 메시지: $errorBody")
                    try {
                        val errorMessage = JSONObject(errorBody).getString("message")
                        Log.d("오류 메시지", errorMessage)
                        Toast.makeText(requireContext(),errorMessage,Toast.LENGTH_SHORT).show()
                    } catch (e: JSONException) {
                        Log.e("JSON 파싱 오류", "오류 메시지를 추출하는 데 실패했습니다: ${e.message}")
                    }
                }
            }

            override fun onFailure(call: Call<PatchProfileImage>, t: Throwable) {
                // 통신 실패 처리
                Log.e("통신 실패", "요청 실패: ${t.message}", t)
            }
        })
    }
이건 갤러리의 사진이 들어간거!! 이건 디폴트 이미지인 로고가 들어간거!!

✨이런 이미지까지 들어가는 마이프로필이 완성된당..!!

이거 해결하고 자러가는.../////////////////

profile
개발을 공부하고 있는 대학생

0개의 댓글

관련 채용 정보