국비교육(22-23)

109일차(1)/Android App(73) : 모바일 갤러리 기능 구현(7)

서리/Seori 2023. 3. 17. 01:18

109일차(1)/Android App(73) : 모바일 갤러리 기능 구현(7)

 

- 이전 게시물 참고

2023.03.08 - [국비교육] - 102일차(1)/Android App(67) : 모바일 갤러리 기능 구현(1)

 

 

- 유틸리티를 사용해 한 작업의 에러 처리

 

 

- 유틸리티를 만들어서 쉽게 Http 요청을 보내고 코드를 축약함

- HttpUtil 안에서 UrlConnection객체를 요청해서 요청에 반응해주고 있다.

 

 

- 응답 코드는 200번이면 정상, 300번대는 리다이렉트 요청,

 코드가 400번대이면 요청을 잘못한 것이고, 500번대는 서버 오류이다

- 200번 이외에는 의도적으로 예외를 발생시키고, 메시지를 담아서 응답해주었다.

 

 

- 모든 종류의 Exception을 아래 catch 블록에서 잡아주고 있다.

 (Exception이 부모타입으로 모든 종류의 예외를 받아줄 수 있다)

 

 

- 예외 객체로부터 예외 메시지를 얻어낼 수 있다.

 

 

- getMessage라는 메소드로 예외 메세지를 얻어내고,

 onCancelled 메소드를 호출한다. 그러면 onPostExecute가 호출되지 않는다.

 


 

GalleryListActivity

package com.example.step25imagecapture;

import android.app.ProgressDialog;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.step25imagecapture.databinding.ActivityGalleryListBinding;
import com.example.step25imagecapture.util.MyHttpUtil;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class GalleryListActivity extends AppCompatActivity implements View.OnClickListener,
        MyHttpUtil.RequestListener {
    ActivityGalleryListBinding binding;
    //서버에서 받아온 갤러리 목록을 저장할 객체
    List<GalleryDto> list=new ArrayList<>();

    GalleryAdapter adapter;

    //진행중 알림을 띄우기 위한 객체
    ProgressDialog progress;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //바인딩 객체의 참조값을 필드에 저장
        binding=ActivityGalleryListBinding.inflate(getLayoutInflater());
        //바인딩 객체를 이용해서 화면 구성
        setContentView(binding.getRoot());

        //진행중 알림을 구성한다.
        progress=new ProgressDialog(this);
        progress.setProgressStyle(ProgressDialog.STYLE_SPINNER);
        progress.setMessage("로딩중입니다...");

        //ListView에 연결할 아답타 객체 생성
        adapter=new GalleryAdapter(this, R.layout.listview_cell, list);
        //ListView에 아답타 연결하기
        binding.listView.setAdapter(adapter);
        //버튼에 리스너 등록하기
        binding.takePicBtn.setOnClickListener(this);
        binding.refreshBtn.setOnClickListener(this);
        //ListView 의 cell을 클릭했을 때 리스너
        binding.listView.setOnItemClickListener((parent, view, position, id) -> {
            //position은 클릭한 cell의 인덱스 값이다.
            GalleryDto dto=list.get(position);
            Intent intent=new Intent(this, DetailActivity.class);
            intent.putExtra("dto",dto);
            startActivity(intent);
        });
    }

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

        //원격지 서버로부터 갤러리 목록을 받아오는 요청을 한다.
        //new GalleryListTask().execute(AppConstants.BASE_URL+"/api/gallery/list");

        new MyHttpUtil(this).sendGetRequest(1,
                AppConstants.BASE_URL+"/api/gallery/list", null, this);
        //ProgressDialogue를 띄운다.
        progress.show();
    }

    //버튼을 눌렀을때 호출되는 메소드
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.takePicBtn:
                //사진을 찍어서 올리는 액티비티를 실행한다.
                Intent intent=new Intent(this, MainActivity.class);
                startActivity(intent);
                break;
            case R.id.refreshBtn:
                //목록을 다시 받아온다.
                //new GalleryListTask().execute(AppConstants.BASE_URL+"/api/gallery/list");
                new MyHttpUtil(this).sendGetRequest(1,
                        AppConstants.BASE_URL+"/api/gallery/list", null, this);
                progress.show();
                break;
        }
    }

    @Override
    public void onSuccess(int requestId, String data) {
        //여기는 UI 스레드 (자유롭게 UI작업을 할수있다)
        //jsonStr 은 [{},{},...] 형식의 문자열이기 때문에 JSONArray 객체를 생성한다.
        list.clear();
        try {
            JSONArray arr=new JSONArray(data);
            for(int i=0; i<arr.length(); i++){
                //i번째 JSONObject 객체를 참조
                JSONObject tmp=arr.getJSONObject(i);
                int num=tmp.getInt("num");
                String writer=tmp.getString("writer");
                String caption=tmp.getString("caption");
                String imagePath=tmp.getString("imagePath");
                String regdate=tmp.getString("regdate");
                GalleryDto dto=new GalleryDto();
                dto.setNum(num);
                dto.setWriter(writer);
                dto.setCaption(caption);
                //http://xxx/xxx/resources/upload/xxx.jpg 형식의 문자열을 구성해서 넣기
                dto.setImagePath(AppConstants.BASE_URL+imagePath);
                dto.setRegdate(regdate);
                //ArrayList 객체에 누적시키기
                list.add(dto);
            }
            //모델의 데이터가 바뀌었다고 아답타에 알려서 listView가 업데이트 되도록 한다.
            adapter.notifyDataSetChanged();

        } catch (JSONException je) {
            Log.e("onPostExecute()", je.getMessage());
        }
        progress.dismiss();
    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {
        progress.dismiss();
        //예외 메시지 얻어내기
        String errMsg=(String) result.get(MyHttpUtil.ERR_MSG);
        //토스트로 출력하기
        Toast.makeText(this, errMsg, Toast.LENGTH_LONG).show();
    }

}

 

 

- HttpUtil에서 예외 메세지를 얻어내고 알려준다. 토스트메시지로 출력하기!

 

 

- DetailActivity의 새로고침(refreshBtn) 에서 잘못된 요청을 해볼 것!

- 표시한 부분의 링크를 다른 것으로 바꿔준다.

 

 

- 요청 경로가 잘못되었을 때 나타나는 오류.

 

 

- Spring Boot 서버를 끈 경우 나타나는 오류.

 

 

- 이러한 오류는 url.openConnection에서 예외가 발생한 것이다.

- 연결이되지않아서 네트워크 exception이 발생한 것!

 

 

- 의도적으로 500번 오류를 발생시키기

 

 

- 500번 오류 메시지 출력

 

 

 

- MyHttpUtil의 리스너에 예외 정보를 이렇게 담아주었기 때문에 해당 메시지가 출력되는 것!

 

 


 

MyHttpUtil 예외처리 추가 

package com.example.step25imagecapture.util;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.AsyncTask;
import android.util.Log;

import androidx.annotation.Nullable;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/*
    - Http 요청을 할때 서버가 응답하는 쿠키를 모두 읽어서 저장하고 싶다
    - 다음번 Http 요청을 할때 저장된 쿠키를 모두 보내고 싶다
    - 쿠키 값이 수정되어서 응답되면 저장되어 있는 쿠키를 수정해야 한다.
    - 그러면 쿠키를 SQLiteDataBase 를 활용해서 관리하면 빠르게 처리 할수 있지 않을까?
 */
public class MyHttpUtil {
    //필드
    private Context context;
    private DBHelper dbHelper;
    //상수
    public static final String ERR_MSG="errMsg";
    //생성자
    public MyHttpUtil(Context context){
        this.context=context;
        //DBHelper 객체의 참조값을 얻어내서 필드에 저장해 둔다.
        dbHelper=new DBHelper(context, "CookieDB.sqlite", null, 1);
    }
    //이 유틸리티를 사용하는 곳에서 구현해야 하는 인터페이스
    public interface RequestListener{
        public void onSuccess(int requestId, String data);
        public void onFail(int requestId, Map<String, Object> result);
    }

    class DBHelper extends SQLiteOpenHelper{

        public DBHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
            super(context, name, factory, version);
        }
        //해당 DBHelper 를 처음 사용할때 호출되는 메소드 (new DBHelper() 를 처음 호춯할때)
        @Override
        public void onCreate(SQLiteDatabase db) {
            //테이블을 만들면 된다.
            String sql="CREATE TABLE board_cookie (cookie_name TEXT PRIMARY KEY, cookie TEXT)";
            db.execSQL(sql);
        }
        //DB 를 리셋(업그레이드)할때 호출되는 메소드
        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            //업그레이드할 내용을 작성하면 된다.
            db.execSQL("DROP TABLE IF EXISTS board_cookie"); //만일 테이블이 존재하면 삭제한다.
            //다시 만들어 질수 있도록 onCreate() 메소드를 호출한다.
            onCreate(db);
        }
    }
    /*
        GET 방식 요청을 하는 메소드
     */
    public void sendGetRequest(int requestId, String requestUrl, Map<String, String> params,
                               RequestListener listener){
        //GET 방식 요청을 할 비동기 Task 객체를 생성해서
        GetRequestTask task=new GetRequestTask();
        //필요한 값을 넣어주고
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        //비동기 Task 를 실행한다.
        task.execute(params);
    }
    /*
        POST 방식 요청을 하는 메소드
    */
    public void sendPostRequest(int requestId, String requestUrl, Map<String, String> params,
                                RequestListener listener){
        //POST 방식 요청을 할 비동기 Task 객체를 생성해서
        PostRequestTask task=new PostRequestTask();
        //필요한 값을 넣어주고
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        //비동기 Task 를 실행한다.
        task.execute(params);
    }
    /*
        파일업로드 요청을 하는 메소드
     */
    public void fileUploadRequest(int requestId, String requestUrl, Map<String, String> params,
                                  RequestListener listener, File file){
        FileUploadTask task=new FileUploadTask();
        //필요한 값을 넣어주고
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        task.setFile(file);
        //비동기 Task 를 실행한다.
        task.execute(params);
    }
    private class FileUploadTask extends AsyncTask<Map<String, String>, Void, String>{
        //필요한 필드 구성
        private int requestId;
        private String requestUrl;
        private RequestListener listener;
        private File file;
        //전송되는 파일의 파라미터명 설정 (프로젝트 상황에 맞게 변경해서 사용해야한다)
        private final String FILE_PARAM_NAME="image";

        private final String boundary;
        private static final String LINE_FEED = "\r\n"; //개행기호 설정
        private String charset;

        private String errMsg;

        //생성자
        public FileUploadTask(){
            // 경계선은 사용할때 마다 다른 값을 사용하도록 time milli 를 조합해서 사용한다. (캐쉬방지)
            boundary = "===" + System.currentTimeMillis() + "===";
            charset="utf-8";
        }

        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }

        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }

        public void setFile(File file) {
            this.file = file;
        }

        @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(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setDoOutput(true);
                    conn.setDoInput(true);
                    conn.setRequestMethod("POST");
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //저장된 쿠키가 있다면 읽어내서 쿠키도 같이 보내기
                    SQLiteDatabase db=dbHelper.getReadableDatabase();
                    String sql="SELECT cookie FROM board_cookie";
                    //select 된 결과를 Cursor 에 담아온다.
                    Cursor cursor=db.rawQuery(sql, null);
                    while(cursor.moveToNext()){
                        String cookie=cursor.getString(0);
                        conn.addRequestProperty("Cookie", cookie);
                    }

                    //전송하는 데이터에 맞게 값 설정하기
                    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 객체의 참조값이 있기 때문에 필드의 값을 사용하면 된다.
                    String filename=file.getName(); //파일명
                    pw.append("--" + boundary).append(LINE_FEED);
                    pw.append("Content-Disposition: form-data; name=\"" + FILE_PARAM_NAME + "\"; 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);
                        }
                    }else if(responseCode==301 || responseCode==302 || responseCode==303){
                        //리다일렉트 요청할 경로를 얻어내서
                        String location=conn.getHeaderField("Location");
                        //해당 경로로 다시 요청을 해야 한다.
                        conn.disconnect();
                        /*
                            location 은
                            1. http://hostname/xxx/xxx 이런 경우도 있고
                            2. /xxx/xxx  이런 경우도 있다.
                            따라서 2번의 경우를 대비해야 한다.
                         */
                        if(location.startsWith("/")){ //만일 location 이 슬레시(/) 로 시작된다면
                            location=url.getProtocol()+"://"+url.getHost()+location;
                        }
                        //요청 url 을 수정하고
                        requestUrl=location;
                        //doInBackground() 메소드를 다시 호출해서 요청이 다시 되게 한다.
                        doInBackground(maps);
                    }else if(responseCode >= 400 && responseCode < 500){
                        //요청 오류인 경우에 이 요청은 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("잘못된 요청에 의해 작업이 실패 되었습니다.");
                    }else if(responseCode == 500){
                        //서버의 잘못된 동작으로 인한 요청 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("서버의 오류로 인해 작업이 실패 되었습니다. 조속히 복구 하겠습니다.");
                    }
                    //서버가 응답한 쿠키 목록을 읽어온다.
                    List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                    //만일 쿠키가 존대 한다면
                    if(cookList != null){
                        //반복문 돌면서 DB 에 저장한다.
                        //새로 응답된 쿠키라면 insert, 이미 존재하는 쿠키라면 update
                        SQLiteDatabase db2=dbHelper.getWritableDatabase();
                        for(String cookie:cookList){
                            //쿠키의 이름
                            String cookie_name=cookie.split("=")[0];
                            //쿠키의 이름을 String[] 에 담고
                            String[] arg={cookie_name};
                            //해당 쿠키가 이미 존재하는지 select 해 본다.
                            Cursor cursor2=db2.rawQuery("SELECT * FROM board_cookie WHERE cookie_name=?", arg);
                            //select 된 row 의 갯수
                            int selectRow=cursor2.getCount();
                            if(selectRow == 0){//새로운 쿠키이면 저장
                                Object[] args={cookie_name, cookie};
                                db2.execSQL("INSERT INTO board_cookie (cookie_name, cookie) VALUES(?, ?)", args);
                            }else{//이미 존재하는 쿠키이면 수정
                                Object[] args={cookie, cookie_name};
                                db2.execSQL("UPDATE board_cookie SET cookie=? WHERE cookie_name=?", args);
                            }
                        }
                        // .close() 해야지만 실제로 반영된다.
                        db2.close();
                    }
                }
            }catch(Exception e){//예외가 발생하면
                Log.e("FileUploadTask", e.getMessage());
                //예외가 발생한 경우 이 작업은 실패이다.
                errMsg=e.getMessage(); //예외 메세지를 필드에 담고
                this.cancel(true); //이 비동기 작업을 취소 시킨다.
            }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 onCancelled() {
            super.onCancelled();
            //예외 메세지를 Map 에 담아서
            Map<String, Object> map=new HashMap<>();
            //미리 정의된 상수를 key 값으로 해서 예외 메세지를 담는다.
            map.put(ERR_MSG, errMsg);
            //리스너에 전달한다.
            listener.onFail(requestId, map);
        }
        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            listener.onSuccess(requestId, s);
        }
    }


    private class GetRequestTask extends AsyncTask<Map<String, String>, Void, String>{
        //필요한 필드 구성
        private int requestId;
        private String requestUrl;
        private RequestListener listener;
        //에러 메세지를 담을 필드
        private String errMsg;

        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }

        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }

        @Override
        protected String doInBackground(Map<String, String>... maps) {
            //maps 배열의 0 번방에 GET 방식 요청 파라미터가 들어 있다.
            //파라미터가 없으면 null 이 전달될 예정
            Map<String, String> param = maps[0];
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                StringBuffer buffer=new StringBuffer();
                //Map 에 존재하는 key 값을 Set 에 담아오기
                Set<String> keySet=param.keySet();
                Iterator<String> it=keySet.iterator();
                boolean isFirst=true;
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while(it.hasNext()){
                    String key=it.next();
                    String arg=null;
                    //파라미터가 한글일 경우 깨지지 않도록 하기 위해.
                    String encodedValue=null;
                    try {
                        encodedValue= URLEncoder.encode(param.get(key), "utf-8");
                    } catch (UnsupportedEncodingException e) {}
                    if(isFirst){
                        arg="?"+key+"="+encodedValue;
                        isFirst=false;
                    }else{
                        arg="&"+key+"="+encodedValue;
                    }
                    buffer.append(arg);
                }
                String data=buffer.toString();
                //GET 방식 요청 파라미터를 요청 url 뒤에 연결한다.
                requestUrl +=data;
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            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);//케쉬 사용 여부
                    //저장된 쿠키가 있다면 읽어내서 쿠키도 같이 보내기
                    SQLiteDatabase db=dbHelper.getReadableDatabase();
                    String sql="SELECT cookie FROM board_cookie";
                    //select 된 결과를 Cursor 에 담아온다.
                    Cursor cursor=db.rawQuery(sql, null);
                    while(cursor.moveToNext()){
                        String cookie=cursor.getString(0);
                        conn.addRequestProperty("Cookie", cookie);
                    }
                    //응답 코드를 읽어온다. (200, 404, 500 등등의 값)
                    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);
                        }
                    }else if(responseCode==301 || responseCode==302 || responseCode==303){
                        //리다일렉트 요청할 경로를 얻어내서
                        String location=conn.getHeaderField("Location");
                        //해당 경로로 다시 요청을 해야 한다.
                        conn.disconnect();
                        /*
                            location 은
                            1. http://hostname/xxx/xxx 이런 경우도 있고
                            2. /xxx/xxx  이런 경우도 있다.
                            따라서 2번의 경우를 대비해야 한다.
                         */
                        if(location.startsWith("/")){ //만일 location 이 슬레시(/) 로 시작된다면
                            location=url.getProtocol()+"://"+url.getHost()+location;
                        }
                        //요청 url 을 수정하고
                        requestUrl=location;
                        //doInBackground() 메소드를 다시 호출해서 요청이 다시 되게 한다.
                        doInBackground(maps);
                    }else if(responseCode >= 400 && responseCode < 500){
                        //요청 오류인 경우에 이 요청은 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("잘못된 요청에 의해 작업이 실패 되었습니다.");
                    }else if(responseCode == 500){
                        //서버의 잘못된 동작으로 인한 요청 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("서버의 오류로 인해 작업이 실패 되었습니다. 조속히 복구 하겠습니다.");
                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                //만일 쿠키가 존재 한다면
                if(cookList != null){
                    //반복문 돌면서 DB 에 저장한다.
                    //새로 응답된 쿠키라면 insert, 이미 존재하는 쿠키라면 update
                    SQLiteDatabase db=dbHelper.getWritableDatabase();
                    for(String cookie:cookList){
                        //쿠키의 이름
                        String cookie_name=cookie.split("=")[0];
                        //쿠키의 이름을 String[] 에 담고
                        String[] arg={cookie_name};
                        //해당 쿠키가 이미 존재하는지 select 해 본다.
                        Cursor cursor=db.rawQuery("SELECT * FROM board_cookie WHERE cookie_name=?", arg);
                        //select 된 row 의 갯수
                        int selectRow=cursor.getCount();
                        if(selectRow == 0){//새로운 쿠키이면 저장
                            Object[] args={cookie_name, cookie};
                            db.execSQL("INSERT INTO board_cookie (cookie_name, cookie) VALUES(?, ?)", args);
                        }else{//이미 존재하는 쿠키이면 수정
                            Object[] args={cookie, cookie_name};
                            db.execSQL("UPDATE board_cookie SET cookie=? WHERE cookie_name=?", args);
                        }
                    }
                    // .close() 해야지만 실제로 반영된다.
                    db.close();
                }

            }catch(Exception e){
                Log.e("MyHttpUtil.sendGetRequest()", e.getMessage());
                //예외가 발생한 경우 이 작업은 실패이다.
                errMsg=e.getMessage(); //예외 메세지를 필드에 담고
                this.cancel(true); //이 비동기 작업을 취소 시킨다.
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //응답받은 문자열을 리턴한다.
            return builder.toString();
        }
        //비동기 작업이 취소 되면 호출되는 메소드
        @Override
        protected void onCancelled() {
            super.onCancelled();
            //예외 메세지를 Map 에 담아서
            Map<String, Object> map=new HashMap<>();
            //미리 정의된 상수를 key 값으로 해서 예외 메세지를 담는다.
            map.put(ERR_MSG, errMsg);
            //리스너에 전달한다.
            listener.onFail(requestId, map);
        }
        //비동기 작업이 성공 되면 호출되는 메소드
        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            //RequestListener 객체에(액티비티 or 서비스 or 프레그먼트 .. ) 넣어주기
            listener.onSuccess(requestId, s);
        }
    }


    private class PostRequestTask extends AsyncTask<Map<String, String>, Void, String>{
        //필요한 필드 구성
        private int requestId;
        private String requestUrl;
        private RequestListener listener;
        private String errMsg;

        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }

        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }

        @Override
        protected String doInBackground(Map<String, String>... maps) {
            //maps 배열의 0 번방에 GET 방식 요청 파라미터가 들어 있다.
            //파라미터가 없으면 null 이 전달될 예정
            Map<String, String> param = maps[0];
            //query 문자열을 누젓 시킬 StringBuffer
            StringBuffer buffer=new StringBuffer();
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                //Map 에 존재하는 key 값을 Set 에 담아오기
                Set<String> keySet=param.keySet();
                Iterator<String> it=keySet.iterator();
                boolean isFirst=true;
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while(it.hasNext()){
                    String key=it.next();
                    String arg=null;
                    //파라미터가 한글일 경우 깨지지 않도록 하기 위해.
                    String encodedValue=null;
                    try {
                        encodedValue= URLEncoder.encode(param.get(key), "utf-8");
                    } catch (UnsupportedEncodingException e) {}
                    if(isFirst){
                        arg=key+"="+encodedValue;
                        isFirst=false;
                    }else{
                        arg="&"+key+"="+encodedValue;
                    }
                    //query 문자열을 StringBuffer 에 누적 시키기
                    buffer.append(arg);
                }
            }
            //post 방식으로 전송할때 사용할 query문자열
            String queryString=buffer.toString();

            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            PrintWriter pw=null;
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setRequestMethod("POST");//POST 방식
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //저장된 쿠키가 있다면 읽어내서 쿠키도 같이 보내기
                    SQLiteDatabase db=dbHelper.getReadableDatabase();
                    String sql="SELECT cookie FROM board_cookie";
                    //select 된 결과를 Cursor 에 담아온다.
                    Cursor cursor=db.rawQuery(sql, null);
                    while(cursor.moveToNext()){
                        String cookie=cursor.getString(0);
                        conn.addRequestProperty("Cookie", cookie);
                    }
                    //전송하는 데이터에 맞게 값 설정하기
                    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); //폼전송과 동일
                    //출력할 스트림 객체 얻어오기
                    OutputStreamWriter osw=
                            new OutputStreamWriter(conn.getOutputStream());
                    //문자열을 바로 출력하기 위해 osw 객체를 PrintWriter 객체로 감싼다
                    pw=new PrintWriter(osw);
                    //서버로 출력하기
                    pw.write(queryString);
                    pw.flush();

                    //응답 코드를 읽어온다. (200, 404, 500 등등의 값)
                    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);
                        }
                    }else if(responseCode==301 || responseCode==302 || responseCode==303){
                        //리다일렉트 요청할 경로를 얻어내서
                        String location=conn.getHeaderField("Location");
                        //해당 경로로 다시 요청을 해야 한다.
                        conn.disconnect();
                        /*
                            location 은
                            1. http://hostname/xxx/xxx 이런 경우도 있고
                            2. /xxx/xxx  이런 경우도 있다.
                            따라서 2번의 경우를 대비해야 한다.
                         */
                        if(location.startsWith("/")){ //만일 location 이 슬레시(/) 로 시작된다면
                            location=url.getProtocol()+"://"+url.getHost()+location;
                        }
                        //요청 url 을 수정하고
                        requestUrl=location;
                        //doInBackground() 메소드를 다시 호출해서 요청이 다시 되게 한다.
                        doInBackground(maps);
                    }else if(responseCode >= 400 && responseCode < 500){
                        //요청 오류인 경우에 이 요청은 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("잘못된 요청에 의해 작업이 실패 되었습니다.");
                    }else if(responseCode == 500){
                        //서버의 잘못된 동작으로 인한 요청 실패!

                        //예외 발생 시키기
                        throw new RuntimeException("서버의 오류로 인해 작업이 실패 되었습니다. 조속히 복구 하겠습니다.");
                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                List<String> cookList=conn.getHeaderFields().get("Set-Cookie");
                //만일 쿠키가 존재 한다면
                if(cookList != null){
                    //반복문 돌면서 DB 에 저장한다.
                    //새로 응답된 쿠키라면 insert, 이미 존재하는 쿠키라면 update
                    SQLiteDatabase db=dbHelper.getWritableDatabase();
                    for(String cookie:cookList){
                        //쿠키의 이름
                        String cookie_name=cookie.split("=")[0];
                        //쿠키의 이름을 String[] 에 담고
                        String[] arg={cookie_name};
                        //해당 쿠키가 이미 존재하는지 select 해 본다.
                        Cursor cursor=db.rawQuery("SELECT * FROM board_cookie WHERE cookie_name=?", arg);
                        //select 된 row 의 갯수
                        int selectRow=cursor.getCount();
                        if(selectRow == 0){//새로운 쿠키이면 저장
                            Object[] args={cookie_name, cookie};
                            db.execSQL("INSERT INTO board_cookie (cookie_name, cookie) VALUES(?, ?)", args);
                        }else{//이미 존재하는 쿠키이면 수정
                            Object[] args={cookie, cookie_name};
                            db.execSQL("UPDATE board_cookie SET cookie=? WHERE cookie_name=?", args);
                        }
                    }
                    // .close() 해야지만 실제로 반영된다.
                    db.close();
                }

            }catch(Exception e){
                Log.e("MyHttpUtil.sendGetRequest()", e.getMessage());
                //예외가 발생한 경우 이 작업은 실패이다.
                errMsg=e.getMessage(); //예외 메세지를 필드에 담고
                this.cancel(true); //이 비동기 작업을 취소 시킨다.
            }finally {
                try{
                    if(pw!=null)pw.close();
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //응답받은 문자열을 리턴한다.
            return builder.toString();
        }
        //비동기 작업이 취소 되면 호출되는 메소드
        @Override
        protected void onCancelled() {
            super.onCancelled();
            //예외 메세지를 Map 에 담아서
            Map<String, Object> map=new HashMap<>();
            //미리 정의된 상수를 key 값으로 해서 예외 메세지를 담는다.
            map.put(ERR_MSG, errMsg);
            //리스너에 전달한다.
            listener.onFail(requestId, map);
        }
        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            //RequestListener 객체에(액티비티 or 서비스 or 프레그먼트 .. ) 넣어주기
            listener.onSuccess(requestId, s);
        }
    }
}

 

- 300번대 리다이렉트 요청이 필요

 

 

- Url을 수정한 다음에 다시 호출하면 된다.

- 최초 URL이 아닌 다른 URL로 요청해야 한다.

 

 

1) http://hostname/xxx/xxx

2) /xxx/xxx

 

- 리다이렉트 경로는 1로 시작할 때도 있고 2로 시작될 때도 있는데,

 2번 형태의 문자열이 여기로 바로 들어가면 new URL() 요청이 잘못된 요청이 될 수가 있다.

- 따라서 경로 앞에 문자열을 새롭게 구성해주어야 한다.

 

- / 로 시작하는 경우와 시작하지 않는 경우를 구분

 

 

- 이런 형태로 문자열을 만들어주면 된다!

 

 

- Spring boot에 리다이렉트 테스트용 test 메소드 만들어보기

 

 

- 이 경로를 요청하면 자동으로 /gallery/list 로 리다이렉트된다.

 

 

- 이렇게 작성해도 정상적으로 들어간다.

 


 

 

- 키 값이 이렇게 저장되어 있는 상태인데,

 키 값을 오타를 낼 수도 있으므로.. 키 값을 상수로 가지고있도록 해준다.

 

 

- 공개된 상수 ERR_MSG 를 하나 만들어두고, 상수를 활용하는 구조로 바꾼다.

 

 

- MyHttpUtil 안에서도 활용하고, 

 

 

- 유틸리티를 사용하는 액티비티에서도 이렇게 사용 가능!

 

 

- PostRequestTask 에도 상수 추가해주기

 

 

- 필요한 필드, 상수값 추가

 

 

- FileUploadTask에서도 응답 코드를 사용해서 에러를 응답하고

 비동기 task를 캔슬하도록 onCancelled() 메소드를 넣어준다.

 

 

- 그러면 모든 메소드에서 예외 처리가 가능하다.

 

 

- 오류 발생시 토스트메시지와 에러정보가 출력된다.

 


 

DetailActivity는 삭제, 로그인체크 2가지

package com.example.step25imagecapture;

import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.bumptech.glide.Glide;
import com.example.step25imagecapture.databinding.ActivityDetailBinding;
import com.example.step25imagecapture.util.MyHttpUtil;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;

public class DetailActivity extends AppCompatActivity implements MyHttpUtil.RequestListener {

    ActivityDetailBinding binding;

    GalleryDto dto;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_detail);
        binding=ActivityDetailBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        //DetailActivity가 활성화될 때 전달받은 Intent 객체의 참조값 얻어오기
        //GalleryListActivity 에서 생성한 Intent 객체이기 때문에 "dto" 라는 키값으로 GalleryDto 객체가 들어있다.
        Intent intent=getIntent();
        dto=(GalleryDto)intent.getSerializableExtra("dto");

        //이미지 출력(Glide 활용)
        Glide.with(this)
                .load(dto.getImagePath())
                .centerCrop()
                .placeholder(R.drawable.ic_launcher_background)
                .into(binding.imageView);
        //세부 정보 출력
        binding.writer.setText("writer:"+dto.getWriter());
        binding.caption.setText(dto.getCaption());
        binding.regdate.setText(dto.getRegdate());

        //삭제 버튼에 리스너 등록
        binding.deleteBtn.setOnClickListener(v -> {
            new AlertDialog.Builder(this)
                    .setMessage("삭제 하시겠습니까?")
                    .setPositiveButton("네", (dialog, which) -> {
                        //삭제할 갤러리 사진의 Primary key를 이용해서 삭제 작업을 진행한다.
                        int num=dto.getNum();

                        Map<String, String> map=new HashMap<>();
                        //삭제할 번호를 Map에 담는다.
                        map.put("num", Integer.toString(num));
                        //삭제 요청하기
                        new MyHttpUtil(this).sendPostRequest(2,
                                AppConstants.BASE_URL+"/api/gallery/delete", map,this);

                    })
                    .setNegativeButton("아니요", null)
                    .create()
                    .show();
        });
    }

    @Override
    protected void onStart() {
        super.onStart();
        //로그인 체크
        new MyHttpUtil(this).sendGetRequest(1,
                AppConstants.BASE_URL+"/music/logincheck", null, this);
    }

    @Override
    public void onSuccess(int requestId, String data) {
        switch (requestId){
            case 1: //로그인 체크의 결과
                try {
                    //json문자열을 이용해서 JSONObject 객체를 생성한다.
                    JSONObject obj=new JSONObject(data);
                    //로그인 여부
                    boolean isLogin=obj.getBoolean("isLogin");
                    if(isLogin){
                        //로그인된 아이디를 읽어와서
                        String id=obj.getString("id");
                        //갤러리 writer와 비교해서 같으면 삭제 버튼을 보이게 한다.
                        if(id.equals(dto.getWriter())){
                            //삭제 버튼 보이게하기
                            binding.deleteBtn.setVisibility(View.VISIBLE);
                        }

                    }
                }catch (JSONException je){
                    Log.e("onPostExecute", je.getMessage());
                }
                break;
            case 2: //삭제 요청에 대한 결과
                //data는 {"isSuccess",true} 형식의 json 문자열이다. 필요하다면 여기서 사용하면 된다.

                //DetailActivity를 종료시켜서 GalleryListActivity가 다시 활성화되도록 한다.
                finish();
                break;
        }

    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {
        //에러 메시지를 읽어와서
        String errMsg=(String)result.get(MyHttpUtil.ERR_MSG);
        switch (requestId){
            case 1:
                new AlertDialog.Builder(this)
                        .setTitle("로그인 체크")
                        .setMessage(errMsg)
                        .setNeutralButton("확인", null)
                        .create()
                        .show();
                break;
            case 2:
                new AlertDialog.Builder(this)
                        .setTitle("Gallery 삭제 에러")
                        .setMessage(errMsg)
                        .setNeutralButton("확인", null)
                        .create()
                        .show();
                break;
        }
    }
}

 

 

 

- onFail() 에 코드별로 오류를 넣어줌

- Utility를 사용하면서 실패하는 상황에 대한 에러 대응이 가능해진다.

 

 

- 삭제시 창을 띄우도록 설정해주었다.