본문 바로가기
수업내용

20230724 Android HttpRequest2

by titlejjk 2023. 7. 24.

지난 시간에 했던 방법을 Util을 이용해 구현해보겠다.

com.example.step04httprequest2에 새로운 Java Class를 만들어 준다. 이름은 Util Class

 

package com.example.step04httprequest2;

import android.os.AsyncTask;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class Util {
    public static interface RequestListener{
        public void onSuccess(int requestId, Map<String, Object> result);
        public void onFail(int requestId, Map<String, Object> result);
    }
    /*
        1. 사용할때 RequestListener 인터페이스 Type 을 전달한다.
        2. 결과는 RequestListener 객체에 전달된다.
        3. Map<String,Object>  에서 응답 코드는
            "code" 라는 키값으로 200, 404, 500, -1 중에 하나가 리턴되고
             -1 이 리턴되면 Exception 발생으로 실패이다. onFail() 호출
     */
    public static void sendGetRequest(int requestId,
                                      String requestUrl,
                                      Map<String,String> params,
                                      RequestListener listener){
        RequestTask task=new RequestTask();
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        task.execute(params);
    }
    private static class RequestTask extends AsyncTask<Map<String,String>, Void, Map<String,Object>> {
        private int requestId;
        private String requestUrl;
        private RequestListener listener;

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }

        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }
        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }
        @Override
        protected Map<String, Object> doInBackground(Map<String, String>... params) {
            String requestUrl=this.requestUrl;
            Map<String, String> param=params[0];
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                StringBuffer buffer=new StringBuffer();
                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();
                requestUrl +=data;
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            //결과값을 담을 Map Type 객체
            Map<String,Object> map=new HashMap<String,Object>();
            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);//케쉬 사용 여부
                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();
                    //Map 객체에 응답 코드를 담는다.
                    map.put("code", responseCode);
                    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);
                        }
                        //출력받은 문자열 전체 얻어내기
                        String str=builder.toString();
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map.put("data", str);
                    }
                }
            }catch(Exception e){//예외가 발생하면
                //에러 정보를 담는다.
                map.put("code",-1);
                map.put("data", e.getMessage());
            }finally {
                try{
                    if(isr!=null)isr.close();
                    if(br!=null)br.close();
                    if(conn!=null)conn.disconnect();
                }catch(Exception e){}
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map;
        }

        @Override
        protected void onPostExecute(Map<String, Object> map) {
            super.onPostExecute(map);
            int code=(int)map.get("code");
            if(code!=-1){//성공이라면
                listener.onSuccess(requestId, map);
            }else{//실패 (예외발생)
                listener.onFail(requestId, map);
            }
        }
    }
    //POST 방식 REQUEST
    public static void sendPostRequest(int requestId, String requestUrl, Map<String, String> params, RequestListener listener){
        PostRequestTask task=new PostRequestTask();
        task.setRequestId(requestId);
        task.setRequestUrl(requestUrl);
        task.setListener(listener);
        task.execute(params);
    }

    public static class PostRequestTask extends AsyncTask<Map<String, String>, Void, Map<String, Object>>{
        private int requestId;
        private String requestUrl;
        private RequestListener listener;

        public void setListener(RequestListener listener) {
            this.listener = listener;
        }
        public void setRequestId(int requestId) {
            this.requestId = requestId;
        }
        public void setRequestUrl(String requestUrl) {
            this.requestUrl = requestUrl;
        }
        @Override
        protected Map<String, Object> doInBackground(Map<String, String>... params) {
            Map<String, String> param=params[0];
            String queryString="";
            if(param!=null){//요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                StringBuffer buffer=new StringBuffer();
                Set<String> keySet=param.keySet();
                Iterator<String> it=keySet.iterator();
                boolean isFirst=true;
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while(it.hasNext()){
                    String key=it.next();
                    String arg=null;
                    if(isFirst){
                        arg=key+"="+param.get(key);
                        isFirst=false;
                    }else{
                        arg="&"+key+"="+param.get(key);
                    }
                    buffer.append(arg);
                }
                queryString=buffer.toString();
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            StringBuilder builder=new StringBuilder();
            HttpURLConnection conn=null;
            InputStreamReader isr=null;
            BufferedReader br=null;
            PrintWriter pw=null;
            //결과값을 담을 Map Type 객체
            Map<String,Object> map=new HashMap<String,Object>();
            try{
                //URL 객체 생성
                URL url=new URL(requestUrl);
                //HttpURLConnection 객체의 참조값 얻어오기
                conn=(HttpURLConnection)url.openConnection();
                if(conn!=null){//연결이 되었다면
                    conn.setConnectTimeout(20000); //응답을 기다리는 최대 대기 시간
                    conn.setDoOutput(true);
                    conn.setRequestMethod("POST");
                    conn.setUseCaches(false);//케쉬 사용 여부
                    //전송하는 데이터에 맞게 값 설정하기
                    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();

                    //응답 코드를 읽어온다.
                    int responseCode=conn.getResponseCode();
                    //Map 객체에 응답 코드를 담는다.
                    map.put("code", responseCode);
                    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);
                        }
                        //출력받은 문자열 전체 얻어내기
                        String str=builder.toString();
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map.put("data", str);
                    }
                }
            }catch(Exception e){//예외가 발생하면
                //에러 정보를 담는다.
                map.put("code",-1);
                map.put("data", e.getMessage());
            }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){}
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map;
        }

        @Override
        protected void onPostExecute(Map<String, Object> map) {
            super.onPostExecute(map);
            int code=(int)map.get("code");
            if(code!=-1){//성공이라면
                listener.onSuccess(requestId, map);
            }else{//실패 (예외발생)
                listener.onFail(requestId, map);
            }
        }
    }
}

 

그런 다음 전 시간에 사용했던 Layout 그대로 복사해서 사용해준다.

<?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=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:id="@+id/editText"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="#00ff00"
        android:focusable="false" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <EditText
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:id="@+id/inputUrl"
            android:text="http://acornacademy.co.kr/index.jsp"/>
        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="요청"
            android:id="@+id/requestBtn"/>
    </LinearLayout>

</LinearLayout>

 

MainActivity

 

package com.example.step04httprequest2;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Map;

public class MainActivity extends AppCompatActivity implements Util.RequestListener{
    EditText editText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        editText=findViewById(R.id.editText);
        EditText inputUrl=findViewById(R.id.inputUrl);
        //요청 버튼을 클릭했을때 동작할 준비
        Button requestBtn=findViewById(R.id.requestBtn);
        requestBtn.setOnClickListener(view -> {
            //1. 입력한 url 주소를 읽어와서
            String url=inputUrl.getText().toString();
            Util.sendGetRequest(0, url, null, this);
        });
    }

    @Override
    public void onSuccess(int requestId, Map<String, Object> result) {
        if(requestId == 999){
            //map 에 data라는 키 값으로 담긴 String type을 읽어온다.
            String data=(String)result.get("data");
            //결과 문자열을 EdtiText에 출력하기
            editText.setText(data);
        }
    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {
        //에러 메세지를 읽어와서 EditText에 출력하기
        String data=(String)result.get("data");
        editText.setText(data);
    }
}

 

이렇게 Util을 이용해서 실행을 하면

 

 

위와 같이 작성이 된다.

 

이 다음으로는 Util을 이용해 Http 요청을 해보겠다.

다시 새로운 모듈을 만들어주고 Layout을 수정해주었다.

 

 

<?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=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:id="@+id/editText"
        android:background="#f98802"
        android:focusable="false"
        android:textSize="30sp"
        android:textColor="#ffffff"
        />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="메세지 입력"
        android:id="@+id/inputMsg"
        />

    <Button
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Send"
        android:id="@+id/sendBtn"
        />
</LinearLayout>

 

Layout은 위와 같이 디자인 되었으며 이제 AndoridManifest.xml에서 인터넷연결을 위한 설정을 해준다.

 

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- 인터넷을 사용하겠다는 허가 얻기-->
    <uses-permission android:name="android.permission.INTERNET"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Hello"
        android:usesCleartextTraffic="true">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

 

그리고 MainActivity에서 참조값들을 얻어오고 Button에 동작하기 위한 리스너를 등록해준다.

 

package com.example.step04example;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

import java.util.HashMap;
import java.util.Map;

/*
 *  문자열을 입력하고 "전송" 버튼을 누르면 Util클래스를 이용해서
 *  GET방식으로 http://아이피주소:8080/android/tweet에 요청을 하는 예제
 *  요청 파라미터는 msg라는 파라미터 명으로 입력한 문자열이 전송되도록 한다.
 *  서버가 응답하는 문자열은 오렌지색 EditText에 출력하기.
 */

public class MainActivity extends AppCompatActivity implements Util.RequestListener{

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

        //activity_main.xml 문서에 정의된 객체의 참조값 얻어오기
        editText=(EditText) findViewById(R.id.editText);
        EditText inputMsg=(EditText) findViewById(R.id.inputMsg);
        Button sendBtn=(Button) findViewById(R.id.sendBtn);
        //Button에 리스너 등록하기
        sendBtn.setOnClickListener(view -> {
            //입력한 문자열
            String msg = inputMsg.getText().toString();
            //요청 파라미터로 전달하기 위해 "msg"라는 키값으로 Map에 담는다.
            Map<String, String>map = new HashMap<>();
            map.put("msg",msg);
            //Util을 이용해서 http요청을 한다.
            Util.sendGetRequest(0,"http://192.168.0.4:8080/android/tweet",
                    map,this);
        });
    }

    @Override
    public void onSuccess(int requestId, Map<String, Object> result) {
        if(requestId==0){
            String data=(String) result.get("data");
            editText.setText(data);
        }
    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {
        if(requestId==0){
            String data=(String) result.get("data");
            editText.setText(data);
        }
    }
}

 

이 작업들은 문자열을 입력하고 "전송"버튼을 누르면 Util 클래스를 이용해서 

Get방식으로 http://아이피주소:8080/android/tweet에 요청을 하는 예제이다.

요청 파라미터는 msg라는 파라미터 명으로 입력한 문자열이 전송되도록하고 서버가 응답하는 문자열은 위 레이아웃에서 오렌지색 EditText에 출력하게 한다,

 

안드로이드에서의 LocalHost는 기기에서 사용하는 IP이기 때문에 아이피 주소를 사용해줘야한다.

 

이렇게 입력해주고 스프링부트에서 Console.log를 확인해보면

 

 

 

다음으로 이번에는 Get방식이 아닌 Post 방식으로 해보겠다.

 

먼저 인텔리제이에서 Post로 받기 위해 아래와 같은 코드를 추가로 넣어준다.

 

@PostMapping("/android/tweet2")
    public Map<String , Object>tweet2(String msg){
        System.out.println("안드로이드에서 전송된 문자열 : " + msg);
        Map<String, Object> map = new HashMap<>();
        map.put("isSuccess", true);
        return map;
    }

 

그리고 이를 실행하기 위해 두번째 버튼을 activity_main.xml에서 만들어준다.

 

<Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="전송2"
            android:id="@+id/sendBtn2"/>

 

다음으로 MainActivity.java Class에서 두번째 버튼에도 리스너를 등록해주고

 

 //두번째 버튼도 리스너 등록하기
        Button sendBtn2=(Button) findViewById(R.id.sendBtn2);
        sendBtn2.setOnClickListener(view->{
            //입력한 문자열
            String msg=inputMsg.getText().toString();
            //요청 파라미터로 전달하기 위해 "msg" 라는 키값으로 Map 에 담는다.
            Map<String, String> map=new HashMap<>();
            map.put("msg", msg);
            Util.sendPostRequest(1,
                    "http://192.168.0.31:9000/boot07/android/tweet2",
                    map,
                    this);
        });

 

onSuccess에 else if로 두번째 값을 받아줄 코드를 작성해준다.

JsonObject로 "isSuccess"키 값에 담긴 value를 추출해 준다.

 

package com.example.step04example;

import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;

import androidx.appcompat.app.AppCompatActivity;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;

/*
    문자열을 입력하고 "전송" 버튼을 누르면  Util 클래스를 이용해서
    GET 방식으로 http://아이피주소:9000/boot07/android/tweet 에 요청을 하는예제
    요청 파라미터는 msg 라는 파라미터 명으로 입력한 문자열이 전송되도록 한다.
    서버가 응답하는 문자열은 오렌지색 EditText 에 출력하기
 */
public class MainActivity extends AppCompatActivity implements Util.RequestListener{
    EditText editText;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // activity_main.xml 문서에 정의된 객체의 참조값 얻어오기
        editText=(EditText) findViewById(R.id.editText);
        EditText inputMsg=(EditText) findViewById(R.id.inputMsg);
        Button sendBtn=(Button) findViewById(R.id.sendBtn);
        // 버튼에 리스너 등록하기
        sendBtn.setOnClickListener(view->{
            //입력한 문자열
            String msg=inputMsg.getText().toString();
            //요청 파라미터로 전달하기 위해 "msg" 라는 키값으로 Map 에 담는다.
            Map<String, String> map=new HashMap<>();
            map.put("msg", msg);
            //Util 을 이용해서 http 요청을 한다.
            Util.sendGetRequest(0,
                    "http://192.168.0.31:9000/boot07/android/tweet",
                    map,
                    this);
        });
        //두번째 버튼도 리스너 등록하기
        Button sendBtn2=(Button) findViewById(R.id.sendBtn2);
        sendBtn2.setOnClickListener(view->{
            //입력한 문자열
            String msg=inputMsg.getText().toString();
            //요청 파라미터로 전달하기 위해 "msg" 라는 키값으로 Map 에 담는다.
            Map<String, String> map=new HashMap<>();
            map.put("msg", msg);
            Util.sendPostRequest(1,
                    "http://192.168.0.31:9000/boot07/android/tweet2",
                    map,
                    this);
        });
        //세번째 버튼도 리스너 등록하기
        Button sendBtn3=(Button) findViewById(R.id.sendBtn3);
        sendBtn3.setOnClickListener(view->{
            //입력한 문자열
            String msg=inputMsg.getText().toString();
            //요청 파라미터로 전달하기 위해 "msg" 라는 키값으로 Map 에 담는다.
            Map<String, String> map=new HashMap<>();
            map.put("msg", msg);
            Util.sendGetRequest(2,
                    "http://192.168.0.31:9000/boot07/android/tweet3",
                    map,
                    this);
        });
    }

    @Override
    public void onSuccess(int requestId, Map<String, Object> result) {
        if(requestId == 0){
            //서버가 응답한 문자열
            String data=(String)result.get("data");
            editText.setText(data);
        }else if(requestId == 1){
            String data=(String)result.get("data");
            //data는 json문자열이다.
            editText.setText(data);

            //{"isSuccess":true}형식의 json문자열은 JSONObject객체를 이용해서 원하는 데이터를 추출 할 수 있다.
            try {
                //JSONObject를 생성해서
                JSONObject obj = new JSONObject(data);
                //"isSuccess"라는 키값으로 저장된 true라는 boolean type데이터 얻어내기
                boolean isSuccess = obj.getBoolean("isSuccess");
                editText.setText(Boolean.toString(isSuccess));
            } catch (JSONException e) {
                //data가 json형식에 어긋나면 예외가 발생한다.
                throw new RuntimeException(e.getMessage());
            }
        } else if (requestId==2) {
            String data=(String)result.get("data");
            //data는[]형식의 json문자열이다. []형식의 json문자열은 JSONArray객체를 활용한다.
            try {
                JSONArray arr = new JSONArray(data);
                //반복문을 돌면서
                for(int i = 0; i<arr.length(); i++){
                    //i의 갯수만큼 String type을 얻어내서
                    String tmp=arr.getString(i);
                    //editText객체에 출력해준다.
                    editText.append(tmp+"\n");
                }

            } catch (JSONException e) {
                //data가 json형식에 어긋나면 예외가 발생한다.
                editText.setText(e.getMessage());
            }

        }
    }

    @Override
    public void onFail(int requestId, Map<String, Object> result) {
        if(requestId == 0){
            String data=(String)result.get("data");
            editText.setText(data);
        }
    }
}

JSONObject obj = new JSONObject(data);

boolean isSuccess = obj.getBoolean("isSuccess");

 

 

const result = JSON.parse(data);

const isSuccess=result.isSuccess

 

이번에는 list를 출력하는 Button을 만들어보겠다.

먼저 Layout에 버튼을 추가해주고

<Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/listBtn"
        android:text="글 목록보기"/>

 

MainActivity Class에 버튼의 listener를 만들어준다.

 

 Button listBtn=findViewById(R.id.listBtn);
        listBtn.setOnClickListener(view -> {
            Util.sendGetRequest(3,"http://192.168.0.31:9000/boot07/android/list",
                    null,
                    this);
        });

 

 

'수업내용' 카테고리의 다른 글

20230725 Android 다른화면으로 넘어가기  (0) 2023.07.25
20230725 Android  (0) 2023.07.25
20230721 Android HttpRequest  (0) 2023.07.21
20230721 Android CustomAdapter 2  (0) 2023.07.21
20230721 CSS3 Flex  (0) 2023.07.21

댓글