API를 이용해 앱 만들기

Jang Seok Woo·2020년 6월 12일
3

Android Studio

목록 보기
1/20

이번엔 data.go.kr에 주어진 공공데이터를 이용해 어플리케이션을 만들어 보도록 하겠다.

API 데이터를 다루는 방법을 알 수 있었고,
URL로 데이터 요청하는 interface 구성도 알 수 있었다.

먼저 Main

MainActivity.java

package com.daniel.app.mask.main;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.DecelerateInterpolator;
import android.widget.Button;

import com.daniel.app.mask.maskstore.MaskStoreActivity;
import com.daniel.app.mask.R;

public class MainActivity extends AppCompatActivity {

Button mGotoMaskStore;
Button mExit;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    //뷰 연결
    mExit = findViewById(R.id.main_exit);
    mGotoMaskStore = findViewById(R.id.main_mask_store_btn);
    //애니메이션 페이드인 작업
    mGotoMaskStore.setVisibility(View.INVISIBLE);
    mExit.setVisibility(View.INVISIBLE);
    Animation fadeIn = new AlphaAnimation(0, 1);
    fadeIn.setInterpolator(new DecelerateInterpolator()); // add this
    fadeIn.setDuration(5000);

    AnimationSet animation = new AnimationSet(false); // change to false
    animation.addAnimation(fadeIn);
    mGotoMaskStore.setAnimation(animation);
    mExit.setAnimation(animation);
    animation.start();

    mGotoMaskStore.setVisibility(View.VISIBLE);
    mExit.setVisibility(View.VISIBLE);
    //클릭리스너 이동
    mGotoMaskStore.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent intent  = new Intent(MainActivity.this, MaskStoreActivity.class);
            startActivity(intent);
        }
    });

    mExit.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            finish();
        }
    });
}

}

메인페이지엔 많은 내용을 담지 않았다.

어플의 이름과 주변 마스크판매소 및 미세먼지 정보를 보는 버튼과 종료하기 버튼으로 이루어져 있다.

이미지를 만지면서 페이드인 애니메이션을 버튼에 넣어보는 것도 좋을 것 같아 시도해보았다.

다음은 실질적 메인 페이지

MaskStoreActivity.java

package com.daniel.app.mask.maskstore;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.graphics.Color;
import android.graphics.PointF;
import android.location.Address;
import android.location.Geocoder;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.widget.GridLayout;
import android.widget.TextView;

import com.daniel.app.mask.R;
import com.daniel.app.mask.list.DustListActivity;
import com.naver.maps.geometry.LatLng;
import com.naver.maps.map.MapFragment;
import com.naver.maps.map.NaverMap;
import com.naver.maps.map.OnMapReadyCallback;
import com.naver.maps.map.UiSettings;
import com.naver.maps.map.overlay.InfoWindow;
import com.naver.maps.map.overlay.Marker;
import com.naver.maps.map.overlay.Overlay;
import com.naver.maps.map.overlay.OverlayImage;
import com.naver.maps.map.util.FusedLocationSource;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;

public class MaskStoreActivity extends AppCompatActivity implements NaverMap.OnMapClickListener, Overlay.OnClickListener, OnMapReadyCallback, NaverMap.OnCameraChangeListener, NaverMap.OnCameraIdleListener {

private FusedLocationSource mfusedLocationSource;
private List<Marker> mMarkerList = new ArrayList<Marker>();
NaverMap mnaverMap;
private InfoWindow minfoWindow;
private boolean mIsCameraAnimated = false;
GridLayout mDustView;
TextView mTvLocality;
TextView mTvStat;
TextView mTvSo2;
TextView mTvCo;
TextView mTvO3;
TextView mTvNo2;
TextView mTvPm10;
TextView mTvPm25;
String mSido;
dustThread mDustThread1;
FineDustResult mResult;
LatLng mCenter;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_mask_store);
    mDustView = findViewById(R.id.dust_view);
    //네이버 맵 async로 불러오기
    MapFragment mapFragment = (MapFragment) getSupportFragmentManager().findFragmentById(R.id.map);
    mapFragment.getMapAsync(this);
    //미세먼지 클릭리스너
    mDustView.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Intent goToDustListActivity = new Intent(MaskStoreActivity.this, DustListActivity.class);
            //리스트 인텐트로 보내기
            goToDustListActivity.putExtra("list", (Serializable) mResult.list);
            startActivity(goToDustListActivity);
        }
    });
}

@Override
public void onMapReady(@NonNull NaverMap naverMap) {
    //네이버 맵 연결?
    this.mnaverMap = naverMap;
    //현재위치
    mfusedLocationSource = new FusedLocationSource(this, 1);
    naverMap.setLocationSource(mfusedLocationSource);
    //현재위치 불러오는 UI 띄우기
    UiSettings uiSettings = naverMap.getUiSettings();
    uiSettings.setLocationButtonEnabled(true);
    //맵 위치 변경시 리스너
    naverMap.addOnCameraChangeListener(this);
    naverMap.addOnCameraIdleListener(this);
    naverMap.setOnMapClickListener(this);
    //맵 중앙위치
    mCenter = naverMap.getCameraPosition().target;
    //중앙위치 주소 불러오기
    Address address = fromLatLngToAddress(mCenter);
    //주소에서 구 단위로 하려 했으나 네이버 맵은 '구'만 null임 네이버맵 잘못된 부분이라함
    mSido = getSidoFromAddr(address);
    //마스크 판매소 API를 이용해 현재위치, 반경으로 데이터 가져옴
    FetchStoreSale(mCenter.latitude, mCenter.longitude, 1000);
    //미세먼지 정보 API를 이용해 가져옴
    FetchFineDust(mSido, "DAILY");
    //인포윈도우 부분 꾸미기
    minfoWindow = new InfoWindow();
    //인포윈도우 어댑터
    minfoWindow.setAdapter(new InfoWindow.DefaultViewAdapter(this) {
        @NonNull
        @Override
        protected View getContentView(@NonNull InfoWindow infoWindow) {
            //마커
            Marker marker = infoWindow.getMarker();
            Mask mask = (Mask) marker.getTag();
            //뷰 연결
            View view = View.inflate(MaskStoreActivity.this, R.layout.info_window, null);
            //데이터 출력
            ((TextView) view.findViewById(R.id.name)).setText(mask.name);
            if ("plenty".equalsIgnoreCase(mask.remain_stat)) {
                ((TextView) view.findViewById(R.id.remaining)).setText("100ea↑");
            } else if ("some".equalsIgnoreCase(mask.remain_stat)) {
                ((TextView) view.findViewById(R.id.remaining)).setText("30~100ea");
            } else if ("few".equalsIgnoreCase(mask.remain_stat)) {
                ((TextView) view.findViewById(R.id.remaining)).setText("2~30ea");
            } else if ("empty".equalsIgnoreCase(mask.remain_stat)) {
                ((TextView) view.findViewById(R.id.remaining)).setText("1ea↓");
            } else if ("break".equalsIgnoreCase(mask.remain_stat)) {
                ((TextView) view.findViewById(R.id.remaining)).setText("없음");
            } else {
                ((TextView) view.findViewById(R.id.remaining)).setText(null);
            }
            ((TextView) view.findViewById(R.id.time)).setText("입고 " + mask.stock_at);

            return view;
        }
    });
}

//개인정보 위치 허가코드
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    switch (requestCode) {
        case 1:
            mfusedLocationSource.onRequestPermissionsResult(requestCode, permissions, grantResults);
            return;
    }
}

//마스크 판매장소 API로 데이터 가져오는 함수
private void FetchStoreSale(double lat, double lng, int m) {
    Retrofit retrofit = new Retrofit.Builder().baseUrl("https://8oi9s0nnth.apigw.ntruss.com").addConverterFactory(GsonConverterFactory.create()).build();
    MaskAPI maskApi = retrofit.create(MaskAPI.class);
    maskApi.getStoresByGeo(lat, lng, m).enqueue(new Callback<StoreSaleResult>() {
        @Override
        public void onResponse(Call<StoreSaleResult> call, Response<StoreSaleResult> response) {
            if (response.code() == 200) {
                StoreSaleResult result = response.body();
                updateMapMarkers(result);
            }
        }

        @Override
        public void onFailure(Call<StoreSaleResult> call, Throwable t) {

        }
    });
}

//미세먼지 API로 데이터 가져오는 함수
private void FetchFineDust(String SidoName, String SearchCondition) {
    Retrofit retrofit = new Retrofit.Builder().baseUrl(DustAPI.BASE_URL).addConverterFactory(GsonConverterFactory.create()).build();
    DustAPI dustApi = retrofit.create(DustAPI.class);
    dustApi.getFineDustbySido(SidoName, SearchCondition).enqueue(new Callback<FineDustResult>() {
        @Override
        public void onResponse(Call<FineDustResult> call, Response<FineDustResult> response) {
            //200이 성공했다는 뜻
            if (response.code() == 200) {
                mResult = response.body();
                setDustOnView();
            }
        }

        @Override
        public void onFailure(Call<FineDustResult> call, Throwable t) {
        }
    });
}

//API에서 가져온 가게 좌표마다 marker 띄움
private void updateMapMarkers(StoreSaleResult result) {
    resetMarkerList();
    if (result.stores != null && result.stores.size() > 0) {
        for (Mask mask : result.stores) {
            Marker marker = new Marker();
            marker.setTag(mask);
            marker.setPosition(new LatLng(mask.lat, mask.lng));

            if ("plenty".equalsIgnoreCase(mask.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_green));
            } else if ("some".equalsIgnoreCase(mask.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_yellow));
            } else if ("few".equalsIgnoreCase(mask.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_red));
            } else {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_gray));
            }
            marker.setAnchor(new PointF(0.5f, 1.0f));
            marker.setMap(mnaverMap);
            marker.setOnClickListener(this);
            mMarkerList.add(marker);
        }
    }
}

//뷰에 데이터 출력 및 1초마다 구단위로 데이터 순환하면 뿌리는 쓰레드 연결
private void setDustOnView() {

    int count = mResult.totalCount;

    mTvLocality = findViewById(R.id.locality);
    mTvStat = findViewById(R.id.stat);
    mTvSo2 = findViewById(R.id.so2);
    mTvCo = findViewById(R.id.co);
    mTvO3 = findViewById(R.id.o3);
    mTvNo2 = findViewById(R.id.no2);
    mTvPm10 = findViewById(R.id.pm10);
    mTvPm25 = findViewById(R.id.pm25);

    mDustThread1 = new dustThread(0);
    mDustThread1.start();

}

//미세먼지 데이터 출력 쓰레드
public class dustThread extends Thread {
    private boolean stop = false;
    int index = 0;
    String city;

    dustThread(int index) {
        this.index = index;
        city = mResult.list.get(index).cityName;
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                Message msg1 = new Message();
                msg1.arg1 = index;
                dustHandler.sendMessage(msg1);
                int offtime = 1000;
                Thread.sleep(offtime);
                if (city.equals(mResult.list.get(index + 1).cityName)) {
                    index = 0;
                } else {
                    index += 1;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void threadStop(boolean stop) {
        this.stop = stop;
    }
}

//핸들러
Handler dustHandler = new Handler() {

    @Override
    public void handleMessage(Message msg) {

        mTvLocality.setText(mResult.list.get(msg.arg1).cityName);
        mTvSo2.setText("SO2 " + mResult.list.get(msg.arg1).so2Value + " ppm");
        mTvCo.setText("CO " + mResult.list.get(msg.arg1).coValue + " ppm");
        mTvO3.setText("O3 " + mResult.list.get(msg.arg1).o3Value + " ppm");
        mTvNo2.setText("NO2 " + mResult.list.get(msg.arg1).no2Value + " ppm");
        mTvPm10.setText("PM10 " + mResult.list.get(msg.arg1).pm10Value + " ㎍/㎥");
        mTvPm25.setText("PM2.5 " + mResult.list.get(msg.arg1).pm25Value + " ㎍/㎥");

        int value = Integer.parseInt(mResult.list.get(msg.arg1).pm25Value);
        //미세먼지 수준에 따른 구분
        if (value <= 15 && value >= 0) {
            mTvStat.setText("좋음");
            mTvStat.setTextColor(Color.parseColor("#84E6F3"));
        } else if (value <= 35 && value >= 16) {
            mTvStat.setText("보통");
            mTvStat.setTextColor(Color.parseColor("#41FA17"));
        } else if (value <= 75 && value >= 36) {
            mTvStat.setText("나쁨");
            mTvStat.setTextColor(Color.parseColor("#ED8619"));
        } else {
            mTvStat.setText("아주나쁨");
            mTvStat.setTextColor(Color.parseColor("#EC149D"));
        }
    }
};

//좌표로 주소 구하는 함수
private Address fromLatLngToAddress(LatLng center) {
    Geocoder geocoder = new Geocoder(this);
    List<Address> addr = null;
    Address address = null;
    try {
        addr = geocoder.getFromLocation(center.latitude, center.longitude, 7);

        address = addr.get(0);
    } catch (IOException e) {
        e.printStackTrace();
    }
    return address;
}

//주소에서 도 추출 및 도별로 URL로 보낼 수 있도록 문자열 변환
private String getSidoFromAddr(Address address) {

    String sido = address.getAdminArea();

    if (sido.equals("충청북도")) {
        sido = "충북";
    } else if (sido.equals("충청남도")) {
        sido = "충남";
    } else if (sido.equals("전라북도")) {
        sido = "전북";
    } else if (sido.equals("전라남도")) {
        sido = "전남";
    } else if (sido.equals("경상북도")) {
        sido = "경북";
    } else if (sido.equals("경상남도")) {
        sido = "경남";
    } else {
        sido = sido.substring(0, 2);
    }

    return sido;
}

//마커 리스트 지우기
private void resetMarkerList() {
    if (mMarkerList != null && mMarkerList.size() > 0) {
        for (Marker marker : mMarkerList) {
            marker.setMap(null);
        }
        mMarkerList.clear();
    }
}

//지도가 이동시에 이동중임을 확인
@Override
public void onCameraChange(int reason, boolean animated) {
    mIsCameraAnimated = animated;
}

//지도가 멈춘 위치의 좌표로 API로 URL post
@Override
public void onCameraIdle() {
    if (mIsCameraAnimated) {
        LatLng mapCenter = mnaverMap.getCameraPosition().target;
        Address address = fromLatLngToAddress(mapCenter);
        String sidoIdle = getSidoFromAddr(address);
        if (!sidoIdle.equals(mSido)) {
            mDustThread1.threadStop(true);
            FetchFineDust(sidoIdle, "DAILY");
        }
        mSido = sidoIdle;
        FetchStoreSale(mapCenter.latitude, mapCenter.longitude, 1000);
    }
}

//마커 띄운 후 사라지게하기
@Override
public void onMapClick(@NonNull PointF pointF, @NonNull LatLng latLng) {
    if (minfoWindow.getMarker() != null) {
        minfoWindow.close();
    }
}

//클릭시 마커 띄우기
@Override
public boolean onClick(@NonNull Overlay overlay) {
    if (overlay instanceof Marker) {
        Marker marker = (Marker) overlay;
        if (marker.getInfoWindow() != null) {
            minfoWindow.close();
        } else {
            minfoWindow.open(marker);
        }
        return true;
    }
    return false;
}

@Override
public void onPointerCaptureChanged(boolean hasCapture) {
}

}

네이버 API를 이용해 지도를 띄우고, 공공마스크판매소 API를 이용해 marker로 위치를 띄웠다. 상단에는 미세먼지 API를 이용해 지역구별로 1초마다 정보가 띄워지도록 만들었다.
**본래 의도는 이동하는 지도의 위치 '구'단위로 상단에 미세먼지 정보를 띄우려 했으나, 네이버지도는 도,동은 추출이 되는데 '구'만 null데이터로 들어가있어 가져올 수가 없다.

AVD로는 위치정보가 등록되지 않아 미세먼지정보를 띄우지 않고 있으나 갤럭시로 돌려본 결과 잘 나온다.

DustAPI.java

package com.daniel.app.mask.maskstore;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.Query;

public interface DustAPI {
//베이스 URL
String BASE_URL = "http://openapi.airkorea.or.kr/openapi/services/rest/ArpltnInforInqireSvc/";
//헤더
@Headers("Accept: application/json")
//GET (서비스키 + 리턴타입 + 페이지당 리턴 row 수)
@GET("getCtprvnMesureSidoLIst?ServiceKey=Lhp0GWghhWvVn4aUZSfe1rqUFsdQkNLvT%2BZLt5RNHiFocjZjrbruHxVFaiKBOmTnOypgiM7WqtCcWTSLbAmIeA%3D%3D&_returnType=json&numOfRows=100")
//Call <클래스> 함수명 (쿼리)
Call getFineDustbySido(@Query("sidoName") String sidoName, @Query("searchCondition") String searchCondition);
}

MaskAPI.java

package com.daniel.app.mask.maskstore;

import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Headers;
import retrofit2.http.Query;
//DustAPI부분과 동일
public interface MaskAPI {
@Headers("Accept: application/json")
@GET("/corona19-masks/v1/storesByGeo/json")
Call getStoresByGeo(@Query("lat") double lat, @Query("lng") double lng, @Query("m") int m);
}

마스크 API는 요청쿼리부분이 설명이 잘 안되어있어 유튜브에 미리 만들어보신 분의 코드를 참고하였다.

해당 API문서를 보고 똑.같.이 구조체와 클래스를 만들어주어야 한다.
변수명을 달리 했더니 당연한 얘기지만 작동되지 않았다.

DustListActivity.java

package com.daniel.app.mask.list;

import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.Intent;
import android.os.Bundle;
import com.daniel.app.mask.R;
import com.daniel.app.mask.maskstore.FineDust;
import java.util.ArrayList;

public class DustListActivity extends AppCompatActivity {

RecyclerView mRecyclerView;
ArrayList<FineDust> mDustList = new ArrayList<>();
RecyclerViewAdaper mRecyclerViewAdapter;

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

    //뷰 연결
    mRecyclerView = findViewById(R.id.rv_view);

    //리사이클러뷰 크기 고정
    mRecyclerView.setHasFixedSize(true);

    //리스트 인텐트
    Intent fromMaskStoreActivity = getIntent();
    mDustList = (ArrayList<FineDust>) fromMaskStoreActivity.getSerializableExtra("list");

    //중복되는 데이터 부터 리스트 자르기
    int available = 0;
    for(int i=1;i<mDustList.size();i++){
    if(mDustList.get(0).cityName.equals(mDustList.get(i).cityName)){
        available = i;
        break;
        }
    }
    ArrayList<FineDust> subDustList = new ArrayList<FineDust>(mDustList.subList(0,available));

    //리싸이클러뷰 레이아웃매니저
    LinearLayoutManager layoutManager = new LinearLayoutManager(this);
    layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
    mRecyclerView.setLayoutManager(layoutManager);

    //어댑터 연결
    mRecyclerViewAdapter = new RecyclerViewAdaper(subDustList, this);
    mRecyclerView.setAdapter(mRecyclerViewAdapter);
}

}

상단의 미세먼지정보 부분을 클릭하면
리사이클러뷰로 만든 미세먼지정보가 해당 구단위로 정리되어 보이도록 해 두었다.
URL로 정보를 받아보면, 강남구 영등포구 ~~ 종로구 중랑구 강남구 영등포구 ~~ 이런식으로 반복된다. 반복되는 부분을 찾아 subList로 잘라준 후 한번만 리스트에 나오도록해 두었다.



profile
https://github.com/jsw4215

2개의 댓글

comment-user-thumbnail
2020년 8월 21일

private void updateMapMarkers(StoreSaleResult result) {
resetMarkerList();

    if (result.stores != null && result.stores.size() > 0) {
        for (Store store : result.stores) {
            Marker marker = new Marker();
            marker.setTag(store);
            marker.setPosition(new LatLng(store.lat, store.lng));
            if ("plenty".equalsIgnoreCase(store.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_green));
            } else if ("some".equalsIgnoreCase(store.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_yellow));
            } else if ("fiew".equalsIgnoreCase(store.remain_stat)) {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_red));
            } else {
                marker.setIcon(OverlayImage.fromResource(R.drawable.marker_gray));
            }
            marker.setAnchor(new PointF(0.5f, 1.0f));
            marker.setMap(naverMap);
            marker.setOnClickListener(this);
            markerList.add(marker);
        }
    }
}

----------------------------------- 위 4번째 라인" for (Store store"가 빨강색 언더라인입니다.----------
3일째 골머리 썩는중입니다.. 왕초보로 남들이 만들거 따라하는건데도 잘 안되네요.
부탁드립니다. ,참고로 Store라는 다른 Activity가 따로 있습니다.

아래 Store Activity 내용 첨부드립니다.
ackage com.example.loginfirebase;

import com.naver.maps.map.overlay.OverlayImage;

import java.util.Comparator;

public class Store implements Comparable {

Store store = new Store();
public String addr;
public String code;
public String created_at;
public double lat;
public double lng;
public String name;
public String remain_stat;
public String stock_at;
public String type;

public int getAmount() {
    if ("plenty".equalsIgnoreCase(remain_stat)) {
        return 0;
    } else if ("some".equalsIgnoreCase(remain_stat)) {
        return 1;
    } else if ("few".equalsIgnoreCase(remain_stat)) {
        return 2;
    } else if ("empty".equalsIgnoreCase(remain_stat)) {
        return 3;
    } else if ("break".equalsIgnoreCase(remain_stat)) {
        return 4;
    } else {
        return 5;
    }
}

@Override
public int compareTo( Store other) {
    return getAmount() - other.getAmount();
}

public static class NameSorter implements Comparator<Store> {
    public int compare( Store store1, Store store2 ) {
        store1.name = ( store1.name == null) ? "" : store1.name;
        store2.name = ( store2.name == null) ? "" : store2.name;
        return store1.name.compareTo(store2.name);
    }
}

public static class StockAtSorter implements Comparator<Store> {
    public int compare( Store store1, Store store2 ) {
        store1.stock_at = ( store1.stock_at == null) ? "" : store1.stock_at;
        store2.stock_at = ( store2.stock_at == null) ? "" : store2.stock_at;
        return store1.stock_at.compareTo(store2.stock_at);
    }
}

}

1개의 답글