본문 바로가기

카테고리 없음

78 안드로이드 상세보기(JSON Parsing, 파일업로드), 회원관리(롬복,하이버네이트)

** 안드로이드 상세보기

=> itemid를 넘겨받아서 detail에 요청을 전송해서 상세보기를 구현(itemid를 1로 가정)

=> image도 다운로드 받아서 구현

=> 웹에서 텍스트와 이미지를 다운로드 받아서 출력하는 경우에 하나의 스레드에서 구현할 수 있지만 대부분의 경우는 일단 텍스트를 먼저 다운로드 받아서 출력을 하고 이미지는 별도의 스레드에서 다운로드 받아서 출력하는 것을 권장

 

=> 안드로이드에서 서버와 연동할 때 주의할 점 

1. INTERNET 권한을 부여해야 한다.

2. Http 서버와 연동할 때 별도의 설정이 필요하다.

3. Android는 네트워크 사용하는 코드는 반드시 스레드에 작성되어야 한다.

4. Android에서는 메인 스레드를 제외하고는 출력하는 코드를 작성할 수 없다. 스레드를 사용해서 데이터를 다운로드 받은 후 UI를 갱신할 때는 Handler를 이용해야 한다.

=> Thread와 Handler로 나누어서 작성하는 것이 싫으면 AsyncTask를 사용하면 된다.

AsyncTask는 메소드로 Thread의 기능과 Handelr의 기능을 분할해 놓은 클래스이다.

 

** 상세보기 구현

1.Server에 img 디렉토리가 있는지 확인하고 없으면 img 디렉토리를 생성하고 기본 이미지를 삽입

 

2.Android Project에 실행 가능한 Activity 추가(ItemDetailActivity)

 

3.레이아웃 파일에 화면 디자인

=>상단에 itemname을 출력

=>설명:description 을 출력

=>가격:price 를 출력

=>pictureurl에 해당하는 이미지를 출력

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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="wrap_content"
    tools:context=".ItemDetailActivity"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="32sp"
        android:id="@+id/lblitemname"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="설명"
            android:textSize="32dp"/>
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:textSize="32dp"
            android:id="@+id/lbldescription"/>

    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:text="가격"
            android:textSize="32dp"/>
        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="3"
            android:textSize="32dp"
            android:id="@+id/lblprice"/>

    </LinearLayout>

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/imgpictureurl"/>

</LinearLayout>

 

4.DetailActivity.java 파일 작성

 

package com.example.androidportfolio;

import androidx.appcompat.app.AppCompatActivity;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.widget.ImageView;
import android.widget.TextView;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

public class ItemDetailActivity extends AppCompatActivity {
    TextView lblitemname, lbldescription, lblprice;
    ImageView imgpictureurl;

    //텍스트 데이터를 웹에서 다운로드 받아서 출력
    //다운로드 -> 파싱 -> 출력의 과정을 거친다.

    //텍스트 데이터를 출력할 핸들러
    Handler textHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message message){
            //넘어온 데이터 찾아오기
            Map<String,Object> map = (Map<String,Object>)message.obj;
            //데이터 출력하기
            lblitemname.setText((String)map.get("itemname"));
            lblprice.setText((Integer)map.get("price") +"");
            lbldescription.setText((String)map.get("description"));
            //이미지 파일명을 ImageThread에게 넘겨서 출력
            new ImageThread((String)map.get("pictureurl")).start();
        }
    };

    //텍스트 데이터를 가져올 스레드 클래스
    class TextThread extends Thread{
        StringBuilder sb = new StringBuilder();
        @Override
        public void run(){
            //텍스트 데이터 다운로드
            try{
                URL url = new URL("http://192.168.0.200:8080/mysqlserver/detail?itemid=" + 1);
                //Connection 객체 만들기
                HttpURLConnection con = (HttpURLConnection)url.openConnection();
                //옵션 설정
                con.setUseCaches(false);
                con.setConnectTimeout(30000);

                //스트림 객체 생성
                BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
                //문자열 읽기
                while(true){
                    String line = br.readLine();
                    if(line == null){
                        break;
                    }
                    sb.append(line + "\n");
                }
                br.close();
                con.disconnect();
            }catch(Exception e){
                //이 메시지가 보이면 서버가 구동 중인지 확인하고 URL은 제대로 입력했는지 확인
                Log.e("다운로드 에러", e.getMessage());
            }
            Log.e("다운로드 받은 문자열", sb.toString());

            try{
                //다운로드 받은 문자열에서 필요한 데이터 추출하기
                JSONObject object = new JSONObject(sb.toString());
                JSONObject item = object.getJSONObject("item");
                String itemname = item.getString("itemname");
                int price = item.getInt("price");
                String description = item.getString("description");
                String pictureurl = item.getString("pictureurl");

                //4개의 데이터를 하나로 묶기
                Map<String, Object> map = new HashMap<>();
                map.put("itemname", itemname);
                map.put("price", price);
                map.put("description", description);
                map.put("pictureurl", pictureurl);

                //핸들러에게 데이터를 전송하고 호출
                Message message = new Message();
                message.obj = map;
                textHandler.sendMessage(message);

            }catch(Exception e){
                Log.e("파싱 에러", e.getMessage());
            }
        }
    }

    //이미지 출력을 위한 핸들러
    Handler imageHandler = new Handler(Looper.getMainLooper()){
        @Override
        public void handleMessage(Message message){
            //스레드가 전달해준 데이터를 이미지 뷰에 출력
            Bitmap bitmap = (Bitmap)message.obj;
            imgpictureurl.setImageBitmap(bitmap);
        }
    };

    //이미지 다운로드를 위한 스레드 클래스
    class ImageThread extends Thread {
        String pictureurl;
        public ImageThread(String pictureurl){
            this.pictureurl = pictureurl;
        }
        @Override
        public void run() {
            //이미지 다운로드 받는 코드
            try{
                URL url = new URL("http://192.168.0.200:8080/mysqlserver/img/" + pictureurl);
                HttpURLConnection con = (HttpURLConnection)url.openConnection();
                con.setUseCaches(false);
                con.setConnectTimeout(30000);

                //바로 출력
                InputStream is = url.openStream();
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                //Message에 저장
                Message message = new Message();
                message.obj = bitmap;
                imageHandler.sendMessage(message);

            }catch (Exception e){
                Log.e("이미지 다운로드 실패", e.getMessage());
            }
        }
    }

    @Override
    public void onResume(){
        super.onResume();
        new TextThread().start();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_item_detail);

        //디자인 한 뷰 찾아오기
        lblitemname = (TextView)findViewById(R.id.lblitemname);
        lbldescription = (TextView)findViewById(R.id.lbldescription);
        lblprice = (TextView)findViewById(R.id.lblprice);

        imgpictureurl = (ImageView)findViewById(R.id.imgpictureurl);
    }
}

 

 

JSON Parsing

=> JSON : 자바스크립트 데이터 표현방식으로 데이터를 표현

=> [ ] : Array, { } : Map

Array(List) : 인덱스를 이용해서 데이터를 조회 - 길이를 알아야 한다.

Map : Key(Attribute)를 이용해서 데이터를 조회

 

=> Java에서는 Parsing 해주는 라이브러리를 추가해서 파싱

=> Android에서는 별도의 라이브러리 없이 파싱 가능

=> JavaScript에서도 별도의 라이브러리 추가 없이 파싱 가능

 

 

**HttpURLConnnection 을 이용한 post 전송

=> 전송하는 방법이 어려워서 별도의 라이브러리를 다운로드 받아서 사용하기도 한다.

=> 다른 라이브러리를 사용하는 것은 라이브러리에 종속이 되고 스마트폰 프로그래밍에서는 조심해서 사용해야 한다.

=> 스마트폰 프로그래밍은 마켓에서 심사를 한다는 것이다.

라이브러리가 위험한 코드를 내포하고 있으면 reject 가 된다.

=> 파일을 전송할 때는 ENCTYPE을 multipart/form-data로 설정해야 하고

Content-Type도 multipart/form-data 그리고 boundary 까지 설정해야 한다.

 

=> 직접 입력을 받아서 서버에 전송할 때는 데이터의 유효성을 검사해서 유효성을 통과한 경우에만 전송

유효성 검사는 정규식을 이용하는 경우가 많다.

 

** Android에서 데이터 삽입

=> itemname, decription, price를 파라미터로 전송해야 하고 pictureurl이 이미지 파일이다.

 

1. 실행가능한 Activity 생성 - insertActivity

 

2. 화면 디자인 - EditText 3개와 버튼 1개

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context="com.example.androidportfolio.insertActivity"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="이름을 입력하세요"
        android:id="@+id/itemnameinput"/>
    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="가격을 입력하세요"
        android:id="@+id/priceinput"
        android:inputType="number"/>
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="데이터 추가"
        android:id="@+id/btninsert"/>

</LinearLayout>

 

3. res 디렉토리에 raw 디렉토리를 만들고 ball.png 파일을 복사

 

 

4. InsertActivity.java 파일에 코드 작성

 

package com.example.androidportfolio;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.UUID;

public class InsertActivity extends AppCompatActivity {
    EditText itemnameinput, priceinput, descriptioninput;
    Button btninsert;

    class InsertThread extends Thread{
        public void run(){
            try{
                //업로드할 주소
                URL url = new URL("http://192.168.0.200:8080/mysqlserver/insert");
                //서버에게 넘겨줄문자열 파라미터를 생성
                String [] data = {itemnameinput.getText().toString().trim(),
                        priceinput.getText().toString().trim(),
                        descriptioninput.getText().toString().trim()};
                String [] dataName ={"itemname", "price", "description"};

                //파라미터 전송에 필료한 변수 생성
                String lineEnd = "\r\n"; // \r은 커서를 맨앞으로, \n은 줄바꿈
                //파일 업로드를 할 때는 boundary 값이 있어야 한다.
                //랜덤하게 생성하는 것을 권장
                //String boundary = "androidinsert";
                String boundary = UUID.randomUUID().toString();

                //업로드 옵션을 설정
                HttpURLConnection con =(HttpURLConnection)url.openConnection();
                con.setRequestMethod("post");
                con.setUseCaches(false);
                con.setConnectTimeout(30000);
                con.setDoInput(true);
                con.setDoOutput(true);

                //파일 업로드 옵션
                con.setRequestProperty("ENCTYPE", "multipart/form-data");
                con.setRequestProperty("Content-Type", "multipart/form-data;boundary=" + boundary);

                //문자열 파라미터를 전송
                String delimiter = "--" + boundary + lineEnd;
                StringBuffer postDataBuilder = new StringBuffer();
                for(int i=0; i<data.length; i=i+1){
                    postDataBuilder.append(delimiter);
                    postDataBuilder.append("Content-Disposition:form-data;name=\"" + dataName[i] + "\"" + lineEnd + data[i] + lineEnd);
                }
                //업호드할 파일이 있는 겨웅에만 작성
                String fileName = "ball.png";
                if(fileName != null){
                    postDataBuilder.append(delimiter);
                    postDataBuilder.append(("Content-Disposition:form-data;nema=\"" + "pictureurl" + "\";filename\"" + fileName + "\"" + lineEnd));
                }
                //파라미터 전송
                DataOutputStream ds = new DataOutputStream(con.getOutputStream());
                ds.write(postDataBuilder.toString().getBytes());

                //파일 업로드
                if(fileName != null){
                    ds.writeBytes(lineEnd);
                    //파일 읽어오기 - id에 해당하는 파일을 raw 디렉토리에 복사
                    InputStream fres = getResources().openRawResource(R.raw.ball);
                    byte [] buffer = new byte[fres.available()];
                    int length = -1;
                    //파일의 내용을 읽어서 읽은 내용이 있으면 그 내용을 ds에 기록
                    while((length = fres.read(buffer)) != -1){
                        ds.write(buffer, 0, length);
                    }
                    ds.writeBytes(lineEnd);
                    ds.writeBytes(lineEnd);
                    ds.writeBytes("--"+boundary+"--"+lineEnd);
                    fres.close();
                }else{
                    ds.writeBytes(lineEnd);
                    ds.writeBytes("--"+boundary+"--"+lineEnd);
                }
                ds.flush();
                ds.close();

                //서버로 부터 응답 가져오기
                BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()));
                StringBuffer sb = new StringBuffer();
                while(true){
                    String line = br.readLine();
                    if(line == null){
                        break;
                    }
                    sb.append(line + "\n");
                }
                br.close();
                con.disconnect();

                //JSON 파싱
                JSONObject object = new JSONObject(sb.toString());
                boolean result = object.getBoolean("result");
                //핸들러에게 결과를 전송
                Message message = new Message();
                message.obj = result;
                insertHandler.sendMessage(message);

            }catch(Exception e){
                Log.e("업로드 에러", e.getMessage());
            }

        }
    }

    Handler insertHandler = new Handler(Looper.getMainLooper()){
       @Override
       public void handleMessage(Message message){
           boolean result = (Boolean)message.obj;
           if(result == true){
               Toast.makeText(InsertActivity.this, "삽입 성공", Toast.LENGTH_LONG).show();
               //키보드 내리기
               InputMethodManager imm = (InputMethodManager)getSystemService(INPUT_METHOD_SERVICE);
               imm.hideSoftInputFromWindow(itemnameinput.getWindowToken(), 0);
               imm.hideSoftInputFromWindow(priceinput.getWindowToken(), 0);
               imm.hideSoftInputFromWindow(descriptioninput.getWindowToken(), 0);
           }
       }
    } ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_insert);

        itemnameinput = (EditText)findViewById(R.id.itemnameinput);
        priceinput = (EditText)findViewById(R.id.priceinput);
        descriptioninput = (EditText)findViewById(R.id.descriptioninput);

        btninsert = (Button)findViewById(R.id.btninsert);
        btninsert.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View view) {
                //유효성 검사
                if(itemnameinput.getText().toString().trim().length() < 1){
                    Toast.makeText(InsertActivity.this, "이름은 필수 입력입니다.",
                            Toast.LENGTH_LONG).show();
                    return;
                }

                new InsertThread().start();
            }
        });

    }
}

 

 

 

5.서버와 안드로이드 앱을 모두 구동해서 테스트

1)안드로이드 화면에 삽입 성공이 출력되는지 확인

 

2)데이터베이스에서 확인

 

3)서버 프로젝트를 저장한 디렉토리에서 .metadata 디렉토리의 .plugins/org.eclipse.wst.server.core/tmp0(tmp다른숫자)/wtpwebapps/프로젝트이름/업로드되는디렉토리이름/ 을 확인해서 파일이 업로드 되었는지 확인

 

 

**회원관리를 위한 서버와 안드로이드 앱 만들기

=>회원 정보

  email(primary key - 복호화가 가능한 암호화를 이용해서 저장)

  nickname(unique - 로그인 할 때 아이디로 사용)

  pw(not null - 복호화는 불가능하고 비교만 가능한 형태의 암호화를 이용해서 저장)   

  profile(이미지 파일의 경로를 저장 - 기본 이미지 이름은 default.jpg)

=>회원가입, 로그인, 회원정보 수정, 회원 탈퇴 4가지 작업 구현

 

1.데이터를 설계 - 데이터베이스 작업

CREATE TABLE  Member(

email varchar(200) primary key,

nickname varchar(200) unique,

pw varchar(100) not null,

profile varchar(100)

)engine=InnoDB DEFAULT CHARSET=utf8;

 

2.Server 작업

1)암호화 작업을 위한 준비

=>pom.xml 파일에 아래 dependency가 설정되었는지 확인

<!-- 복호화가 불가능한 암호화 라이브러리 -->

<dependency>

<groupId>org.mindrot</groupId>

<artifactId>jbcrypt</artifactId>

<version>0.4</version>

</dependency>

 

=>복호화가 가능한 암호화를 위한 클래스를 프로젝트에 삽입

 

2) webapp 디렉토리에 파일을 저장할 디렉토리를 생성

=> profile 디렉토리를 생성

 

서버에서 데이터를 제공하는 디렉토리를 만들 때는 텍스트 파일을 만들어서 마지막 업데이트 한 날짜를 저장

 

3) 유저의 기본 이미지로 사용할 default.jpg 파일을 profile 디렉토리에 복사

 

4) Member 테이블과 연동할 클래스를 domain 패키지에 생성

 

import lombok.Data;

@Data
public class Member {
	private String email;
	private String pw;
	private String nickname;
	private String profile;
}

 

=>자동으로 getter 나 setter가 안만들어지면 직접 생성하면 됩니다.

 

5) Member 테이블과 Member 클래스를 매핑하는 파일을 dao 패키지에 생성

=>member.hbm.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping 
package="kakao.itggangpae.mysqlserver.domain">
	<class name="Member" table="Member">
		<id name="email" column="email"/>
			<property name="pw" column="pw"/>
			<property name="nickname" column="nickname"/>
			<property name="profile" column="profile"/>
	</class>
</hibernate-mapping>

 

6) 매핑 파일을 등록

=>root-context.xml 파일에 추가 : web.xml 파일의 listener 태그 근처의 context-param 태그에 설정된 파일에 작성 - 웹 애플리케이션이 시작될 때 사용할 bean의 설정 파일 경로

 

 

	<!-- 하이버네이트 설정  -->
	<bean id="sessionFactory"
	class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
		<property name="dataSource" ref="dataSource"/>
		<property name="mappingResources">
			<list>
				<value>
				kakao/itggangpae/mysqlserver/dao/item.hbm.xml
				</value>
				<value>
				kakao/itggangpae/mysqlserver/dao/member.hbm.xml
				</value>
			</list>
		</property>
		<property name="hibernateProperties">
			<value>
			hibernate.dialect=org.hibernate.dialect.MySQLDialect
			</value>
		</property>
	</bean>

 

 

 

7) MemberDAO 클래스를 만들고 필요한 메소드를 구현

 

import java.util.List;

import org.hibernate.SessionFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import kakao.itggangpae.mysqlserver.domain.Member;

@Repository
public class MemberDAO {
	@Autowired
	private SessionFactory sessionFactory;
	
	//email 중복검사를 위한 메소드
	public List<String> emailcheck(){
		List<String> list = 
				sessionFactory.getCurrentSession()
				.createNativeQuery(
					"select email from member")
				.getResultList();
		return list;
	}
	
	//nickname 중복검사를 위한 메소드
	public List<String> nicknamecheck(String nickname){
		List<String> list = 
				sessionFactory.getCurrentSession()
				.createNativeQuery(
					"select nickname from member "
					+ "where nickname = \'" + nickname 
					+ "\'")
				.getResultList();
		return list;
	}
	
	//회원가입을 위한 메소드
	public void join(Member member) {
		sessionFactory.getCurrentSession().save(member);
	}
	
	//로그인을 위한 메소드
	//nickname 과 pw를 가지고 로그인
	//nickname을 가지고 모든 정보를 전부 찾아가면 됩니다.
	public List<Member> login(String nickname){
		List<Member> list = 
				sessionFactory.getCurrentSession()
				.createNativeQuery(
					"select nickname, pw, email, profile "
					+ "from member "
					+ "where nickname = \'" + nickname 
					+ "\'")
				.getResultList();
		return list;
	}
	
	//회원정보를 수정하는 메소드
	public void update(Member member) {
		//다른 SQL 작업과 혼합이 되는 경우 한꺼번에 수행할 때는
		//update 대신에 merge를 사용하며 
		//없으면 저장하고 있으면 수정하고자 하는 경우에는 saveOrUpdate
		sessionFactory.getCurrentSession().merge(member);
	}
	
	//회원정보를 삭제하는 메소드
	public void delete(Member member) {
		//이전에 수행 중인 내용을 전부 삭제하고 작업을 수행
		sessionFactory.getCurrentSession().clear();
		sessionFactory.getCurrentSession().delete(member);
	}
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

** 하이버네이트 연동 서비스

=> DTO - 테이블과 연동할 클래스

=> him.xml - DTO 와 테이블을 매핑하는 파일

=> DAO

=> Service, ServiceImpl

=> RestController

 

=> Controller, View : 웹 애플리케이션을 만들 때

 

** 회원 가입

=> email, nickname 중복 검사

email은 암호화해서 저장할 거라서 데이터베이스 직접 비교가 안된다.

데이터를 암호화할 때 동일한 데이터라도 결과는 달라지게 된다.

AAA -> 1BBB

        -> 2CCC

email은 전부 가져와서 복호화하면서 동일한지 비교

 

nickname은 암호화가 안되어 있으므로 데이터베이스에 nickname이 있는지 조회

 

회원가입해주는 메소드 - insert