서버에 파일을 전송할 때, 다음과 같이 form-data 를 사용해서 보내는데
이것을 안드로이드에서는 어떻게 구현하는지 알아보도록 하겠습니다.
안드로이드 Retrofit2 에서는 form-data 로 데이터를 전송하기 위한 기능을 제공합니다.
implementation 'com.squareup.retrofit2:converter-scalars:2.5.0'
implementation 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.squareup.retrofit2:converter-gson:2.1.0'
Retrofit2 를 사용하기 위해서 Gradle 에 라이브러리를 추가 해줍니다.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
인터넷 권한과 갤러리 접근을 위한 권한을 설정합니다.
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
object RetrofitInstance {
val BASE_URL = "https://386e-119-67-181-215.jp.ngrok.io"
val client = Retrofit
.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
fun getInstance(): Retrofit{
return client
}
}
다음과 같이 object 를 생성하고 레트로핏을 빌드 한 객체를 리턴하는 함수를 두면,
레트로핏이 필요한 곳에서 RetrofitInstance.getInstance() 로 사용할 수 있습니다.
import okhttp3.MultipartBody
import retrofit2.Call
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface MyApi {
@Multipart
@POST("/save/image")
fun sendImage(
@Part imageFile: MultipartBody.Part
): Call<String>
}
@Multipart 어노테이션을 사용해서 File을 보낼 것임을 명시해줍니다.
이미지 파일을 전송할 것이기 때문에 데이터타입은 MultipartBody.Part 로 지정해야 합니다.
@Multipart
@POST("서버경로")
fun profileSend(
@Part("userId") userId: String,
@Part imageFile : MultipartBody.Part
): Call<String>
만약 이미지 파일 외에 다른 데이터도 전송하고 싶다면, 다음과 같이 추가해주면 됩니다.
// 임포트 생략..
class MainActivity : AppCompatActivity() {
private val imageResult = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
){ result ->
if(result.resultCode == Activity.RESULT_OK){
val imageUri = result.data?.data ?: return@registerForActivityResult
val file = File(absolutelyPath(imageUri, this))
val requestFile = RequestBody.create(MediaType.parse("image/*"), file)
val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)
Log.d("testt",file.name)
sendImage(body)
binding.imageView.setImageURI(imageUri)
}
}
companion object{
const val REQ_GALLERY = 1
}
private val binding by lazy{
ActivityMainBinding.inflate(layoutInflater)
}
private val retrofit = RetrofitInstance.getInstance().create(MyApi::class.java)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
binding.imageBtn.setOnClickListener {
selectGallery()
}
}
private fun selectGallery(){
val writePermission = ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
val readPermission = ContextCompat.checkSelfPermission(
this,
android.Manifest.permission.READ_EXTERNAL_STORAGE
)
if(writePermission == PackageManager.PERMISSION_DENIED ||
readPermission == PackageManager.PERMISSION_DENIED){
ActivityCompat.requestPermissions(
this,
arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE),
REQ_GALLERY
)
}else{
val intent = Intent(Intent.ACTION_PICK)
intent.setDataAndType(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
"image/*"
)
imageResult.launch(intent)
}
}
// 절대경로 변환
fun absolutelyPath(path: Uri?, context : Context): String {
var proj: Array<String> = arrayOf(MediaStore.Images.Media.DATA)
var c: Cursor? = context.contentResolver.query(path!!, proj, null, null, null)
var index = c?.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
c?.moveToFirst()
var result = c?.getString(index!!)
return result!!
}
fun sendImage(body: MultipartBody.Part){
retrofit.sendImage(body).enqueue(object: Callback<String>{
override fun onResponse(call: Call<String>, response: Response<String>) {
if(response.isSuccessful){
Toast.makeText(this@MainActivity, "이미지 전송 성공", Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this@MainActivity, "이미지 전송 실패", Toast.LENGTH_SHORT).show()
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
Log.d("testt", t.message.toString())
}
})
}
}
코드 설명을 덧붙이자면,
val file = File(absolutelyPath(imageUri, this))
absolutelyPath 함수에 갤러리에서 가져온 사진의 uri 를 넣고, 사진의 절대경로를 가져옵니다. 그 절대 경로를 통해 File() 함수로 이미지 파일을 얻고, file 변수에 저장합니다.
val requestFile = RequestBody.create(MediaType.parse("image/*"), file)
다음 구문을 통해 Request 형식으로 바꾸고...
val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)
다음 구문을 통해 form-data 형식으로 바꿔줍니다.
sendImage(body)
그 다음은 sendImage 메서드를 호출해 서버에 이미지를 전달 합니다.
https://kong-dev.tistory.com/151
Node.js 서버는 위 링크를 참조해서 만들고, 테스트 했습니다.
const storage = multer.diskStorage({
destination(req, file, done) { // 이미지를 저장할 경로 지정
done(null, 'profileImages/');
},
filename(req, file, done) {
const ext = path.extname(file.originalname);
const fileName = `${path.basename(
file.originalname,
ext
)}_${Date.now()}${ext}`;
done(null, fileName);
},
});
const limits = { fileSize: 5 * 1024 * 1024 };
const multerConfig = {
storage,
limits,
};
const upload = multer(multerConfig);
// 프로필이미지를 서버에 업로드
router.post("/save/image", upload.single('profile'), (req, res)=>{
const body = req.body
console.log(req.file);
res.send('upload!');
})
upload.single('profile') 에서 profile 은 클라이언트 단에서 form-data 를 생성할 때 지정했던 profile 과 동일 해야합니다.
val body = MultipartBody.Part.createFormData("profile", file.name, requestFile)
서버를 시작하고 클라이언트가 이미지를 전달하면 '서버경로/profileImages/이미지파일' 경로에 전달받은 이미지를 저장하게 됩니다.
안녕하세요. 이 글 보고 코틀린 서버전송 해보려고 하는데 메인액티비티가 실행이 되지않아서 연락남깁니다. 따로 버전이나 뭐맞춰야하는게있나요?