2026/02/19

오늘의 이야기

조금 지나긴 했지만, 이슈가 되었던 요소수, 그걸 판매하는 주유소 정보를 공공데이터 포털에서 제공하기 시작했다.  현재 (2021.12.20 기준)는 111개 주유소의 정보만 제공이 되고 있는 것 같으나, 일단 그걸 이용해서 데이터 제공을 하는 앱을 구성해 보았다.  이번 앱은 이미 만들었던 앱에 retrofit 서비스 호출을 구성하여 데이터를 읽어 오는 부분만 구성해 보았다. 


 


가는길에 앱 메인



 이 앱은 현재 가는길에 들려야 하는 곳을 찾아서 기록해 두기 위해서 만들었던 앱이다.  집 가는 길에,  학교 가는 길에, 어디 가는 길에... 들렸다 오라는 엄마의 , 여보의 말을 기억해야 하나... 깜밖 거리는 나를 위해서...


 


본론으로 와서 공공데이터 포털에서 데이터 활용 신청을 해 보자.


공공데이터 포털



data.go.kr 을 접속해 보면 위와 같이 요소수 중점 유통 주유소 재고현황 API에 대한 활용 정보가 제공되고 있음을 알 수 있다. 저 팝업을 클릭해 들어가 보면 활용에 대한 정보를 볼 수 있다. 


 


api 활용 안내



 


활용신청을 클릭해 보자. 신청 단계는 사용신청 사항을 입력해서 확인을 진행하게 되면 바로 처리가 되고 있어서 자세한 설명을 접어 두고 신청 후 화면을 보면 아래와 같이 일반 인증키가 2개 보이는데,  source 작성 시에는 decoding 된 것을 가져다가 source 작성 시에 encoding 해서 전달하는 방식으로 처리를 하면 좋을 것 같다. 


 


api 활용 승인



 


 


api 호출시 파라미터 설명


api 구조체 설명



아래로 내려보면 데이터 구조를 설명하는 부분들이 나와 있으므로 이것을 근간으로 해서 data 구조 class 을 선언해 보자.


 


import com.google.gson.annotations.SerializedName;

import java.util.ArrayList;

/**
* 2021.11.26 요소수 주요소 정보
*/
public class AdBlueBean {
@SerializedName("currentCount")
int currentCount;
@SerializedName("data")
ArrayList<InventoryModelBean> data ;
@SerializedName("matchCount")
int matchCount ;
@SerializedName("page")
int page ;
@SerializedName("perPage")
int perPage;
@SerializedName("totalCount")
int totalCount ;

public void setPage(int page) {
this.page = page;
}

public int getPage() {
return page;
}

public void setPerPage(int perPage) {
this.perPage = perPage;
}

public int getPerPage() {
return perPage;
}

public void setMatchCount(int matchCount) {
this.matchCount = matchCount;
}

public int getMatchCount() {
return matchCount;
}

public void setCurrentCount(int currentCount) {
this.currentCount = currentCount;
}

public int getCurrentCount() {
return currentCount;
}

public void setTotalCount(int totalCount) {
this.totalCount = totalCount;
}

public int getTotalCount() {
return totalCount;
}

public void setData(ArrayList<InventoryModelBean> data) {
this.data = data;
}

public ArrayList<InventoryModelBean> getData() {
return data;
}
}

위 그림에 inventory_api 을 보면 위 AdBlueBean과 같이 구조체를 가지고 있고 실제 상세 자료는 inventory_model 형식의 데이터 구조를 가지고 있다. 그래서 일단은 AdBlueBean class을 선언하고 model 데이터는 아래처럼 InventoryBean class을 구성하였다. 


 


import com.google.gson.annotations.SerializedName;

/**
* 2021.11.26 요소수 재고량 정보
*/
public class InventoryModelBean {
@SerializedName("addr")
String addr ; // 주유소 주소
@SerializedName("code")
String code ; // 주유소 코드
@SerializedName("inventory")
String inventory ; // 재고량
@SerializedName("lat")
String lat ; // 위도
@SerializedName("lng")
String lng ; // 경도
@SerializedName("name")
String name ; // 주유소 이름
@SerializedName("openTime")
String openTime ; // 영업 시간
@SerializedName("price")
String price ; // 가격
@SerializedName("regDt")
String regDt ; // 업데이트 일시
@SerializedName("tel")
String tel ; // 전화번호
@SerializedName("color")
String color ; // 잔량 수량 구간

public void setAddr(String addr) {
this.addr = addr;
}

public String getAddr() {
return addr;
}

public void setCode(String code) {
this.code = code;
}

public String getCode() {
return code;
}

public void setInventory(String inventory) {
this.inventory = inventory;
}

public String getInventory() {
return inventory;
}

public void setLat(String lat) {
this.lat = lat;
}

public String getLat() {
return lat;
}

public void setLng(String lng) {
this.lng = lng;
}

public String getLng() {
return lng;
}

public void setName(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void setOpenTime(String openTime) {
this.openTime = openTime;
}

public String getOpenTime() {
return openTime;
}

public void setPrice(String price) {
this.price = price;
}

public String getPrice() {
return price;
}

public void setRegDt(String regDt) {
this.regDt = regDt;
}

public String getRegDt() {
return regDt;
}

public void setTel(String tel) {
this.tel = tel;
}

public String getTel() {
return tel;
}

public void setColor(String color) {
this.color = color;
}

public String getColor() {
return color;
}

}

@SerializedName는 api로 제공받는 데이터 구조체가 json 형식으로 받게 될 것인데, Gson을 이용해 데이터를 받을 때 연결되는 정보라는 정도로 이해를 하면 될 것 같다.  실제 source 코드에서 사용되는 변수명과 같은 경우가 되더라도 데이터를 연결해 주기 위해서는 api을 제공하는 쪽에서 제시한 이름으로 일치시켜 주어야 한다. 


 


이제 통신을 구성하기 위해서 retrofit 에 대한 설정을 상기해 보자.


 


https://billcorea.tistory.com/26



 


안드로이드 앱 만들기 API 연동을 위한 retrofit 구현 이야기.


안드로이드 폰에서 Restful 호출을 위해서 StringRequest 을 사용해 보기도 했지만, Retrofit 을 알고 나서는 간편하게 잘 쓰게 되었다. data.go.kr 에서 제공하는 공공데이터를 이용해서 앱을 만들어 보고


billcorea.tistory.com




이 글에서도 잠깐 설명을 하기는 했었는데,  지금 보니 조금 허접한 느낌이 드는 건 뭘까?


 


일단, gradle(Module) 파일에서 설정은 


 


    implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-gson:2.7.2'
implementation 'com.squareup.retrofit2:converter-simplexml:2.1.0'

이렇게 3줄이 들어가야 하는 데, gson 은 데이터를 json 형식으로 받을 거라서 필요하고, simplexml 은 데이터를 xml 형식으로 받을 때 필요하나, 이번에는 사용하지 않기 때문에 없어도 뭐... 


 


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

/**
* http 호출을 위한 API
* 앱 등록 및 사용자 설정 필수 category
*
*/

public interface RetrofitApi {

/**
* 2021.11.26 요소수 데이터 수집
*/
@GET("/api/uws/v1/inventory")
Call<AdBlueBean> getAdBlueData(@Query(value="serviceKey", encoded = true) String serviceKey,
@Query("page") int iPage,
@Query("perPage") int perPage,
@Query(value="cond[addr::LIKE]", encoded = true) String addressLike
);

/**
* 전체 목록을 수집
* @param serviceKey
* @param iPage
* @param perPage
* @return
*/
@GET("/api/uws/v1/inventory")
Call<AdBlueBean> getAllAdBlueData(@Query(value="serviceKey", encoded = true) String serviceKey,
@Query("page") int iPage,
@Query("perPage") int perPage
);

}

서비스 호출을 하기 위한 interface을 구현해 보았는데, 실상은 getAllBlueData 함수만 사용하였다. 위에 있는 getAdBlueData 함수는 조회 조건에 addr에 like 검색을 지원하는 호출이 필요할 때 사용해 볼 수 있다. 


 


이제 activity에서 호출을 해 보자.



....

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

public class MapsFragment extends Fragment implements OnCompleteListener<Void>,
OnBackPressedListener,
OnMapReadyCallback,
GoogleMap.OnMapLongClickListener,
GoogleMap.OnMarkerClickListener {

GoogleMap mMap;

....

RetrofitApi service;

....

Context context;

public static MapsFragment newInstance(String keyWord, float latitude, float longitude) {
MapsFragment fragment = new MapsFragment();
Bundle args = new Bundle();
...
return fragment;
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {

context = container.getContext();
...

binding = FragmentMapsBinding.inflate(getLayoutInflater());

...

return binding.getRoot();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
SupportMapFragment mapFragment =
(SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.map);

...

// 사용을 위해서 선언
retrofitBlue = new Retrofit.Builder()
.baseUrl(serviceURL) // 기본 url 은 공공데이터 포털에서 확인 가능
.addConverterFactory(GsonConverterFactory.create())
.build();
// interface 와 연결하기 위해서 선언
serviceBlue = retrofitBlue.create(RetrofitApi.class);

...

}

private void doFindAllAdBlue(GoogleMap googleMap) {

infoBinding = AdblueInfoBinding.inflate(getLayoutInflater());

for(int i=1 ; i < 13 ; i++) {
// 서비스 호출을 통해서 데이터을 구해옴 현재는 페이지다 10건씩 수신 하도록 선언
serviceBlue.getAllAdBlueData(serviceKey, i, 10)
.enqueue(new Callback<AdBlueBean>() {
@Override
public void onResponse(Call<AdBlueBean> call, Response<AdBlueBean> response) {
Log.e(TAG, "Total=" + response.body().getTotalCount());
Log.e(TAG, "Current=" + response.body().getCurrentCount());
for (InventoryModelBean inventoryModelBean : response.body().getData()) {
...
// 가져온 데이터를 화면에 보여 주도록 설정
infoBinding.textName.setText(inventoryModelBean.getName());
infoBinding.textPhone.setText(inventoryModelBean.getTel());
infoBinding.textPrice.setText(inventoryModelBean.getPrice()+"원");
infoBinding.textInventory.setText(inventoryModelBean.getInventory()+"L");
LatLng sydney = new LatLng(Double.parseDouble(inventoryModelBean.getLat()), Double.parseDouble(inventoryModelBean.getLng()));
googleMap.addMarker(new MarkerOptions().position(sydney).title(inventoryModelBean.getName())
.icon(BitmapDescriptorFactory.fromBitmap(createDrawableFromView(context, infoBinding.getRoot()))));
}
}

@Override
public void onFailure(Call<AdBlueBean> call, Throwable t) {
Log.e(TAG, "error=" + t.toString());
}
});
}
}

...

}

source에 필요해 보이는 부분들만 발췌를 했는데, retrofitBlue라는 설정을 통해서 retrofit 사용을 위한 기본 설정을 하고 service을 호출해서 앞에서 구현한 interface을 호출해서 서비스 호출을 통해서 데이터를 가져오면 된다. 


response 가 json으로 오기 때문에 앞에서 선언했던 AdBlueBean을 이용해서 데이터를 받아서 분석해서 사용하면 된다.  어ㅣ예시에서는 for 구문을 통해서 12번 데이터를 받아오는 것처럼 되어 있는데. 현재까지 알려진 것으로는 api 가 제공하는 데이터가 110개 정도 되고 한 번에 10개의 데이터를 제공하고 있기 때문에 반복해서 전제 자료를 받아오기 위함이다. 


 


이렇게 해서 데이터가 오는 걸 보면 다음과 같이 구현이 되었다. 


 


 



데이터 조회 처리 앱 구동 예시


map에 custom marker을 구현해서 표시가 되고 있지만, 아직은 모양이 이쁘지는 않은 것 같다. 이 부분은 좀 더 고민을 해 볼 필요가 있을 것 같다.


 


이 걸로 오늘 앱 만들기...


 


 


 





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

앱을 만들면서 지도가 필요한 경우 카카오맵도 해 봤고, 구글맵도 구현해 보기는 했다. 카카오맵을 사용하면 나름 카카오의 지원(?)을 받아 이런 저런 것들을 해 볼 수 있다.  


 


우선 좋은 점은 카카오가 지원하는 다른 서비스들과 연동이 수월하다. 하다 못해 카카오 내비를 호출해서 바로 길찾기를 지원받을 수 있으니... 다만, onestore 에 앱을 등록하는 것이 번거롭다(?) 여러번 시도를 했는데, 어떤 방법으로 앱을 검증 하는 지는 모르겠으나, 번번히 실패 ... 그래서 구글맵으로 수정앱을 등록해보니, 한번에 패스~


 


그래서 일단 구글맵에 지도를 표현해 보도록 하겠다. 


일단 기본적으로 구현이 수월한 방법... android studio 에서 new - acitivity - gallery 를 선택하면 ...


 


액티비티 선택



여러 종류의 activity 가 있는데, 그 중에서 지도가 들어있는 maps 을 선택하면 기본적인 설정은 android studio 가 해 준다.


 


그 다음은 manifest 파일에 meta 정보를 설정하는 것이 나온다. meta에 google-api-key 을 설정해 주어야 하는데, 


<application
android:allowBackup="true"
android:icon="@mipmap/ic_info_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_info_logo_round"
android:supportsRtl="true"
android:theme="@style/Theme.OpdGang1127">

<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="@string/API_KEY" />

api-key 는 그럼 어디서 받아오는 가 ?


https://console.cloud.google.com 



 


Google Cloud Platform


하나의 계정으로 모든 Google 서비스를 Google Cloud Platform을 사용하려면 로그인하세요.


accounts.google.com




cloud platform 의 console 화면에서 사용자 인증 정보에 들어가서 api 키를 추가 하거나 기존에 등록했던 것에서 가져 오면 된다.


 


google dash board



이미 등록해 놓은 것이 있으니 그걸 하나 선택해서 상세 내용을 볼껀데,


api 세부 설정



이 내용에서 한가지 생각해 볼 것은 android 앱을 만들고 있기 때문에  애플리케이션 제한사항을 선택할 때 꼭 안드로이드를 선택해야만 할 것 같은 생각이 들었던 경험이 있는 데,  그 때는 주의해야할 부분이 인증서 해쉬값을 등록해 줄 때 debug 용 release 용 두개를 각각 설정해 주어야 하고, 그것들을 다 project에  찾아서 설정해 주어야 했던 것 같은 기억이 있다.


 


그래서 이번에는 애플리케이션 제한사항은 설정하지 않았다. 다만, api 제한 사항을 선택해서 maps sdk 와 firebase 부분만 선택해서 사용하는 것으로 설정하고,  api key는 오른쪽 상단에 있는 apikey을 복사해서 manifest 파일에 등록해 주었다.


 


저 제한사항 둘 중 하나를 선택해 주지 않으면 메일이 자주 온다. google 에서 궁시렁 궁시렁...


아무튼 이렇게 설정이 끝나고 나면 이제 activity 를 구성해 볼 차례 인데, 기본적으로 maps activity 을 선택 했기 때문에 기본 code 작업은 되어 있게 된다. 


 


그래서 그 안에 필요한 코드등을 추가해 보면 된다.  다음과 같이...


import androidx.core.app.ActivityCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.recyclerview.widget.LinearLayoutManager;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import com.billcoreatech.opdgang1127.DataBase.DBHandler;
import com.billcoreatech.opdgang1127.JejuFD6.JejuFD6Adapter;
import com.billcoreatech.opdgang1127.JejuFD6.JejuFD6infoBean;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.UiSettings;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.billcoreatech.opdgang1127.databinding.ActivityMapsBinding;

import java.util.ArrayList;

public class MapsActivity extends FragmentActivity implements OnMapReadyCallback {

private static final String TAG = "MapsActivity";
private GoogleMap mMap;
private ActivityMapsBinding binding;
ArrayList<JejuFD6infoBean> dataBeans ;
JejuFD6Adapter adapter ;
DBHandler dbHandler ;
UiSettings mUiSettings;
String placeName;
double lat;
double lon;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

binding = ActivityMapsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

// Obtain the SupportMapFragment and get notified when the map is ready to be used.
SupportMapFragment mapFragment = (SupportMapFragment) getSupportFragmentManager()
.findFragmentById(R.id.map);
mapFragment.getMapAsync(this);
Intent intent = getIntent();
Bundle extras = intent.getExtras();
placeName = extras.getString("placeName");
Log.e(TAG, "y=" + extras.getString("y").trim());
Log.e(TAG, "x=" + extras.getString("x").trim());
lat = Double.parseDouble(extras.getString("y").trim());
lon = Double.parseDouble(extras.getString("x").trim());
}

@SuppressLint("Range")
@Override
public void onMapReady(GoogleMap googleMap) {
mMap = googleMap;
mUiSettings = googleMap.getUiSettings();
mUiSettings.setZoomControlsEnabled(true);
mUiSettings.setMyLocationButtonEnabled(true);

LatLng sydney = new LatLng(lat, lon);
mMap.addMarker(new MarkerOptions().position(sydney).title(placeName));
mMap.moveCamera(CameraUpdateFactory.newLatLngZoom(sydney, 15));
dataBeans = new ArrayList<>();
adapter = new JejuFD6Adapter(MapsActivity.this, dataBeans, "url");
dbHandler = DBHandler.open(MapsActivity.this);
Cursor rs = dbHandler.selectInfo(placeName);
while (rs.moveToNext()) {
JejuFD6infoBean dataBean = new JejuFD6infoBean();
dataBean.setPlace_name(rs.getString(rs.getColumnIndex("place_name")));
dataBean.setPhone(rs.getString(rs.getColumnIndex("phone")));
dataBean.setRoad_address_name(rs.getString(rs.getColumnIndex("road_address_name")));
dataBean.setCategory_name(rs.getString(rs.getColumnIndex("category_name")));
dataBean.setUrl(rs.getString(rs.getColumnIndex("url")));
dataBean.setY(rs.getString(rs.getColumnIndex("y")));
dataBean.setX(rs.getString(rs.getColumnIndex("x")));
Log.e(TAG, dataBean.getUrl() == null ? "null" : dataBean.getUrl()) ;
dataBeans.add(dataBean);
}
binding.listPlace.setAdapter(adapter);
binding.listPlace.setLayoutManager(new LinearLayoutManager(getApplicationContext()));

adapter.setOnItemClickListener(new JejuFD6Adapter.OnItemClickListener() {
@Override
public void onItemClick(View v, int position) {
if (dataBeans.get(position) != null){
if (dataBeans.get(position).getUrl() != null) {
String viewUrl = "" ;
if (dataBeans.get(position).getUrl().startsWith("http://") ||
dataBeans.get(position).getUrl().startsWith("https://")) {
viewUrl = dataBeans.get(position).getUrl();
} else {
viewUrl = "https://" + dataBeans.get(position).getUrl();
}
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(viewUrl));
startActivity(browserIntent);
}
}
}
});
}

@Override
public void onBackPressed() {
super.onBackPressed();

setResult(RESULT_OK);
finish();

}
}

 onCreate 와 onMapReady 부분을 구현해 주면 된다. 위 예시는 db 에서 데이터를 읽어와서 map 에 marker 을 표시하도록 구현하였다.


 


 





 


지도 구현은 이런식으로 구성이 끝~


 


 





오늘의 이야기

화면을 만들다 보면 간혹 뭔가를 선택해야 하는 경우가 발생하게 된다. 그럴 때 콤보라고 쓰고 스피너라고 읽는 layout item에 대한 이야기를 잠시해 두고 넣어가고자 한다.  이유는 간혹 작업을 하면서 사용하게 되는데, 나도 인간인지라 깜빡깜빡해서 사용할 때마다 찾아봐야 한다는 것이다. ㅋ~


 


일단 화면에 spinner 을 넣는다 


<?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:weightSum="30"
tools:context=".MainActivity">

<Spinner
android:id="@+id/spAclass"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2" />

<EditText
android:id="@+id/editSearchKey"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"
android:ems="10"
android:hint="@string/msgSearchKey"
android:inputType="textPersonName" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/listData"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="22" />
</LinearLayout>

화면에 spinner 을 넣는다.  그러면 화면 design 은 다음과 같이 생긴다. 


spinner layout



디자인하는 동안에는 아무것도 들어 있지 않기 때문에... 위 그림과 같이 표현이 될 듯하고. 이제 activity에서 데이터를 넣는 것을 구현해 볼 차례인데, 


 


예시에서는 ArrayString 에 들어 있는 데이터를 가지고 그리는 것을 예시로 할 것이다.  그런 이유는 데이터를 넣는 방법에는 Activity에서 String Array로 지정해서 고정시키는 방법도 있을 테고, 


String[] codeItem = new String{'코드1', '코드2','코드3'};
...

아니면 ArrayList 만 선언해 두고 데이터를 db 에서 읽어오는 방식으로 구현할 수 도 있을 테니...


ArrayList<String> listSpinnerA ;

그렇게 선언하고 나서 onCreate() 에서는 초기화만 한 다음


listSpinnerA = new ArrayList<>();


...


dbHandler = DBHandler.open(getApplicationContext());
Cursor rs = dbHandler.selectClass(listSpinnerA.get(binding.spAclass.getSelectedItemPosition()), "B", "");
listSpinnerA.clear();
while (rs.moveToNext()) {
listSpinnerA.add(rs.getString(rs.getColumnIndex("classNameB")));
}
ArrayAdapter<String> spAdapter = new ArrayAdapter<>(getApplicationContext(),
android.R.layout.simple_spinner_dropdown_item, listSpinnerA);
binding.spBclass.setAdapter(spAdapter);
rs.close();
dbHandler.close();

...

db에서 데이터를 읽어서 ArrayList에 넣고 그 값을 이용해서 spinner을 위한 adapter을 선언해 주는 것이다.  여기서 또 보고 가야 할 것은 simple_spinner_dropdown_item을 선언하는 것인데, 이건 spinner을 모양을 선택해 주는 것이니, 참고하고, 다른 선언을 하고 싶은 경우는 찾아보고 변경해 주면 될 것 같다.


 


그럼... 선언한 화면이 어떻게 움직이는 지 봐야 할 듯...


 


 





 


실제로 구동하는 예시는 이런 모습으로 나타난다... 화면에는 spinner 가 3개가 있는데, 첫 번째 spinner 가 변할 때마다, 2번쨰을 다시 읽어오고, 2번째를 선택하면 3번쨰을 다시 읽어 오는 그런 모습으로 구동하게 된다.


 


이것으로 spinner을 구현해 보았다.


 





오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

재외 제주인 : 제주도를 떠나 사는 제주사람.


나도 그중에 한 명... 떠난 지 하도 오래되어 이제는 제주도를 가도 내가 모르는 곳에 온 것 같고, 그래서 주변에 어딜 가보고 싶어도 잘 알 수 없는 현실이 되었다.  그래서 찾아보기로 했다. 주변에 가고 싶은 곳이 어디에 있는 가? 


 


현실은 제주도가 아닌 이곳에서 어떻게 그걸 다 찾을 것인가? ㅋㅋㅋ 그래도 다행(?)인 것은 인터넷을 서핑하다 보면 다양한 사람들이 방문한 다양한 방문 기록들이 차고 넘쳐난다는 것이다. 


 


https://billcoreapython.tistory.com/40



 


파이썬으로 제주의 맛집을 찾아서 (3)


오늘은 정리되고 있는 자료를 타인(?)들과 공유하기 위해서... Firebase 의 Realtime Database 에 저장을 해 보도록 하겠다. import requests import sqlite3 import firebase_admin from firebase_admin import c..


billcoreapython.tistory.com




옆집(?)에 포스팅 한 것처럼... 맛집을 찾아 기록해 주는 글들은 많다. 그것들은 어떻게 정리할 것인가?  그것이 고민인 것이지...


 


정리된 자료를 받아서 이제 하나의 로컬 데이터를 구성해 보도록 하겠다.  저 글에서는 수집된 정보가 firebase 의 realtime database에 기록되어 있으니 그것을 받아서 내가 보고 싶은 자료로 만들어 보는 것이다. 


 


먼저 기록된 데이터의 구조를 볼까 ?


제주맛집 정보 샘플



이런 자료들이 여러 개 수집되어 있고, 매일처럼 갱신된 자료를 수집하고 있다. 이것들을 어떻게 활용할 것인지? 생각을 해 보자.


 


먼저 카테고리를 분해(?) 해서 구분을 조회 기준을 만든다.   음식점 > 한식 > 해물, 생선... 이런 식으로 들어 있으니 split('>')을 하게 되면 값이 나누어질 것이고, 이것을 기록해 두기로 했다. 


 


    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

jejuFD6 = FirebaseDatabase.getInstance().getReference("jejuFD6info");

jejuFD6.orderByKey().addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
int iCnt = 1 ;
dbHandler = DBHandler.open(MainActivity.this);
for(DataSnapshot ds : snapshot.getChildren()) {
JejuFD6infoBean dataBean = ds.getValue(JejuFD6infoBean.class);
Log.e(TAG, iCnt + ")=" + dataBean.getPlace_name());
Log.e(TAG, dataBean.getCategory_name());
/*
* 카테고리를 분해해서 코드정보를 만들기
*/
String[] className = dataBean.getCategory_name().replaceAll(" ", "").split(">");
long nCnt = 0 ;
Log.e(TAG, "length=" + className.length);
if (className.length > 1) {
nCnt = dbHandler.appendClassCode("A", className[1]);
Log.e(TAG, " A class=" + nCnt);
}
if (className.length > 2) {
nCnt = dbHandler.appendClassCode("B", className[2]);
Log.e(TAG, " B class=" + nCnt);
}
if (className.length > 3) {
nCnt = dbHandler.appendClassCode("C", className[3]);
Log.e(TAG, " C class=" + nCnt);
}
Log.e(TAG, dataBean.getY() + "/" + dataBean.getX());
nCnt = dbHandler.updateCnt(dataBean.getY(), dataBean.getX(), dataBean.getPlace_name()) ;
Log.e(TAG, " cntData =" + nCnt);
try {
Log.e(TAG, dataBean.getUrl());
} catch (Exception e) {

}
nCnt = dbHandler.insert(dataBean);
Log.e(TAG, " infoData =" + nCnt);
iCnt++;
}
dbHandler.close();
}

@Override
public void onCancelled(@NonNull DatabaseError error) {

}
}) ;
}

코드를 보면 split(">") 하기 전에 공백을 제거하는 처리를 먼저 했다.  데이터를 받아서 보니 글자들 사이에 들어가 있는 공백은 별 필요가 없는데, 그리고 나누어 기록을 했다.   그걸 하기 위해서 먼저 데이터를 저장할 dbHelper을 구현해 보았다.


    /**
* aClassCode, bClassCode, cClassCode : 코드 분류 1,2,3 차
* cntData : 위치별-상호별 중복 건수
* infoData : 상세 정보 y,x,place_name 으로 cntData 와 연계
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {

StringBuffer sb = new StringBuffer();
sb.append("create table aClassCode");
sb.append("( _id integer primary key autoincrement, ");
sb.append(" className text ");
sb.append(" ) ");
db.execSQL(sb.toString());

sb = new StringBuffer();
sb.append("create table bClassCode");
sb.append("( _id integer primary key autoincrement, ");
sb.append(" className text ");
sb.append(" ) ");
db.execSQL(sb.toString());

sb = new StringBuffer();
sb.append("create table cClassCode");
sb.append("( _id integer primary key autoincrement, ");
sb.append(" className text ");
sb.append(" ) ");
db.execSQL(sb.toString());

sb = new StringBuffer();
sb.append("create table cntData");
sb.append("( _id integer primary key autoincrement, ");
sb.append(" y text, ");
sb.append(" x text, ");
sb.append(" place_name text, ");
sb.append(" atCnt number ");
sb.append(" ) ");
db.execSQL(sb.toString());

sb = new StringBuffer();
sb.append("create table infoData");
sb.append("( _id integer primary key autoincrement, ");
sb.append(" y text, ");
sb.append(" x text, ");
sb.append(" address_name text, ");
sb.append(" category_group_code text, ");
sb.append(" category_group_name text, ");
sb.append(" category_name text, ");
sb.append(" distance text, ");
sb.append(" id text, ");
sb.append(" phone text, ");
sb.append(" place_name text, ");
sb.append(" place_url text, ");
sb.append(" road_address_name text, ");
sb.append(" url text, ");
sb.append(" regTime text ");
sb.append(" ) ");
db.execSQL(sb.toString());

}

 


오늘의 코딩은 여기까지...  이제 기본 화면을 구성해 봐야 할 텐데...


 


카테고리 별 조회 샘플



 


오늘의 작업은 여기 까지... 카테고리를 선택하면 해당 카테고리별로 리스트가 나오는 화면을 만들었다.





오늘의 이야기

https://developer.android.com/guide/topics/connectivity/telecom/selfManaged?hl=ko 



 


통화 앱 빌드  |  Android 개발자  |  Android Developers


통화 앱 빌드 통화 앱을 통해 사용자는 기기에서 음성 통화 또는 영상 통화를 받거나 걸 수 있습니다. 다음 스크린샷과 같이 통화 앱은 통화 시 기본 전화 앱 인터페이스를 사용하는 대신 자체


developer.android.com




이런 건 무리일까?


 


전화 기본 앱을 만들어 본다는 것은... 개발자 문서를 봐서는 도저히 감당이 되지 않을 것 같은 생각이 들기는 하지만,


그런데, 저런 앱을 하나 만들었다쳐도 나 말고 누가 사용할 것인가?


누군가에게 애써 만든 걸 줄 수 있나? ㅋㅋㅋ


 





오늘의 이야기


#스하리1000명프로젝트,
有时候和外劳说话很难,对吧?
我制作了一个简单的应用程序,可以帮助您!你用你的语言写作,其他人用他们的语言看到它。
它根据设置自动翻译。
超级方便,可以轻松聊天。有机会就来看看吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

아직 잘 모르겠다. 내 앱에서 알림이 왔을 때, 내 손목에 있는 워치에도 알림이 동시에 뜨고 하는 것들에 대해서, 이제 조금 이해가 되기 하기는 하지만, 아직 정리가 잘 되지 않는다.


 


https://codechacha.com/ko/notifications-in-android/



 


안드로이드의 다양한 Notification 종류와 구현 방법


Android의 다양한 Notification 종류와 구현 방법에 대해서 정리하였습니다. 먼저 Notification Channel을 등록해야 하고, Notification은 BigText, BigPicture, Inbox, Messaging, Media Style 등으로 구현할 수 있습니다. 또


codechacha.com




여러 가지 구현에 대한 이야기를 적어 놓고 있는데, 아직 적용을 해 보지 못해서 다 이해가 되는 것은 아니기는 하지만,


조금은 알 것 같기도 하고...


 


일단은 내가 만든 앱에서 알림이 뜨면 내 손목에 있는 워치에도 알림이 뜨기는 한다. 그리고 한 가지 꼭 기억하고 가야 할 것은 내 폰의 설정에서 알림 부분에 내가 만든 앱의 알림이 허용되어 있는지 봐야 한다. 또한 워치 앱에서도 (갤럭시 워치 4는 Galaxy Wearable)에서 알림 부분에 내 앱의 알림에 대한 설정이 허용되어 있는지 봐야 한다.


난 그것도 이해를 하지 못해서 몇 날(?)을 허송세월을 보낸 것 같다.


 


ㅋ... 


 


여하튼 저 알림을 다 이해하는 날이 올때까지.... 파이팅~





오늘의 이야기

안드로이드 개발자 문서에서는 아래 링크와 같이 예시를 볼 수 있다.  오늘은 그중에서 그냥 쉽게 EditBox 에서 입력한 내용을 클립보드에 저장해서 이용하는 예제를 만들어 보겠다. 


 


https://developer.android.com/guide/topics/text/copy-paste?hl=ko#java 



 


복사하여 붙여넣기  |  Android 개발자  |  Android Developers


복사하여 붙여넣기 Android는 복사하여 붙여넣기를 지원하는 강력한 클립보드 기반 프레임워크를 제공합니다. 텍스트 문자열, 복잡한 데이터 구조, 텍스트 및 바이너리 스트림 데이터, 애플리케


developer.android.com




 


먼저 내가 만드는 앱에서 이용할 부분을 찾아보자.  




자주 사용할 수 있는 부분이 Editbox 가 될 것 같다.


예을 들어 오른쪽 그림과 같은 예시 화면이 있다면


 


제목 에 입력된 내용을 클립보드에 넣었다가


내용에 붙여넣는 것을 해 보기로 하겠다.


 


 


 


 


 


 


 


 


 


import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;

...

/**
* A simple {@link Fragment} subclass.
* Use the {@link BoardAppendFragment#newInstance} factory method to
* create an instance of this fragment.
*/
public class BoardAppendFragment extends Fragment implements OnBackPressedListener {

...

ClipboardManager clipboard ;
ClipData clipData ;

public BoardAppendFragment() {
// Required empty public constructor
}

...

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...

clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);

...

binding.edTitle.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
clipData = ClipData.newPlainText("title", binding.edTitle.getText().toString());
clipboard.setPrimaryClip(clipData);
Toast.makeText(getContext(), getString(R.string.msgClipboardCutText), Toast.LENGTH_SHORT).show();
return true;
}
});
return binding.getRoot();
}

...
}

내용은 간단하다... Clipboard 와 Clipdata 을 선언하고 clipbaord 을 구현했다.  그리고 화면에 있는 editbox 의 longclick 이벤트을 이용해서 editbox 에 입력된 텍스트를 클릭보드에 담아내는 것 까지만 구현을 했다. 


 


붙여넣기는 붙여넣을 곳에 가서 마찬가지로 long click 을 하면 붙여넣기 메뉴가 안드로이드에서 표시를 해 주니 그것을 이용하면 된다 


 





실행되는 모습은 위 동영상과 같이 제목에 글자를 입력하고 long click 을 해서 클립보드에 붙여진 것을 확인후 


내용에 가서 long click 으로 붙여 넣기를 해 보는 것이다.   


 


 


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle,羽毛球俱乐部必备应用!
👉 比洞赛 – 记录分数并寻找对手 🎉
适合任何地方,独自一人、与朋友一起或在俱乐部! 🤝
如果你喜欢羽毛球,一定要尝试一下

前往应用程序👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

조금 지나긴 했지만, 이슈가 되었던 요소수, 그걸 판매하는 주유소 정보를 공공데이터 포털에서 제공하기 시작했다.  현재 (2021.12.20 기준)는 111개 주유소의 정보만 제공이 되고 있는 것 같으나, 일단 그걸 이용해서 데이터 제공을 하는 앱을 ...