2026/02/15

오늘의 이야기






귀요미...
어부바...
가득 가득 채워주기 바래





반응형





























오늘의 이야기




앞에 이야기를 보고 준비를 잘 했다면 이제 하나씩 만들어 보자.


카카오 지도 준비는 되었으니, 이제 카카오 개발자 페이지에서 주변 정보를 수집할 방법에 대하여 생각해 보자, 구글에서도 place API 을 지원하고 있으나, 사용에 부담이 되는 것은 아무래도 간혹 나오는 영문 데이터 떄문이다, 아직은 한국적인 느낌이 들지 않는다.


    <uses-permission android:name="android.permission.INTERNET" />

먼저 manifasts 파일에 인터넷 사용을 위한 권한등록을 한다. 데이터는 retrofit API을 이용해서 받아올 꺼니까


    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'

다음은 gradle 파일에 retrofit 사용을 위해서 implementation 을 선언한다. 나는 데이터를 json 방식으로 받아서 처리를 할 것이라서 simplexml 을 없어도 되나. 데이터를 혹시나 xml 구조로 받아야 하는 경우가 있을 때는 기술할 필요가 있다.




이제 카카오개발자 페이지에서 새로 앱을 만들기 등록을 하고, API 키를 받는다...(저 그림의 키는 일부이니 붙여넣기를 해도 소용이 없을 듯...)




그리고 플랫폼에 android 앱에 대한 정보를 하나 등록을 해야 하고, 키 해시는 앱을 실행해서 나오는 키 해시 값을 등록해 주어야 카카오 지도를 활용할 수 있다.


    public PackageInfo getPackageInfo(final Context context, int flag) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), flag);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "Unable to get PackageInfo", e);
}
return null;
}

public String getKeyHash(Context context) {
PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES);
if (packageInfo == null)
return null;

for (Signature signature : packageInfo.signatures) {
try {
MessageDigest md = MessageDigest.getInstance("SHA");
md.update(signature.toByteArray());
return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
} catch (NoSuchAlgorithmException e) {
Log.e("getKeyHash", "Unable to get MessageDigest. signature=" + signature, e);
}
}
return null;
}

구글링을 해보면 다 나오겠지만, 위와 같이 코드를 넣어 실행 결과를 받아보면 해시값을 알 수 있다.


이제 주변 정보를 얻기 위해서. 카카오의 로컬 서비스를 호출할 준비를 해 보자.


https://developers.kakao.com/docs/latest/ko/local/dev-guide



 


Kakao Developers


카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.


developers.kakao.com




아직 까지는 (2021.08.현재) rest 호출만 지원하고 있다 하니, 앞에서 준비할 것 처럼 http 통신을 해서 결과를 수집해야 할 것 같다.


내가 사용할 방법은 키워드를 통해서 주변을 찾아 보는 것이다. 




 


이런 내용이 있는 위치 부터 읽어 본다.  그리고 응답을 받아서 관리하기 위해서 아래와 같이 응답 구조체 class 을 만들었다.


package ...

import com.google.gson.annotations.SerializedName;

import java.util.ArrayList;

public class ResponseBean {
@SerializedName("meta")
Meta meta ;
@SerializedName("documents")
ArrayList<Documents> documents ;

public Meta getMeta() {
return meta;
}

public ArrayList<Documents> getDocuments() {
return documents;
}

public void setMeta(Meta meta) {
this.meta = meta;
}

public void setDocuments(ArrayList<Documents> documents) {
this.documents = documents;
}

public class Meta {
@SerializedName("same_name")
SameName sameName ;
@SerializedName("pageable_count")
int pageableCount ;
@SerializedName("total_count")
int totalCount ;
@SerializedName("is_end")
boolean isEnd ;

public SameName getSameName() {
return sameName;
}

public int getPageableCount() {
return pageableCount;
}

public int getTotalCount() {
return totalCount;
}

public boolean isEnd() {
return isEnd;
}

public void setSameName(SameName sameName) {
this.sameName = sameName;
}

public void setPageableCount(int pageableCount) {
this.pageableCount = pageableCount;
}

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

public void setEnd(boolean end) {
isEnd = end;
}
}

public class Documents {
@SerializedName("place_name")
String placeName ;
@SerializedName("distance")
String distance ;
@SerializedName("place_url")
String placeUrl ;
@SerializedName("category_name")
String categoryName ;
@SerializedName("address_name")
String addressName ;
@SerializedName("road_address_name")
String roadAddressName ;
@SerializedName("id")
String id ;
@SerializedName("phone")
String phone;
@SerializedName("category_group_code")
String categoryGroupCode ;
@SerializedName("category_group_name")
String categoryGroupName ;
@SerializedName("x")
String posX ;
@SerializedName("y")
String posY ;

public String getAddressName() {
return addressName;
}

public String getCategoryGroupCode() {
return categoryGroupCode;
}

public String getCategoryGroupName() {
return categoryGroupName;
}

public String getCategoryName() {
return categoryName;
}

public String getDistance() {
return distance;
}

public String getId() {
return id;
}

public String getPhone() {
return phone;
}

public String getPlaceName() {
return placeName;
}

public String getPlaceUrl() {
return placeUrl;
}

public double getPosX() {
return Double.parseDouble(posX);
}

public double getPosY() {
return Double.parseDouble(posY);
}

public String getRoadAddressName() {
return roadAddressName;
}

public void setAddressName(String addressName) {
this.addressName = addressName;
}

public void setCategoryGroupCode(String categoryGroupCode) {
this.categoryGroupCode = categoryGroupCode;
}

public void setCategoryGroupName(String categoryGroupName) {
this.categoryGroupName = categoryGroupName;
}

public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}

public void setDistance(String distance) {
this.distance = distance;
}

public void setId(String id) {
this.id = id;
}

public void setPhone(String phone) {
this.phone = phone;
}

public void setPlaceName(String placeName) {
this.placeName = placeName;
}

public void setPlaceUrl(String placeUrl) {
this.placeUrl = placeUrl;
}

public void setPosX(String posX) {
this.posX = posX;
}

public void setPosY(String posY) {
this.posY = posY;
}

public void setRoadAddressName(String roadAddressName) {
this.roadAddressName = roadAddressName;
}
}

public class SameName {
@SerializedName("region")
String[] region ;
@SerializedName("keyword")
String keyWord ;
@SerializedName("selected_region")
String selectedRegion;

public String getSelectedRegion() {
return selectedRegion;
}

public String getKeyWord() {
return keyWord;
}

public String[] getRegion() {
return region;
}

public void setRegion(String[] region) {
this.region = region;
}

public void setKeyWord(String keyWord) {
this.keyWord = keyWord;
}

public void setSelectedRegion(String selectedRegion) {
this.selectedRegion = selectedRegion;
}
}
}

부분 쪼개서 class 로 나누는 것도 좋기는 하나, 뭐 그러나 저러나 비슷한 것 같아서... 그냥 파일 하나에 다 담아서 정리를 했다. 그 다음은 이제 저 class 을 이용해서 호출하는 api 을 하나 만들어 보자 (2021.08.21 아래 소스 일부 수정)


package ...

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

/**
* http 호출을 위한 API
* 앱 등록 및 사용자 설정 필수 category
*
* query String 검색을 원하는 질의어 O
* category_group_code String 카테고리 그룹 코드 * 결과를 카테고리로 필터링을 원하는 경우 사용 X
* x String 중심 좌표의 X값 혹은 longitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능 X
* y String 중심 좌표의 Y값 혹은 latitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능 X
* radius Integer 중심 좌표부터의 반경거리. 특정 지역을 중심으로 검색하려고 할 경우 중심좌표로 쓰일 x,y와 함께 사용 * 단위 meter, 0~20000 사이의 값 X
* rect String 사각형 범위내에서 제한 검색을 위한 좌표. 지도 화면 내 검색시 등 제한 검색에서 사용 가능 * 좌측 X 좌표,좌측 Y 좌표, 우측 X 좌표, 우측 Y 좌표 형식 X
* page Integer 결과 페이지 번호 * 1~45 사이의 값 (기본값: 1) X
* size Integer 한 페이지에 보여질 문서의 개수 * 1~15 사이의 값 (기본값: 15) X
* sort String 결과 정렬 순서, distance 정렬을 원할 때는 기준 좌표로 쓰일 x, y와 함께 사용 * distance 또는 accuracy (기본값: accuracy) X
*
* category_group_code String 카테고리 코드 O
* x String 중심 좌표의 X값 혹은 longitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능. (x,y,radius) 또는 rect 필수
* y String 중심 좌표의 Y값 혹은 latitude * 특정 지역을 중심으로 검색하려고 할 경우 radius와 함께 사용 가능. (x,y,radius) 또는 rect 필수
* radius Integer 중심 좌표부터의 반경거리. 특정 지역을 중심으로 검색하려고 할 경우 중심좌표로 쓰일 x,y와 함께 사용. 단위 meter, 0~20000 사이의 값 (x,y,radius) 또는 rect 필수
* rect String 사각형 범위내에서 제한 검색을 위한 좌표 * 지도 화면 내 검색시 등 제한 검색에서 사용 가능 * 좌측 X 좌표, 좌측 Y 좌표, 우측 X 좌표, 우측 Y 좌표 형식 * x, y, radius 또는 rect 필수 X
* page Integer 결과 페이지 번호 * 1~45 사이의 값 (기본값: 1) X
* size Integer 한 페이지에 보여질 문서의 개수 * 1~15 사이의 값 (기본값: 15) X
* sort String 결과 정렬 순서, distance 정렬을 원할 때는 기준좌표로 쓰일 x, y 파라미터 필요 * distance 또는 accuracy (기본값: accuracy) X
*/
public interface RetrofitApi {
@Headers("Authorization:KakaoAK 647a2bd........0b9d8")
@GET("/v2/local/search/keyword.json")
Call<ResponseBean> getKeywordData (@Query(value="query", encoded = true) String strAddr,
@Query("x") double x,
@Query("y") double y,
@Query("radius") int radius,
@Query("page") int page
);

@Headers("Authorization:KakaoAK 647a2........970b9d8")
@GET("/v2/local/search/category.json")
@GET("/v2/local/search/category.json")
Call<ResponseBean> getCategoryData (@Query("category_group_code") String categoryCode,
@Query("x") double x,
@Query("y") double y,
@Query("radius") int radius,
@Query("rect") String rect,
@Query("page") int page);
);
}

여기서 ResponseBean 은 위에서 말한 class 이름이고, @Headers 에 들어 있는 문장에 키값은 위에서 받은 rest api 키 이니 참고 하시길...  자 이제 호출해 볼까 ?  아래와 같이 화면의 버튼 event 등에서 getData을 호출하면 된다. 


넘어가는 파라미터는 검색할 때 사용할 strKeyword 그리고 중심이 되는 x, y 좌표(Longitude, Latitude)값, 그리고 반경(radius), 그리고 받아올 페이지의 시작값(iPage) 


페이지의 시작값은 난 무조건 1페이지 분량만 받아서 할 거라서 1로 설정해서 받아왔지만, 주변 검색을 더 하고 싶으면 페이지 번호를 계속 변경해서 받아오면 된다.  그 페이지가 마지막 인지 여부를 응답구조체 중에서 meta is_end 이 값을 보고 알 수 있다.


    public void getData(String strKeyword, double x, double y, int radius, int iPage) {
Log.e(TAG, "(" + x + "," + y + ")" + strKeyword);
service.getKeywordData(strKeyword, x, y, radius, iPage).enqueue(new Callback<ResponseBean>() {
@Override
public void onResponse(Call<ResponseBean> call, Response<ResponseBean> response) {
Log.i(TAG, "code=" + response.code() + "" ) ;
try {
for (ResponseBean.Documents documents : response.body().getDocuments()) {
Log.e(TAG, documents.getPlaceName());
MapPoint mapPoint = MapPoint.mapPointWithGeoCoord(documents.getPosY(), documents.getPosX());
addMarker(mapPoint, documents.getPlaceName());
}
} catch (Exception e) {
e.printStackTrace();
}
}

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

Log.e(TAG, t.toString()) ;
t.printStackTrace();

}
});
}

그리고 결과를 받아 왔다면 addMarker 함수를 호출해서 지도에 마커들을 출력하면 끝...


 


다른 것들은 이제 다음에...





반응형





























오늘의 이야기


#스하리1000명프로젝트,
บางครั้งการพูดคุยกับแรงงานต่างด้าวก็ยากใช่ไหม?
ฉันสร้างแอปง่ายๆ ที่ช่วยได้! คุณเขียนเป็นภาษาของคุณ และคนอื่นๆ ก็เห็นเป็นภาษาของพวกเขา
มันแปลอัตโนมัติตามการตั้งค่า
มีประโยชน์มากสำหรับการแชทที่ง่ายดาย ควรดูเมื่อมีโอกาส!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기










오늘 부터 앱을 하나 만들꺼다... 생각만 하고 있던... 그래서 하나씩 도전을 해 볼껀데,  잘 할 수 있을까 ???


구현 목적 : 집에 가는 길에 약국에 들러서 약을 하나 사 가야겠다는 생각을 하고 퇴근을 했는데,  땀 삐질 거리며 집으로 와서 보니 으헉~ 집에 오는 길에 있는 약국앞을 그냥 지나쳐 온 것이다.   그래서 준비를 하기 시작 했다.


할려는 방안 : 카카오 개발자 페이지를 활용해서 앱에 지도를 넣고, 주변 검색 기능도 넣는다. 그리고 그걸 이용해서 집에 가는 길에 있는 약국 위치에 기록을 해 두는 것이지 , 여기 근처에 가면 해열제 하나를 꼭 사야 한다고...


생각은 되었으니, 이제 구현을 하나씩 해 보자.


준비물 : 카카오 개발자 페이지 등록, API 키 받기, 통신을 위한 retrofit 에 대한 이해. 그리고 개발툴 (android studio Arctic Fox 2020.3.1 버전 기준 : 2021.07.29쯤 업데이트가 적용 되었다)


먼저 카카오 지도 연동에 대한 이야기는 이제 내용을 참조한다.


https://billcorea.tistory.com/23



 


Kakao 지도 연동...


몇해전에는 카카오 지도를 연동하는 데, 애로 사항을 많이 느꼈다. 카카오의 기술지원은 어디에 있는 것인지 찾을 수 도 없고... 이번에 다시금 도전~ apis.map.kakao.com/android/ 이 페이지는 예나 지금


billcorea.tistory.com




다음 준비를 해야 할 부분은 아무래도 backgroud location 에 대한 준비를 해야 할 것 같다.


android 가 API29 이상으로 넘어가면서 background location 에 대한 권한에 제한이 많아지는 것 같다.


https://developer.android.com/training/location/permissions?hl=ko 



 


위치 정보 액세스 권한 요청  |  Android 개발자  |  Android Developers


사용자 개인 정보를 보호하려면 위치 서비스를 사용하는 앱에서 위치 정보 액세스 권한을 요청해야 합니다. 위치 정보 액세스 권한을 요청할 때는 다른 런타임 권한을 요청할 때와 동일한 권장


developer.android.com




 


꼭 읽어보고 가급적이면 github 에 올라와 있는 예제도 살펴볼 필요가 있을 것 같다.


https://github.com/android/location-samples/tree/main/Geofencing



 


GitHub - android/location-samples: Multiple samples showing the best practices in location APIs on Android.


Multiple samples showing the best practices in location APIs on Android. - GitHub - android/location-samples: Multiple samples showing the best practices in location APIs on Android.


github.com




 


여하튼 여기 까지가 내용들을 잘 살펴 보았다면, 준비는 끝일 것 같다.


이제 작성하는 건 다른 글에서...





반응형























오늘의 이야기




android studio 에서 gradle build 을 하는 동안 서버 인증서 때문에 오류가 발생하는 경우를 경험하게 되었다. 


이런 경우 다음과 같은 해소 방안을 찾을 수 있을 것 같다.


Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

at sun.security.validator.PKIXValidator.doBuild(Unknown Source)

at sun.security.validator.PKIXValidator.engineValidate(Unknown Source)

at sun.security.validator.Validator.validate(Unknown Source)

at sun.security.ssl.X509TrustManagerImpl.validate(Unknown Source)

at sun.security.ssl.X509TrustManagerImpl.checkTrusted(Unknown Source)

at sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(Unknown Source)

... 62 common frames omitted

Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

at sun.security.provider.certpath.SunCertPathBuilder.build(Unknown Source)

at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(Unknown Source)

at java.security.cert.CertPathBuilder.build(Unknown Source)

... 68 common frames omitted



출처: https://yunyun-onon.tistory.com/205 [XY 염색체의 진지한 시선 블로그]

 


이런 오류들을 만나게 되는 경우 


1. 시작 -> 명령 프롬프트(cmd)를 실행한다.



2. cd ${자바 설치 경로}\jre\bin

자바 jre를 설치한 경로 안에 bin폴더로 이동하기 위해 해당 명령어를 실행한다.

(bin폴더 안에 keytool이 있기 때문에 이동하여 실행하고자 한다.)



3. keytool -import -file "${인증서 경로}\인증서명.crt" -keystore "${자바 설치 경로}\jre\lib\security\cacerts" -storepass "changeit"



출처: https://yunyun-onon.tistory.com/205 [XY 염색체의 진지한 시선 블로그]

 


그런데 문제는 내컴퓨터에 서버 인증서 파일이 있는가 하는 것이다.


나의 경우는 회사에서 받은 파일을 이용하여 해소 하기는 했는데,  개인적인 경우라면...


음...





반응형





























오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle แอปที่สโมสรแบดมินตันต้องมี!
👉 แมทช์เพลย์ – บันทึกคะแนนและค้นหาคู่ต่อสู้ 🎉
เหมาะสำหรับทุกที่ คนเดียว กับเพื่อนฝูง หรือในคลับ! 🤝
ถ้าคุณชอบแบดมินตันลองดูแน่นอน

ไปที่แอป 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기




앱을 만들다 보니 로딩 화면에서 광고문구등을 넣어서 사용하고 싶은 요청이 있다. 이런건 어떻게 ? 그냥 쉬운 생각으로 빈 activity 을 만들어서 잠시 보여주고 그냥 닫아 주면 되지 않을까 ? 그래서 다소 빈약한 layout을 가지는 activity을 하나 만들었다.


먼저 layout 


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:paddingLeft="18dp"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="18dp"
android:paddingBottom="@dimen/activity_vertical_margin"

tools:context=".LoadingPage">

<TextView
android:id="@+id/textView"
android:layout_width="180dp"
android:layout_height="65dp"
android:text="@string/loading"
android:textSize="34sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.593"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.421" />

<TextView
android:id="@+id/textView8"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="420dp"
android:gravity="center_horizontal|center_vertical"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/textView9"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="460dp"
android:gravity="center_horizontal|center_vertical"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/textView10"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="36dp"
android:layout_marginTop="500dp"
android:gravity="center_horizontal|center_vertical"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>



 결과물은 이런 정도 메시지가 들어가는 layout


예시물에는 사용자의 요청에 따라 일부 문구가 들어가기는 하지만, 


이건 단순 예제 이니 참고만...


 


 


 


 


 


 


 


 


 


다음은 activity을 만들면...


public class LoadingPage extends Activity implements View.OnClickListener {

private static final String TAG = "LoadingPage";
ActivityLoadingPageBinding binding ;
SharedPreferences sp ;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);
binding = ActivityLoadingPageBinding.inflate(getLayoutInflater());
View view = binding.getRoot() ;

setContentView(view);

}

@Override
protected void onStart() {
super.onStart();
startLoading();
}

private void startLoading() {
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}

}

이런 정도의 코드... acitivity 의 lifecycle 을 보고 어디쯤에서 종료를 하게 만들껀가 그것을 고민했고, 다음은 열리자 마자 닫아 버리면 안되니까... handler 을 이용해서 약간의 지연 (3000ms = 3sec) 을 두고 앱을 종료해 주는 것으로 끝.


그럼 이걸 어떻게 호출하지 하는 건 다음과 같이 MainActvity 에서 호출하는 방식으로 처리를 했다.


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

...

Intent intent = new Intent(this, LoadingPage.class);
startActivity(intent);

...

}

이렇게 MainActivity 을 시작하자 마자 LoadingPage 을 호출하는 것으로 그렇게 하면 사용자는 메인화면위에 올라온 loading 페이지를 잠시 보고 사라지는 것을 보게될 것이다. 


 


이것으로 끝,,,


 


 


 





반응형





























오늘의 이야기










앱을 만들어보다보니... 색상을 입히는 것도 테크니컬 해야 할 것 같은 생각이 들었다. 


쭈욱 작업을 하고 마무리를 하는 시점이 되었을 때 배경색을 변경해 달라고 하거나, 글짜색이 마음에 들지 않는 다고 한다. 흑~... theme 을 모르던 시절에는 하나하나 선택해서 font 도 지정하고 나름 color 도 지정해서 만들어 더니만.


이런이런... 


그럼 준비를 해 볼까 ?


먼저 theme.xml 을 이용해서 사용하고 싶은 style 을 만들어 본다.


<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.Weroom714" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/default_color</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:background">@color/default_color</item>
<item name="android:windowBackground">@color/default_color</item>
<item name="android:colorBackground">@color/default_color</item>
<item name="android:textColor">@color/white</item>
<item name="colorAccent">@color/softBlue</item> <!-- -->
<item name="android:textColorPrimaryDisableOnly">@color/white</item> <!-- checkbox text color -->
<item name="android:textColorSecondary">@color/softBlue</item>
<item name="android:buttonTint">@color/white</item><!-- checkbox 의 체크상자 색상 -->
</style>
<style name="Theme.Weroom714.purple" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:background">@color/purple_200</item>
<item name="android:windowBackground">@color/purple_200</item>
<item name="android:colorBackground">@color/purple_200</item>
<item name="android:textColor">@color/white</item>
<item name="colorAccent">@color/white</item> <!-- -->
<item name="android:textColorPrimaryDisableOnly">@color/white</item> <!-- checkbox text color -->
<item name="android:textColorSecondary">@color/softBlue</item>
<item name="android:buttonTint">@color/white</item><!-- checkbox 의 체크상자 색상 -->
</style>
<style name="Theme.Weroom714.softBlack" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/softBlack</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:background">@color/softBlack</item>
<item name="android:windowBackground">@color/softBlack</item>
<item name="android:colorBackground">@color/softBlack</item>
<item name="android:textColor">@color/white</item>
<item name="colorAccent">@color/white</item> <!-- -->
<item name="android:textColorPrimaryDisableOnly">@color/white</item> <!-- checkbox text color -->
<item name="android:textColorSecondary">@color/softBlue</item>
<item name="android:buttonTint">@color/white</item><!-- checkbox 의 체크상자 색상 -->
</style>
<style name="Theme.Weroom714.darkBlue" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/darkBlue</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/darkBlue</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:background">@color/darkBlue</item>
<item name="android:windowBackground">@color/darkBlue</item>
<item name="android:colorBackground">@color/darkBlue</item>
<item name="android:textColor">@color/white</item>
<item name="colorAccent">@color/white</item> <!-- -->
<item name="android:textColorPrimaryDisableOnly">@color/white</item> <!-- checkbox text color -->
<item name="android:textColorSecondary">@color/softBlue</item>
<item name="android:buttonTint">@color/white</item><!-- checkbox 의 체크상자 색상 -->
</style>
<style name="Theme.Weroom714.darkGreen" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/darkGreen</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/darkGreen</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/white</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
<item name="android:fontFamily">@font/poorstory_regular</item>
<item name="android:background">@color/darkGreen</item>
<item name="android:windowBackground">@color/darkGreen</item>
<item name="android:colorBackground">@color/darkGreen</item>
<item name="android:textColor">@color/white</item>
<item name="colorAccent">@color/white</item> <!-- -->
<item name="android:textColorPrimaryDisableOnly">@color/white</item> <!-- checkbox text color -->
<item name="android:textColorSecondary">@color/softBlue</item>
<item name="android:buttonTint">@color/white</item><!-- checkbox 의 체크상자 색상 -->
</style>
<style name="Theme.Weroom714.Loading" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:background">@color/default_color</item>
<item name="android:windowBackground">@color/default_color</item>
<item name="android:colorBackground">@color/default_color</item>
<item name="colorPrimary">@color/default_color</item>
<item name="android:textColor">@color/softBlue</item>
<item name="android:fontFamily">@font/poorstory_regular</item>
</style>
<style name="ButtonText" parent="@android:style/Widget.Button">
<item name="android:textColor">@color/white</item>
<item name="android:background">@drawable/background_btn</item>
</style>
<style name="CustomRadioButton" parent="Theme.AppCompat.Light">
<item name="colorAccent">@color/white</item>
<item name="colorControlNormal">@color/default_color</item>
</style>
<style name="Theme.Weroom714.Transparent" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:textColor">@color/softBlue</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:fontFamily">@font/poorstory_regular</item>
</style>
<style name="AppCompatAlertDialogStyle" parent="Theme.AppCompat.Light.Dialog.Alert">
<item name="colorAccent">@color/softBlue</item>
<item name="android:textColor">@color/softBlue</item>
<item name="android:textColorPrimary">@color/white</item>
<item name="android:background">@color/default_color</item>
</style>
</resources>

이렇게 길어야 하는 생각이 들기는 하지만, 아직 조금의 미숙(?)함이 있는 관계로 다가 비슷한 sylte 을 복사해서 여러개 만들었다.


기본테마 Theme.Weroom714 부터 Theme.Weroom714_darkGreen, Theme.Weroom714_darkBlue, Theme.Weroom714_softBlack 까지 사용자는 4개의 색상 조합을 요청했다.  자세히 살펴 보면 다른 건 없고, 배경색만 다르게 지정하는 구성이다. 


이걸 어떻게 지정하는 건가 ?


    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);
if (sp.getString("THEME", "default_color").equals("darkBlue")) {
setTheme(R.style.Theme_Weroom714_darkBlue);
} else if (sp.getString("THEME", "default_color").equals("darkGreen")) {
setTheme(R.style.Theme_Weroom714_darkGreen);
} else if (sp.getString("THEME", "default_color").equals("softBlack")) {
setTheme(R.style.Theme_Weroom714_softBlack);
} else {
setTheme(R.style.Theme_Weroom714);
}
binding = ActivityMainBinding.inflate(getLayoutInflater());
View view = binding.getRoot() ;
setContentView(view);

}

적용하려는 activity 의 onCreate 에 위와 같이 적용하면 된다. 여기서 주의할 것은 viewBinding 을 하고 있기 때문에 그 위치를 잡는데, 테스트를 몇차례해서 정한건데, layout 을 binding 하기전에 setTheme()을 이용해서 먼저 설정하고 binding을 해야 실행 했을때 적용이 되는 것을 확인할 수 있었다.




이렇게 선택적으로 배경색을 변경하는 앱을 만들어 볼 수 있었다.


 


오늘도 즐~ 코딩...


 


p.s 이 미숙함을 지적해 주실꺼면 아래 댓글로 부탁 드립니다. 꾸벅^^;;





반응형























오늘의 이야기


#스하리1000명프로젝트,
迷失在韓國?即使您不會說韓語,這個應用程式也可以幫助您輕鬆出行。
只需說出您的語言即可 - 它會翻譯、搜尋並以您的語言顯示結果。
非常適合旅行者!支援英語、日語、中文、越南語等10多種語言。
現在就試試吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/02/14

오늘의 이야기



아무래도 휴대폰에 앱을 개발하는 일이다 보니, 문자 전송 같은 기본 기능을 이용하는 앱을 개발하는 일이 많기는 하다.  요새는 카카오톡등을 이용하거나, FCM 등을 이용해서 알림을 보내는 기능등을 구현해 보지만, 예전 방식 처럼 SMS 을 전송해 보는 것도 오랜만 이기는 하다.


구글이 계정 정책등으로 인해 앱을 등록할 때 기본앱으로 사용할 수 없는 SMS 앱은 등록에 무리가 있다. 그래서 그냥 개인적으로 사용하기 위해서 기본앱 기능은 아니지만, 필요에 의한 문자 전송을 구현하는 앱을 만들어 볼 까 싶다.


먼저 앱을 구동하기 위해서는 Manifest 파일에 권한 설정을 등록해 준다. 


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

<uses-permission android:name="android.permission.SEND_SMS" />

<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.SMSSend0823">

......

</application>

</manifest>

SMS 발송 권한을 설정했으니, 앱을 실행하면서 실제 동작이 발생 하기 전에 사용자에게 권한을 획득 하는 코드를 activity 에 넣어 보자.


import android.Manifest;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.telephony.SmsManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.navigation.fragment.NavHostFragment;

import com.example.smssend0823.Database.DBHandler;
import com.example.smssend0823.Database.MsgTargetBean;
import com.example.smssend0823.Database.SendMessageAdapter;
import com.example.smssend0823.Database.SendMessageBean;
import com.example.smssend0823.Utils.OnBackPressedListener;
import com.example.smssend0823.databinding.FragmentMsgselectBinding;

import java.util.ArrayList;

public class MsgSelectFragment extends Fragment implements OnBackPressedListener {

private static final String TAG = "MsgSelectFragment";
private FragmentMsgselectBinding binding;
ArrayList<SendMessageBean> beans;
SendMessageAdapter adapter;
DBHandler dbHandler ;
String phoneNo ;
String message ;
private static final int PERMISSION_RQST_SEND = 101;

public static Fragment newInstance() {
MsgSelectFragment fragment = new MsgSelectFragment();
return fragment;
}

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

binding = FragmentMsgselectBinding.inflate(inflater, container, false);
return binding.getRoot();

}

public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

beans = new ArrayList<>();
doDisplaySendMessage();
binding.listSendMessage.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
int iCnt = 0 ;
for(int i=0; i < beans.size() ; i++) {
if (beans.get(i).isSelectedMsg()) iCnt++;
}
if (iCnt > 0) {
beans.get(position).setSelectedMsg(false);
adapter.notifyDataSetChanged(beans);
Toast.makeText(getContext(), iCnt +"개 이상 선택할 수 없습니다.", Toast.LENGTH_SHORT).show();
return;
}
beans.get(position).setSelectedMsg(!beans.get(position).isSelectedMsg());
adapter.notifyDataSetChanged(beans);
}
});

binding.btnSendMessage.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = 0 ; int iCnt = 0 ;
for(int i=0; i < beans.size() ; i++) {
if (beans.get(i).isSelectedMsg()) {
position = i;
iCnt++;
break;
};
}
if (iCnt == 0) {
Toast.makeText(getContext(), "전송할 메시지를 선택하세요.", Toast.LENGTH_SHORT).show();
return;
}
message = beans.get(position).getSendMessage() ;
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("메시지 전송")
.setMessage("[" + beans.get(position).getSendMessage() + "] 로 전송할까요?")
.setPositiveButton("확인", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dbHandler = DBHandler.open(getContext());
Cursor rs = dbHandler.selectAll();
while(rs.moveToNext()) {
MsgTargetBean msgTargetBean = new MsgTargetBean();
msgTargetBean.setId(rs.getInt(rs.getColumnIndex("_id")));
msgTargetBean.setSendSeqNo(rs.getString(rs.getColumnIndex("sendSeqNo")));
msgTargetBean.setReceivePhoneNum(rs.getString(rs.getColumnIndex("receivePhoneNum")));
msgTargetBean.setSendMessage(rs.getString(rs.getColumnIndex("sendMessage")));
msgTargetBean.setSendTy(rs.getString(rs.getColumnIndex("sendTy")));
if("S".equals(msgTargetBean.getSendTy())) {
phoneNo = msgTargetBean.getReceivePhoneNum();
if (sendSMSMessage()) {
msgTargetBean.setSendTy("Y");
msgTargetBean.setSendMessage(message);
dbHandler.update(msgTargetBean);
}
}
}
dbHandler.close();
}
})
.setNegativeButton("취소", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {

}
});
AlertDialog dialog = builder.create();
dialog.show();
}
});
}

private void doDisplaySendMessage() {
beans.clear();
dbHandler = DBHandler.open(getContext());
Cursor rs = dbHandler.selectMsgAll();
while (rs.moveToNext()) {
SendMessageBean bean = new SendMessageBean();
bean.setSendMessage(rs.getString(rs.getColumnIndex("messageString")));
beans.add(bean);
}
adapter = new SendMessageAdapter(beans, getContext());
adapter.notifyDataSetChanged(beans);
binding.listSendMessage.setAdapter(adapter);
}

protected boolean sendSMSMessage() {
if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
Toast.makeText(getContext(), "SMS 전송 권한에 대한 허가가 필요합니다.", Toast.LENGTH_SHORT).show();
if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(),Manifest.permission.SEND_SMS)) {
Log.e(TAG, "shouldShowRequestPermissionRationale");
}
else { ActivityCompat.requestPermissions(getActivity(), new String[]{Manifest.permission.SEND_SMS}, PERMISSION_RQST_SEND);
Log.e(TAG, "requestPermissions");
}
} else {
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendTextMessage(phoneNo, null, message, null, null);
Toast.makeText(getContext(), "메시지가 전송 되었습니다.",Toast.LENGTH_LONG).show();
return true ;
}
return false;
}
//Now once the permission is there or not would be checked
@Override
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
switch (requestCode) {
case PERMISSION_RQST_SEND: {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

} else {Toast.makeText(getContext(), "SMS 발송이 되지 않았습니다. 잠시 뒤에 다시 시도 하세요.", Toast.LENGTH_LONG).show();
return;
}
}
}
}

@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}

@Override
public void onBackPressed() {
getParentFragmentManager().beginTransaction()
.replace(R.id.container, MsgSendListFragment.newInstance())
.commitNow();
}
}

 


source code 의 예시와 같이 sendSMSMessage() 함수에는 메시지 발송전에 권한 획득 여부를 확인하고, 권한이 없다면 사용자에게 권한 허가를 요청하는 알림을 띄워 권한을 획득 하게 된다.










다만, 이런 기능을 앱으로 이용하려면 playstore 에는 등록에 제한이 있으므로 다른 스토어를 이용하여야 한다.


SMS을 보내는 다른 방법은 권한 없이 사용할 수 있는 SMS Retriever API 을 활용하는 방법이 있다고 할 수 있는데, 실제 앱을 개발하다 보면 불편한 사항이 생기게 되어 있다.


https://developers.google.com/identity/sms-retriever/overview



 


SMS Retriever API를 사용한 자동 SMS 확인  |  SMS Verification APIs


이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English SMS Retriever API를 사용한 자동 SMS 확인 SMS Retriever API를 사용하면 사용자가 인증 코드를 수동으로 입력 할 필요없이 추가 앱 권


developers.google.com




참고해서 보면 좋을 것 깥다.


 















오늘의 이야기



Android 스튜디오: 절전 모드


Android Studio: 절전 모드는 Android 스튜디오 절전 모드입니다. 코드를 연 후 자동으로 프롬프트가 표시되지 않고 어떤 클래스와 메서드가 참조되는지 직관적이지 않으며 코드 자체 검사가 적용되지 않습니다.


인터넷을 찾다가 발견한 설명이다. 




 


이걸 체크 하면 불편한 점...  코드 입력시 auto coding 을 지원 하지 않는다.  이것이 제일 불편하다고 할 수 있을 것 같다.










 























오늘의 이야기

  1. 전체 기능 설계도(텍스트 플로우) 필요기능 [회원 관리] | v [참여자 인식 (Watch)] | v [팀 배정/리그 대진 생성] | v [경...