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를 사용하면서 실패하는 상황에 대한 에러 대응이 가능해진다.
- 삭제시 창을 띄우도록 설정해주었다.
'국비교육(22-23)' 카테고리의 다른 글
110일차(1)/CSS(9) : 3차원 컨텐츠 만들기(2) (1) | 2023.03.20 |
---|---|
109일차(2)/CSS(8) : 3차원 컨텐츠 만들기(1) (0) | 2023.03.17 |
108일차(1)/Android App(72) : 모바일 갤러리 기능 구현(6) (0) | 2023.03.16 |
107일차(1)/Android App(71) : 모바일 갤러리 기능 구현(5) (0) | 2023.03.15 |
105일차(1)/Android App(70) : 모바일 갤러리 기능 구현(4) (0) | 2023.03.11 |