2026/02/28

오늘의 이야기

예전 부터 만들어 관리하던 앱이 있는데...  알림을 카카오톡을 보내주는 ... 카카오에서 api 을 upgrade 하고 있어서 나도 해 보기로 했다.


 


먼저 gradle 파일의 변화


 


변경전


implementation group: project.KAKAO_SDK_GROUP, name: 'usermgmt', version: project.KAKAO_SDK_VERSION
implementation group: project.KAKAO_SDK_GROUP, name: 'kakaotalk', version: project.KAKAO_SDK_VERSION
implementation group: project.KAKAO_SDK_GROUP, name: 'friends', version: project.KAKAO_SDK_VERSION

변경후


implementation "com.kakao.sdk:v2-user:2.9.0" // 카카오 로그인
implementation "com.kakao.sdk:v2-talk:2.9.0" // 친구, 메시지(카카오톡)
implementation "com.kakao.sdk:v2-story:2.9.0" // 카카오스토리
implementation "com.kakao.sdk:v2-link:2.9.0" // 메시지(카카오링크)
implementation "com.kakao.sdk:v2-navi:2.9.0" // 카카오내비

 


이제 로그인 시도해 보는 방법의 차이 


 


변경전에는 제공된 login activity 을 이용해서 login 을 시도 하면 계정으로 로그인하는 것만을 지원했다면... 


변경후에는 


 


if (UserApiClient.getInstance().isKakaoTalkLoginAvailable(InitActivity.this)) {
UserApiClient.getInstance().loginWithKakaoTalk(InitActivity.this, new Function2<OAuthToken, Throwable, Unit>() {
@Override
public Unit invoke(OAuthToken oAuthToken, Throwable throwable) {
if (throwable != null) {
Log.e(TAG, throwable.getMessage());
return null;
} else {
Intent intent = new Intent(InitActivity.this, MonthlyMain.class);
startActivity(intent);
finish();
}
return null;
}
});
} else {
Log.e(TAG, "keyHash=" + Utility.INSTANCE.getKeyHash(InitActivity.this));
UserApiClient.getInstance().loginWithKakaoAccount(InitActivity.this, prompts, new Function2<OAuthToken, Throwable, Unit>() {
@Override
public Unit invoke(OAuthToken oAuthToken, Throwable throwable) {
Log.e(TAG, oAuthToken.getAccessToken());
if (throwable != null) {
Log.e(TAG, throwable.getMessage());
return null;
} else {
Intent intent = new Intent(InitActivity.this, MonthlyMain.class);
startActivity(intent);
finish();
}
return null;
}
});
};

** 2022.03.24 수정 : 위 소스에서 getApplicationContext() 에서 InitActivity.this 로 수정함.


** getApplicationContext() 에 파라미터를 전달하게 되면 정상적으로 실행이 되지 않는 현상이 발견됨.


 


내폰에서 카카오톡으로 로그인이 가능한 상태 인지를 확인해 보고 가능 하다면 단순 로그인이 지원이 되고 그렇지 않을 떄면 예전 처럼 계정으로 로그인을 시도 하도록 구성할 수 있다는 차이가 생겼다.


 


친구 목록을 받아오는 방법은


 


변경전


AppFriendContext friendContext = new AppFriendContext( AppFriendOrder.NICKNAME, 0, 100, "asc");

KakaoTalkService.getInstance().requestAppFriends(friendContext,
new TalkResponseCallback<AppFriendsResponse>() {
@Override
public void onNotKakaoTalkUser() {
// 발신자가 카카오톡 유저가 아님
Log.e(TAG, "onNotKakaoTalkUser") ;
}

@Override
public void onFailure(ErrorResult errorResult) {
// 그 외 에러
Log.e(TAG, "getErrorMessage =" + errorResult.getErrorMessage());
Log.e(TAG, "getErrorCode =" + errorResult.getErrorCode());
Log.e(TAG, "getHttpStatus =" + errorResult.getHttpStatus());
Log.e(TAG, "errorResult =" + errorResult.toString());

kakaoToast.makeToast(StrValueAdd.this, errorResult.getErrorMessage(), Toast.LENGTH_LONG).show();

}

@Override
public void onSessionClosed(ErrorResult errorResult) {
// 액세스토큰 및 리프레시토큰이 만료됨. 재로그인 필요.
Log.e(TAG, "onSessionClosed" ) ;
redirectLoginActivity() ;
}

@Override
public void onSuccess(AppFriendsResponse result) {
// context의 beforeUrl과 afterUrl이 업데이트 된 상태.
appFriendInfos = result.getFriends();
adapter = new kakaoFriendinfoAdapter(appFriendInfos);

listAppFriendInfo.setAdapter(adapter);
appFriendCount = result.getTotalCount();
Log.e(TAG, "onSuccess " + result.getTotalCount()) ;
if (!"".equals(strUUID)) {
for(int i=0; i < appFriendCount ; i++) {
if (strUUID.equals(appFriendInfos.get(i).getUUID())) {
Log.e(TAG, strUUID + "/" + appFriendInfos.get(i).getUUID()) ;
appFiendIndex = i;
break ;
}
}
if (appFriendInfos != null) {
try {
kakaoUserId.setText(appFriendInfos.get(appFiendIndex).getProfileNickname());
} catch (Exception e) {
kakaoUserId.setText("");
}
} else {
kakaoUserId.setText("");
}
}
}
});

 


변경후


TalkApiClient.getInstance().friends(new Function2<Friends<Friend>, Throwable, Unit>() {
@Override
public Unit invoke(Friends<Friend> friendFriends, Throwable throwable) {

if (friendFriends != null) {
appFriends = friendFriends;
adapter = new kakaoFriendinfoAdapter(appFriends);
listAppFriendInfo.setAdapter(adapter);
appFriendCount = appFriends.getTotalCount();
Log.e(TAG, "onSuccess " + appFriends.getTotalCount()) ;
if (!"".equals(strUUID)) {
for(int i=0; i < appFriendCount ; i++) {
if (strUUID.equals(appFriends.getElements().get(i).getUuid())) {
Log.e(TAG, strUUID + "/" + appFriends.getElements().get(i).getUuid()) ;
appFiendIndex = i;
break ;
}
}
try {
kakaoUserId.setText(appFriends.getElements().get(appFiendIndex).getProfileNickname());
} catch (Exception e) {
kakaoUserId.setText("");
}

} else {
kakaoUserId.setText("");
}

}
return null;
}
});

훨씬 소스의 길이가 간결해진다.


 


다음은 문자공유하는 sendMessage 의 차이


 


변경전


LinkObject link = LinkObject.newBuilder().setWebUrl("https://developers.kakao.com")
.setMobileWebUrl("https://developers.kakao.com")
.build();
TextTemplate params = TextTemplate.newBuilder(strBody, link)
.setButtonTitle("OK").build();

if (!"".equals(rs.getString(5))) {
List<String> uuids = Collections.singletonList(rs.getString(5));

KakaoTalkService.getInstance().sendMessageToFriends(uuids, params, new TalkResponseCallback<MessageSendResponse>() {
@Override
public void onNotKakaoTalkUser() {
Log.e(TAG, " 발신자가 카카오톡 유저가 아님 ");
}

@Override
public void onFailure(ErrorResult errorResult) {
Log.e(TAG, " 그 외 에러 ");
}

@Override
public void onSessionClosed(ErrorResult errorResult) {
Log.e(TAG, " 액세스토큰 및 리프레시토큰이 만료됨. 재로그인 필요. ");
}

@Override
public void onSuccess(MessageSendResponse result) {
Log.e(TAG, "API 호출 성공. 일부 사용자에게는 전송이 실패했을 수 있음. ");
kakaoToast.makeToast(context, strBody, Toast.LENGTH_LONG).show();

}
});
} else {
KakaoTalkService.getInstance().requestSendMemo(new TalkResponseCallback<Boolean>() {
@Override
public void onNotKakaoTalkUser() {
Log.e(TAG, " 발신자가 카카오톡 유저가 아님 ");
}

@Override
public void onSessionClosed(ErrorResult errorResult) {
Log.e(TAG, " 그 외 에러 ");
}
@Override
public void onSuccess(Boolean result) {
Log.e(TAG, "API 호출 성공. 일부 사용자에게는 전송이 실패했을 수 있음. ");
kakaoToast.makeToast(context, "To Me : " + strBody, Toast.LENGTH_LONG).show();
}
}, params);
}

변경후


 


Link link = new Link("http://billcorea.tistory.com","http://billcorea.tistory.com", null, null);
TextTemplate textTemplate = new TextTemplate(strBody, link, null, "알림을 카톡으로");
if (!"".equals(rs.getString(5))) {
List<String> uuids = Collections.singletonList(rs.getString(5));
TalkApiClient.getInstance().sendDefaultMessage(uuids, textTemplate, new Function2<MessageSendResult, Throwable, Unit>() {
@Override
public Unit invoke(MessageSendResult messageSendResult, Throwable throwable) {
Log.e("context", messageSendResult.toString());
return null;
}
});

} else {
TalkApiClient.getInstance().sendDefaultMemo(textTemplate, new Function1<Throwable, Unit>() {
@Override
public Unit invoke(Throwable throwable) {
Log.e("context", "메모 등록");
return null;
}
});
}

수정하면서 url 등은 변경을 했지만... 훨씬 간결하게 코드를 구현할 수 있게 된 것 같다.


 


이제 안드로이드 패치가 되어도 더는 고민할 이유가 없어졌다.


 


로그인 실행된 샘플 이미지는 다음과 같이...


로그인 하기



 





오늘의 이야기


#스하리1000명프로젝트,
Kadang-kadang susah nak bercakap dengan pekerja asing kan?
Saya membuat aplikasi mudah yang membantu! Anda menulis dalam bahasa anda, dan orang lain melihatnya dalam bahasa mereka.
Ia auto-terjemah berdasarkan tetapan.
Sangat berguna untuk sembang mudah. Lihatlah apabila anda mendapat peluang!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

https://github.com/PierfrancescoSoffritti/android-youtube-player



 


GitHub - PierfrancescoSoffritti/android-youtube-player: YouTube Player library for Android and Chromecast, stable and customizab


YouTube Player library for Android and Chromecast, stable and customizable. - GitHub - PierfrancescoSoffritti/android-youtube-player: YouTube Player library for Android and Chromecast, stable and c...


github.com




 youtube 에 올라가 있는 동영상을 내 앱에서도 볼 수 있을까 하는 생각이 든다.  위 링크의 라이브러리를 이용하면


가능해 보인다.


 


도전 !


 


 


youtube play 하는 앱 만들기



 


구현할 것은 별 거 없어 보인다.   먼저 gradle 파일에 종속성을 추가한다.


 


implementation 'com.pierfrancescosoffritti.androidyoutubeplayer:core:11.0.1'

이것은 위에 기술한 링크에 있는 라이브러리를 연결하기만 하면 되고 layout 은 그냥 player 만 올리면 된다. 


 


 


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

<com.pierfrancescosoffritti.androidyoutubeplayer.core.player.views.YouTubePlayerView
android:id="@+id/youtube_player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingClass,MissingConstraints" />

</androidx.constraintlayout.widget.ConstraintLayout>

아무것도 없이 그냥 플레이어만 있는 layout 을 만들고 activity 을 만들면 끝이다. 


 



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

import android.os.Bundle;

import com.billcoreatech.myyoutube.databinding.ActivityMainBinding;
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.YouTubePlayer;
import com.pierfrancescosoffritti.androidyoutubeplayer.core.player.listeners.AbstractYouTubePlayerListener;

public class MainActivity extends AppCompatActivity {

ActivityMainBinding binding ;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
getLifecycle().addObserver(binding.youtubePlayerView);
binding.youtubePlayerView.addYouTubePlayerListener(new AbstractYouTubePlayerListener() {
@Override
public void onReady(@NonNull YouTubePlayer youTubePlayer) {
super.onReady(youTubePlayer);
String videoId = "cd_X7Os3OVE";
youTubePlayer.loadVideo(videoId, 0);
}
});
}
}

저기 나오는 viodeId 는 youtube 에 올라간 영상의 링크 값이다.  다만, youtube 에 올라간 영상의 경우에는 youtube 설정에서 다른 player 에서 플레이가 되도록 설정하는 게 필요해 보인다.


 


 



예제 앱 동영상이 나오는 화면


 


이것으로 youtube 의 영상 플레이어 구현은 끝.





오늘의 이야기

이전에 posting 했던 pdf 공유 하기이 다음 이야기 정도가 될 것 같다.  요 몇일은 앱 수리를 하느라.. 좀 


아무튼 이번에 작업하면서 찾아낸 것에 대해서 기억을 정리해 두어야 겠다. 


 


이번에 하게 된 일은 pdf 을 만들어서 공유를 하는 것인데, 방법이 2가지 정도는 되는 것 같다. 


 


첫번째는 이전 posting 에서 처럼 공유할 파일만 지정해서 ACTION_SEND 액션을 실행하는 방법이고, 이번에는 그것을 특정앱으로 한정해 보는 것이다. 


 


public void sharePdf(String sPackageName) {
File pdfFile = new File(getCacheDir(), "/" + fileName + ".pdf");
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), getApplicationContext().getPackageName()+".fileProvider", pdfFile);

Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.setPackage(sPackageName);
if (getPackageList(sPackageName)) {
shareIntent.setPackage(sPackageName);
shareIntent.putExtra(Intent.EXTRA_SUBJECT, "전달할 제목");
shareIntent.putExtra(Intent.EXTRA_TEXT, "전달할 메시지 내용.");
startActivity(Intent.createChooser(shareIntent, "알림TITLE"));
} else {
String url = "market://details?id=" + sPackageName;
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(i);
}
}

public boolean getPackageList(String packageName) {
boolean isExist = false;

PackageManager pkgMgr = getPackageManager();
List<ResolveInfo> mApps;
Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
mApps = pkgMgr.queryIntentActivities(mainIntent, 0);

try {
for (int i = 0; i < mApps.size(); i++) {
Log.e("PackageName=", "" + mApps.get(i).activityInfo.packageName);

if(mApps.get(i).activityInfo.packageName.startsWith(packageName)){
isExist = true;
break;
}
}
}
catch (Exception e) {
isExist = false;
}
return isExist;
}

이렇게 함수를 만들어 놓으면 구현 가능한 부분이 쉬워진다. 보내고 싶은 앱의 패키지 이름만 알아낸다면 그 앱으로 내가 만든 파일 (예시에서는 pdf 가 있다고 가정했다.) 을 바로 전달을 하게 되는 것이다. 


 


// com.dho.mobilefax : skt 모바일 팩스
// com.google.android.gm : gmail
// com.google.android.apps.docs : google drive 문서저장
// com.samsung.android.messaging : 삼성 문자 메시지
// com.google.android.apps.messaging : 구글 기본 메시지
// net.daum.android.mail : 다음메일
// com.kakao.talk : 카카오톡
// com.sec.print.mobileprint : 삼성모바일 프린트
// epson.print : epson print

예을 들면 이런 package name 을 찾아서 위에서 기술한 함수에 호출을 넣어 주면 된다.   그리고 위 예시 소스에서 getPackageList 함수를 이용해서 해당 앱이 사용중인 폰에 설치 되어 있는 지 확인하고 없으면 앱을 설치하도록 유도 페이지로 이동시키면 되는 것이다. 


 


 


코드 예시



 


이렇게 코드를 구현해 보았다. 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, aplikasi mesti ada untuk kelab badminton!
👉 Main Perlawanan – Rekod Markah & Cari Lawan 🎉
Sesuai untuk mana-mana sahaja, bersendirian, bersama rakan-rakan atau dalam kelab! 🤝
Jika anda suka badminton, pasti mencubanya

Pergi ke aplikasi 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

바탕화면 이미지



 


이런 이미지는 어떤 생각에서 만들어 내는 것일까 ?


 


식물인지 동물인지 야간은 애매한 이미지 이다.  늘 새로운 생각이 드는 건 맞는 데... 이걸 보면서 오늘은 또 어떤 생각이 들어야 하는 건지...


 


또 다른 시간이 오길 기다리며...





오늘의 이야기

앱을 만들다 보니 이런 일도 생긴다. 주 내용은 다름이 아니라  앱의 접근성에 관한 부분이다.

구글은 사용자가 편리하게 앱을 사용하게 될 수 있도록 하기 위해서  이기는 하겠지만 ,  개발자 입장에서는  아무래도 번거로운 일이 아닐까 싶다.

앱을 만들어 console 에 올리고 내부 테스트 버전을 적용하였더니,   한 두시간후에 메일와 와서 열어 보았더니, 아래 와 같은 출시 사전 보고서를 볼 수 있었다.

사전 출시 보고서


접근성과 관련 해서는  앱의 버튼 메뉴등을 만들어 달았는데,  그것들이 시각장애를 가지고 있는 친구들이 사용하기에 불편함이 없도록 하려면 그것들을 청각적인 요소로 들려 주어야 하는데, 그것을 알려 주어야 하는 것이다.

방법은 각 layout object 에 contentDesciption 을 설정해 주는 것이다.  앱의 구동되고 있는 동안에도 동적으로 그 설정을 지정할 수 있는 데,

그런때에는 setContentDescription 을 이용해서 설정을 해 주는 것이다.

그럼... 오늘은 여기 까지...





오늘의 이야기


#스하리1000명프로젝트,
Bị lạc ở Hàn Quốc? Ngay cả khi bạn không nói được tiếng Hàn, ứng dụng này vẫn giúp bạn đi lại dễ dàng.
Chỉ cần nói ngôn ngữ của bạn—nó sẽ dịch, tìm kiếm và hiển thị kết quả bằng ngôn ngữ của bạn.
Tuyệt vời cho du khách! Hỗ trợ hơn 10 ngôn ngữ bao gồm tiếng Anh, tiếng Nhật, tiếng Trung, tiếng Việt, v.v.
Hãy thử nó ngay bây giờ!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/02/27

오늘의 이야기

최종 추천 번호:
추천 [16,27,30,31,38,39]
추천 [02,04,18,29,33,40]
추천 [03,11,17,25,32,45]
추천 [07,15,24,28,34,42]
추천 [09,19,26,35,41,43]

### 다음 라운드 번호 추천 분석

#### 1. 번호 출현 빈도 분석
전체 회차(20회) 번호별 출현 빈도:
01: 5회, 02: 2회, 03: 4회, 04: 2회, 05: 3회, 06: 3회, 07: 4회, 08: 3회, 09: 3회, 10: 2회, 11: 1회, 12: 2회, 13: 1회, 15: 3회, 16: 5회, 17: 4회, 18: 1회, 19: 1회, 20: 2회, 21: 1회, 22: 1회, 23: 3회, 24: 4회, 25: 2회, 26: 3회, 27: 6회, 28: 3회, 29: 2회, 30: 4회, 31: 4회, 32: 2회, 33: 4회, 34: 2회, 35: 4회, 36: 4회, 37: 4회, 38: 5회, 39: 4회, 40: 3회, 41: 3회, 42: 2회, 43: 1회, 44: 2회, 45: 2회
가장 많이 출현한 번호 6개: [16,27,30,31,38,39]

#### 2. 홀/짝 비율 분석
회차별 홀/짝 조합 빈도:
짝 3개/홀 3개: 9회, 짝 2개/홀 4개: 5회, 짝 4개/홀 2개: 5회, 짝 1개/홀 5개: 1회
가장 빈번한 홀/짝 조합은 짝 3개, 홀 3개입니다. 마지막 라운드는 짝 3개, 홀 3개였습니다.

#### 3. 총합 및 평균 분석
역대 총합 범위: 104 - 187 (평균: 147.25)
역대 평균값 범위: 17.33 - 31.17 (평균: 24.54)
마지막 라운드(1212) 총합: 154, 평균: 25.67

#### 4. 간격 패턴 분석
번호 간격별 출현 빈도:
1: 16회, 2: 13회, 3: 16회, 4: 5회, 5: 6회, 6: 7회, 7: 6회, 8: 7회, 9: 2회, 10: 4회, 11: 2회, 12: 2회, 13: 1회, 14: 1회, 15: 3회, 16: 1회, 17: 3회, 18: 1회, 21: 1회, 22: 1회, 24: 1회, 25: 1회, 26: 1회, 30: 1회
마지막 라운드(1212) 간격: [3, 17, 6, 10, 3]
작은 간격(1-5)과 중간 간격(6-15)의 혼합이 자주 관찰됩니다.

#### 5. 패턴 일치도 및 주기 분석
과거 데이터를 통해 총합, 평균, 홀/짝 비율, 간격 패턴 등 4가지 속성을 기준으로 패턴 일치도를 분석했습니다. 일부 속성에서 동일 패턴 반복 주기가 관찰되었으나, 20회차 데이터만으로는 강력한 주기성을 단정하기 어렵습니다. 다만, 특정 패턴(예: 3짝 3홀 조합, 중간 범위의 총합)이 자주 나타나는 경향이 있습니다.

### 추천 조합 생성 근거

**추천1 [16,27,30,31,38,39]**: 전체 20회차 데이터에서 가장 높은 출현 빈도를 보인 6개의 번호를 조합했습니다.
**추천2 [02,04,18,29,33,40]**: 과거 당첨 번호의 통계적 균형(예: 짝수 3개, 홀수 3개, 중간 범위의 총합)을 고려하여 선정했습니다.
**추천3 [03,11,17,25,32,45]**: 최근 라운드의 번호 간격 패턴(작은 간격, 중간 간격, 큰 간격의 조화)을 반영하여 번호의 분포를 예측했습니다.
**추천4 [07,15,24,28,34,42]**: 출현 빈도가 높은 '핫'한 번호와 상대적으로 출현이 적은 '콜드'한 번호를 균형 있게 조합하여 선정했습니다.
**추천5 [09,19,26,35,41,43]**: 과거 특정 당첨 라운드(예: 1201회차)의 주요 특징(홀/짝 비율, 총합, 평균 등)을 모방하여 유사한 통계적 특성을 가지는 번호 조합을 생성했습니다.

### 마지막 라운드(1212회차)와 추천 조합 비교 분석

**1212회차 당첨 번호**: [05,08,25,31,41,44]
- 총합: 154, 평균: 25.67
- 홀/짝: 짝수 3개, 홀수 3개
- 간격: [3, 17, 6, 10, 3]

**추천1 조합 ([16,27,30,31,38,39]) 분석**:
- 총합: 181, 평균: 30.17 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [11, 3, 1, 7, 1] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 동일함. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천2 조합 ([02,04,18,29,33,40]) 분석**:
- 총합: 126, 평균: 21.00 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [2, 14, 11, 4, 7] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 동일함. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천3 조합 ([03,11,17,25,32,45]) 분석**:
- 총합: 133, 평균: 22.17 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 2개, 홀수 4개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [8, 6, 8, 7, 13] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 다름, 일부 간격 패턴이 1212회차와 겹침. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천4 조합 ([07,15,24,28,34,42]) 분석**:
- 총합: 150, 평균: 25.00 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 3개, 홀수 3개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [8, 9, 4, 6, 8] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 유사함, 홀/짝 비율이 동일함, 일부 간격 패턴이 1212회차와 겹침. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.

**추천5 조합 ([09,19,26,35,41,43]) 분석**:
- 총합: 173, 평균: 28.83 (1212회차 총합 154, 평균 25.67)
- 홀/짝: 짝수 1개, 홀수 5개 (1212회차 짝수 3개, 홀수 3개)
- 간격: [10, 7, 9, 6, 2] (1212회차 간격 [3, 17, 6, 10, 3])
- **1212회차 대비 특징**: 총합이 다소 차이 남, 홀/짝 비율이 다름. 이 조합은 1212회차와 번호 구성이 다르며, 최근 10회차 당첨 번호와도 중복되지 않습니다.



사용하는 예시 영상 보기
이 앱이 궁금 하다면, 아래 링크에서 설치할 수 있습니다.
로또 645






오늘의 이야기

 


https://developer.android.com/guide/components/broadcasts?hl=ko 



 


브로드캐스트 개요  |  Android 개발자  |  Android Developers


브로드캐스트 개요 Android 앱은 Android 시스템 및 기타 Android 앱에서 게시-구독 디자인 패턴과 유사한 브로드캐스트 메시지를 받거나 보낼 수 있습니다. 관심 있는 이벤트가 발생할 때 이러한 브로


developer.android.com




앱을 만들다 보니 구글이 싫어하는 암시적 intent 설정에 대한 이슈가 있었다. 그래서 찾아보다 알게된 것...


특히 앱에서 broadcasting 을 하게 되면, 구글은 모든앱이 받는 이슈에 대해서 싫어 한다. 


 


그래서 이제 부터는 모든 것을 명시적으로 선언해 주어야 만 하는 난관(?)에 봉착하게 된다.  앞으로는 개발하는 게 더 힘들어 질 것 같다. 흑~


 


    Intent intent = new Intent();
    intent.setAction("com.example.broadcast.MY_NOTIFICATION");
    intent.putExtra("data","Notice me senpai!");
intent.setPackage(getApplicationPackageName());
    sendBroadcast(intent);

앞으로는 이렇게 그냥 나의 앱 packageName 을 명시적으로 선언해 주는 습관을 드려야 할 것 같다.


 


playstore 에 앱을 게시하고자 올리면 테스트 단계를 거치는 데, 7~8 시간 대기가 필요하게 되므로... 시간 절약을 위해서.


 





오늘의 이야기

오늘은 내 앱의 화면에 올라온 내용중에 webView 에 들어 있는 것을 이미지 파일로 만들고 pdf 파일에 담아서 공유하는 기능을 구현해 보고자 한다. 


 


먼저할 것은 인터넷 접속을 위한 권한 부여


 


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

굳이 사용자에게 허가를 받지 않아도 된다.  그 다음에 layout 을 구성해 보았다. 


 


변환할 webView 에시



보는 것 처럼 화면에는 버튼 한개와 webView 만 한개 만들어 두었다.


 


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:id="@+id/layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<Button
android:id="@+id/pdfShare"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="40dp"
android:layout_marginTop="16dp"
android:text="Button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<WebView
android:id="@+id/webView"
android:layout_width="0dp"
android:layout_height="645dp"
android:layout_marginTop="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pdfShare" />

</androidx.constraintlayout.widget.ConstraintLayout>

 


그 다음은 이미지를 임시로 저장하고 그걸 pdf 로 전환하는 방법을 구현할 예정이기 때문에 임시저장소를 선택할 수 있도록 file_provider 의 값을 등록해 준다.  res/xml 폴더에 file_provider.xml 로 아래 내용을 저장해 주었다. 


cache 을 사용하는 것은  파일 저장을 하기 위해서 사용자에게 권한을 허가 받지 않아도 되기 떄문이다. 


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

 


다음은 activity을 구현해 보아야겠다.


먼저 webView 에 나의 블로그를 load할 수 있도록 하고, load가 다 되었다면 그 때 이미지 파일에 임시 저장을 한 다음, 


버튼을 클릭하면 그 이미지 파일을 열어서 공유하는 intent 을 호출 하도록 구현할 예정이다. 


 



import androidx.appcompat.app.AppCompatActivity;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.app.ActivityCompat;
import androidx.core.app.ShareCompat;
import androidx.core.content.FileProvider;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.pdf.PdfDocument;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import com.billcoreatech.pdftest.databinding.ActivityMainBinding;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";
Bitmap bitmap, scalebmp ;
int pageWidth = 1200 ;
ActivityMainBinding bind ;

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

bind.webView.loadUrl("https://billcorea.tistory.com");
bind.webView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
new Background().execute();
}
});

bind.pdfShare.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (createPDF ()) {
File pdfFile = new File(getCacheDir(), "/pizza.pdf");
Uri contentUri = FileProvider.getUriForFile(getApplicationContext(), getApplicationContext().getPackageName()+".fileProvider", pdfFile);

Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("application/pdf");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

List<ResolveInfo> resInfoList = getPackageManager().queryIntentActivities(shareIntent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, contentUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
startActivity(Intent.createChooser(shareIntent, "PDF 공유"));
};
}
});

}

private boolean createPDF() {

File file = new File(getCacheDir(), "/aaaa.png");
if (!file.isFile()) {
Toast.makeText(getApplicationContext(), "파일 생성이 되지 않았습니다.", Toast.LENGTH_SHORT).show();
return false ;
}

bitmap = BitmapFactory.decodeFile(getCacheDir().toString() + "/aaaa.png");

PdfDocument pdfDocument = new PdfDocument();
Paint paint = new Paint();

PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(bitmap.getWidth(), bitmap.getHeight(), 1).create();
PdfDocument.Page page = pdfDocument.startPage(pageInfo);
Canvas canvas = page.getCanvas();
canvas.drawBitmap(bitmap, 0, 0, paint);

pdfDocument.finishPage(page);

File file1 = new File(getCacheDir(), "/pizza.pdf");
try {
pdfDocument.writeTo(new FileOutputStream(file1));
} catch (IOException e) {
return false ;
}
pdfDocument.close();

return true ;
}

class Background extends AsyncTask<Void, Void, Bitmap>
{
@Override
protected Bitmap doInBackground(Void... params)
{
try
{
Thread.sleep(2000);
Bitmap bitmap = Bitmap.createBitmap(bind.webView.getWidth(), bind.webView.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
bind.webView.draw(canvas);
return bitmap;
}
catch (InterruptedException e){}
catch (Exception e){}
return null;
}
@SuppressLint("WrongThread")
@Override
protected void onPostExecute(Bitmap bm)
{
try {
OutputStream fOut = null;
File file = new File(getCacheDir(), "/aaaa.png");
fOut = new FileOutputStream(file);

bm.compress(Bitmap.CompressFormat.PNG, 50, fOut);
fOut.flush();
fOut.close();
bm.recycle();

Log.e(TAG, "fileName=" + getCacheDir() + "/aaaa.png");

Toast.makeText(getApplicationContext(), "공유할 파일이 생성되었습니다.", Toast.LENGTH_SHORT).show();
} catch (Exception e) {
e.printStackTrace();

Log.e(TAG, "" + e.toString());
}
}
}
}

 


webView 에 load는 시간이 소요 되기 떄문에 Background 로 load 된 이미지를 png 로 변환하도록 구현이 되었고, 


버튼을 클릭하면 그 때 png 파일이 있는 지 체크 해서 없으면 알림을 보여주고, 있으면 해당 이미지 파일을 pdf 로 전환해서 하고, pdf 가 생성되었다면, intent 을 호출해서 공유하도록 구현하는 방식이다. 


 


다만, API 32 기준으로 AVD에서 테스트를 해 보면서 오류 로그가 보이기는 했으나, 실행은 되었다. provider 에게 uri read 권한을 주어야 할 것 같은데, 아직 구글링을 했을 때 정확한 해소 방안은 찾을 수 없었다.


 


어떻게 하지 ?


 


 



실행 예시


 


알게될 날이 올까 ?


 


오류 해소를 위해서 다음 링크 까지는 해 보았으나... 아직 ... ㅠㅠ;;


 


https://stackoverflow.com/questions/57689792/permission-denial-while-sharing-file-with-fileprovider/59439316#59439316



 


Permission Denial while sharing file with FileProvider


I am trying to share file with FileProvider. I checked that file is shared properly with apps like Gmail, Google Drive etc. Even though following exception is thrown: 2019-08-28 11:43:03.169 12573-...


stackoverflow.com




 





오늘의 이야기

소셜 로그인 firebase에서 지원하고 있는 소셜 로그인(?)은 Google, Facebook, Apple, Microsoft, Twitter 등 대부분 외국계(?)입니다. firebase 의 소셜 로그인 지원 우리나라에서 대다수가 사용하는 nave...