<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>panax.w</title>
        <link>https://velog.io/</link>
        <description>Android Developer</description>
        <lastBuildDate>Mon, 12 May 2025 06:00:35 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>panax.w</title>
            <url>https://images.velog.io/images/onegold-11/profile/2f9765cc-dcd9-4bbe-8962-bc4a3c40fbb2/paws.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. panax.w. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/onegold-11" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Android 멀티 모듈에 build-logic 적용]]></title>
            <link>https://velog.io/@onegold-11/Android-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88%EC%97%90-build-logic-%EC%A0%81%EC%9A%A9</link>
            <guid>https://velog.io/@onegold-11/Android-%EB%A9%80%ED%8B%B0-%EB%AA%A8%EB%93%88%EC%97%90-build-logic-%EC%A0%81%EC%9A%A9</guid>
            <pubDate>Mon, 12 May 2025 06:00:35 GMT</pubDate>
            <description><![CDATA[<h2 id="⚙️-목표">⚙️ 목표</h2>
<ul>
<li>안드로이드 프로젝트를 멀티 모듈 구조로 변경하면서 <code>build.gradle.kts</code> 파일이 많아 관리가 힘든 문제 발생</li>
<li><code>common.gradle</code>파일을 사용해 공통되는 부분을 줄였지만, 여전히 파일을 하나하나 관리해야 함</li>
<li><code>build-logic</code> 모듈을 사용해 플러그인 형태로 관리하려고 함</li>
</ul>
<h2 id="🧱-build-logic-모듈-생성">🧱 build-logic 모듈 생성</h2>
<ul>
<li><p><code>build-logic</code>모듈 생성</p>
<ul>
<li>Android 모듈과 Kotlin 모듈 둘 다 해봤는데 되는 것 같음</li>
</ul>
</li>
<li><p>테스트, 리소스 등 불필요한 파일은 정리</p>
</li>
</ul>
<h2 id="🧭-설정-파일-구성">🧭 설정 파일 구성</h2>
<h3 id="🔧-build-logicsettingsgradlekts">🔧 <code>build-logic/settings.gradle.kts</code></h3>
<p>해당 파일이 없으면 직접 생성해야함함</p>
<pre><code class="language-kotlin">enableFeaturePreview(&quot;TYPESAFE_PROJECT_ACCESSORS&quot;)

@Suppress(&quot;UnstableApiUsage&quot;)
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }

    // 버전 카탈로그 사용할 수 있게 설정정
    versionCatalogs {
        create(&quot;libs&quot;) {
            from(files(&quot;../gradle/libs.versions.toml&quot;))
        }
    }
}</code></pre>
<h3 id="🔧-프로젝트의-settingsgradlekts">🔧 프로젝트의 <code>settings.gradle.kts</code></h3>
<pre><code class="language-kotlin">pluginManagement {
    includeBuild(&quot;build-logic&quot;)
}</code></pre>
<p>❗ 모듈을 만들면 추가되는 <code>include(&quot;:build-logic&quot;)</code>는 제거</p>
<h2 id="📦-라이브러리-정의-libsversionstoml">📦 라이브러리 정의 (<code>libs.versions.toml</code>)</h2>
<pre><code class="language-toml">[versions]
kotlin = &quot;2.0.0&quot;
androidGradlePlugin = &quot;8.5.0&quot;
org-jetbrains-kotlin-android = &quot;1.9.24&quot;

[libraries]
android-gradlePlugin = { group = &quot;com.android.tools.build&quot;, name = &quot;gradle&quot;, version.ref = &quot;androidGradlePlugin&quot; }
kotlin-gradlePlugin = { group = &quot;org.jetbrains.kotlin&quot;, name = &quot;kotlin-gradle-plugin&quot;, version.ref = &quot;org-jetbrains-kotlin-android&quot; }
compose-compiler-gradle-plugin = { module = &quot;org.jetbrains.kotlin:compose-compiler-gradle-plugin&quot;, version.ref = &quot;kotlin&quot; }</code></pre>
<h2 id="📜-build-logicbuildgradlekts">📜 <code>build-logic/build.gradle.kts</code></h2>
<pre><code class="language-kotlin">plugins {
    `kotlin-dsl`
    `kotlin-dsl-precompiled-script-plugins`
}

dependencies {
    implementation(libs.android.gradlePlugin)
    implementation(libs.kotlin.gradlePlugin)
    compileOnly(libs.compose.compiler.gradle.plugin)
}</code></pre>
<h2 id="🛠️-코드-작성-예시">🛠️ 코드 작성 예시</h2>
<h3 id="✅-공통-확장-함수-extensionkt">✅ 공통 확장 함수 (<code>Extension.kt</code>)</h3>
<p>필수는 아니지만 있으면 편함</p>
<pre><code class="language-kotlin">/**
 * Application extension
 */
internal val Project.applicationExtension: CommonExtension&lt;*, *, *, *, *, *&gt;
    get() = extensions.getByType&lt;ApplicationExtension&gt;()

/**
 * Library extension
 */
internal val Project.libraryExtension: CommonExtension&lt;*, *, *, *, *, *&gt;
    get() = extensions.getByType&lt;LibraryExtension&gt;()

/**
 * Android extension
 */
internal val Project.androidExtension: CommonExtension&lt;*, *, *, *, *, *&gt;
    get() = runCatching { libraryExtension }
        .recoverCatching { applicationExtension }
        .onFailure { println(&quot;Could not find Library or Application extension from this project&quot;) }
        .getOrThrow()

/**
 * version catalog
 */
internal val ExtensionContainer.libs: VersionCatalog
    get() = getByType&lt;VersionCatalogsExtension&gt;().named(&quot;libs&quot;)</code></pre>
<h3 id="✅-compose-설정-함수-composeandroidkt">✅ Compose 설정 함수 (<code>ComposeAndroid.kt</code>)</h3>
<pre><code class="language-kotlin">internal fun Project.configureComposeAndroid() {

    // plugin
    with(plugins) {
        apply(&quot;org.jetbrains.kotlin.plugin.compose&quot;)
    }

    // library
    val libs = extensions.libs
    androidExtension.apply {
        dependencies {
            val bom = libs.findLibrary(&quot;androidx-compose-bom&quot;).get()
            add(&quot;implementation&quot;, platform(bom))
            add(&quot;androidTestImplementation&quot;, platform(bom))

            add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx.compose.material3&quot;).get())
            add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx.compose.ui&quot;).get())
            add(&quot;implementation&quot;, libs.findLibrary(&quot;androidx.compose.ui.tooling.preview&quot;).get())

            add(&quot;androidTestImplementation&quot;, libs.findLibrary(&quot;androidx.test.ext&quot;).get())
            add(&quot;androidTestImplementation&quot;, libs.findLibrary(&quot;androidx.test.espresso.core&quot;).get())
            add(&quot;androidTestImplementation&quot;, libs.findLibrary(&quot;androidx.compose.ui.test&quot;).get())

            add(&quot;debugImplementation&quot;, libs.findLibrary(&quot;androidx.compose.ui.tooling&quot;).get())
            add(&quot;debugImplementation&quot;, libs.findLibrary(&quot;androidx.compose.ui.testManifest&quot;).get())
        }
    }

    extensions.getByType&lt;ComposeCompilerGradlePluginExtension&gt;().apply {
        enableStrongSkippingMode.set(true)
        includeSourceInformation.set(true)
    }
}</code></pre>
<h2 id="🔌-플러그인-적용-방법">🔌 플러그인 적용 방법</h2>
<h3 id="방법-1-gradleplugin-dsl">방법 1: gradlePlugin DSL</h3>
<p>다음 클래스 생성해야 함</p>
<pre><code class="language-kotlin">internal class HiltAndroidPlugin : Plugin&lt;Project&gt; {

    override fun apply(target: Project) {
        with(target) {
            configureHiltAndroid()
        }
    }
}</code></pre>
<pre><code class="language-kotlin">gradlePlugin {
    plugins {
        register(&quot;androidHilt&quot;) {
            id = &quot;test.android.hilt&quot;
            implementationClass = &quot;com.test.app.HiltAndroidPlugin&quot;
        }
    }
}</code></pre>
<p>사용 시</p>
<pre><code class="language-kotlin">plugins {
    id(&quot;test.android.hilt&quot;)
}</code></pre>
<h3 id="방법-2-precompiled--스크립트">방법 2: Precompiled  스크립트</h3>
<ul>
<li>파일명: <code>test.android.compose.gradle.kts</code></li>
</ul>
<pre><code class="language-kotlin">import com.example.build_logic.configureComposeAndroid

configureComposeAndroid()</code></pre>
<p>사용 시</p>
<pre><code class="language-kotlin">plugins {
    // 파일 이름으로 지정함
    id(&quot;test.android.compose&quot;)
}</code></pre>
<h2 id="⚠️-주의-사항">⚠️ 주의 사항</h2>
<ul>
<li><code>build-logic</code>에서 다른 프로젝트를 사용해야 할때 kotlin-dsl 같은 방식은 사용이 안되는 것 같음<pre><code class="language-kotlin">// 됨
implementation(project(&quot;:core:ui&quot;))
</code></pre>
</li>
</ul>
<p>// 안됨
implementation(projects.core.ui)</p>
<p>```</p>
<hr>
<h2 id="📚-참고자료">📚 참고자료</h2>
<ul>
<li><a href="https://github.com/android/nowinandroid/tree/main/build-logic/convention">Nowinandroid</a></li>
<li><a href="https://github.com/droidknights/DroidKnightsApp/blob/main/build-logic">DroidKnightsApp</a></li>
</ul>
<h2 id="✅-정리">✅ 정리</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>기존 방식</th>
<th>build-logic 방식</th>
</tr>
</thead>
<tbody><tr>
<td>유지보수</td>
<td>공통 gradle 파일을 직접 import</td>
<td>모듈 단위로 재사용</td>
</tr>
<tr>
<td>적용 위치</td>
<td><code>build.gradle.kts</code>마다 직접 설정</td>
<td><code>plugins</code> 선언으로 간편 적용</td>
</tr>
</tbody></table>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Compose 커스텀 알림 UI 예제]]></title>
            <link>https://velog.io/@onegold-11/Android-Compose-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%95%8C%EB%A6%BC-UI-%EC%98%88%EC%A0%9C</link>
            <guid>https://velog.io/@onegold-11/Android-Compose-%EC%BB%A4%EC%8A%A4%ED%85%80-%EC%95%8C%EB%A6%BC-UI-%EC%98%88%EC%A0%9C</guid>
            <pubDate>Mon, 12 May 2025 04:57:27 GMT</pubDate>
            <description><![CDATA[<h2 id="🎯-목표">🎯 목표</h2>
<ul>
<li>각 알림이 <strong>화면에 등장할 때 애니메이션</strong>으로 표시</li>
<li>삭제 버튼을 누르면 <strong>퇴장 애니메이션 후 알림 제거</strong></li>
</ul>
<h2 id="💡-개선된-코드-예시">💡 개선된 코드 예시</h2>
<hr>
<pre><code class="language-kotlin">// 알림 그룹
val currentAlarms by rememberUpdatedState(alarms)

LazyColumn(
    modifier = modifier
        .wrapContentWidth()
        .height(300.dp),
    verticalArrangement = Arrangement.spacedBy(10.dp)
) {
    // 특정 개수만 알림 표시
    items(
        items = currentAlarms.takeLast(maxCount),
        key = { it.id }
    ) { alarm -&gt;

        var isVisible by remember { mutableStateOf(false) }
        var isDeleted by remember { mutableStateOf(false) }

        // 알림이 나타날때 애니메이션에 필요
        LaunchedEffect(alarm.id) {
            isVisible = true
        }

        AnimatedVisibility(
            modifier = Modifier.animateItem(),
            visible = isVisible &amp;&amp; !isDeleted,
            enter = slideInVertically { it } + fadeIn(),
            exit = slideOutVertically { -it } + fadeOut(),
            exitFinishedListener = {
                // 애니메이션 종료 후 알림 제거
                onAlarmRemoved?.invoke(alarm.id)
            }
        ) {
            AlarmItem(
                event = alarm,
                onClickDismiss = {
                    // 삭제 버튼을 누르면 알림이 지워진 것처럼 애니메이션션
                    isDeleted = true
                }
            )
        }
    }
}</code></pre>
<h2 id="📌-주요-포인트">📌 주요 포인트</h2>
<hr>
<h3 id="1-알림-삭제">1. <code>알림 삭제</code></h3>
<ul>
<li>알림을 삭제할 때 목록에서 바로 삭제하면 애니메이션 없이 삭제되버린다.</li>
<li>삭제 애니메이션이 끝나 후 삭제하게 만들어야 함함</li>
</ul>
<h3 id="2-id">2. <code>id</code></h3>
<ul>
<li>목록에서 아이템을 id로 구별하고 애니메이션도 id를 기준으로 동작하기 때문에 id가 중복되면 안됨됨</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Kotlin에서 SHA-256 암호화]]></title>
            <link>https://velog.io/@onegold-11/Kotlin%EC%97%90%EC%84%9C-SHA-256-%EC%95%94%ED%98%B8%ED%99%94</link>
            <guid>https://velog.io/@onegold-11/Kotlin%EC%97%90%EC%84%9C-SHA-256-%EC%95%94%ED%98%B8%ED%99%94</guid>
            <pubDate>Mon, 12 May 2025 03:20:26 GMT</pubDate>
            <description><![CDATA[<p>로그인 기능을 구현할 때, 비밀번호나 민감한 데이터를 서버에 평문으로 전송하면 보안상 위험</p>
<p> 이를 방지하기 위해 클라이언트에서 <strong>SHA-256 해시 함수를 이용한 암호화</strong>를 적용할 수 있음</p>
<h2 id="🔧-sha-256-암호화-함수">🔧 SHA-256 암호화 함수</h2>
<pre><code class="language-kotlin">/**
 * SHA256 암호화
 */
fun encryptSha256(keyword: String): String {
    return try {
        val md = MessageDigest.getInstance(&quot;SHA-256&quot;)
        md.update(keyword.toByteArray())
        bytesToHex(md.digest())
    } catch (e: NoSuchAlgorithmException) {
        e.printStackTrace()
        &quot;&quot;
    }
}</code></pre>
<h2 id="🔄-바이트-배열을-16진수-문자열로-변환">🔄 바이트 배열을 16진수 문자열로 변환</h2>
<pre><code class="language-kotlin">/**
 * 바이트 배열을 문자열로
 *
 * @param bytes 바이트 배열
 * @return 문자열
 */
private fun bytesToHex(bytes: ByteArray): String {
    val builder = StringBuilder()
    for (b in bytes) {
        builder.append(String.format(&quot;%02x&quot;, b))
    }
    return builder.toString()
}</code></pre>
<h2 id="🛡️-사용-예">🛡️ 사용 예</h2>
<pre><code class="language-kotlin">val encryptedPassword = encryptSha256(&quot;myPassword123&quot;)
println(&quot;Encrypted: $encryptedPassword&quot;)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[osmdroid에서 타일 오버레이 일부만 보이는 문제 해결]]></title>
            <link>https://velog.io/@onegold-11/osmdroid%EC%97%90%EC%84%9C-%ED%83%80%EC%9D%BC-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4-%EC%9D%BC%EB%B6%80%EB%A7%8C-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</link>
            <guid>https://velog.io/@onegold-11/osmdroid%EC%97%90%EC%84%9C-%ED%83%80%EC%9D%BC-%EC%98%A4%EB%B2%84%EB%A0%88%EC%9D%B4-%EC%9D%BC%EB%B6%80%EB%A7%8C-%EB%B3%B4%EC%9D%B4%EB%8A%94-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0</guid>
            <pubDate>Mon, 12 May 2025 03:04:37 GMT</pubDate>
            <description><![CDATA[<h1 id="🗺️-osmdroid에서-추가-타일-오버레이-일부만-보이는-문제-해결">🗺️ osmdroid에서 추가 타일 오버레이 일부만 보이는 문제 해결</h1>
<p>osmdroid를 사용하여 지도에 커스텀 타일을 추가할 때, 기본 타일은 잘 보이지만 <strong>추가한 타일 오버레이가 일부만 보이는 현상</strong> 발생</p>
<p>이는 <strong>타일은 다운로드되었지만, 실제 그리기 작업이 이루어지지 않았기 때문</strong>으로 보임임</p>
<h2 id="⚠️-문제-상황">⚠️ 문제 상황</h2>
<ul>
<li>기본 타일 소스는 정상적으로 전체 영역 표시됨</li>
<li>추가한 타일 소스는 <strong>일부 타일만 그려짐</strong></li>
<li>로그 상으로는 타일이 로드된 것으로 보임 → <strong>렌더링 트리거 누락 가능성</strong></li>
</ul>
<h2 id="✅-해결-방법">✅ 해결 방법</h2>
<p>추가 타일 오버레이 생성 시 <code>tileRequestCompleteHandler</code>를 등록</p>
<h2 id="🔧-코드-예시">🔧 코드 예시</h2>
<pre><code class="language-kotlin">val tileSource = MapTileSource.WarshipLocalPortals[level] ?: return

val tileProvider = MapTileProviderBasic(context, tileSource).apply {
    tileRequestCompleteHandlers.add(mapView.tileRequestCompleteHandler)
}

val overlay = TilesOverlay(tileProvider, context)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[Compose에서 LazyColumn / LazyRow의 끝 검사]]></title>
            <link>https://velog.io/@onegold-11/Compose%EC%97%90%EC%84%9C-LazyColumn-LazyRow%EC%9D%98-%EB%81%9D-%EA%B2%80%EC%82%AC</link>
            <guid>https://velog.io/@onegold-11/Compose%EC%97%90%EC%84%9C-LazyColumn-LazyRow%EC%9D%98-%EB%81%9D-%EA%B2%80%EC%82%AC</guid>
            <pubDate>Mon, 12 May 2025 01:49:09 GMT</pubDate>
            <description><![CDATA[<p>Jetpack Compose에서 <code>LazyColumn</code>이나 <code>LazyRow</code>를 사용할 때, 스크롤이 <strong>리스트의 시작 또는 끝에 도달했는지</strong>를 검사하는 코드</p>
<h2 id="🔍-코드">🔍 코드</h2>
<hr>
<p><code>LazyListState</code>의 <code>layoutInfo.visibleItemsInfo</code>를 활용하여 현재 화면에 보이는 아이템 정보를 가져올 수 있음</p>
<h3 id="✅-시작에-도달했는지-확인">✅ 시작에 도달했는지 확인</h3>
<pre><code class="language-kotlin">/**
 * 현재 스크롤에 첫번째가 포함되는지 검사
 */
internal fun LazyListState.isScrolledStart(): Boolean {
    return layoutInfo.visibleItemsInfo.firstOrNull()?.index == 0
}</code></pre>
<h3 id="✅-끝에-도달했는지-확인">✅ 끝에 도달했는지 확인</h3>
<pre><code class="language-kotlin">/**
 * 현재 스크롤에 마지막이 포함되는지 검사
 */
internal fun LazyListState.isScrolledEnd(): Boolean {
    return layoutInfo.visibleItemsInfo.lastOrNull()?.index ==
        layoutInfo.totalItemsCount - 1
}</code></pre>
<h2 id="🧪-사용-예시">🧪 사용 예시</h2>
<hr>
<pre><code class="language-kotlin">val state: LazyListState = rememberLazyListState()

Icon(
    imageVector = Icons.Default.ArrowUpward,
    contentDescription = null,
    tint = if (state.isScrolledStart().not()) Gray10 else Color.Transparent,
)

LazyColumn(
    state = state,
) {
    // 리스트 아이템들
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[홀로렌즈2 화면을 안드로이드 앱에서 보기]]></title>
            <link>https://velog.io/@onegold-11/%ED%99%80%EB%A1%9C%EB%A0%8C%EC%A6%882-%ED%99%94%EB%A9%B4%EC%9D%84-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1%EC%97%90%EC%84%9C-%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@onegold-11/%ED%99%80%EB%A1%9C%EB%A0%8C%EC%A6%882-%ED%99%94%EB%A9%B4%EC%9D%84-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1%EC%97%90%EC%84%9C-%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Mon, 12 May 2025 01:39:41 GMT</pubDate>
            <description><![CDATA[<p>홀로렌즈 2는 웹에서 실시간 스트리밍을 지원하는 API를 제공</p>
<p>이 API를 활용하면 앱에서도 화면 미러링을 할 수 있음 </p>
<p>본 글에서는 <strong>ExoPlayer를 이용해 미러링을 구현</strong>하는 방법을 소개</p>
<h2 id="📱-테스트-환경">📱 테스트 환경</h2>
<ul>
<li>Galaxy Tab S6 Lite, Galaxy Z Flip 4  </li>
<li>Android API 33  </li>
<li>ExoPlayer 버전: <code>media3:1.3.1</code>  </li>
</ul>
<h2 id="🔧-exoplayer-설정">🔧 ExoPlayer 설정</h2>
<p>라이브러리 버전 설정, 아래 코드는 libs.versions.toml 파일 기준준</p>
<pre><code class="language-kotlin">media3 = &quot;1.3.1&quot;

media-exoplayer = { group = &quot;androidx.media3&quot;, name = &quot;media3-exoplayer&quot;, version.ref = &quot;media3&quot; }
media-exoplayer-dash = { group = &quot;androidx.media3&quot;, name = &quot;media3-exoplayer-dash&quot;, version.ref = &quot;media3&quot; }
media-ui = { group = &quot;androidx.media3&quot;, name = &quot;media3-ui&quot;, version.ref = &quot;media3&quot; }

dependencies {
    implementation(libs.media.exoplayer)
    implementation(libs.media.exoplayer.dash)
    implementation(libs.media.ui)
}</code></pre>
<h2 id="🚨-문제-1-인증서-오류-https">🚨 문제 1: 인증서 오류 (HTTPS)</h2>
<p>스트리밍 API는 HTTPS로 제공되는데, <strong>인증서를 신뢰하지 못하면 오류</strong>가 발생</p>
<p>이를 우회하려면 인증서를 강제로 신뢰하도록 설정해야 함</p>
<h3 id="✅-application에-인증서-무시-로직-추가">✅ Application에 인증서 무시 로직 추가</h3>
<pre><code class="language-kotlin">class TestApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        disableSSLCertificateChecking()
    }

    private fun disableSSLCertificateChecking() {
        val trustAllCerts: Array&lt;TrustManager&gt; = arrayOf(
            object : X509TrustManager {
                override fun checkClientTrusted(chain: Array&lt;out X509Certificate&gt;?, authType: String?) {}
                override fun checkServerTrusted(chain: Array&lt;out X509Certificate&gt;?, authType: String?) {}
                override fun getAcceptedIssuers(): Array&lt;X509Certificate&gt; = arrayOf()
            }
        )

        try {
            val sc = SSLContext.getInstance(&quot;TLS&quot;)
            sc.init(null, trustAllCerts, SecureRandom())

            // 반드시 설정
            HttpsURLConnection.setDefaultSSLSocketFactory(sc.socketFactory)

            // 선택 사항
            HttpsURLConnection.setDefaultHostnameVerifier { _, _ -&gt; true }

        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}</code></pre>
<p>Manifest에 Application 추가</p>
<pre><code class="language-xml">&lt;application
    android:name=&quot;.TestApplication&quot;
    ... /&gt;</code></pre>
<h2 id="🔐-문제-2-인증-헤더-추가">🔐 문제 2: 인증 헤더 추가</h2>
<p>홀로렌즈 API는 인증이 필요하며, <strong>아이디와 비밀번호를 Base64로 인코딩한 값을 Authorization 헤더에 포함</strong>시켜야 함</p>
<h3 id="예시">예시</h3>
<pre><code class="language-kotlin">&quot;Authorization&quot;: &quot;Basic Zm9ybWFsd29ya3M6Zm9ybWFsd29ya3M=&quot;</code></pre>
<ul>
<li><code>&quot;아이디:비밀번호&quot;</code> 문자열을 Base64로 인코딩</li>
</ul>
<h2 id="🎞-datasourcefactory-구현">🎞 DataSourceFactory 구현</h2>
<pre><code class="language-kotlin">@OptIn(UnstableApi::class)
private fun getSourceFactory(): DataSource.Factory {
    val defaultFactory = DefaultHttpDataSource.Factory()
        .setUserAgent(Util.getUserAgent(this, getString(R.string.app_name)))

    val headers = mapOf(
        &quot;Authorization&quot; to &quot;Basic Zm9ybWFsd29ya3M6Zm9ybWFsd29ya3M=&quot;
    )

    return ResolvingDataSource.Factory(defaultFactory) { spec -&gt;
        spec.withAdditionalHeaders(headers)
    }
}</code></pre>
<h2 id="▶️-exoplayer-연결">▶️ ExoPlayer 연결</h2>
<pre><code class="language-kotlin">@OptIn(UnstableApi::class)
private fun getExoPlayer(): ExoPlayer {
    val mediaItem = MediaItem.Builder().setUri(streamUri).build()
    val mediaSource = ProgressiveMediaSource.Factory(getSourceFactory())
        .createMediaSource(mediaItem)

    return ExoPlayer.Builder(this)
        .setLoadControl(DefaultLoadControl())
        .setTrackSelector(DefaultTrackSelector(this))
        .build().apply {
            setMediaSource(mediaSource)
            playWhenReady = true
            repeatMode = Player.REPEAT_MODE_OFF
        }
}</code></pre>
<h2 id="🔗-스트리밍-uri-예시">🔗 스트리밍 URI 예시</h2>
<pre><code class="language-kotlin">private val streamUri =
    &quot;https://~~~~~~~~~~/api/holographic/stream/live_low.mp4?holo=true&amp;pv=false&amp;mic=false&amp;loopback=false&amp;RenderFromCamera=true&quot;</code></pre>
<hr>
<h2 id="📚-참고-문서">📚 참고 문서</h2>
<ul>
<li><a href="https://learn.microsoft.com/en-us/windows/mixed-reality/develop/advanced-concepts/device-portal-api-reference#mixed-reality-streaming">Device portal API reference - Mixed Reality | Microsoft Learn</a></li>
</ul>
<h2 id="✅-결론">✅ 결론</h2>
<ol>
<li>인증서 신뢰 우회 (HTTPS)</li>
<li>인증 헤더 처리 (Basic Auth)</li>
<li>ExoPlayer에서 <code>DataSource.Factory</code> 커스터마이징</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[KMP 데스크탑에서 화면 캡처]]></title>
            <link>https://velog.io/@onegold-11/KMP-%EB%8D%B0%EC%8A%A4%ED%81%AC%ED%83%91%EC%97%90%EC%84%9C-%ED%99%94%EB%A9%B4-%EC%BA%A1</link>
            <guid>https://velog.io/@onegold-11/KMP-%EB%8D%B0%EC%8A%A4%ED%81%AC%ED%83%91%EC%97%90%EC%84%9C-%ED%99%94%EB%A9%B4-%EC%BA%A1</guid>
            <pubDate>Fri, 09 May 2025 05:43:12 GMT</pubDate>
            <description><![CDATA[<h2 id="kmp-데스크탑에서-화면-캡처-기능-구현">KMP 데스크탑에서 화면 캡처 기능 구현</h2>
<p>Kotlin Multiplatform(KMP)의 데스크탑에서 화면 캡처 기능이 필요한 경우, Java AWT의 <code>Robot</code> 클래스를 활용하여 지정된 화면 영역을 이미지로 저장할 수 있다.</p>
<hr>
<h3 id="📄-전체-코드-예시">📄 전체 코드 예시</h3>
<pre><code class="language-kotlin">import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import androidx.compose.ui.window.rememberWindowState
import java.awt.*
import java.io.File
import javax.imageio.ImageIO

// 지정된 영역을 캡처하여 PNG 이미지로 저장
fun saveScreenImage(rectangle: Rectangle) {
    try {
        val screenImage = Robot().createScreenCapture(rectangle)

        val fileFrame = Frame(&quot;이미지 저장&quot;)
        val fileDialog = FileDialog(fileFrame, &quot;이미지 저장&quot;, FileDialog.SAVE)
        fileDialog.isVisible = true

        if (fileDialog.file == null) return

        val fileName = File(fileDialog.directory, &quot;${fileDialog.file}.png&quot;)
        ImageIO.write(screenImage, &quot;png&quot;, fileName)
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

fun main() = application {
    val windowState = rememberWindowState(size = DpSize(800.dp, 1000.dp))

    Window(
        onCloseRequest = ::exitApplication,
        title = &quot;&quot;,
        state = windowState
    ) {
        App(
            onScreenSave = {
                val rectangle = Rectangle(
                    window.locationOnScreen.x,
                    window.locationOnScreen.y,
                    window.width,
                    window.height
                )
                saveScreenImage(rectangle)
            }
        )
    }
}</code></pre>
<h3 id="⚠️-주의-사항">⚠️ 주의 사항</h3>
<ul>
<li>캡처할 영역을 내가 지정해 줘야하는 문제점이 있음</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android studio Gradle tasks 표시 문제]]></title>
            <link>https://velog.io/@onegold-11/Android-studio-Gradle-tasks-%ED%91%9C%EC%8B%9C-%EB%AC%B8%EC%A0%9C</link>
            <guid>https://velog.io/@onegold-11/Android-studio-Gradle-tasks-%ED%91%9C%EC%8B%9C-%EB%AC%B8%EC%A0%9C</guid>
            <pubDate>Thu, 08 May 2025 08:38:40 GMT</pubDate>
            <description><![CDATA[<h2 id="🧩-문제-상황">🧩 문제 상황</h2>
<p>Android Studio의 <strong>Gradle Tool Window</strong>에서 Gradle tasks가 보이지 않는 문제 발생</p>
<h2 id="🔧-해결-방법">🔧 해결 방법</h2>
<p>Gradle tasks 조회는 <strong>무거운 작업</strong>이라 관련 설정이 비활성화되어 있으면 표시되지않음</p>
<p><strong>실험적 기능(Experimental Settings)</strong> 에서 해당 옵션을 활성화 필요</p>
<ol>
<li>메뉴바에서 <code>File</code> → <code>Settings</code> (Mac: <code>Android Studio</code> → <code>Preferences</code>) 선택  </li>
<li>좌측 검색창에 <code>Experimental</code> 입력  </li>
<li><strong>Experimental → Gradle</strong> 항목에서<br><code>Configure all Gradle tasks during Gradle Sync (this can make Gradle Sync slower)</code> 옵션을 <strong>체크</strong>   </li>
<li>설정을 저장한 후 <strong>Gradle sync 시작</strong></li>
</ol>
<p>🔗 관련 링크: <a href="https://stackoverflow.com/questions/67405791/gradle-tasks-are-not-showing-in-the-gradle-tool-window-in-android-studio-4-2">Gradle tasks are not showing in the gradle tool window in Android Studio 4.2 - Stack Overflow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Android Studio Logcat 창 자동 표시 설정]]></title>
            <link>https://velog.io/@onegold-11/Android-Studio-Logcat-%EC%B0%BD-%EC%9E%90%EB%8F%99-%ED%91%9C%EC%8B%9C-%EC%84%A4%EC%A0%95</link>
            <guid>https://velog.io/@onegold-11/Android-Studio-Logcat-%EC%B0%BD-%EC%9E%90%EB%8F%99-%ED%91%9C%EC%8B%9C-%EC%84%A4%EC%A0%95</guid>
            <pubDate>Wed, 07 May 2025 14:27:37 GMT</pubDate>
            <description><![CDATA[<h2 id="🧩-문제-상황">🧩 문제 상황</h2>
<p>Android Studio에서 앱을 실행할 때마다 Logcat 창을 수동으로 열어야 해서 번거롭습니다.</p>
<h2 id="🔧-해결-방법">🔧 해결 방법</h2>
<p>실행/디버그 설정에서 Logcat 창을 자동으로 표시하도록 설정할 수 있습니다.</p>
<ol>
<li>실행 버튼 옆 톱니바퀴 아이콘 클릭 → <code>Edit Configurations</code> 선택  </li>
<li>좌측에서 실행 구성을 선택한 뒤, 아래쪽 <strong>Miscellaneous</strong> 항목 클릭  </li>
<li><code>Show logcat automatically</code> 옵션 체크  </li>
<li><strong>Apply</strong> → <strong>OK</strong></li>
</ol>
<h2 id="✅-결과">✅ 결과</h2>
<p>이제 앱을 실행하면 Logcat 창이 자동으로 열려 로그를 바로 확인할 수 있습니다.  </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 전역 변수]]></title>
            <link>https://velog.io/@onegold-11/Kotlin-%EC%A0%84%EC%97%AD-%EB%B3%80%EC%88%98</link>
            <guid>https://velog.io/@onegold-11/Kotlin-%EC%A0%84%EC%97%AD-%EB%B3%80%EC%88%98</guid>
            <pubDate>Sun, 21 Jan 2024 09:41:27 GMT</pubDate>
            <description><![CDATA[<h1 id="🍠static">🍠static</h1>
<p>자바에서 전역 변수는 <strong>static</strong>로 선언된다.</p>
<p>반면 코틀린은 static 키워드가 없기 때문에 다른 방법을 사용한다.
코틀린에서 사용하는 방법들에 대해 조사하려 한다.</p>
<h1 id="🍠top-level-property">🍠Top-level property</h1>
<p>클래스 없이 파일에 작성하는 변수</p>
<pre><code class="language-kotlin">// Top-level
val a = &quot;A&quot;</code></pre>
<p>자바 파일로 변환하면 아래와 같다.
파일 이름으로 클래스가 만들어지고 static 변수로 설정된다.</p>
<pre><code class="language-java">public final class StaticTestKt {
   @NotNull
   private static final String a = &quot;A&quot;;

   @NotNull
   public static final String getA() {
      return a;
   }
}</code></pre>
<h1 id="🍠companion-object">🍠Companion object</h1>
<p>클래스에 포함된 형태로 작성하는 변수</p>
<pre><code class="language-kotlin">// Companion object
class B {
    companion object {
        val b = &quot;B&quot;
    }
}</code></pre>
<p>자바 파일로 변환하면 아래와 같다.
Companion 클래스를 추가로 생성했으며 static 변수로 클래스를 선언했다.
변수에 대한 접근을 Companion 클래스를 통해서만 하게 만들었다.</p>
<pre><code class="language-java">public final class B {
   @NotNull
   private static final String b = &quot;B&quot;;
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      @NotNull
      public final String getB() {
         return B.b;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}</code></pre>
<h1 id="🍠object">🍠Object</h1>
<p>object 키워드를 사용해서 만든 싱글턴 클래스의 멤버로 작성하는 변수</p>
<pre><code class="language-kotlin">// object
object C {
    val c = &quot;C&quot;
}</code></pre>
<p>자바 파일로 변환하면 아래와 같다.
일반적인 싱글턴 패턴으로 만들었으며 변수에 대한 접근을 클래스를 통해서만 가능하게 했다.</p>
<pre><code class="language-java">public final class C {
   @NotNull
   private static final String c;
   @NotNull
   public static final C INSTANCE;

   @NotNull
   public final String getC() {
      return c;
   }

   private C() {
   }

   static {
      C var0 = new C();
      INSTANCE = var0;
      c = &quot;C&quot;;
   }
}</code></pre>
<h1 id="🍠차이점">🍠차이점</h1>
<p>일단 변수를 사용할 때는 다음 차이점이 있다.
a는 변수가 어디에 있는지 알 수 없지만, b, c는 어디에 있는지 알 수 있다.</p>
<pre><code class="language-kotlin">a
B.b
C.c</code></pre>
<p>자바 코드로 비교하면 다음과 같다. Top-level이 성능에 영향이 가장 적을 것으로 예상된다.</p>
<p><strong>Top-level</strong>: 가장 간단한 형태
<strong>companion object</strong>: 코드도 길고 클래스도 2개를 사용하는 형태
<strong>object</strong>: Top-level 보다는 복잡하지만 companion object 보다는 간단한 형태</p>
<h1 id="🍠정리">🍠정리</h1>
<p><strong>Top-level</strong>
클래스와 크게 연관이 없고 독립적으로 사용되는 값. 주로 사용하는 걸 추천</p>
<p><strong>companion object</strong>
클래스와 깊게 연관된 경우 사용하지만, 단순히 변수를 선언하는 용도로는 추천하지 않음.
다만 메소드의 경우 companion object를 많이 활용하는 것으로 보임</p>
<p><strong>object</strong>
companion object 대신 사용하기 적합
연관된 클래스와 다른 이름을 사용해야 하는 문제가 있음</p>
<h1 id="🍠참고-자료">🍠참고 자료</h1>
<p><a href="https://discuss.kotlinlang.org/t/best-practices-for-top-level-declarations/2198">Kotlin Discussions</a>
<a href="https://stackoverflow.com/questions/49969319/kotlin-difference-between-constant-in-companion-object-and-top-level">Stack overflow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Composa] 터치 - scroll]]></title>
            <link>https://velog.io/@onegold-11/Composa-%ED%84%B0%EC%B9%98-scroll</link>
            <guid>https://velog.io/@onegold-11/Composa-%ED%84%B0%EC%B9%98-scroll</guid>
            <pubDate>Sun, 21 Jan 2024 04:31:22 GMT</pubDate>
            <description><![CDATA[<h1 id="🧊scroll">🧊Scroll</h1>
<p>컴포즈에서 <strong>스크롤 동작</strong>을 처리하는 방법에 대한 소개</p>
<p>컴포즈에서 스크롤은 다음 Modifier 속성을 사용해서 구현할 수 있다.
<strong>horizontalScroll</strong>, <strong>verticalScroll</strong>, <strong>scrollable</strong>, <strong>nestedScroll</strong></p>
<h1 id="🧊horizontalscroll">🧊horizontalScroll</h1>
<p><strong>가로 스크롤</strong>을 활성화하는 속성
자식 컴포넌트가 부모 컴포넌트보다 크면, 스크롤을 할 수 있게 해준다.</p>
<pre><code class="language-kotlin">@Composable
private fun HorizontalScroll() {
    Row(
        modifier = Modifier
            .size(100.dp)
            .background(color = Color.LightGray)
            .horizontalScroll(state = rememberScrollState())
    ) {
        repeat(20) {
            Text(text = &quot;Item $it &quot;)
        }
    }
}</code></pre>
<h1 id="🧊verticalscroll">🧊verticalScroll</h1>
<p><strong>세로 스크롤</strong>을 활성화하는 속성
방향이 세로인 점만 빼면 <strong>horizontalScroll</strong>과 동일하다.
실제로 내부 코드를 보면 둘 다 <strong>scrollable</strong>을 사용하고 있다.</p>
<pre><code class="language-kotlin">@Composable
private fun VerticalScroll() {
    Column(
        modifier = Modifier
            .size(100.dp)
            .background(color = Color.LightGray)
            .verticalScroll(state = rememberScrollState())
    ) {
        repeat(20) {
            Text(text = &quot;Item $it&quot;)
        }
    }
}</code></pre>
<h1 id="🧊scrollable">🧊scrollable</h1>
<p><strong>horizontalScroll</strong>과 <strong>verticalScroll</strong>은 내부적으로 <strong>scrollable</strong>을 사용하고 있다.</p>
<p>하지만 <strong>scrollable</strong>은 스크롤 애니메이션을 만들지 않는다.
내부 코드를 보면 <strong>스크롤 애니메이션</strong>과 <strong>스크롤 계산</strong>을 따로 하는 것을 볼 수 있다.
그래서 scrollable을 사용해도 <strong>스크롤이 되지 않는 것</strong>을 볼 수 있다.</p>
<p>대신 scrollable은 사용자의 <strong>스크롤 동작에 대한 자세한 정보</strong>를 얻을 수 있다.
horizontalScroll과 verticalScroll은 현재 <strong>스크롤 위치</strong>와 <strong>진행 상태</strong>만 확인할 수 있다.
반면 scrollable은 스크롤 동작에 대한 <strong>delta</strong> 값을 받을 수 있다.</p>
<p>스크롤 애니메이션은 필요없지만, 사용자의 <strong>스크롤 동작을 알아야할 때</strong> 사용할 수 있다.</p>
<pre><code class="language-kotlin">@Composable
private fun ScrollableScroll() {
    var offset by remember { mutableFloatStateOf(0f) }
    val scrollState = rememberScrollableState { delta -&gt;
        offset += delta
        delta
    }

    Column(
        modifier = Modifier
            .size(100.dp)
            .background(color = Color.LightGray)
            .scrollable(
                orientation = Orientation.Vertical,
                state = scrollState
            ),
        verticalArrangement = Arrangement.Center,
    ) {
        Text(text = &quot;$offset&quot;)
    }
}</code></pre>
<h1 id="🧊nestedscroll">🧊nestedScroll</h1>
<p>nestedScroll은 조금 특이한 속성으로 <strong>다른 스크롤의 정보를 처리</strong>할 수 있는 속성이다.</p>
<p>nestedScroll은 스크롤을 2가지 방법으로 처리할 수 있다.
<strong>NestedScrollConnection</strong>을 사용해서 <strong>자식 컴포넌트의 스크롤</strong>을 가로채는 방법과
<strong>NestedScrollDispatcher</strong>를 사용해서 부모 컴포넌트에서 <strong>스크롤 이벤트를 전달</strong>하는 방법이 있다.</p>
<p>NestedSCrollConnection을 사용하면 자식 컴포넌트가 스크롤하는 걸 알 수 있다.
이걸 활용하면 <strong>스크롤에 따라 다른 UI가 변하게</strong> 만들 수 있다.
아래 코드는 스크롤에 따라 상단에 있는 UI가 변하는 코드로 이런 식으로 활용할 수 있다.</p>
<pre><code class="language-kotlin">@Composable
private fun NestedScroll() {
    var offset by remember { mutableFloatStateOf(0f) }
    val scrollState = remember {
        object : NestedScrollConnection {
            override fun onPreScroll(
                available: Offset,
                source: NestedScrollSource
            ): Offset {
                val newOffset = (offset + available.y).coerceIn(-20f, 0f)
                offset = newOffset

                return super.onPreScroll(available, source)
            }
        }
    }

    Column(
        modifier = Modifier
            .size(100.dp)
            .nestedScroll(connection = scrollState)
    ) {
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(20.dp)
                .offset(y = offset.dp)
                .background(color = Color.Green),
            content = {},
        )
        VerticalScroll()
    }
}</code></pre>
<h2 id="☕nestedscrollconnection">☕NestedScrollConnection</h2>
<p><strong>NestedScrollConnection은</strong> 인터페이스지만, 메소드가 구현되어 있어서 <strong>필요한 메소드만</strong> 구현하면 된다.</p>
<p><strong>onPreScroll</strong>
자식 컴포넌트가 <strong>스크롤 이벤트를 처리하기 전</strong>에 발생
return으로 처리에 사용한 스크롤 값을 보낼 수 있다.</p>
<p>만약 파라미터로 받은 스크롤 가능한 값을 전부 return하면
스크롤을 전부 처리했다고 보기 때문에 자식 컴포넌트가 스크롤되지 않는다.</p>
<p><strong>onPostScroll</strong>
자식 컴포넌트가 <strong>스크롤 이벤트를 처리하고</strong> 발생
자식 컴포넌트가 사용한 스크롤 값과 남은 스크롤 값을 받을 수 있다.</p>
<p><strong>onPreFling</strong>
자식 컴포넌트에서 <strong>fling 이벤트를 처리하기 전</strong>에 발생
fling 이벤트는 스크롤과 달리 짧은 순간에 발생하기 때문에 1번만 호출된다.
해당 메소드는 suspend로 처리된다.</p>
<p><strong>onPostFling</strong>
자식 컴포넌트에서 <strong>fling 이벤트를 처리하고</strong> 발생</p>
<h2 id="☕nestedscrolldispatcher">☕NestedScrollDispatcher</h2>
<p>NestScrollDispatcher은 구현이 복잡해서 예제가 있는 <a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/input/nestedscroll/package-summary.html#(androidx.compose.ui.Modifier).nestedScroll(androidx.compose.ui.input.nestedscroll.NestedScrollConnection,androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher)">공식 문서</a>를 참고하는 걸 추천</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Comppose] 터치 - Press]]></title>
            <link>https://velog.io/@onegold-11/Comppose-%ED%84%B0%EC%B9%98-Press</link>
            <guid>https://velog.io/@onegold-11/Comppose-%ED%84%B0%EC%B9%98-Press</guid>
            <pubDate>Tue, 16 Jan 2024 14:45:44 GMT</pubDate>
            <description><![CDATA[<h1 id="☕press">☕Press</h1>
<p>사용자 동작 중에서 <strong>누르는</strong> 동작에 대한 내용을 소개
<strong>High-level</strong> 단계에서 처리하는 방법이 중심으로 작성됨</p>
<p><strong>Tap</strong>(Click): 누르고 떼기
<strong>Double tap</strong>: Tap 이후 일정 시간 안에 Tap
<strong>Press</strong>: 누르고 있기
<strong>Long press</strong>: 일정 시간 Press</p>
<h1 id="☕clickable">☕clickable</h1>
<p>가장 기본적인 클릭 처리 방법</p>
<pre><code class="language-kotlin">// 컴포넌트 크기 만큼 터치 가능
Modifier
    .size(100.dp)
    .background(Color.Cyan)
    .clickable {}

// 컴포넌트 크기에서 10.dp 만큼 줄어든 영역만 터치 가능
Modifier
    ...
    .padding(10.dp)
    .clickable{}</code></pre>
<blockquote>
<p>padding 속성 이후에 선언하면 패딩이 적용된 영역만 터치가 적용된다.</p>
</blockquote>
<h1 id="☕combinedclickable">☕combinedClickable</h1>
<p>combinedClickable은 clickable에서 <strong>더블 클릭</strong>과 <strong>길게 누르기</strong>가 추가됐다.</p>
<pre><code class="language-kotlin">Modifier
    .combinedClickable(
        onLongClick = {},
        onDoubleClick = {},
        onClick = {}
    )</code></pre>
<p>기본적으로 더블 클릭과 길게 누르기의 시간은 각각 <strong>0.3, 0.4초</strong>로 설정되어 있다.
만약 시간을 바꾸려면 <strong>ViewConfiguration</strong> 인터페이스를 구현해야 한다.
구현한 클래스를 <strong>LocalViewConfiguration에</strong> 설정하면 시간을 변경할 수 있다.</p>
<pre><code class="language-kotlin">// 커스텀 ViewConfriguration
// touchSlop는 8f가 기본으로 되어 있다.
private class CustomViewConfiguration : ViewConfiguration {
    override val doubleTapMinTimeMillis: Long
        get() = 1000
    override val doubleTapTimeoutMillis: Long
        get() = 2000
    override val longPressTimeoutMillis: Long
        get() = 3000
    override val touchSlop: Float
        get() = 8f
}

// 커스텀 클래스를 설정해준다.
CompositionLocalProvider(
    LocalViewConfiguration provides CustomViewConfiguration()
) {
    // 테스트 확인용 텍스트
    var text by remember { mutableStateOf(&quot;&quot;) }

    Text(
        modifier = Modifier
            .combinedClickable(
                onLongClick = { text = &quot;Long Clicked&quot; },
                onDoubleClick = { text = &quot;Double Clicked&quot; },
                onClick = { text = &quot;Clicked&quot; }
            ),
        text = text,
    )
}
</code></pre>
<h1 id="☕selectable">☕selectable</h1>
<p><strong>선택</strong>하는 컴포넌트를 구현할 때 사용하는 Modifier 속성
clickable로 비슷한 동작을 만들 수 있고, 실제 내부 코드를 보면 <strong>clickable</strong>을 사용하고 있다.
selectable을 사용하는 이유는 <strong>UI 접근성</strong>과 <strong>테스트</strong> 때문으로 자세한 내용은 <a href="https://developer.android.com/jetpack/compose/semantics">공식 문서</a> 참조</p>
<p>clickable와 달리 <strong>선택된 상태</strong>를 전달해야 한다.</p>
<pre><code class="language-kotlin">Modifier
    .selectable(
       selected = true or false,
       onClick = { },
    )</code></pre>
<h1 id="☕toggleable">☕toggleable</h1>
<p>toggleable은 다른 속성과 달리 <strong>콜백 함수로 토글 상태</strong>를 전달받는다.
toggleable도 clickable로 구현이 되지만, <strong>not 연산</strong>이 <strong>자동</strong>이라 좀 더 편하다.
toggleable은 내부적으로 밑에서 나올 <strong>triStateToggleable</strong>을 사용한다.</p>
<pre><code class="language-kotlin">// onValueChange은 항상 value에 not 연산 결과가 나온다.
// 이 경우에는 false가 나온다.
Modifier
    .toggleable(
       value = true,
       onValueChange = { state -&gt; },
    )</code></pre>
<h1 id="☕tristatetoggleable">☕triStateToggleable</h1>
<p>내부적으로 <strong>clickable</strong>을 사용한다.
toggleable와 다른 점은 toggleable은 콜백으로 <strong>Boolean 타입</strong>을 주지만
triStateToggleable은 <strong>ToggleableState</strong>라는 enum 클래스를 반환한다.</p>
<p>ToggleableState는 <strong>On</strong>, <strong>Off</strong>, <strong>Indeterminate</strong>로 이루어져 있다.
토글 기능을 만들 때 On, Off 외에 <strong>다른 상태</strong>를 사용하고 싶을 때 사용할 수 있다.</p>
<pre><code class="language-kotlin">Modifier
    .triStateToggleable(
        state = ToggleableState.Indeterminate,
        onClick = {  },
    )</code></pre>
<h1 id="☕ripple-effect">☕Ripple effect</h1>
<p>Ripple은 버튼을 눌렀을 때 <strong>살짝 어두워지는 효과</strong>를 말한다.
위에 나온 속성을 적용하면 기본 효과가 적용되는데, 디자인에 따라 변경해야할 수도 있다.
크게 2가지 방법으로 <strong>직접 만들거</strong>나 <strong>Indication</strong>을 적용하는 방법이 있다.</p>
<p>직접 만드는 방법은 말 그대로 클릭 상태에 따라 <strong>background 색상</strong>을 바꾸는 방법이다.
디자인에 따라 쉬울 수도 있지만, 컴포넌트마다 따로 만들어야 하는 문제가 있다.</p>
<p>반면 <strong>Indication</strong>을 사용하는 방법은 여러 컴포넌트에 사용할 수 있고 전체에 적용할 수도 있다.</p>
<blockquote>
<p><strong>InteractionSource</strong>: 사용자 상호작용 정보를 가지는 인터페이스, 속성마다 존재
<strong>Indication</strong>: InteractionSource에서 전달받은 정보로 시각적인 효과를 만듬</p>
</blockquote>
<p>먼저 <strong>Indication</strong>을 만든다. 색상은 빨간색으로 만들었다.</p>
<pre><code class="language-kotlin">private class CustomIndication: Indication {
    private class CustomIndicationInstance(
        private val isPressed: State&lt;Boolean&gt;,
        private val isHovered: State&lt;Boolean&gt;,
        private val isFocused: State&lt;Boolean&gt;,
    ) : IndicationInstance {
        override fun ContentDrawScope.drawIndication() {
            drawContent()
            if (isPressed.value) {
                drawRect(color = Color.Red.copy(alpha = 0.8f), size = size)
            } else if (isHovered.value || isFocused.value) {
                drawRect(color = Color.Red.copy(alpha = 0.8f), size = size)
            }
        }
    }

    @Composable
    override fun rememberUpdatedInstance(
        interactionSource: InteractionSource
    ): IndicationInstance {

        val isPressed = interactionSource.collectIsPressedAsState()
        val isHovered = interactionSource.collectIsHoveredAsState()
        val isFocused = interactionSource.collectIsFocusedAsState()

        return remember(interactionSource) {
            CustomIndicationInstance(isPressed, isHovered, isFocused)
        }
    }
}</code></pre>
<p>컴포넌트 <strong>하나에 적용</strong>하려면 컴포넌트 파라미터 중에 <strong>indication</strong>에 추가하면 된다.</p>
<pre><code class="language-kotlin">Modifier
    .clickable(
        interactionSource = remember { MutableInteractionSource() },
        indication = CustomIndication(),
        onClick = {}
    )</code></pre>
<p>컴포넌트 <strong>전체에 적용</strong>하려면 <strong>CompositionLocalProvider</strong>를 사용한다.</p>
<pre><code class="language-kotlin">CompositionLocalProvider(
    LocalIndication provides CustomIndication()
) {
    ...
    Modifier
        .clickable {}
    ...
}</code></pre>
<p>더 자세한 내용은 <a href="https://developer.android.com/jetpack/compose/touch-input/handling-interactions">공식 문서</a> 참조</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Compose] 사용자 터치]]></title>
            <link>https://velog.io/@onegold-11/Compose-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%84%B0%EC%B9%98</link>
            <guid>https://velog.io/@onegold-11/Compose-%EC%82%AC%EC%9A%A9%EC%9E%90-%ED%84%B0%EC%B9%98</guid>
            <pubDate>Sun, 14 Jan 2024 05:03:06 GMT</pubDate>
            <description><![CDATA[<h1 id="🍩터치">🍩터치</h1>
<p>컴포즈에서 클릭, 드래그 같은 <strong>사용자 터치 동작</strong>을 처리하는 방법에 대한 소개
<a href="https://developer.android.com/jetpack/compose/touch-input/pointer-input/understand-gestures">공식 문서</a>를 기준으로 작성됨</p>
<h1 id="🍩개념-정의">🍩개념 정의</h1>
<p><strong>Pointer</strong></p>
<ul>
<li>사용자가 앱과 상호작용할 때 사용하는 <strong>물리적인</strong> 대상</li>
<li>손가락, 터치 펜, 마우스 등</li>
<li>상호작용하고 있는 Pointer의 타입은 <a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/input/pointer/PointerType">PointerType</a> value class에서 확인할 수 있음</li>
<li><strong>화면 좌표를 지정</strong>할 수 있어야해서 키보드는 Pointer가 아님</li>
</ul>
<p><strong>Pointer event</strong></p>
<ul>
<li>Low-level 단계에서 터치를 처리할 때 사용하는 개념</li>
<li><a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/input/pointer/PointerEvent">PointerEvent</a> 데이터 클래스에서 확인할 수 있음</li>
</ul>
<p><strong>Gesture</strong></p>
<ul>
<li>Pointer event가 모여서 만들어진 <strong>동작</strong></li>
<li>탭, 드래그, 스와이프 등</li>
</ul>
<h1 id="🍩처리-단계">🍩처리 단계</h1>
<p>컴포즈에서는 3가지 레벨로 터치를 처리할 수 있음
<strong>높은 레벨</strong>이 개발자에서 편한 방법</p>
<p><strong>Top-level</strong></p>
<ul>
<li>Button 같은 컴포넌트가 제공하는 리스너</li>
</ul>
<p><strong>High-level</strong></p>
<ul>
<li>Modifier가 제공하는 clickable, draggable 등</li>
</ul>
<p><strong>Low-level</strong></p>
<ul>
<li>Modifier가 제공하는 pointerInput</li>
</ul>
<h1 id="🍩top-level">🍩Top-level</h1>
<p><strong>컴포넌트</strong>가 제공하는 API
개발자가 따로 처리하지 않아도 동작에 대한 피드백을 받을 수 있음</p>
<pre><code class="language-kotlin">@Composable
fun Button(
    onClick: () -&gt; Unit,
    ...
)</code></pre>
<p><strong>단점</strong></p>
<ul>
<li>세세한 동작을 처리하기 어려움</li>
<li>컴포넌트가 제공하지 않으면 사용 불가</li>
</ul>
<h1 id="🍩high-level">🍩High-level</h1>
<p>컴포넌트의 <strong>Modifier</strong>를 사용한 터치 동작 추가 방법
Modifier가 제공하는 clickable, scrollable, draggable 등이 있음</p>
<p>다음 확장 함수를 제공하는데 내용이 길어 <strong>자세한 내용은 다른 글</strong>에서 정리할 예정</p>
<p><strong>누르기</strong></p>
<ul>
<li>clickable, combinedClickable </li>
<li>selectable</li>
<li>toggable, triStateToggleable</li>
</ul>
<p><strong>스크롤</strong></p>
<ul>
<li>horizontalScroll, verticalScroll, scrollable</li>
</ul>
<p><strong>드래그</strong></p>
<ul>
<li>draggable</li>
<li>swipeable</li>
</ul>
<p><strong>다중 터치</strong></p>
<ul>
<li>transformable</li>
</ul>
<h1 id="🍩low-level">🍩Low-level</h1>
<p>터치를 세밀하게 처리해야 하거나 원하는 동작을 만들어야 할 때 사용
Modifier의 <strong>pointerInput</strong> 안에서 <strong>awaitPointerEventScope</strong>를 사용</p>
<p>이 부분도 내용이 길어져 <strong>다른 글</strong>에서 자세히 다룰 예정</p>
<pre><code class="language-kotlin">Modifier
.pointerInput(Unit) {
    awaitPointerEventScope {
        val touchDown = awaitPointerEvent()

        do {
            val touchMove = awaitointerEvent()
        } while (touchMove.changes.any { it.pressed })

        val touchUp = awaitPointerEvent()
    }
}</code></pre>
<p><strong>단점</strong></p>
<ul>
<li>사용자의 모든 동작을 처리할 수 있지만 그만큼 코드와 로직이 복잡해짐</li>
<li>동작을 직접 구현해야 하기 때문에 난이도가 높은 편</li>
</ul>
<p>그래서 컴포즈는 여러 동작을 몇 가지 메소드를 제공하고 있음</p>
<p><strong>누르기</strong></p>
<ul>
<li>detectTapGestures</li>
</ul>
<p><strong>드래그</strong></p>
<ul>
<li>detectHorizontalDragGestures, dectVerticalDragGestures</li>
<li>detectDragGestures, detectDragGesturesAfterLongPress</li>
</ul>
<p><strong>다중 터치</strong></p>
<ul>
<li>detectTransformGestures</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 숫자를 계단 형태로 변환]]></title>
            <link>https://velog.io/@onegold-11/Android-%EC%88%AB%EC%9E%90%EB%A5%BC-%EA%B3%84%EB%8B%A8-%ED%98%95%ED%83%9C%EB%A1%9C-%EB%B3%80%ED%99%98</link>
            <guid>https://velog.io/@onegold-11/Android-%EC%88%AB%EC%9E%90%EB%A5%BC-%EA%B3%84%EB%8B%A8-%ED%98%95%ED%83%9C%EB%A1%9C-%EB%B3%80%ED%99%98</guid>
            <pubDate>Thu, 21 Dec 2023 13:21:04 GMT</pubDate>
            <description><![CDATA[<h1 id="🍤소개">🍤소개</h1>
<p>사실 적당한 제목이 생각나지 않아 이상한 제목을 붙였다.</p>
<p>숫자를 <strong>계단 함수</strong>처럼 일정하게 만들려고 한다.
예를 들어 <strong>23</strong>은 <strong>20</strong>으로 <strong>31</strong>은 <strong>40</strong>으로 출력하려 한다.
<img src="https://velog.velcdn.com/images/onegold-11/post/c24560b0-d398-49ee-91de-c524d669cba8/image.png" alt=""></p>
<h1 id="🍤코드">🍤코드</h1>
<p>코드는 간단한 편이다.
입력을 단계만큼 나누고, 나머지로 값을 올릴지 내릴지 검사한다.</p>
<pre><code class="language-kotlin">val step = 20
val input = 28
val remainder = input % step

val step = if (remainder &gt; step / 2) {
    input + (step - remainder)
} else {
    input - remainderX
}</code></pre>
<p><strong>round()</strong>를 사용해서 구할 수도 있다.</p>
<pre><code class="language-kotlin">// 소수점 2번째에서 반올림
round(x*10)/10</code></pre>
<p>다만 소수점으로 계산하기 때문에 곱하고 나누는 과정이 들어간다.
원래 값이 클 수록 숫자가 타입이 <strong>허용하는 범위를 넘어</strong>갈 수 있다.</p>
<p>그리고 <strong>10의 제곱 단위</strong>로만 숫자를 변환할 수 있다.
위 코드처럼 간격이 20인 숫자로는 만들 수 없다.</p>
<h1 id="🍤활용">🍤활용</h1>
<p>코드 자체는 간단하지만 쓸 곳이 마땅히 없었는데, 오늘 쓸 일이 있어서 사용하게 됐다.</p>
<p>아래 코드는 <strong>Compose에서</strong> <strong>composable</strong> 위에서 드래그 했을 때 감지하는 코드이다.</p>
<pre><code class="language-kotlin">Modifier.pointerInput(Unit) {
    detectDragGestures(
        onDragStart = {},
        onDragEnd = {},
        onDragCancel = {},
        onDrag = { change, offset -&gt; }
    )
}</code></pre>
<p>위에 코드로 드래그한 좌표를 구할 수 있다.</p>
<p>좌표를 변환하면 픽셀, 도트 같은 좌표를 구할 수 있다.
변환한 좌표를 <strong>Canvas</strong>에 도트처럼 그릴 수 있다.</p>
<pre><code class="language-kotlin">// 도트 값으로 변환하는 함수
private fun Float.toStep(): Float {
    val step = 20
    val intValue = this.toInt()
    val remainder = intValue % step

    return if (remainder &gt; step / 2) {
        (intValue + (step - remainder)).toFloat()
    } else {
        (intValue - remainder).toFloat()
    }
}</code></pre>
<pre><code class="language-kotlin">@Composable
fun DotCanvas() {
    // 드래그 좌표들
    var points by remember { mutableStateOf(emptyList&lt;Offset&gt;()) }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectDragGestures(
                    onDragStart = {},
                    onDragEnd = {},
                    onDragCancel = {},
                    onDrag = { change, offset -&gt;
                        // 드래그한 좌표를 도트 좌표로 변환하고 저장
                        val dotX = change.position.x.toStep()
                        val dotY = change.position.y.toStep()
                        points = points + Offset(dotX, dotY)
                    }
                )
            }
    ) {
        points.forEach { point -&gt;
            // 도트 좌표 그리기
            // 원래는 사각형으로 해야하는데, 원이 코드가 적어서 원으로 했다.
            drawCircle(color = Color.Black, radius = 10f, center = point)
        }
    }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] class 초기화 순서]]></title>
            <link>https://velog.io/@onegold-11/Kotlin-class-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C</link>
            <guid>https://velog.io/@onegold-11/Kotlin-class-%EC%B4%88%EA%B8%B0%ED%99%94-%EC%88%9C%EC%84%9C</guid>
            <pubDate>Wed, 20 Dec 2023 13:56:18 GMT</pubDate>
            <description><![CDATA[<h1 id="🍿초기화-순서">🍿초기화 순서</h1>
<p>코틀린에서 <a href="https://kotlinlang.org/docs/classes.html">클래스</a>를 초기화 할 때 실행되는 순서를 정리하려 한다.</p>
<h1 id="🍿init">🍿init</h1>
<p>init은 클래스를 생성하면 먼저 실행되는 것들 중 하나다.
변수 설정도 같은 종류인데 가장 위에 있는 코드부터 실행된다.</p>
<pre><code class="language-kotlin">fun main() {
    // First value : test
    // First init test
    // Second value : test
    // Second init test
    Person(&quot;test&quot;)
}

class Person(name: String) {
    val firstValue = &quot;First value : $name&quot;.also(::println)

    init {
        println(&quot;First init $name&quot;)
    }
    val secondValue = &quot;Second value : $name&quot;.also(::println)

    init {
        println(&quot;Second init $name&quot;)
    }
}</code></pre>
<h1 id="🍿부-생성자">🍿부 생성자</h1>
<p>부 생성자로 클래스를 생성하면 변수 초기화와 init이 먼저 호출되고 그 다음에 호출된다.</p>
<pre><code class="language-kotlin">fun main() {
    // Value : test
    // Init test
    // Constructor 10
    Person(10)
}

class Person(name: String) {
    val firstValue = &quot;Value : $name&quot;.also(::println)

    constructor(age: Int): this(&quot;test&quot;) {
        println(&quot;Constructor $age&quot;)
    }

    init {
        println(&quot;Init $name&quot;)
    }
}</code></pre>
<h1 id="🍿상속">🍿상속</h1>
<p>부모 클래스가 존재하면 부모 클래스의 코드가 먼저 실행된다.</p>
<pre><code class="language-kotlin">fun main() {
    // Init parent test
    // Size parent 4
    // Init child test
    // Size child 4
    Person(&quot;test&quot;)
}

open class Creature(val name: String) {
    init {
        println(&quot;Init parent $name&quot;)
    }

    open val size: Int = name.length.also { println(&quot;Size parent $it&quot;) }
}

class Person(
    name: String
) : Creature(name) {

    init { println(&quot;Init child $name&quot;) }

    override val size: Int = super.name.length.also { println(&quot;Size child $it&quot;) }
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] return, jump]]></title>
            <link>https://velog.io/@onegold-11/Kotlin-return-jump</link>
            <guid>https://velog.io/@onegold-11/Kotlin-return-jump</guid>
            <pubDate>Sun, 17 Dec 2023 13:36:12 GMT</pubDate>
            <description><![CDATA[<h1 id="🌮흐름">🌮흐름</h1>
<p>코틀린에서는 코드 흐름과 관련된 표현식이 있다.
return: 값을 반환하고 함수를 종료
break: 가장 가까운 반복문을 종료
continue: 가장 가까운 반복문의 다음 단계 진행</p>
<h1 id="🌮label">🌮label</h1>
<p>그리고 코틀린에는 c의 goto 같은 것이 있다.</p>
<pre><code class="language-kotlin">loop@ for (i in 0..2) {
    if(...) break@loop
}</code></pre>
<p>다행히 c의 goto와 달리 반복문에만 사용할 수 있다.</p>
<h1 id="🌮jump">🌮jump</h1>
<p>일반적으로 goto는 코드 실행 흐름이 꼬이기 때문에 사용하면 안된다.
유일하게 사용을 고려해 볼 부분은 중첩 반복문을 탈출할 때다.</p>
<p>중첩된 반복문을 탈출하려면 조건문을 2개를 만드는 게 일반적이다.
하지만 goto나 label을 사용하면 코드가 간결해진다.</p>
<pre><code class="language-kotlin">loop1@ for(i in 0..10) {
     loop2@ for(j in 0..10) {
         if (j == 5) break@loop1
         println(&quot;i: $i, j: $j&quot;)
     }
 }</code></pre>
<h1 id="🌮return">🌮return</h1>
<p>label은 return에도 사용할 수 있다.</p>
<pre><code class="language-kotlin">(0..10).forEach {
    if (it == 5) return@forEach
    println(it)
}</code></pre>
<p>반복문처럼 보이는데 break가 아니라 return을 사용하는 이유는 forEach라 그렇다.
forEach가 익명 함수를 전달받아 반복문을 실행하기 때문에 return으로 함수를 종료해야 한다.
forEach는 자동으로 label이 있기 때문에 main 함수가 같이 종료되지 않는다.</p>
<p>값을 반환하는 경우 다음 코드를 사용한다.</p>
<pre><code class="language-kotlin">return@a 1</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] 테스트 코드 Json 관리]]></title>
            <link>https://velog.io/@onegold-11/Android-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-Json-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@onegold-11/Android-%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%BD%94%EB%93%9C-Json-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Sun, 17 Dec 2023 13:26:00 GMT</pubDate>
            <description><![CDATA[<h1 id="🥖json">🥖Json</h1>
<p>Retrofit이나 okHttp처럼 서버 응답을 테스트할 때 가짜 응답이 필요할 때가 있다.
json 문자열으로 가짜 응답을 만드는데, 너무 길어지면 테스트 코드에서 관리가 힘들어진다.</p>
<p>그래서 별도의 json 파일을 분리하는게 깔끔한데, 문제는 androidTest가 아니라 일반 test에서는 assets, resources처럼 json 파일이 있는 곳에 접근하기가 쉽지 않다.</p>
<h1 id="🧀해결">🧀해결</h1>
<p>nowInAndroid에서 사용하는 방법인데 test에서도 resources에 접근할 수 있다.</p>
<pre><code class="language-kotlin">private fun readJsonFile(fileName: String): String {
    val inputStream = javaClass.classLoader?.getResourceAsStream(fileName)
    return inputStream?.bufferedReader()?.use { it.readText() } ?: &quot;&quot;
}</code></pre>
<p>주의해야할 점은 res가 아니라 test에 resources를 만들어야 한다.
패키지 구조를 안드로이드로 하면 안보이고 project로 설정해야 생성하기 쉽다.
<img src="https://velog.velcdn.com/images/onegold-11/post/88336a4c-7416-4fde-8ea9-634d568a8f37/image.PNG" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Android] Hilt 생성자 default value]]></title>
            <link>https://velog.io/@onegold-11/Android-Hilt-%EC%83%9D%EC%84%B1%EC%9E%90-default-value</link>
            <guid>https://velog.io/@onegold-11/Android-Hilt-%EC%83%9D%EC%84%B1%EC%9E%90-default-value</guid>
            <pubDate>Sun, 17 Dec 2023 13:15:34 GMT</pubDate>
            <description><![CDATA[<h1 id="🍕default-value">🍕Default value</h1>
<p>Kotlin에서는 생성자에 기본 값을 설정할 수 있다.</p>
<pre><code class="language-kotlin">class Person(
    val name: String = &quot;test&quot;
) { ... }</code></pre>
<h1 id="🍕hilt-inject">🍕Hilt inject</h1>
<p>Hilt를 사용해서 의존성 주입을 하면 보통 다음 코드처럼 한다.</p>
<pre><code class="language-kotlin">class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }</code></pre>
<p>그런데 생성자에 다음과 같이 default value를 설정하려고 하면 오류가 발생한다.</p>
<pre><code class="language-kotlin">class AnalyticsAdapter @Inject constructor(
  private val name: String = &quot;test&quot;,
  private val service: AnalyticsService
) { ... }</code></pre>
<p>Hilt에서 String 타입으로 주입할 객체를 찾지 못해서 그런 거 같다.</p>
<h1 id="🍦해결">🍦해결</h1>
<p>내 경우 테스트 코드 때문에 저렇게 처리를 해야 했는데, 아래 방법으로 해결이 가능하다.</p>
<pre><code class="language-kotlin">class MyNetwork(
    url: String,
    networkJson: Json,
    okHttpClientFactory: Call.Factory
) : MyNetworkDataSource {

    @Inject
    constructor(
        networkJson: Json,
        okHttpClientFactory: Call.Factory
    ) : this(
        url = BASE_URL,
        networkJson = networkJson,
        okHttpClientFactory = okHttpClientFactory
    )
}</code></pre>
<p>주 생성자에는 필요한 변수를 전부 선언하고 @Inject 생성자에서는 주입받을 객체만 선언하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Compose] 자동 크기 조절 Text]]></title>
            <link>https://velog.io/@onegold-11/Compose-%EC%9E%90%EB%8F%99-%ED%81%AC%EA%B8%B0-%EC%A1%B0%EC%A0%88-Text</link>
            <guid>https://velog.io/@onegold-11/Compose-%EC%9E%90%EB%8F%99-%ED%81%AC%EA%B8%B0-%EC%A1%B0%EC%A0%88-Text</guid>
            <pubDate>Thu, 07 Dec 2023 13:17:04 GMT</pubDate>
            <description><![CDATA[<h1 id="🥐소개">🥐소개</h1>
<p>주어진 영역에 딱 맞게 텍스트 크기를 조절하고 싶을 때 사용하는 기능
<a href="https://www.youtube.com/watch?v=ntlyrFw0F9U">원본 영상</a>을 참고했습니다.</p>
<h1 id="🥐전체-코드">🥐전체 코드</h1>
<pre><code class="language-kotlin">@Composable
fun ResizeText(
    modifier: Modifier = Modifier,
    style: TextStyle = MaterialTheme.typography.labelSmall,
    text: String
) {
    var resizedTextStyle by remember {
        mutableStateOf(style)
    }
    var shouldDraw by remember {
        mutableStateOf(false)
    }
    val defaultFontSize = MaterialTheme.typography.labelSmall.fontSize

    Text(
        modifier = modifier
            .drawWithContent {
                if (shouldDraw) {
                    drawContent()
                }
            },
        text = text,
        style = resizedTextStyle,
        softWrap = false,
        onTextLayout = { result -&gt;
            if (result.didOverflowWidth) {
                if (style.fontSize.isUnspecified) {
                    resizedTextStyle = resizedTextStyle.copy(
                        fontSize = defaultFontSize
                    )
                }

                resizedTextStyle = resizedTextStyle.copy(
                    fontSize = resizedTextStyle.fontSize * 0.95
                )
            } else {
                shouldDraw = true
            }
        }
    )
}</code></pre>
<h1 id="🥐코드-설명">🥐코드 설명</h1>
<p>텍스트에 적용할 스타일을 저장. 기본은 입력으로 받은 스타일</p>
<pre><code class="language-kotlin">var resizedTextStyle by remember {
    mutableStateOf(style)
}</code></pre>
<p>크기가 완전히 정해졌는지 검사하는 변수
사실 없어도 되는데 이게 없으면 숫자가 줄어드는 걸 사용자가 보게된다.
숫자가 줄어드는 게 이상하면 크기가 고정됐을 때만 보여주게 이걸 추가해야 한다.</p>
<pre><code class="language-kotlin">var shouldDraw by remember {
    mutableStateOf(false)
}
...
modifier.drawWithContent {
      if (shouldDraw) {
        drawContent()
    }
}</code></pre>
<p>텍스트가 이상한 값이 된 경우, 기본으로 사용할 크기
그냥 Sp 타입을 사용해도 된다.</p>
<pre><code class="language-kotlin">val defaultFontSize = MaterialTheme.typography.labelSmall.fontSize
...
if (style.fontSize.isUnspecified) {
    resizedTextStyle = resizedTextStyle.copy(
        fontSize = defaultFontSize
    )
}</code></pre>
<p>Text는 기본적으로 텍스트가 주어진 영역보다 크면 자동으로 줄바꿈을 해버린다.
해당 옵션이 켜져있으면 기능이 적용되지 않아서 꺼야 한다.</p>
<pre><code class="language-kotlin">softWrap = false</code></pre>
<p>여기서는 텍스트가 너비를 벗어났는지만 검사했는데 높이도 검사할 수 있다.</p>
<pre><code class="language-kotlin">if (result.didOverflowWidth)</code></pre>
<p>텍스트 크기를 영역에 맞을 때 까지 줄이는 코드
여기서는 0.95%로 줄였지만 다른 수치를 사용해도 된다.</p>
<pre><code class="language-kotlin">resizedTextStyle = resizedTextStyle.copy(
    fontSize = resizedTextStyle.fontSize * 0.95
)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin] 숫자 타입]]></title>
            <link>https://velog.io/@onegold-11/Kotlin-%EC%88%AB%EC%9E%90-%ED%83%80%EC%9E%85</link>
            <guid>https://velog.io/@onegold-11/Kotlin-%EC%88%AB%EC%9E%90-%ED%83%80%EC%9E%85</guid>
            <pubDate>Thu, 07 Dec 2023 13:00:54 GMT</pubDate>
            <description><![CDATA[<h1 id="🍈소개">🍈소개</h1>
<p>코틀린에서 사용하는 <a href="https://kotlinlang.org/docs/numbers.html#floating-point-numbers-comparison">숫자</a>에 대한 내용 정리</p>
<h1 id="🍈정수형">🍈정수형</h1>
<p><img src="https://velog.velcdn.com/images/onegold-11/post/2d319853-ee2f-410f-84f6-c7449c70f0d4/image.PNG" alt=""></p>
<p>코틀린은 자바와 달리 타입을 명시적으로 선언하지 않으면 자동으로 정해준다.</p>
<pre><code class="language-kotlin">val num1 = 1 // Int
val num2 = 3000000000 // Long

println(num1::class.simpleName)
println(num2::class.simpleName)</code></pre>
<p><code>Long</code> 타입은 뒤에 <code>L</code>을 추가하면 <code>Long</code> 타입이 된다.</p>
<pre><code class="language-kotlin">val num1 = 1L // Long</code></pre>
<p>참고로 선언한 타입의 범위를 벗어나는 수를 선언하면 에러가 발생한다.</p>
<pre><code class="language-kotlin">val num1: Int = 3000000000 // error!</code></pre>
<h1 id="🍈실수형">🍈실수형</h1>
<p><img src="https://velog.velcdn.com/images/onegold-11/post/896b00b0-4f97-4e3a-b8e2-ed52755646f1/image.PNG" alt="">
코틀린에서는 실수형은 <code>Double</code>이 기본이고 <code>Float</code>는 뒤에 <code>f, F</code>를 붙여야 한다.</p>
<pre><code class="language-kotlin">val num1 = 1.0 // Double
val num2 = 1.0f // Float</code></pre>
<p>코틀린은 <code>Double</code>와 <code>Float</code>이 서로 변환이 되지 않는다. 이유는 아래에 설명이 나온다.</p>
<pre><code class="language-kotlin">val num1 = 1.0 // Double
val num2:Float = num1 // error!</code></pre>
<h1 id="🍈표현">🍈표현</h1>
<p>코틀린에서는 <code>0x</code>와 <code>0b</code>를 사용해 16진법과 2진법을 표현할 수 있다.
이때 타입을 따로 정하지 않으면 <code>Int</code>와 <code>Long</code> 중 숫자 크기에 맞는 타입이 선언된다.
8진법은 지원되지 않는다.</p>
<pre><code>val num2 = 0xFF
val num3 = 0b11</code></pre><p>숫자가 너무 긴 경우에는 <code>_</code>을 사용해 가독성을 높일 수 있다.</p>
<pre><code class="language-kotlin">val num1 = 3_000_000_000
val num2 = 0xFF_EC_DE_5E
val num3 = 0b11010010_01101001_10010100_10010010</code></pre>
<h1 id="🍈원시-타입--래퍼-타입">🍈원시 타입 &amp; 래퍼 타입</h1>
<p>코틀린에서는 <code>===</code> 연산자를 선언해 같은 주소를 검사할 수 있는데 숫자를 비교하면 재밌는 결과가 나온다.</p>
<pre><code class="language-kotlin">val num1: Int = 100
val num2: Int = 10000

val numA1: Int = num1
val numA2: Int = num1
val numB1: Int = num2
val numB2: Int = num2

println(numA1 === numA2) // true
println(numB1 === numB2) // true</code></pre>
<p>여기서 <code>Int</code>를 <code>Int?</code> 타입으로 바꾸면 이상한 결과가 나온다.</p>
<pre><code>val num1: Int = 100
val num2: Int = 10000

val numA1: Int? = num1
val numA2: Int? = num1
val numB1: Int? = num2
val numB2: Int? = num2

println(numA1 === numA2) // true
println(numB1 === numB2) // false</code></pre><p>코틀린은 JVM에 의해 컴파일되는데, JVM이 -128~127인 숫자를 참조하는 <code>Int?</code> 객체는 같은 객체로 처리하지만, 범위를 벗어나는 숫자를 참조하는 <code>Int?</code>는 다른 객체로 처리하지 때문이다.<br><img src="https://velog.velcdn.com/images/onegold-11/post/08f189f5-fc3a-44df-ab43-61c45c42af2f/image.PNG" alt=""></p>
<p>보통 숫자는 원시 타입이라 객체가 없다고 생각할 수 있다. 
코틀린에서는 원시 타입과 래퍼 타입을 따로 구분하고 있지 않다.
대신 JVM이 자바 코드로 변환할 때 숫자의 사용 방법에 따라 구분하고 있다.</p>
<p>다음 예시 코드를 보면 코틀린으로 작성하면 타입에 큰 신경을 안써도 된다.</p>
<pre><code class="language-kotlin">val num1:Int = 10
val num2: List&lt;Int&gt; = listOf(10)

val numA = num1
val numB = num2.first()
val numC: Int? = num2.firstOrNull()</code></pre>
<p>이걸 Androdi Studio의 <code>Kotlin Bytecode</code> 기능을 사용해 자바 코드로 변환하면 다음 코드가 나온다.</p>
<pre><code class="language-java">int num1 = true;
List num2 = CollectionsKt.listOf(10);
int numB = ((Number)CollectionsKt.first(num2)).intValue();
Integer numC = (Integer)CollectionsKt.firstOrNull(num2);</code></pre>
<p>하나씩 보면 숫자를 선언만 하면 <code>int</code>로 처리된다.</p>
<pre><code class="language-java">int num1 = true;
int numB = ((Number)CollectionsKt.first(num2)).intValue();</code></pre>
<p>반면 제네릭이나 nullable로 사용하면 래퍼 타입으로 변환된다.</p>
<pre><code class="language-java">// Number는 래터 타입으로 Integer의 추상 클래스
int numB = ((Number)CollectionsKt.first(num2)).intValue();

// Int?는 Integer로 선언된다.
Integer numC = (Integer)CollectionsKt.firstOrNull(num2);</code></pre>
<p>JVM이 이런 동작을 하는 이유는 메모리 최적화 때문이라고 한다.</p>
<p>코틀린에서 사용하는 <code>Int</code>나 <code>Double</code> 같은 숫자 타입은 코드를 보면 클래스로 되어 있다.
숫자는 Number 추상 클래스를 상속하는데 정작 <code>Int</code>나 <code>Long</code>은 같은 정수형이여도 서로 호환되지 않는다.
다른 타입에 저장하려면 <code>toLong()</code> 같은 메소드를 사용해야 한다.</p>
<pre><code class="language-kotlin">var num1: Int = 100
var num2: Long = 100

num2 = num1 // error
num2 = num1.toLong()</code></pre>
<h1 id="🍈실수형-비교">🍈실수형 비교</h1>
<p>실수형은 부동소수점을 사용하기 때문에 컴퓨터에서 정확한 값을 알 수 없다.
그래서 <a href="https://ko.wikipedia.org/wiki/IEEE_754">IEEE754</a>를 기준으로 표현하는데, 이래도 오차가 발생한다.</p>
<p>저 오차 때문에 재밌는 결과가 나온다.</p>
<p>숫자로 연산할 때 불가능한 연산이면 <code>NaN</code>이 출력된다.
이 <code>NaN</code>은 <code>Double</code> 클래스에 전역 변수로 선언되어 있고 <code>-(0.0 / 0.0 )</code>인 <code>Double</code> 타입이다.
결국 부동소수점을 가진 실수형이기 때문에 같은 <code>NaN</code>의 값을 비교하면 <code>false</code>가 나온다.</p>
<pre><code class="language-kotlin">println(Double.NaN == Double.NaN) // false</code></pre>
<p><code>0.0</code>에도 재밌는 현상이 있는데 <code>-0.0</code>과 <code>0.0</code>을 비교하면 다음 결과가 나온다.</p>
<pre><code class="language-kotlin">// 동일한 값
println(0.0 == -0.0) // true

// 동일한 값이지만, list에 넣으면 다르게 인식
println(listOf(0.0) == listOf(-0.0)) // false
println(listOf(1.0) == listOf(1.0)) // true</code></pre>
<p>마지막으로 정렬을 하면 다음 결과가 나온다.
<code>NaN</code>은 <code>-(0.0 / 0.0 )</code>이고 <code>POSITIVE_INFINITY</code>는 <code>1.0/0.0</code>으로 선언되어 있다.</p>
<pre><code>// [-0.0, 0.0, Infinity, NaN]
println(listOf(Double.NaN, Double.POSITIVE_INFINITY, 0.0, -0.0).sorted())</code></pre>]]></description>
        </item>
    </channel>
</rss>