국비교육(22-23)

108일차(1)/Android App(72) : 모바일 갤러리 기능 구현(6)

서리/Seori 2023. 3. 16. 01:36

108일차(1)/Android App(72) : 모바일 갤러리 기능 구현(6)

 

 

- 유틸리티로 이미지를 찍어서 원격지 서버에 업로드한다.

- 이 유틸리티는 java 코드이므로, 안드로이드 말고도 다른 이클립스 등에서도 사용할 수 있다.

 

 

- MyHttpUtil의 이 메소드를 사용해서 디바이스 안의 특정파일을 서버에 업로드!

- 추가로 업로드할 파일 객체의 참조값을 넣어주면 된다.

 

 

- 윈도우 안의 파일이라면 이런 경로를 넣어줄 것이고, 안드로이드의 폴더 경로를 전달해주면 업로드 해준다.

 

- 첫 화면은 GalleryListActivity, 사진찍기 클릭시 MainActivity 활성화

 

 

- 사진을 찍으면 이 파일 객체를 사용해서 사진을 저장해준다.

 

- onActivityResult에서 이미지를 이미지뷰에 출력해주고,

 

 

- 업로드버튼을 누르면 캡션에 문자열이 담기고 map에 캡션을 담아서 그 map을 MyHttpUtil에 전달한다.

 


 

- GalleryListActivity 도 유틸리티를 사용하는 구조로 변경

 

GalleryListActivity

package com.example.step25imagecapture;

import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.View;

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.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
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();
    }
}

 

 

- 이 2가지를 수정해줄 예정!

 

- 액티비티에 리스너를 구현하고 메소드 오버라이드.

 

 

- 요청 파라미터는 없으므로 null, 액티비티가 리스너 역할을 하도록 함

- 위에 있는 작업을 이렇게 바꿔줄 수 있다.

- Refresh 버튼 안에서도 이 작업을 해준다.

 

 

- 여기서는 요청이 하나밖에 없기 때문에, 요청의 번호(requestId)를 다르게 해서 결과를 구분할 필요는 없다!

 

 

 

- onPostExecute() 안에있는 작업을 onSuccess() 로 옮겨주기만 하면 된다.

- 변수명만 jsonStr을 data로 바꾸어 줌!

 

 

 

- 진행중 알림을 띄우는 Progress Dialog 는 액티비티가 가지도록 한다.

- 이 Progress는 요청을 하고 나서 띄우면 된다.

 

 

- 필드로 선언만 하고, onCreate 안에서 참조값을 넣어 구성해준다.

 

 

 

- 구성은 미리 해놓았다가 요청한 시점에 progress를 띄워주면 된다.

 

 

- 작업이 성공했을 때, 실패했을 때 모두 취소하는 코드를 넣어준다.

 

 

- pref, sessionId 관련은 모두 삭제 가능! (유틸리티 안에서 처리)

 

- 그러면 아래의 GalleryListTask를 전부 삭제 가능하다.

- 이렇게 유틸리티를 만들어두면 복잡한 로직 없이 코딩에만 집중할 수 있다는 장점이 있다.

 


 

- DetailActivity에서도 수정 가능하다. 로그인 여부를 확인해서 삭제버튼을 보이게 할지 말지

- LoginCheckTask를 수정

 

DetailActivity

package com.example.step25imagecapture;

import android.content.Intent;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Bundle;
import android.preference.PreferenceManager;
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.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
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("errMsg");
        switch (requestId){
            case 1:
                break;
            case 2:
                break;
        }
    }
}

 

 

- isLogin의 값이 true인지, false인지 여부에 따라 switch로 분기한다.

 

 

- 아래 메소드에서 가져와서 jsonStr대신 이 메소드로 들어오는 인자 data 값을 넣어주면 된다.

- 그러면 아래 로그인 체크 메소드를 전체 삭제가능!

 


 

 

- 삭제요청을 하면 삭제 확인을 위한 알림창이 뜨는데,

 Positive 버튼에 리스너 함수를 걸어두었다. 예를 클릭하면 리스너 함수가 호출된다.

- 이 삭제요청도  MyHttpUtil로 수정해볼 예정!

 

- 이 삭제요청은 Post 요청이다. 요청id는 2번으로 하기

- 경로를 /api/gallery/delete 로 요청한다.

 

 

- 이렇게 람다식으로 쓰면 this가 리스너를 가리키지 않고 액티비티를 가리키게 된다.

- 람다식으로 작성하면 밖으로 빠져나간다! DetailActivity의 참조값을 가리키므로 DetailActivity.this 할 필요가 없다.

 

 

- 삭제할 번호를 담아준다. 문자열을 숫자로 바꾸어 담아준다.(toString 메소드 사용)

- 그러면 "num" 이라는 파라미터명으로 MyHttpUtil에 map에 담긴 숫자(문자형태)가 전달된다.

 

 

- 이미지를 삭제하는 경우를 case2 로 분기!

 

 

- 이 요청을 처리할 갤러리의 서버 측 메소드가 필요하다.

- Spring Boot 에서 GalleryDao에 메소드를 추가한다.

 

GalleryDao

//갤러리 사진 정보 삭제
public void delete(int num);

 

GalleryDaoImpl

@Override
public void delete(int num){
	session.delete("gallery.delete", num);
};

 

- 이렇게 작성하면 parameter type: int, sql id: delete가 된다.

 

GalleryMapper

<delete id="delete" parameterType="int">
	DELETE FROM board_gallery
	WHERE num=#{num}
</delete>

 

- mapper에 삭제 SQL문 추가

 

 

GalleryService

//갤러리 사진 삭제 및 DB에서도 삭제하기
public void deleteGallery(HttpServletRequest request, int num);

 

GalleryServiceImpl

@Override
public void deleteGallery(HttpServletRequest request, int num){
    //삭제할 Gallery 사진 정보를 읽어와서
    GalleryDto dto=dao.getData(num);
    //DB에서 삭제
    dao.delete(num);
    //파일 시스템에서 삭제 (upload폴더) 
    //webapp 폴더까지의 실제 경로
    String realPath=request.getServletContext().getRealPath("/resources/upload");
    //webapp 폴더까지의 실제 경로 + /resources/upload/xxx.jpg
    String imagePath=realPath.separator+dto.getImagePath();
    //삭제할 File 객체 생성
    File file=new File(imagePath);
    file.delete();
};

 

 

- DB, 파일시스템에서 모두 삭제해주는 코드 작성!

- 파일시스템에서 삭제하려면 실제 경로를 추출해낸 후 File객체를 사용해 삭제하면 된다.

 

 

GalleryController

@PostMapping("/api/gallery/delete")
@ResponseBody
public Map<String, Object> apiDelete(HttpServletRequest request, int num){
    service.deleteGallery(request,num);

    Map<String, Object> map=new HashMap<>()
    map.put("isSuccess",true);
    return map;
}

 

 

- Controller에서 POST방식으로 전송 (PostMapping)

- {"isSuccess",true} 라는 json문자열이 ResponseBody 로 응답된다.

 

- 2번 요청에 대해 리스너가 응답하도록 하기

 

 

- 안드로이드에서 수정

- 위에서 리턴한 map을 받아오도록 한다!

 

 

- 삭제(case 2)한 후에는 현재 켜져 있는 DetailActivity를 종료시키면 된다.

 

 

- 위와 같이 알림창이 나오고 예를 누르면 삭제된다.

 


 

- 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 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;

        //생성자
        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);
                        }
                    }
                    //서버가 응답한 쿠키 목록을 읽어온다.
                    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("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);
            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");
                        //해당 경로로 다시 요청을 해야 한다.

                    }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<>();
            map.put("errMsg", 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;

        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");
                        //해당 경로로 다시 요청을 해야 한다.

                    }else if(responseCode >= 400 && responseCode < 500){
                        //요청 오류인 경우에 이 요청은 실패!

                    }else if(responseCode == 500){
                        //서버의 잘못된 동작으로 인한 요청 실패!

                    }
                }
                //서버가 응답한 쿠키 목록을 읽어온다.
                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());
            }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 onPostExecute(String s) {
            super.onPostExecute(s);
            //RequestListener 객체에(액티비티 or 서비스 or 프레그먼트 .. ) 넣어주기
            listener.onSuccess(requestId, s);
        }
    }
}

 

 

- GetRequestTask 에 필드 추가. 에러메세지 필드 생성!

 

 

- 실패시의 에러메시지를 담아주고 예외를 발생시킬 예정!

 

- 301은 리다이렉트 요청을 다시 해주면 된다.

 

 

- 위에서 발생시킨 new RuntimeException 이 이 catch문으로 들어온다.

- 예외가 발생하면 실행의 흐름이 아래의 Catch문으로 뛴다!

 

 

this.cancel(true);

- 비동기작업을 여기서 멈추도록 한다. 그러면 onPostExecute() 는 실행되지 않는다.

 

 

- 실패했을 때 호출될 메소드 onCancelled 오버라이드!

- 안에서 map객체를 생성해 errMsg를 담아준다.

 

- requestTask에서 예외를 다루는 방식 참고!

 

 

DetailActivity

- 이 유틸리티를 활용하는 입장에서 fail인 상황의 값이 onFail 로 들어온다.

- 이제 어떤 요청에 대해서 어떤 오류가 났는지 onFail에서 알 수 있다.

 

 

- 어떤 에러냐에 따라 switch문으로 분기해서 해당 case 블록 안에서 작업해주면 된다.