package com.bsj0420.ex87retrofitmarcketapp;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class EditActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_edit);
getSupportActionBar().setTitle("글 작성");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onSupportNavigateUp() {
finish();
return super.onSupportNavigateUp();
}
}
package com.bsj0420.ex87retrofitmarcketapp;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.converter.scalars.ScalarsConverterFactory;
public class RetrofitHelper {
public static Retrofit getRetrofitInstance(){
Retrofit.Builder builder = new Retrofit.Builder();
builder.baseUrl("http://내 닷홈 주소");
builder.addConverterFactory(ScalarsConverterFactory.create()); //스칼라를 먼저 써여함
builder.addConverterFactory(GsonConverterFactory.create());
Retrofit retrofit = builder.build();
return retrofit;
}
}
android:ems : 글자크기 정해놓기
<?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"
android:orientation="vertical"
android:padding="16dp"
tools:context=".EditActivity">
<EditText
android:id="@+id/et_name"
android:inputType="text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="작성자 이름"
android:ems="10"/>
<EditText
android:id="@+id/et_title"
android:inputType="text"
android:hint="title"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/et_msg"
android:inputType="textMultiLine"
android:layout_weight="1"
android:gravity="top"
android:hint="message"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<EditText
android:id="@+id/et_price"
android:inputType="number"
android:hint="price"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<Button
android:id="@+id/btn_select"
android:layout_gravity="right"
android:text="select img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/iv"
android:background="#DCDCDC"
android:padding="4dp"
android:layout_width="match_parent"
android:layout_height="200dp"/>
<Button
android:id="@+id/btn_complete"
android:text="complete"
android:backgroundTint="@color/black"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
String 파일들 한번에 묶어 보내기 위해 Map에 넣어서 보낸다
package com.bsj0420.ex87retrofitmarcketapp;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.loader.content.CursorLoader;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.widget.Toast;
import com.bsj0420.ex87retrofitmarcketapp.databinding.ActivityEditBinding;
import com.bumptech.glide.Glide;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class EditActivity extends AppCompatActivity {
ActivityEditBinding binding;
//업로드할 이미지의 파일 경로 변수
String imgPath = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding= ActivityEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getSupportActionBar().setTitle("글 작성");
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
//이미지 선택
binding.btnSelect.setOnClickListener(v->clickSelect());
binding.btnComplete.setOnClickListener(v->clickComplete());
}
private void clickComplete() {
//사용자가 입력한 데이터들을 서버에 전송
//전송할 데이터들
//작성자 이름, 타이틀, msg, price, imgPath
String name = binding.etName.getText().toString();
String title = binding.etTitle.getText().toString();
String msg = binding.etMsg.getText().toString();
String price = binding.etPrice.getText().toString();
//작업 5단계
//1
Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
//2 요구 명세
//3.
RetrofitService retrofitService= retrofit.create(RetrofitService.class);
//4.
//Stirng 데이타들
Map<String,String> dataPart = new HashMap<>();
dataPart.put("name",name);
dataPart.put("title",title);
dataPart.put("msg",msg);
dataPart.put("price",price);
//이미지 파일 택배박스
MultipartBody.Part filePart = null;
if(imgPath != null) { //사진 업로드 안할 때
File file = new File(imgPath);
RequestBody body= RequestBody.create(MediaType.parse("image/*"), file);
filePart= MultipartBody.Part.createFormData("img", file.getName(), body);
}
Call<String> call= retrofitService.postDataToServer(dataPart, filePart);
//5.
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
String s= response.body();
Toast.makeText(EditActivity.this, s, Toast.LENGTH_SHORT).show();
//게시글 업로드 성공 했으니 ..글작성화면 자동 종료
finish();
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Toast.makeText(EditActivity.this, "error : " + t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
private void clickSelect() {
Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); //퍼미션 없이 쓸수 있는 이미지픽
//결과를 받기위한 스타트 액티비티 => ActivityResultLauncher
resultLauncher.launch(intent);
}
ActivityResultLauncher<Intent> resultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if(result.getResultCode() == RESULT_CANCELED) return; //사진 선택 안하고 왔으면 리턴
Uri uri = result.getData().getData();
Glide.with(this).load(uri).into(binding.iv);
//Retrofit을 이용하여 서버에 이미지 보낼때는 파일의 주소가 필요
imgPath = getFilePathFromUri(uri);
//new AlertDialog.Builder(this).setMessage(imgPAth).show();
});
//Uri -- > 절대경로로 바꿔서 리턴시켜주는 메소드
String getFilePathFromUri(Uri uri){
String[] proj= {MediaStore.Images.Media.DATA};
CursorLoader loader= new CursorLoader(this, uri, proj, null, null, null);
Cursor cursor= loader.loadInBackground();
int column_index= cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
String result= cursor.getString(column_index);
cursor.close();
return result;
}
@Override
public boolean onSupportNavigateUp() {
finish();
return super.onSupportNavigateUp();
}
}
package com.bsj0420.ex87retrofitmarcketapp;
import java.util.ArrayList;
import java.util.Map;
import okhttp3.MultipartBody;
import retrofit2.Call;
import retrofit2.http.Field;
import retrofit2.http.FormUrlEncoded;
import retrofit2.http.GET;
import retrofit2.http.Multipart;
import retrofit2.http.POST;
import retrofit2.http.Part;
import retrofit2.http.PartMap;
import retrofit2.http.Query;
public interface RetrofitService {
//POST방식으로 보낼 때
//이미지 파일용 Part와 나머지 문자열 데이터들 PartMap(여러 데이타 보내기 위해) 으로 구분하여 전송
//Part는 하나만 들어갈 수 있음
//즉, 택배박스 2개로..
@Multipart
@POST("RetrofitMarket/insertDB.php")
Call<String> postDataToServer(@PartMap Map<String,String> dataPart, @Part MultipartBody.Part filePart);
//@FormUrlEncoded 와 @Multipart 같이 쓸 수 없음
//@Body로 보내거나 데이타용 박스 (Part)와 이미지용 박스 각각 만들어서 보내기
<?php
header("Content-Type:text/plain; charset=utf-8");
//@PartMap으로 전달 된 POST방식의 데이터들
$name= $_POST['name'];
$title= $_POST['title'];
$msg= $_POST['msg'];
$price= $_POST['price'];
//@Part로 전달된 이미지 파일
$file= $_FILES['img'];
$srcName= $file['name']; //원본파일명
$tmpName= $file['tmp_name']; //임시저장소 경로/파일명
//이미지 파일을 영구적으로 저장하기 위해 임시 저장소에서 이동
$dstName= "./image/" . date('YmdHis') . $srcName; //같은 폴더 안에 이미지폴더 안에
move_uploaded_file($tmpName, $dstName);
//메세지 중에 특수문자 사용가능성 있음. - 쿼리문에 문제 될 수 있음
//특수 문자는 sql에서 잘못 동작될 수 있기에
//앞에 슬래시를 추가해주기
$msg= addslashes($msg); // 슬래시가 필요한 곳에 알아서 넣어짐
$title= addslashes($title);
//데이터가 저장되는 시간
$now= date('Y-m-d H:i:s');
//Mysql DB에 데이터 저장 [테이블명 : market]
$db = mysqli_connect('localhost','mrhisj23','hi23bye6!','mrhisj23');
mysqli_query($db, "set names utf8");
//저장할 데이터($name, $title, $msg, $price, $dstName, $now) 삽입 뭐리문
$sql = "INSERT INTO market(name,title,msg,price,image,date) VALUES('$name','$title','$msg','$price','$dstName','$now')";
$result=mysqli_query($db,$sql);
if($result) echo "게시글이 업로드 되었습니다.";
else echo "게시글 업로드에 실패했습니다. 다시 시도해 주세요.";
mysqli_close($db);
?>
SwipeRefreshLayout : 리사이클러뷰를 감싸 작성
-> 스와이프를 사용하기 위해선 라이브러리 추가해야됨!
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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=".MainActivity">
<!-- 리프레시용 레이아웃 : 별도 라이브러리 -->
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:orientation="vertical"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="@color/purple_500"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:src="@drawable/baseline_edit_24"
app:tint="@color/white"
android:layout_margin="16dp"/>
</RelativeLayout>
package com.bsj0420.ex87retrofitmarcketapp;
import androidx.appcompat.app.AppCompatActivity;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import com.bsj0420.ex87retrofitmarcketapp.databinding.ActivityMainBinding;
import java.util.ArrayList;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.fabEdit.setOnClickListener(view -> {
startActivity(new Intent(this, EditActivity.class));
});
//리프레시
binding.refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
loadData();
}
});
}
@Override
protected void onResume() {
super.onResume();
loadData();
}
private void loadData() {
Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
RetrofitService retrofitService = retrofit.create(RetrofitService.class);
Call<ArrayList<MarketItem>> call = retrofitService.loadDataFromServer();
call.enqueue(new Callback<ArrayList<MarketItem>>() {
@Override
public void onResponse(Call<ArrayList<MarketItem>> call, Response<ArrayList<MarketItem>> response) {
ArrayList<MarketItem> items = response.body();
MarketAdapter adapter = new MarketAdapter(MainActivity.this, items);
binding.recycler.setAdapter(adapter); //노티파이 할 필요 없어짐 매번 새로 만드니까
///////////
binding.refreshLayout.setRefreshing(false); //로딩 아이콘 제거하는 명령
// true면 계속 도는 아이콘 보임
}
@Override
public void onFailure(Call<ArrayList<MarketItem>> call, Throwable t) {
Toast.makeText(MainActivity.this, "error = "+ t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
}
//서버에서 echo한 json array를 읽어와서 자동으로 ArrayList로 파싱하는 요구 명세
@GET("RetrofitMarket/loadDB.php")
Call<ArrayList<MarketItem>> loadDataFromServer();
<?php
header("Content-Type:application/json; charset=utf-8");
$db = mysqli_connect('localhost','mrhisj23','hi23bye6!','mrhisj23');
mysqli_query($db, "set names utf8");
$sql = "SELECT * FROM market";
$result = mysqli_query($db,$sql); //결과표를 리턴해줌
//결과표로 부터 총 레코드 수
$rowNum = mysqli_num_rows($result);
//여러줄 읽어야하므로 각 줄($row)을 요소로 가질 빈 인덱스 배열 준비
$rows = array();
for($i=0; $i<$rowNum; $i++){
$row = mysqli_fetch_array($result, MYSQLI_ASSOC);//한줄을 연관배열로
$rows[$i] = $row; //2차원 배열로 저장됨 [{},{}]
}
//2차원 배열 --> json array 문자열로 변환 및 응답
echo json_encode($rows);
mysqli_close($db);
?>
maxLines : 최대 줄 수
ellipsize : 제목 넘어가면 ... 표시
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="8dp"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_height="120dp">
<androidx.cardview.widget.CardView
android:id="@+id/cv"
android:layout_width="120dp"
android:layout_height="match_parent"
app:cardCornerRadius="8dp">
<ImageView
android:id="@+id/iv"
android:scaleType="centerCrop"
android:src="@drawable/baseline_edit_24"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tv_title"
android:layout_toRightOf="@+id/cv"
android:textColor="@color/black"
android:textStyle="bold"
android:text="제목"
android:maxLines="1"
android:ellipsize="end"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="12dp"
android:layout_marginTop="8dp"/>
<TextView
android:id="@+id/tv_msg"
android:layout_marginTop="4dp"
android:maxLines="3"
android:layout_alignLeft="@+id/tv_title"
android:layout_below="@+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/tv_price"
android:layout_alignParentBottom="true"
android:layout_alignLeft="@+id/tv_title"
android:textStyle="bold"
android:textColor="@color/purple_700"
android:text="3000"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<ToggleButton
android:id="@+id/tb_fav"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:background="@drawable/bg_fav"
android:textOff=""
android:textOn=""
android:layout_width="24dp"
android:layout_height="24dp"/>
</RelativeLayout>
package com.bsj0420.ex87retrofitmarcketapp;
public class MarketItem {
String no;
String name;
String title;
String msg;
String price;
String image;
String date;
public MarketItem() {
}
public MarketItem(String no, String name, String title, String msg, String price, String image, String date) {
this.no = no;
this.name = name;
this.title = title;
this.msg = msg;
this.price = price;
this.image = image;
this.date = date;
}
}
package com.bsj0420.ex87retrofitmarcketapp;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bsj0420.ex87retrofitmarcketapp.databinding.RecyclerItemBinding;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class MarketAdapter extends RecyclerView.Adapter<MarketAdapter.VH> {
Context context;
ArrayList<MarketItem> items;
public MarketAdapter(Context context, ArrayList<MarketItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false);
return new VH(itemView); //바인딩 함
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
MarketItem item = items.get(position);
holder.binding.tvTitle.setText(item.title);
holder.binding.tvMsg.setText(item.msg);
holder.binding.tvPrice.setText(item.price + "원");
String address ="";
if (item.image !=null) address = "http://mrhisj23.dothome.co.kr/RetrofitMarket/" + item.image;
Glide.with(context).load(address).error(R.drawable.baseline_hide_image_24).into(holder.binding.iv); //이미지 안나옴 - DB에 사진 이름만 들어있음
//Glide는 이미지 "" 이면 오류 안나고 이미지 칸 비어있음 따라서 이미지가 없을 떄 보여줄 이미지 하나 넣어두기 error(이미지 없을 경우 보여줄 그림)
}
@Override
public int getItemCount() {
return items.size();
}
class VH extends RecyclerView.ViewHolder {
RecyclerItemBinding binding;
public VH(@NonNull View itemView) {
super(itemView);
binding = RecyclerItemBinding.bind(itemView); // itemView = RelativeLayout
}
}
}
package com.bsj0420.ex87retrofitmarcketapp;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.bsj0420.ex87retrofitmarcketapp.databinding.RecyclerItemBinding;
import com.bumptech.glide.Glide;
import java.util.ArrayList;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
public class MarketAdapter extends RecyclerView.Adapter<MarketAdapter.VH> {
Context context;
ArrayList<MarketItem> items;
public MarketAdapter(Context context, ArrayList<MarketItem> items) {
this.context = context;
this.items = items;
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(context).inflate(R.layout.recycler_item, parent, false);
return new VH(itemView); //바인딩 함
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
MarketItem item = items.get(position);
holder.binding.tvTitle.setText(item.title);
holder.binding.tvMsg.setText(item.msg);
holder.binding.tvPrice.setText(item.price + "원");
String address ="";
if (item.image !=null) address = "http://mrhisj23.dothome.co.kr/RetrofitMarket/" + item.image;
Glide.with(context).load(address).error(R.drawable.baseline_hide_image_24).into(holder.binding.iv); //이미지 안나옴 - DB에 사진 이름만 들어있음
//Glide는 이미지 "" 이면 오류 안나고 이미지 칸 비어있음 따라서 이미지가 없을 떄 보여줄 이미지 하나 넣어두기 error(이미지 없을 경우 보여줄 그림)
}
@Override
public int getItemCount() {
return items.size();
}
class VH extends RecyclerView.ViewHolder {
RecyclerItemBinding binding;
public VH(@NonNull View itemView) {
super(itemView);
binding = RecyclerItemBinding.bind(itemView); // itemView = RelativeLayout
//삭제
//binding.getRoot(); 아이템뷰 이렇게 부르거나 그냥 itemView
itemView.setOnLongClickListener(view -> {
//현재 클릭한 아이템 얻어오기
MarketItem item = items.get(getLayoutPosition()); //선택한 녀석의 아이템
//Toast.makeText(context, item.no+"", Toast.LENGTH_SHORT).show();
//레트로핏을 이용해서 DB에서 해당 아이템을 삭제 요청
Retrofit retrofit = RetrofitHelper.getRetrofitInstance();
RetrofitService retrofitService = retrofit.create(RetrofitService.class);
Call<String> call =retrofitService.deleteItem(item.no);
call.enqueue(new Callback<String>() {
@Override
public void onResponse(Call<String> call, Response<String> response) {
String s = response.body();
Toast.makeText(context, s, Toast.LENGTH_SHORT).show();
items.remove(item);
notifyDataSetChanged(); //아답터한테 데이터 변경됐다고 말하기
}
@Override
public void onFailure(Call<String> call, Throwable t) {
Toast.makeText(context, "삭제 실패 : "+ t.getMessage(), Toast.LENGTH_SHORT).show();
}
});
return true; //롱클릭 끝나고 같이 다시 호출 안되게 하려고
});
}
}
}
//서버 db에서 특정 아이템 삭제하도록 요청하는 명세
//안드로이드는 db 직접 못 건드림
@GET("RetrofitMarket/deleteItem.php")
Call<String> deleteItem(@Query("no") String no);
<?php
header("Content-Type:text/plain; charset=utf-8");
//delete 할 아이템의 no 정보
$no = $_GET['no'];
$db = mysqli_connect('localhost','mrhisj23','hi23bye6!','mrhisj23');
mysqli_query($db, "set names utf8");
$sql="SELECT image FROM market WHERE no=$no";
$result = mysqli_query($db, $sql);
$image="";
if($result){
$row=mysqli_fetch_array($result, MYSQLI_ASSOC); //한줄로 가져오는 게 좋다
$image= $row['image']; //경로 찾아오기
}
$sql="DELETE FROM market WHERE no=$no";
$result= mysqli_query($db, $sql);
if($result){
echo "아이템이 삭제되었습니다.";
//저장되어 있는 파일 삭제
unlink($image);
}
else echo "삭제중 오류발생";
mysqli_close($db);
?>