국비교육(22-23)

104일차(1)/Android App(69) : 모바일 갤러리 기능 구현(3)

서리/Seori 2023. 3. 9. 23:29

104일차(1)/Android App(69) : 모바일 갤러리 기능 구현(3)

 

 

- 클릭시 동작할 리스너를 등록했다.

- 람다식으로 작성한 것. 리스너에 override할 메소드가 하나인 경우에만 사용할 수 있다!

- 람다식으로 쓰면 (parent, view, position, id) 형태로 인자의 타입을 생략하여 쓸 수 있다.

 

 

- 원래대로 쓰면 이런 구조가 된다.

- 익명의 이너클래스를 이용해 인터페이스를 구현한 것

- parent, view, position, id 4개의 인자가 전달되는 메소드가 오직 한개일 때에만! 사용할 수 있다.

 

- 리스트의 특정 아이템을 클릭하면 이 메소드 안으로 실행 순서가 들어온다.

- putExtra로 dto를 담아서 intent와 함께 전달한다.

 

 

 

- Serializable은 이렇게 비어 있는 인터페이스이다. 오버라이드할 메소드도 없다. 오로지 타입을 위한 인터페이스!

- 객체를 직렬화할때 사용한다.

 

 

- 찾아보면 HashMap, ArrayList, Random 등에도 구현되어 있다.

- java의 대부분의 클래스에 구현되어 있다.

- 즉 intent에 다양한, 대부분의 타입은 다 담을 수 있다는 것!

 

- Detail Activity에서 이 Intent를 받아와서 사용한다.

 


 

activity_detail.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_margin="10dp"
    tools:context=".DetailActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="0dp"
        android:layout_height="300dp"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <TextView
        android:id="@+id/writer"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:text="작성자:admin"
        android:textSize="20sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/imageView" />

    <TextView
        android:id="@+id/caption"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="어쩌구 저쩌구..."
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/writer" />

    <TextView
        android:id="@+id/regdate"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="2023.03.07 13:00"
        android:textSize="20sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/caption" />

    <Button
        android:id="@+id/deleteBtn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="245dp"
        android:text="삭제"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/regdate"
        android:visibility="invisible"/>

</androidx.constraintlayout.widget.ConstraintLayout>

 

 

- 이런 형태로 디자인을 대략 잡아준다.

 

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

public class DetailActivity extends AppCompatActivity {

    ActivityDetailBinding binding;
    SharedPreferences pref;
    String sessionId;
    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());

        pref= PreferenceManager.getDefaultSharedPreferences(this);
        sessionId=pref.getString("sessionId", "");

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

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

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

        new LoginCheckTask().execute(AppConstants.BASE_URL+"/music/logincheck");
    }

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

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

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

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

            }catch(Exception e){//예외가 발생하면
                Log.e("LoginCheckTask", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //서버에서 응답받은 문자열을 리턴한다.
            return builder.toString();
        }

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

            try {
                //json문자열을 이용해서 JSONObject 객체를 생성한다.
                JSONObject obj=new JSONObject(jsonStr);
                //로그인 여부
                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());
            }
        }
    }
}

 

 

- Gradle에 뷰 바인딩 설정을 해놓으면 자동으로 이런 이름의 클래스가 구성된다.

- 사용해서 참조값을 얻어오기!

 

 

getLayoutInflater()

- 부모가 이 메소드를 가지고 있다. 메소드에서 LayoutInflater 타입을 리턴해준다.

- 화면구성은 binding 객체를 활용해서 한다.

 

 

- 저장한 키값으로 intent 에 저장된 내용 가져오기

- 액티비티에서 Glide를 사용한다.

 

 

- Glide.load() 에 문자열을 전달하면 알아서 로딩한다.

 (단 이것이 동작하려면 AndroidManifest에 인터넷 설정이 있어야 한다!! 주의!!)

- .centerCrop() 가운데 딱 들어맞도록 설정

- .into() 안에는 이미지뷰의 참조값 넣어주기(바인딩으로 참조값을 구한다)

 

 

- binding으로 각각의 TextView 안에 값을 넣어준다.

 

 

- 클릭시 크게 보이는 Detail Activity를 구현했다.

 


 

* 삭제 버튼 만들기

 

<Button
    android:id="@+id/deleteBtn"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginBottom="245dp"
    android:text="삭제"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/regdate" />

 

 

- 이 버튼은 본인의 사진 Detail 페이지에 들어갔을 때에만 출력되도록 하기.

 

 

- activity_detail.xml 파일에서 가시성 여부에 대한 설정값을 지정해줄 수 있다.

 

- gone : 화면에 공간은 차지하지만 안보이는 것

- invisible : 자리를 차지하지 않으면서 안보이는 것

 

 

 

- xml파일에서 옵션을 지정한 후에, 이 버튼을 어떤 조건에서만 보이게 할지 여부를 액티비티에서 코딩으로 조정할 수 있다.

 

 

- 보통 이런 값들은 View 클래스 안에 상수값으로 정의되어 있다.

 

- 이 코드를 조건부로 넣어주면 된다!

 

 

- id라는 키값으로 로그인한 아이디를 받아오고, isLogin 으로 로그인상태 관리

 

- 여러 곳에서 중복되는, 반복적으로 쓰이는 메소드가 있다면, Utility로 만들어버리는 것이 편하다.

- ctrl+c,v 해서 매번 쓰는것보다는 필요할때 import해서 사용할 수 있도록!

- 로그인 체크용으로 따로 유틸 만들기

 

 

- MainActivity의 logincheckTask 를 복사해서 가져와주고

 

 

- onCreate 안에 pref, sessionId를 담아주는 코드를 추가.

 

- 복사해온 메소드 Async의 결과 타입을 String으로 바꿔주었다.

 

로그인체크해서 true이면

아이디를 읽어내서 동작하기

 

 

 

 

다른 메소드안에서 사용하기위해 갤러리DTo를필드로 만들어주었다.

 

 

 

- onStart() 메소드를 오버라이드해서 LoginCheckTask 써주기

 

 

- 내가 올린 게시물에서만 삭제 버튼이 나타난다.

 


 

 

- 삭제버튼에 리스너 등록하기

 

 

- 람다식으로 쓸 수 있다.

 

 

- 이 안에는 사진 정보를 서버 DB에서 삭제하는 작업이 들어가야 하는데 코드가 길고 복잡하다... 

- 유틸리티로 만들어볼 예정

 


 

 

- java 하위패키지 생성- 새 클래스 생성

 

MyHttpUtil (미완성)

package com.example.step25imagecapture.util;

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

import androidx.annotation.Nullable;

import java.util.Map;

/*
    - 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);
    }

    private class GetRequestTask 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) {
            return null;
        }

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

 

- 세션아이디만 쿠키로 보내면 된다.

- 응답한 다른 쿠키는 읽어오지 않는다.

 

- 쿠키를 SQLiteDataBase를 활용해서 자체 관리할 것!

 

 

- 클래스를 만들어주고 상속하면서 생성자, 메소드 2개를 override 한다.

 

 

- 이렇게 쿠키이름쿠키 문자열을 따로 관리한다. key:value 값으로 관리된다.

- 반복문을 집어넣어서 돌릴 것!

 

 

 

- 필드와 생성자를 만들어주고, DBHelper 객체를 사용한다.

- 버전은 1로 고정

 

 

- 사용하는 액티비티에서 new MyHttpUtil(this) 로 작성해주면

  생성자의 인자로 받는 Context에 Activity의 참조값이 전달되어서 DBhelper를 사용할 준비가 된다.

 

 

getReadableDataBase()

getWritableDataBase()

- 이 메소드를 사용해서 SQLiteDataBase 객체를 리턴한다.

 

 

- 비동기 작업이므로 성공했을 때 데이터를 넣어주고 실패했을 때는 실패 정보를 전달하도록!

- 인터페이스 안에 전달

 

 

- 이 안에 JSON 문자열이 들어오도록 한다.

 

 

- GET 방식 요청을 하는 메소드

- id, url을 받고 파라미터가 있다면 받아주고, 위에서 만든 리스너를 인자로 받아준다.

 

 

- 아래 GetRequestTask 에서 한 작업의 결과값을 리스너를 통해서 통보할 것이다.

 

 

- 이 안에서 사용할 수 있도록 각각의 필드를 선언

- generate를 사용해서 setter 메소드를 만들어준다.

 

 

- setRequestId() 등 3개의 메소드를 사용해서 task 안에 값을 넣어준다.

- 인자를 넣어주어서 sendGetRequest 메소드 완성해주기

 

 

- Activity에서 get방식 요청을 하려면 이렇게 하면 된다. 작업을 좀더 간편하게 처리하기 위한 것!

- listener 인자에 this가 들어오도록 하게 하려면 Activity에 리스너를 구현해주어야 한다.

 

(유틸리티는 아직 미완성. 이어서 만들 예정...)