<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Simple&amp;amp;Natural</title>
    <link>https://sandn.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Fri, 10 Jul 2026 14:39:52 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Essense</managingEditor>
    <item>
      <title>구글 인앱결제 테스트 팁</title>
      <link>https://sandn.tistory.com/231</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;최근 사내 인앱결제 기능 개발 중 유용한 도구를 발견했다&lt;br&gt;&lt;br&gt;구글에서 제공하는 PlayBillngLab 을 이용하면&lt;br&gt;국가변경, 신규 프로모션, 구독금액 변경 등의 테스트 케이스를 실험해 볼 수 있다.&lt;br&gt;&lt;br&gt;해외에 출시하는 앱이라 실제로 해외에서 결제 시 어떤 식으로 노출되는지 테스트가 필요했는데 실제 결제수단을 등록하지 않고도 테스트 가능한 방법을 찾고 있었다.&lt;br&gt;&lt;br&gt;아무리 검색을 해도 나오지 않아서 방황하던 중 공식문서에서 우연히 발견.&lt;br&gt;&lt;br&gt;공식문서 내에서 Manifest에 metadata를 설정해주는 부분이 있는데 여기서 largest release audience 태그 설정할 때 주의하자. 이건 실제 출시되는 aab나 apk의 업로드 가능 스코프를 지정하는 거고 enableBillingOverridesTesting에 종속되는 옵션이 아니다. &lt;/p&gt;</description>
      <category>안드로이드(Android)/이슈 및 해결</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/231</guid>
      <comments>https://sandn.tistory.com/231#entry231comment</comments>
      <pubDate>Fri, 6 Dec 2024 18:09:14 +0900</pubDate>
    </item>
    <item>
      <title>Room 마이그레이션 이슈</title>
      <link>https://sandn.tistory.com/228</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;9/1 내용 업데이트&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 Scheme json 파일이 버전별로 모두 동일한 것이 이슈인 것으로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 Migration 시 스키마 변경사항을 인지하지 못해 쿼리를 생성하지 않는 이슈인 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 분명 테이블을 추가했는데 스키마 파일이 동일한 것도 의문이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계속 분석해봐야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 Room은 version=2 인 상황에서 새 테이블을 추가하여 version=3으로 마이그레이션 해야 하는 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1 to 2 의 경우에는 수동으로 마이그레이션을 해주었지만 이번에 간단한 테이블 추가라 AutoMigration을 이용한 상황.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행을 해보면 아래와 같은 로그만 반복된다.&lt;/p&gt;
&lt;pre id=&quot;code_1692103034875&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;expected:
TableInfo{name='recentSearchQueries', columns={query=Column{name='query', type='TEXT', affinity='2', notNull=true, primaryKeyPosition=1, defaultValue='null'}, queriedDate=Column{name='queriedDate', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=0, defaultValue='null'}}, foreignKeys=[], indices=[]}
Found:
TableInfo{name='recentSearchQueries', columns={}, foreignKeys=[], indices=[]}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성된 AutoMigration impl 클래스를 확인해보니 아래처럼 migrate 로직이 전혀 생성이 되지 않았다.&lt;/p&gt;
&lt;pre id=&quot;code_1692103104217&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Generated(&quot;androidx.room.RoomProcessor&quot;)
@SuppressWarnings({&quot;unchecked&quot;, &quot;deprecation&quot;})
class AppDatabase_AutoMigration_2_3_Impl extends Migration {
  public AppDatabase_AutoMigration_2_3_Impl() {
    super(2, 3);
  }

  @Override
  public void migrate(@NonNull SupportSQLiteDatabase database) {
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 여러 번 테스트를 해본 결과 정상적으로 migrate 함수가 쿼리와 함께 생성이 된 경우에도 역시 같은 현상이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Google 이슈트래커와 비슷한 케이스를 몇개 더 검색을 해보니 나와 같은 케이스가 있었다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://issuetracker.google.com/issues/236001085&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://issuetracker.google.com/issues/236001085&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://stackoverflow.com/questions/71002284/room-how-to-automigrate-db-for-new-table&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://stackoverflow.com/questions/71002284/room-how-to-automigrate-db-for-new-table&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은 구글측도 재현이 안돼서 해결을 못하는 중이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이 문제의 경우 수동으로 migration 로직을 넣어주면 이상없이 동작하긴 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AutoMigration 자체에 뭔가 내부적인 버그가 있는 것 같은데 당분간은 수동으로 써야 할 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignCenter&quot; data-emoticon-type=&quot;friends1&quot; data-emoticon-name=&quot;012&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/012.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/friends1/large/012.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드(Android)/이슈 및 해결</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/228</guid>
      <comments>https://sandn.tistory.com/228#entry228comment</comments>
      <pubDate>Tue, 15 Aug 2023 21:42:00 +0900</pubDate>
    </item>
    <item>
      <title>[Android] ViewPager2 와 RecyclerView 사용 시 부드럽게 스크롤하기</title>
      <link>https://sandn.tistory.com/225</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;가로 방향의 ViewPager2 내 세로 방향의 RecyclerView를 가지고 있는 fragment pager 를 사용하던 중 개선사항이 하나 들어왔다.&lt;br&gt;&amp;nbsp;&lt;br&gt;세로 방향으로 스크롤했는데 가끔씩 가로로 스크롤이 된다는것.&lt;br&gt;&amp;nbsp;&lt;br&gt;이 원인은 recyclerView와 viewPager2가 터치 이벤트를&amp;nbsp;서로 경쟁적으로 뺏어가기 때문이다.&lt;br&gt;세로로 스크롤을 했음에도 viewPager2가 터치 이벤트를 가로채버리는 것.&lt;br&gt;이를 해결하기 위해서는 recyclerView에서 먼저 스크롤 이벤트를 감지한 후&lt;br&gt;세로 방향의 스크롤이 더 크면 recyclerView에서 터치 이벤트를 처리하도록 하면 된다&lt;br&gt;&amp;nbsp;&lt;br&gt;계산해보면 수직 45도 각도를 기준으로 이보다 각도가 작게 스크롤하면 viewPager2가,&lt;br&gt;이보다 각도가 커지면 recyclerView가 터치이벤트를 처리한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;이는 안드로이드에서 제공하는 requestDisallowInterceptTouchEvent 속성을 활용하면 된다.&lt;br&gt;&lt;a href=&quot;https://developer.android.com/reference/android/view/ViewGroup#requestDisallowInterceptTouchEvent(boolean)&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://developer.android.com/reference/android/view/ViewGroup#requestDisallowInterceptTouchEvent(boolean)&lt;/span&gt;&lt;/a&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;ViewGroup &amp;nbsp;|&amp;nbsp; Android Developers&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;&quot; data-og-host=&quot;developer.android.com&quot; data-og-source-url=&quot;https://developer.android.com/reference/android/view/ViewGroup#requestDisallowInterceptTouchEvent(boolean)&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/2hO8t/hyRSTACEUn/L3Piv7kLBxxMJxwCjSWm61/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676&quot; data-og-url=&quot;https://developer.android.com/reference/android/view/ViewGroup&quot;&gt;
 &lt;a href=&quot;https://developer.android.com/reference/android/view/ViewGroup&quot; target=&quot;_blank&quot; data-source-url=&quot;https://developer.android.com/reference/android/view/ViewGroup#requestDisallowInterceptTouchEvent(boolean)&quot;&gt;
  &lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/2hO8t/hyRSTACEUn/L3Piv7kLBxxMJxwCjSWm61/img.png?width=1201&amp;amp;height=676&amp;amp;face=0_0_1201_676')&quot;&gt; 
  &lt;/div&gt;
  &lt;div class=&quot;og-text&quot;&gt;
   &lt;p class=&quot;og-title&quot;&gt;ViewGroup &amp;nbsp;|&amp;nbsp; Android Developers&lt;/p&gt;
   &lt;p class=&quot;og-desc&quot;&gt;&lt;/p&gt;
   &lt;p class=&quot;og-host&quot;&gt;developer.android.com&lt;/p&gt;
  &lt;/div&gt;&lt;/a&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;br&gt;아래는 소스코드.&lt;br&gt;이를 recyclerView에 감싸서 사용하면 된다.&lt;br&gt;끗.&lt;br&gt;&amp;nbsp;&lt;br&gt;SmoothVerticalScrollingHost.kt&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.FrameLayout
import com.cobak.android.AppLog
import kotlin.math.abs

class SmoothVerticalScrollingHost: FrameLayout {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constructor(context: Context): super(context)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constructor(context: Context, attrs: AttributeSet?): super(context, attrs)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int): super(context, attrs, defStyleAttr)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int): super(context, attrs, defStyleAttr, defStyleRes)

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private var dx = 0f
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private var dy = 0f
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private var oldX = 0f
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private var oldY = 0f

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ev?.let {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;when (it.action) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MotionEvent.ACTION_DOWN -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dx = 0f
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dy = 0f
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oldX = it.x
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oldY = it.y
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parent.requestDisallowInterceptTouchEvent(true)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;MotionEvent.ACTION_MOVE -&amp;gt; {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dx += abs(ev.x - oldX)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dy += abs(ev.y - oldY)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oldX = ev.x
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;oldY = ev.y
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;parent.requestDisallowInterceptTouchEvent(dy &amp;gt; dx)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;AppLog.d(&quot;ACTION_MOVE -&amp;gt; dx: $dx, dy: $dy, oldX: $oldX, oldY: $oldY ${dy &amp;gt; dx}&quot;)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;return super.onInterceptTouchEvent(ev)
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드(Android)/이것저것 만들어보기</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/225</guid>
      <comments>https://sandn.tistory.com/225#entry225comment</comments>
      <pubDate>Thu, 9 Mar 2023 17:52:26 +0900</pubDate>
    </item>
    <item>
      <title>ConstraintLayout에서 Ellipsized TextView + View 만들기</title>
      <link>https://sandn.tistory.com/224</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 오후 2.36.25.png&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c4lIVE/btr1zmt5UEJ/CSlJ3GvukhKuaiK0H9XoMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c4lIVE/btr1zmt5UEJ/CSlJ3GvukhKuaiK0H9XoMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c4lIVE/btr1zmt5UEJ/CSlJ3GvukhKuaiK0H9XoMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc4lIVE%2Fbtr1zmt5UEJ%2FCSlJ3GvukhKuaiK0H9XoMk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;623&quot; height=&quot;125&quot; data-filename=&quot;스크린샷 2023-03-02 오후 2.36.25.png&quot; data-origin-width=&quot;623&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-03-02 오후 2.36.44.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o8AP3/btr1pYt1zez/YKNQme3zuI0r9ncIIxgIqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o8AP3/btr1pYt1zez/YKNQme3zuI0r9ncIIxgIqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o8AP3/btr1pYt1zez/YKNQme3zuI0r9ncIIxgIqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo8AP3%2Fbtr1pYt1zez%2FYKNQme3zuI0r9ncIIxgIqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;626&quot; height=&quot;133&quot; data-filename=&quot;스크린샷 2023-03-02 오후 2.36.44.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우측에 하나의 View를 고정값으로 두고 TextView를 가변길이 + Ellipsize 처리하려고 할 때 맨 아래와 같이 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 아래의 세 줄이다.&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;android:layout_width=&quot;wrap_content&quot;
...
app:layout_constrainedWidth=&quot;true&quot;
app:layout_constraintHorizontal_bias=&quot;0.0&quot;
app:layout_constraintHorizontal_chainStyle=&quot;packed&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;constrainedWidth를 true로 설정하여 레이아웃 범위를 초과하지 않도록 제약을 걸어주고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bias를 0.0으로 설정하여 좌측으로 정렬한 뒤&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;chainStyle을 pack으로 설정하여 바로 다음 레이아웃 옆에 차곡차곡 쌓이는 구조로 만들어 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;html xml&quot; data-ke-language=&quot;html&quot;&gt;&lt;code&gt;app:layout_constrainedWidth_default=&quot;wrap&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위의 코드로 구현하는 예제도 보이는데 이는 deprecated 되었다고 하니 layout_&lt;/span&gt;width=&quot;wrap_content&quot; &amp;amp; constrainedWidth=&quot;true&quot;를 조합해서 사용하는 것을 권장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;끗&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1677735270477&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;...

&amp;lt;LinearLayout
        android:id=&quot;@+id/layout_name&quot;
        android:layout_width=&quot;wrap_content&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:orientation=&quot;vertical&quot;
        android:gravity=&quot;center_vertical&quot;
        android:layout_marginStart=&quot;8dp&quot;
        app:layout_constraintStart_toEndOf=&quot;@+id/cv_thumbnail&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        android:paddingTop=&quot;10dp&quot;
        android:paddingBottom=&quot;10dp&quot;&amp;gt;
        &amp;lt;LinearLayout
            android:layout_width=&quot;105dp&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:orientation=&quot;horizontal&quot;&amp;gt;
            &amp;lt;TextView
                android:id=&quot;@+id/tv_symbol&quot;
                android:layout_width=&quot;wrap_content&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:fontFamily=&quot;@font/pretendard_semibold&quot;
                android:textSize=&quot;14dp&quot;
                android:textColor=&quot;@color/gray_900&quot;
                android:maxLines=&quot;1&quot;/&amp;gt;
            &amp;lt;TextView
                android:id=&quot;@+id/tv_currency&quot;
                android:layout_width=&quot;wrap_content&quot;
                android:layout_height=&quot;wrap_content&quot;
                android:layout_marginStart=&quot;2dp&quot;
                android:visibility=&quot;gone&quot;
                android:textColor=&quot;@color/gray_500&quot;
                android:textSize=&quot;10dp&quot;
                android:fontFamily=&quot;@font/pretendard_regular&quot;
                android:maxLines=&quot;1&quot;/&amp;gt;
        &amp;lt;/LinearLayout&amp;gt;
        &amp;lt;androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width=&quot;match_parent&quot;
            android:layout_height=&quot;wrap_content&quot;
            android:gravity=&quot;center_vertical&quot;&amp;gt;
            &amp;lt;TextView
                android:id=&quot;@+id/tv_name_kr&quot;
                android:layout_width=&quot;wrap_content&quot;
                android:layout_height=&quot;wrap_content&quot;
                app:layout_constraintStart_toStartOf=&quot;parent&quot;
                app:layout_constraintEnd_toStartOf=&quot;@id/iv_ad&quot;
                app:layout_constrainedWidth=&quot;true&quot;
                app:layout_constraintHorizontal_bias=&quot;0.0&quot;
                app:layout_constraintHorizontal_chainStyle=&quot;packed&quot;
                android:textSize=&quot;10dp&quot;
                android:maxLines=&quot;1&quot;
                android:ellipsize=&quot;end&quot;
                android:textColor=&quot;@color/gray_500&quot;
                android:fontFamily=&quot;@font/pretendard_regular&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot;
                app:layout_constraintBottom_toBottomOf=&quot;parent&quot;/&amp;gt;
            &amp;lt;ImageView
                android:id=&quot;@+id/iv_ad&quot;
                android:layout_width=&quot;17dp&quot;
                android:layout_height=&quot;12dp&quot;
                android:src=&quot;@drawable/ic_market_item_ad&quot;
                android:visibility=&quot;visible&quot;
                android:layout_marginStart=&quot;2dp&quot;
                app:layout_constraintStart_toEndOf=&quot;@+id/tv_name_kr&quot;
                app:layout_constraintEnd_toEndOf=&quot;parent&quot;
                app:layout_constraintTop_toTopOf=&quot;parent&quot;
                app:layout_constraintBottom_toBottomOf=&quot;parent&quot;/&amp;gt;
        &amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;
    &amp;lt;/LinearLayout&amp;gt;

...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드(Android)/이것저것 만들어보기</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/224</guid>
      <comments>https://sandn.tistory.com/224#entry224comment</comments>
      <pubDate>Thu, 2 Mar 2023 14:41:50 +0900</pubDate>
    </item>
    <item>
      <title>[Git] 현재 이전에 존재하는 merge commit message 수정하기</title>
      <link>https://sandn.tistory.com/223</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;현재 커밋 이전에 merge 된 커밋이 존재하고 이 merge 커밋의 메시지를 수정하고 싶다면&lt;/p&gt;
&lt;pre id=&quot;code_1677135748695&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;git rebase -i -p head~2&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 명령 후 merge 커밋에 reword를 달고 메시지 수정 후 재커밋을 하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;--prevent-merges 를 deprecated 되었다고 하는데 어쩐 일인지 --rebase-merges 는 동작하지 않고 prevent만 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추후 살펴봐야 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Git&amp;amp;Github</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/223</guid>
      <comments>https://sandn.tistory.com/223#entry223comment</comments>
      <pubDate>Thu, 23 Feb 2023 16:04:05 +0900</pubDate>
    </item>
    <item>
      <title>[Android] 커스텀 뷰(CustomView) 이용한 비밀번호 입력 화면 만들어보기</title>
      <link>https://sandn.tistory.com/221</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;edited_10.jpeg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/K7nR6/btr2ZTcTNTg/KG98WBVKjI1k2q48r1URKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/K7nR6/btr2ZTcTNTg/KG98WBVKjI1k2q48r1URKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/K7nR6/btr2ZTcTNTg/KG98WBVKjI1k2q48r1URKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FK7nR6%2Fbtr2ZTcTNTg%2FKG98WBVKjI1k2q48r1URKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;313&quot; height=&quot;1280&quot; data-filename=&quot;edited_10.jpeg&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;1280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보안이 필요한 여러 앱에서 볼 수 있는 화면의 모습입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드로 입력을 하면 문자를 노출하는 대신 동그란 아이콘으로 가려 표시해줍니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저런 화면을 어떤 식으로 구현하는지 제가 구현한 코드를 공유해볼게요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 가장 중요한 것은 키보드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안드로이드에서 제공하는 KeyboardView나 인터페이스를 구현하여 만들 수도 있지만,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 시스템 키보드로 발생할 수 있는 여러가지 이슈들을 피하고자 했고 간단한 화면에서 주로 사용할 목적이었기 때문에 커스텀 뷰를 만들어 키보드를 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 키보드 구현부의 소스코드입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CustomKeyboard.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1675394273106&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import kotlin.math.roundToInt

@SuppressLint(&quot;SetTextI18n&quot;)
class CustomKeyboard: LinearLayout {

    companion object {
        private const val SHUFFLE = &quot;재배열&quot;
        private const val BACKSPACE = &quot;&amp;larr;&quot;
    }

    constructor(context: Context): super(context)
    constructor(context: Context, attrs: AttributeSet?): super(context, attrs)

    private val keySet = mutableListOf(
        Key(&quot;1&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;2&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;3&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;4&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;5&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;6&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;7&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;8&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(&quot;9&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(SHUFFLE, 16f, R.color.white, KeyType.UTIL),
        Key(&quot;0&quot;, 24f, R.color.white, KeyType.COMMON),
        Key(BACKSPACE, 24f, R.color.white, KeyType.UTIL),
    )

    private var listener: OnKeyboardListener? = null

    init {
        orientation = VERTICAL
        initKeySet()
    }

    fun setOnKeyboardListener(listener: OnKeyboardListener) {
        this.listener = listener
    }

    private fun initKeySet() {
        val columnCount = 3
        val rowCount = if (keySet.size%columnCount==0) {
            keySet.size/columnCount
        } else {
            (keySet.size/columnCount) + 1
        }

	// 언뜻 복잡해 보이지만, row by column 만큼 버튼을 만들어주는 코드에 불과합니다!
        repeat(rowCount) { rowIndex -&amp;gt;
            val rowView = LinearLayout(context).apply row@{
                orientation = HORIZONTAL
                layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)

                repeat(columnCount) column@{ columnIndex -&amp;gt;
                    val key = keySet[(columnIndex+1 + columnCount*rowIndex)-1]
                    val cell = TextView(context).apply {
                        layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1f)
                        setPadding(
                            key.getTextSizePx(context),
                            key.getTextSizePx(context),
                            key.getTextSizePx(context),
                            key.getTextSizePx(context)
                        )

			// 키보드 클릭 시 Ripple effect
                        foreground = with(TypedValue()) {
                            context.theme.resolveAttribute(android.R.attr.selectableItemBackground, this, true)
                            ContextCompat.getDrawable(context, resourceId)
                        }
                        setBackgroundColor(ContextCompat.getColor(context, R.color.color_2c6ceb))

                        text = key.text
                        setTextColor(Color.WHITE)
                        when(key.text) {
                            SHUFFLE -&amp;gt; {
                                setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
                                this.setOnClickListener {
                                    shuffle()
                                }
                            }
                            BACKSPACE -&amp;gt; {
                                setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
                                this.setOnClickListener {
                                    listener?.onDelete()
                                }
                            }
                            else -&amp;gt; {
                                setTextSize(TypedValue.COMPLEX_UNIT_SP, 24f)
                                this.setOnClickListener {
                                    listener?.onEnterText(key.text)
                                }
                            }
                        }

                        gravity = Gravity.CENTER
                        // 설정하지 않으면 Ripple effect가 생기지 않습니다
                        // 하지만 onClickListener 를 설정하는 경우엔 아래 코드는 필요없습니다!
                        isClickable = true 
                    }
                    this.addView(cell)
                }

            }

            addView(rowView)
        }
    }

    private fun shuffle() {
        val newKeySet = keySet.filter { it.keyType==KeyType.COMMON }
            .shuffled()
        var index = 0
        for (i in 0..keySet.lastIndex) {
            if (keySet[i].keyType == KeyType.COMMON) {
                keySet[i] = newKeySet[index]
                index++
            }
        }
        removeAllViews()
        initKeySet()
    }

    data class Key(
        val text: String,
        val textSizeSp: Float,
        val textColorId: Int,
        val keyType: KeyType
    ) {
        fun getTextSizePx(context: Context): Int = textSizeSp.toPixel(context)
    }

    interface OnKeyboardListener {
        fun onEnterText(text: String)

        fun onDelete()
    }

    enum class KeyType {
        COMMON,
        UTIL
    }

}


fun Float.toPixel(context: Context): Int {
    val density = context.resources.displayMetrics.density
    return (this * density).roundToInt()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용은 아래와 같이 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MainActivity.kt&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1675394426386&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;binding.customKeyboard.setOnKeyboardListener(object: CustomKeyboard.OnKeyboardListener {

    @SuppressLint(&quot;SetTextI18n&quot;)
    override fun onEnterText(text: String) {
        binding.tvInput.text = binding.tvInput.text.toString() + text
        makeToast(text)
    }

    override fun onDelete() {
        binding.tvInput.text = binding.tvInput.text.toString().dropLast(1)
    }

})

binding.tvInput.doAfterTextChanged {
    (0 until binding.layoutBullet.childCount).forEach { index -&amp;gt;
        (binding.layoutBullet.getChildAt(index) as CardView?)?.apply {
            if (index &amp;gt; it.toString().lastIndex) {
                setCardBackgroundColor(ContextCompat.getColor(context, R.color.color_eaedf0))
            } else {
                setCardBackgroundColor(ContextCompat.getColor(context, R.color.color_2c6ceb))
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;activity_main.xml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1675394873298&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&amp;gt;
&amp;lt;androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
    xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
    xmlns:tools=&quot;http://schemas.android.com/tools&quot;
    android:background=&quot;@color/white&quot;
    android:layout_width=&quot;match_parent&quot;
    android:layout_height=&quot;match_parent&quot;
    tools:context=&quot;.MainActivity&quot;&amp;gt;

    &amp;lt;LinearLayout
        android:id=&quot;@+id/layout_bullet&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        android:orientation=&quot;horizontal&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/tv_input&quot;
        android:gravity=&quot;center&quot;
        android:padding=&quot;24dp&quot;&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_1&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_2&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_3&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_4&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_5&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
        &amp;lt;androidx.cardview.widget.CardView
            android:id=&quot;@+id/cv_6&quot;
            android:layout_width=&quot;12dp&quot;
            android:layout_height=&quot;12dp&quot;
            android:layout_margin=&quot;8dp&quot;
            app:cardCornerRadius=&quot;6dp&quot;
            app:cardBackgroundColor=&quot;@color/color_eaedf0&quot;/&amp;gt;
    &amp;lt;/LinearLayout&amp;gt;

    &amp;lt;TextView
        android:id=&quot;@+id/tv_input&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:layout_constraintBottom_toTopOf=&quot;@id/custom_keyboard&quot;
        android:maxLength=&quot;6&quot;
        android:textColor=&quot;@color/black&quot;
        android:textSize=&quot;20sp&quot;
        android:padding=&quot;24dp&quot;
        android:lineSpacingExtra=&quot;8dp&quot;
        android:gravity=&quot;center&quot;/&amp;gt;

    &amp;lt;com.example.customkeyboardapp.CustomKeyboard
        android:id=&quot;@+id/custom_keyboard&quot;
        android:layout_width=&quot;match_parent&quot;
        android:layout_height=&quot;wrap_content&quot;
        app:layout_constraintBottom_toBottomOf=&quot;parent&quot;
        app:layout_constraintEnd_toEndOf=&quot;parent&quot;
        app:layout_constraintStart_toStartOf=&quot;parent&quot;
        app:layout_constraintTop_toTopOf=&quot;parent&quot; /&amp;gt;

&amp;lt;/androidx.constraintlayout.widget.ConstraintLayout&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드의 변경 이벤트는 interface를 통해서 처리하도록 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bullet Layout 아래에 하위 뷰로 CardView를 비밀번호 길이만큼 만들고&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키보드 입력이 들어오면 총 텍스트의 갯수만큼 ViewGroup 하위의 CardView를 업데이트 해주는 방식입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Bullet layout의 경우도 여러 화면에서 사용한다면 따로 class로 만들어 사용하면 더 좋겠죠?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구현 영상을 올리니 티스토리 화면 전체가 깨지는 오류가 있어 파일로 올립니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문 있으시면 댓글 남겨주세요!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;fileblock&quot; data-ke-align=&quot;alignCenter&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/dzIXtg/btrXWLkhiRw/4Frp8BzK3o6RtbBgoK5ci0/XRecorder_Edited_03022023_120711.mp4?attach=1&amp;amp;knm=tfile.mp4&quot; class=&quot;&quot;&gt;
    &lt;div class=&quot;image&quot;&gt;&lt;/div&gt;
    &lt;div class=&quot;desc&quot;&gt;&lt;div class=&quot;filename&quot;&gt;&lt;span class=&quot;name&quot;&gt;XRecorder_Edited_03022023_120711.mp4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;size&quot;&gt;2.47MB&lt;/div&gt;
&lt;/div&gt;
  &lt;/a&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/221</guid>
      <comments>https://sandn.tistory.com/221#entry221comment</comments>
      <pubDate>Fri, 3 Feb 2023 12:29:54 +0900</pubDate>
    </item>
    <item>
      <title>안드로이드 스튜디오에서 Java, Kotlin 전환 비율 알아내기</title>
      <link>https://sandn.tistory.com/220</link>
      <description>&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;UPDATE&lt;br&gt;&lt;br&gt;밑에 방법은 뻘짓이고 그냥 AndroidStudio 내 Statistics 플러그인을 사용하면 알아서 계산을 해준다... ㅎㅎ&lt;br&gt;&lt;a href=&quot;https://plugins.jetbrains.com/plugin/4509-statistic&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;https://plugins.jetbrains.com/plugin/4509-statistic&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;opengraph&quot; data-og-title=&quot;Statistic - IntelliJ IDEs Plugin | Marketplace&quot; data-ke-align=&quot;alignCenter&quot; data-og-description=&quot;Shows project statistic.&quot; data-og-host=&quot;plugins.jetbrains.com&quot; data-og-source-url=&quot;https://plugins.jetbrains.com/plugin/4509-statistic&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dIpngG/hyRkp1R2XD/DninrgPGS0tKKzgiG0yzdk/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/LR63v/hyRkuPFLQH/smhTqOocGOTS1hYRQLeeV0/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600&quot; data-og-url=&quot;https://plugins.jetbrains.com/plugin/4509-statistic&quot;&gt;
 &lt;a href=&quot;https://plugins.jetbrains.com/plugin/4509-statistic&quot; target=&quot;_blank&quot; data-source-url=&quot;https://plugins.jetbrains.com/plugin/4509-statistic&quot;&gt;
  &lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dIpngG/hyRkp1R2XD/DninrgPGS0tKKzgiG0yzdk/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600,https://scrap.kakaocdn.net/dn/LR63v/hyRkuPFLQH/smhTqOocGOTS1hYRQLeeV0/img.png?width=600&amp;amp;height=600&amp;amp;face=0_0_600_600')&quot;&gt; 
  &lt;/div&gt;
  &lt;div class=&quot;og-text&quot;&gt;
   &lt;p class=&quot;og-title&quot;&gt;Statistic - IntelliJ IDEs Plugin | Marketplace&lt;/p&gt;
   &lt;p class=&quot;og-desc&quot;&gt;Shows project statistic.&lt;/p&gt;
   &lt;p class=&quot;og-host&quot;&gt;plugins.jetbrains.com&lt;/p&gt;
  &lt;/div&gt;&lt;/a&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt; &lt;br&gt; &lt;br&gt; &lt;br&gt; &lt;br&gt;플러그인을 설치하고 하단 statistics -&amp;gt; refresh 를 누르면 아래와 같이 알아서 타입별로 통계를 내준다.&lt;/p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3522&quot; data-origin-height=&quot;990&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IvukM/btrWEJ9Buhl/nEAnodWMKZFmExSHKRMUj0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IvukM/btrWEJ9Buhl/nEAnodWMKZFmExSHKRMUj0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IvukM/btrWEJ9Buhl/nEAnodWMKZFmExSHKRMUj0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIvukM%2FbtrWEJ9Buhl%2FnEAnodWMKZFmExSHKRMUj0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3522&quot; height=&quot;990&quot; data-origin-width=&quot;3522&quot; data-origin-height=&quot;990&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt; &lt;br&gt; &lt;br&gt;현재 통계를 보니 절반 정도가 Kotlin 코드로 전환되었다.&lt;br&gt; &lt;br&gt; &lt;br&gt; &lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt;&lt;br&gt;안드로이드 스튜디오 내에서 전환율을 찾아주는 기능을 찾지 못했다.&lt;br&gt; &lt;br&gt;그래서 터미널을 이용해 직접 카운팅하는 방법을 사용.&lt;br&gt; &lt;br&gt;안드로이드 스튜디오 내 프로젝트 파일들이 모여있는 경로로 이동하여 아래 커맨드를 입력한다.&lt;br&gt; &lt;br&gt;안드로이드 폴더 하부에서 java 확장자 파일을 모두 찾아 갯수를 카운팅하는 커맨드&lt;/p&gt;
&lt;pre data-ke-type=&quot;codeblock&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;find ./android -name '*.java' | wc -l&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot; style=&quot;text-align: left;&quot;&gt; &lt;br&gt;단점이 있다면 line by line이나 절대량으로는 계산이 안되고 파일 갯수로만 비교가 가능하다는 것.&lt;br&gt; &lt;br&gt; &lt;/p&gt;</description>
      <category>안드로이드(Android)</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/220</guid>
      <comments>https://sandn.tistory.com/220#entry220comment</comments>
      <pubDate>Thu, 19 Jan 2023 12:28:44 +0900</pubDate>
    </item>
    <item>
      <title>유튜브 안드로이드 장인?</title>
      <link>https://sandn.tistory.com/219</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;개발 관련 유튜브를 보다가 뭔가 안드로이드 장인의 느낌이 나서 스크랩 해둔다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div id=&quot;header&quot;&gt;
&lt;div id=&quot;header-author&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;author-text&quot; href=&quot;https://www.youtube.com/channel/UCpm6_1kwoW7gcjnkgkLG-Wg&quot;&gt;&lt;span&gt;lucky-Man&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;a href=&quot;https://www.youtube.com/watch?v=wD8GnE_AJQQ&amp;amp;lc=Ugxb4CtXG7o_oiFRlFF4AaABAg&quot;&gt;1년 전&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;comment-content&quot;&gt;
&lt;div id=&quot;content&quot;&gt;&lt;span&gt;안드로이드도 시장에 개발자가 많다고 하더라도, 그중에 안드로이드 글랜드 슬램 개발 달성한 개발자는 몇 안되더군요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;안드로이드 글랜드 슬램으로는 안드로이드 응용프로그램단 + 프레임웍단 개발 + JNI(NDK) 드라이버단 정도를 모두 비벼본 개발자들이 &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;거의 없더군요. 본인도 안드로이드로 갈때까지 실무로 다 경험을 해봤지만, 아이오에스로 넘어갈까 하다가, 개발자 1년단위 갱신과 장비들에 너무 돈빨이 들어가서 &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그냥 접고... 웹쪽으로 파볼까 해서 모바일쪽 회사 나와서 프론트엔드와 백엔드쪽으로 공부좀하다가 프리로 대기업 프로젝트 뛰어보고 해보니, 나름 웹쪽도 괜찮은 것 같더군요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그러나 아직까지 국내에서는 웹개발자는 모바일 개발자보다 단가가 적습니다. 중고급도 좀 차이나구요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;그리고 그간 대기업 중소기업 벤쳐 등등... 여러회사를 거쳐보니...&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;윗사람들의 마인드가... 개발자 기술 교육에는 돈쓰는 것이 낭비라고 생각하는 사람들이 많더군요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;팀장들이 뭐라 뭐라 얘기해서 교육좀 해달라 하면은... 돈아끼려고 개발자 총 20명이면 단 3명 정도만 교육 보내고, 그 3명이 전파교육하는 경우도 많습니다. 중소기업이 유독 심함.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;회사 입장에서는 아주 뛰어난 버그없는 양질에 프로그램을 만들라고 독촉을 하겠지만, 실제 이런 기술들이 업무에 녹아내리려면은, 개인 스스로 공부하는 것도 한계가 있습니다. 회사 차원의 지원도 반드시 있어야 하는데... 이런게 없어요... ㅜ.ㅜ&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;마치 예전 쌍팔년도 때 한국이 축구할 수 있는 전용 축구 경기장의 인프라조차 없는데도 불구하고... 오로지 정신력으로 16강에 들어가라는 말도 안되는 정신나간 생각과 같죠.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;결론은 회사가 본인들의 우수한 인력과 인재들을 붙잡기 위해서라도... 개발자들의 교육과 개발 환경 개선을 위해 힘써야 겠죠.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;아마 L회사 모바일 사업 철수도 소프트웨어 개발자 대우와 교육에 의한 트렌드 선도 등... 개발환경 개선의 문제에 대한 영향이 없진 않을겁니다..&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;커리어 영상 잘봤습니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;div id=&quot;header&quot;&gt;
&lt;div id=&quot;header-author&quot;&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;a id=&quot;author-text&quot; href=&quot;https://www.youtube.com/channel/UCpm6_1kwoW7gcjnkgkLG-Wg&quot;&gt;&lt;span&gt;lucky-Man&lt;/span&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;a href=&quot;https://www.youtube.com/watch?v=WF_bzlpaW0I&amp;amp;lc=UgytxToNMR5QeZaQJI14AaABAg&quot;&gt;2년 전&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&quot;comment-content&quot;&gt;
&lt;div id=&quot;content&quot;&gt;&lt;span&gt;이책을 처음 본게 10년 전이 였던것 같은데... 매번 프로젝트 진행할때마다 자주 책장에서 뽑아서 봅니다. 대략 5번은 정독으로 읽은것 같네요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;참고로 영문판과 한글판 둘다 가지고 있지만, 한글판이 1.5배더 폭이 큽니다. 둘다 양장본(하드카바)이구요. 종이 질은 한글판이 훨씬 좋습니다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;한글판사세요. 책표지도 전부 원판과 거의 똑같습니다.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;한글판 번역도 깔끔하게 잘되어 있고, 영문과 번역 둘다 이질감 없습니다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;옛날에 갱스오브포 원년 멤버들이 출판한 디자인패턴을 처음 번역해서 욕 한바가지 먹었던 번역서가 있는데요. 구글링해도 욕이 많았던...ㅋ&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;결국 라면 받침대로 사용하지만... 이 리팩토링 번역판은 반대로 번역 정말 잘되어 있어요. 만약 주니어 분들이 보신다면 한글판 사는게 나을 겁니다.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;정답은 없지만, 제 개인적인 추천으로는... 저는 아래 순서 대로 읽었더니 코더에서 프로그래머가 되듯이... 뭔가 깨달음을 얻게 되더라구요.&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;[본인이 읽었던 순서]&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;일반 디자인 패턴 쉽게 나온것 읽다가 이해가 된다 싶으면 -&amp;gt; 이영상에서 추천한 리팩토링책 -&amp;gt; 패턴을 활용한 리팩토링 (출판사 : X사이트) -&amp;gt; 클린코드 -&amp;gt; code reading -&amp;gt; 실용주의 프로그래머 -&amp;gt; code complete -&amp;gt; beautiful code -&amp;gt; The Art of Computer Programming 시리즈 -&amp;gt; 피플웨어 -&amp;gt; 애자일 프렉틱스 -&amp;gt; 익스트림 프로그래밍 -&amp;gt; &quot; 개발 업계에서 살아가려면... 끊임없이 계속 읽어야함~ &quot;&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;The Art of Computer Programming 이후부터는 프로그래밍 철학적인 내용들이라 한번 쯤은 읽어볼만 합니다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;특히 본인이 개발 PM이거나 프로그래머와 상관없이 기획자인데 개발관련 PM 이라면은 관리자급에서는 꼭! &quot;피플웨어&quot;는 읽어보시길 권합니다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;책은 몇십년 이전에 나온 책이지만, 정말 잘만들었습니다. 사소하지만, 관리자와 개발자가 시너지를 낼수 있는 여러가지 상황과 해결책들을 기존 프로젝트를 바탕으로 설명합니다. &lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;심지어 개발자 책상 배치나 성격이 다른 부서라도 독과 약이 되는 것등... 우리가 현재까지도 겪고 있는 현실들이 비슷하게 나와있습니다.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;</description>
      <category>개발 관련 상담&amp;amp;조언</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/219</guid>
      <comments>https://sandn.tistory.com/219#entry219comment</comments>
      <pubDate>Thu, 12 Jan 2023 23:55:17 +0900</pubDate>
    </item>
    <item>
      <title>TextWatcher를 사용하여 안드로이드 EditText 에 단위를 붙여보자</title>
      <link>https://sandn.tistory.com/218</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;UI를 구현하거나 앱들을 쓰다 보면 보면 특정 editText에 콤마나 단위 등을 고정하여 표시하는 경우가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 은행 앱에서 출금 시 자동으로 콤마를 붙여주고 뒤에 '원'이 붙는 경우를 한 번 쯤은 보셨을 겁니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-05 오후 4.17.54.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;162&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beosy8/btrVVe8ZkwS/PEw0GkVRW5q6DWKCjaEWgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beosy8/btrVVe8ZkwS/PEw0GkVRW5q6DWKCjaEWgk/img.png&quot; data-alt=&quot;금액 입력 전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beosy8/btrVVe8ZkwS/PEw0GkVRW5q6DWKCjaEWgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeosy8%2FbtrVVe8ZkwS%2FPEw0GkVRW5q6DWKCjaEWgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;726&quot; height=&quot;162&quot; data-filename=&quot;스크린샷 2023-01-05 오후 4.17.54.png&quot; data-origin-width=&quot;726&quot; data-origin-height=&quot;162&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;금액 입력 전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2023-01-05 오후 4.18.30.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QJcq7/btrVUzMkirL/g61zmKYFJjtlbX4pc0tI0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QJcq7/btrVUzMkirL/g61zmKYFJjtlbX4pc0tI0K/img.png&quot; data-alt=&quot;금액 입력 후&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QJcq7/btrVUzMkirL/g61zmKYFJjtlbX4pc0tI0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQJcq7%2FbtrVUzMkirL%2Fg61zmKYFJjtlbX4pc0tI0K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;564&quot; height=&quot;136&quot; data-filename=&quot;스크린샷 2023-01-05 오후 4.18.30.png&quot; data-origin-width=&quot;564&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;금액 입력 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 화면은 어떻게 구현해야 할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 TextWatcher를 사용하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TextWatcher는 안드로이드에서 제공하는 인터페이스로서 EditText의 텍스트 입력이 들어올 때 마다 콜백이 호출됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3가지 함수를 오버라이드 하게 되는데요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beforeTextChanged&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onTextChanged&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;afterTextChanged&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;beforeTextChanged는 바뀌기 전의 입력을 알 수 있고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;onTextChanged는 바뀐 후의 입력을 알 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 afterTextChanged는 언제 사용할까요?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서는 변경된 문자열을 기준으로 추가적인 수정을 처리할 때 사용합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서에 보시면 친절하게 아래와 같이 설명되어 있습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;... It is legitimate to make further changes to s from this callback, but be careful not to get yourself into an infinite loop, ...&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 콜백에서 텍스트를 변경해도 되지만 무한루프에 빠지지 않게 주의하라네요.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 이제 실제로 어떻게 구현하는지 아래 소스코드를 첨부드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1673338636676&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;abstract class SuffixedTextWatcher(
    private val editText: EditText,
    private val suffix: String
) : TextWatcher {
    var beforeText = &quot;&quot;
    var afterText = &quot;&quot;
    var startPos = 0
    var endPos = 0

    override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        beforeText = p0.toString()
    }

    override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
        afterText = p0.toString()
        startPos = p2
        endPos = p3
    }

    override fun afterTextChanged(e: Editable?) {
        if (e == null) return

        // 무한루프를 방지
        if (beforeText == afterText) return

        // suffix 가 수정된 경우 마지막 문자열을 suffix 로 만들어 주고 커서를 suffix 바로 앞으로 이동시키는 부분
        if (!e.toString().endsWith(suffix, false)) {
        	// 첫 입력인 경우 suffix 를 세팅해준다
            if (beforeText.isEmpty()) {
                // 값이 입력된 경우 값도 앞에 붙여줌
                if (afterText.isNotBlank()) {
                    val suffixedText = afterText.plus(suffix)
                    editText.setText(suffixedText)
                    editText.setSelection(suffixedText.length - suffix.length)
                    return
                }
                editText.setText(suffix)
                editText.setSelection(0)
                return
            }
            
            editText.setText(beforeText)
            
            if (beforeText.length &amp;lt; suffix.length) {
                // 문자를 지운 경우
                editText.setSelection(0)
            } else {
                // 문자가 삽입된 경우
                editText.setSelection(afterText.length - suffix.length)
            }
            return
        }

        // 이하 suffix 에 문제가 없는 경우
        
        // 빈값이 들어오면 0으로 리턴해줍니다
        val num = e.toString()
            .replace(&quot;,&quot;, &quot;&quot;)
            .removeSuffix(suffix)
            .toBigIntegerOrNull()
            ?: kotlin.run {
                onNumberChanged(BigInteger.ZERO)
                return
            }

        // 정상적으로 입력값이 들어온 경우
        if (beforeText != afterText) {
            // 값에 suffix 를 붙여주고
            val str = NumberFormat.getInstance().format(num).plus(suffix)
            editText.setText(str)
            
            // suffix 바로 앞으로 커서를 이동시켜줍니다
            val selectedPos = if (editText.text.length - suffix.length &amp;lt; 0) {
                0
            } else {
                editText.text.length - suffix.length
            }
            editText.setSelection(selectedPos)
        }

        // 마지막으로 콜백도 호출해줍니다, 현재는 정수형 입출력만 가능하도록 되어 있습니다
        // 만약 실수형의 데이터를 사용하고 싶다면 데이터 타입을 바꿔줘야 합니다, 유의하세요!
        onNumberChanged(num)
    }

    abstract fun onNumberChanged(number: BigInteger)

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 현재 기준은 정수형만 사용이 가능하기 때문에 아래와 같이 inputType을 number로 설정해주어야 오류가 나지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실수형을 사용하고자 하는 경우 약간의 커스터마이징이 필요한데 필요하시면 실수형 버전도 올려보도록 할게요!&lt;/p&gt;
&lt;pre id=&quot;code_1673339954139&quot; class=&quot;html xml&quot; data-ke-language=&quot;html&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;&amp;lt;EditText
    android:id=&quot;@+id/et_amount&quot;
    android:layout_width=&quot;wrap_content&quot;
    android:layout_height=&quot;wrap_content&quot;
    android:hint=&quot;금액 입력&quot;
    android:maxLength=&quot;9&quot;
    android:textColorHint=&quot;@color/color_cfd6e4&quot;
    android:textSize=&quot;40sp&quot;
    android:fontFamily=&quot;@font/pretendard_regular&quot;
    android:paddingStart=&quot;40sp&quot;
    android:paddingEnd=&quot;40sp&quot;
    android:paddingBottom=&quot;16dp&quot;
    android:inputType=&quot;number&quot;
    android:layout_marginTop=&quot;8dp&quot;
    android:layout_gravity=&quot;center_horizontal&quot;
    android:gravity=&quot;center&quot;/&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로는 이런 식으로 사용하시면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1673339808605&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;val suffix = &quot; CC&quot;
binding.etAmount.addTextChangedListener(object : SuffixedTextWatcher(binding.etAmount, suffix) {
    override fun onNumberChanged(number: BigInteger) {
    	// do something
    }
})&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모르거나 궁금하신 부분 그리고 개선할 점이 있다면 언제든 의견 남겨주세요!&lt;/p&gt;</description>
      <category>안드로이드(Android)/이것저것 만들어보기</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/218</guid>
      <comments>https://sandn.tistory.com/218#entry218comment</comments>
      <pubDate>Tue, 10 Jan 2023 17:35:12 +0900</pubDate>
    </item>
    <item>
      <title>BindingAdapter 를 이용하여 EditText 에 양방향 바인딩 사용하기</title>
      <link>https://sandn.tistory.com/217</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;DataBinding과 LiveData를 사용하면 여러모로 편리한 점이 많다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 뷰모델에서 사용하는 데이터와 자료형이 다른 경우가 생긴다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면, 잔액을 표시하는 경우&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 뷰모델에서는&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;val amount: LiveData&amp;lt;Int&amp;gt; = ...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 형식으로 정의되어 있는데&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 TextView나 EditText에 보여줄 때는 문자열 타입이 필요하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양방향 바인딩을 사용하는 경우 이럴 때 어떻게 해야 할지 참 난감해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럴 땐 BindingAdapter를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1672900561953&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;/**
 * LiveData 에서는 숫자(Int, Long, BigInteger...) 타입을 유지하고
 * EditText 에 표시할 때는 문자열 타입으로 표시해주기 위해 사용함.
 * BindingAdapter 를 사용하면 Comma 를 사용하는 양방향 데이터 바인딩이 가능함.
 *
 * **/
@BindingAdapter(&quot;textWithComma&quot;)
fun EditText.textWithComma(num: Any?): String {
    return when (num) {
        is Int -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        is Long -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        is BigInteger -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        else -&amp;gt; &quot;&quot;
    }.also {
        if (text.toString() != it) {
            setText(it)

            // 양방향 바인딩 사용 시 문자열 입력/삭제 시 커서가 처음으로 이동하는 문제가 있음.
            // 문자열 입력/제거 시 항상 마지막 커서로 이동시켜주기 위해 사용함.
            setSelection(it.length)
        }
        Logger.d(&quot;textWithComma &amp;gt;&amp;gt; $it&quot;)
    }
}

@InverseBindingAdapter(attribute = &quot;textWithComma&quot;, event = &quot;textAttrChanged&quot;)
fun EditText.getTextString(): Int {
    return (text.toString().replace(&quot;,&quot;, &quot;&quot;).toIntOrNull() ?: 0).also { Logger.d(&quot;textAttrChanged &amp;gt;&amp;gt; $it&quot;) }
}

@BindingAdapter(&quot;textAttrChanged&quot;)
fun EditText.setTextWatcher(textAttrChanged: InverseBindingListener){
    addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(p0: Editable?) { textAttrChanged.onChange() }

        override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) { }

        override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {

        }
    })
}

@BindingAdapter(&quot;textWithComma&quot;)
fun TextView.textWithComma(num: Any?): String {
    return when (num) {
        is Int -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        is Long -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        is BigInteger -&amp;gt; DecimalFormat(&quot;###,###&quot;).format(num)
        else -&amp;gt; &quot;&quot;
    }.also {
        text = it
        Logger.d(&quot;TextView.textWithComma &amp;gt;&amp;gt; type is ${num?.javaClass?.simpleName}, value is $it&quot;)
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 만들어 사용하고 있는 함수들이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>안드로이드(Android)</category>
      <author>Essense</author>
      <guid isPermaLink="true">https://sandn.tistory.com/217</guid>
      <comments>https://sandn.tistory.com/217#entry217comment</comments>
      <pubDate>Thu, 5 Jan 2023 15:37:52 +0900</pubDate>
    </item>
  </channel>
</rss>