2026/04/25

오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기

 


 


📍 Jetpack Compose + ARCore + Google Maps로 위치 기반 AR 구현하기


가이드앱 초안 이미지



 


ARCore의 Geospatial API는 GPS 좌표와 같은 실제 세계의 위치를 기준으로 가상 객체를 배치할 수 있는 기능을 제공합니다. 본 게시물에서는 Jetpack ComposeGoogle Maps Compose를 활용해 지도와 AR 콘텐츠를 동시에 표시하는 방법을 소개합니다.


💡 이 예제는 관광지 안내, 실외 AR 내비게이션, 위치 기반 게임 등에 활용할 수 있습니다.



1️⃣ 프로젝트 구성 및 의존성 추가


build.gradle.kts에 다음 라이브러리들을 추가합니다.


dependencies {
implementation("com.google.maps.android:maps-compose:4.1.1")
implementation("com.google.android.gms:play-services-maps:18.2.0")
implementation("com.google.ar:core:1.43.0")
}

추가적으로 ARCore 사용을 위한 권한 및 카메라 기능도 AndroidManifest에 설정해야 합니다.




2️⃣ ARCore 렌더러 구현 (ArCoreRenderer)


ARCore 프레임을 받아서 SurfaceView 위에 AR 객체를 렌더링하는 ArCoreRenderer 클래스를 작성합니다. Geospatial API를 통해 현재 위치의 위도, 경도, heading 값을 받아 지도 위치와 연동할 수 있습니다.


if (earth?.trackingState == TrackingState.TRACKING) {
val pose = earth.cameraGeospatialPose
Log.d("Geo", "Lat: ${pose.latitude}, Lng: ${pose.longitude}, Heading: ${pose.heading}")
}

지도의 좌표를 클릭했을 때 해당 위치에 Anchor를 생성해 AR 콘텐츠를 띄울 수 있습니다.


fun onMapClick(latLng: LatLng) {
val earth = session?.earth ?: return
if (earth.trackingState != TrackingState.TRACKING) return

earthAnchor?.detach()
earthAnchor = earth.createAnchor(
latLng.latitude, latLng.longitude, 0.0,
0f, 0f, 0f, 1f // 회전값 (Quaternion)
)
}



3️⃣ AR SurfaceView를 Compose에 포함시키기


Jetpack Compose에서는 AndroidView를 통해 SurfaceView를 삽입합니다.
렌더링 준비가 완료되면 SampleRender를 실행해 OpenGL 렌더링 루프를 시작합니다.


@Composable
fun ArSurfaceView(
modifier: Modifier = Modifier,
onSurfaceReady: (SurfaceView) -> Unit
) {
AndroidView(
factory = { context ->
val renderer = HelloArRenderer(
context = context,
sessionProvider = sessionProvider,
latLng = currentLocation?.let {
LatLng(
it.latitude,
it.longitude
)
} ?: LatLng(37.5665, 126.9780),
tapQueue = tapQueue // 🔥 renderer로 전달
) // 예시
sessionProvider.setOnRendererEventCallback { event ->
rendererEvent = event
}
sessionProvider.setOnTrackingMessageCallBack { message ->
trackingMessage = message
}
SampleRender(glSurfaceView, renderer, context.assets)
glSurfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY
glSurfaceView
},
modifier = Modifier.fillMaxSize()
.pointerInteropFilter { motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_UP) {
tapQueue.offer(motionEvent) // 🔥 터치 이벤트 큐에 전달
}
true
}
)
}



4️⃣ 지도와 AR 통합 Compose 화면 만들기


GoogleMap과 AR SurfaceView를 하나의 Compose 화면에 겹쳐서 배치합니다. 지도를 클릭하면 해당 좌표에 Anchor를 생성하고 AR로 렌더링합니다.


@Composable
fun ArWithMapScreen(activity: MainComposeActivity) {
val lifecycleOwner = LocalLifecycleOwner.current
val arRenderer = remember { ArCoreRenderer(activity) }

DisposableEffect(Unit) {
lifecycleOwner.lifecycle.addObserver(arRenderer)
onDispose { lifecycleOwner.lifecycle.removeObserver(arRenderer) }
}

Box(modifier = Modifier.fillMaxSize()) {
var clickedLatLng by remember { mutableStateOf<LatLng?>(null) }

GoogleMap(
modifier = Modifier.fillMaxSize(),
onMapClick = { latLng ->
clickedLatLng = latLng
arRenderer.onMapClick(latLng)
}
)

ArSurfaceView(
modifier = Modifier
.fillMaxSize()
.zIndex(1f),
onSurfaceReady = { surfaceView ->
SampleRender(surfaceView, arRenderer, activity.assets)
}
)
}
}



5️⃣ MainActivity 설정


Jetpack Compose와 ARCore를 통합한 MainActivity입니다. ArCoreSessionHelper는 세션 초기화 및 생명주기 처리를 돕습니다.


@AndroidEntryPoint
class MainComposeActivity : ComponentActivity() {
lateinit var arCoreSessionHelper: ArCoreSessionHelper

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arCoreSessionHelper = ArCoreSessionHelper(this)
arCoreSessionHelper.initialize()

setContent {
ArWithMapScreen(this)
}
}

override fun onResume() {
super.onResume()
arCoreSessionHelper.session?.resume()
}

override fun onPause() {
super.onPause()
arCoreSessionHelper.session?.pause()
}
}



✅ 마무리


Jetpack Compose로 Google Map과 ARCore를 동시에 사용할 수 있다는 점은 매우 큰 장점입니다. UI를 선언적으로 작성하면서도 실시간 위치 기반 AR 콘텐츠를 렌더링할 수 있어, 차세대 사용자 경험을 만드는 데 적합한 아키텍처입니다.



  • 지도 기반 AR 관광 가이드

  • 위치 기반 보물 찾기 게임

  • 실외 AR 내비게이션


🚀 다음으로는 AR 오브젝트가 지도 위에 정확하게 위치하고, 거리/방향을 시각화하는 기능도 확장해보세요!




오늘의 이야기


#스하리1000명프로젝트

오늘 내가 만든앱 하나 알려주고 싶어, 이 앱은 알림수집기 라고 이름을 붙였는 데,
내 폰에 표시 되는 알림을 읽어서 내가 지정한 단어가 들어 있고, 지출기록을 남겨야 하는 알림이
있으면 수집하고, 카카오톡으로 친구에게 전달해 주는 기능을 구현해 줄꺼야. 📲

이번 패치에서는 하루 한번 지정한 시간에 나에게 알림(노티) 하도록 기능을 추가 했어. 🙏
한번 써보고 불편한 거 있으면 말해줘.

앱 바로가기
👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

새로운 버전의 안내문



 


이전 버전은 어떤 모습이였길래 ?

 


안드로이드 앱 : 옵디강 (제주맛집) 리스트 조회.



 


안드로이드 앱 : 옵디강 (제주맛집) 리스트 조회.


스토어에 게시 되었어요. https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127 옵디강 (제주맛집 리스트 모아보기) - Google Play 앱 인터넷에 널린(?) 제주 맛집 정보를 모아 봅니다. play.google.com


billcorea.tistory.com




 


이전 버전의 제주 지역에 한정된 정보를 수집해 알려 주는 기능으로 만들어져 있었습니다.   여기에 표시 되는 핫플레이스는 매일 처럼 수집 되며 공개 되었습니다.  자료의 수집은 인터넷 상에 맛집 이라고 게시 되는 정보를 참조해 보았습니다. 


 


리뉴얼을 하려했던 이유는 

 


구글지도에는 한국에 왔을 때 상세한 주변 정보를 제공 하지 못 합니다. 또한 네이버지도나 카카오 지도 등은 한국어에는 잘 반영 되어 적용 되고 있기는 하지만, 외국인 관광객이 사용 하기에는 불편함이 있는 것으로 보입니다. 


 


명동 인근에서 프로젝트를 하는 중인데, 명동거리를 다녀 보면 외국인 관광객들이 길에서 스마트폰만 쳐다 보면 주변을 탐색 하는 모습을 보게 됩니다. 잘 찾아 다니고 계시는 지들... 영어 실력 하챦은 입장에서 물어봐 알려 줄 수도 없고 해서... 앱을 수정해 보기로 했습니다. 


 


이전 글에서 미리 게시해 본 것 처럼, AI 들에게 질문을 해 얻은 결과들을 녹여 이 앱의 수정에 반영 했습니다. 이 글을 쓰는 지금은 play store 에 게시를 진행 중이라,  조만간 게시 되면 이 앱을 사용해 볼 수 있을 듯 합니다. 


 


Welcome Jeju (제주맛집 리스트 모아보기) - Google Play 앱



 


Welcome Jeju (제주맛집 리스트 모아보기) - Google Play 앱


인터넷에 널린(?) 제주 맛집 정보를 모아 봅니다.


play.google.com




 


같은 링크를 사용할 예정이니, 몇일 뒤 이 링크에서 확인해 볼 수 있습니다. 


 


 



  •  Google Maps does not provide detailed surrounding information when visiting Korea. Additionally, while Naver Maps and Kakao Maps are well-optimized for Korean speakers, they seem inconvenient for foreign tourists.As mentioned in my previous post, I incorporated the insights I gained by asking AI into this app modification. At the time of writing this post, I am in the process of publishing it on the Play Store. Once it is officially posted, users will be able to try the app soon.

  • I am working on a project near Myeong-dong, and while walking through the streets, I have noticed foreign tourists staring at their smartphones, seemingly trying to explore their surroundings. I wonder if they are navigating well… With my limited English skills, I can't really ask and help them, so I decided to modify the app.

  • Googleマップは、韓国に訪れた際に詳しい周辺情報を提供できません。また、ネイバーマップやカカオマップは韓国語にはよく適用されていますが、外国人観光客が使用するには不便な面があるようです。前回の投稿でも書いたように、AIに質問して得た結果をアプリの修正に反映しました。この記事を書いている現在、Playストアへの公開作業を進めており、まもなく公開されれば、このアプリを利用できるようになるでしょう。

  • 明洞付近でプロジェクトを進めているのですが、明洞の街を歩いていると、外国人観光客がスマートフォンを見つめながら周囲を探索している様子をよく目にします。ちゃんと目的地にたどり着けているのか…英語が得意ではないので、聞いて教えてあげることもできず、それならばと思いアプリを修正することにしました。

  • 谷歌地图在来到韩国时无法提供详细的周边信息。此外,虽然Naver地图和Kakao地图对于韩语用户来说优化得很好,但似乎对外国游客而言并不方便。正如我在之前的帖子中提到的,我将通过向AI提问获得的见解融入到这款应用的修改中。目前,我正在进行Play商店的发布工作,一旦正式发布,用户将可以体验这款应用。

  • 我正在明洞附近进行一个项目,每次走在明洞街头,都能看到外国游客低头看着手机,似乎在探索周围环境。我不知道他们是否顺利找到目的地…由于我的英语能力有限,无法主动询问并帮助他们,于是决定修改这款应用。

  • Google地圖在抵達韓國時無法提供詳細的周邊資訊。此外,雖然Naver地圖和Kakao地圖對韓語使用者來說優化得很好,但對外國遊客而言似乎不太方便。正如我在之前的帖子中提到的,我將透過向AI提問獲得的見解融入到這款應用程式的修改中。目前,我正在進行Play商店的發佈工作,一旦正式發佈,使用者將可以體驗這款應用程式。

  • 我正在明洞附近進行一個項目,每次走在明洞街頭,都能看到外國遊客低頭看著手機,似乎在探索周圍環境。我不知道他們是否順利找到目的地…由於我的英語能力有限,無法主動詢問並幫助他們,於是決定修改這款應用程式。

  • Hindi nagbibigay ang Google Maps ng detalyadong impormasyon sa paligid kapag bumibisita sa Korea. Bukod dito, bagama't mahusay na na-optimize ang Naver Maps at Kakao Maps para sa mga nagsasalita ng Koreano, tila hindi ito maginhawa para sa mga dayuhang turista.Tulad ng nabanggit ko sa nakaraang post, isinama ko ang mga pananaw na nakuha ko mula sa pagtatanong sa AI sa pagbabago ng app na ito. Sa kasalukuyan, nasa proseso ako ng pag-post nito sa Play Store, kaya't sa lalong madaling panahon, maaaring magamit na ang app na ito.

  • May ginagawa akong proyekto malapit sa Myeong-dong, at habang naglalakad sa mga kalye, madalas kong nakikita ang mga dayuhang turista na nakatitig sa kanilang mga smartphone, tila sinusubukan nilang tuklasin ang paligid. Iniisip ko kung maayos ba nilang natutunton ang kanilang destinasyon… Dahil limitado ang aking kasanayan sa Ingles, hindi ko sila matanong o matulungan, kaya't napagpasyahan kong baguhin ang app.

  • 베트남어: Google Maps không cung cấp thông tin chi tiết về khu vực xung quanh khi đến Hàn Quốc. Ngoài ra, mặc dù Naver Maps và Kakao Maps được tối ưu hóa rất tốt cho người dùng tiếng Hàn, nhưng có vẻ không thuận tiện cho khách du lịch nước ngoài.Như tôi đã đề cập trong bài viết trước, tôi đã kết hợp những hiểu biết thu được từ việc đặt câu hỏi cho AI vào bản chỉnh sửa của ứng dụng này. Hiện tại tôi đang trong quá trình đăng tải lên Play Store, và một khi được phát hành, người dùng sẽ có thể trải nghiệm ứng dụng này.

  • Tôi đang thực hiện một dự án gần khu vực Myeong-dong, và khi đi bộ trên các con phố, tôi thường thấy khách du lịch nước ngoài cứ chăm chú nhìn vào điện thoại thông minh của họ, có vẻ như họ đang cố gắng khám phá xung quanh. Tôi tự hỏi liệu họ có tìm đường tốt không… Với khả năng tiếng Anh hạn chế của mình, tôi không thể hỏi han hay hướng dẫn họ, vì thế tôi quyết định chỉnh sửa ứng dụng này.

  • 태국어: Google Maps ไม่สามารถให้ข้อมูลโดยละเอียดเกี่ยวกับพื้นที่โดยรอบเมื่อมาถึงเกาหลี นอกจากนี้ แม้ว่า Naver Maps และ Kakao Maps จะได้รับการปรับแต่งอย่างดีสำหรับผู้ใช้ชาวเกาหลี แต่ดูเหมือนจะไม่สะดวกสำหรับนักท่องเที่ยวชาวต่างชาติ

  • ฉันกำลังทำโปรเจ็กต์ใกล้กับเมียงดง และเมื่อเดินไปตามถนน ฉันสังเกตเห็นนักท่องเที่ยวชาวต่างชาติจ้องมองที่สมาร์ทโฟนของพวกเขา ดูเหมือนว่าพวกเขากำลังพยายามสำรวจพื้นที่โดยรอบ ฉันสงสัยว่าพวกเขานำทางไปได้ดีหรือไม่... ด้วยทักษะภาษาอังกฤษที่จำกัดของฉัน ฉันไม่สามารถถามหรือช่วยพวกเขาได้ ดังนั้นฉันจึงตัดสินใจแก้ไขแอปนี้


 


*** 이 게시글의 모든 번역은 copilot 이 도와 주었습니다. 


새버전의 시작은.

 




 


새 버전의 시작은 환영 인사로 시작 합니다.  새버전에서는 우리말(한글), 


영어, 중국어, 대만어, 필리핀어, 베트남어, 태국어 을 지원 합니다. 


 


우리나라에서 가까운 동남아시아 지역의 외국인 관광객이 많을꺼라는 생각 때문에 일단 언어 지원은 그렇게  정했습니다. 


 


나중에 더 추가할 수도 있을 듯 합니다. 


 


 


 


 


다음 버튼을 눌러 볼께요...


 



  • I'll press the Next button and take a look...

  • 次のボタンを押して見てみます...

  • 我会按下“下一步”按钮看看...

  • 我會按下「下一步」按鈕看看...

  • Pipindutin ko ang Susunod na button at titingnan...

  • Tôi sẽ nhấn nút Tiếp theo và xem thử...

  • ฉันจะกดปุ่มถัดไปและดู...


 


안내문구 표시



 


간단한 안내문에 표시 됩니다. 앞에 화면에서 언어 선택을 하는 것에 따라 다른 언어로 표시 됩니다.


 


 



  • It is displayed as a simple guide. It will be shown in different languages depending on the language selected on the previous screen.

  • 簡単な案内文として表示されます。前の画面で選択した言語に応じて異なる言語で表示されます。

  • 它将作为简单的指南显示。它会根据前一个屏幕选择的语言以不同的语言显示。

  • 它將作為簡單的指南顯示。它會根據前一個畫面選擇的語言以不同的語言顯示。

  • Ipinapakita ito bilang isang simpleng gabay. Ipapakita ito sa iba't ibang wika depende sa napiling wika sa naunang screen.

  • Nó được hiển thị như một hướng dẫn đơn giản. Nó sẽ được hiển thị bằng các ngôn ngữ khác nhau tùy thuộc vào ngôn ngữ được chọn trên màn hình trước đó.

  • จะแสดงเป็นคำแนะนำง่ายๆ และจะแสดงในภาษาต่างๆ ขึ้นอยู่กับภาษาที่เลือกในหน้าจอก่อนหน้า


 


 


기본화면



 


이 화면에서는 주변 검색을 지원 합니다. 반경 5Km 이내의 주제어에 따른 검색과 카테고리 선택에 따른 검색이 지원 됩니다. 또한  매일 정보를 수집한 핫플레이스에 대한 정보도 같이 표시 됩니다.


 


 



  • This screen supports nearby searches. It provides searches based on keywords within a 5Km radius and searches based on selected categories. Additionally, information on hot places collected daily is also displayed.

  • この画面では、周辺検索をサポートします。5Km以内のキーワードによる検索と、カテゴリ選択による検索が可能です。また、毎日収集された人気スポットの情報も表示されます。

  • 此屏幕支持周边搜索。它提供基于5公里范围内的关键词搜索和基于类别选择的搜索。此外,每天收集的热门地点信息也会一并显示。

  • 此畫面支援周邊搜尋。它提供基於5公里範圍內的關鍵字搜尋和基於類別選擇的搜尋。此外,每天收集的熱門地點資訊也會一起顯示。

  • Sinusuportahan ng screen na ito ang paghahanap sa paligid. Nagbibigay ito ng mga paghahanap batay sa mga keyword sa loob ng 5Km radius at mga paghahanap batay sa napiling kategorya. Bukod dito, ipinapakita rin ang impormasyon sa mga mainit na lugar na kinokolekta araw-araw.

  • Màn hình này hỗ trợ tìm kiếm xung quanh. Nó cung cấp các tìm kiếm dựa trên từ khóa trong phạm vi 5Km và tìm kiếm dựa trên danh mục đã chọn. Ngoài ra, thông tin về các địa điểm nổi bật được thu thập hàng ngày cũng được hiển thị.

  • หน้าจอนี้รองรับการค้นหาโดยรอบ โดยสามารถค้นหาตามคำหลักภายในรัศมี 5 กม. และค้นหาตามหมวดหมู่ที่เลือกได้ นอกจากนี้ ยังแสดงข้อมูลเกี่ยวกับสถานที่ยอดนิยมที่รวบรวมข้อมูลทุกวันอีกด้วย


언어설정 화면



 


언어설정을 다시 하고 싶을때에는 메뉴에서 언어설정을 선택 하여 다시 언어설정을 할 수 있습니다. 


 


 



  • If you want to reset the language settings, you can select "Language Settings" from the menu and reconfigure them.

  • 言語設定を再設定したい場合は、メニューから「言語設定」を選択して再設定できます。

  • 如果您想重新设置语言,可以从菜单中选择“语言设置”并进行重新配置。

  • 如果您想重新設定語言,可以從選單中選擇「語言設定」並重新配置。

  • Kung gusto mong i-reset ang mga setting ng wika, maaari mong piliin ang "Mga Setting ng Wika" mula sa menu at muling i-configure ang mga ito.

  • Nếu bạn muốn đặt lại cài đặt ngôn ngữ, bạn có thể chọn "Cài đặt ngôn ngữ" từ menu và cấu hình lại chúng.

  • หากคุณต้องการรีเซ็ตการตั้งค่าภาษา คุณสามารถเลือก "การตั้งค่าภาษา" จากเมนูและกำหนดค่าใหม่ได้


 


 


 


음성 인식 화면



 


음성을 이용해서 검색할 수 도 있습니다. 다만, 키워드만 검색 하기 때문에 자연스런 문장의 해석은 지원 되지 않습니다.  언어 설정에 따라  당신의 언어로 말하면 검색은 한국어 기준으로 검색하고 다시 당신의 언어로 번역해 표시해 줍니다.  이 기능을 사용하려면 사용전에 음성 인식 권한을 허가해 주시면 됩니다. 


 


 



  • You can also search using voice. However, only keyword searches are supported, so natural sentence interpretation is not available. Based on the language settings, if you speak in your language, the search will be conducted in Korean and then translated back to your language for display. To use this feature, you need to grant voice recognition permission beforehand.

  • 音声を利用して検索することもできます。ただし、キーワード検索のみがサポートされるため、自然な文章の解釈は対応していません。言語設定に応じて、あなたの言語で話せば検索は韓国語で行われ、その後あなたの言語に翻訳されて表示されます。この機能を使用するには、事前に音声認識の許可を与えてください。

  • 您也可以使用语音进行搜索。但仅支持关键词搜索,因此不支持自然句子的解析。根据语言设置,如果您使用自己的语言进行讲话,搜索将以韩语进行,然后再翻译成您的语言进行显示。要使用此功能,您需要事先授予语音识别权限。

  • 您也可以使用語音進行搜尋。但僅支持關鍵字搜尋,因此不支援自然句子的解析。根據語言設定,如果您使用自己的語言進行說話,搜尋將以韓語進行,然後再翻譯成您的語言進行顯示。要使用此功能,您需要事先授予語音識別權限。

  • Maaari ka ring maghanap gamit ang boses. Gayunpaman, sinusuportahan lamang ang paghahanap ng keyword, kaya't hindi available ang interpretasyon ng natural na pangungusap. Batay sa mga setting ng wika, kung magsasalita ka sa iyong wika, ang paghahanap ay isasagawa sa Koreano at pagkatapos ay isasalin pabalik sa iyong wika para sa pagpapakita. Upang magamit ang tampok na ito, kailangan mong bigyan ng pahintulot ang pagkilala sa boses nang maaga.

  • Bạn cũng có thể tìm kiếm bằng giọng nói. Tuy nhiên, chỉ hỗ trợ tìm kiếm theo từ khóa, vì vậy không có khả năng diễn giải câu tự nhiên. Dựa trên cài đặt ngôn ngữ, nếu bạn nói bằng ngôn ngữ của mình, tìm kiếm sẽ được thực hiện bằng tiếng Hàn và sau đó sẽ được dịch lại sang ngôn ngữ của bạn để hiển thị. Để sử dụng tính năng này, bạn cần cấp quyền nhận diện giọng nói trước.

  • คุณสามารถค้นหาโดยใช้เสียงได้เช่นกัน อย่างไรก็ตาม รองรับเฉพาะการค้นหาด้วยคำหลักเท่านั้น ดังนั้นจึงไม่สามารถตีความประโยคธรรมชาติได้ ตามการตั้งค่าภาษา หากคุณพูดภาษาของคุณ การค้นหาจะดำเนินการเป็นภาษาเกาหลีแล้วแปลกลับเป็นภาษาของคุณเพื่อแสดง ในการใช้ฟีเจอร์นี้ คุณต้องให้สิทธิ์การรู้จำเสียงล่วงหน้า


검색 결과 화면



 


여기 보이는 마크는 2가지 입니다.  



  • 위치정보 모양의 마크는 검색 하는 정보에 따라 표시 되는 위치 정보 이며,

  • 플래그 모양의 마크는 이 앱이 제공 하는 핫플레이스 정보 입니다  핫 플레이스 정보는 한국의 블로거 들이 다녀본 맛집의 정보를 게시한 블로그 등에서 스크래핑 하여 수집한 정보 을 기초로 해서 제공 합니다.  스크랩 되는 정보이기 때문에 실제와 다를 수 있음을 양해해 주시기 바랍니다. 


 



  • There are two types of marks shown here.

  • The location icon represents location information displayed based on the searched data. The flag icon represents hot place information provided by this app. The hot place information is based on data collected from blogs where Korean bloggers have visited and posted restaurant reviews. Since this information is scraped, it may differ from reality, so please understand.

  • ここに表示されるマークは2種類あります。

  • 位置情報アイコンは、検索した情報に応じて表示される位置情報です。 旗のアイコンは、このアプリが提供するホットプレイス情報です。ホットプレイス情報は、韓国のブロガーが訪れた飲食店のレビューを投稿したブログからスクレイピングして収集した情報を基に提供されます。スクレイピングされた情報のため、実際の情報とは異なる場合がありますのでご了承ください。

  • 这里显示的标记有两种。

  • 位置信息图标是根据搜索信息显示的位置信息。 旗帜图标是该应用程序提供的热门地点信息。热门地点信息是基于从韩国博主访问过的餐厅博客中收集的数据提供的。由于这些信息是抓取的,可能与实际情况有所不同,敬请谅解。

  • 此處顯示的標記有兩種。

  • 位置資訊圖示是根據搜索的資訊顯示的位置資訊。 旗幟圖示是該應用程式提供的熱門地點資訊。熱門地點資訊是基於從韓國部落客造訪過的餐廳部落格中收集的數據提供的。由於這些資訊是擷取的,可能與實際情況有所不同,敬請理解。

  • Mayroong dalawang uri ng marka na makikita dito.

  • Ang icon ng lokasyon ay nagpapakita ng impormasyong pang-lokasyon batay sa hinanap na impormasyon. Ang icon ng bandila ay kumakatawan sa impormasyon ng hot place na ibinibigay ng app na ito. Ang impormasyon ng hot place ay batay sa nakolektang datos mula sa mga blog kung saan nag-post ang mga Korean blogger tungkol sa mga restoran na kanilang binisita. Dahil ang impormasyong ito ay hinango, maaaring may pagkakaiba ito sa aktwal na impormasyon, kaya't sana'y maunawaan ito.

  • Có hai loại dấu hiệu hiển thị ở đây.

  • Biểu tượng vị trí hiển thị thông tin vị trí dựa trên dữ liệu được tìm kiếm. Biểu tượng lá cờ đại diện cho thông tin về địa điểm phổ biến do ứng dụng này cung cấp. Thông tin về địa điểm phổ biến được cung cấp dựa trên dữ liệu thu thập từ các blog của những blogger Hàn Quốc đã từng đến và đăng bài đánh giá về nhà hàng. Vì thông tin này được thu thập tự động, nó có thể khác với thực tế, mong bạn thông cảm.

  • มีเครื่องหมายอยู่สองประเภทที่แสดงที่นี่

  • ไอคอนตำแหน่งแสดงข้อมูลตำแหน่งตามข้อมูลที่ค้นหา ไอคอนธงแสดงข้อมูลสถานที่ยอดนิยมที่แอปนี้ให้บริการ ข้อมูลสถานที่ยอดนิยมใช้ข้อมูลที่รวบรวมจากบล็อกที่บล็อกเกอร์ชาวเกาหลีได้เยี่ยมชมและโพสต์รีวิวเกี่ยวกับร้านอาหาร เนื่องจากข้อมูลเหล่านี้ถูกรวบรวม อาจแตกต่างจากข้อมูลจริง ดังนั้นโปรดเข้าใจ


상세 정보 화면



 


각 마크 표시를 클릭 하면 상세 정보 표시 화면이 표시 됩니다 이 화면에서는 뒤로 가기 버튼 사용이 제한 되어 있으니, 꼭 위치정보 앞에 있는 뒤로화살표 버튼을 이용해 돌아가거나 팝업된 창을 내리면 됩니다.  또한 정보 하단에  이 앱에서 벗어나 웹 에서 검색할 수 있도록 외부 공유 버튼이 제공 됩니다. 


 


 



  • When you click on each mark, a detailed information screen will appear. On this screen, the back button function is restricted, so please use the back arrow button in front of the location information or swipe down the popup window to go back. Additionally, at the bottom of the information, an external sharing button is provided to allow searching on the web outside of this app.

  • 各マークをクリックすると、詳細情報画面が表示されます。 この画面では、戻るボタンの使用が制限されているため、位置情報の前にある戻る矢印ボタンを使用するか、ポップアップウィンドウを下にスワイプして戻ってください。 また、情報の下部には、このアプリを離れてウェブで検索できるように、外部共有ボタンが提供されています。

  • 单击各标记时,将显示详细信息屏幕。 在此屏幕上,返回按钮的功能受到限制,因此请使用位置信息前面的返回箭头按钮或向下滑动弹出窗口以返回。 另外,在信息底部提供了外部共享按钮,以便可以在此应用程序之外的网页上进行搜索。

  • 單擊各標記時,將顯示詳細資訊畫面。 在此畫面上,返回按鈕的功能受到限制,因此請使用位置資訊前面的返回箭頭按鈕或向下滑動彈出視窗以返回。 另外,在資訊底部提供了外部分享按鈕,以便可以在此應用程式之外的網頁上進行搜索。

  • Kapag pinindot mo ang bawat marka, lalabas ang detalyadong impormasyon sa screen. Sa screen na ito, ang paggamit ng back button ay limitado, kaya mangyaring gamitin ang back arrow button sa harap ng impormasyong panglokasyon o i-swipe pababa ang popup window upang bumalik. Bukod pa rito, sa ilalim ng impormasyon, may nakalaang external sharing button upang makapaghanap sa web sa labas ng app na ito.

  • Khi bạn nhấp vào từng biểu tượng, màn hình hiển thị thông tin chi tiết sẽ xuất hiện. Trên màn hình này, chức năng nút quay lại bị hạn chế, vì vậy hãy sử dụng nút mũi tên quay lại trước thông tin vị trí hoặc vuốt xuống cửa sổ popup để quay lại. Ngoài ra, ở phần dưới thông tin, có nút chia sẻ bên ngoài để tìm kiếm trên web bên ngoài ứng dụng này.

  • เมื่อคุณคลิกที่แต่ละเครื่องหมาย หน้าจอแสดงข้อมูลรายละเอียดจะปรากฏขึ้น ในหน้าจอนี้ ฟังก์ชันปุ่มย้อนกลับถูกจำกัด ดังนั้นโปรดใช้ปุ่มลูกศรย้อนกลับที่ด้านหน้าข้อมูลตำแหน่ง หรือปัดหน้าต่างป๊อปอัพลงเพื่อย้อนกลับ นอกจากนี้ ที่ด้านล่างของข้อมูล มีปุ่มแชร์ภายนอกเพื่อให้สามารถค้นหาเว็บนอกแอปนี้ได้


 


이 앱의 요청 권한 정보

 


이 앱의 기기의 위치 정보, 음성 인식 등의 권한을 요청 합니다.  위치 정보는 지도 상에 표시 되어야 하는 현재 위치을 표시 하기 위해서 이며, 음성인식은 기기에서 사용자의 음성을 텍스트로 변환 후 검색에 사용하기 위해서 사용 됩니다. 


 


 



  • This app requests permissions for device location information and voice recognition. Location information is required to display the current position on the map, and voice recognition is used to convert the user's speech into text for search purposes.

  • このアプリは、デバイスの位置情報と音声認識の権限を要求します。 位置情報は地図上に現在位置を表示するために必要であり、音声認識はユーザーの音声をテキストに変換して検索に使用されます。

  • 此应用请求设备位置信息和语音识别权限。 位置信息用于在地图上显示当前位置,而语音识别用于将用户的语音转换为文本,以便进行搜索。

  • 此應用程式請求設備位置資訊和語音識別權限。 位置資訊用於在地圖上顯示目前位置,而語音識別用於將使用者的語音轉換為文字,以進行搜尋。

  • Humihingi ang app na ito ng mga pahintulot para sa impormasyon ng lokasyon ng device at pagkilala sa boses. Ang impormasyon ng lokasyon ay kinakailangan upang ipakita ang kasalukuyang posisyon sa mapa, at ginagamit ang pagkilala sa boses upang i-convert ang pananalita ng user sa text para sa paghahanap.

  • Ứng dụng này yêu cầu quyền truy cập thông tin vị trí của thiết bị và nhận dạng giọng nói. Thông tin vị trí cần thiết để hiển thị vị trí hiện tại trên bản đồ, và nhận dạng giọng nói được sử dụng để chuyển đổi giọng nói của người dùng thành văn bản phục vụ tìm kiếm.

  • แอปนี้ร้องขอสิทธิ์สำหรับข้อมูลตำแหน่งของอุปกรณ์และการรู้จำเสียง ข้อมูลตำแหน่งจำเป็นสำหรับการแสดงตำแหน่งปัจจุบันบนแผนที่ และการรู้จำเสียงใช้เพื่อแปลงคำพูดของผู้ใช้เป็นข้อความสำหรับการค้นหา


 


*** help@billcorea.com : Help & Information for App's


 


 


 


 


 


 


 





오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기

 


 


DevExpress dxDataGrid 활용 및 JSON 데이터 처리


데이터 그리드



 


Q: DxDataGrid에서 선택된 행의 데이터를 배열 형태로 받는 방법

DxDataGrid에서 선택된 행의 데이터를 배열 형태로 가져오려면 getSelectedRowsData() 메서드를 사용합니다.
var selectedData = $("#dxDataGrid").dxDataGrid("instance").getSelectedRowsData();
console.log(selectedData); // 배열 형태로 출력


Q: 선택된 행의 데이터를 AJAX로 서버에 보내는 방법

`ajaxSubmit`을 사용하여 데이터를 JSON으로 변환한 후 서버로 전송할 수 있습니다.
$("#myForm").ajaxSubmit({
type: "POST",
url: "/server-endpoint",
data: { selectedRows: JSON.stringify(selectedData) },
success: function(response) {
console.log("서버 응답:", response);
}
});


Q: 서버에서 JSON을 HashMap으로 변환하는 방법

**Jackson 라이브러리**를 사용하면 간단하게 변환할 수 있습니다.
ObjectMapper objectMapper = new ObjectMapper();
HashMap<String, Object> map = objectMapper.readValue(json, HashMap.class);


Q: List 형태의 HashMap 데이터를 열어서 보는 방법

`for` 반복문을 사용하여 데이터를 출력할 수 있습니다.
for (HashMap<String, String> map : list) {
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
}


Q: DxDataGrid에서 JSON 리스트와 비교하여 같은 값이 있으면 선택되도록 하는 방법

`filter()`와 `selectRows()` 메서드를 활용합니다.
var selectedKeys = dataGrid.getVisibleRows()
.filter(row => jsonList.some(jsonItem => jsonItem.id === row.data.id && jsonItem.name === row.data.name))
.map(row => row.key);

dataGrid.selectRows(selectedKeys);


Q: SxDataGrid에서 selection mode가 multiple일 때 checkbox 클릭 시 선택을 제외하는 방법

`onCellClick` 이벤트에서 체크박스 컬럼을 감지하여 동작을 막습니다.
dataGrid.option("onCellClick", function(e) {
if (e.column.dataField === "selectionColumn") {
e.event.stopPropagation(); // 이벤트 전파 방지
}
});





오늘의 이야기

외국인을 위한 한국 여행 가이드 앱 개발 일지 - 위치 권한과 구글맵 화면 구현 (Jetpack Compose + Hilt)


앱 화면



 


오늘은 Jetpack Compose 기반으로 개발 중인 한국 여행 가이드 앱에서
인트로 화면 이후 위치 권한을 요청하고, 구글 지도를 표시하는 메인 화면을 구현했습니다.




✅ 오늘 구현한 핵심 기능

































항목 구현 방식
위치 권한 요청 Accompanist Permissions
위치 정보 획득 FusedLocationProviderClient (Hilt 주입)
지도 표시 Google Maps Compose
기본 위치 fallback 서울 시청 (37.5665, 126.9780)
권한 거부 시 안내 설정 화면으로 유도 (Intent)
상단바 겹침 방지 statusBarsPadding() 적용



📦 Hilt로 FusedLocationProviderClient 주입


@Module
@InstallIn(SingletonComponent::class)
object LocationModule {

@Provides
fun provideFusedLocationProviderClient(
@ApplicationContext context: Context
): FusedLocationProviderClient =
LocationServices.getFusedLocationProviderClient(context)
}

🧭 ViewModel에서 현재 위치 가져오기


@HiltViewModel
class MapViewModel @Inject constructor(
private val locationClient: FusedLocationProviderClient
) : ViewModel() {

var currentLocation by mutableStateOf<LatLng?>(null)
private set

fun fetchLocation(context: Context) {
try {
locationClient.lastLocation.addOnSuccessListener { location ->
location?.let {
currentLocation = LatLng(it.latitude, it.longitude)
}
}
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}

🗺️ MapScreen Composable


@Composable
fun MapScreen(viewModel: MapViewModel = hiltViewModel()) {
val context = LocalContext.current
val permissionState = rememberMultiplePermissionsState(
listOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)

val defaultLocation = LatLng(37.5665, 126.9780)
val currentLocation = viewModel.currentLocation ?: defaultLocation
val cameraPositionState = rememberCameraPositionState {
position = CameraPosition.fromLatLngZoom(currentLocation, 15f)
}

LaunchedEffect(Unit) {
permissionState.launchMultiplePermissionRequest()
}

LaunchedEffect(permissionState.allPermissionsGranted) {
if (permissionState.allPermissionsGranted) {
viewModel.fetchLocation(context)
}
}

Scaffold(
topBar = {
TopAppBar(
title = { Text("한국 여행 가이드") },
modifier = Modifier.statusBarsPadding()
)
},
contentWindowInsets = WindowInsets.systemBars
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
when {
permissionState.allPermissionsGranted -> {
GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState
) {
Marker(
state = MarkerState(position = currentLocation),
title = "현재 위치"
)
}
}

permissionState.shouldShowRationale -> {
PermissionExplanationUI {
permissionState.launchMultiplePermissionRequest()
}
}

else -> {
PermissionDeniedUI()
}
}
}
}
}

👮 권한 거절 대응 UI


@Composable
fun PermissionExplanationUI(onRequest: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("이 기능을 사용하려면 위치 권한이 필요합니다.")
Button(onClick = onRequest) {
Text("권한 요청하기")
}
}
}

@Composable
fun PermissionDeniedUI() {
val context = LocalContext.current
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("위치 권한이 영구적으로 거부되었습니다.")
Button(onClick = {
context.startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", context.packageName, null)
})
}) {
Text("설정에서 권한 허용하기")
}
}
}

📌 상단바가 StatusBar와 겹칠 때 해결법


TopAppBar에 Modifier.statusBarsPadding()을 추가하면 겹침 현상이 해결됩니다.


TopAppBar(
title = { Text("앱 이름") },
modifier = Modifier.statusBarsPadding()
)

또한 Scaffold에 아래 설정을 함께 추가하는 것이 좋습니다:


contentWindowInsets = WindowInsets.systemBars



📘 다음 목표



  • 실시간 위치 추적 추가

  • 주변 관광지 마커 표시

  • Navigation Compose와 통합


이 앱은 Jetpack Compose, Hilt, Room 등을 활용해 외국인들이 한국 여행을 쉽게 즐길 수 있도록 돕는 것이 목적입니다.
계속해서 개발 과정을 공유해보겠습니다! 👋





오늘의 이야기


#스하리1000명프로젝트,
迷失在韩国?即使您不会说韩语,这个应用程序也可以帮助您轻松出行。
只需说出您的语言即可 - 它会翻译、搜索并以您的语言显示结果。
非常适合旅行者!支持英语、日语、中文、越南语等10多种语言。
现在就试试吧!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




오늘의 이야기

Android 헬스 커넥트 시작하기: 통합 및 권한 관리 가이드


ㅎㅎㅎ





• 이 가이드는 Android 앱에서 헬스 커넥트를 사용하는 방법을 설명하며, Android 14(API 레벨 34)부터는 Android 프레임워크의 일부로 설정이 필요 없지만, Android 13 이하 버전에서는 Google Play 스토어에서 헬스 커넥트 앱을 설치해야 합니다.

• 헬스 커넥트는 사용자의 건강 데이터를 안전하게 저장하고 관리하며, 앱은 Health Connect SDK를 통해 데이터에 접근합니다.  데이터 접근은 읽기 및 쓰기 권한으로 관리되며, AndroidManifest.xml 파일에 해당 권한을 선언해야 합니다.

• 앱은 사용자에게 필요한 데이터 유형에 대한 권한을 요청해야 하며,  Play Console에서 선언된 권한과 일치해야 합니다.  <uses-permission> 태그를 사용하여 권한을 선언하고, 개인정보처리방침 링크를 포함하여 사용자에게 투명성을 제공해야 합니다.

• Android 13 이하 버전에서는 사용자가 개인정보처리방침 링크를 클릭할 경우 권한에 대한 추가 설명을 제공하는 Activity를 선언해야 하며, Android 14 이상에서는 activity-alias를 사용합니다.

• HealthConnectClient는 헬스 커넥트 API의 진입점으로, 데이터 스토어에 대한 연결을 관리하고 요청 및 응답을 처리합니다.  앱은 먼저 HealthConnectClient.getSdkStatus()를 사용하여 헬스 커넥트가 설치되어 있는지 확인해야 합니다.

• 헬스 커넥트가 설치되어 있지 않거나 업데이트가 필요한 경우, 앱은 사용자를 Google Play 스토어로 리다이렉트하여 설치 또는 업데이트를 유도할 수 있습니다.  HealthConnectClient 인스턴스를 생성한 후, 앱은 사용자에게 권한을 요청하고, 사용자는 언제든지 권한을 부여하거나 거부할 수 있습니다.

• 헬스 커넥트 SDK는 Android 8 (API 레벨 26) 이상을 지원하지만, 헬스 커넥트 앱 자체는 Android 9 (API 레벨 28) 이상에서만 호환됩니다. 따라서 Android 8 사용자는 헬스 커넥트 기능을 사용할 수 없다는 점을 고려해야 합니다.

https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started?hl=ko



 


헬스 커넥트 시작하기  |  Android health & fitness  |  Android Developers


이 페이지는 Cloud Translation API를 통해 번역되었습니다. 헬스 커넥트 시작하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 이 가이드는 Health Connect 버전 1.1


developer.android.com




 


github 에서 찾은 예제을 활용해 기초 코드을 작성해 보았습니다.


nari4169/Health501_public: public



 


GitHub - nari4169/Health501_public: public


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


github.com




 





2026/04/24

오늘의 이야기










🌍 외국인을 위한 다국어 환영 앱 첫 화면, Jetpack Compose로 구현하기

인트로 화면


 
한국을 방문하는 외국인 여행자들이 보다 따뜻한 인사를 받을 수 있도록, 앱의 첫 화면에서 다양한 언어로 환영 인사를 전하는 기능을 Jetpack Compose로 구현해보았습니다.
이 포스트에서는 그 구현 과정을 공유합니다.


✅ 프로젝트 개요


  • 목표: 한국을 방문한 외국인에게 친근하게 다가가는 애니메이션 환영 화면 제공

  • 기술 스택: Jetpack Compose, Kotlin

  • 특징:

    • 다국어 환영 인사 애니메이션

    • 각 언어에 해당하는 국기 아이콘 표시

    • 배경으로 한국의 가을 이미지 사용

    • 선택된 언어를 앱의 언어 설정으로 반영



🖼 구현된 주요 화면

1. 환영 애니메이션 화면


  • 왼쪽 상단에서 등장하여 오른쪽 하단으로 이동

  • 도중에 회전하며 중앙으로 이동, 확대 애니메이션 포함

  • 언어:

    • 한국어, 영어, 중국어, 일본어, 베트남어, 대만어, 필리핀어, 태국어


  • 언어에 맞는 국기 아이콘 함께 표시

  • 문장 길이에 따라 자동 위치 보정

※ Jetpack Compose의 Animatable, graphicsLayer, LaunchedEffect 등을 활용

2. 다음 버튼

  • 하단 중앙 배치
  • 현재 표시 중인 언어로 버튼 텍스트 자동 변경
  • 클릭 시 Haptic Feedback 제공
  • 해당 언어를 앱의 기본 언어로 설정
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.LocaleList
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.os.LocaleListCompat
import com.billcoreatech.opdgang1127.R
import kotlinx.coroutines.delay
import java.util.Locale
import androidx.core.content.edit

@SuppressLint("ConfigurationScreenWidthHeight")
@Composable
fun NewFaceMain(onNextClick: () -> Unit) {
    val messages = listOf(
        "환영합니다",               // 한국어
        "欢迎",                    // 중국어
        "ようこそ",               // 일본어
        "Welcome",                // 영어
        "Chào mừng",              // 베트남어
        "歡迎",                    // 대만어
        "Maligayang pagdating",   // 필리핀어
        "ยินดีต้อนรับ"   // 태국어
    )

    val flags = mapOf(
        "환영합니다" to R.drawable.flag_korea,
        "欢迎" to R.drawable.flag_china,
        "ようこそ" to R.drawable.flag_japan,
        "Welcome" to R.drawable.flag_usa,
        "Chào mừng" to R.drawable.flag_vietnam,
        "歡迎" to R.drawable.flag_taiwan,
        "Maligayang pagdating" to R.drawable.flag_philippines,
        "ยินดีต้อนรับ" to R.drawable.flag_thailand
    )

    val nextTexts = mapOf(
        "환영합니다" to "다음",
        "欢迎" to "下一步",
        "ようこそ" to "次へ",
        "Welcome" to "Next",
        "Chào mừng" to "Tiếp theo",
        "歡迎" to "下一步",
        "Maligayang pagdating" to "Susunod",
        "ยินดีต้อนรับ" to "ถัดไป"
    )

    val languageCodeMap = mapOf(
        "환영합니다" to "ko",
        "欢迎" to "zh",
        "ようこそ" to "ja",
        "Welcome" to "en",
        "Chào mừng" to "vi",
        "歡迎" to "zh-TW",
        "Maligayang pagdating" to "tl",
        "ยินดีต้อนรับ" to "th"
    )

    var currentIndex by remember { mutableStateOf(0) }
    val message = messages[currentIndex]
    val nextText = nextTexts[message] ?: "Next"
    val flagRes = flags[message] ?: R.drawable.flag_korea
    val haptic = LocalHapticFeedback.current

    val offsetX = remember { Animatable(0f) }
    val offsetY = remember { Animatable(0f) }
    val rotation = remember { Animatable(0f) }
    val scale = remember { Animatable(1f) }

    val configuration = LocalConfiguration.current
    val screenWidth = configuration.screenWidthDp.dp
    val screenHeight = configuration.screenHeightDp.dp
    val density = LocalDensity.current

    val messageOffsetAdjustment = mapOf(
        "환영합니다" to Pair(-150, -50),
        "欢迎" to Pair(-110, -50),
        "ようこそ" to Pair(-130, -50),
        "Welcome" to Pair(-140, -50),
        "Chào mừng" to Pair(-200, -50),
        "歡迎" to Pair(-110, -50),
        "Maligayang pagdating" to Pair(-250, -50),
        "ยินดีต้อนรับ" to Pair(-120, -50)
    )

    val context = LocalContext.current

    val onLanguageSelected = {
        val langCode = languageCodeMap[message] ?: "en"

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            val localeList = LocaleListCompat.forLanguageTags(langCode)
            AppCompatDelegate.setApplicationLocales(localeList)
        } else {
            val locale = Locale(langCode)
            Locale.setDefault(locale)
            val config = Configuration()
            config.setLocale(locale)
            context.resources.updateConfiguration(config, context.resources.displayMetrics)
        }

        context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)
            .edit {
                putString("language", langCode)
            }
        // 언어 설정을 저장하고 다음 화면으로 이동
        onNextClick()
    }


    LaunchedEffect(message) {
        offsetX.snapTo(0f)
        offsetY.snapTo(0f)
        rotation.snapTo(0f)
        scale.snapTo(1f)

        val endX = with(density) { (screenWidth * 0.8f).toPx() }
        val endY = with(density) { (screenHeight * 0.3f).toPx() }

        val (adjustX, adjustY) = messageOffsetAdjustment[message] ?: Pair(-100, -50)
        val centerX = with(density) { (screenWidth / 2).toPx() + adjustX}
        val centerY = with(density) { (screenHeight / 2).toPx() + adjustY }

        // 곡선형 경로 이동
        val midX = (endX + centerX) / 2 + 100f
        val midY = (endY + centerY) / 2 - 100f

        offsetX.animateTo(endX, animationSpec = tween(800, easing = FastOutSlowInEasing))
        offsetY.animateTo(endY, animationSpec = tween(800, easing = LinearOutSlowInEasing))

        rotation.animateTo(360f, animationSpec = tween(600))

        offsetX.animateTo(midX, animationSpec = tween(400))
        offsetY.animateTo(midY, animationSpec = tween(400))

        offsetX.animateTo(centerX, animationSpec = tween(400))
        offsetY.animateTo(centerY, animationSpec = tween(400))

        scale.animateTo(
            targetValue = 1.5f,
            animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing)
        )
        delay(500)
        scale.animateTo(1f)

        delay(1000)
        currentIndex = (currentIndex + 1) % messages.size
    }

    Box(modifier = Modifier.fillMaxSize()) {
        Image(
            painter = painterResource(id = R.drawable.autumn_korea),
            contentDescription = "Autumn in Korea",
            contentScale = ContentScale.Crop,
            modifier = Modifier.fillMaxSize()
        )
        Image(
            painter = painterResource(id = R.drawable.ic_opdigang_v2),
            contentDescription = "App Logo",
            modifier = Modifier
                .padding(start = 16.dp, top = 48.dp)
                .size(64.dp)
        )
        Box(
            modifier = Modifier
                .fillMaxSize()
                .background(
                    brush = Brush.verticalGradient(
                        colors = listOf(Color.Transparent, Color(0x88000000)),
                        startY = 300f, endY = 1200f
                    )
                )
        ) {}

        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .offset {
                    val bounceX = offsetX.value + kotlin.math.sin(offsetY.value / 60) * 8
                    val bounceY = offsetY.value
                    IntOffset(bounceX.toInt(), bounceY.toInt())
                }
                .graphicsLayer(
                    rotationZ = rotation.value,
                    scaleX = scale.value,
                    scaleY = scale.value
                )
        ) {
            Image(
                painter = painterResource(id = flagRes),
                contentDescription = "Flag",
                modifier = Modifier
                    .size(36.dp)
                    .padding(bottom = 4.dp)
            )
            Text(
                text = message,
                fontSize = if (message.length > 15) 24.sp else 32.sp,
                fontWeight = FontWeight.Bold,
                color = Color.White,
                textAlign = TextAlign.Center,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis,
                modifier = Modifier.widthIn(max = 240.dp)
            )
        }

        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(bottom = 48.dp),
            verticalArrangement = Arrangement.Bottom,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(
                onClick = {
                    haptic.performHapticFeedback(HapticFeedbackType.LongPress)
                    onLanguageSelected()
                },
                shape = RoundedCornerShape(24.dp),
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xAA000000)),
                contentPadding = PaddingValues(horizontal = 32.dp, vertical = 12.dp)
            ) {
                Text(nextText, fontSize = 18.sp, color = Color.White)
            }
        }
    }
}

🌐 다국어 및 로컬 설정


  • res/values-XX/strings.xml 파일을 각 언어별로 분리

  • AppCompatDelegate를 통해 앱 내 언어 설정 변경 지원

  • LocaleListCompat를 사용하여 호환성 확보

예시 - 태국어 strings.xml

<string name="intro_text">ทำให้การเดินทางของคุณง่ายขึ้น! ค้นหาข้อมูลท้องถิ่นตอนนี้เลย</string>
<string name="get_started">เริ่มต้น</string>

🇹🇭 태국어 추가 시 작업 내용


  • 문장: “ยินดีต้อนรับ” 추가

  • 국기: flag_thailand.png 리소스 추가 및 연결

  • 위치 보정값: Pair(-120, -50) 사용


🔚 다음 작업 예고

  • 인트로 이후 화면 디자인 및 기능 구현
  • 위치 기반 추천 정보 및 서비스 안내
  • 사용자 설정 및 프로필 저장 기능 연동

🧩 마무리하며

이번 작업을 통해 외국인 사용자에게 보다 문화적 배려를 담은 UX를 제공할 수 있는 첫 발걸음을 마련했습니다.
Jetpack Compose의 직관적인 구조와 애니메이션 지원 덕분에 자연스럽고 따뜻한 인사 화면을 구현할 수 있었습니다.
다음 포스트에서는 인트로 이후 화면을 구성해보겠습니다. 감사합니다!
 
 

움직이는 인트로 화면





반응형
























오늘의 이야기

#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase 🎯 야 너 토요일마다 로또 확인하냐? 나도 맨날 “혹시나~” 하면서 봤거든 ㅋㅋ 근데 이제는 그냥 안 해 AI한테 맡겼어 🤖✨ 그것도 구글 Gemini로다가! ...