2026/03/20

오늘의 이야기

오늘은 챗봇에게 질문을 했습니다. 타원형  progress bar를그려 보자고...


 


동그란 progress bar 구현



같은 지점에서 출발해서 2개의 입력이 동시에 그려지는 progress bar 그리기입니다.   이 하나의 그림을 얻기 위해서 질문을 여러 번 해야 했습니다. 


 


1. Kotlin으로 타원형 진행률 표시줄을 그리는 코드 만들기


2. 하나의 그래프에 두 개의 서로 다른 입력을 받는 진행률을 그릴 수 있습니까?


3. 이 코드는 오류가 없습니까?


4. 타워의 경계를 따라 진행 상황을 보여주는 kotlin 코드


5. 두 개의 입력을 받고 진행률을 동시에 표시하도록 kotlin 코드 수정


6. canvas.drawPath를 사용하여 다시 만듭니다.


7. 놀이터 트랙처럼 보이는 타원에 트랙을 따라 동일한 지점에서 시작하여 두 개의 진행률을 표시하는 kotlin 코드를 작성합니다.


8. 하단이 아닌 테두리에만 표시해야 합니다.


9. 진행률이 테두리를 따라 곡선으로 표시되도록 만들어주세요


10. 바닥면은 도색 안해도 되나요?


11. 바닥은 아직 칠하고 있지만 칠하고 싶지 않습니다.


12. 둥근 타원 그래프


13. 약간 더 넓은 원형 타원 그래프


 


이렇게 13번의 수정 질문을 던져서 받은 코드입니다.


import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View

class DualOvalProgressBar(context: Context, attrs: AttributeSet) : View(context, attrs) {

private val progressPaint1: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPaint2: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val progressPath1: Path = Path()
private val progressPath2: Path = Path()

private var progress1: Float = 0f
private var progress2: Float = 0f
private var maxProgress: Float = 100f

init {
progressPaint1.color = Color.BLUE
progressPaint2.color = Color.RED
progressPaint1.style = Paint.Style.STROKE
progressPaint2.style = Paint.Style.STROKE
progressPaint1.strokeWidth = 40f
progressPaint2.strokeWidth = 40f
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

val centerX = width / 2f
val centerY = height / 2f
val horizontalRadius = Math.min(width, height) / 2f - progressPaint1.strokeWidth / 2f
val verticalRadius = horizontalRadius / 2f

// Calculate the sweep angles for the progress arcs
val progressSweepAngle1 = 360f * progress1 / maxProgress
val progressSweepAngle2 = 360f * progress2 / maxProgress

// Calculate the start and end angles for the progress arcs
val startAngle1 = -90f
val endAngle1 = startAngle1 + progressSweepAngle1
val startAngle2 = endAngle1
val endAngle2 = startAngle2 + progressSweepAngle2

// Create the progress paths
progressPath1.reset()
progressPath1.addArc(
centerX - horizontalRadius,
centerY - verticalRadius,
centerX + horizontalRadius,
centerY + verticalRadius,
startAngle1,
progressSweepAngle1
)
progressPath2.reset()
progressPath2.addArc(
centerX - horizontalRadius,
centerY - verticalRadius,
centerX + horizontalRadius,
centerY + verticalRadius,
startAngle2,
progressSweepAngle2
)

// Draw the progress paths
canvas.drawPath(progressPath1, progressPaint1)
canvas.drawPath(progressPath2, progressPaint2)
}

fun setProgress1(progress: Float) {
this.progress1 = progress
invalidate()
}

fun setProgress2(progress: Float) {
this.progress2 = progress
invalidate()
}

fun setMaxProgress(maxProgress: Float) {
this.maxProgress = maxProgress
invalidate()
}
}

 


이렇게 class을 하나 만들어 내고 난 다음.  layout xml에 다음을 추가했습니다. 


 


<?xml version="1.0" encoding="utf-8"?>
<layout 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">

<data>

</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
tools:context=".MainActivity"
tools:ignore="HardcodedText,SpUsage,ContentDescription">


<!-- app:layout_constraintStart_toEndOf="@+id/guideLine"-->
<com.example.twowayprogressgraph.DualOvalProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_editor_absoluteX="20dp"
tools:layout_editor_absoluteY="20dp" />



</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

 


이렇게 layout 을 만들고 mainActivity에서는 다음과 같이 호출하여 사용합니다. 


 


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val progressBar: DualOvalProgressBar = findViewById(R.id.progressBar)
progressBar.setMaxProgress(100f)
progressBar.setProgress1(30f)
progressBar.setProgress2(70f)

}
}

 


이제 API을 통해서 결과를 받아오고 그것을 표시하는 progress bar을 구현해야 한다면... 이렇게 구현해 볼 수 있을 것 같습니다. 


 


https://github.com/nari4169/TwoWayProgressGraph



 


GitHub - nari4169/TwoWayProgressGraph


Contribute to nari4169/TwoWayProgressGraph development by creating an account on GitHub.


github.com




코드는 이렇게 공유를 해 보겠습니다.


 


 





오늘의 이야기

이 글은 앱을 이용하여 회원관리를 할 수 있도록 구성하기 위한 방편으로 관리자용 업무 기능 지원하는 앱 사용자를 위한 사용자 설명서 페이지입니다. 


 


회원관리를 위한 앱은 바코드 할인쿠폰으로  playstore에 게시되어 있으며, 이전 설명 자료를 참고해 보실 수 있습니다. 


https://billcorea.tistory.com/324



 


할인쿠폰 모아보기 앱 사용자 가이드 (Ver 0.5.0 변경분)


이 앱은... 이 앱은 최초에 마트에서 제공해 주는 할인쿠폰을 모아 보기 위해서 기획 의도 되고 개발된 앱 이었습니다. 어느 날 요청자 께서 말씀하시길 회원이 보여주는 영수증도 모아볼 수 있


billcorea.tistory.com




 


이제 부터는 관리자를 위한 앱 사용자 설명을 해 보도록 하겠습니다. 


바코드 할인쿠폰 (관리자용)

이 앱은 Ver 0.5.0부터 시작합니다. 사유는 회원관리용 앱을 통합해 관리를 했으나, 사용자 관점에서 불편한 부분들이 발생하고 있어서 앱의 역할을 명확하게 분리하고자 하는 생각이 있었습니다.


 



  • 설정 버튼 : 관리자의 정보 관리를 위한 버튼

  • 뒤로가기 : 앱을 종료하고자 할 때 사용

  • 추가하기 (+) : 현재는 관리자도 바코드 할인쿠폰을 등록할 수 있도록 지원

  • 영수증 관리 : 회원의 제시한 영수증을 관리할 수 있도록 지원

  • 영수증 목록 : 등록된 영수증 목록을 조회할 수 있도록 지원

  • 회원 목록 : 등록된 회원 리스트를 조회하고 알림을 전달할 수 있도록 지원

  • 대기자 목록 : 회원앱과 관리자 앱이 근처에서 활성화된 경우 회원 앱에서 방문 기록을 요청하면 등록되어 조회되도록 지원

  • 홍보성 이미지 관리 : 이곳에서 등록된 이미지는 관리자앱 근처에 나타나는 회원앱이 활성화 된 경우 공유 설정된 이미지를 전달할 수 있도록 지원


기본 버튼의 메뉴는 이상과 같이 관리가 됩니다.




화면 상단에 나타나는 버튼 이미지 입니다. 이 버튼 이미지가 나타났을 때 클릭 하면 관리자 설정에서 생성된 QRCode 을 근처에 도착한 회원 앱으로 전달하는 기능도 지원됩니다.


 


메인 화면



 


 


관리자 설정 (화면이미지의 버전 표시는 변경될 수 있습니다.)

관리자 설정이 필요한 이유는 회원 관리를 하기 위해서는 데이터 저장을 해야 하는 데, 관리를 위해서 온라인으로 진행하고자 합니다. 해서 관리자 설정을 해 주세요.
 






관리자 등록 화면에서는



  • 먼저 관리자로 사용된 이메일 ID와 비밀번호를 입력하고  관리자 추가를 하게 되면 다음부터는 관리자 확인만 가능합니다

  • 관리자 확인이 되고 나면 QRcode 위에 있는 공유 버튼을 클릭하여 회원 가입 링크를 공유합니다.  그 이후에는 화면에 QRcode 전달을 위한 링크가 생겨 납니다.

  • 그다음부터는 QRCode 을 이용해서 고객 가입을 유도할 수 있습니다.


** 관리자의 비밀번호를 분실한 경우에는 비밀번호 하단의 제일 오른쪽 버튼을 이용하여 등록한 이메일 계정으로 관리자 비밀번호 변경할 수 있는 링크를 전달할 수 있습니다.
*** 관리자를 삭제하고 다시 등록하는 경우에는 5회 이상 반복 하게 되면 해당 이메일 계정은 더 이상 등록할 수 없도록 제한될 수 있으니 주의가 필요합니다.
 


회원가입 링크 공유



회원 가입 유도를 위한 링크 공유는 SNS 또는 SMS , 이메일 등등 모든 공유 수단을 활용할 수 있습니다. 


공유방법 선택



공유하는 방법을 선택하는 경우 '한 번만', '항상' 중에서 선택할 수 있는 데, 이 경우



  • 한 번만 하는 경우에는 다음에 또 같은 선택을 해야 합니다.

  • 항상을 선택하는 경우에는 다음번부터 같은 공유 방법으로만 할 수 있기 때문에 주의가 필요합니다. (물론 휴대폰의 설정에서 변경할 수 있습니다.)


** 더 쉬운 방법은 상대방이 앞에 있다면 위 그림의 QRCode 을 상대방의 휴대폰 카메라에 보여주는 것이 제일 수월 합니다.  이 경우에는 상대방 휴대폰 카메라앱에서 바로 링크가 나타납니다. (다만, 안드로이드 폰에서만 가능합니다.)
 


회원으로 가입이 유도되는 경우 다음과 같이 화면에 변경됩니다.   (회원앱의 설명에 근거해 확인해 주세요)

회원 가입 링크를 클릭해 실행된 경우



회원 화면으로 실행된 경우에는  회원의 이름과 연락처만 입력하고 저장하게 되고, 회원 스스로 할인 쿠폰을 관리하고자 하는 경우에는 할인 쿠폰을 등록해 보관 관리 할 수 있도록 지원됩니다.  
 


영수증 등록 하기 

영수증 등록해 보기



영수증 등록은 회원정보를 먼저 확인한 이후에 저장 버튼이 활성화됩니다. (저장 버튼의 색갈이 파란색이 됩니다.  준비 시점에는 저장 버튼 색갈이 검은색입니다.)
 
다음은 영수증 이미지를 선택하여야 합니다. 


이미지 선택하기 방법



영수증 이미지 선택 방법은 카메라 직접 촬영을 하거나, 이미 촬영된 이미지를 읽어오는 방법을 선택할 수 있습니다. 
가져온 영수증 이미지에서 필요한 부분만 선택적으로 자르기를 할 수 있도록 지원하고 있습니다. 사진 전체에서 이미지를 선택하고 자르기 버튼을 클릭하면 화면에 선택된 부분의 이미지만 가져오는 것을 볼 수 있습니다.


이미지 자르기 전 후



이미지 선택이 된 이후에  회원 정보 위에 있는 문자 인식 버튼을 클릭하여 화면의 내용 중에서 문자로 인식된 부분을 조회해 볼 수 있습니다. 


문자 인식된 내용중에서 해당하는 금액 선택 하기



인식된 문자들 중에서 숫자 변환될 수 있는 부분의 내용을 표시하고 있으니 그 부분에서 해당금액이 있는 부분을 선택하면 거래 금액이 표시 되게 됩니다.  (** 이 경우에는 거래금액을 수정할 수 없습니다.)
 


영수증 목록 보기

저장된 이후에는 영수증 목록 보기에서 저장된 목록을 확인할 수 있습니다.  영수증 목록에서는 회원의 연락처 정보를 이용해서 해당 해당 회원만 검색할 수 있도록 지원되고 있습니다.
 


저장된 영수증 목록 보기



 


고객 리스트 조회

 
고객명단은 회원으로 가입된 고객의 리스트를 조회할 수 있도록 지원됩니다. 고객이 많은 경우 이름의 첫 글자부터 한 글자 이상 입력 후 검색해 볼 수 있습니다. 
 


고객 명단 조회



또한 해당 고객에게 알림을 보내고자 하는 경우 간단한 문구를 입력하여 고객이 설치한 앱으로 푸시 알림을 보낼 수 있도록 지원됩니다. 


알림 메시지 전송



알림 메시지를 전송하게 되면 고객의 휴대폰으로 푸시 알림이 전송되며, 고객이 알림을 선택하면 고객화면으로 전달된 알림 내용을 확인해 볼 수 있습니다.  고객 화면의  위 부분에는 그동안 수신된 안내 메시지가 나오게 되어 있으며, 아래 부분에는 인식된 영수증의 리스트를 조회할 수 있도록 하고 있습니다.
 


고객 화면 알림 및 영수증 내역 조회



 


대기자 목록 조회

 


대기자 목록 조회는 회원앱과 관리자 앱이 근처(100m 이내)에서 활성화되는 경우 회원앱도 나타나는 근처 인식 버튼을 이용하여 관리자 앱으로 대기 신청을 했을 때 등록된 정보를 조회할 수 있는 화면입니다. 


대기리스트 연결 순서



이 그림과 같이 관리자앱과 회원앱이 근처에서 활성화되면 상단에 버튼 이미지가 추가로 나타납니다. 이때 회원 앱에서 해당 버튼을 클릭하면 관리자 앱으로 회원 정보를 전달하게 됩니다. 도달이 완료 되면 대기자 리스트에 추가 되며, 회원앱으로는 대기자 리스트에 등록 되었다는 알림을 전달 하게 됩니다.


 


 


광고 이미지 등록 관리

광고 이미지 등록 관리 화면에서


광고 이미지 등록 예시



이 화면에서 관리되는 홍보성 이미지는 대기자 목록 조회에서 처럼 근접한 회원 앱으로 자동 전달 됩니다.  회원은 해당 이미지를 보고 삭제할 수 도 있지만, 전달하고자 하는 의도는 구현이 될 듯합니다.


 


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



 


바코드 할인쿠폰 (관리자) - Google Play 앱


이 앱은 바코드 할인쿠폰 앱과 연동 하여 회원관리를 지원 하기 위한 관리자 전용 입니다.


play.google.com




 


앱을 설치해 보시면 회원관리가 수월해 집니다.





오늘의 이야기


#스하리1000명프로젝트,
Đôi khi thật khó để nói chuyện với người lao động nước ngoài phải không?
Tôi đã tạo một ứng dụng đơn giản có ích! Bạn viết bằng ngôn ngữ của bạn và những người khác nhìn thấy nó bằng ngôn ngữ của họ.
Nó tự động dịch dựa trên cài đặt.
Siêu tiện dụng để trò chuyện dễ dàng. Hãy xem khi bạn có cơ hội!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

Certainly! Here's some information about Seoul in 2023:

1. Major Events: Seoul is a vibrant city that hosts numerous events throughout the year. In 2023, you can expect various cultural festivals, music concerts, art exhibitions, and sporting events. Some notable events might include the Seoul Lantern Festival, Seoul International Music Festival, Seoul Fashion Week, and the Seoul Marathon. Make sure to check the local event calendars for specific dates and details.

2. Attractions: Seoul offers a wide range of attractions to explore. You can visit historical landmarks like Gyeongbokgung Palace, Changdeokgung Palace, and Bukchon Hanok Village to experience the city's rich history and traditional architecture. Don't miss the bustling markets such as Namdaemun Market and Dongdaemun Market, known for their shopping and street food. Other popular sites include N Seoul Tower, Myeongdong shopping district, and the Han River Park.

3. Technology and Innovation: Seoul is renowned for its advancements in technology. You can explore high-tech neighborhoods like Gangnam and Digital Media City, where you'll find futuristic buildings, cutting-edge technology, and innovation centers. Don't forget to visit the famous Samsung d'light exhibition center to see the latest gadgets and innovations.

4. Cuisine: Seoul is a food lover's paradise, offering a diverse range of culinary delights. You can savor traditional Korean dishes like bibimbap, bulgogi, and kimchi, as well as enjoy international cuisines in the city's many restaurants. Don't miss the chance to experience street food at popular spots like Gwangjang Market and Myeongdong, where you can try tasty treats like tteokbokki (spicy rice cakes) and hotteok (sweet pancakes).

5. Transportation: Seoul has a well-developed public transportation system, making it easy to navigate the city. You can utilize the subway, buses, and taxis to travel to different parts of Seoul conveniently. Additionally, Seoul offers bike-sharing services and rental cars for more flexibility in exploring the city.

Please note that the above information is based on the current knowledge available up until September 2021, and there might be additional developments and changes in the city by 2023. It's always a good idea to check for the latest information and updates closer to your travel dates.


 


틀림없이! 2023년 서울에 대한 정보는 다음과 같습니다. 1. 주요 행사: 서울은 연중 수많은 행사가 열리는 활기찬 도시입니다. 2023년에는 다양한 문화제와 음악회, 미술 전시회, 스포츠 행사를 기대할 수 있습니다. 서울빛초롱축제, 서울국제음악제, 서울패션위크, 서울마라톤 등이 대표적이다. 특정 날짜 및 세부 정보는 지역 행사 일정을 확인하십시오. 2. 관광명소: 서울은 탐험할 수 있는 다양한 관광명소를 제공합니다. 경복궁, 창덕궁, 북촌 한옥마을 등의 역사적 명소를 방문하여 도시의 풍부한 역사와 전통 건축물을 경험할 수 있습니다. 쇼핑과 길거리 음식으로 유명한 남대문시장, 동대문시장 등 북적이는 시장을 놓치지 마세요. 다른 인기 명소로는 N서울타워, 명동 쇼핑가, 한강공원 등이 있습니다. 3. 기술과 혁신: 서울은 기술 발전으로 유명합니다. 미래형 건물, 최첨단 기술, 혁신 센터가 있는 강남과 디지털 미디어 시티와 같은 첨단 기술 지역을 탐험할 수 있습니다. 유명한 Samsung d'light 전시 센터를 방문하여 최신 장치와 혁신을 보는 것을 잊지 마십시오. 4. 요리: 서울은 다양한 종류의 요리를 제공하는 미식가의 천국입니다. 비빔밥, 불고기, 김치와 같은 전통 한식을 맛볼 수 있을 뿐만 아니라 도시의 많은 레스토랑에서 세계 각국의 요리를 즐길 수 있습니다. 떡볶이, 호떡 등 맛있는 음식을 맛볼 수 있는 광장시장, 명동 등 인기 명소에서 길거리 음식을 경험할 기회를 놓치지 마세요. 5. 교통: 서울은 대중교통이 잘 발달되어 있어 이동이 편리합니다. 지하철, 버스, 택시를 이용하여 서울의 여러 지역으로 편리하게 이동할 수 있습니다. 또한 서울은 자전거 공유 서비스와 렌터카를 제공하여 보다 유연하게 도시를 둘러볼 수 있습니다. 위의 정보는 2021년 9월까지 사용 가능한 현재 지식을 기반으로 하며 2023년까지 도시에서 추가 개발 및 변경이 있을 수 있습니다. 여행 날짜에 가까운 최신 정보 및 업데이트를 항상 확인하는 것이 좋습니다..


 


이상은 gpt가 말해준 2023년의 서울 이야기입니다.


Gpt3.0은 2021년까지의 정보만 있다는 걸 스스로 말해 주네요.





오늘의 이야기

오늘은 앱을 구현하는 동안 알게 된 Nearby Connections API에 대한 이야기를 정리해 두고자 합니다.  이 기능은 wifi 가 되지 않더라도 주변(10m 반경)에 같은 앱을 사용하는 사용자가 있을 때, 데이터, 텍스트, 이미지 등을 공유하는 기능을 구현하기 위해서 사용됩니다. 


 


https://medium.com/androiddevelopers/two-way-communication-without-internet-nearby-connections-b1185 30cb84d



 


Two-way communication without internet: Nearby Connections (Part 2 of 3)


The Nearby Connections API allows your users to connect with each other even when no internet is available. The API uses Bluetooth and…


medium.com




참고글의 링크 하나를 달아 둡니다.  읽어 보면 도움이 될 듯합니다.   그리고 안드로이드 개발자 매뉴얼은 다음 링크를 참조하였습니다. 


 


https://developers.google.com/nearby/connections/android/get-started?hl=ko 



 


시작하기  |  Nearby Connections  |  Google for Developers


메일링 리스트에 가입하여 Nearby에 관한 소식 및 공지사항을 확인하세요. 이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English 시작하기 컬렉션을 사용해 정리하기 내 환경설정을


developers.google.com




 


이제 알아본 내용들을 기초로 하여 구현을 시도해 보겠습니다.  개발자 페이지 내용을 보다 보면 참고할 소스 코드가 github에 올라가 있는 것을 볼 수 있었습니다. 그것을  fork 하여 개인 계정으로 가져온 다음 일단은 그대로 저장해 실행을 해 보기록 했습니다. 


 


https://github.com/nari4169/connectivity-samples/tree/main/NearbyConnectionsWalkieTalkie



 


GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.


Multiple samples showing the best practices in connectivity on Android. - GitHub - nari4169/connectivity-samples: Multiple samples showing the best practices in connectivity on Android.


github.com




여러 개의 예제 소스가 있기는 했지만, 가장 필요한 기능에 유사한 것은 간단한 무선통신(워키토키)을 지원하는 코드가 눈에 들어와 실행을 해 보았습니다. 


 


다만, 원래 코드가 2018년쯤에 만들어진 이후 업데이트가 되지 않아서 최신 환경에 맞지 않는 부분들이 있었고, java 코드로 되어 있어 그대로 일단은 실행이 될 수 있도록 하는 부분만 수정해서 개인 계정에는 commit을 해 두었습니다.  저 링크에 있는 코드는 2023년 현재에도 그대로 실행해 볼 수 있는 코드입니다. 


 


저는 그것을 가지고 와서  kotlin을 수정된 코드를 활용해서 만들고 있는 앱에 적용해 보기로 했습니다.   아무튼 기능 구현을 해 보도록 하겠습니다. 


 


먼저 권한 설정을 시작합니다. 


 


manifest 파일에 다음과 같이 권한을 추가했습니다. 


    <!-- Required for Nearby Connections -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />

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

<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />

 


이제 기본으로 사용할 연결 작성을 위해서 ConnectionsActivity을 작성해 보겠습니다.  사실 이 코드는 개발자 페이지에 있는 코드를 kotlin을 변환한 코드입니다. java 코드 그대로라고 봐도 무방(?) 합니다. 


import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import android.net.wifi.WifiManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.CallSuper
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.billcorea.barcodevoucher0225.R
import com.billcorea.barcodevoucher0225.database.DBHandler
import com.billcorea.barcodevoucher0225.database.ViewModels
import com.billcorea.barcodevoucher0225.databean.ReceiveMessage
import com.google.android.gms.common.api.Status
import com.google.android.gms.nearby.Nearby
import com.google.android.gms.nearby.connection.AdvertisingOptions
import com.google.android.gms.nearby.connection.ConnectionInfo
import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback
import com.google.android.gms.nearby.connection.ConnectionResolution
import com.google.android.gms.nearby.connection.ConnectionsClient
import com.google.android.gms.nearby.connection.ConnectionsStatusCodes
import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo
import com.google.android.gms.nearby.connection.DiscoveryOptions
import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback
import com.google.android.gms.nearby.connection.Payload
import com.google.android.gms.nearby.connection.PayloadCallback
import com.google.android.gms.nearby.connection.PayloadTransferUpdate
import com.google.android.gms.nearby.connection.Strategy
import com.google.android.gms.tasks.OnFailureListener
import com.google.firebase.auth.FirebaseAuth
import java.util.Locale

/** Nearby Connections에 연결하고 편리한 메서드 및 콜백을 제공하는 클래스입니다. */
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
abstract class ConnectionsActivity : ComponentActivity() {

private val dataViewModel : ViewModels by viewModels()

/**
* ConnectionsActivity 권한으로 앱에 필요한 모든 권한을 풀링하는 선택적 후크
*요청합니다.
*
* @return 앱이 제대로 작동하는 데 필요한 모든 권한입니다.
*/
/**
* 이러한 권한은 Nearby Connections에 연결하기 전에 필요합니다.
*/

protected open val requiredPermissions: Array<String> = arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
)

private val REQUEST_CODE_REQUIRED_PERMISSIONS = 1

private val wifiManager: WifiManager by lazy {
applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
}

/**
* 로깅을 위해 [Status]를 영어로 읽을 수 있는 메시지로 변환합니다.
*
* @param status 현재 상태
* @return 읽을 수 있는 문자열. 예. 파일을 찾을 수 없습니다.
*/
private fun toString(status: Status): String {
return String.format(
Locale.US,
"[%d]%s",
status.statusCode,
if (status.statusMessage != null) status.statusMessage else ConnectionsStatusCodes.getStatusCodeString(
status.statusCode
)
)
}

private fun hasPermissions(context: Context?, vararg permissions: String?): Boolean {
if (context != null) {
for (permission in permissions) {
logD("permission=$permission", "")
if (ActivityCompat.checkSelfPermission(
context,
permission!!
) != PackageManager.PERMISSION_GRANTED
) {
return false
}
}
}
return true
}

/** Nearby Connections에 대한 핸들러입니다. */
lateinit var mConnectionsClient: ConnectionsClient

/** 우리 주변에서 발견한 장치. */
private val mDiscoveredEndpoints: MutableMap<String, Endpoint> = HashMap()

/**
* 보류 중인 연결이 있는 장치입니다. [ ][.acceptConnection] 또는 [.rejectConnection]을 호출할 때까지 보류 상태로 유지됩니다.
*/
private val mPendingConnections: MutableMap<String, Endpoint> = HashMap()

/**
* 현재 연결된 장치. 광고주의 경우 이는 클 수 있습니다. 발견자에게는
* 이 맵에는 하나의 항목만 있습니다.
*/
private val mEstablishedConnections: MutableMap<String, Endpoint?> = HashMap()
/** 현재 다른 장치에 연결을 시도하고 있으면 'true'를 반환합니다. */
/**
* 검색된 장치에 연결을 요청하는 경우 True입니다. 우리가 묻는 동안, 우리는 다른 것을 요청할 수 없습니다
* 장치.
*/
protected var isConnecting = false
private set
/** 현재 검색 중이면 'true'를 반환합니다. */
/** 발견하는 경우 참입니다. */
protected var isDiscovering = false
private set
/** 현재 광고 중인 경우 'true'를 반환합니다. */
/** 광고하는 경우 참입니다. */
protected var isAdvertising = false
private set

/** 다른 장치에 대한 연결을 위한 콜백. */
private val mConnectionLifecycleCallback: ConnectionLifecycleCallback =
object : ConnectionLifecycleCallback() {
override fun onConnectionInitiated(endpointId: String, connectionInfo: ConnectionInfo) {
logD(
String.format(
"onConnectionInitiated(endpointId=%s, endpointName=%s)",
endpointId, connectionInfo.endpointName
),
""
)
val endpoint = Endpoint(endpointId, connectionInfo.endpointName)
mPendingConnections[endpointId] = endpoint
this@ConnectionsActivity.onConnectionInitiated(endpoint, connectionInfo)
}

override fun onConnectionResult(endpointId: String, result: ConnectionResolution) {
logD(
String.format(
"onConnectionResponse(endpointId=%s, result=%s)",
endpointId,
result
),
""
)

// We're no longer connecting
isConnecting = false
if (!result.status.isSuccess) {
logW(
String.format(
"Connection failed. Received status %s.",
toString(result.status)
)
)
onConnectionFailed(mPendingConnections.remove(endpointId))
return
}
connectedToEndpoint(mPendingConnections.remove(endpointId))
}

override fun onDisconnected(endpointId: String) {
if (!mEstablishedConnections.containsKey(endpointId)) {
logW("Unexpected disconnection from endpoint $endpointId")
return
}
disconnectedFromEndpoint(mEstablishedConnections[endpointId])
}
}

/** 다른 장치에서 우리에게 보낸 페이로드(데이터 바이트)에 대한 콜백. */
private val mPayloadCallback: PayloadCallback = object : PayloadCallback() {
override fun onPayloadReceived(endpointId: String, payload: Payload) {
logD(
String.format("onPayloadReceived(endpointId=%s, payload=%s)", endpointId, payload),
""
)
onReceive(mEstablishedConnections[endpointId], payload)
}

override fun onPayloadTransferUpdate(endpointId: String, update: PayloadTransferUpdate) {
logD(
String.format(
"onPayloadTransferUpdate(endpointId=%s, update=%s)", endpointId, update
),
""
)
}
}

/** 활동이 처음 생성될 때 호출됩니다. */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mConnectionsClient = Nearby.getConnectionsClient(this)
}

/** 활동이 사용자에게 표시될 때 호출됩니다. */
override fun onStart() {
super.onStart()
// 이건 33 일때만 적용 가능
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S_V2) {
logD("SDK=${Build.VERSION.SDK_INT}","")
requiredPermissions.plus(Manifest.permission.NEARBY_WIFI_DEVICES)
}
if (!hasPermissions(applicationContext, *requiredPermissions)) {
requestPermissions.launch(requiredPermissions)
}
// startLocalOnlyHotspot()
}

private fun startLocalOnlyHotspot() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
this,
Manifest.permission.NEARBY_WIFI_DEVICES
) != PackageManager.PERMISSION_GRANTED
) {
return
}
wifiManager.startLocalOnlyHotspot(
object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
super.onStarted(reservation)

}

override fun onFailed(reason: Int) {
super.onFailed(reason)
Toast.makeText(
applicationContext,
"Error Local Only Hotspot: $reason",
Toast.LENGTH_SHORT,
).show()
}

override fun onStopped() {
super.onStopped()

}
},
null,
)
}

private val requestPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()) { permissions ->

var chkE = false ;
permissions.entries.forEach {
logD("DEBUG", "${it.key} = ${it.value}")
if (!it.value) {
chkE = true
}
}
if (chkE) {
MaterialDialog(this).show {
icon(R.mipmap.ic_barcode_voucher_round)
title(R.string.titleGetPermission)
message(R.string.msgGetPermissonsNon)
positiveButton(R.string.OK) {
finish()
}
}
}
}

/**
* 장치를 광고 모드로 설정합니다. 검색 모드에서 다른 장치로 브로드캐스트합니다.
* [.onAdvertisingStarted] 또는 [.onAdvertisingFailed] 중 하나가 한 번 호출됩니다.
* 이 모드에 성공적으로 진입했는지 확인했습니다.
*/
protected fun startAdvertising() {
isAdvertising = true
val localEndpointName = name
val advertisingOptions = AdvertisingOptions.Builder()
advertisingOptions.setStrategy(strategy!!)
mConnectionsClient
.startAdvertising(
localEndpointName,
serviceId,
mConnectionLifecycleCallback,
advertisingOptions.build()
)
.addOnSuccessListener {
logV("Now advertising endpoint $localEndpointName")
onAdvertisingStarted()
}
.addOnFailureListener(
object : OnFailureListener {
override fun onFailure(e: Exception) {
isAdvertising = false
logW("startAdvertising() failed.", e)
onAdvertisingFailed()
}
})
}

/** Stops advertising. */
protected fun stopAdvertising() {
isAdvertising = false
mConnectionsClient.stopAdvertising()
}

/** Called when advertising successfully starts. Override this method to act on the event. */
protected fun onAdvertisingStarted() {}

/** Called when advertising fails to start. Override this method to act on the event. */
protected fun onAdvertisingFailed() {}

/**
* 원격 엔드포인트와의 보류 중인 연결이 생성될 때 호출됩니다. [연결 정보] 사용
* 연결에 대한 메타데이터(예: 수신 대 발신 또는 인증 토큰). 만약에
* 연결을 계속하려면 [.acceptConnection]을 호출합니다. 그렇지 않으면,
* [.rejectConnection]을 호출합니다.
*/
protected open fun onConnectionInitiated(
endpoint: Endpoint?,
connectionInfo: ConnectionInfo?
) {
}

/** Accepts a connection request. */
protected fun acceptConnection(endpoint: Endpoint) {
mConnectionsClient
.acceptConnection(endpoint.id, mPayloadCallback)
.addOnFailureListener { e -> logW("acceptConnection() failed.", e) }
}

/** Rejects a connection request. */
protected fun rejectConnection(endpoint: Endpoint) {
mConnectionsClient
.rejectConnection(endpoint.id)
.addOnFailureListener { e -> logW("rejectConnection() failed.", e) }
}

/**
* 장치를 검색 모드로 설정합니다. 이제 광고 모드에서 장치를 수신합니다. 어느 하나
* [.onDiscoveryStarted] 또는 [.onDiscoveryFailed]는 일단 찾으면 호출됩니다.
* 이 모드에 성공적으로 진입한 경우 출력됩니다.
*/
protected fun startDiscovering() {
isDiscovering = true
mDiscoveredEndpoints.clear()
val discoveryOptions = DiscoveryOptions.Builder()
discoveryOptions.setStrategy(strategy!!)
mConnectionsClient
.startDiscovery(
serviceId,
object : EndpointDiscoveryCallback() {
override fun onEndpointFound(endpointId: String, info: DiscoveredEndpointInfo) {
logD(
String.format(
"onEndpointFound(endpointId=%s, serviceId=%s, endpointName=%s)",
endpointId, info.serviceId, info.endpointName
),
""
)
if (serviceId == info.serviceId) {
val endpoint = Endpoint(endpointId, info.endpointName)
mDiscoveredEndpoints[endpointId] = endpoint
onEndpointDiscovered(endpoint)
}
}

override fun onEndpointLost(endpointId: String) {
logD(String.format("onEndpointLost(endpointId=%s)", endpointId), "")
}
},
discoveryOptions.build()
)
.addOnSuccessListener { onDiscoveryStarted() }
.addOnFailureListener(
object : OnFailureListener {
override fun onFailure(e: Exception) {
isDiscovering = false
logW("startDiscovering() failed.", e)
onDiscoveryFailed()
}
})
}

/** Stops discovery. */
protected fun stopDiscovering() {
isDiscovering = false
mConnectionsClient.stopDiscovery()
}

/** Called when discovery successfully starts. Override this method to act on the event. */
protected fun onDiscoveryStarted() {}

/** Called when discovery fails to start. Override this method to act on the event. */
protected fun onDiscoveryFailed() {}

/**
* Called when a remote endpoint is discovered. To connect to the device, call [ ][.connectToEndpoint].
*/
protected open fun onEndpointDiscovered(endpoint: Endpoint?) {}

/** Disconnects from the given endpoint. */
protected fun disconnect(endpoint: Endpoint) {
mConnectionsClient.disconnectFromEndpoint(endpoint.id)
mEstablishedConnections.remove(endpoint.id)
}

/** Disconnects from all currently connected endpoints. */
protected fun disconnectFromAllEndpoints() {
for (endpoint in mEstablishedConnections.values) {
mConnectionsClient.disconnectFromEndpoint(endpoint!!.id)
}
mEstablishedConnections.clear()
}

/** Resets and clears all state in Nearby Connections. */
protected fun stopAllEndpoints() {
mConnectionsClient.stopAllEndpoints()
isAdvertising = false
isDiscovering = false
isConnecting = false
mDiscoveredEndpoints.clear()
mPendingConnections.clear()
mEstablishedConnections.clear()
}

/**
* Sends a connection request to the endpoint. Either [.onConnectionInitiated] or [.onConnectionFailed] will be called once we've found out
* if we successfully reached the device.
*/
protected fun connectToEndpoint(endpoint: Endpoint) {
logV("Sending a connection request to endpoint $endpoint")
// Mark ourselves as connecting so we don't connect multiple times
isConnecting = true

// Ask to connect
mConnectionsClient
.requestConnection(name, endpoint.id, mConnectionLifecycleCallback)
.addOnFailureListener { e ->
logW("requestConnection() failed.", e)
isConnecting = false
onConnectionFailed(endpoint)
}
}

private fun connectedToEndpoint(endpoint: Endpoint?) {
logD(String.format("connectedToEndpoint(endpoint=%s)", endpoint), "")
mEstablishedConnections[endpoint!!.id] = endpoint
onEndpointConnected(endpoint)
}

private fun disconnectedFromEndpoint(endpoint: Endpoint?) {
logD(String.format("disconnectedFromEndpoint(endpoint=%s)", endpoint), "")
mEstablishedConnections.remove(endpoint!!.id)
onEndpointDisconnected(endpoint)
}

/**
* Called when a connection with this endpoint has failed. Override this method to act on the
* event.
*/
protected open fun onConnectionFailed(endpoint: Endpoint?) {}

/** Called when someone has connected to us. Override this method to act on the event. */
protected open fun onEndpointConnected(endpoint: Endpoint?) {}

/** Called when someone has disconnected. Override this method to act on the event. */
protected open fun onEndpointDisconnected(endpoint: Endpoint?) {}

/** Returns a list of currently connected endpoints. */
protected val discoveredEndpoints: Set<Endpoint>
get() = HashSet(mDiscoveredEndpoints.values)

/** Returns a list of currently connected endpoints. */
protected val connectedEndpoints: Set<Endpoint?>
get() = HashSet(mEstablishedConnections.values)

/**
* 현재 연결된 모든 엔드포인트에 [페이로드]를 보냅니다.
*
* @param payload 보내려는 데이터.
*/
protected fun send(payload: Payload) {
send(payload, mEstablishedConnections.keys)
}

fun send(payload: Payload, endpoints: Set<String>) {
mConnectionsClient
.sendPayload(ArrayList(endpoints), payload)
.addOnFailureListener { e -> logW("sendPayload() failed.", e) }
}

/**
* 우리와 연결된 누군가가 우리에게 데이터를 보냈습니다. 이벤트에 대해 작동하도록 이 메서드를 재정의합니다.
*
* @param endpoint 발신자.
* @param payload 데이터.
*/
protected open fun onReceive(endpoint: Endpoint?, payload: Payload?) {}

/** Returns the client's name. Visible to others when connecting. */
protected abstract val name: String

/**
* 서비스 ID를 반환합니다. 이것은 이 연결에 대한 작업을 나타냅니다. 발견할 때,
* 연결을 고려하기 전에 광고주가 동일한 서비스 ID를 가지고 있는지 확인합니다.
*/
protected abstract val serviceId: String

/**
* 다른 장치에 연결하는 데 사용하는 전략을 반환합니다. 동일한 전략을 사용하는 장치만
* 및 서비스 ID는 검색 시 나타납니다. Stragies는 들어오고 나가는 수를 결정합니다.
* 동시에 연결이 가능하며 사용 가능한 대역폭은 얼마입니까?
*/
protected abstract val strategy: Strategy?
@CallSuper
protected open fun logV(msg: String) {
Log.e(Constants.TAG, msg)
}

@CallSuper
protected open fun logD(msg: String, s: String) {
Log.e(Constants.TAG, String.format("%s %s", msg, s))
}

@CallSuper
protected open fun logW(msg: String) {
Log.e(Constants.TAG, msg)
}

@CallSuper
protected open fun logW(msg: String, e: Throwable) {
Log.e(Constants.TAG, msg, e)
}

@CallSuper
protected open fun logE(msg: String, e: Throwable) {
Log.e(Constants.TAG, msg, e)
}

protected open fun doSendNearBy(auth: FirebaseAuth) {}

protected open fun doAdvertSendNearBy() {}

/** Represents a device we can talk to. */
protected class Endpoint(val id: String, val name: String) {

override fun equals(other: Any?): Boolean {
if (other is Endpoint) {
return id == other.id
}
return false
}

override fun hashCode(): Int {
return id.hashCode()
}

override fun toString(): String {
return String.format("Endpoint{id=%s, name=%s}", id, name)
}
}

}

영문 코멘트는 번역기를 통해서 일부 번역을 해 보았습니다. 


 


오늘은 여기까지 기술해 보겠습니다. 다음 이야기는 테스트 중에 발견된 일부 사항을 수정한 다음 계속해 보겠습니다.


 


** 어제 확인이 미처 되지 못했던 부분에 대한 확인을 오늘에야 끝냈습니다.  일단, 권한 기술하는 부분에서 android 13에 맞게 기술이 되지 않았던 부분이 확인이 되었습니다.  2023.05.31 이전에 보셨다면 다시 확인해 보셔야 할 것 같습니다. 


 


아무튼 이렇게 기술해서 정리를 하고 나니 앱에서 상호 접속 하는 부분등이 정리가 되는 것을 확인할 수 있었습니다.


 


neayby 샘플 화면



 


연결이 되고 나면 오른쪽 상단에  개발자 페이지에 제공된 nearby 공식 이미지를 사이즈만 조금 줄여서 표시가 되도록 하였더니, 노출이 됩니다. 연결되었다는 메시지도 나오고요.


 


 





잠깐 동안의 영상을 통해서 접속이 잘 되는 지도 확인해 볼 수 있었습니다. 


 


이렇게 작성된 예제 코드는 아무튼 위에 있는 github을 참고해 보시도록 하시면 되겠습니다.    오늘도 즐 코딩 하세요.


 


 p.s : 한 가지 정리 중인 부분은 payload에 byte 문자열을 보내는 것을 테스트해보고 있는 데, 이미지를 byte 문자열로 해서 보내려고 했더니  아주 작은 사이즈 이미지 말고는 불가능해 보입니다.   그래서 다음번에는 file로 저장해서 전달하는 기능을 구현해 보고 정리를 해 보도록 하겠습니다.


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, một ứng dụng cần có cho các câu lạc bộ cầu lông!
👉 Đấu trận – Ghi điểm & Tìm đối thủ 🎉
Hoàn hảo cho mọi nơi, một mình, với bạn bè hoặc trong câu lạc bộ! 🤝
Nếu bạn thích cầu lông, nhất định phải thử nó

Vào ứng dụng 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

챗봇에서 이미지를 그리는 무료 사이트를 정리해보았습니다.

1. QuickDraw Bot
- 장점: 빠르고 쉽게 그림을 그릴 수 있다.
- 단점: 그림의 자세한 설정이 어려우며, 그림의 크기와 색상을 조정할 수 없다.
- 링크: https://quickdraw.withgoogle.com/

2. SketchBot
- 장점: 세부적인 그림 설정이 가능하며, 레이어를 적용하여 그림을 그릴 수 있다.
- 단점: 그림을 그리는 데 시간이 조금 걸린다.
- 링크: https://sketchbot.io/

3. DrawBot
- 장점: 정교한 그림을 그릴 수 있으며, 크기와 색상을 조정할 수 있다.
- 단점: 그림 그리기에 시간이 걸리며, 처음부터 그림을 그리거나 이미지를 업로드해야 한다.
- 링크: https://www.drawbot.com/

그 이외에도 많은 챗봇 이미지 그리기 사이트가 있으니 참고하시길 바랍니다!





오늘의 이야기



 


어느 날 받은 구글 playstore에서 받은 메일의 본문 내용입니다. 앱을 하나 게시하고 있는 중인데 말입니다. 


앱의 제목에 '할인 쿠폰 모아보기' 라는 제목을 정해서 릴리즈 요청을 드렸더니... 이런 메일이 왔지 먼가요?


 


흠... 저 제목에서 사용자 평점, 리뷰, 설치 수 정책 정보가 위반이라는 말인 데... 그런 느낌이 드시나요?


아무튼 알 수 없는 구글(?) 입니다.  그래서 앱의 제목을 '바코드 할인 쿠폰'이라고 변경을 했더랍니다. 그렇게 해서 다시 배포를 하고 있기는 합니다.


 


구글의 개발자 정책이 고지식 한 부분이 있다는 것에 대해서 이해를 하고 있기는 하지만... 말입니다.  좀 과하다는 생각이 들 때가 있습니다. 


 


오늘도 마무리 잘 하시길 바랍니다.





오늘의 이야기


#스하리1000명프로젝트,
¿Perdido en Corea? Incluso si no hablas coreano, esta aplicación te ayuda a moverte fácilmente.
Simplemente hable su idioma: traduce, busca y muestra resultados en su idioma.
¡Genial para viajeros! Admite más de 10 idiomas, incluidos inglés, japonés, chino, vietnamita y más.
¡Pruébalo ahora!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/03/19

오늘의 이야기


이미지를 그리는 GPT 프롬프트 20개입니다.

1. Draw a beautiful sunset over the ocean.
2. Draw a cute puppy playing with a ball.
3. Draw a majestic mountain range with snow on the peaks.
4. Draw a delicious-looking pizza with all your favorite toppings.
5. Draw a serene forest with a small stream running through it.
6. Draw a futuristic city with flying cars and neon lights.
7. Draw a cozy cabin in the woods with a roaring fire inside.
8. Draw a magical castle with turrets and a moat.
9. Draw a peaceful garden with blooming flowers and a pond.
10. Draw a group of friends having a picnic in a park.
11. Draw a spooky haunted house with ghosts and bats.
12. Draw a bustling city street filled with people and shops.
13. Draw a beautiful butterfly perched on a flower.
14. Draw a majestic unicorn running through a meadow.
15. Draw a delicious-looking ice cream sundae with all the toppings.
16. Draw a serene beach with palm trees and clear blue water.
17. Draw a playful dolphin jumping out of the ocean.
18. Draw a beautiful rainbow over a field of flowers.
19. Draw a cozy coffee shop with comfortable chairs and good music.
20. Draw a fierce dragon guarding its treasure hoard.

이것들이 도움이 되었기를 바랍니다!





오늘의 이야기

그냥 이미지



 


열심히 살아 보아야 합니다.  파이팅~





오늘의 이야기

오늘은 챗봇에게 질문을 했습니다. 타원형   progress bar를 그려 보자고...   동그란 progress bar 구현 같은 지점에서 출발해서 2개의 입력이 동시에 그려지는 progress bar 그리기입니다.   이 하나의 그림을 얻기 위해서...