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의 직관적인 구조와 애니메이션 지원 덕분에 자연스럽고 따뜻한 인사 화면을 구현할 수 있었습니다.
다음 포스트에서는 인트로 이후 화면을 구성해보겠습니다. 감사합니다!
 
 

움직이는 인트로 화면





반응형
























오늘의 이야기

Android 개발자 블로그 요약: Gemini Nano 실험용 액세스 제공


copilot 의 그림



 


Google은 개발자를 위해 Gemini Nano를 Android 기기에서 실험적으로 사용할 수 있도록 액세스를 열었습니다. 이는 온디바이스 AI 모델로, 기기 내에서 직접 AI 연산을 수행할 수 있어 개인 데이터 보호와 비용 효율성을 제공합니다.


Gemini Nano는 특히 텍스트 생성 및 변형 작업(예: 요약, 교정, 스마트 답장)에서 뛰어난 성능을 발휘하며, 현재 Pixel 9 시리즈에서 실험할 수 있습니다. 더 많은 기기와 사용 사례가 곧 지원될 예정입니다.


개발자는 AICoreAI Edge SDK를 활용해 Gemini Nano를 앱에 통합할 수 있으며, 다양한 모델 설정을 조정하여 맞춤형 추론이 가능합니다. 사용자는 이를 통해 더욱 빠르고 정확한 AI 지원 기능을 경험할 수 있습니다.


자세한 내용과 시작 가이드는 공식 문서를 참고하세요!


 


Android 개발자 블로그: 이제 실험용 액세스를 통해 Android에서 Gemini Nano를 사용할 수 있습니다.



 


Gemini Nano is now available on Android via experimental access


Gemini Nano with Multimodality, Google's on-device generative AI model offers privacy, offline functionality, integration guides, and more.


android-developers.googleblog.com




 


이렇게 해서 라도 앱에 AI 을 접목해 볼 수 있을까요?





오늘의 이야기

 


 


외국인 관광객을 위한 앱 만들기 : Jetpack Compose에서 Google Maps로 실시간 위치 추적 및 야간 모드 적용하기


dark mode google map



 


Jetpack Compose에서 Google Map을 전면에 표시하고, 실시간으로 위치를 추적하며 지도 카메라를 이동시키고, 야간 모드까지 적용하는 방법을 단계별로 정리합니다.




1. Hilt ViewModel에서 위치 실시간 추적


FusedLocationProviderClient를 활용하여 위치를 계속 추적합니다:



@HiltViewModel
class LocationViewModel @Inject constructor(
@ApplicationContext private val context: Context
) : ViewModel() {

private val fusedLocationClient = LocationServices.getFusedLocationProviderClient(context)
private val _locationFlow = MutableStateFlow<LatLng?>(null)
val locationFlow: StateFlow<LatLng?> = _locationFlow

private val locationCallback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult) {
result.lastLocation?.let {
_locationFlow.value = LatLng(it.latitude, it.longitude)
}
}
}

init {
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 3000).build()
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
fusedLocationClient.requestLocationUpdates(request, locationCallback, Looper.getMainLooper())
}
}

override fun onCleared() {
fusedLocationClient.removeLocationUpdates(locationCallback)
}
}



2. Compose에서 지도와 위치 UI 표시


ViewModel의 위치를 수신하여 Google Map 카메라를 이동시킵니다:



@Composable
fun RealTimeLocationMapScreen(viewModel: LocationViewModel = hiltViewModel()) {
val cameraPositionState = rememberCameraPositionState()
val currentLocation by viewModel.locationFlow.collectAsState()

LaunchedEffect(currentLocation) {
currentLocation?.let {
cameraPositionState.animate(
update = CameraUpdateFactory.newLatLngZoom(it, 16f),
durationMs = 1000
)
}
}

GoogleMap(
modifier = Modifier.fillMaxSize(),
cameraPositionState = cameraPositionState,
properties = MapProperties(isMyLocationEnabled = true),
uiSettings = MapUiSettings(
myLocationButtonEnabled = true,
zoomControlsEnabled = true,
compassEnabled = true
)
)
}



3. 위치 권한 요청 (Compose)



@Composable
fun RequestLocationPermissions(onPermissionsGranted: () -> Unit) {
val context = LocalContext.current
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
if (permissions[Manifest.permission.ACCESS_FINE_LOCATION] == true) {
onPermissionsGranted()
}
}

LaunchedEffect(Unit) {
permissionLauncher.launch(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
))
}
}



4. Google Map 옵션 설정 (내 위치 버튼 등)



GoogleMap(
properties = MapProperties(
isMyLocationEnabled = true
),
uiSettings = MapUiSettings(
myLocationButtonEnabled = true,
zoomControlsEnabled = true,
compassEnabled = true,
rotateGesturesEnabled = true
)
)



5. Google Map에 야간 모드 적용하기


야간 스타일 JSONres/raw 또는 assets로 저장한 후 적용합니다.


1. res/raw/google_night_style.json 예시:


[
{ "elementType": "geometry", "stylers": [ { "color": "#242f3e" } ] },
{ "elementType": "labels.text.fill", "stylers": [ { "color": "#746855" } ] }
// ...
]

2. 적용 코드:



val mapStyle = remember {
MapStyleOptions.loadRawResourceStyle(context, R.raw.google_night_style)
}

GoogleMap(
properties = MapProperties(
isMyLocationEnabled = true,
mapStyleOptions = mapStyle
)
)



6. 시스템 다크 모드에 따라 자동 전환



@Composable
fun getMapStyleByUiMode(context: Context): MapStyleOptions {
return if (isSystemInDarkTheme()) {
MapStyleOptions.loadRawResourceStyle(context, R.raw.google_night_style)
} else {
MapStyleOptions.loadRawResourceStyle(context, R.raw.google_day_style)
}
}



마무리


Jetpack Compose를 활용해 Google Maps에 실시간 위치를 반영하고, 다양한 사용자 경험을 위해 UI 옵션 및 스타일을 적용하는 방법을 정리했습니다.
야간 모드 스타일링이나 위치 추적 성능 최적화는 향후 앱 완성도에 매우 중요하므로 잘 적용해 보시기 바랍니다.





오늘의 이야기



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

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

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

그것도 구글 Gemini로다가!

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

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

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


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




오늘의 이야기










《시간을 파는 가게》

chatgpt


 

4장. 돌아온 하루

눈을 뜬 순간—
세상은 이상할 정도로 조용했다.
햇살은 부드럽고,
창문 밖으론 고양이 울음소리와 나뭇잎 흔들리는 소리만 들렸다.
휴대폰도, 회사 메신저도, 회의 알림도 없었다.
모든 게 멈춰 있었다.
아니, 아니었다.
모든 게… 돌아가 있었다.
그는 침대에서 몸을 일으켰다.
하지만 이건 지금 그의 방이 아니었다.
묘하게 낯익고, 오래된 구조의 작은 방.
벽엔 그가 어릴 적 좋아하던 로봇 포스터가 붙어 있었고,
서랍 속엔 세월이 덮은 색연필과 딱지들이 가지런히 들어 있었다.
거울을 보았다.
자신은 여전히 지금의 모습이었다.
하지만 세상은—1996년의 어느 봄날로 돌아가 있었다.
그는 마당으로 나갔다.
햇빛이 따뜻하게 뺨을 쓸고 지나갔다.
그리운 흙냄새.
옆집 아주머니가 부르는 개 짖는 소리.
멀리서 들리는, 아이들의 웃음소리.
그리고 그 소리.
딱, 그 소리.
“…그 아이?”
그는 발길을 따라 골목길로 향했다.
그리고 거기—
시간 속에 묻혀 있던 그 장면이 다시 펼쳐졌다.
철문 앞에 서 있는 작은 아이.
긴 머리, 수줍은 눈빛.
그는 너무 놀라서 말을 잊었다.
“...안녕.”
그 아이가 말했다.
그 순간, 그는 어린 자신이 아니라
지금의 시선으로 그 아이를 바라보고 있었다.
기억 속 그 목소리, 그 표정.
정확히, 그대로였다.
그는 웃음과 눈물이 섞인 얼굴로 인사했다.
“…안녕.”


그 하루는 마치 기적 같았다.
함께 숨바꼭질을 하고,
아이스크림을 나눠 먹고,
작은 비밀기지를 만들어 웃던 하루.
그는 처음엔 어색했지만,
점점 이 시간이 기억이 아니라 현실이라는 것을 받아들이기 시작했다.
이 하루는 너무 완벽했다.
다시 살아도 좋을 만큼, 완벽하게 아름다웠다.
그는 속으로 중얼거렸다.
‘이 하루를 다시 산다면,
잃게 될 것이 있어도…
후회하지 않을 수 있을까?’
해가 뉘엿뉘엿 기울 무렵,
그 아이가 작은 목소리로 말했다.
“내일도 볼 수 있어?”
그는 대답하지 못했다.
목이 멨고, 마음이 흔들렸다.
왜냐면—
내일은 없다는 걸 그는 알고 있었으니까.


해가 완전히 지자,
세상은 다시 조용해졌다.
그리고 어두운 가게 안,
그는 눈을 떴다.
모래시계가 멈춰 있었다.
그는 여전히 그대로였다.
하지만 뭔가—
무언가가 사라졌다는 느낌이 들었다.
주인은 말없이 서 있었다.
눈빛은 조용히 그를 바라보고 있었다.
“…무엇을 잃었죠?”
그가 물었다.
주인은 천천히 말했다.
“그건 당신만이 알게 될 겁니다.
곧, 아주 가까운 시간 안에.”





반응형
























오늘의 이야기

티스토리 수익화 관련 운영 정책 변경 안내 - Google AdSense 커뮤니티



 


티스토리 수익화 관련 운영 정책 변경 안내 - Google AdSense 커뮤니티


 


support.google.com




 


광고제한 안내문



 


티스토리 해서 얼마나 많은 광고 수익 생기는 건지 ??? ㅠㅠ ;;


아무튼 새로운 제약 사항이 생겼습니다.  시행이 6월 1일 이라니 준비를 해야 할 것 같습니다. 


 


아래 글은 adsens 지원팀의 게시글 입니다. 


 


티스토리 광고 설정 정책에 관해 중요한 안내사항이 있어 전달드립니다. 

티스토리 커뮤니티에 공지된 바와 같이, 티스토리의 자체 광고 운영 정책 변경 결정에 따라 티스토리에서 “앵커 광고와 오퍼월 광고”는 설정이 불가능해집니다. 애드센스 자동광고 설정에서 앵커 광고를 켜는 것과 직접 html 스크립트를 통해 구현하는 것을 포함 모든 방식의 앵커 광고와 오퍼월 광고는 허용되지 않습니다.

자동 광고에서 앵커 광고 끄는 방법:
애드센스 로그인
왼쪽 패널에서 “광고” 클릭
해당 사이트에 “연필” 아이콘 클릭
“광고 설정”에서 “오버레이 형식” 클릭
“형식”에서 “앵커 광고” 설정 OFF 하기

오퍼월 광고 끄는 방법:
애드센스 로그인
왼쪽 패널에서 “개인 정보 보호 및 메시지” 클릭
Offerwall “관리” 클릭
게시 버튼을 끄거나 “⋮” 버튼을 클릭하여 오퍼월 메시지를 삭제.

6월 1일 (월)부터 설정 불가 규제가 시작되니, 이 점 참고하셔서 적절한 조치를 취해주시면 감사하겠습니다.

애드센스팀 드림

 


참고 하셔서 꼭 설정 하세요.





오늘의 이야기

eclipse template 설정해 보기



 






Eclipse java code templates 생성 하는 방법?

 






이클립스에서 새로운 클래스 템플릿을 생성하려면 다음 단계를 따를 수 있습니다:



  1. 이클립스 실행Window > Preferences로 이동합니다.

  2. Java > Code Style > Code Templates를 선택합니다.

  3. Class 항목을 선택한 후 Edit 버튼을 클릭합니다.

  4. 원하는 템플릿 내용을 입력하고 OK를 눌러 저장합니다.

  5. 새 클래스를 생성할 때 해당 템플릿이 적용됩니다.


더 자세한 설명은 여기에서 확인할 수 있습니다. 도움이 필요하면 알려주세요! 😊






 





이때 class name을 생성 하는 파일 이름으로 지정하고 class body을 구성 하려면?

 






 


이클립스에서 클래스 이름을 파일 이름으로 지정하고 클래스 본문을 구성하려면 다음과 같이 설정할 수 있습니다:



  1. 클래스 이름 자동 지정

    • 이클립스에서 New Java Class를 생성할 때, Name 필드에 입력한 값이 자동으로 클래스 이름이 됩니다.

    • 파일 이름과 클래스 이름은 동일해야 하며, 이를 변경하면 컴파일 오류가 발생할 수 있습니다.



  2. 클래스 본문 구성

    • Code Templates를 활용하여 기본 클래스 구조를 자동으로 생성할 수 있습니다.

    • Window > Preferences > Java > Code Style > Code Templates에서 Class 항목을 수정하면 새 클래스 생성 시 자동으로 본문이 포함됩니다.




이 방법을 활용하면 파일 이름을 기반으로 클래스 이름을 지정하고, 원하는 클래스 본문을 자동으로 구성할 수 있습니다. 도움이 필요하면 알려주세요! 😊









오늘의 이야기

Gradle buildSrc에서 KSP(코틀린 심볼 프로세싱) 플러그인 사용하기 예제


언제나 개발해 보기



 


Gradle 프로젝트를 설정할 때, buildSrc 디렉토리에서 id("com.google.devtools.ksp") 플러그인을 찾을 수 없다는 오류가 발생하는 경우가 있습니다. 이는 Gradle의 빌드 스크립트 컨텍스트와 플러그인 정의 위치 사이의 차이로 인해 발생합니다.


이번 글에서는 이 문제를 해결하는 방법에 대해 단계별로 알아보겠습니다.




1. buildSrc 프로젝트의 build.gradle.kts 수정


buildSrc는 Gradle의 독립적인 빌드 환경으로 동작하기 때문에, 사용하려는 플러그인을 명시적으로 추가해야 합니다. buildSrc/build.gradle.kts 파일을 열고 다음을 추가하세요:


plugins {
`kotlin-dsl` // Gradle Kotlin DSL 플러그인
}

repositories {
gradlePluginPortal()
mavenCentral()
}

dependencies {
implementation("com.google.devtools.ksp:symbol-processing-gradle-plugin:<KSP_VERSION>")
}

<KSP_VERSION> 부분을 사용하려는 KSP 플러그인의 정확한 버전으로 교체하세요. 예를 들어, 최신 버전이 1.9.0-1.0.13이라면 다음과 같이 작성합니다:


implementation("com.google.devtools.ksp:symbol-processing-gradle-plugin:1.9.0-1.0.13")



2. buildSrc 내에서 플러그인 적용


buildSrc 내부에서는 id("com.google.devtools.ksp")를 바로 사용할 수 없을 수 있습니다. 이 경우, 다음과 같이 apply 메소드를 사용하여 플러그인을 적용할 수 있습니다:


apply(plugin = "com.google.devtools.ksp")



3. 루트 프로젝트의 settings.gradle.kts 수정


루트 프로젝트의 settings.gradle.kts 파일에서 pluginManagement 블록을 추가하여 플러그인 저장소를 명시적으로 설정합니다:


pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral()
}
}



4. KSP 플러그인 최신 버전 확인


KSP 플러그인의 최신 버전을 사용하려면, Maven Central의 KSP 페이지 또는 Gradle의 플러그인 포털을 참고하세요.




5. Gradle 캐시 정리 및 동기화


Gradle 캐시가 손상되었거나 플러그인이 올바르게 로드되지 않을 수 있습니다. 다음 명령어를 실행하여 캐시를 정리하고 프로젝트를 동기화하세요:


./gradlew --stop
./gradlew clean
./gradlew build --refresh-dependencies



마무리


위 단계를 따르면 buildSrc 디렉토리에서도 com.google.devtools.ksp 플러그인을 정상적으로 사용할 수 있습니다. 문제가 계속 발생한다면, module.kotlin.gradle.kts 파일이나 관련 Gradle 설정 파일의 내용을 검토하여 추가적인 설정이 필요한지 확인하세요.


이 글이 Gradle 프로젝트 설정에 도움이 되길 바랍니다. 질문이나 피드백이 있다면 댓글로 남겨주세요!





오늘의 이야기


#스하리1000명프로젝트,
韓国で迷子になりましたか?韓国語が話せなくても、このアプリを使えば簡単に移動できます。
あなたの言語で話すだけで、翻訳、検索が行われ、結果があなたの言語で表示されます。
旅行者に最適!英語、日本語、中国語、ベトナム語などを含む 10 以上の言語をサポートします。
今すぐ試してみましょう!
https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127




2026/04/23

오늘의 이야기

초기화면 예시



앱의 첫 화면은 외국인 여행자가 직관적으로 사용할 수 있도록 간결하면서도 유용한 정보를 제공해야 해요. 아래와 같은 방식으로 구성하면 좋을 것 같아요!


1. 환영 화면 (Splash Screen)



  • 앱 로고 및 간단한 환영 메시지 (예: “Welcome to Seoul! Let’s explore with ease.”)

  • 배경 이미지는 명동의 대표적인 랜드마크 또는 친근한 디자인

  • 2~3초 후 메인 화면으로 자동 이동


2. 메인 화면 (Home Screen)



  • 실시간 길 찾기 버튼 (GPS 기반 ‘현재 위치에서 목적지 찾기’ 기능)

  • 번역 기능 바로가기 (음성 및 텍스트 번역을 위한 빠른 액세스 버튼)

  • 추천 장소 (현재 위치 기반 인기 관광지 및 음식점 추천)

  • 긴급 도움 요청 (현지 도움을 받을 수 있는 기능)

  • 탐색 바 (카테고리별 정보: 교통, 쇼핑, 음식, 관광)


3. 하단 내비게이션 바



  • 홈 아이콘: 기본 화면으로 돌아가기

  • 지도 아이콘: 실시간 길 찾기 서비스 접속

  • 번역 아이콘: 텍스트 및 음성 번역 기능

  • 커뮤니티 아이콘: 현지인과 연결, 여행자끼리 정보 공유

  • 설정 아이콘: 언어 선택, 앱 사용 방법 안내


4. 사용자 맞춤 화면



  • 처음 앱을 실행할 때 여행 목적(관광, 업무, 쇼핑 등)과 선호 기능을 선택 가능하도록 설정

  • 사용자의 여행 일정 및 방문 목적지에 따라 맞춤 추천 제공


이렇게 구성하면 사용자 경험이 편리하고 직관적일 거예요! 더 추가하고 싶은 기능이 있나요? 😊 어디 까지를 구현해 볼 수 있을까요???


 





오늘의 이야기

Xwings를 활용한 엑셀 하이퍼링크 관리


하이퍼 링크



 


엑셀에서 Xwings 라이브러리를 사용하여 하이퍼링크를 추가, 수정, 삭제하는 방법을 알아보겠습니다.


1. Xwings 설치 및 엑셀 파일 열기


먼저, Xwings가 설치되어 있어야 합니다. 터미널이나 CMD에서 다음 명령어를 실행하세요.



bash



pip install xlwings




이후, 엑셀 파일을 열거나 새로 생성하여 작업을 시작할 수 있습니다.



python



import xlwings as xw

wb = xw.Book("example.xlsx") # 기존 파일 열기
ws = wb.sheets["Sheet1"] # 원하는 시트 선택




2. 하이퍼링크 추가하기


웹사이트로 이동하는 하이퍼링크를 추가하려면 다음과 같이 작성합니다.



python



ws.range("A1").value = "클릭하여 이동"
ws.range("A1").api.Hyperlinks.Add(
Anchor=ws.range("A1").api,
Address="https://www.microsoft.com",
TextToDisplay="Microsoft"
)




3. 같은 파일 내 다른 시트로 이동하는 하이퍼링크


엑셀의 특정 시트로 이동하려면 SubAddress를 사용해야 합니다.



python



ws.range("A1").value = "다른 시트로 이동"
ws.range("A1").api.Hyperlinks.Add(
Anchor=ws.range("A1").api,
Address="",
SubAddress="Sheet2!A1",
TextToDisplay="Sheet2로 이동"
)




4. 하이퍼링크 삭제하기


셀에 설정된 하이퍼링크를 제거하려면 다음 코드를 사용하세요.



python



if ws.range("A1").api.Hyperlinks.Count > 0:
ws.range("A1").api.Hyperlinks.Item(1).Delete()




5. 파일 저장 및 닫기


모든 작업이 완료되면 엑셀 파일을 저장하고 닫습니다.



python



wb.save()
wb.close()




 





오늘의 이야기

🌍 외국인을 위한 다국어 환영 앱 첫 화면, Jetpack Compose로 구현하기 인트로 화면   한국을 방문하는 외국인 여행자들이 보다 따뜻한 인사를 받을 수 있도록, 앱의 ...