국비교육(22-23)

101일차(1)/Android App(66) : 카메라 앱으로 사진 촬영, 저장(3) / 서버 전송

서리/Seori 2023. 3. 7. 02:00

101일차(1)/Android App(66) : 카메라 앱으로 사진 촬영, 저장(3) / 서버 전송

 

 

- 이전 예제 코드리뷰

- 서버로 찍은 사진 파일 전송하기 

 

 

- 이전에 만든 카메라를 활용하는 앱 코드리뷰!

2023.03.03 - [국비교육] - 99일차(1)/Android App(64) : 카메라 앱으로 사진 촬영, 저장

 

 

- 터치 입력에 반응하는 확대,축소가 가능한 이미지뷰를 만들어봄

 

- 특정 패키지에 이 TouchImageView를 집어넣어 놓으면 쉽게 사용할 수 있다

 

 

 

- 특정 클래스를 사용하고 싶은 경우 패키지명.클래스명 으로 사용하면 된다.

- 이전에 슈팅게임을 만들때 GameView를 사용했던 것처럼!

- 기존 이미지뷰에 터치기능을 가미한 것

 

- 이곳에 출력하는 이미지는 카메라어플로 찍은 것이다.

 

 

- 이미지를 캡쳐하고자 하는 의도를 갖고 있는 intent 객체를 생성해서,

 

 

- startActivityForResult() 메소드에 전달하면 안드로이드 운영체제에서 이 액티비티를  찾아서 활성화시켜준다.

 

- startActivityForResult()는 결과가 있는 메소드이다.. 사진을 찍은 결과를 가져온다!

 

 

- 이전에 연습했던 전화를 거는 예제에서는, intent를 가지면 startActivity로 넣어주었다.

- 이렇게 메소드를 호출하면서 바로 intent 를 넣어주어도 되고,

 아니면 의도를 생성자의 인자로 넣어주어도 사용 가능하다.(사진찍기)

 

 

- 단 이것은 전화를 거는 기능으로 끝난다! 어떤 결과를 받아오는 작업은 아니다.

 

 

 

- 2개의 메소드는 둘이 짝으로 있는 메소드이다

- 1이라는  req 코드가 들어오면 자동으로 onActivityResult 메소드가 호출된다.

 

 

- req code가 1이고 RESULT_OK가 들어오는 조건에서만 아래 코드가 수행되도록 한다!

 

 

- URI 안에 파일 객체를 담고 이 인텐트를 카메라앱에 전달한 개념이라고 보면 된다!

 

 

- 카메라 앱에서는 사진 데이터를 저 파일객체를 사용해서 저장한다.

- 출력할 String 객체를 이용해서 저장한다.

 

 

- 저 파일 객체는 어디에 만들어진 것인지?

- MyApp 안에  파일을 저장할 권한을 주기위해서 Provider로 만들었다.

 

 

- FileProvider.getUriForFile() 메소드를 활용해 권한을 준것이다.

- 저 "" 안의 내용은 Provider의 아이디라고 생각하면 된다.

 

 

- AndroidManifest에 Provider가 등록되어 있어야 한다.

- 단, 이 아이디는 이 안에서 유일한 것이여야 한다. 그래서 패키지명을 조합해서 쓰는것!

- filePath를 받아서 쓰는 것이다.

 

- 이 권한은 외부에서 우리 파일에 access 하기 위해서 필요하다.

- 이 권한의 아이디를 사용해서 MainActivity에서 불러다가 쓰는 것이다.

 

 

- putExtra() 로 intent에 담아서 전달. 출력용도로 써달라는 의미!

 

 

 

 

- 찍은 파일은 이 경로에 저장된다. (내부저장소)

 

 

 

- 촬영한 이미지 데이터가 이렇게 들어가있다.

 

- 여기에 파일을 만들수있는 파일객체를 하나 생성해서,

 이 파일객체를 Uri에 포장하고, 그 Uri를 intent 로 포장해서 카메라앱에 전달하는 것이다.

 

- 겹치지않도록 UUID로 파일명을 생성하고, .jpg를 붙여주었다.

 


 

** 서버로 찍은 사진 파일 전송하기 

 

 

- 이렇게 안드로이드에서 카메라 앱으로 사진을 찍어 저장한 파일에 추가로 어떤 작업을 할 수 있을까?

- 스프링부트 서버에 저장하기, 업로드된 정보들을 DB에 저장하기 등, 갤러리와 연동된 앱을 만들 수 있다!

 

 

- 저장된 사진에 대한 정보를 json 목록으로 출력해주면

 안드로이드 app에서도 갤러리 목록을 보여주는 기능을 만들 수 있다.

 

- 파일을 업로드하려면 로그인이 필요하도록 설정

- 로그인 정보를 select해서 목록 출력하기

 

 

- 그럼 파일을 전송하는 방법은? get, post방식으로 전송할 수는 있지만 파일은 어떻게 전송할까?

- 웹브라우저라면 <form enctype="multipart/form-data"> <input type="file" .... > 이렇게 전송하면 웹브라우저가 알아서 해줬지만, 안드로이드는 FileData를 직접 실어서 보내야한다.

- 2진데이터, 파일명 등등... 부수적인 정보를 직접 보내야 한다.

 

- 파일데이터를 읽어서 인터넷으로 보내기!

 


 

- 모든 파일은 byte (2진수 8개) 알갱이로 구성되어 있다. 이미지파일도 마찬가지이다.

- xxx.jpg 파일이 있으면 이 파일도 byte 알갱이로 구성되어 있다.

- 이 파일을 서버에 전송하고 싶으면 xxx.jpg 파일에서 읽어들일 스트림 객체(InputStream or FileInputStream) 을 이용해서 byte 알갱이를 읽어들인 후, network(인터넷)을 통해서 스트림 객체(OutputStream)을 이용해서 출력한다.

 

- 반복문으로 읽어들인 후 네트워크를 사용해서 출력한다!

 

 

 

- 이미지와 같이 읽어들여서 네트워크를 통해서 출력!

- 스프링 부트 서버에서 받아서 서버의 파일시스템에 저장하고, DB에 저장한다.

 

 

- 안드로이드에서는 HttpUrlConnection 객체를 이용해서 참조값을 얻어낸다.

 

 

** 네트워크를 사용해 파일을 전송하는 메소드

 

 

- httpUrlConnection 객체 사용

- 요청 url만 잘 전달하면 된다.

 

 

- form에서 파일을 작성하던 방식으로 쓰면 위와 같다.

- connection에 이와 같은 설정을 해주는 것이다. muiltipart/form-data 라는것을 알려주기!

 

 

- boundary는 경계선이다. 경계선 설정에는 어떤 문자열을 활용할 예정

 

 

[ 서버에 출력하는 데이터 예시 ]

 

 

- 이런 문자열을 서버에 보낸다고 하면, 중간에 위와 같은 경계선을 만들 수 있다.

- 이런 경계선으로 데이터를 구분할 수 있다. (문자열 데이터/2진 데이터 등...)

- 이 경계선이 바운더리 설정이다!

 

- 이 바운더리를 아래에서 활용하고 있다.

 

- 파일 데이터만 보내는것이 아니고, 파일명, 파일 정보를 함께 보내서 쓰는 것

 


 

 

- 이것은 서버에게 추가정보를 보내는것이다.

- User Agent 가 Java Code라는 것을 알려주는 것

 

 

- connection으로 outputstream을 얻어낸다.

 

 

- 파일에서 읽어들여서 outputStream을 사용해서 출력한다.

- boundary를 사용해서 출력했음을 알려준다.

 


 

activity_main.xml

 

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <EditText
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:id="@+id/inputCaption"
        android:hint="설명 입력..."/>
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="업로드"
        android:id="@+id/uploadBtn"/>
</LinearLayout>

 

- 새 리니어레이아웃(horizontal)으로 버튼, EditText를 추가해주기

 

 

- 이 4개의 컴포넌트는 높이를 경쟁한다.(vertical)

- 그런데 지금 123으로 이미 꽉 차서, 높이를 다 갖게 되어있어서 4는 들어갈 자리가 없다.

 

 

- 이대로 run 해보면 EditText는 자리가 없어서 보이지 않는다.

- TouchImageView가 일단 높이를 가지고 있지 않다가(0dp) 남는 높이를 다 가지도록 수정해주면 된다.

 

 

- 그러면 이런 형태가 된다. EditText에 내용을 입력하고 버튼을 누르면 파일과 텍스트가 같이 전송되도록 할 것이다.

 

 

- 위와 같이 사진에 대한 설명을 넣고 서버에 전송하면 된다.

- input type="file", input type="text" 를 함께 전송한다고 생각하면 된다.

 


 

- step18login에서 로그인 기능 가져오기

 

//view binding 을 사용하기 위한 설정
buildFeatures {
    viewBinding = true
}

 

- gradle 파일에 뷰바인딩 설정 추가 + Sync Now!!

 

 

- Activity 2개, xml 파일 2개 복사해오기

- login, logout에 필요한 파일에 AppConstants도 추가

 

- AppConstants는 BASE_URL 만 남겨두기

 

 

MainActivity

package com.example.step25imagecapture;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.FileProvider;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Serializable;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public class MainActivity extends AppCompatActivity {
    ImageView imageView;
    //저장된 이미지의 전체 경로
    String imagePath;
    //필요한 필드
    String sessionId, id;
    SharedPreferences pref;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //사진을 출력할 ImageView 의 참조값 필드에 저장하기
        imageView=findViewById(R.id.imageView);

        Button takePicture=findViewById(R.id.takePicture);
        takePicture.setOnClickListener(v->{
            //사진을 찍고 싶다는 Intent 객체 작성하기
            Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            //운영체제에 해당 인턴트를 처리할수 있는 App 을 실행시켜 달라고 하고 결과 값도 받아올수 있도록 한다.
            startActivityForResult(intent, 0);

        });

        Button takePicutre2=findViewById(R.id.takePicture2);
        takePicutre2.setOnClickListener(v->{
            //사진을 찍고 싶다는 Intent 객체 작성하기
            Intent intent=new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            //외부 저장 장치의 절대 경로
            String absolutePath=getExternalFilesDir(null).getAbsolutePath();
            //파일명 구성
            String fileName= UUID.randomUUID().toString()+".jpg";
            //생성할 이미지의 전체 경로
            imagePath=absolutePath+"/"+fileName;
            //이미지 파일을 저장할 File 객체
            File photoFile=new File(imagePath);
            //File 객체를 Uri 로 포장을 한다.
            //Uri uri= Uri.fromFile(photoFile);
            Uri uri=FileProvider.getUriForFile(this,
                    "com.example.step25imagecapture.fileprovider",
                    photoFile);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
            startActivityForResult(intent, 1);
        });

        EditText inputCaption=findViewById(R.id.inputCaption);

        //업로드 버튼에 대한 동작
        Button uploadBtn=findViewById(R.id.uploadBtn);
        uploadBtn.setOnClickListener(v->{
            //입력한 caption 과 찍은 사진 파일을 서버에 업로드 한다.
            String caption=inputCaption.getText().toString();
            //서버에 전송할 요철 파라미터를 Map 에 담고
            Map<String, String> map=new HashMap<>();
            map.put("caption", caption);
            //비동기 테스크를 이용해서 전송한다.
            new UploadTask().execute(map);
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        pref= PreferenceManager.getDefaultSharedPreferences(this);
        sessionId=pref.getString("sessionId", "");
        //로그인 했는지 체크하기
        new LoginCheckTask().execute(AppConstants.BASE_URL+"/music/logincheck");
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        //만일 위에서 요청한 요청과 같고 결과가 성공적이라면
        if(requestCode == 0 && resultCode == RESULT_OK){
            //data Intent 객체에 결과값(섬네일 이미지 데이터) 가 들어 있다.
            Bitmap image=(Bitmap)data.getExtras().get("data");
            //ImageView 에 출력하기
            imageView.setImageBitmap(image);
        }else if(requestCode == 1 && resultCode == RESULT_OK){
            //만일 여기가 실행된다면 imagePath 경로에 이미지 파일이 성공적으로 만들어진 것이다
            //Bitmap image=BitmapFactory.decodeFile(imagePath);
            //imageView.setImageBitmap(image);

            fitToImageView(imageView, imagePath);
        }
    }

    //이미지 뷰의 크기에 맞게 이미지를 출력하는 메소드
    public static void fitToImageView(ImageView imageView, String absolutePath){
        //출력할 이미지 뷰의 크기를 얻어온다.
        int targetW = imageView.getWidth();
        int targetH = imageView.getHeight();
        // Get the dimensions of the bitmap
        BitmapFactory.Options bmOptions = new BitmapFactory.Options();
        bmOptions.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(absolutePath, bmOptions);
        int photoW = bmOptions.outWidth;
        int photoH = bmOptions.outHeight;

        // Determine how much to scale down the image
        int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

        // Decode the image file into a Bitmap sized to fill the View
        bmOptions.inJustDecodeBounds = false;
        bmOptions.inSampleSize = scaleFactor;
        bmOptions.inPurgeable = true;
        Bitmap bitmap = BitmapFactory.decodeFile(absolutePath, bmOptions);
        /* 사진이 세로로 촬영했을때 회전하지 않도록 */
        try {
            ExifInterface ei = new ExifInterface(absolutePath);
            int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
            switch(orientation) {
                case ExifInterface.ORIENTATION_ROTATE_90:
                    bitmap = rotateImage(bitmap, 90);
                    break;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    bitmap = rotateImage(bitmap, 180);
                    break;
                // etc.
            }
        }catch(IOException ie){
            Log.e("####", ie.getMessage());
        }

        imageView.setImageBitmap(bitmap);
    }
    //Bitmap 이미지 회전시켜서 리턴하는 메소드
    public static Bitmap rotateImage(Bitmap source, float angle) {
        Bitmap retVal;

        Matrix matrix = new Matrix();
        matrix.postRotate(angle);
        retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);

        return retVal;
    }
    //로그인 여부를 체크하는 작업을 할 비동기 task
    class LoginCheckTask extends AsyncTask<String, Void, Boolean> {

        @Override
        protected Boolean doInBackground(String... strings) {
            //로그인 체크 url
            String requestUrl=strings[0];
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            boolean isLogin=false;
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setRequestMethod("GET");//Default 설정
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
                    if(!sessionId.equals("")) {
                        // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
                        conn.setRequestProperty("Cookie", sessionId);
                    }

                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();

                    if(responseCode==200){//정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr=new InputStreamReader(conn.getInputStream());
                        br=new BufferedReader(isr);
                        //반복문 돌면서 읽어오기
                        while(true){
                            //한줄씩 읽어들인다.
                            String line=br.readLine();
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            if(line==null)break;
                            //읽어온 문자열 누적 시키기
                            builder.append(line);
                        }
                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                //만일 쿠키가 존대 한다면
                if(cookList != null){
                    //반복문 돌면서
                    for(String tmp : cookList){
                        //session id 가 들어 있는 쿠키를 찾아내서
                        if(tmp.contains("JSESSIONID")){
                            //session id 만 추출해서
                            String sessionId=tmp.split(";")[0];
                            //SharedPreferences 을 편집할수 있는 객체를 활용해서
                            SharedPreferences.Editor editor=pref.edit();
                            //sessionId 라는 키값으로 session id 값을 저장한다.
                            editor.putString("sessionId", sessionId);
                            editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
                            //필드에도 담아둔다.
                            MainActivity.this.sessionId=sessionId;
                        }
                    }
                }
                //출력받은 문자열 전체 얻어내기
                JSONObject obj=new JSONObject(builder.toString());
                /*
                    {"isLogin":false} or {"isLogin":true, "id":"kimgura"}
                    서버에서 위와 같은 형식의 json 문자열을 응답할 예정이다.
                 */
                Log.d("서버가 응답한 문자열", builder.toString());
                //로그인 여부를 읽어와서
                isLogin=obj.getBoolean("isLogin");
                //만일 로그인을 했다면
                if(isLogin){
                    //필드에 로그인된 아이디를 담아둔다.
                    id=obj.getString("id");
                }
            }catch(Exception e){//예외가 발생하면
                Log.e("LoginCheckTask", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //로그인 여부를 리턴하면 아래의 onPostExecute() 메소드에 전달된다.
            return isLogin;
        }

        @Override
        protected void onPostExecute(Boolean isLogin) {
            super.onPostExecute(isLogin);
            //만일 로그인 하지 않았다면
            if(!isLogin){
                //로그인 액티비티로 이동
                Intent intent=new Intent(MainActivity.this, LoginActivity.class);
                startActivity(intent);
            }
        }
    }

    class UploadTask extends AsyncTask<Map<String, String>, Void, String>{
        private String filePath;
        private String fileParamName;
        private final String boundary;
        private static final String LINE_FEED = "\r\n"; //개행기호 설정
        private String charset;

        //생성자
        public UploadTask(){
            // 경계선은 사용할때 마다 다른 값을 사용하도록 time milli 를 조합해서 사용한다. (캐쉬방지)
            boundary = "===" + System.currentTimeMillis() + "===";
            charset="utf-8";
            //서버에서 GalleryDto 의 image 라는 필드명과 일치(MultipartFile)
            fileParamName="image"; // <input type="file" name="image"/>
            //업로드할 파일이 어디 있는지 정보를 필드에 넣어준다.
            filePath=imagePath;
        }


        @Override
        protected String doInBackground(Map<String, String>... maps) {
            Map<String, String> param=maps[0];
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            PrintWriter pw=null;
            OutputStream os=null;
            FileInputStream fis=null;
            BufferedReader br=null;

            try{
                //URL 객체 생성
                URL url=new URL(AppConstants.BASE_URL+"/api/gallery/insert");
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setDoOutput(true);
                    conn.setDoInput(true);
                    conn.setRequestMethod("POST");
                    conn.setUseCaches(false);//케쉬 사용 여부

                    //App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
                    if(!sessionId.equals("")) {
                        // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
                        conn.setRequestProperty("Cookie", sessionId);
                    }

                    //전송하는 데이터에 맞게 값 설정하기
                    conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
                    conn.setRequestProperty("User-Agent", "CodeJava Agent");
                    //인터냇을 통해서 서버로 출력할수 있는 스트림 객체의 참조값 얻어오기
                    os=conn.getOutputStream();
                    //출력할 스트림 객체 얻어오기
                    pw=new PrintWriter(new OutputStreamWriter(os, charset));
                    //-------------- 전송 파라미터 추가  ------------------
                    if(param!=null){//요청 파리미터가 존재 한다면

                        Set<String> keySet=param.keySet();
                        Iterator<String> it=keySet.iterator();

                        //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                        while(it.hasNext()){
                            String key=it.next();
                            pw.append("--" + boundary).append(LINE_FEED);
                            pw.append("Content-Disposition: form-data; name=\"" + key + "\"")
                                    .append(LINE_FEED);
                            pw.append("Content-Type: text/plain; charset=" + charset).append(
                                    LINE_FEED);
                            pw.append(LINE_FEED);
                            pw.append(param.get(key)).append(LINE_FEED);
                            pw.flush();
                        }
                    }
                    //------------- File Field ------------------
                    File file=new File(filePath);
                    String filename=file.getName(); //파일명
                    pw.append("--" + boundary).append(LINE_FEED);
                    pw.append("Content-Disposition: form-data; name=\"" + fileParamName + "\"; filename=\"" + filename + "\"")
                            .append(LINE_FEED);
                    pw.append("Content-Type: " + URLConnection.guessContentTypeFromName(filename))
                            .append(LINE_FEED);
                    pw.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
                    pw.append(LINE_FEED);
                    pw.flush();
                    //파일에서 읽어들일 스트림 객체 얻어내기
                    fis = new FileInputStream(file);
                    //byte 알갱이를 읽어들일 byte[] 객체 (한번에 4 kilo byte 씩 읽을수 있다)
                    byte[] buffer = new byte[4096];

                    //반복문 돌면서
                    while(true){
                        //byte 를 읽어들이고 몇 byte 를 읽었는지 리턴 받는다.
                        int readedByte=fis.read(buffer);
                        //더이상 읽을게 없다면 반복문 탈출
                        if(readedByte==-1)break;
                        //읽은 만큼큼 출력하기
                        os.write(buffer, 0, readedByte);
                        os.flush();
                    }

                    pw.append(LINE_FEED);
                    pw.flush();
                    pw.append(LINE_FEED).flush();
                    pw.append("--" + boundary + "--").append(LINE_FEED);
                    pw.flush();
                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();

                    if(responseCode==200){//정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr=new InputStreamReader(conn.getInputStream());
                        br=new BufferedReader(isr);
                        //반복문 돌면서 읽어오기
                        while(true){
                            //한줄씩 읽어들인다.
                            String line=br.readLine();
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            if(line==null)break;
                            //읽어온 문자열 누적 시키기
                            builder.append(line);
                        }
                    }
                    //서버가 응답한 쿠키 목록을 읽어온다.
                    List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                    //만일 쿠키가 존대 한다면
                    if(cookList != null){
                        //반복문 돌면서
                        for(String tmp : cookList){
                            //session id 가 들어 있는 쿠키를 찾아내서
                            if(tmp.contains("JSESSIONID")){
                                //session id 만 추출해서
                                String sessionId=tmp.split(";")[0];
                                //SharedPreferences 을 편집할수 있는 객체를 활용해서
                                SharedPreferences.Editor editor=pref.edit();
                                //sessionId 라는 키값으로 session id 값을 저장한다.
                                editor.putString("sessionId", sessionId);
                                editor.apply();//apply() 는 비동기로 저장하기 때문에 실행의 흐름이 잡혀 있지 않다(지연이 없음)
                                //필드에도 담아둔다.
                                MainActivity.this.sessionId=sessionId;
                            }
                        }
                    }
                }
            }catch(Exception e){//예외가 발생하면
                Log.e("UploadTask", e.getMessage());
            }finally {
                try{
                    if(pw!=null)pw.close();
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(fis!=null) isr.close();
                    if(os!=null)os.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //응답 받은 json 문자열 리턴하기
            return builder.toString();
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
        }
    }
}

 

 

 

- onStart에서 로그인했는지 확인해서 하지 않았으면 LoginActivity로 보내는 절차

 


 

- Spring Boot에서 코드 수정

- MusicController 에서 일부 가져오기(로그인 메소드 등...)

 

GalleryController

//안드로이드 앱에서 사진을 업로드하는 요청을 처리하는 메소드
@PostMapping("/api/gallery/insert")
@ResponseBody
public Map<String, Object> apiInsert(GalleryDto dto, HttpServletRequest request){
	/*
    	GalleryDto에 담긴 내용을 활용해서 이미지를 파일시스템에 저장하고 이미지 정보를 DB에도 저장한다.
    */
	service.saveImage(dto, request);
	Map<String, Object> map=new HashMap<>();
	map.put("isSuccess", "true");
	return map;
}

 

 

- GalleryController에 안드로이드용 메소드 추가

- 서비스에 이 업로드하는 로직이 이미 들어있다.

 

 

- Service의 saveImage 메소드를 사용한다.

- 파일시스템, DB에 모두 저장한다.

 

 

- GalleryDto를 보면 caption, image가 있다.

- 앱에서는 사진에 대한 설명과 파일을 전송한다.

- form 형태라면 이렇게 전달할 것! 필드명input 요소의 name속성의 value를 일치시킨다.

 

- 안드로이드에서도 이미지를 업로드할때 각각 저 captionimage 라는 파라미터로 정보를 직접 보내주어야 한다.

→ 안드로이드에서 caption, image 라는 단어를 기억하고 코딩해야 한다!

 


 

- config 로그인 설정 (인터셉터)

 

mobileLoginInterceptor

 

- "/api/gallery/insert" 경로 추가

 

- 갤러리 list는 무조건 요청이 가능하도록 할 예정(비회원 가능)

 


 

activity_login.xml 수정

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/inputId"
    android:hint="아이디..."
    android:layout_marginTop="100dp"/>

 

 

- 세로 가운데정렬을 없애고 marginTop만 추가!

 


 

MainActivity 수정

@Override
protected void onPostExecute(Boolean isLogin) {
    super.onPostExecute(isLogin);
    if(!isLogin){
        Intent intent = new Intent(MainActivity.this, LoginActivity.class);
        startActivity(intent);
    }
}

 

 

- LoginCheckTask를 로그인되었는지 여부를 알아내서 onPostExecute로 리턴해준다.

 

 

- 새로 만든 버튼도 참조값 찾아주기!

 

 

- 파라미터 타입 map, 과정타입 없음(void), 결과타입 String 인 UploadTask 클래스 생성

 

 

- doInBackground, onPostExecute 를 오버라이드

 

 

 

- 위에서 봤던 파일 전송 메소드를 doInBackground 메소드 안에 작성해준다.

 

 

- 필드, 생성자 만들어주기

 

 

- 캐쉬가 만들어지지 않도록, boundary를 그때그때 다르게 하고자 currentTimeMillis() 사용

 

 

- URL 경로는 이렇게 AppConstants를 사용해서 넣어준다.

 

 

- getOutputStream() 으로 읽어온다.

 

 

- map에 담아서 전달하면 while문 안에서 돌면서 이 안에서 구성해준다.

- caption이라는 키값(파라미터명)으로 hello를 담아서 넘겨준다고 하면 여기서 map에 담아 저장해주는 것

 

 

- 파일 저장경로는 imagePath를 사용한다.

- MainActivity의 필드 imagePath를 filePath에 넣어주고, 파일명(fileParamName)도 같이 전송해준다.

 

- 파일에서 읽어들일 스트림 객체 얻어내기

 

 

- 위 내용을 이전에 작성했던 방식으로 변경했다.

- 미리 준비한 바이트 배열을 전달해서 파일로 읽어들이고, 읽은만큼 출력해준다.

- readedByte에서 -1이 리턴되면 루프를 빠져나오도록 한다.

 

//App 에 저장된 session id 가 있다면 요청할때 쿠키로 같이 보내기
if(!sessionId.equals("")) {
    // JSESSIONID=xxx 형식의 문자열을 쿠키로 보내기
    conn.setRequestProperty("Cookie", sessionId);
}

 

- 그리고 로그인정보도 같이 보내야한다. 복사해서 메소드안으로 넣어줌

 

 

- 서버가 응답한 쿠키 목록(cookList) 부분도 필요하므로 응답을 다 받은 다음에 추가하기

 

 

- 이 부분에 추가

 

 

- 해시맵은 사용하지 않으므로 지워주었다.

 

 

 

- onCreate 안에서 EditText의 참조값을 얻어내고, 이 uploadTask를 사용하는 코드를 추가한다.

 


 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyAndroid"
        android:usesCleartextTraffic="true">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name=".LoginActivity" android:exported="false"/>
        <activity android:name=".LogoutActivity" android:exported="false"/>
        <provider
            android:authorities="com.example.step25imagecapture.fileprovider"
            android:name="androidx.core.content.FileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths"/>
        </provider>
    </application>

</manifest>

 

 

 

- 인터넷 사용 권한 추가

- 액티비티 2개도 추가

 

 

 

- 로그인하고 나서 사진 찍기 (원본 사진)

- 아래 EditText에서 문자열을 입력해서 전송한다.

 

 

- DB에서 확인해보면 파일이 업로드되어 있다.

 

 

 

- 웹브라우저의 갤러리 목록보기에서도 확인할 수 있다.

 


 

- 이후에는 안드로이드에서 보이는 화면을 리스트 뷰로 이미지가 작게 보이도록 처리하고, 클릭하면 크게 보이도록 수정할 예정!

(그러면 페이징 처리도 필요없고 스크롤로만 로딩하면 된다.)