** 안드로이드 상세보기
=> 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