<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wiz_hey.log</title>
        <link>https://velog.io/</link>
        <description>파이팅!</description>
        <lastBuildDate>Mon, 01 Apr 2024 10:48:14 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <copyright>Copyright (C) 2019. wiz_hey.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wiz_hey" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 17]]></title>
            <link>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-4%EC%9B%94-1%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-4%EC%9B%94-1%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 01 Apr 2024 10:48:14 GMT</pubDate>
            <description><![CDATA[<h3 id="✏20240325-월요일--✏20240329-금요일">✏20240325 월요일 ~ ✏20240329 금요일</h3>
<h3 id="📖브로셔-제작-및-최종-발표-자료-준비">📖브로셔 제작 및 최종 발표 자료 준비</h3>
<p><img src="https://velog.velcdn.com/images/wiz_hey/post/22f5557b-6bab-47c0-b0fd-d10b57492985/image.png" alt=""></p>
<ul>
<li><p>최종 발표회에서 사용 할 브로셔를 제작 했다.</p>
</li>
<li><p>브로셔에는 Architecture, Main Stacks, 주요 기능, 의사 결정 사항, 트러블 슈팅 등에 대한 내용이 들어간다.</p>
</li>
<li><p>그 동안 작성한 TIL과 5분기록보드를 통해 적어 놓은 내용들을 바탕으로 의사결정사항과 트러블슈팅을 쉽게 적을 수 있었다.</p>
</li>
<li><p>최종 발표 자료는 중간 자료와 비슷하지만, 주요 기능 페이지에서 실 사용 모습도 (gif) 같이 시연하고, 앞으로 유저 피드백을 받아서 최종 배포 후에도 유지보수 계획이 있다는 내용을 첨부했다.</p>
</li>
</ul>
<br>

<h3 id="📖최종-앱-배포">📖최종 앱 배포</h3>
<ul>
<li>주말 동안 마감한 코드를 합쳐서 구글 플레이 콘솔에 올려두었었는데, 사용했던 AES 암호화의 ECB 방식이 보안에 문제가 생겼다는 알림을 받아서, 담당 팀원분이 <code>GCM, NoPadding방식</code>으로 고치셔서 다시 올린 뒤에야 정규 출시가 되었다.</li>
<li>최종 앱 배포와 동시에 비공개 테스트가 아닌 실 유저 테스트를 받기로 했다. 약 일주일 동안 테스트 이벤트를 진행하는데, 구글 폼을 통해 앱 사용 인증과 그에 대한 피드백을 주면 기프티콘을 증정하는 이벤트이다!</li>
</ul>
<br>

<h3 id="📖최종-발표회">📖최종 발표회</h3>
<ul>
<li><p>최종 발표회에는 그 동안 가르쳐주신 튜터님들과 진행을 도와주신 매니저님들을 포함해서, 지인 초대와 심사를 해주실 현업 개발자 분들 까지 초대해서 세미 레퍼런스식으로 진행이 되었다.</p>
</li>
<li><p>다른 팀에서 만든 어플들도 구경하고, 발표회 후에는 튜터님들과 심사위원분들이 오셔서 면접 대비 질문들과 어플에 대해 궁금한 점들을 물어보시고, 답변을 들은 후 조언까지 해주시는 시간을 가졌다.</p>
</li>
<li><p>그리고, 수업을 들은 수강생들 다 같이 마무리 회고를 가졌다. 4개월의 시간 동안 내가 얼만큼 코드카타를 하고, TIL을 작성하고, 성취를 할 수 있었는지 지표로도 확인을 하고 알찬 시간이었다!</p>
</li>
<li><p>정규 프로그램은 이렇게 마무리 되었지만, 어쨌든 유저 피드백을 통한 유지보수는 계속 해나아가기로 팀원들과 약속했기 때문에 앞으로도 계속 신경을 써야할 것 같다!!</p>
</li>
<li><p>그래도 이렇게 완수한 것에 대해 뿌듯한 마음이다!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 16]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-16</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-16</guid>
            <pubDate>Thu, 21 Mar 2024 13:36:28 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240321-목요일-tiltoday-i-learned-오늘-배운-것--br-✏240322-금요일-tiltoday-i-learned-오늘-배운-것">✏240321 목요일 TIL(Today I learned) 오늘 배운 것 ~ <br> ✏240322 금요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖firebase를-활용한-구글-로그인-authentication">📖Firebase를 활용한 구글 로그인 (Authentication)</h3>
<ul>
<li><p>구글 간편로그인에 대해서 간단히 찾아보니까, 구글 클라우드 콘솔을 통해서 바로 만드는 방법과 파이어베이스의 <code>Authentication</code>을 통해 간편로그인을 구현하는 2가지 방법이 있었다.</p>
</li>
<li><p>우리는 이미 파이어베이스를 사용하고 있기도 하고, 파이어베이스에서 자체적으로 보안 기능을 내장하고 있기도 하다고 해서 별 고민 없이 <code>Authentication</code>로 간편 로그인을 구현하였다.</p>
<pre><code class="language-kotlin">//LoginActivity.kt
private lateinit var firebaseAuth: FirebaseAuth
private lateinit var googleSighInClient: GoogleSignInClient
private lateinit var googleLoginResult: ActivityResultLauncher&lt;Intent&gt;

override fun onCreate(savedInstanceState: Bundle?) {
      initActivityResult()
      firebaseAuth = FirebaseAuth.getInstance()
      val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
          .requestIdToken(getString(R.string.google_web_client_id))
          .requestId()
          .requestEmail()
          .requestProfile()
          .build()
      googleSighInClient = GoogleSignIn.getClient(this, googleSignInOptions)

      binding.cvGoogle.setOnClickListener {
          googleSignIn()
      }
}

    private fun initActivityResult() {
      googleLoginResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -&gt;
          if (result.resultCode == Activity.RESULT_OK) {
              val data = result.data
              try {
                  val task = GoogleSignIn.getSignedInAccountFromIntent(data)
                  val account = task.getResult(ApiException::class.java)
                  firebaseAuthWithGoogle(account.idToken)
              } catch (e: ApiException) {
                  // Google 로그인 실패 처리
                  Timber.tag(&quot;GoogleLoginError&quot;).d(&quot;Google 로그인 실패: ${e.statusCode}&quot;)
                  finish()
              }
          } else {
              // 사용자가 로그인을 취소했거나 결과가 OK가 아닐 때의 처리
              Timber.tag(&quot;GoogleLoginError&quot;).d(&quot;로그인 사용자 취소 또는 실패&quot;)
              finish()
          }
      }
  }

  private fun firebaseAuthWithGoogle(idToken: String?) {
      val credential = GoogleAuthProvider.getCredential(idToken, null)
      binding.clLoginLoading.visibility = View.VISIBLE
      val db = Firebase.firestore
      firebaseAuth.signInWithCredential(credential).addOnSuccessListener { result -&gt;
          val documentRef = db.collection(&quot;users&quot;).document(&quot;Google${result.user?.uid}&quot;)
          val encryptedNickName = result.user?.displayName?.take(10)?.let { encrypt(it, AES_KEY) }
          val encryptedUserEmail = result.user?.email?.let { encrypt(it, AES_KEY) }
          val userModel = hashMapOf(
              &quot;userId&quot; to &quot;Google${result.user?.uid}&quot;,
              &quot;nickName&quot; to encryptedNickName,
              &quot;profileImage&quot; to &quot;${result.user?.photoUrl}&quot;,
              &quot;userEmail&quot; to encryptedUserEmail,
              &quot;bookmarked&quot; to null
          )
          documentRef.get().addOnCompleteListener { task -&gt;
              if (task.isSuccessful) {
                  val documentSnapshot = task.result
                  if (!documentSnapshot.exists()) {
                      profileImgUpload(Uri.parse(result.user?.photoUrl.toString()), &quot;Google${result.user?.uid}&quot;)
                      documentRef.set(userModel)
                      Toast.makeText(this, &quot;로그인 성공!&quot;, Toast.LENGTH_SHORT).show()
                  }
              }
              Toast.makeText(this, &quot;환영합니다.&quot;, Toast.LENGTH_SHORT).show()
              EncryptedPrefs.saveMyId(&quot;Google${result.user?.uid}&quot;)
              binding.clLoginLoading.visibility = View.GONE
              finish()
          }
      }
          .addOnFailureListener {
              Timber.tag(&quot;GoogleLoginError&quot;).d(it.toString())
              binding.clLoginLoading.visibility = View.GONE
              finish()
          }
  }

  private fun googleSignIn() {
      val signInIntent = googleSighInClient.signInIntent
      googleLoginResult.launch(signInIntent)
  }</code></pre>
<ul>
<li><p>파이어베이스에서 <code>Authentication</code>을 활성화 시키는 방법은 어렵지 않으니 바로 코드 설명으로 들어가 보자면, </p>
</li>
<li><p><code>onCreate</code>에서 <code>initActivityResult(googleLoginResult)</code>가 초기화 될 수 있도록 함수를 따로 빼서 걸어주었다.
또, <code>firebaseAuth</code>와 <code>googleSighInClient</code>도 같이 초기화 해주었다.
이때, <code>googleSignInOptions</code>을 선언하여 필요한 값인 고유아이디(UUID로 활용), 이메일주소, 프로필이미지 를 받아올 수 있도록 설정값을 저장했다.</p>
</li>
<li><p><code>initActivityResult</code>에서는 구글 로그인 버튼을 클릭 했을 때, 구글 로그인 창을 통해 선택한 구글 간편 로그인 아이디의 값(데이터)들을 저장해서 <code>firebaseAuthWithGoogle</code>에 넘겨준다.</p>
</li>
<li><p><code>firebaseAuthWithGoogle</code>을 통해 직접 로그인 절차를 수행하게 되는데, 로그인이 되는 동안 <code>binding.clLoginLoading.visibility = View.VISIBLE</code>를 통해 로딩화면이 보여지도록 설정해주었다.
후에는, 카카오 로그인 절차와 동일하게 존재하는 유저인지 확인하기 위해서 파이어스토어를 연결해주고, 첫 로그인 시에 유저 정보를 저장할 수 있도록 <code>userModel = hashMapOf()</code>을 만들어주었다.
<code>if (!documentSnapshot.exists())</code> 를 통해 존재하지 않는 유저라면 <code>documentRef.set(userModel)</code> 유저모델 값을 새롭게 저장한다.
존재한다면 <code>EncryptedPrefs.saveMyId(&quot;Google${result.user?.uid}&quot;)</code>로 SharedPrefernce에 유저 아이디 값을 저장하고 로그인 통과를 시켜준다.
<code>binding.clLoginLoading.visibility = View.GONE</code> 로그인이 통과/실패 된다면 로딩 중 화면을 없앤 후 원래 페이지로 돌아가도록 <code>finish</code>함수를 사용했다.</p>
</li>
<li><p>[트러블 슈팅] 로그인 과정 중에 뒤로가기 등을 누르면, 로그인 프로세스만 종료되는 게 아니라 강제 종료가 되기 때문에 <code>addOnFailureListener</code>와 <code>try-catch</code>를 통해서 앱 크래시가 나지 않도록 에러 처리를 했다. 이 포스트의 코드에는 나오지 않지만, 카카오 로그인 시에도 오류 메시지로 사용자에게 혼란을 주지 않기 위해 로그처리를 해주었다.</p>
</li>
<li><p>[트러블 슈팅] 원래는 첫 로그인 시 프로필 이미지를 받아올 때, 프로필 URI를 스토리지에 먼저 업로드 하고, 스토리지에 업로드 된 이미지의 URI를 받아오는 방식으로 코드를 짰었다. 그랬더니 <code>StorageException has occurred.</code> 오류가 발생해서 <code>profileImgUpload</code>함수를 콜백함수로 변경하여 사용했는데, 처음 로그인을 할 때 5초 이상의 시간이 걸렸다.</p>
</li>
<li><blockquote>
<p>팀원들과 상의한 결과, 처음에는 그냥 간편로그인 시 제공해주는 프로필 URI를 바로 유저데이터에 올리고, 스토리지에는 따로 프로필이미지를 업로드하여 파일을 미리 생성하는 방법을 사용하기로 했다.</p>
</blockquote>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="📖유저데이터-정보-불러오기-뷰모델로-전환">📖유저데이터 정보 불러오기 뷰모델로 전환</h3>
<ul>
<li><p>화면이 로드될 때마다 직접 <code>ProfileFragment</code>에서 파이어베이스에 연결된 데이터들을 불러왔더니, 매번 새로 데이터가 들어오는 모습이 보여져서 화면이 깜빡 거리는 것처럼 보인다는 피드백이 들어왔다. 그래서 이참에 데이터를 뷰모델로 저장해서 불러올 수 있도록 코드를 리팩토링 했다.</p>
<pre><code class="language-kotlin">//package com.brandon.campingmate.data.remote.dto
data class UserDTO(
  val userId: String? = null,
  val userEmail: String? = null,
  val nickName: String? = null,
  val profileImage: String? = null,
  val bookmarked: List&lt;String&gt;? = null
)

//ProfileViewModel.kt
private val _userData: MutableLiveData&lt;UserDTO?&gt; = MutableLiveData()
val userData: LiveData&lt;UserDTO?&gt; get() = _userData

fun getUserData(userID: String) {
      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;users&quot;).document(userID)
      docRef.get().addOnSuccessListener {
          val item = it.toObject(UserDTO::class.java)
          _userData.value = item
      }
          .addOnFailureListener {
              Timber.tag(&quot;LoadUserDataFail&quot;).d(it.toString())
          }
  }

//ProfileFragment.kt 의 initLogin() 함수 안
viewModel.userData.observe(viewLifecycleOwner) {
              val decryptedNickName = it?.nickName?.let { it1 -&gt; decrypt(it1, AES_KEY) }
              val decryptedEmail = it?.userEmail?.let { it1 -&gt; decrypt(it1, AES_KEY) }
              if (profileImgUri == null) {
                  ivProfileImg.scaleType = ImageView.ScaleType.CENTER_CROP
                  Glide.with(binding.root).load(it?.profileImage).into(ivProfileImg)
                  ivProfileImg.visibility = View.VISIBLE
                  if (llEditConfirm.visibility == View.GONE) {
                      tvProfileName.textSize = 20f
                      tvProfileName.text = decryptedNickName
                  }
                  tvProfileEmail.text = decryptedEmail
              }
          }</code></pre>
<ul>
<li>UserDTO의 경우, 이미 다른 파일에서도 사용하고 있는 곳들이 있어서, 기존에 생성된 것을 그대로 활용하여 사용하기로 했다. 파이어스토어에있는 유저 데이터 필드 이름과 동일하게 작성되어있다.</li>
<li><code>ProfileViewModel.kt</code>에서는 <code>toObject</code>함수를 통해서 파이어스토에서 가져온 정보를 UserDTO 인스턴스로 전환하여 저장하고 그것을 <code>_userData</code>에 담아서 저장한다.</li>
<li>그리고 <code>ProfileFragment.kt</code>에서 <code>viewModel.userData.observe</code> 유저데이터를 옵저빙 하여 화면에 뿌려준다. 이때, 닉네임(이름)과 이메일 부분은 개인정보에 가깝기 때문에 파이어 스토어에 저장될 때 암호화 처리를 해주었기 때문에 복호화해서 가져오는 작업을 해줬다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 15]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-15</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-15</guid>
            <pubDate>Wed, 20 Mar 2024 06:00:31 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240320-수요일-tiltoday-i-learned-오늘-배운-것">✏240320 수요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖이미지-저장소-권한-요청-기능-구현">📖이미지 저장소 권한 요청 기능 구현</h3>
<ul>
<li>권한 추가를 하기 전에, 팀장님이 기능을 사용할 때 접근 권한 퍼미션을 받는게 필수라는 말을 하셨는데,  다른 팀의 테스트 어플을 사용할 때에는 앱 시작하자마자 권한 요청을 하고, 종종 앱 시작시 무조건 권한을 받는 경우가 있었기 때문에 튜터님에게 조언을 구하러 갔다.</li>
<li>기능을 사용할 때 접근 권한 퍼미션을 받는게 필수 사항은 아니지만 권장 사항은 맞다고 해주셨다. 다만, 자주 사용되고 중요한 부분이라고 판단될 경우 어플을 켜자마자 어디서 사용되는지 안내하면서 체크하게 해도 무방하다고도 해주셨다.</li>
</ul>
<br>

<ul>
<li><p>나의 경우 마이페이지 자체가 앱 내에서 비중있는 기능이 아니고, 특히 그 중에서도 이미지 수정은 보조 기능이기 때문에 마이페이지가 켜지자마자 나오게 하면 안되고, 수정 버튼을 눌렀을 때 퍼미션 체크를 해서 없으면 권한을 받도록 한다.</p>
</li>
<li><p>또, <code>override fun onRequestPermissionsResult</code>가 deprecated 되었기 때문에, <code>permissionLauncher</code>를 사용하여 권한을 받도록 했다.</p>
<pre><code class="language-kotlin">private fun initActivityResultContracts() {
      permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -&gt;
          if (isGranted) {
              getImg()
          } else {
              val builder = AlertDialog.Builder(requireContext())
              builder.setMessage(&quot;프로필 이미지 수정을 하시려면\n파일 및 미디어 권한을 허용해주세요&quot;)
                  .setPositiveButton(&quot;확인&quot;, DialogInterface.OnClickListener { _, _ -&gt;
                      val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(Uri.parse(&quot;package:&quot; + requireActivity().packageName))
                      startActivity(intent)
                  })
                  .setNegativeButton(&quot;취소&quot;, null)
              builder.show()
          }
      }

      imageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -&gt;
          if (result.resultCode == Activity.RESULT_OK) {
              binding.btnProfileEdit.visibility = View.GONE
              profileImgUri = result.data?.data
              binding.ivProfileImg.scaleType = ImageView.ScaleType.CENTER_CROP
              Glide.with(binding.root).load(profileImgUri).into(binding.ivProfileImg)
          }
      }
  }

  private fun checkPermissionVersion() {
      if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.TIRAMISU) {
          requestPermission(android.Manifest.permission.READ_MEDIA_IMAGES)
      } else {
          requestPermission(android.Manifest.permission.READ_EXTERNAL_STORAGE)
      }
  }

  private fun requestPermission(permission: String) {
      if (ContextCompat.checkSelfPermission(requireContext(), permission) != PackageManager.PERMISSION_GRANTED) {
          permissionLauncher.launch(permission)
      } else {
          getImg()
      }
  }

  private fun clickEditImg() {
      with(binding) {
          btnEditImg.setOnClickListener {
              checkPermissionVersion()
          }
      }
  }

  private fun getImg() {
      val intent = Intent(Intent.ACTION_PICK)
      intent.type = &quot;image/*&quot;
      imageLauncher.launch(intent)
      profileImgUri = &quot;&quot;.toUri()
  }</code></pre>
<ul>
<li>원래는 이미지 수정 버튼을 클릭하면 바로 이미지 선택 인텐트로 넘어가고, <code>imageLauncher</code>를 통해 바로 가져온 이미지를 <code>ivProfileImg</code>에 세팅해주는 방식이었는데, 
이미지 권한이 존재해야지 이미지를 가져올 수 있으므로, 우선 이미지를 가져오도록 인텐트 연결하는 부분을 <code>getImg()</code>로 분리했다. 이 때, <code>profileImgUri</code>이 null 인지 아닌지로  이미지 선택 인텐트에서 돌아올 때 유저 정보가 갱신되는지 아닌지를 판별하기 때문에 <code>&quot;&quot;</code> 빈값을 넣어서 갱신되지 않도록 해준다.</li>
</ul>
<ul>
<li><code>imageLauncher</code>는 <code>permissionLauncher</code>와 함께 초기화 되도록 <code>initActivityResultContracts</code>에 묶어서 <code>onViewCreated</code>에서 초기화 되도록 빼주었다.</li>
<li><code>clickEditImg</code>을 통해 이미지 수정 버튼을 누르면 먼저 <code>checkPermissionVersion</code>으로 안드로이드 버전에 따른 이미지 접근 권한을 갖도록 한다. 티라미수(13) 버전을 기점으로 저장소에 대한 접근 권한이 세부적(오디오,비디오,이미지)으로 바뀌었는데, 나는 프로필 이미지를 바꾸는 것이기 때문에 13버전부터는 <code>READ_MEDIA_IMAGES</code>를 통해 이미지에 대한 권한만 요구하도록하고, 그 전 버전들은 <code>READ_EXTERNAL_STORAGE</code> 저장소 권한을 그대로 사용한다.</li>
<li>권한 버전 체크를 하면 <code>requestPermission</code>에서 권한이 없는 경우 해당하는 권한을 요청하도록 <code>permissionLauncher.launch</code>를 연결한다. else를 통해 권한이 있다면 바로 이미지 선택 인텐트로 넘어가도록 <code>getImg</code>를 걸어준다.</li>
<li><code>permissionLauncher</code>에서는 <code>permissionLauncher.launch</code>를 통해 권한이 허용 되었는지 접근 결과값을 <code>isGranted</code> 로 받아서 권한이 허용되었다면 이미지 선택 인텐트로 넘어가고, 허용되지 않았다면 다시 이미지 권한을 선택할 수 있도록 다이얼로그를 생성한다.</li>
<li><blockquote>
<p>권한 체크에서 한 번 <code>허용 안함</code>을 눌러버리면 다시 권한 확인 창이 뜨지 않는다. 그래서 이미지 수정 버튼을 다시 눌러도 권한이 없다고만 나왔기 때문에, 수정을 하고 싶으면 어플을 나가서 앱 설정을 켜서 권한을 추가해줘야한다. 이런 방법으로는 어플 이탈율만 높이고 사용성이 좋지 않다고 판단했기 때문에 다이얼로그를 통해 바로 권한 설정을 할 수 있는 앱 설정 페이지로 넘기는 인텐트를 연결해주었다.</p>
</blockquote>
</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 14]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-14</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-14</guid>
            <pubDate>Tue, 19 Mar 2024 11:52:53 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240319-화요일-tiltoday-i-learned-오늘-배운-것">✏240319 화요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖ondismissed를-활용한-스와이프-삭제-기능-구현">📖<code>onDismissed</code>를 활용한 스와이프 삭제 기능 구현</h3>
<ul>
<li><p>어제 팀원들과 상의해 본 결과, 기능이 통일된 모습이 좀 더 사용성이 좋을 것 같다는 의견이 우세해서 작성한 글도 그냥 되돌리기 스낵바를 통해 삭제 지원을 하기로 했다. 이때, 누르자마자 삭제되고 되돌리기를 누르면 새롭게 아이템이 생성되는 방법이 아니고 <code>onDismissed</code>를 사용해서 스낵바가 사리진 후에 데이터베이스에서 정식으로 삭제되게 구현하는 방식으로 구현하는 것이 좋겠다는 의견을 주셔서 그 방법을 사용하기로 했다.</p>
<pre><code class="language-kotlin">override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
              val position = viewHolder.adapterPosition
              when (recyclerView) {
                  binding.rvBookmarked -&gt; {
                      val bookmarkID = bookmarkAdapter.currentList[position]
                      viewModel.removeBookmarkAdapter(bookmarkID.contentId.toString())
                      undoSnackbar = Snackbar.make(binding.root, &quot;해당 북마크를 삭제했습니다.&quot;, 5000).apply {
                          anchorView = (activity)?.findViewById(R.id.bottom_navigation)
                      }
                      undoSnackbar?.setAction(&quot;되돌리기&quot;) {
                          viewModel.undoBookmarkCamp()
                          if (binding.lineBookmarked.visibility == View.VISIBLE) {
                              if (position == 0) {
                                  binding.rvBookmarked.post { binding.rvBookmarked.smoothScrollToPosition(0) }
                              }
                          } else {
                              binding.rvBookmarked.visibility = View.GONE
                          }
                      }
                      undoSnackbar?.show()
                      val snackbarCallBack = object : BaseTransientBottomBar.BaseCallback&lt;Snackbar&gt;() {
                          override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
                              super.onDismissed(transientBottomBar, event)
                              if (event != DISMISS_EVENT_ACTION) {
                                  viewModel.removeBookmarkDB(userId.toString())
                              }
                          }
                      }
                      undoSnackbar?.addCallback(snackbarCallBack)
                  }

                  binding.rvWriting -&gt; {
                      val postID = postAdapter.currentList[position]
                      viewModel.removePostAdapter(postID.postId.toString())
                      undoSnackbar = Snackbar.make(binding.root, &quot;해당 작성 글을 삭제했습니다.&quot;, 5000).apply {
                          anchorView = (activity)?.findViewById(R.id.bottom_navigation)
                      }
                      undoSnackbar?.setAction(&quot;되돌리기&quot;) {
                          viewModel.undoPost()
                          if (binding.lineWriting.visibility == View.VISIBLE) {
                              if (position == 0) {
                                  binding.rvWriting.post { binding.rvWriting.smoothScrollToPosition(0) }
                              }
                          } else {
                              binding.rvWriting.visibility = View.GONE
                          }
                      }
                      undoSnackbar?.show()
                      val snackbarCallBack = object : BaseTransientBottomBar.BaseCallback&lt;Snackbar&gt;() {
                          override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
                              super.onDismissed(transientBottomBar, event)
                              if (event != DISMISS_EVENT_ACTION) {
                                  viewModel.removePostDB()
                              }
                          }
                      }
                      undoSnackbar?.addCallback(snackbarCallBack)
                  }
              }

          }</code></pre>
<ul>
<li><code>onDismissed</code>를 콜백함수로 걸어서 그 안에서 <code>removeBookmarkDB</code> <code>removePostDB</code>를 통해 데이터베이스에서 삭제되도록 로직을 변경했다. 그러나 그냥 onDismissed를 냅다 걸어버리면, 되돌리기를 눌러서 삭제된 아이템을 복구하더라도 스낵바가 사라진 것이기 때문에 데이터베이스에서 삭제되는 현상이 발생했다. 따라서 <code>if (event != DISMISS_EVENT_ACTION)</code>으로 액션버튼(여기서는 되돌리기)를 누르지 않고 스낵바가 사라졌을 때 즉, 시간이 지나서 자동으로 사라졌을 때, 페이지가 바뀌어서 스낵바가 사라졌을 때 등의 상황에서만 데이터베이스에서 삭제되도록 만들었다.</li>
</ul>
</li>
<li><p>따라서 뷰모델에서도 어댑터에서만 사라지게 보이는 함수, 되돌리기 함수, 데이터베이스에서 삭제되는 함수 3가지로 나눠서 작성했다.</p>
<pre><code class="language-kotlin"> private val _bookmarkedList: MutableLiveData&lt;List&lt;CampEntity&gt;&gt; = MutableLiveData()
  val bookmarkedList: LiveData&lt;List&lt;CampEntity&gt;&gt; get() = _bookmarkedList
  private val _postList: MutableLiveData&lt;List&lt;PostDTO&gt;&gt; = MutableLiveData()
  val postList: LiveData&lt;List&lt;PostDTO&gt;&gt; get() = _postList
  private var removeBookmarkItem: CampEntity? = null
  private var removeBookmarkIndex : Int? = null
  private var removePostItem: PostDTO? = null
  private var removePostIndex: Int? = null

fun removeBookmarkAdapter(contentID: String) {
      _bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
          removeBookmarkItem = find { it.contentId == contentID }
          removeBookmarkIndex = indexOf(removeBookmarkItem)
          remove(removeBookmarkItem)
      } ?: mutableListOf()
  }

  fun removeBookmarkDB(userID: String) {
      val db = FirebaseFirestore.getInstance()
      val docRef =  db.collection(&quot;users&quot;).document(userID)
      docRef.update(&quot;bookmarked&quot;,FieldValue.arrayRemove(removeBookmarkItem?.contentId))
  }

  fun undoBookmarkCamp() {
      _bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
          removeBookmarkItem?.let {
              if (removeBookmarkIndex != null &amp;&amp; removeBookmarkIndex!! in 0 until size) {
                  add(removeBookmarkIndex!!, it)
              } else {
                  add(it)
              }
          }
      } ?: mutableListOf()
  }

  fun removePostAdapter(postID: String) {
      _postList.value = _postList.value?.toMutableList()?.apply {
          removePostItem = find { it.postId == postID }
          removePostIndex = indexOf(removePostItem)
          remove(removePostItem)
      } ?: mutableListOf()
  }

  fun removePostDB() {
      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;posts&quot;)
      docRef.whereEqualTo(&quot;postId&quot;, removePostItem?.postId).get().addOnSuccessListener {
          for (doc in it) {
              doc.reference.delete()
          }
      }
  }

  fun undoPost() {
      _postList.value = _postList.value?.toMutableList()?.apply {
          removePostItem?.let {
              if (removePostIndex != null &amp;&amp; removePostIndex!! in 0 until size) {
                  add(removePostIndex!!, it)
              } else {
                  add(it)
              }
          }
      } ?: mutableListOf()
  }</code></pre>
<ul>
<li><code>removeBookmarkAdapter</code> <code>removePostAdapter</code>를 통해 어댑터와 연결된 라이브 데이터 리스트에서만 해당 아이템이 삭제되도록 했다. 이때, 되돌리기의 경우를 고려하여 삭제한 아이템의 정보(remove<del>Item)와 위치(remove</del>Index)도 저장해두었다.</li>
<li><code>undoBookmarkCamp</code> <code>undoPost</code>의 경우에는, 삭제된 아이템의 위치를 기억해서 되돌렸을 때, 삭제했었을 때의 위치로 되돌아갈 수 있도록 만들어주었다.</li>
<li><code>removeBookmarkDB</code> <code>removePostDB</code>는 사용하는 파이어스토어 데이터베이스에서 삭제되도록 저장한 remove~Item에서 일치하는 값을 찾아서 삭제 되도록 만들어주었다.
이때, 북마크 캠핑장의 경우는 북마크 필드 안에 리스트로 저장한 값을 삭제하는 것이기 때문에 <code>docRef.update</code> 함수를 사용해주었지만, 작성한 글의 경우는 해당 문서(document) 전체를 삭제하는 것이기 때문에 <code>doc.reference.delete()</code>를 사용해주었다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="📖글라이드-requirecontext에-대한-javalangillegalstateexception-오류-수정">📖글라이드 <code>requireContext()</code>에 대한 <code>java.lang.IllegalStateException</code> 오류 수정</h3>
<ul>
<li><p>바텀네비게이션 이동 중에, ProfileFragment에서 사용중인 Glide에 대해서 <code>java.lang.IllegalStateException</code>가 발생했다. 찾아보니까 <code>Glide.with(requireContext()).load(it.getString(&quot;profileImage&quot;)).into(ivProfileImg)</code>에서 <code>requireContext()</code> 호출에 관련된 문제라고 했다.
=&gt; 처음에는 예외처리를 해줘야한 다는 해결방법을 듣고, try-catch같은 예외 처리를 해줘야한다는줄 알았는데, 글라이드를 사용할 때 그 정도로 예외처리 해주는 방법은 보지 못했고, 기능에 비해 코스트가 과하다는 생각이 들어서 다른 방법을 좀 더 찾아보았다.
=&gt; detach될 때 특히 문제인 것 같아서 Glide를 onDestroyView에서 같이 clear될 수 있도록 <code>Glide.with(requireActivity()).clear(binding.ivProfileImg)</code>을 걸어주었는데, 이래도 똑같은 오류가 발생했다.
=&gt; 결국 그냥 requireContext나 requireActivity를 사용하는 방법 대신 <code>binding.root</code>로 뷰를 참조하도록 바꾸었다.</p>
</li>
<li><p>안드로이드에서 context는 어플리케이션 혹은 액티비티에 대한 포괄적인 정보를 지니고 있는 객체이므로 항상 밀접한 스코프 범위의 context를 골라 사용해야한다는 주의를 듣긴 들었었는데, 이렇게 직접 문제가 발생하니까 까다롭다는 것을 실감하는 것 같다... 이번 프로젝트 이후에 context에 대해서도 좀 더 명확한 정리를 해둘 필요를 느꼈다.</p>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점과-내일-할-일">✏ 느낀 점과 내일 할 일</h3>
<ul>
<li><p>오늘은 스와이프 기능을 보완해서 PR을 완료했다. 이전에 단순하게 전화 걸기 인텐트 연결 이 정도로 사용하는 것만 봤었어서 이렇게 삭제 기능을 만들 때 저런 callback 함수들이 있는 줄 몰랐다. 하다보면 늘 이렇게 경험이 부족한게 느껴진다. 그래도 알려주신 것을 바탕으로 쉽게 만들 수 있는 기능이어서 바로 마무리할 수 있었다. 다행이다.</p>
</li>
<li><p>오늘 예상치 못한 오류를 만나서 시간을 좀 썼다. 위에도 작성했지만, 얼마 전에 받은 예상 기술 면접 질문에서 context관한 이야기가 없었다면, 어떻게 처리해야할지 감도 아예 못올뻔했다. 다만, 그 때 정리한다고 했는데도 부족했던 것 같아서 이번 프로젝트가 끝나면 꼭 한 번 제대로 봐야겠다. 요즘 코드 마감하느라 시간이 없다는 핑계로 자꾸 하나씩 미뤄두는게 생기는 것 같아서 아쉽다.. 근데 실제로 오늘 이미지 저장 권한 코드는 작성 시작도 못해서..ㅎㅎ</p>
<br>
</li>
<li><p>또, 오늘 회의 시간과 별개로 마감을 위한 중간 MVP 점검을 했는데, 아직 구현되지 못한 채팅과 연관된 유저디테일정보페이지를 만드는 것 보다는 다른 팀원의 스코프로 있던 구글 로그인 구현 기능이 더 중요하다고 판단되어서, 프로필 화면이 일단락된 내가 빠르게 구글 로그인을 진행하는 것이 좋겠다는 판단이 있었다. 나도 충분히 동의하고 원래 스코프를 가지고 있던 팀원 분도 코드 마감을 위해 아직 마감 못한 구현 중 기능을 정리하고 싶다고 하셔서 내가 빠르게 진행하기로 했다.</p>
</li>
<li><p>따라서 내일은 꼭! 이미지 저장 권한에 관련된 로직을 완성하고, 구글 로그인을 연결해야 겠다. 다음주에는 최종 발표 준비를 하느라 비공개 테스트를 완료하고 진짜 배포를 하려면 금요일에는 꼭 마무리가 되어야한다고 했다. 마감을 지키기 위해 더 파이팅해야겠다.!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 13]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-13</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-13</guid>
            <pubDate>Mon, 18 Mar 2024 10:41:22 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240318-월요일-tiltoday-i-learned-오늘-배운-것">✏240318 월요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖-스와이프-삭제-기능-구현하기">📖 스와이프 삭제 기능 구현하기</h3>
<ul>
<li><p>북마크 캠핑장/ 작성한 글 탭에서는 각 아이템에 대해 좌방향 스와이프를 통한 삭제 기능을 넣기로 했다.</p>
</li>
<li><p>사용성을 고려했을 때, 삭제는 보통 바로 삭제되지 않고 중간에 한 번 더 &#39;삭제하시겠습니까?&#39; 같은 다이얼로그로 삭제 결정에 대해 되돌릴 수 있도록 만드는데, 우리 팀은 마이페이지에서 간편하게 스와이프로 삭제 하기 때문에 다이얼로그를 띄우기보다는 스낵바를 통해 &#39;되돌리기&#39;기능을 추가하기로 했다.</p>
<pre><code class="language-kotlin">//ProfileFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
      super.onViewCreated(view, savedInstanceState)
      ...
      swipeRecyclerView(binding.rvBookmarked)
      swipeRecyclerView(binding.rvWriting)
      ...
  }

private fun swipeRecyclerView(recyclerView: RecyclerView) {
      val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
          //드래그앤드롭에 관한 함수. 사용하지 않기 때문에 SimpleCallback 매개변수 0, false return
          override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
              return false
          }

          override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
              val position = viewHolder.adapterPosition
              when (recyclerView) {
                  binding.rvBookmarked -&gt; {
                      val bookmarkID = bookmarkAdapter.currentList[position]
                      viewModel.removeBookmarkCamp(userId.toString(), bookmarkID.contentId.toString())
                      val undoSnackbar = Snackbar.make(binding.root,&quot;해당 북마크를 삭제했습니다.&quot;,5000)
                      undoSnackbar.setAction(&quot;되돌리기&quot;){
                          viewModel.undoBookmarkCamp(userId.toString())
                      }
                      undoSnackbar.show()
                  }
                  binding.rvWriting -&gt; {
                      //되돌리기가 아니고 진짜 삭제하겠냐고 묻는 다이얼로그 필요
//                        val undoSnackbar = Snackbar.make(binding.root,&quot;해당 작성 글을 삭제했습니다.&quot;,5000)
//                        undoSnackbar.setAction(&quot;되돌리기&quot;){
//                            viewModel.undoPost()
//                        }
//                        undoSnackbar.show()
                      val postID = postAdapter.currentList[position]
                      val builder = AlertDialog.Builder(requireContext())
                      builder.setMessage(&quot;정말로 삭제하시겠습니까?&quot;)
                          .setPositiveButton(&quot;삭제&quot;,DialogInterface.OnClickListener { _, _ -&gt;
                              viewModel.removePost(postID.postId.toString())
                          })
                          .setNegativeButton(&quot;취소&quot;,DialogInterface.OnClickListener { _, _ -&gt;
                              postAdapter.notifyDataSetChanged()
                          })
                      builder.show()
                  }
              }
          }
          override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {
              val icon: Bitmap
              if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
                  val itemView = viewHolder.itemView
                  val height = (itemView.bottom - itemView.top).toFloat()
                  val width = height / 4
                  val paint = Paint()
                  if (dX &lt; 0) {
                      paint.color = Color.WHITE
                      val background = RectF(itemView.right.toFloat() + dX, itemView.top.toFloat(), itemView.right.toFloat(), itemView.bottom.toFloat())
                      c.drawRect(background, paint)
                      icon = BitmapFactory.decodeResource(resources, R.drawable.ic_delete)
                      val iconTop = itemView.top.toFloat() + (height - width) / 2
                      val iconRight = itemView.right.toFloat() - width + dX
                      val iconDst = RectF(iconRight, iconTop, iconRight + width, iconTop + width)
                      c.drawBitmap(icon, null, iconDst, null)
                  }
              }
              super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)
          }
      })
      itemTouchHelper.attachToRecyclerView(recyclerView)
  } 
</code></pre>
<ul>
<li>북마크 캠핑장/ 작성한 글 각각의 리사이클러뷰에서 스와이프가 적용되기 때문에 각 리사이클러뷰를 매개변수로 받아서 작동하도록 <code>swipeRecyclerView</code>함수를 만들었다.</li>
<li><code>onSwipe</code>에서 when문을 통해 어떤 리사이클러뷰가 들어오는지 확인하고 각각에 맞는 함수가 동작되도록 구현했다.</li>
<li>[트러블슈팅] 북마크 캠핑장의 경우는 삭제하고 되돌리는 정보가 캠핑장의 <code>contentId</code> 뿐이라서(contentId를 통해 상세 정보를 불러오는 로직) 삭제와 되돌리기 시에도 문제가 없었는데, 작성한 글의 경우는 직접 글의 모든 정보를 한번에 저장하여 사용하다보니 되돌리기를 하면 글이 다시 생성되는 게시 시간의 문제도 있고, 다시 생성된 게시글의 게시물Id가 달라져서 상세페이지로 이동되었을 떄 정보가 제대로 로드되지 않았다.
=&gt; 따라서 작성한 글을 삭제할 때는 되돌리기가 아니라 한 번 더 선택할 수 있도록 다이얼로그를 통해 삭제하는 방식을 달아놨다. 또, 북마크와 달리 작성한 글의 경우는 실제로 다이얼로그를 통한 삭제 방식이 일반적인 모양이라고도 생각하기도 해서 이따 저녁 회의 때 팀원들에게 보여주고 괜찮다고 하면 이대로 PR을 올릴 것이다.</li>
</ul>
</li>
<li><p>뷰모델에서는 파이어스토어와 연결하여 <code>remove~</code>함수를 통해 바로 삭제 하고 <code>undo~</code>함수를 통해 삭제한 아이템이 다시 추가될 수 있게 만들어주었다. </p>
<pre><code class="language-kotlin">//ProfileViewModel.kt
fun removeBookmarkCamp(userID: String, contentID: String) {
      _bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
          removeBookmarkItem = find { it.contentId == contentID }
          remove(removeBookmarkItem)
      } ?: mutableListOf()
      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;users&quot;).document(userID)
      val updateBookmarkList = mutableListOf&lt;String&gt;()
      _bookmarkedList.value?.forEach {
          updateBookmarkList.add(it.contentId.toString())
      }
      docRef.update(&quot;bookmarked&quot;, updateBookmarkList)
  }
  fun undoBookmarkCamp(userID: String) {
      _bookmarkedList.value = _bookmarkedList.value?.toMutableList()?.apply {
          removeBookmarkItem?.let { add(it) }
      } ?: mutableListOf()
      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;users&quot;).document(userID)
      val updateBookmarkList = mutableListOf&lt;String&gt;()
      _bookmarkedList.value?.forEach {
          updateBookmarkList.add(it.contentId.toString())
      }
      docRef.update(&quot;bookmarked&quot;, updateBookmarkList)
  }

  fun removePost(postID: String) {
      _postList.value = _postList.value?.toMutableList()?.apply {
          removePostItem = find { it.postId == postID }
          remove(removePostItem)
      } ?: mutableListOf()

      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;posts&quot;)
      docRef.whereEqualTo(&quot;postId&quot;,removePostItem?.postId).get().addOnSuccessListener {
          for(doc in it) {
              doc.reference.delete()
          }
      }
  }
//    fun undoPost() {
//        _postList.value = _postList.value?.toMutableList()?.apply {
//            removePostItem?.let { add(it) }
//        } ?: mutableListOf()
//
//        val db = Firebase.firestore
//        val docRef = db.collection(&quot;posts&quot;).document()
//        val postItem = hashMapOf(
//            &quot;authorId&quot; to removePostItem?.authorId,
//            &quot;authorName&quot; to removePostItem?.authorName,
//            &quot;authorProfileImageUrl&quot; to removePostItem?.authorProfileImageUrl,
//            &quot;content&quot; to removePostItem?.content,
//            &quot;imageUrls&quot; to removePostItem?.imageUrls,
//            &quot;postId&quot; to removePostItem?.postId,
//            &quot;timestamp&quot; to removePostItem?.timestamp,
//            &quot;title&quot; to removePostItem?.title
//        )
//        docRef.set(postItem)
//    }</code></pre>
</li>
</ul>
<br>

<h3 id="📖앱-내에서-로그인-세션-유지-방법-변경">📖앱 내에서 로그인 세션 유지 방법 변경</h3>
<ul>
<li><p>기존에는 간편로그인(카카오UserApiClient)에서 제공해주는 토큰을 인식하여 로그인하는 중을 인식하고 있었으나, 화면을 그릴 때마다 매번 비동기방식으로 토큰을 조회하는 것도 비효율적이고, 구글 로그인 방식도 도입해야하고, 좀 더 각각의 프레그먼트 단위가 아닌 액티비티 단위에서 한 번에 컨트롤 할 수 없을지 고민하고 있어서 튜터님을 방문해 조언을 구하기로 했다.
=&gt; 결론적으로는 어플에서는 웹과 다르게 따로 쿠키나 세션을 유지해주는 기능이 없기 때문에 로그인이 필요한 항목에서 로그인 중임을 확인해주는 것이 보통이라는 피드백을 주셨다.
다만. 지금처럼 계속 간편로그인의 토큰을 인식하는 것은 비효율적이라고 해주셨는데, 첫번째로는 우리 어플을 사용할 때 굳이 간편로그인에서 제공해주는 (지금은)12시간 유지 토큰이 의미가 없다고 하셨다. 12시간 후에 토큰이 만료되는 것과 상관없이 한 번 로그인을 하면 계속 기능을 사용할 수 있기 때문에 SharedPreferences에 유저정보를 저장해서 그것을 확인하는 것이 좋다고 해주셨다.
=&gt; 팀원들과 상의를 해본 결과, 유저 아이디를 SharedPreferences에 저장해서 로그인 중임을 확인하도록 바꾸기로 하고, 그 과정에서 유저 정보 암호화를 위해 일반적인 SharedPreferences가 아닌 <code>EncryptedSharedPreferences</code>를 사용하기로 했다.</p>
<pre><code class="language-kotlin">object EncryptedPrefs {
  lateinit var sharedPreferences: SharedPreferences
  private const val PREFS_FILE_NAME = &quot;user_prefs&quot;

  fun initialize(context: Context) {
      if (!::sharedPreferences.isInitialized) {
          val masterKey = MasterKey.Builder(context.applicationContext)
              .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
              .build()

          sharedPreferences = EncryptedSharedPreferences.create(
              context.applicationContext,
              &quot;user_prefs&quot;,
              masterKey,
              EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
              EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
          )
      }
  }

  fun saveMyId(token: String) {
      sharedPreferences.edit().putString(&quot;myID&quot;, token).apply()
  }

  fun getMyId(): String? = sharedPreferences.getString(&quot;myID&quot;, null)

  fun deleteMyId() {
      sharedPreferences.edit().remove(&quot;myID&quot;).apply()
  }
}</code></pre>
<ul>
<li>object로 선언해서, 로그인할 때 <code>saveMyId</code>를 사용하여 userId를 저장하고, 로그인 여부를 확인할 때에는 <code>getMyId</code>로 저장된 userId를 가져와서 유저 정보를 확인하고, <code>deleteMyId</code>를 통해서 로그아웃하면 로그인 정보를 유지할 수 없게 userId를 삭제한다.</li>
</ul>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점과-내일-할-일">✏ 느낀 점과 내일 할 일</h3>
<ul>
<li><p>이제 이번주말이면 코드작성이 마감이 된다. 목요일 까지 기능 마감을 하고 금요일에는 최종 배포를 하자고 했다. 프로필 화면을 마무리하면 유저 디테일 화면을 만들어야하는데, 유저 디테일 화면은 프로필 화면을 재활용하는 것이나 다름 없기 때문에 금방 만들 수 있을 것 같다. </p>
</li>
<li><p>다만, 기존 기능들에 대해서 좀 더 오류에 대해 안정적이기를 팀장님이 바라셔서 사용성 면에서도 그렇고. 생각해보니까 이미지 사용 권한도 추가해줘야하고.. 유저 데이터 암호화부터.. 저번에 말한 메모리릭 문제나.. 그런거에 대해서 더 다듬는다고 생각하면 남은 시간이 긴 것도 아닌 것 같다...ㅎㅎ</p>
<br>
</li>
<li><p>내일은 아까 말했던 것 처럼 스와이프 기능을 최종적으로 PR 해야한다. 스와이프 기능까지 추가하면 기획 단계에서 정한 최종 MVP를 충족하게 된다.ㅎㅎ</p>
</li>
<li><p>물론.. 이미지 저장권한에 관한 디테일을 좀 다듬어야하기 때문에 내일은 이미지 저장 권한 추가도 해주어야한다.</p>
</li>
<li><p>디자인 등에 대한 피드백에서는 내 부분에서 크게 고칠 일이 없기 때문에 좀 더 기능 구현에 집중할 수 있을 것 같다.! 정말 얼마 남지 않았다. 남은 시간 까지 정신 빠짝 차리고 화이팅해야겠다. 파이팅!!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 12]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-12</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-12</guid>
            <pubDate>Mon, 18 Mar 2024 10:39:17 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240314-목요일-tiltoday-i-learned-오늘-배운-것">✏240314 목요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖-프로필에서-작성한-글-확인하기">📖 프로필에서 작성한 글 확인하기</h3>
<ul>
<li><p>최종 MVP에서는 북마크 뿐만 아니라, 내가 작성한 글도 프로필 화면에서 조회가 가능하다. 원래는, 북마크와 똑같이 유저 데이터베이스 자체에서 작성한 글의 아이디를 가지고 있다가 그 글아이디를 통해 목록을 불러오는 형태로 만들려고 했으나, 게시판/포스트 담당 팀원분이 이미 posts 데이터베이스 안에 유저아이디를 포함시킨 것을 보고, posts에서 일치하는 유저ID의 목록을 직접 가져오는 방식으로 구현하기로 결정하였다.</p>
<pre><code class="language-kotlin">//ProfileViewModel.kt
fun getPosts(userID: String) {
      val db = Firebase.firestore
      val baseQuery: Query = db.collection(&quot;posts&quot;)
      val result = baseQuery.whereIn(&quot;authorId&quot;, listOf(userID))
      writingPost.clear()
      result.get().addOnSuccessListener {
          for (doc in it) {
              val post = doc.toObject(Post::class.java)
              writingPost.add(post)
          }
          _postList.value = writingPost
      }
  }

//ProfileFragment.kt
private fun setPostAdapter(userId: String) = with(binding) {
      rvWriting.adapter = postAdapter
      val layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, true)
      rvWriting.layoutManager = layoutManager
      viewModel.getPosts(userId)
      viewModel.postList.observe(viewLifecycleOwner) {
          postAdapter.submitList(it.toList())
          if (it.isNotEmpty()) {
              tvWritingSize.text = it.size.toString()
              tvWritingSize.visibility = View.VISIBLE
          }else {
              tvWritingSize.text = it.size.toString()
              if(lineWriting.visibility == View.VISIBLE){
                  tvTabWriting.visibility = View.VISIBLE
              }
          }
          layoutManager.scrollToPosition(it.size-1)
      }
  }</code></pre>
<ul>
<li>북마크 때에는 <code>_bookmarkedList</code>가 누적되지 않도록 해당 유저의 북마크 아이디를 조회한 후 해당 아이디의 전체 아이템을 담기 전에 리스트를 비워줬었는데, 이번에는 그런 과정 없이 바로 직접 가져오다 보니까 언제 어떻게 리스트를 비워줘야할지 고민이 있었다.저번에 처음 시도해봤던 것 처럼 <code>_postList.value</code>를 <code>emptyList()</code>로 초기화하는 방법을 이번에도 시도해봤지만 통하지 않았다.
그래서 이번에도 전체 객체를 담기 전에 clear할 수 있도록 만들어주었다. 이렇게 만들고 보니까 <code>callBookmarkCamp()</code> 에서도 같은 위치에서 clear해줘도 충분했겠구나 싶었다.</li>
</ul>
</li>
<li><p>작성한 글을 연결하고 나니까, 같은 영역을 공유하고있는 북마크 캠핑장 탭 부분의 리사이클러뷰가 다른 화면을 다녀오면 틀어져있었다. 또, 다른 화면을 갔다오면 작성한 글의 리사이클러뷰가 한번에 활성화가 안되는 문제들이 있었다. 때문에, 클릭 시 흐름 제어를 다시 한 번 점검 후 수정해주고, 에뮬레이터 문제인지 확인을 하기 위해서 실기기로도 테스트를 해주었다.
=&gt; 팀원들과 CPU메모리와 JSON 표준양식에 대해서 공통적으로 계속 같은 문제를 겪고 있다는 사실을 알게 되었는데, 일단 고질적으로 겪고 있던 메모리 누수 문제에 대해서 <code>App Profiler</code>나 <code>카나리아</code> 사용에 대해 권유 받아서 팀장님이 직접 확인을 해보기로 하셨다.</p>
</li>
</ul>
<br>

<h3 id="📖안드로이드-스튜디오-commit---shelve기능">📖안드로이드 스튜디오 Commit - Shelve기능</h3>
<ul>
<li>깃으로 stash를 하지 않아도 안드로이드스튜디오(인텔리제이) 자체에서 제공해주는 임시 저장 기능이 있다는 것을 알게 되었다.</li>
<li>commit하는 창에서 리프래시 아이콘이 있는 곳을 보면 끝에 바구니에 아래화살표로 담기는 듯한 모양을 하고있는 아이콘이 있다. 그게 shelve기능을 사용할 수 있는 아이콘이다.</li>
<li>stash처럼 클릭하면, 임시 저장이 가능하고, stash와 다른 점은 원한다면 특정적으로 원하는 파일에 한 해서 부분적으로도 임시저장이 가능하다는 것이다.</li>
<li>저장을 하면 commit 탭 옆에 shelf라는 탭이 생기는데, 그곳에서는 원하는 임시 저장 항목에 대해 담기는 것과 반대로 보이는 위로 화살표 모양의 아이콘을 누르면 다시 저장되었던 항목들이 합쳐지고, 충돌이 생기면 충돌을 해결할 수 도 있다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 11]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-11</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-11</guid>
            <pubDate>Tue, 12 Mar 2024 12:12:35 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240312-화요일-tiltoday-i-learned-오늘-배운-것--br✏240313-수요일-tiltoday-i-learned-오늘-배운-것">✏240312 화요일 TIL(Today I learned) 오늘 배운 것 ~ <br>✏240313 수요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖스토리지-생성-및-연결">📖스토리지 생성 및 연결</h3>
<h4 id="화요일-">화요일 :</h4>
<ul>
<li><p>[트러블슈팅] 중간 발표 때 프로필화면에서 프로필 이미지가 <code>resolveUri failed on bad bitmap uri</code> 오류로 로드가 안되고 있었다. 나는 이게 보안 문제 등으로 인한 변환 오류인줄만 알고 스토리지에 연결해서 암호화..된 URI가 들어오면 괜찮을 줄 알았는데, 스토리지 URI도 전혀 먹히지 않았다.
=&gt; 해결책을 구글링해보면 일단 비트맵 변환에 대한 이야기가 주로 나와서, 비트맵 변환 방식을 직접 적용해야하나 고민하다가 일단 <code>Glide</code> 사용 중이니까 일단 글라이드로 로드되도록 코드를 바꾸니까 원래 카카오이미지주소도 잘 로드가 되는 것 같다. </p>
</li>
<li><p>어쨌든, <code>resolveUri failed on bad bitmap uri</code> 문제가 아니더라도, 로컬 이미지를 가지고 올떄에는 안드로이드 자체의 보안에 의해서 <code>SecurityException</code> 이 발생하므로, 프로필 수정을 위해서는 파이어스토어의 <code>Storage</code>가 꼭 필요하다.</p>
<pre><code class="language-kotlin">//스토리지에 이미지 업로드하는 방법
fun profileImgUpload(imageURI: Uri, userID: String) {
  val storage = Firebase.storage
  val storageRef = storage.getReference(&quot;profileImage&quot;)

  if (imageURI.toString().startsWith(&quot;http&quot;)) {
      GlobalScope.launch(Dispatchers.IO) {
          //인터넷URL의 경우 putStream사용.
          storageRef.child(userID).putStream(URL(imageURI.toString()).openStream())
      }
  } else {
      //로컬 파일 업로드 시 putFile 사용
      storageRef.child(userID).putFile(imageURI)
  }
}</code></pre>
<ul>
<li><p>스토리지에 이미지를 저장하는 것은, <code>LoginActivity</code>에서 처음 접속을 할 때에도 사용하고, 프로필이미지를 수정할 때 <code>ProfileFragment</code>에서도 사용하기 때문에, 공용으로 사용할 수 있도록 FirebaseUtils.kt에 작성해 주었다.</p>
</li>
<li><p>[트러블슈팅] 처음에 만들었을 때, putFile 예제만 보고 그냥 사용했다가 오류를 만났다.
=&gt; 인터넷 URL을 사용할 때에는 putStream을 사용해야한다는 것을 구글링을 통해 알아서, 적용하였더니 정상작동했다.</p>
</li>
</ul>
</li>
<li><p>스토리지에 이미지를 업로드하면, 바로 스토리지의 이미지를 가져오는 것이 아니라 스토리지에 저장된 URI를 파이어스토어에도 저장해줘야한다. 팀장님과 상의 결과 어쨋든 파이어스토어를 백엔드용으로 겸해서 사용하고 있는 것인데, 데이터를 분산해서 가져오는 것은 아키텍쳐적으로 좋지 못한 모양새라는 점에 동의해서 저장한 스토리지의 URI를 가져와서 저장하는 방법을 사용하기로 했다.</p>
<pre><code class="language-kotlin">Toast.makeText(this, &quot;로그인에 성공하였습니다.&quot;, Toast.LENGTH_SHORT).show()
      val db = Firebase.firestore
      UserApiClient.instance.me { user, _ -&gt;

          //스토리지에 이미지를 미리 업로드
          profileImgUpload(Uri.parse(user?.kakaoAccount?.profile?.profileImageUrl), &quot;Kakao${user?.id}&quot;)

          val userModel = hashMapOf(
              &quot;nickName&quot; to &quot;${user?.kakaoAccount?.profile?.nickname}&quot;,
              &quot;profileImage&quot; to null,
              &quot;userEmail&quot; to &quot;${user?.kakaoAccount?.email}&quot;,
              &quot;bookmarked&quot; to null,
              &quot;writing&quot; to null
          )
          val documentRef = db.collection(&quot;users&quot;).document(&quot;Kakao${user?.id}&quot;)
          documentRef.get().addOnSuccessListener {
              if (!it.exists()) {
                  documentRef.set(userModel)
                  //스토리지에 업로드한 이미지 URI를 가져와서 파이어스토어에도 저장해주기
                  Firebase.storage.getReference(&quot;profileImage&quot;).child(&quot;Kakao${user?.id}&quot;).downloadUrl.addOnCompleteListener {
                      if (it.isSuccessful) {
                          val profileImgURI = hashMapOf&lt;String, Any&gt;(
                              &quot;profileImage&quot; to it.result.toString()
                          )
                          documentRef.update(profileImgURI)
                      }
                  }
              }
          }
          finish()
      }</code></pre>
<ul>
<li><p>기존에 다이렉트로 프로필URL을 저장했던 hashmap 안의 &quot;profileImage&quot;를 null처리해주고, 먼저 유저에 대한 문서를 생성해 준 후, 들어오는 이미지를 스토리지에 먼저 저장하고, 저장한 이미지의 downloadURL을 가져와서 &quot;profileImage&quot;에 넣어준 후 생성한 유저문서에 업데이트 해주는 방식.</p>
</li>
<li><p>[트러블슈팅] 스토리지에 있는 downloadURL을 바로 profileImgURI의 hashmap 안에서 저장해주려고 했다가 <code>java.lang.IllegalStateException: Task is not yet complete</code> 비동기처리 오류를 만나서 GPT의 힘을 좀 빌렸다. 작동되는 코드를 보고 있으니 내가 했던 시도가 좀 어이없긴한데, 비동기 처리에 대해서 좀 더 유의해야겠다.</p>
<br>


</li>
</ul>
</li>
</ul>
<h3 id="📖프로필-이미지-수정">📖프로필 이미지 수정</h3>
<h4 id="수요일-">수요일 :</h4>
<ul>
<li><p>스토리지를 연결했으니, 로컬에서 고른 이미지를 안전하게 저장하고 가져올 수 있게 되었다. 이를 바탕으로 프로필 수정을 눌렀을 때 프로필 이미지를 변경할 수 있도록 만들었다.</p>
<pre><code class="language-kotlin">private fun handleClickEdit(confirm: Boolean) {
      with(binding) {
          if (confirm) { //수정 확인 버튼
              UserApiClient.instance.me { user, _ -&gt;
                  val documentRef = db.collection(&quot;users&quot;).document(&quot;Kakao${user?.id}&quot;)
                  //이름수정
                  val updateNickname = hashMapOf&lt;String, Any&gt;(&quot;nickName&quot; to &quot;${tvProfileName.text}&quot;)
                  //이미지수정
                  if (profileImgUri != null) {
                      profileImgUpload(profileImgUri!!, &quot;Kakao${user?.id}&quot;)
                      Firebase.storage.getReference(&quot;profileImage&quot;).child(&quot;Kakao${user?.id}&quot;).downloadUrl.addOnCompleteListener {
                          if (it.isSuccessful) {
                              val profileImgURI = hashMapOf&lt;String, Any&gt;(&quot;profileImage&quot; to it.result.toString())
                              documentRef.update(profileImgURI)
                          }
                      }
                      //profileImgUri = null
                  }
                  documentRef.get().addOnSuccessListener {
                      documentRef.update(updateNickname)
                  }
              }
              //수정된 이름이 바로 적용되도록  작성
              tvProfileName.text = tvProfileName.text
          } else { //수정 취소버튼 눌렀을 때. 이미지와 사진이 비었다가 깨끗하게 다시 채워지도록 작성.
              profileImgUri = null
              tvProfileName.text = &quot;&quot;
              ivProfileImg.setImageURI(profileImgUri)
              initLogin()
          }

         ...
      }

  }</code></pre>
<ul>
<li>프로필 수정 버튼은 하나인데, 저장해야하는 값은 이름, 이미지 2개여서 이름만 수정하면 profileImgUri에 null값이 저장될까봐 방지하기 위해서 if문을 사용해주었다.</li>
<li>[트러블슈팅] 위에서 주석 처리한  <code>//profileImgUri = null</code>의 위치를 <code>tvProfileName.text = tvProfileName.text</code> 위로 잡았다가, <code>if (profileImgUri != null)</code>에서 아예 profileImgUri를 null로 잡아서 이미지 수정이 안되는 이슈가 있었다. profileImgUri은 imageLauncher를 통해 고르기 때문에, 전역 변수로 관리하고 있었는데, 사진을 고르고 새로 ProfileFragment가 onResume되는 때에도 profileImgUri 값을 유지하고 있었으면서, 귀신같이 확인 버튼 눌렀을 때만 null로 인식을 해서 업데이트가 안되었다.
=&gt; 위에서 말했던 것 처럼 일일이 로그를 찍어서 어디까지 profileImgUri값이 유지되는지 확인하고, null로 만드는 영향을 주는 줄을 찾아서 정상 작동하도록 위치를 옮겨주었다.
=&gt;옮겨준 위치에서 정상작동하지만 주석처리한 이유는, 일단 프로필 수정 취소를 하면 이전의 값이 제대로 로드되도록 initlogin을 할 때 profileImgUri이 null일 때만 데이터베이스에서 유저정보를 가져오도록하는데, 이때만 profileImgUri을 사용하기 때문에 다른 코드에 영향이 없다는 점과, 오히려 주석을 풀어서 활성화 시켜놓으면 다른 화면에 갔다가 돌아왔을 때 이미지가 깜빡이면서 재로드 되는 현상이 있어서 주석처리를 해놓았다. 삭제하기엔 다른 이슈가 생길지 몰라서 삭제까지는 하지 않고 주석처리만 했다.</li>
</ul>
</li>
</ul>
<hr> 

<h3 id="✏-느낀-점과-내일-할-일">✏ 느낀 점과 내일 할 일</h3>
<ul>
<li><p>새삼 프로필 화면을 만들다 보니, 당연하게 여기는 모든 것들이 일일히 코드를 작성해서 제어해줘야한다는 것을 새삼 느꼈다. 취소버튼을 눌렀을 떄, 확인 버튼을 눌렀을 때, 이름만 수정했을 때, 아무것도 수정안했지만 확인버튼 눌렀을 때 등등 경우의 수가 참 많다. 최대한 확인하고 있지만, 생각치도 못한 일이 발생할까봐 긴장하고 있다.ㅎㅎ</p>
</li>
<li><p>사실 오늘은 이름 수정 시 자동 키보드 활성화가 어느 기점으로 부터 실행이 안되어서, 어짜피 deprecated된 코드들을 활용하고 있다보니 고쳐볼려고 도전을 했다가 실패했다. 더 붙잡고 있었으면 되긴 되었을텐데 마감을 위한 우선순위 고려로 2시간 이상 잡고 있는건 아니라고 생각했기 때문에 보내주었다. 다만, 원인 확인까지는  되었는데</p>
</li>
<li><blockquote>
<p>내가 이름 수정을 AlartDialog를 사용했는데, 여기서 키보드 자동 활성화는 플러그 문제로 쉽게 활성화 되지 않는다고 했다. 또, showSoftInput의 경우는 view를 받고있는데, 이 뷰가 다이얼로그에서 좀 까다롭게 작동하는 것 같다.</p>
</blockquote>
</li>
<li><blockquote>
<p>플래그를 GPT도움을 받아서 정리하니까 키보드가 나오긴 나왔는데, 다이얼로그 뒤에 프로필프래그먼트에서 키보드가 활성화 되는 경우도 있었다. ㅎ showSoftInput안에 view문제인것같아서 잠깐 고쳐봤는데 고치면 아예 안나오고... 그래서 그냥 일단 5분기록보드에 적어놓고, 보류해두었다.</p>
</blockquote>
</li>
</ul>
<br>

<ul>
<li><p>약 2주간의 일정중에서 벌써 3일차인데, 처음 정리한거에 비해 일찍 끝나는 부분들이 있었지만, 그만큼 뭔가 신경써야할 부분들이 늘어나서 마감이 다시 초조해지기 시작했다....ㅎㅎ</p>
</li>
<li><p>어쩄든, 내일은 스크럼시간에 브리핑한대로 작성한글 탭을 완성시킬 것이다. 겸사겸사 북마크 탭에서 상세페이지를 불러오는 것에 대해 공통적인 업데이트 작업이 필요해서 팀원분이 제시해주신 코드를 보고 고쳐주어야한다.</p>
</li>
<li><p>그 외에도 리사이클러뷰 아이템 스와이프로 삭제하기, 유저디테일페이지완성하기, 아까 언급했던 키보드 활성화 문제 고치기, 세세한 디자인 피드백 등등 의 문제가 있지만 빡빡하더라도 기간내에는 충분히 해결할 수 있는 문제라고 생각한다. 마지막까지 파이팅!!!!!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin/프로그래머스] 24년 3월 2주차 코드카타정리]]></title>
            <link>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-3%EC%9B%94-2%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-3%EC%9B%94-2%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 12 Mar 2024 01:03:20 GMT</pubDate>
            <description><![CDATA[<h3 id="✏20240312-화요일">✏20240312 화요일</h3>
<h3 id="📖배열-만들기-4"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181918">📖배열 만들기 4</a></h3>
<ul>
<li>주어진 조건 그대로를 따라 변수 i를 만들어 초기값을 0으로 설정한 후,  i가 arr의 길이보다 작으면 다음 작업을 반복할 수 있도록 while문을 만든다.</li>
<li>while문 안에서 when문을 통해 제시되는 조건을 수행한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(arr: IntArray): IntArray {
      var stk: IntArray = intArrayOf()
      var i = 0
      while (i &lt; arr.size) {
          when {
              stk.isEmpty() || stk.last() &lt; arr[i] -&gt; {
                  stk += arr[i]
                  i++
              }
              stk.last() &gt; arr[i] || stk.last() == arr[i] -&gt; 
                  stk = stk.dropLast(1).toIntArray()
          }
      }
      return stk
  }
}</code></pre>
</li>
<li>맨 처음에 아무생각없이 removeLast()를 썼다가 동적배열이 아니여서 오류를 만났다.</li>
<li>다른 사람들의 풀이를 보니 스택의 특성을 살려서 작성한 풀이가 많았는데, 요즘 깃허브쓰면서 맨날 stash 할 때 pop apply를 많이 썼는데, 추가할 때 push를 쓴다는 것을 새삼 한 번 더 되새길 수 있었던 것 같다.</li>
</ul>
<hr>

<h3 id="✏20240314-목요일">✏20240314 목요일</h3>
<h3 id="📖문자열-계산하기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/120902">📖문자열 계산하기</a></h3>
<ul>
<li>띄어쓰기를 기준으로 문자열을 분리하고, answer에 식의 시작 숫자를 담아둔다.</li>
<li>for문으로 arr을 돌면서 기호에 맞춰 기호 뒤에 요소를 answer에 더하거나 뺀 후 answer을 반환한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(my_string: String): Int {
      val arr= my_string.split(&quot; &quot;)
      var answer: Int = arr[0].toInt()
      for(c in arr.indices){
          if(arr[c] == &quot;+&quot;) {
              answer += arr[c+1].toInt()
          } else if(arr[c] == &quot;-&quot;){
              answer -= arr[c+1].toInt()
          }
      }
      return answer
  }
}</code></pre>
</li>
<li>처음에는 예문이 &quot;3 + 4&quot; 뿐이라서 반복문 없이 바로<code>arr[0] arr[1] arr[2]</code>로만 따져서 <code>arr[1]</code>의 기호를 when문으로 판단해서 풀었다. 너무 쉬운데?라고 생각하자마자 테스트만 통과하고 제출하니까 실패 폭탄을 받았다.ㅎ</li>
<li><code>my_string에는 연산자가 적어도 하나 포함되어 있습니다.</code> 이 문구가 그냥 연산식 오류가 없다는 얘기인 줄 알았는데, 연산자가 둘 이상일 수도 있다는 말인줄은 몰랐다.. 여튼 오류를 발견하고 연산자가 여러개라고 해서 반복문을 통해 풀리도록 식을 바꿨다.<hr>

</li>
</ul>
<h3 id="✏20240315-금요일">✏20240315 금요일</h3>
<h3 id="📖배열-만들기-6"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181859">📖배열 만들기 6</a></h3>
<ul>
<li>주어진 조건 그대로를 따라 변수 i를 만들어 초기값을 0으로 설정한 후,  i가 arr의 길이보다 작으면 다음 작업을 반복할 수 있도록 while문을 만든다.</li>
<li>while문 안에서 when문을 통해 제시되는 조건을 수행한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(arr: IntArray): IntArray {
      var answer: IntArray = intArrayOf()
      var i = 0
      while(i &lt; arr.size) {
          when {
              answer.size == 0 || answer.last() != arr[i] -&gt; {
                  answer += arr[i]
                  i++
              }
              answer.last() == arr[i] -&gt; {
                  answer = answer.dropLast(1).toIntArray()
                  i++
              }
          }
      }
       if(answer.size == 0) {
           answer += -1
       }
      return answer
  }
}</code></pre>
</li>
<li>배열만들기4와 똑같은 문제 그 때 헤매었던 풀이를 이번엔 단번에 풀 수 있었다. 다만 배열만들기4 때도 말했던 것 처럼 역시 다음에도 비슷한 문제를 만나면 그 땐 꼭 stack을 활용하여 문제를 풀어야겠다.</li>
<li>다른 사람의 풀이를 봤을 때 java.util.*을 임포트하긴 했지만 어쨌든 push와 pop을 사용한 풀이가 깔끔해 보인다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 10]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-10</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-10</guid>
            <pubDate>Fri, 08 Mar 2024 13:27:12 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240308-금요일-tiltoday-i-learned">✏240308 금요일 TIL(Today I learned)</h3>
<h3 id="📖1차-mvp-마감-발표자료-준비">📖1차 MVP 마감 발표자료 준비</h3>
<ul>
<li>사실 어제 코드 마감인 와중에도.. 구글 플레이스토어 콘솔을 통해 앱 배포 등록 절차를 수행했다. 이전에 이미 개인정보 처리방침과, 사용 등급 등에 대한 절차를 마친 후라서 AAB 번들 파일을 만들어서 올리는 과정이 있었다. </li>
<li>중간 발표 자료는, 이전 프로젝트 최종 발표 떄와 별반 다를 거 없이 준비되었지만 최종 MVP 까지의 향후 개발 방향을 추가로 발표 구성에 넣기로 했다.</li>
</ul>
<h3 id="📖개인적인-1차-mvp회고">📖개인적인 1차 MVP회고</h3>
<ul>
<li><p>발표 자료를 제출 한 후에는 회고 시간을 가졌다. 이번주에는 솔직히 처음 지정한 스코프를 다 이루지 못하고 진짜 최소한의 스코프만 겨우 채워서 냈다. 중간에 한 번더 회의를 거쳐서 쳐낼 부분을 쳐내지 못했더라면 정리되지 못한 채로 발표준비와 배포테스트번들을 제출하게 됐을 것이다.</p>
</li>
<li><p>이번 주에 특히 아쉬운 점은, 좀 더 MVVM 패턴을 적용할 수 있엇을 것 같은데? 라는 생각이 드는 것이다. 프로필에서 북마크목록을 불러올 때에는 contentID로 전체정보를 가져오는 과정에 대해서 ViewModel로 처리했지만, 북마크 저장 기능, 유저 정보 불러오기 같은 간단한 기능들은 간단하기도 하고, 시간도.. 없어서 그냥 프래그먼트에서 바로 데이터를 불러와서 적용할 수 있도록 만들었는데, 오늘 팀원들의 1차 마감 코드를 보니까 좀 더 MVVM을 구분해서 작성할 수 있지 않았을까? 하는 아쉬움이 있었다.</p>
</li>
<li><p>이제와서 구현한 코드를 리팩토링 한다는 것은 이제 겨우 반을 채운 진행도를 생각했을 때 너무 비효율적인 행동인 것 같고, 실제로 팀장님도 그렇게 조언을 해주셨기 때문에 만약 유저정보 등에 대해서도 MVVM을 적용하고 싶다면 UserDetail에서 좀 더 구분 지어 가져오는 것을 하면 될 것 같다. </p>
</li>
<li><p>MVVM에 대해서 솔직히 좀 복잡하다는 생각을 우선적으로 가지고 있었는데, 이제 보면 MVVM은 딱히 어렵지 않은 것 같다. UserDetail로 예를 들어보자면 뷰에서 이벤트를 만들면(클릭 혹은 초기화 시 뷰모델 연결) 뷰모델에서 유저정보를 불러오는 파이어베이스 호출하는 펑션을 만들어서 그 유저 정보를 <code>&lt;UserEntity&gt;</code>모델을 만들어서 이걸 통해 가져오고, 이 값들을 라이브데이터에 담아서, 받아온 라이브데이터값을 뷰에서 뿌려주면 된다.</p>
</li>
<li><p>다른 팀원분들은 MVVM에 클린아키텍쳐도 적용하고 있어서 잠시 검색을 해봤는데 클린아키텍쳐에서 도메인이 사용되면 요즘엔 LiveData보다 Flow를 사용하는 것이 더 효율적인 방법이라는 것을 본 적이 있다. 하지만, 클린 아키텍쳐는 분리성이 명확한 장점에 비해 그 만큼 하나의 기능에 대해서 분리되는 클래스들이 많아서 오히려 복잡하지 않은 어플의 경우 그 효용성과 비용을 충분히 감당할 수 있는지 생각해보는 것도 좋은 관점이라는 튜터님의 조언이 있었다. 어쨌든, 내 지금의 경험과 맡은 부분에서 클린아키텍쳐까지는 무리인 것 같아서 다음주에는 적극적으로 MVVM을 고려하여 코드를 짜는 것을 포인트로 할 것이다.</p>
<ul>
<li>이렇게 회고하고 정리하는 타임이 있어서 다행이다. 계속 코드 작업을 진행했더라면 MVVM이고 뭐고 정신 없이 되기만 하는 코드를 작성하고 그것에 자괴감을 느꼈을 것 같다. 사실 이번에도 이렇게 코드를 되돌아보면서 좀 더 고쳐서 작성했으면 좋았을 부분들을 보며 많이 아쉬웠던 것 같다. 1차 마감은 너무 급작스럽게 챙기게 된 감이 강했어서 최종 마감은 좀 더 체계적으로 진행 될 수 있도록 많이 신경쓸 것이다.</li>
</ul>
</li>
</ul>
<h3 id="📖-최종-마감을-위해서-해야할-일">📖 최종 마감을 위해서 해야할 일</h3>
<ul>
<li><p>목요일 금요일에 실제 어플을 보고서 디자인 튜터님이 개선점 피드백을 주셨다. 그걸 바탕으로 디자인 수정이 한 번 있어야 한다.</p>
</li>
<li><p>코드 부분에서는 일단 우선적으로 파이어베이스에서 스토리지를 통해 이미지 저장하는 방법을 적용해야한다. 이미지 수정 뿐 만 아니라 사실 로그인 했을 때 저장되는 이미지도 보안 상의 문제로 비트맵 전환이 안되어서 이미지 자체를 가져오는 것이 안되고 있다.  UI 적으로 프로필 이미지를 이제와서 빼는 것은 안되고.. 딱히 스토리지 사용법은 어렵지도 않은 것 같지만, 한 이틀 정도 할애하는 것을 생각하고있다. 오랜 시간이 걸리지 않았으면 좋겠다. </p>
</li>
<li><p>북마크 목록만 가져오고 작성한 글 가져오는 것은 연결 조차 못해서,, 게시판 담당 팀원 분께 데이터 저장 시 유저정보에 작성한 글 아이디를 따로 저장할지, 아니면 게시판에 저장된 <code>authorId</code>를 통해 조회할 지에 대해 한 번 더 논의한 후 작성하면 될 것 같다.</p>
</li>
<li><p>UserDetail 정보도 xml 만 만들어 놓고 코트파일을 아예 만들지 못했기 때문에 UserEntitiy를 만들어서 위에서 다짐했던 것 처럼 좀 더 MVVM을 고려한 코드를 작성할 것이다.</p>
</li>
<li><p>사실 최종 발표일은 29일 금요일인데, 발표준비한다고 하루 빼고, 실제 배포 번들 검토 기간을 고려하고 어쩌고 하면 2주 정도 빠듯하게 남아있는 것 같다. 무사히 프로젝트를 마칠 수 있도록 화이팅해야겠다! 파이팅~</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 9]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-9</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-9</guid>
            <pubDate>Thu, 07 Mar 2024 03:30:23 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240307-목요일-tiltoday-i-learned">✏240307 목요일 TIL(Today I learned)</h3>
<ul>
<li>오늘은 1차 MVP 구현 마감날이다. MVP(Minimum Viable Product)라는 것 자체가 최소 기능 구현제품 이라는 것인데, 처음 계획을 짤 때에는 최대한의 스코프를 담아서 계획을 짜기 때문에 그 중에서 진짜 MVP만을 고른다는 것이 쉽지 않았다. 이번에는 직접 기획과 디자인을 진행해서 더욱 그렇게 느껴졌다. 하지만 마감은 다가오고 어짜피 중요한 기능을 우선 순위로 잡아서 작업했기 때문에 우선 순위로 잡은 부분에서도 진짜 진짜 핵심 부분을 잡아서 한 번 더 일정을 조정한 후 오늘 까지 코드 구현 마무리를 하기로 했다.</li>
</ul>
<h3 id="📖fix--로그인하면-유저정보-초기화-되는-문제-해결">📖Fix : 로그인하면 유저정보 초기화 되는 문제 해결</h3>
<ul>
<li>마무리를 위해 정상 작동을 확인하는 과정에서, 로그아웃 후 새로 로그인을 하면 유저 정보가 아예 초기화되어 북마크 정보도 사라지고, 이름도 초기화된다는 것을 알게되었다. 그래서 LoginActivity의 callback코드에서 로그인에 성공해서 유저정보를 등록하려고 하는 코드를 <code>if (!it.exists())</code>을 추가하여 존재하지 않은 경우에만 새롭게 추가하도록 만들어주었다. 이를 통해 파이어베이스 쓰기 비용도 꼭 필요한 경우에만 사용된다.</li>
</ul>
<br>

<h3 id="📖-프로필에서-북마크-기능-가져오기">📖 프로필에서 북마크 기능 가져오기</h3>
<ul>
<li><p>어제 CampDetail에서 북마크를 추가할 수 있는 기능을 만들었으니, 오늘은 프로필 화면에서 내가 북마크한 목록을 확인 할 수 있도록 만들었다.</p>
<pre><code class="language-kotlin">//ProfileFragment.kt
private fun setBookmarkedAdapter(userId : String) = with(binding) {
      rvBookmarked.adapter = adapter
      rvBookmarked.layoutManager = LinearLayoutManager(requireContext(),LinearLayoutManager.VERTICAL,false)
      viewModel.getBookmark(userId)
      viewModel.bookmarkedList.observe(viewLifecycleOwner){
          adapter.submitList(it)
          if(it.isNotEmpty()) {
              tvBookmarkedSize.visibility =View.VISIBLE
              tvTabBookmarked.visibility =View.GONE
              rvBookmarked.visibility = View.VISIBLE
              tvBookmarkedSize.text = it.size.toString()
          } else{
              tvTabBookmarked.visibility =View.VISIBLE
              rvBookmarked.visibility = View.GONE
              tvBookmarkedSize.text = it.size.toString()
          }
      }
  }</code></pre>
</li>
<li><p>목록은 리사이클러뷰 어댑터를 활용하여 화면에 뿌려줄 것이다. 이를 위해 ListAdapter를 만들어주었고, 그 안에서 북마크 목록의 아이템을 클릭하면 상세페이지로 넘어갈 수 있도록 아이템 클릭 이벤트도 만들어주었다.</p>
<pre><code class="language-kotlin">class ProfileBookmarkAdapter : ListAdapter&lt;CampEntity, ProfileBookmarkAdapter.Holder&gt;(diffUtil) {
  inner class Holder(val binding: ItemBookmarkedBinding) : RecyclerView.ViewHolder(binding.root) {
      fun bind(data: CampEntity) {
          with(binding) {
              if (data.firstImageUrl.isNullOrBlank()) {
                  ivCampImg.setImageResource(R.drawable.default_camping)
              } else {
                  Glide.with(binding.root).load(data.firstImageUrl).into(binding.ivCampImg)
              }
              tvCampName.text = data.facltNm
              tvCampAddr.text = data.addr1
              if (data.lctCl.toString() == &quot;[]&quot;) {
                  tvCampType.text = &quot;[일반]&quot;
              } else {
                  tvCampType.text = data.lctCl.toString()
              }
              tvCampInduty.text = data.induty.toString()

              binding.root.setOnClickListener {
                  val myData = CampEntity(
                      addr1 = data.addr1,
                      contentId = data.contentId,
                      ...
                      themaEnvrnCl = data.themaEnvrnCl
                  )

                  val intent = Intent(binding.root.context, CampDetailActivity::class.java).apply {
                      putExtra(&quot;campData&quot;, myData)
                  }
                  binding.root.context.startActivity(intent)

              }
          }
      }
  }

  companion object {
      private val diffUtil = object : DiffUtil.ItemCallback&lt;CampEntity&gt;() {
          override fun areItemsTheSame(oldItem: CampEntity, newItem: CampEntity): Boolean {
              return oldItem == newItem
          }

          override fun areContentsTheSame(oldItem: CampEntity, newItem: CampEntity): Boolean {
              return oldItem == newItem
          }
      }
  }

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
      val binding = ItemBookmarkedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
      return Holder(binding)
  }

  override fun onBindViewHolder(holder: Holder, position: Int) {
      val item = getItem(position)
      holder.bind(item)
  }
}</code></pre>
</li>
<li><p>북마크 목록을 가져오는 것은 MVVM 패턴을 활용하여 만들었다. 상세 페이지로 캠핑장 전체 데이터를 intent해서 넘겨주어야 하기 때문에, 북마크로 저장하고 있는 contentID를 통해 캠핑장 정보 자체를 담을 수 있도록 데이터 가공을 거쳐서 저장을 해야한다.</p>
<pre><code class="language-kotlin">class ProfileViewModel : ViewModel() {
  private val _bookmarkedList: MutableLiveData&lt;List&lt;CampEntity&gt;&gt; = MutableLiveData()
  val bookmarkedList: LiveData&lt;List&lt;CampEntity&gt;&gt; get() = _bookmarkedList
  private val bookmarkCamp: MutableList&lt;CampEntity&gt; = mutableListOf()

  fun getBookmark(userID: String) {
      Timber.tag(&quot;겟북마크검사&quot;).d(&quot;작동중&quot;)
      val db = FirebaseFirestore.getInstance()
      val docRef = db.collection(&quot;users&quot;).document(&quot;Kakao${userID}&quot;)
      val contentIds = mutableListOf&lt;String&gt;()
      docRef.get().addOnSuccessListener {
          if (it.exists()) {
              val bookmarkData = it.get(&quot;bookmarked&quot;) as? List&lt;*&gt;
              Timber.tag(&quot;겟북마크목록검사&quot;).d(bookmarkData.toString())
              if (bookmarkData != null) {
                  for (item in bookmarkData) {
                      contentIds.add(item.toString())
                  }
                  Timber.tag(&quot;북마크목록검사&quot;).d(&quot;$contentIds&quot;)
              }
              bookmarkCamp.clear()
              if (contentIds.isNotEmpty()) {
                  callBookmarkCamp(contentIds)
              }
          }
      }
  }

  private fun callBookmarkCamp(bookmarkedItemList: MutableList&lt;String&gt;) {
      val db = Firebase.firestore
      val baseQuery: Query = db.collection(&quot;camps&quot;)
      val result = baseQuery.whereIn(&quot;contentId&quot;, bookmarkedItemList)
      result.get().addOnSuccessListener {
          for (doc in it) {
              val camp = doc.toObject(CampEntity::class.java)
              bookmarkCamp.add(camp)
          }
          _bookmarkedList.value = bookmarkCamp
          Timber.tag(&quot;콜북마크검사&quot;).d(bookmarkCamp.size.toString())
      }
  }
}</code></pre>
<ul>
<li>[트러블슈팅] callBookmarkCamp에서 <code>whereIn</code>의 경우는 null이 들어오면 강제종료되는 이슈가 있었다. 그래서 getBookmark함수에서 <code>if (contentIds.isNotEmpty())</code>을 통해 null이 아닐 때만 callBookmarkCamp가 작동할 수 있도록 고쳐주었다.<ul>
<li>[트러블슈팅] 처음에 <code>getBookmark()</code>에서 <code>bookmarkCamp.clear()</code>를 해주지 않았더니, 매번 불러오는 북마크 리스트가 중복 누적되는 것을 발견했다.
=&gt; callBookmarkCamp에서 사용중인 bookmarkCamp를 초기화 해주지 않아서 발생하는 문제로, 처음에는 ProfileFragment의 setBookmarkedAdapter쪽에서 clear를 해주는 함수를 사용해서 초기화해보려고했으나 코드 위치를 변경해보아도 반응이 없었고, 그 다음에는 callBookmarkCamp 쪽에서 초기화하려고 해보았으나, 역시나 반응이 좋지 않았다. 고민을 하다가, getBookmark에서 북마크 목록을 아예 넘겨주기 전에 초기화를 시키니까, 누적되는 일 없이 정상작동하였다.</li>
</ul>
</li>
</ul>
</li>
</ul>
<br>

<h3 id="📖-프로필-유저정보-수정-정보-데이터베이스에-저장하기">📖 프로필 유저정보 수정 정보 데이터베이스에 저장하기</h3>
<ul>
<li>프로필 수정 기능을 화면 상에서만 가능하게하고, 데이터베이스와 연동을 아직 하지 않았었다. 유저정보가 데이터베이스에 생겼기 때문에 더 늦기 전에 프로필 수정 정보도 데이터베이스에 연결해주기로 했다.<pre><code class="language-kotlin">private fun handleClickEdit(confirm: Boolean) {
      with(binding) {
          if (confirm) {
              //Todo. 사진에 대한 변경값을 다시 스토리지를 통헤 데이터베이스에 넘겨서 저장
              UserApiClient.instance.me { user, error -&gt;
                  val documentRef = db.collection(&quot;users&quot;).document(&quot;Kakao${user?.id}&quot;)
                  val updateNickname = hashMapOf&lt;String, Any&gt;(&quot;nickName&quot; to &quot;${tvProfileName.text}&quot;)
                  //val updateProfileImg = hashMapOf&lt;String,Any&gt;(&quot;profileImage&quot; to img_URI.toString())
                  documentRef.get().addOnSuccessListener {
                      documentRef.update(updateNickname)
                      //documentRef.update(updateProfileImg)
                  }
              }
              tvProfileName.text = tvProfileName.text
              //ivProfileImg.setImageURI(img_URI)
          } else {
              tvProfileName.text = &quot;&quot;
              initLogin()
          }
          ...
          if(rvBookmarked.visibility == View.VISIBLE){
              tvTabBookmarked.visibility = View.GONE
          }
      }
  }</code></pre>
<ul>
<li>프로필에서는 닉네임과 프로필이미지가 각각 수정이 가능하므로 필드명과 변경값을 hashMap으로 저장해주어 update를 사용해 DB에 저장되도록 만들었다.</li>
<li>여기서 문제는, 사진의 경우 이미지 저장을 할 때 로컬 주소로 데이터베이스에 저장되면 보안 문제로 접근이 안되는 오류가 발생한다는 것이다. 이런 문제는 파이어베이스의 스토리지를 사용하는 것으로 해결해야하는데, 당장 오늘이 마감이므로 지금에 와서 스토리지를 연결하기엔 시간이 부족하다는 판단으로 팀원들과 상의 하여 최종 마감때 처리하는 것으로 일단 미루기로 했다. ...</li>
<li>이름 수정 취소를 누르면 initLogin을 통해서 원래 데이터베이스의 이름이 떠오른다. </li>
<li>프로필 수정 후에 다시 원래 프로필 화면으로 돌리는 과정에서 북마크 목록이 있을 때와 없을 떄의 화면이 다르므로 if문을 통해 조절해 주었다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 7]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-7</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-7</guid>
            <pubDate>Tue, 05 Mar 2024 07:53:25 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240305-화요일-tiltoday-i-learned">✏240305 화요일 TIL(Today I learned)</h3>
<h3 id="📖데이터베이스에-회원-등록">📖데이터베이스에 회원 등록</h3>
<ul>
<li><p>[트러블슈팅] 회원 등록에 앞서서,, 파이어스토어 데이터베이스에 유저정보를 저장하기 위해서 로그인 할 수 있도록 카카오 간편로그인 API를 통해 로그인을 진행하는데, 동의서에 체크를 하고 계속하기를 누르면 &#39;로그인 성공&#39;이 아닌 &#39;기타 에러&#39;가 등장하는 문제가 있었다.
=&gt; 처음에는 로그캣을 확인해서 직접적인 원인을 조회하여 고쳐야한다고 하는 글을 보았다. 로그캣을 확인해 보았더니 <code>java.io.IOException: Failed to load asset path</code>오류가 나와서 이것 때문인 줄 알고 열심히 구글링을 했는데, 도저히 해결되는 현상이 없어서 다른 팀원 분들께도 로그인 절차 진행을 부탁드려보니 로그인을 직접 연결 구현하신 팀원분을 제외하고 모두가 <code>기타에러</code> 오류가 생성되는 것을 확인했다.
=&gt; 카카오 API 간편 로그인 기타에러에 대해 좀 더 구글링을 해보니 키해시 문제가 가장 많이 등장하고 있었다. 이때, 나는 키해시 등록 절차를 해본적이 없기 때문에 로그인 연결하신 팀원분께 물어보니 키해시 등록절차를 알려주셨다.</p>
<pre><code class="language-kotlin">//메인액티비티
import com.kakao.sdk.common.util.Utility

override fun onCreate(savedInstanceState: Bundle?) {
  val keyHash = Utility.getKeyHash(this) 
  Log.d(&quot;Hash&quot;, keyHash)
}</code></pre>
<ul>
<li><p>카카오 Utility를 임포트해서 로그를찍어서 얻은 keyHash값을 전달해드렸더니 키해시 등록을 해주셨고, 그 이후로는 정상적으로 로그인이 된다!</p>
<p>+키해시의 경우는 앱이 악용이 되는 것을 방지하기 위해 정상적인 절차를 걸쳐서 진행된 어플인지 확인용이기 때문에, 개발자들은 디버그용 키해시를 일일이 등록해야하지만, 릴리즈키해시의 경우는 앱을 등록하는 스토어의 인증서 키해시를 등록하면 되므로 일반사용자들의 키해시 등록 문제에 대해서 걱정할 필요는 없다!</p>
</li>
</ul>
</li>
</ul>
<ul>
<li>카카오 로그인 callback 절차 속에서 로그인이 성공했을 때, 즉 token이 존재했을 때 <code>UserApiClient.instance.me { user, error -&gt; }</code>를 활용하여 userId를 문서의키값으로 등록하고, 기본적인 이름, 프로필이미지, 유저이메일을 hashMap으로 담아서 파이어스토어에 등록했다.<pre><code class="language-kotlin">UserApiClient.instance.me { user, _ -&gt;
    val userModel = hashMapOf(
       &quot;nickName&quot; to &quot;${user?.kakaoAccount?.profile?.nickname}&quot;,
       &quot;profileImage&quot; to &quot;${user?.kakaoAccount?.profile?.profileImageUrl}&quot;,
       &quot;userEmail&quot; to &quot;${user?.kakaoAccount?.email}&quot;,
       &quot;bookmarked&quot; to &quot;null&quot;,
       &quot;writing&quot; to null
    )
    Firebase.firestore.collection(&quot;users&quot;).document(&quot;Kakao${user?.id}&quot;).set(userModel)</code></pre>
=&gt; 여기서 UUID로 사용하는 <code>&quot;Kakao${user?.id}&quot;</code>는 ID 정보는 이메일 정보와 다르게 변경이 어렵고 또, 예민한 개인정보가 아니기 때문에 카카오 측에서도 키값으로 ID사용을 권장하고 있다. 거기에 카카오로그인, 구글로그인, 네이버로그인 등 다양한 소셜로그인을 사용하고있다면, 그 타입과+(social)ID를 활용하면 중복될 일 없는 고유한 식별키로 활용이 가능하다고 한다. 그래서 <code>로그인타입+제공되는socialID</code>를 활용하여 만들었다.</li>
</ul>
<br>

<h3 id="📖로그인-세선-유지를-통한-로그인정보-조회">📖로그인 세선 유지를 통한 로그인정보 조회</h3>
<ul>
<li><p>프로필 화면에서는 비로그인/로그인 화면의 차이가 존재하므로, 그 차이를 구분하는데에 자동로그인으로 유지되는 토큰을 활용하기로 했다. 마찬가지로 카카오에서 제공하는 <code>UserApiClient</code>를 사용하고, 거기서 userID가 존재하면 로그인 화면, 존재하지 않으면 로그아웃 화면이 나오도록 했다.</p>
<pre><code class="language-kotlin">fun checkLogin() {
      UserApiClient.instance.me { user, error -&gt;
          if (user?.id != null) {
              initLogin()
          } else initLogout()
      }
  }</code></pre>
<ul>
<li>[트러블슈팅] 로그인 체크는 비로그인/로그인을 판별하기 위한 중요한 부분이고, 로그인을 할 수 있는 곳은 꼭 프로필 페이지에만 존재하는 것이 아니기 때문에 화면이 <code>onStart()</code>될 때마다  새로 판별하도록 만들어두었다.</li>
</ul>
<p>&lt;=처음에는 프로그램 전체적으로 로그인 중인지 아닌지 조회할 수 있게 메인 액티비티에 따로 코드를 만들자는 의견이 있었지만, 그렇게 하지 않은 이유는 어짜피 UserApiClient 자체로 판별하는 방법이 그 역할을 한다고 생각했기 때문이다. 그래서 실제로 위의 코드 처럼 구현하고, <code>onCreateView()</code>에 코드를 걸어놨는데! 처음에 들어갔을 때에만 화면이 반응을 하고, 다른 화면을 돌아 다니다가 프로필화면으로 들어가면, 처음의 화면이 고정된 상태로 로그인/비로그인에 대한 새로운 상태를 인식하지 않았다. 하지만 직전의 프로젝트 때에도 같은 문제를 경험한 기억이 떠올라서 금방 화면이 갱신될 때마다 코드를 불러오는 onStart()에 코드를 걸었는데, onStart가. 화면의 생명주기가 전혀 먹히질 않았다.</p>
<p>=&gt;원인은 뷰페이저에 있었다. 커스텀된 바텀네비게이션라이브러리를 쓰면서, 화면 애니메이션이 부드러울 수 있도록 뷰페이저의 <code>offscreenPageLimit</code>을 팀장님이 4로 걸어놓으셨는데, 이 <code>offscreenPageLimit</code>이란게 기준 화면을 중심으로 좌우 페이지들을 설정한 개수만큼 함께 로드하는 함수로, 페이지가 캐싱되는 문제가 발생하여 페이지 갱신이 되지 않았던 것이다.</p>
<p>=&gt; 근데 사실 이걸 주석 처리하니까 다시 원래대로 페이지 갱신이 시작되긴 했는데, 어쨌든 공용으로 사용하는 코드이기도 하고 팀장님이 라이브러리 사용에 필요한 요소라서 제거하게되면 커스텀바텀네비게이션을 쓰는 이유가 사라질 것 같다는 의견을 주셨다. 그래서 다른 프래그먼트에서는 onResume onPause등이 되는 것을 바탕으로 다시 조사를 해보니, 안드로이드스튜디오에서 사용하던 버츄얼 에뮬레이터가 복잡해진 코드와 메모리를 감당하지 못하고 정상 작동하지 못하고 있다는 사실을 알게 되었다. 바로 에뮬레이터를 실기기로 옮기고 onStart에 함수롤 거니까 잘 작동하는 것을 확인했다.</p>
</li>
<li><p>[트러블슈팅] 이외에도, 유저 정보를 가져올 때 유저데이터에 자꾸 null 값이 돌아와서 왜 그런지 찾아보니까 <code>K</code>akao로 써놓고 조회를 <code>k</code>akao로 하는 휴먼에러가 있었다. 상수값을 설정해서 사용해야하는데, 일단 공동으로 관리하는 상수클래스가 없어서 깡으로 써놨더니 이꼴이 났다. 상수 페이지 생성에 대해 제안해 봐야겠다.</p>
</li>
<li><p>[트러블슈팅] 데이터베이스에 값이 없는데, 카카오토큰이 유지될 때를 고의적으로 만들어봤는데, 이미지를 불러올 때 <code>Uri.parse(it.getString(&quot;profileImage&quot;))</code>에 대해서 nullPointException 오류로 강제종료 당했다. 데이터베이스 값을 인위적으로 삭제하지 않으면 발생하지 않을 일이긴 하지만, 강제종료는 안되니까 오류코드부터 확인해보았다.
=&gt;결론은 uriString이 null인 상태에서 Uri.parse()를 호출하려고 한게 문제였다고 해서 if(it.getString(&quot;profileImage&quot;) != null)으로 잘 바꿔주었다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 6]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-6</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-6</guid>
            <pubDate>Sat, 02 Mar 2024 07:43:22 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240302-토요일-tiltoday-i-learned-오늘-배운-것--br✏240304-월요일-tiltoday-i-learned-오늘-배운-것">✏240302 토요일 TIL(Today I learned) 오늘 배운 것 ~ <br>✏240304 월요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖프로필-수정---이미지-수정">📖프로필 수정 - 이미지 수정</h3>
<h4 id="토일요일">토,일요일:</h4>
<ul>
<li><p>레이아웃 연결과 흐름 잡는 작업을 끝마치고, 유저 정보 설정에는 아직 권한이 없어서 당장 할 수 있는 기능 구현 중에 프로필 수정에 관련된 작업부터 시작했다. 그 중에서도 바로 할 수 있는 간단한 작업인 이미지 선택 기능 부터 연결해주었다.  </p>
<pre><code class="language-kotlin">private lateinit var imageLauncher: ActivityResultLauncher&lt;Intent&gt;

private fun clickEditImg() {
      with(binding) {
          btnEditImg.setOnClickListener {
              val intent = Intent(Intent.ACTION_PICK)
              intent.type = &quot;image/*&quot;
              imageLauncher.launch(intent)
          }
      }

      imageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -&gt;
          if (result.resultCode == Activity.RESULT_OK) {
              val img_URI = result.data?.data
              binding.ivProfileImg.scaleType = ImageView.ScaleType.CENTER_CROP
              binding.ivProfileImg.setImageURI(img_URI)
          }
      }
  }</code></pre>
<ul>
<li>예전에 앱 개발 입문 때 클론 코딩을 배우면서 사용했던 이미지 선택 인텐트를 그대로 활용하여 이미지 선택 어플을 연결해주었다.</li>
<li>[트러블슈팅] 원래는 이미지 선택 후 데이터베이스에 값을 넘겨서 가져온 유저정보로 프로필 이미지를 세팅해주어야하지만, 지금은 잘 가져왔는지 확인하기 위해서 바로 <code>binding.ivProfileImg.setImageURI(img_URI)</code>를 해주었다. onViewCreated안에 그냥 넣어두었다가 복잡해 보여서 빼두려고 할 때 코드의 위치를 잘 못 잡아서 늦은 초기화 오류를 만났다.
=&gt; 여기에서  imageLauncher의 위치가 제대로 <code>btnEditImg.setOnClickListener</code> 밖에 있어야 onViewCreated 안에 있는 clickEditImg() 에서 바로 접근이 가능하고, private lateinit var imageLauncher에서 늦은 초기화에 대한 오류 없이 정상 동작한다. </li>
</ul>
<br>

</li>
</ul>
<h3 id="📖프로필-수정---취소확인-버튼-리팩토링">📖프로필 수정 - 취소,확인 버튼 리팩토링</h3>
<h4 id="토일요일-1">토,일요일:</h4>
<ul>
<li>이미지 수정 기능을 만들면서 취소 확인 버튼에 대해서도 겹치는 함수가 너무 많아져서 하나로 리팩토링 할 수 있도록 먼저 로직을 정리하였다.<ul>
<li>프로필 수정 취소 -&gt; 데이터베이스 저장 없이 initLogin() 불러오고 수정버튼화면에서 없애기</li>
<li>프로필 수정 확인 -&gt; 데이터베이스에 저장하고 initLogin() 불러오고 수정버튼화면에서 없애기
=&gt; 취소 확인 버튼이 데이터베이스에 저장하고 안하고 차이니까 분리해둔 두개의 함수를 하나로 합쳐놓기.</li>
</ul>
</li>
</ul>
<ul>
<li><p>위에서 정리한 로직을 바탕으로 처음에는 if문을 취소버튼을 눌렀을 때. 확인버튼을 눌렀을 때로 바꾸려고 했는데, 그러면 그 안에 코드가 각각 들어가야하는게 마찬가지라서 공통 함수를 빼서 묶고 그 안에서 true/false로 취소 확인 버튼의 동작을 구분하기로 했다.</p>
<pre><code class="language-kotlin">private fun clickEditListener() {
      with(binding) {
          btnEditConfirm.setOnClickListener { handleClickEdit(true) }
          btnEditCancel.setOnClickListener { handleClickEdit(false) }
      }
  }

  private fun handleClickEdit(confirm: Boolean) {
      with(binding) {
          if (confirm) {
              Toast.makeText(requireContext(), &quot;데이터베이스로 저장!&quot;, Toast.LENGTH_SHORT).show()
              //Todo. 설정된 이름, 사진에 대한 변경값을 다시 데이터베이스에 넘겨서 저장
          }

       // 취소, 확인 버튼을 눌렀을 때 공통으로 적용되는 부분들 
          initLogin()
          btnEditName.visibility = View.GONE
          btnEditImg.visibility = View.GONE
          llEditConfirm.visibility = View.INVISIBLE
          tvProfileName.visibility = View.VISIBLE
      }

  }</code></pre>
<ul>
<li><code>handleClickEdit</code>함수를 생성해서 confirm을 통해 true(=확인)일 때 데이터베이스에 바꾼 이름과, 바꾼 이미지 프로필이 데이터베이스에 저장되는 로직을 넣어줄 것이다. 지금은 임시로 정상 작동 되는지 확인하기 위해 토스트 메시지를 넣어두었다.</li>
<li><code>clickEditListener</code>에서 각 버튼이 눌렸을 때 true인지 false인지 전달하여 handleClickEdit 함수를 알맞게 사용한다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="📖프로필-수정---유저-이름-수정">📖프로필 수정 - 유저 이름 수정</h3>
<h4 id="일월요일-">일,월요일 :</h4>
<ul>
<li><p>이름 수정의 경우는 카카오톡 이름 수정하는 기능에서 착안하여 EditUserName 프래그먼트를 따로 만들어서 수정이 가능하도록 기능요구서를 작성했다. 필수 MVP는 아니라서 우선순위가 <code>하</code> 이긴 하지만 유저 정보 설정에 대한 코드를 받기 전에 시간이 남기도 하고, 다이얼로그를 걸면서 어렵지 않게 구현할 수 있을 것 같아서 다이얼로그를 통해 바로 구현해보기로 했다.</p>
<pre><code class="language-kotlin">private fun clickEditName() {
      with(binding) {
          btnEditName.setOnClickListener {

              val builder = AlertDialog.Builder(requireContext())
              val editUserNameDialog = layoutInflater.inflate(R.layout.dialog_edit_user_name, null)
              builder.setView(editUserNameDialog)
              val dialog = builder.create()

              //다이얼로그 영역(기본값 화이트) 투명화로 둥근 테두리가 묻히지 않고 보이도록 설정
              dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
              dialog.show()

              val layoutParams = WindowManager.LayoutParams().apply {
                  // 다이얼로그의 크기를 화면에 꽉 차게 조절
                  copyFrom(dialog.window?.attributes)
                  width = WindowManager.LayoutParams.MATCH_PARENT
                  height = WindowManager.LayoutParams.MATCH_PARENT
                  //화면 투명도 설정 (투명0~선명1)
                  dimAmount = 0.9f
              }
              dialog.window?.attributes = layoutParams

              val etEditUserName = dialog.findViewById&lt;EditText&gt;(R.id.et_edit_user_name)
              etEditUserName.setText(tvProfileName.text.toString())
              dialog.findViewById&lt;TextView&gt;(R.id.tv_current_length).text = etEditUserName.length().toString()

              //키보드 자동활성화
              etEditUserName.requestFocus()
              val inputMethodManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
              inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)

              etEditUserName.addTextChangedListener(object : TextWatcher {
                  override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                      //입력 전 호출 메서드 (입력 하여 변화가 생기기 전)
                  }

                  override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                      //입력 중 호출 메서드 (변화와 동시에 처리)
                      dialog.findViewById&lt;TextView&gt;(R.id.tv_current_length).text = etEditUserName.length().toString()
                  }

                  override fun afterTextChanged(p0: Editable?) {
                      //입력 후 호출 메서드 (입력이 끝났을 때 처리)
                  }

              })

              with(dialog) {
                  findViewById&lt;ImageView&gt;(R.id.btn_clear_name).setOnClickListener {
                      etEditUserName.text.clear()
                  }

                  findViewById&lt;TextView&gt;(R.id.btn_edit_name_cancel).setOnClickListener {
                      //화면 취소하면 키보드 없애주기
                      inputMethodManager.hideSoftInputFromWindow(dialog.window?.decorView?.windowToken, 0)
                      dialog.dismiss()
                  }

                  findViewById&lt;TextView&gt;(R.id.btn_edit_name_confirm).setOnClickListener {
                      tvProfileName.text = etEditUserName.text.toString().replace(&quot;\n&quot;, &quot;&quot;)
                      inputMethodManager.hideSoftInputFromWindow(dialog.window?.decorView?.windowToken, 0)
                      dialog.dismiss()
                  }
              }
          }
      }
  }</code></pre>
<ul>
<li><p>당연히, EditUserName에 관한 xml을 만드는 것이 우선이지만, 카카오톡의 이름 수정 화면과 유사하기도하고 복잡하지 않으므로 따로 xml 파일까지는 여기에 올리지 않겠다.</p>
</li>
<li><p>다만, xml을 만들 때, 문자열을 clear해 줄 버튼을 drawable로 넣을지 그냥 새로 imageView위젯을 넣어줄 지 고민했었는데, drawable에 클릭 이벤트를 넣으려면 넣을 수 있지만, 코드가 너무 복잡해지는 것 같아서 다이얼로그 안에 걸꺼니까 조금이라도 덜복잡하려고 그냥 imageView를 사용해주었다.</p>
</li>
<li><p>여기서는 다이얼로그가 팝업창 형태가 아니고, 화면에 꽉차게 나오기 때문에 화면 조정해주는 코드와, 따로 백그라운드를 두지 않을 것이기 때문에 다이얼로그 연결 시 자동적으로 검게 dimed 처리되는 불투명도를 0.9정도로 높여주는 코드를 작성해주었다.</p>
</li>
<li><p>그리고, 이름 수정 기능에 바로 집중 할 수 있게 기본적으로 키보드를 활성화 시켜주었다. 원래 지금 사용하는 코드에서 <code>toggleSoftInput</code>과 <code>SHOW_FORCED</code>는 자바에서 deprecated 되었기 때문에, 다른 <code>showSoftInput</code> <code>SHOW_IMPLICIT</code>과 같은 함수를 적용해보려고 했으나, 무슨 일인지 전혀 적용이 안되어서 그냥 toggleSoftInput를 사용하기로 했다. 이 코드는 키보드가 없으면 생성해주고, 있으면 없애주는 함수이지만, 어짜피 이름 수정 버튼을 누르기 이전에는 키보드가 활성화될 일이 없기 때문에 그냥 그대로 사용하기로 했다.
+여기에 더불어서 이름 수정이 완료(취소,확인 둘다)되면 키보드를 내리는 함수도 추가해주었다.</p>
</li>
<li><p>[트러블슈팅] 앱 개발 입문 때 이후로 EditText에 텍스트 할당하는게 오랜만이라서..그런가.. 아무생각없이 TextView에 할당하듯이 <code>.toString()</code>을 썼다가 타입 미스 오류를 만났다. EditText에 문자를 할당할 때는 제대로 <code>setText</code>를 사용하여 Editable 타입에 맞출 수 있도록 하는 것을 잊지말아야겠다.</p>
</li>
<li><p>그리고 EditText를 TextView에 할당할 때도 꼭 <code>.toString()</code>을 사용해야한다. 문자열로 바꿔주지 않으면 editable 타입의 특성 때문에 글자에 밑줄이 들어간 채 TextView에 할당이 된다.</p>
</li>
<li><p>[트러블슈팅] EditText에서 또 트러블 슈팅이 있었는데, 바로 이름 수정 줄에서 엔터 줄바꿈이 먹히는 문제였다. 검색해봤을 때 <code>android:inputType=&quot;text|textNoSuggestions&quot;</code>과 <code>android:imeOptions=&quot;actionNone&quot;</code> 두개를 같이 사용하라고 나왔는데, 각각 자동완성/제안 비활성화와 입력완료동작 등의 기타 특정 동작을 정의하는 코드들인데, 설명을 듣고 <code>imeOptions</code>만 걸어주면 될 줄 알고 뻘짓을 했다가 안되는 것을 확인하고 코드 두개를 다 걸어주고 나서야 editText에서 줄바꿈이 적용되지 않았다.
=&gt; 근데 editText에서만 줄바꿈이 안보이는거지, TextView에 할당해 줄 때는 줄바꿈이 적용된 채여서, 어떻게 할까 고민하다가 제일 쉬워보이는 <code>replace(&quot;\n&quot;, &quot;&quot;)</code>를 사용하여 그냥 줄바꿈이 설령 입력되더라도 아예 없애는 방향으로 코드를 보완해주었다.</p>
</li>
</ul>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점과-내일-할-일">✏ 느낀 점과 내일 할 일</h3>
<ul>
<li><p>트러블 슈팅에 따로 적을까 하다가 여기에 적는데, 주말에 프로젝트 구조화 변경으로 모든 팀원들의 초반 구현 코드를 PR받아서 에뮬레이터를 켰는데, 로그아웃 다이얼로그가 메모리 부족으로 작동이 되질 않았다. 지도를 가져오시는 과정에서 메모리릭(메모리누수)가 나타난 것 같은데, 임시 방편으로 Manifest에서 메모리를 늘려주는.. 방법으로 고쳐주었다. 에뮬이 아닌 일반 핸드폰 기기에서는 문제 없이 어플이 작동된다는 점에서도 임시방편이기 때문에 따로 트러블슈팅을 여기에 적지는 않지만, 팀원들과의 이슈 공유 기록에는 적어놓았다.</p>
</li>
<li><p>이름 수정하는 부분을 따로 기능 정의서로 적어놓은 만큼 은근히 손이 가는 부분들이 많아서 놀랐다. 그래도 TextWatcher나 키보드 고정/내리기 같은 기능을 맨날 팀원들이 구현한 것만 보다가 직접 작성하게 되서 재미있었다.</p>
<br>
</li>
<li><p>오늘 로그인 구현 팀원분과 이야기를 나눴을 때, 데이터베이스에 유저 정보 올리는 것 까지 완성하셨다고 해서 확인해보니까 이름,닉네임,이메일만 딱 업로드 되어있는 것을 확인했다. 그래서 UUID도 넣는 것을 한 번 더 팀원들과 확인하고, </p>
<ul>
<li>UUID를 생성할 때 로컬에 가지고있다가.. 로그인에 세션이 유지되는 것이 확인되면 가져오는.. 방법이.. 괜찮을까?</li>
<li>아니면 토큰 정보로 사용자 조회를..해도 .. 될까? (뭔가 사실 이 방법은 안될것같긴하다.)</li>
<li>찾아보니까 sharedPrefernce를 쓰는 방법이 있던데, 일단 최대한 이방법을 활용해봐야겠다.</li>
</ul>
</li>
<li><p>=&gt; 그래서 내일은 로그인 세션 유지와 이 로그인 정보를 통한 마이페이지 정보 할당하기를 할 것이다. 내일 하루..만에.. 끝낼 수 있어야할텐데 싶지만 목표는 내일 완수하기.  그리고 이 다음에는 진짜 게시판 담당 팀원분과 상의해서 작성한 게시글 가져오기를 하고.. 지금 의외로 디테일 캠핑장 정보를 하고 계시는 분이 없어서.. 일단 이게 있어야 북마크한 캠핑장도 가져올 수 있어서.. 이거 두개는 좀 걱정이 되지만, 일단 여태껏 해온게 있기 때문에 어떻게든 할 수  있을 것 같다. 이번주 안에 최대한 MVP 70~80퍼를 구현하고 발표해야하는데, 그러면 일단 수요일까지 기본 사항을 잡아  놓았는데.. 수요일까지 마이페이지에 데이터베이스 정보를 할당하는 것을 마지노선으로 이틀 동안 열심히 해야겠다!@</p>
</li>
</ul>
<p>+추가적으로. 오늘 개발자 계정을 통한 플레이스토어 앱 등록 기본 세팅을 팀원들과 진행하였는데, 개인정보동의서, 사용자등급설정, 회원탈퇴정리, 데이터 관리 정보 등 의외로 신경써야할 부분들이 많아서 놀랐다. 이번에 앱 등록이 더 빡빡해졌다고 듣긴 했는데, 실제로 해보니까 쉽지 않은 것이 느껴진다. 테스터도 20명이나 모집해서 테스트를 2주간 진행해야하고, 테스트 과정에서 앱이 강제종료라도 발생하면 선처가 없다고 해서 꼼꼼한 점검이 필요할 것 같다. 예전에 비해 많이 어려워졌다고는 하는데, 그래도 확실히 기본적으로 챙겨야할 것들이라고 생각해서 단단히 준비해야겠다는 마음이다.!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 5]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-5</guid>
            <pubDate>Thu, 29 Feb 2024 12:57:32 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240229-목요일-tiltoday-i-learned-오늘-배운-것-br-✏240301-금요일-tiltoday-i-learned-오늘-배운-것">✏240229 목요일 TIL(Today I learned) 오늘 배운 것 ~<br> ✏240301 금요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖profile-ui-레이아웃-작업-2">📖Profile UI 레이아웃 작업 2</h3>
<h4 id="목요일-">목요일 :</h4>
<ul>
<li><p>수요일에는 로그인 한 프로필(마이페이지)을 보고 똑같이 따라만드는데 집중을 해서, 피그마에 그려둔 와이어프레임 모양 그대로 만드는 것에 성공했다.</p>
</li>
<li><p>이번에 구현하게 된 프로필(마이페이지)은 하나의 프레그먼트 안에서 비로그인/로그인(로그인수정포함)을 전부 보여주기 때문에 페이지 내부에서의 흐름이 어떻게 진행되는지 정리하는 것이 상당히 중요해졌다. 탭레이아웃도 보여지기만 탭레이아웃으로  보이는거지.. 사실은 버튼으로 구현하기 때문에 그에 대한 관리도 필요해졌다..</p>
</li>
<li><p>기본 페이지는 비로그인 페이지로 잡고, 로그인이 되어서 그에 대한 유저 세션이 유지되고 있다면 <code>initLogin()</code>으로 화면을 로드할 수 있도록 할 것이다.</p>
<pre><code class="language-kotlin">//로그인세션유지되어있을 때 
private fun initLogin() {
  //프로필 사진, 유저네임(크기도조정), 유저이메일 보여주기
  //로그아웃 버튼 보여주기
  //로그인 버튼 -&gt; 프로필 수정 버튼으로 바꿔주기
}</code></pre>
</li>
<li><p>로그인 후에는 수정 가능 버튼이 생성되므로, 수정버튼이  생겼을 때의 화면 전환도 설정해주었다.</p>
<pre><code class="language-kotlin">//수정버튼 눌렀을 때
private fun clickEditProfile() {
  //프로필 수정 취소 / 확인 버튼 영역 활성화
  //수정가능한 유저네임/프로필사진 수정영역으로 넘겨주는 버튼 활성화
  //로그아웃 버튼 없애기(안보여주기)
  //프로필 수정 버튼 없애기(안보여주기)
}</code></pre>
<ul>
<li><code>visibility</code>를  사용해서 위에 작성한 흐름대로 코드를 작성한 후, 화면이 원하는 대로 전환되는 것을 확인했다. 수정 취소 버튼을 눌렀을 때도 제대로 로그인한 프로필 화면이 보여지도록 추가적으로 <code>clickEditCancel()</code>함수를 만들어주었다.
이후 에도 다른 각종 버튼들이 수행해야하는 함수들을 생성하고 아직 유저데이터가 데이터베이스에 없고, 로그인 로그아웃 수행에 관해서는 담당 팀원이 따로 계시기 때문에  todo주석으로 만들어야할 코드를 관리해주었다. </li>
</ul>
</li>
</ul>
<br>

<h3 id="📖userdetail-ui-레이아웃-작업">📖UserDetail UI 레이아웃 작업</h3>
<h4 id="금요일-">금요일 :</h4>
<ul>
<li><p>당장 돌아오는 주였나..? 중간발표때였나?에 배포 테스트에 바로 들어가야하기 때문에 MVP 80% 이상의 구현을 당부하는 매니저님의 공지가 있었다. 그래서 일단 필요한 화면을 마저 완성해두기로 했다. UserDetail 화면은 프로필화면과 매우 유사하기 때문에 Profile화면을 활용하여 만들면 된다.</p>
</li>
<li><p>차이점은 바텀네비게이션이 있는 메인화면의 프래그먼트에서 보여주는 것이 아니기 때문에 어떤 페이지인지 명시해주고, 뒤로가기 버튼을 만들어줘야한다. 이것 때문에 fragment로 만들어야할지 activity로 만들어야할지 고민했는데, 일단은 fragment이름으로 생성한 후 xml파일만 만들어두었다.</p>
</li>
<li><p>UsetDetail화면은 내가 아닌 다른 유저의 프로필을 확인할 수 있는 페이지이기 때문에 팀원들과의 논의결과 이메일주소같은 개인정보는 표시하지 않기로 했다.
마찬가지로 로그아웃도 여기서는 필요없는 기능이기 때문에 빼주었다.</p>
</li>
<li><p>원래 프로필 수정으로 사용하고 있던 버튼은 메시지 보내기 버튼으로 바꿔주었다. 튜터님들의 조언에 따라 우선순위에서 밀렸지만 chat기능도 주요 기능 중에 하나이고, 유저 간의 상호작용을 위해서라도 메시지 보내기 버튼을 만들기로 했다.</p>
</li>
<li><p>나머지는 프로필과 똑같이 북마크한 캠핑장과 작성한 글을 보여주는 영역이다. 여기서는 로그인 비로그인의 차이가 없기 때문에 바로 북마크한 캠핑장을 보여주는 방식을 기본으로 잡아 활성화 시켜주었다.</p>
</li>
</ul>
<br>

<h3 id="📖logout-dialog-연결-1">📖Logout Dialog 연결 1</h3>
<h4 id="금요일--1">금요일 :</h4>
<ul>
<li><p>목요일에 화면 전환이 제대로 된 것을 확인했기 때문에, 각 버튼이 제대로 동작할 수 있도록 화면에 관한 기본 기능을 연결해두기로 했다. 우선적으로 한 것은 로그아웃 버튼을 클릭 했을 때 Logout Dialog가 연결될 수 있도록 하는 것이다. 로그아웃 자체는 아직 유지되는 세션이 없기 때문에 구현할 수 없지만, 다이얼로그로 한 번 더 확인 절차를 거칠 때에 그냥 취소로 돌아가거나, 로그아웃 버튼을 눌렀을 때 화면을 비로그인 상태로 돌려놓는 작업이 필요하다.</p>
</li>
<li><p>이번에는 이전에 만들었던 다이얼로그들과 달리 취소, 로그아웃 버튼 2개 뿐인 다이얼로그이기 때문에, 다이얼로그프래그먼트를 사용하지 않고, 그냥 함수로 다이얼로그를 걸어주기로 했다.</p>
<pre><code class="language-kotlin">private fun clickLogout() {
  with(binding) {
      btnLogout.setOnClickListener {

          val logoutDialog = layoutInflater.inflate(R.layout.dialog_logout, null)

          val dialog = AlertDialog.Builder(requireContext())
              .setView(logoutDialog)
              .create()

          //다이얼로그 영역(기본값 화이트) 투명화로 둥근 테두리가 묻히지 않고 보이도록 설정
          dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)
          dialog.show()

          dialog.findViewById&lt;TextView&gt;(R.id.btn_logout_cancel)?.setOnClickListener {
                  dialog.dismiss()
          }

          dialog.findViewById&lt;TextView&gt;(R.id.btn_logout_comfirm)?.setOnClickListener {
              //todo. 실제 로그아웃 절차 수행 &lt;- 수행시 토스트 삭제!
              Toast.makeText(requireContext(), &quot;로그아웃 되었습니다.&quot;, Toast.LENGTH_SHORT).show()

              //화면 상에서 비로그인 화면으로 되돌리기
              ivProfileImg.setImageResource(R.drawable.ic_camp)
              tvProfileName.textSize = 20f
              tvProfileName.text = getString(R.string.profile_login_text)//&quot;로그인 후 사용해주세요&quot;
              tvProfileEmail.visibility = View.GONE
              btnLogout.visibility = View.INVISIBLE
              btnGoLogin.visibility = View.VISIBLE
              btnProfileEdit.visibility = View.GONE

              dialog.dismiss()
          }
      }

  }
}</code></pre>
<ul>
<li>logoutDialog 변수에 만들어 놓은 로그아웃다이얼로그xml 파일을 할당해둔다.</li>
<li>dialog 변수에  <code>AlertDialog.Builder</code> 객체를 사용하여 <code>setView()</code>를 통해 커스텀 뷰를 설정하고, <code>create()</code>로 반환(호출)한다.</li>
<li>[트러블슈팅] 내가 만든 다이얼로그는 배경이 하얗고, 모서리가 둥근 다이얼로그였는데 바로 <code>dialog.show()</code>로 다이얼로그를 표시하니까 칼같은 네모 배경의 다이얼로그가 보여졌다.
=&gt; 구글링해보니까 <code>create()</code>로 보여지는 다이얼로그 영역 자체에 기본값으로 하얀색 배경이 들어가기 때문에 로그아웃다이얼로그xml에서 자체적으로 걸어줬던 background가 묻혀있어서 테두리가 구분이 되지 않았던 것이었다. 실제로 background를 검정색 바탕으로 확인해보니까, 검정색 둥근 모서리의 나머지 부분이 하얗게 채워지는 것을 확인했다. 그래서 배경을 투명하게 설정해주는 코드<code>dialog.window?.setBackgroundDrawableResource(android.R.color.transparent)</code> 를 넣어서 해결했다.</li>
<li>이외에도 다이얼로그의 취소 버튼과 로그아웃(확인)버튼에 <code>setOnClickListener</code>를 걸어서 클릭 이벤트를 수행할 수 있게 만들었다.</li>
</ul>
</li>
</ul>
<br>

<h3 id="📖깃허브-커밋-amend와-git-log">📖깃허브 커밋 amend와 git log</h3>
<h4 id="목금요일-">목,금요일 :</h4>
<ul>
<li><p>이번에는 깃허브 컨벤션을 신경쓰는 만큼, 나도 기능을 만들 때마다 까먹지 않고 commit을 했다. 안드로이드 스튜디오 자체에서 commit관련 창을 잘 만들어두었기 때문에 이를 활용하니까 어렵지 않게 commit을 관리할 수 있었다.</p>
</li>
<li><p>원래라면 터미널에서 <code>git commit --amend</code> 명령어를 통해서 커밋을 수정할 수 있었다면, commit 창에는 <code>Amend</code>를 체크하는 것만으로도 커밋을 쉽게 수정할 수 있다.
Amend 체크를 누르면, 커밋에 포함될 추가 파일을 고를수도있고, 커밋문구를 수정할수도 있게된다. 뿐 만 아니라, 커밋에 포함된 파일들이 보여지게 되는데, 그 파일들을 더블 클릭하면 어떤 변경 사항이 있는지도 바로 확인이 가능하다.</p>
</li>
<li><p>amend를 되돌리는 방법도 물론 있다. <code>git reflog</code>를 사용하여 되돌리고자 하는 커밋의 해시를 복사한 후, <code>git reset --hard &lt;commit_hash&gt;</code> 명령어에<commit_hash> 부분에 복사한 커밋해시를 붙여넣으면 변경내용을 완전히 삭제하게한다. 그러나 말그대로 완전 삭제이기 때문에 주의가 필요하다. 되돌리기 전에 변경 내용을 백업하거나 다른 작업 공간에 저장하는 등의 방법을 함께 사용하는 것이 좋다.</p>
</li>
<li><p>터미널에서 <code>git log</code> 명령어롤 틍해 자세한 커밋 사항을 확인할 수도 있다. 깃 로그를 확인하면 commit 해쉬값과 함께 내가 커밋한 목록이 나오는데, 내가 확인하고 싶은 로그의 커밋 해쉬값을 복사한 후 <code>q</code>를 눌러 빠져나와 <code>git show 커밋해쉬값</code> 명령어를 다시 입력하면, 변경된 파일의 상세내역을 확인 할 수 있다!</p>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점과-다음에-할-일">✏ 느낀 점과 다음에 할 일</h3>
<ul>
<li><p>commit을 하면서 코드 변경점을 관리하는 것은 좋은데 commit으로 관리하니까 막상 또 PR 넣은 것을 까먹는다. 이번에 조금 바보같을 뻔 한게 로그아웃 다이얼로그를 연결할 때 커밋에다가 [Feat]으로 넣어뒀는데, 생각해보니까 feat으로 구분하고 싶으면 그 전에 design으로 PR을 올렸어야 했던게 아닐까? 라는 생각이 드는 것이다. 마침 프로젝트 구조에 대해서 확장성과 명확한 책임 구분을 위해 구조 변경을 하겠다고 PR을 올려달라고 하셔서 타이밍 맞게 올리지 않았더라면 commit만 쌓이고 PR에서는 어떤 기능을 작성했는지 구분이 안될 뻔 했다. 여하튼 feat으로 써두긴 했지만, 사이드로 들어가는 거기도 하고 feat이면서 결국엔 다이얼로그 연결하는 ui적 요소가 강하다고 판단해서 PR에는 그냥 Design으로 대괄호를 묶어서 보냈다. commit과 PR관리에 보다 신경을 써야할 것 같다.</p>
</li>
<li><p>visibility를 써서 화면 전환을 관리하는 방법에 대해 걱정이 많았다. 이렇게 하는게 맞는건지 틀린건지를 몰라가지고.. 근데 팀원들과 리뷰시간에 보여주셨는데, 다들 그냥 잘된 것 같다고 별 말씀이 없으시길래 이 방법을 고수하기로 했다. 괜히 xml 파일도 길어지고 어쩌고 저쩌고 걱정하는 말을 했는데, 나보다 xml 파일이 훨씬 긴 다른 팀원도 계시고 코틀린 파일도 복잡한 분들이 계셔서 그냥 내 기우였을 뿐이란 것을 단단히 확인했다.ㅎ 이미 발을 들였기 때문에 이제는 그냥 자신감을 가지고 되는 방법을 선택해야할 것 같다!</p>
</li>
<li><p>다음에는 프로필 수정에서 사진 선택과, 이름 변경하는 것을 우선적으로 연결한 후 로그인/로그아웃을 담당한 팀원 분과 같이 로그인 로그아웃 코드를 걸면 될 것 같다. 로그인 로그아웃이 되면 유저 데이터베이스도 생성하고 관리할 수 있기 때문에, 북마크 캠핑장이나 작성한 글도 확인할 수 있을 것 같다. 이번에 플레이스토어 배포 컷이 높아졌다고 매니저님들이 걱정을 많이 하시던데, 통과 될 수 있도록 열심히 해봐야겠다. 파이팅!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 4]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4</guid>
            <pubDate>Wed, 28 Feb 2024 13:16:58 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240228-수요일-tiltoday-i-learned-오늘-배운-것">✏240228 수요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖-firebase-사용시-고려해야할-점">📖 FireBase 사용시 고려해야할 점</h3>
<ul>
<li><p>원래 게시판 부분을 담당하기로 해서, FireStore와 Realtime Database 중에 어디에 게시판을 저장하면 좋을지 간단한 자료 조사를 진행했다.</p>
</li>
<li><p>가볍게 구글링을 했을 때, Realtime Database를 사용하여 게시판을 구현한 예시를 많이 보게 되었는데, 이 예시들은 전부 텍스트만 관리하고, 댓글 기능이 없이 바로 게시글을 읽을 수 있는 구현들이었다. 이번 프로젝트에서 만들어야 하는 게시판은 이미지 등록이 가능하고, 검 기능과 댓글 작성 후 읽기 또한 가능하기 때문에 Realtime Database 만으로 작성 시에는 코드가 너무 복잡해질 문제가 우려되었다.</p>
</li>
<li><p><code>Realtime Database</code>는 이미지를 저장하려고 할때 <code>storage</code>나 <code>fireStore</code>를 연계해서 저장해야한다. 이미지 자체를 저장하지 못하고, URL등의 메타 데이터만 저장할 수 있기 때문이다. 그리고 검색 기능을 구현하려고 할떄도, 복잡한 쿼리문을 제공하지 않기 때문에 조건문을 통해 원하는 정보를 가져오는 것이 쉽지 않다. 댓글 작성 시에도 데이터 구조가 점차 복잡하게 될 경우 코드 작성 시에 코드가 많이 복잡하고 더러워질 수 있다.</p>
</li>
<li><p><code>FireStore</code>는 위에 realtime database에서 언급한 단점들을 상쇄하기에 좋다. 이미지 자체를 바로 저장하고, realtime database보다 where문을 통한 상세한 쿼리문이 제공된다. 데이터를 관리하는 것도 컬렉션(폴더)-다큐먼트(문서)식으로 관리하기 때문에 댓글관리도 보다 쉬울 수 있다.
하지만, realtime database 보다 조금 더 비싼 비용문제가 있었다.</p>
</li>
<li><p>=&gt; 위의 두 자료를 가지고 팀원들과 상의를 해봤는데, 게시글 본문은 firestore를 쓰고 댓글만 realtimedatabase를 쓴다던지, 그냥 둘다 realtime으로 하고 storage로 이미지를 관리한다던지.. 등등 에 대해서 논의를 하다가 검색 때문이라도 fireStore를 사용하자는 쪽으로 말이 나왔을 때, 알고보니 fireStore의 where문으로도 원하는 검색 결과를 확실하게 가져오는데 문제가 있다는 것을 알게되었다. 게시글의 제목을 검색한다고 했을 때, 게시글 제목은 보통 문장형이기 때문에 조사가 붙는데, 단어만으로는 조사가 붙었을 때 검색이 되지 않는 문제가 발생할 수 있다는 것이다.
=&gt; 이를 해결하기 위해 팀장님이 검색에 특화된 외부 데이터베이스 서비스를 함께 사용하는 방법을 찾아오셨는데, 내가 해결하기엔.. 시간 내에 완료하지 못할 가능성이 높아서 팀장님의 파트와 내 파트를 바꾸기로 했다. 팀장님도 하루 아침에 연결이 되는건 아니고 좀 더 여러 데이터베이스 서비스를 찾아보시면서 어떤 방법을 사용하면 좋을지 계속 공유해주시겠다고 했다. 그래서! 나는 게시판에서 프로필(마이페이지) 구현으로 담당이 변경되었다.</p>
<br>

</li>
</ul>
<h3 id="📖profile-ui-레이아웃-작업-1">📖Profile UI 레이아웃 작업 1</h3>
<ul>
<li><p>담당이 변경된 후, 프로필 부분은 어쨌든 유저 정보가 있어야 기능 구현을 자세하게 할 수 있기 때문에, 로그인 부분을 담당하는 팀원과 속도를 맞추기 위해 레이아웃 그리는 작업 부터 들어갔다. 이번에는 피그마에 디자인이 완벽한 와이어프레임이 있기 때문에, 이를 바탕으로 최대한 똑같이 구현하는 것을 목표로 레이아웃을 그렸다.</p>
</li>
<li><p>이미지와 텍스트를 넣어서 프로필 정보를 구현하는 것은 당연히 문제가 없었는데, 탭 레이아웃을 통해 북마크아이템과, 작성한 게시글을 보여주기로 한 것에서 문제가 발생했다. 프로필에서 구현하고 싶은 모습은 탭 레이아웃의 탭에서 북마크아이템(몇개), 작성한 게시글(몇개) 이렇게 보여주는 형태였는데, 저 몇개. 를 표현하기 위해서 어떻게 해야할지 고민이었던 것이다.
=&gt; 일단은 머티리얼 디자인에서 tabs 속성을 자세히 살펴보았더니, 뱃지에 대한 기능이 있었다. 알림 개수 같은 모습이긴 하지만 어쨌든 개수를 표시하는데에는 코드로 매번 세팅하고 눌러도 바로 사라지는게 아니라는 것을 알게 되어서 뱃지를 사용해보기로 했다. </p>
<pre><code class="language-kotlin">//xml에 있는 TabLayout과 뷰바인딩을 통한 연결
 val tabLayout = binding.tabProfile
 //탭레이아웃에서 0번째 탭을 getTabAt을 통해 가져오기
  val bookmarkedTab = tabLayout.getTabAt(0)
  //가져온 0번째 탭에 orCreateBadge를 통해 뱃지를 생성하여 숫자 할당
  bookmarkedTab?.orCreateBadge?.number = 30

  val badge = bookmarkedTab?.badge
  //badgeGravity를 통해 위치조정이 가능하다. 제공되는 위치가 한정적이다
  badge?.badgeGravity = BadgeDrawable.BOTTOM_END
  // 뱃지의 배경색 변경가능. 여기서 TRANSPARENT는 투명을 뜻함
  badge?.backgroundColor = Color.TRANSPARENT
  // 뱃지의 텍스트 색 변경가능
  badge?.badgeTextColor = Color.GRAY</code></pre>
<p>구현과정에는 어려운게 없었으나, 뱃지의 배경색, 뱃지텍스트색이 변경은 가능해도, 위치를 조정하는데에 상당히 제한적이라는 것을 알게되었다. </p>
</li>
<li><p>그리고 res에 있는 테마의 스타일을 통해서 탭레이아웃의 디자인을 변경할 때 <code>&lt;item name=&quot;tabIndicatorColor&quot;&gt;</code>로 인디케이터 색은 변경이 바로 적용되었는데, <code>&lt;item name=&quot;android:fontFamily&quot;&gt;</code>를 통한 폰트 변경이나 <code>&lt;item name=&quot;android:textSize&quot;&gt;</code> 같은 textsize변경은 탭레이아웃 디자인에 적용이 되지 않았다. 탭 모양을 <code>TabLayout.Secondary</code> 모양으로 하기로 해서 <code>parent=&quot;Widget.Material3.TabLayout.Secondary&quot;</code>를 이렇게 잡아서 텍스트 관련된 요소들이 적용이 안되었던 걸까? 테마를 이중? 삼중?으로 잡으면 고칠 수 있긴 있을 것도 같은데 어쨋거나 일단 뱃지가 위치가 제한적인 것과 더불어 텍스트를 가리는 치명적인 문제가 발생했기 때문에, 
=&gt; 리사이클러뷰 아이템에 스와이프 기능을 사용하면서 탭 자체를 넘기는 스크롤 기능을 끄기로 사전에 합의가 있었던 것이 기억나서, 모양만 탭레이아웃형태를 따라가고 그냥 버튼 형식으로 아이템을 오가는 모습으로 만들기로 팀원들께 설명을 드리고 합의를 보았다.</p>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점">✏ 느낀 점</h3>
<ul>
<li><p>게시판 CRUD는 부담스럽긴 해도 이전에 자바로 만들어 본 경험이 있었어서 어떻게든 되겠지란 마음으로 해보겠다고 한거였는데, 외부.. 데이터베이스를 새로 연결해야한다? 그것도 연동으로 해야한다? 이건 확실히 지금의 내가 시간 내에 하기엔 여러가지 부분에서 부담이 너무 컸다. 팀장님이 선뜻 먼저 바꾸자고 해주셔서 감사하다.</p>
</li>
<li><p>UI 구현은 오늘 또 결국 한번에 되는 일이 없다..ㅎㅎ 예전에 수준별 수업에서 클론 UI로 탭레이아웃을 만들어본 적이 있는데, 그때 이걸 이렇게 visvilty로 막 만들어도 되는지 고민이 많았는데, 결국 이번에도 그렇게 되어 버렸다.ㅎ 그때보다 심각해진건가? 그땐 탭레이아웃 위젯이라도 썼지.. 지금은 그냥 냅다  layout영역을 버튼으로 만든거니까..ㅎㅎ 중요한건 이부분이 마이페이지와 다른유저페이지 등 두어곳에서 사용된다는건데.. 포폴 코드니까.. 조금 뜯어봤을 떄 보고 어떻게 생각하시려나? 하는 ..ㅎㅎ 그런 걱정이 있긴하다. 음. 근데 고칠 수 있는거니까.. ...... 뭐 맘만 먹으면 고칠 수 있을 것같다. 아니면 튜터님을 방문해서 언제 한 번 조언을 구해봐야겠다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 3]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3</guid>
            <pubDate>Tue, 27 Feb 2024 05:13:17 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240221-수요일-tiltoday-i-learned-오늘-배운-것--br-✏240227-화요일-tiltoday-i-learned-오늘-배운-것">✏240221 수요일 TIL(Today I learned) 오늘 배운 것 ~ <br> ✏240227 화요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖디자인-작업">📖디자인 작업</h3>
<h4 id="수목금토요일-">수,목,금,토요일 :</h4>
<ul>
<li>기획에 이어서 디자인도 직접하게 되었다. 이번 프로젝트에는 디자이너 튜터님이 따로 시간을 내셔서 피드백을 주시는 시간이 있다.</li>
<li>우리는 미리 작성해 둔 UI틀에 가까운 와이어 프레임을 바탕으로 실제로 제공되는 어플들의 레퍼런스를 모아 필요한 부분들을 완성해나갔다.</li>
<li>디자인 튜터님도, 이미 서비스하고 있는 어플들의 장점을 따와서 화면을 구성하는 것은 문제가 있는 것이 아니고 오히려 사용자의 실사용 면에서 유용함을 입증한 디자인이기 때문에 최대한 고려하는 것이 좋다고 해주셨다.</li>
<li>이외에도 디자인 튜터님의 피드백을 모아보자면 아래와 같다.<ul>
<li>그림자 같은 것은 함부로 쓰면 금방 촌스러워질 수 있다.</li>
<li>색감은 화사한 것을 고르는 것이 좋다</li>
<li>특별히 강조되어야할 정보가 아니면 흰배경에 까만글씨로 텍스트를 전달하는 것이 좋다고 생각한다.</li>
<li>각 페이지 용도에 맞는 디자인을 해주는 것이 좋다.
(ex.정보를 제공하기 위해서라면 그 정보를 충분히 보여줄 수 있다던가, 게시판 처럼 슥슥 훑고 지나갈 아이템들은 적당히 여유있는 패딩과 마진으로 피로도를 덜어야한다던가)<br>

</li>
</ul>
</li>
</ul>
<h3 id="📖깃허브-세팅">📖깃허브 세팅</h3>
<h4 id="월요일-">월요일 :</h4>
<ul>
<li>SA작성을 마무리하면서, 깃허브 Organization 세팅과 더불어 기본 환경을 세팅했다. 빌드 버전들과 안드로이드스튜디오 환경 세팅과 더불어서 본격적인 코드 작업에 들어가기 전에 Color, 이미지 아이콘, 폰트 등 필요한 재료들도 추가해 두었다.</li>
<li>이렇게 세팅한 것들을 커밋하면서, 한 번 더 커밋 컨벤션을 제대로 맞추고 풀리퀘스트 시 컨벤션도 맞춰보았다. 이번에 스쿼시머지라는 것을 처음 들어봤는데 <code>다수의 커밋을 단일 커밋으로 압축하여 풀리퀘스트를 깔끔하게 유지하는 방법</code>이라고 한다. 우리 팀은 이번에 스쿼시 머지로 히스토리 관리를  하기로 했다. 마지막 포트폴리오 이기 때문에 깃허브관리도 신경써서 해야겠다.</li>
</ul>
<br>

<h3 id="📖db작업-with-firebase">📖DB작업 with FireBase</h3>
<h4 id="수요일--db구성정리">수요일 : [DB구성정리]</h4>
<ul>
<li>사실은 수요일에 디자인 작업을 시작하기 전, 와이어 프레임을 구성한 후 부터 DB 작업을 했는데, 아무래도 로컬 데이터 만으로는 제공할 수 없는 게시판, 채팅 등을 포함하여 외부 데이터베이스를 바탕으로 데이터를 뿌려주는게 효율적이라는 의견이 나와서, 우선은 데이터베이스에서 관리해야할 데이터들이 뭐가 있는지 구성부터 했다.</li>
<li>각 페이지마다 제공되어야할 정보들이 뭐가 있는지 확인하고, 어떻게 관리할지, 어떤 키로 연결될지 등을 정리했다.</li>
<li>사실 디자이너 튜터님의 피드백에 따라 제공되는 정보가 좀 바뀌기도 하고, 우리끼리 의논을해서 재구성한 페이지들이 있기 때문에 초반에 잡아 놓은 DB 구성에서 디자인이 완성되고 다시 한 번 검증을 통해 필요한 정보들을 정리했다.</li>
</ul>
<h4 id="월화요일--firebase">월,화요일 : [FireBase]</h4>
<ul>
<li><p>이렇게 정리한 데이터들을 바탕으로 우리는 <code>FireBase</code>를 사용하기로 했다.
FireBase에서는 구글에서 제공하는 간편로그인 관리부터, 채팅이나 게시판 같은 실시간 데이터들의 동기화를 <code>Realtime Database</code>로 제공하고, 이에 대한 알림도 <code>FCM(Firebase Cloud Messaging)</code>으로 쉽게 관리할 수 있는 등 애플리케이션 구축 및 운영 등에서 다양하고 효율적으로 활용되는 플랫폼이다.</p>
</li>
<li><p>이번에 만드는 어플에서 우리가 원하는 모습으로 데이터를 가공하여 제공하기 위해서, 공공데이터 포털에서 제공해주는 api 데이터를 데이터베이스에 보관하여 사용하는 방법을 먼저 고려했기 때문에, 제일 많이 언급된 <code>Realtime Database</code>에 JSON 데이터를 집어 넣어 보기로 했다. <code>Realtime Database</code>는  JSON 트리 구조를 사용하여 데이터를 저장하기 때문에 CLI 등을 사용하여 쉽게 데이터를 업로드 할 수 있었다.
=&gt; 문제는.. 업로드한 데이터에서 우리가 원하는 조건으로 데이터를 가져오는 것이 상당히 제한적이라는 것이다. <code>Realtime Database</code>에서는 쿼리지원을 정렬 정도로만 지원하고 복잡한 쿼리는 지원하지 않기 때문에, 구현하려고 하는 검색 기능이나, 조건에 맞는 데이터만 뿌려주는 것을 하기에 적합하지 않았다.
=&gt; 그래서 <code>FireStore</code>를 사용하기로 했는데, <code>FireStore</code>에는 Json 파일을 올리는게 쉽지 않았다. 하나 하나 넣는 방법은 많은데 <code>Realtime Database</code>에서 처럼 CLI를 사용해서 올리는 것이 되지 않았다. 결국엔 팀장님이 node.js를 통해서 해결해주셨는데, 결제 계정 이야기와 더불어서 개인적으로 해결하기에 소극적인 부분이 아쉬웠어서 나중에 개인적으로 프로젝트를 진행할 때에는 좀 더 적극적으로 시도해봐야겠다.</p>
</li>
</ul>
<hr>

<h3 id="✏-느낀-점">✏ 느낀 점</h3>
<ul>
<li><p>사실 디자인을 할 때 시간이 너무 오래 걸렸다. 팀원분들도 다들 모르는 분야이고, 그렇다고 무시하기엔 서비스 제공 시에 보여지는 화면이 얼마나 중요한지 알아서, 최대한 모든 의견을 들어보고 좋은 것을 반영하려고 하다보니 제공된 시간에서 너무 많은 부분을 디자인에 할애해버렸다. 그래도 다같이 회고를 통해 원인을 파악하고 해결하기 위한 방안들을 이야기하는 시간을 통해 개선할 수 있는 시간이 있었어서 매번 정리하고 회고하는 시간이 중요하다는 것을 깨달았다.</p>
</li>
<li><p>나는 이전 프로젝트 때 매번 디자인을 후순위로 놓고 작업을 했어서, 이번에 이렇게 디자인을 먼저 고려하는 순위로 작업을 진행하는 것이 상당히 새롭게 느껴졌다. 그리고 내가 왜 여태 했던 프로젝트 들은 디자인이 단조로울 수 밖에 없는지 깨달았다. 이미 코드를 작성해서 기능을 완성해 버리면 그거에 맞춰서 겉 껍데기 색깔만 바꿔줄 뿐, 1열 수직 리니어레이아웃을 수평 2열 그리드로 배정해주거나 하는 문제는 시간이 오래걸리기 때문에 이번 프로젝트 처럼 미리 모양을 잡는 것이 상당히 중요하다는 것을 깨달았다. 그러고 보면 심화 수업 때 앱개발 프로세스에 대해 배울 때에도 디자인이 꽤 앞부분에 있었던 기억이 난다. 여태껏 프로젝트들에 제공되는 기간들이 짧았어서 급한 마음에 늘 디자인을 뒤로 밀었었는데, 이번 프로젝트 때에라도 알게 되어서 다행이라고 생각한다.</p>
</li>
<li><p>프로젝트를 위해 firebase를 배우는 것은 좋은데, 프로젝트 기간 내에 firebase를 배워서 하자니 시간이 오래 걸리는 것 같아서 조금 초조하다. 내가 맡은 게시판 부분을 위해서 <code>realtimebase</code> 사용은 하나의 값 넣고 하나의 값 읽고 하면 충분할 것 같기 때문에 화요일처럼 어렵지는 않을 것 같은데, 이번 팀프로젝트를 진행하면서, 내가 배운 범위보다 훨씬 넓은 범위의 기술들이 등장하니까 좀 소극적이게 되는 것 같다. 최대한 따라가고 더 배우고 기운내야겠다! 파이팅!</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin/프로그래머스] 24년 2월 5주차 코드카타정리]]></title>
            <link>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-2%EC%9B%94-5%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-2%EC%9B%94-5%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Tue, 27 Feb 2024 00:54:23 GMT</pubDate>
            <description><![CDATA[<h3 id="✏20240227-화요일">✏20240227 화요일</h3>
<h3 id="📖조건에-맞게-수열-변환하기-2"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181881">📖조건에 맞게 수열 변환하기 2</a></h3>
<ul>
<li>while 반복문 안에서 for문을 통해 조건에 맞춰서 arr을 바꾼다. 이때, arr이 바뀌면 count에 +1을 더해서 변화되었음을 알려준다.</li>
<li>if문을 통해 count가 0인지 아닌 것으로 arr의 원소값이 이전과 똑같은지 아닌지를 판별하고, 0(똑같다)이면 반복문을 빠져나오고, 0이 아니면 answer에 +1을 해준후 계속 반복한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(arr: IntArray): Int {
      var answer: Int = 0
      while(true){
          var count = 0
          for(i in arr.indices){
              if(arr[i]&gt;=50 &amp;&amp; arr[i]%2==0) {
                  arr[i] = arr[i]/2 
                  count++
              } else if(arr[i]&lt;50 &amp;&amp; arr[i]%2!=0) {
                  arr[i] = arr[i]*2 +1 
                  count++
              }
          }
          if(count == 0) break;
          answer++
      }
      return answer
  }
}</code></pre>
</li>
<li>count 말고 flag로 true false를 통해 코드를 작성한 다른 사람의 풀이가 있었는데, flag를 사용하는게 더 자연스럽고 연산적으로도 좋지 않았을까 하는 생각이 든다.</li>
<li>처음에는 while문을 한바퀴 돌기 이전의 arr 값을 저장했다가 한바퀴 돈 후 의 arr값과 비교해서 같은지 아닌지를 판별해야 하나? 라고 생각했는데, 그러면 작성해줘야할 사전변수들이 많아질 것 같아서 값이 바뀌는 곳 자체의 변화를 체크하는 방법을 사용했는데, 덜 복잡하고 좋은 것 같다.</li>
</ul>
<hr>

<h3 id="✏20240229-목요일">✏20240229 목요일</h3>
<h3 id="📖이진수-더하기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/120885">📖이진수 더하기</a></h3>
<ul>
<li><code>bin1.toInt(2) + bin2.toInt(2)</code>에 대해서 이진수로 표현하기 위해 <code>Integer.toBinaryString()</code>을 사용한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(bin1: String, bin2: String): String 
  = Integer.toBinaryString(bin1.toInt(2) + bin2.toInt(2))
}</code></pre>
</li>
<li><code>toInt()</code>보다 <code>parseInt()</code>를 사용한 풀이가 꽤 보였는데, <code>parseInt()</code>가 속도에서 좀 더 빠르다고 한다.</li>
<li><code>(bin1.toInt(2) + bin2.toInt(2)).toString(2)</code> :  처음에 <code>(bin1.toInt(2) + bin2.toInt(2))</code>이렇게 만 적었다가 오류 때문에 문제가 안풀려서 검색했던건데 뒤에 toString(2)를 붙여주면 되었던거였다니.. 새롭게 알아간다.</li>
</ul>
<br>

<h3 id="📖소인수분해"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/120852">📖소인수분해</a></h3>
<ul>
<li>n을 계속 나눠주면서 소인수를 찾아야하기 때문에 변수 num을 선언해서 n을 담아준다.</li>
<li>while반복문을 통해 소인수 분해를 진행한다. 이때, 2부터 나눠서 소인수 분해를 하기 때문에 i를 2부터 시작한다.</li>
<li>while 안에서 num이 i로 딱 나누어 떨어진다면 num을 i로 나누고 answer에 i를 담는다. 만약 떨어지지 않는다면 i에 +1씩 하여 다음 나누어 떨어지는 숫자를 찾는다.</li>
<li>num이 1이 될 때까지 반복한 후, 담겨진 answer를 문제에서 요구한대로 중복된 값 없이 하나의 값 들만 남기기 위해 <code>distinct()</code>를 사용하고 리스트로 변환된 값을 다시 <code>toIntArray()</code>를 통해 타입을 맞춰준다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(n: Int): IntArray {
      var answer: IntArray = intArrayOf()
      var num = n
      var i = 2
      while(num&gt;1) {
          if(num % i == 0){
              num /= i
              answer += i
          } else i++
      }
      return answer.distinct().toIntArray()
  }
}</code></pre>
</li>
<li>맨 처음에 answer를 Set으로 바꿀까도 생각했는데, distinct가 생각이나서 그냥 answer에 distinct를 붙여줬더니 반환 타입이 리스트라고 타입 미스매치를 만났다..ㅎ 이제와서 Set으로 바꾸기도 뭐하고 그냥 toIntArray로 바로 타입을 맞춰주었다.</li>
</ul>
<hr>

<h3 id="✏20240301-금요일">✏20240301 금요일</h3>
<h3 id="📖문자-개수-세기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181902/solution_groups?language=kotlin">📖문자 개수 세기</a></h3>
<ul>
<li><p>answer에 대문자26개+소문자26개 를 합친 52개 크기 만큼의 intArray를 초기화 시킨 후</p>
</li>
<li><p>for문을 통해 my_string을 돌면서 아스키코드를 활용하여 대문자의 범위 안에 있다면 answer에서 대문자 A가 인덱스 0번 부터 시작하기 때문에 <code>-65(A[65]-65=0)</code>로 해당 인덱스를 찾아서 +1을 해주고, 소문자라면 소문자 a가 인덱스가 26부터 시작하기 때문에 <code>-71(a[97]-71=26)</code>을 해주어 해당 인덱스를 찾아서 +1해준다.</p>
<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(my_string: String): IntArray {
      var answer: IntArray = IntArray(52)

      for (i in my_string.indices) {
      when {
          my_string[i] in &#39;A&#39;..&#39;Z&#39; -&gt; answer[my_string[i].toInt() - 65]++
          my_string[i] in &#39;a&#39;..&#39;z&#39; -&gt; answer[my_string[i].toInt() - 71]++
      }
  }
      return answer
  }
}</code></pre>
</li>
<li><p>다른 사람의 풀이를 보니까 forEach를 사용해서 보다 간단하게 풀어낸 사람들이 많았다. it을 사용하니까 확실히 코드 가독성이 훨씬 증가한 것을 느낄 수 있었다.</p>
</li>
<li><p>마음이 급해도 이제는 for보다 forEach나 map 같은 확장함수를 먼저 사용하도록 해야겠다.... 맨날 초조하다는 핑계로 for만 썼더니 다른 함수를 더 못쓰게 되는 것 같다...</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 2]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8F%AC%ED%8A%B8%ED%8F%B4%EB%A6%AC%EC%98%A4-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2</guid>
            <pubDate>Tue, 20 Feb 2024 12:47:53 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240220-화요일-tiltoday-i-learned-오늘-배운-것">✏240220 화요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖요구사항-정의서">📖요구사항 정의서</h3>
<ul>
<li><p>어제 말한 접목 기술 및 실현 가능성 검증으로 <code>요구사항 정의서</code>를 작성하기로 했다. </p>
</li>
<li><p>요구사항 정의서를 작성하기 위해 우리가 만들고자 하는 어플에 대해 <code>문제 정의</code>부터 하였다.</p>
<ul>
<li>우선적으로 타켓층을 생각해보고,</li>
<li>어플을 만들고자  했던 이유 (평소에 느꼈던 문제점)</li>
<li>위에서 언급한 문제점에 대한 해결 방안 (구현하고자하는 기능)</li>
<li>더해서 우리만의 차별화된 기능</li>
<li>모든 것에 대한 한계점 을 집중적으로 논의하였다.</li>
</ul>
</li>
<li><p>처음에 1순위였던 레시피의 경우, 데이터 재가공이나 사용하려는 기술이 인공지능을 기반으로 하여 배웠던 스킬들과 결이 너무 달라서 문제 정의에서 아쉽지만 정리를 하고, 그 다음인 캠핑 주제에 대해서 문제정의를 하였는데, 한계점이 적고 다른 캠핑 예약 어플들이 충분히 많지만, 나름의 차별화된 기능을 제공할 수 있다고 생각한다는 여러 의견들을 종합하여 요구사항 정의서까지 작성하게 되었다.</p>
</li>
<li><p>요구사항 정의서. 라고 해야할지 기능명세서라고 해야할 지 우리는 요구사항 정의서라는 이름으로 작성했다. 작성 양식은 일단 3가지로 정했다.</p>
<ol>
<li>화면별 구분</li>
<li>주기능</li>
<li>기능 상세</li>
</ol>
</li>
<li><p>원래라면 기능명세서 안에서 실현 가능성도 따져봐야하는데, 우리는 문제 정의에서 기술 실현 가능성이 있는 내용들을 미리 따져본 후 요구사항 정의서를 작성했기 때문에, 거기까지는 따로 또 추가해서 작성하지 않았다.</p>
</li>
</ul>
<h3 id="📖-와이어-프레임-작성">📖 와이어 프레임 작성</h3>
<ul>
<li><p>이렇게 작성한 요구사항 정의서를 바탕으로 와이어프레임을 작성했다.
요구사항 정의서가 화면별로 작성되었기 때문에 쉽게 와이어 프레임의 초안을 만들 수 있었다.</p>
</li>
<li><p>UI 큰 틀을 잡으면서 각자의 <strong>사용자 경험성</strong>을 토대로 한 화면 구성의 이유를 들어가며 배치도를 작성할 떄, 설득을 하기도 하고 설득이 되기도 하면서 다른 팀원들의 의견을 통해 확실히 시야가 넓어진 기분이 들었다.</p>
</li>
<li><p>또, 로그인 방법 (회원가입을 하게 된다면 이에 관한 개인정보 관리 등의 후처리) 등 기능적으로 효율적인 방안들을 이번 단계에서 상의하게 되었는데, 최대한 기능의 기초를 다지는 역할에 대해서도 유의미 했다고 생각한다.</p>
</li>
<li><p>다만, 디자인에 대해서는 이번에도.. 특별한 프레임을 잡지 못했는데.. 이번에는 디자인 튜터님이 따로 계시니까.. 그 쪽에서 보완을 할 수 있을거라고 생각해본다.</p>
</li>
</ul>
<h3 id="✏-내일-할-일">✏ 내일 할 일</h3>
<ul>
<li><p>내일은 팀장님의 주도 하에 전체 개발 일정을 잡아보기로 했다. WBS.. 인가 무슨 다른 방법을 쓰신다고 하시던가 에 대해서도 내일 상의해보자고 하셨는데, ㅎ 나는 그게 애초에 뭔지 잘..ㅎㅎ 몰라서 내일 새롭게 배워볼 생각이다.</p>
</li>
<li><p>오늘은 추가학습에 대해서도 개별 상담을 짧게 가졌다. 공동데이터API를 사용하는 것을 할지, CATAPI? 를 사용할지, 공공API에 인공지능ChatGPT을 더해 활용하는 과제를 할지 고르라고 해주셨는데.. 인공지능까지는 당연히.ㅎ 내가 감당이 안될 것 같고 공공API는.. 사실 팀원들과 토의를 했을 때 떨어진 내가 낸 아이디어가 있는데 이거 솔직히 나는 만들어보고 싶어서 이거를 해보고 싶다고 말씀드릴 생각이다. 다만.. 나 혼자 만들려면 스코프를 훅~ 줄여야할텐데 ..ㅎㅎ 과연 내가 추가 학습을 무사히 끝마칠수 있을지.&gt;! 스스로도 도전이다. 그래도 하겠다고 했으니 의욕을 가지고 팀플도 개인추가학습도 무사히 마무리짓고 싶다. 화이팅~</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 최종 : 포트폴리오 프로젝트 1]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%B5%9C%EC%A2%85-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1</guid>
            <pubDate>Mon, 19 Feb 2024 12:37:43 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240219-월요일-tiltoday-i-learned-오늘-배운-것">✏240219 월요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖최종-포트폴리오-프로젝트-기획-브레인스토밍">📖최종 포트폴리오 프로젝트 기획 (브레인스토밍)</h3>
<ul>
<li><p>오늘부터는 최종 팀프로젝트에 들어갔다. 이번 프로젝트를 통해 최종 포트폴리오를 확정 짓기 때문에, 나도 팀원분들도 모두 의욕적이시다.ㅎㅎ</p>
</li>
<li><p>나는 직전 팀 프로젝트도 그렇고 그 전 프로젝트도 그렇고 늘 초반부에 기획을 확실히 못잡는 것에 대한 아쉬움을 KPT회고에 작성했었는데, 이번에는 내가 건의하지 않아도 팀장님이 기초를 확실히 잡는 플랜으로 기획을 이끌어주셔서 마음 편히 따라갔다.</p>
</li>
<li><p>특히, 이번 프로젝트는 이전의 프로젝트들과 달리 가이드라인이 없는 진짜 자율 기획 프로젝트이기 때문에 S.A 마감 일자도 넉넉해져서 자유롭게 아이디어를 내고, 하나씩 검증해보는 단계를 일일히 가져갈 수 있어서 좋았던 것 같다. 아주 잠깐.. 솔직히 어. 이렇게까지? 라고 아주 잠깐 생각했는데, 곧 최대한 디테일을 잡고 가는게 좋다는 사실을 떠올리고 조용히 검증 과정을 따라갔다.</p>
</li>
<li><p>검증 과정은 <strong>아이디어 브레인 스토밍 -&gt; 긍정적 피드백 (아이디어 모으기) -&gt; 방향성을 통한 우선 고려 사항 정하기 -&gt; 방향성을 통한 부정적 피드백 (아이디어 거르기) -&gt; 남은 아이디어들 중에 투표 -&gt; 많이 나온 아이디어부터 접목 기술 및 실현 가능성 검증</strong> 이다.</p>
</li>
</ul>
<hr>

<h3 id="✏-오늘-배운-것과-내일-할-일">✏ 오늘 배운 것과 내일 할 일</h3>
<ul>
<li><p>오늘은 배웠다고 해야할까? 아이디어 회의에 대해 제대로 경험해볼 수 있는 하루가 되는 것 같다. 위의 검증 과정을 굳이 한 번 더 작성한 이유도, 이런 방향으로 진행하는게 어떻게 보면 효율적으로 깊게 회의를 할 수 있는 하나의 방향성이라는 긍정적인 경험을 했다고 생각하기 때문이다.</p>
</li>
<li><p>여기서. <code>많이 나온 아이디어부터 접목 기술 및 실현 가능성 검증 부분</code>에 대해 이렇게까지? 라고 생각했지만, 이렇게 까지 하는게 좋은 것 같다. 한 번도 이렇게까지 안해봤으니까..,,ㅎ 이렇게 까지 해보는게 좋은 것 같다. 사실 이 부분은 오늘 하루의 시간이 다 가서 내일 팀원 분들과 진행하기로 했다. 아마 이 과정을 통해 탄탄한 기초를 잡을 수 있을 것이라고 기대한다. 개인적인 바람은 오전 중으로 최대한 검증 하고, 오후에는 와이어 프레임까지 작성해볼 수 있었으면 좋겠는데, 나 혼자 바란다고 되는게 아니니까 내일도 열심히 팀원들을 따라가야 할 것 같다. 파이팅~~</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Kotlin/프로그래머스] 24년 2월 4주차 코드카타정리]]></title>
            <link>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-2%EC%9B%94-4%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</link>
            <guid>https://velog.io/@wiz_hey/Kotlin%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-24%EB%85%84-2%EC%9B%94-4%EC%A3%BC%EC%B0%A8-%EC%BD%94%EB%93%9C%EC%B9%B4%ED%83%80%EC%A0%95%EB%A6%AC</guid>
            <pubDate>Mon, 19 Feb 2024 12:24:53 GMT</pubDate>
            <description><![CDATA[<h3 id="✏20240219-월요일">✏20240219 월요일</h3>
<h3 id="📖한-번만-등장한-문자"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/120896">📖한 번만 등장한 문자</a></h3>
<ul>
<li>한 번만 등장하는 문자를 사전 순으로 정렬해야하기 때문에 먼저 문자열 s를 리스트로 바꾸어 정렬해 준다.</li>
<li>정렬한 s에 대해 filter를 사용하여 두 번 이상 등장하는 문자열은 처음 인덱스와 끝 인덱스가 다르다는 점을 통해 한 번만 등장하는 문자를 남긴 후 <code>joinToString(&quot;&quot;)</code>을 사용하여 문자열로 반환한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(s: String): String 
  = s.toList().sorted()
      .filter{ s.indexOf(it) == s.lastIndexOf(it)}.joinToString(&quot;&quot;)
}</code></pre>
</li>
<li>한 번만 등장한다는 조건이 많이 당혹스러웠다. <code>set</code>이나 <code>distinct</code>를 사용하면 두번 이상 등장한 문자들도 하나의 문자로 남아서 반환해주기 때문에 오히려 상황을 꼬아놓는 것 같아서 열심히 써치하다가 인덱스를 통한 풀이방법을 사용했다. 개인적으로 깔끔한 풀이같아서 만족스럽다.</li>
</ul>
<blockquote>
<h4 id="📖한-번만-등장한-문자의-다른-사람-풀이">📖한 번만 등장한 문자의 다른 사람 풀이</h4>
</blockquote>
<pre><code class="language-kotlin">class Solution {//다른 사람 풀이
    fun solution(s: String) = s.toSet()
        .filter { uniqueChar -&gt;
            s.count { it == uniqueChar } == 1
        }.sorted().joinToString(&quot;&quot;)
}</code></pre>
<ul>
<li><code>toSet()</code>을 이용하여 중복된 문자들을 제거 한 후, 남아 있는 문자열 중에 원래의 s 문자열에서 2번 이상 카운트 되는 문자열을 filter로 거르고, 정렬하여 문자열로 반환한다.</li>
<li>중복 제거 후에 원 문자열을 통해 두 번 이상 나오는 문자를 count로 세서 없애는 방법이 있었다. 원래 생각한 방향성과 비슷한 것 같아서 문제를 가져와봤다. 그래도  위에 언급한 것 처럼 인덱스를 통한 풀이가 좀 더 깔끔한 것 같다.</li>
</ul>
<br>

<h3 id="📖배열의-길이를-2의-거듭제곱으로-만들기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181857">📖배열의 길이를 2의 거듭제곱으로 만들기</a></h3>
<ul>
<li>while문을 통해 2의 거듭제곱을 만들어 준다. 이때, <code>while(k &lt; arr.size)</code> 이렇게 조건문을 주면, k가 arr.size보다 커졌을 때 비로소 반복문이 멈추기 때문에 arr 크기에서 가까운 2의 거듭제곱수를 바로 찾을 수 있다.</li>
<li>answer에 arr을 초기화 값을 선언한 후, 다시 반복문을 통해 위에서 찾은 k(arr 크기에서 가까운 2의 거듭제곱수)에 맞게 0을 추가하는 방법을 사용하여 값을 반환한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(arr: IntArray): IntArray {
      var answer = arr
      var k = 1
      while(k &lt; arr.size) {
          k *= 2
      }
      while(k != answer.size) {
          answer += 0
      }
      return answer
  }
}</code></pre>
</li>
<li>맨 처음에는 제곱근을 이용해서 2의 거듭제곱수를 찾아 반복문 한개로 코드를 작성하려고 하다가 도저히 2의 거듭제곱을 조건으로 만들 수 없어서 깡으로 2의 거듭제곱을 곱해서 만든 후, 거기에 맞춰서 0을 추가할 수 있도록 만들어주었다.</li>
<li>실제로 다른 사람들의 풀이에 제곱근은 아니여도 <code>pow(제곱)</code> <code>ceil(올림)</code> 등을  사용한 풀이가 있었는데, 오히려 int형에서 double형 등으로 소수가 가능한 타입으로 풀이를 해야한다는 점에서 코드가 더 복잡해 보여서 내가 푼 풀이가 마음에 든다.</li>
<li><code>while(k != answer.size)</code> 대신에 <code>repeat(n - arr.size)</code>을 사용한 풀이도 있었는데, repeat은 늘.. 잘 사용하지 않게 되어서.. 까먹지 말자는 취지로 작성해본다. 필요할 때 안까먹고 꼭 사용할 날이 오기를 기대한다.ㅎㅎ</li>
</ul>
<hr>

<h3 id="✏20240220-화요일">✏20240220 화요일</h3>
<h3 id="📖세-개의-구분자"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181862">📖세 개의 구분자</a></h3>
<ul>
<li><code>a</code> <code>b</code> <code>c</code> 가 구분자로 사용되야 하므로, 정규식을 사용하여 abc를 공통의 모양 <code>&quot; &quot;</code>(나는 공백문자로 바꿨다.)으로 바꾼 후, 그 모양을 기준으로 split을 사용해준다. 그 후, 빈칸을 제거 하고 다시 반환타입을 맞춰준다.</li>
<li>만약, 빈 배열이 되면 문제에서 요구하는대로 <code>&quot;EMPTY&quot;</code>를 넣어 반환해준다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(myStr: String): Array&lt;String&gt; {
      var answer: Array&lt;String&gt; 
      = myStr.replace(&quot;[abc]&quot;.toRegex(), &quot; &quot;)
          .split(&quot; &quot;).filter{it.isNotEmpty()}.toTypedArray()
      if(answer.isEmpty()) answer += &quot;EMPTY&quot;
      return answer
  }
}</code></pre>
</li>
<li>배열에 추가할 때 <code>+=</code>을 사용하는게 버릇? 익숙해져서 위에 if문을 따로 빼서 answer에 <code>&quot;EMPTY&quot;</code>를 추가해줬다.</li>
<li>다른 사람의 풀이를 보니 <code>if (answer.isEmpty()) arrayOf(&quot;EMPTY&quot;) else answer</code> return에 바로 한줄을 적어서 반환해준게 있었다. <code>arrayOf(&quot;EMPTY&quot;)</code> 이렇게 바로 배열에 값을 집어넣는 생각을 우선적으로 할 수 있도록 해야겠다.</li>
</ul>
<br>

<h3 id="📖문자열-반복해서-출력하기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181950">📖문자열 반복해서 출력하기</a></h3>
<ul>
<li>정수 a만큼 반복하도록 repeat함수를 사용하여 문자열 s1을 출력한다.<pre><code class="language-kotlin">fun main(args: Array&lt;String&gt;) {//나의 풀이
      val input = readLine()!!.split(&#39; &#39;)
  val s1 = input[0]
  val a = input[1]!!.toInt()
  repeat(a) { print(s1)}
}</code></pre>
</li>
<li>쉬운 문제이지만 repeat이 반가워서 작성해본다.</li>
<li>다른 사람 풀이를 보면 <code>println(s1.repeat(a))</code> for문 사용 등으로 문제를 풀었다. 은근히 출력할 때 어떤 모양으로 코드를 짜야 출력이 되는지 까다로울 때가 있는데, <code>repeat(a) { print(s1)}</code> 이 모양으로도 <code>println(s1.repeat(a))</code> 이 모양으로도 같은 값이 나오는게 코드 작성은 정말 스타일의 차이라는 것을 새삼 다시 느끼게 되었다.</li>
</ul>
<br>

<h3 id="📖2의-영역"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181894">📖2의 영역</a></h3>
<ul>
<li>arr 배열에 2가 있다면 arr을 List로 변환하여 subList를 적용해 첫번째2의 인덱스 부터, 마지막 2의 인덱스까지 자른 후, 다시 IntArray로 타입을 맞춰준다.</li>
<li>만약 arr 배열에 2가 없다면 -1이 IntArray에 담겨 반환되도록 한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(arr: IntArray): IntArray 
  = if(arr.contains(2)) {
      arr.toList().subList(arr.indexOf(2),arr.lastIndexOf(2)+1).toIntArray()
  } else IntArray(1){-1}
</code></pre>
</li>
</ul>
<p>}</p>
<pre><code>- `intArrayOf(-1)`를 두고 `IntArray(1){-1}`라고 작성 하는 나...??ㅎㅎ. 배열 값을 자르거나 추가하기만 했지 배열 자체를 만드는 건 좀 낯설다..ㅎㅎ 손으로 직접 쓰려면 맨날 눈으로본다고 다가 아니라는 것을 오늘도 깨닫고 반성하게 된다.
- subList 말고 slice를 사용한 풀이도 있었는데, slice는 범위 인자를 받기 때문에 `..`을 사용하면 굳이 `arr.lastIndexOf(2)+1`여기에서 +1을 안해줘도 됐을텐데.. 라고 뒤늦게 생각이 들었다.
- 원래 indexOf는 찾는 값이 없으면 -1을 반환해서 이를 활용한 풀이도 있었는데,
어짜피 if문을 사용하게 되는건 똑같아서 굳이 따로 작성까지는 하지 않는다.


&lt;hr&gt;

### ✏20240221 수요일
### [📖수열과 구간 쿼리 4](https://school.programmers.co.kr/learn/courses/30/lessons/181922)
- 2차원배열로 이루어진 queries을 for문을 통해 q라는 변수에 할당하여 1차원 배열로 꺼내고 문제에서 주어진 대로 `q[0] .. q[1]` 범위 만큼의 i가 k의 배수일 떄를 if 조건 문으로 주어서 조건에 해당할 때 해당 인덱스의 값이 +1 되도록 한 후 반환한다.
```kotlin
class Solution {//나의 풀이
    fun solution(arr: IntArray, queries: Array&lt;IntArray&gt;): IntArray {
        var answer: IntArray = arr
        for(q in queries) {
            for(i in q[0] .. q[1]){
                if(i % q[2] == 0) answer[i]++
            }
        }
        return answer
    }
}</code></pre><ul>
<li>처음에 문제에서 제공하는 입출력을 보고 돌아가는 프로세스가 전혀 이해가 가지 않았는데, 알고보니 인덱스에 대한 이야기 였다. 문제를 이해하면 쉽게 풀 수 있는 문제이므로 익숙한 for문을 사용해서 후딱 풀었다.</li>
<li>다른 사람의 풀이를 보니 굳이 arr을 answer에 담지 않아도 <code>arr[i]++</code>로 바로 문제를 풀 수 있었겠구나 싶다.</li>
</ul>
<blockquote>
<h4 id="📖수열과-구간-쿼리-4-문제의-다른사람-풀이">📖수열과 구간 쿼리 4 문제의 다른사람 풀이</h4>
</blockquote>
<pre><code class="language-kotlin">class Solution {//다른 사람 풀이
    fun solution(arr: IntArray, queries: Array&lt;IntArray&gt;): IntArray 
    = arr.apply {
        queries.forEach { (s, e, k) -&gt; 
        (s..e).filter { it % k == 0 }.forEach { arr[it]++ } 
        }
    }
}</code></pre>
<ul>
<li>forEach를 사용하여 요소로 가지고 있는 1차원 배열을 바로 <code>(s, e, k)</code>로 지정하여 문제를 풀었다. </li>
<li><code>s..e</code> 범위에 대해서 filter로 k의 배수를 찾고, 찾은 모든 값을 forEach로 반복하여 arr의 해당 인덱스 값을 <code>++</code>로 +1해준다.</li>
<li>나도 이렇게 람다식에서 <code>(s, e, k)</code> 이런식으로 풀어서? 변수를 지정해주는 모양에도 이젠 익숙해 질 때가 온 것 같다. 눈으로라도 먼저 많이 익혀둬야겠다.</li>
</ul>
<br>

<h3 id="📖문자열-묶기"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181855">📖문자열 묶기</a></h3>
<ul>
<li>groupingBy 함수를 사용해서 it.length를 키로 하고 strArr의 요소를 값으로 갖도록 변환한다.</li>
<li>eachCount를 사용해서  그루핑된 키 값을 기준으로 value의 개수를 세고, eachCount가 센 개수를 담아놓은 것이 values이기 때문에 values의 가장 큰 값을 반환하도록 한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(strArr: Array&lt;String&gt;): Int 
  = strArr.groupingBy{it.length}.eachCount().values.maxOrNull()!!
}</code></pre>
</li>
<li><code>groupBy</code> : 주어진 키 선택자 함수에 따라 각 요소를 그룹화하고, 그 결과로 Map을 반환한다.</li>
<li><code>groupingBy</code> : groupBy 의 지연 연산을 제공한다. 필요한 시점에 그룹화 연산을 수행하기 때문에 중간 결과가 메모리에 저장되지 않는다.</li>
<li><code>eachCount()</code> : 컬렉션 내 각 요소를 순회하며 개수를 계산하는 역할을 한다. 따라서 <code>요소(키)-개수(값)</code>의 Map형태로 반환한다</li>
<li>솔직히 아직도 groupBy와 groupingBy의 차이점이 크게 와닿지 않는다. 다만 groupBy를 쓰면 <code>type mismatch</code>오류가 나는데 <code>중간 결과가 메모리에 저장되지 않는다.</code> 이거 차이 때문에 그런가? 라고 짐작해볼 뿐이다.</li>
<li>eachCount( ) 함수는 여기서 처음 만났는데. 뭔가 유용해보인다. 까먹지 말고 꼭! 사용해볼 수 있도록 기억해둘것이다!</li>
</ul>
<hr>

<h3 id="✏20240222-목요일">✏20240222 목요일</h3>
<h3 id="📖커피-심부름"><a href="https://school.programmers.co.kr/learn/courses/30/lessons/181837">📖커피 심부름</a></h3>
<ul>
<li>for문으로 order배열을 돌면서 <code>&quot;cafelatte&quot;</code>를 포함하고 있으면 5000원을 그외의 것들은 4500원을 answer에 더한 후 반환한다.<pre><code class="language-kotlin">class Solution {//나의 풀이
  fun solution(order: Array&lt;String&gt;): Int {
      var answer: Int = 0
      for(i in order){
          if(i.contains(&quot;cafelatte&quot;)) answer += 5000
          else answer +=4500
      }
      return answer
  }
}</code></pre>
</li>
<li>원래는 forEach로 order문을 풀다가 아니.. 바로 sumOf로 계산하면 되잖아? 하고 <code>order.sumOf { (if (&quot;latte&quot; in it) 5000 else 4500)}</code>라고 작성했더니 <code>error: overload resolution ambiguity</code>오류가 나왔다. 뭐.. 매개변수가 애매모호하다? 확신을 하지 못한다? 이런 류의 이야기 였는데 도저히 무슨소리인지 모르겠어서 포기하고 그냥 for문으로 풀었다. map을 사용해서 5000(혹은 4500원)으로 바꾸고 sum을 해도 되었지만 위에서 했던 시도가 안먹히니까 안전하고 빠르게 풀고 싶어서 for문을 사용했다..ㅎㅎ</li>
<li>하여튼 다른 사람 풀이에서 <code>sumOf</code>를 사용한 풀이를 발견했는데, <code>order.sumOf { (if (&quot;latte&quot; in it) 5000 else 4500) as Int }</code>라고 작성한 것을 발견했다. <code>as Int</code>!!!! int타입이라는것을 as를 사용하여 명확히 해주는 과정을 추가해줘야한다는 것을 꺠달았다. type missmatch나 맨날 만났지 이런 경우는 처음이었는데, 앞으로 당황하지 않을 수 있을 것 같다!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Android 앱 개발 심화 : 팀 프로젝트 4]]></title>
            <link>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%8B%AC%ED%99%94-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4</link>
            <guid>https://velog.io/@wiz_hey/TIL-Android-%EC%95%B1-%EA%B0%9C%EB%B0%9C-%EC%8B%AC%ED%99%94-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4</guid>
            <pubDate>Fri, 16 Feb 2024 07:50:42 GMT</pubDate>
            <description><![CDATA[<h3 id="✏240216-금요일-tiltoday-i-learned-오늘-배운-것">✏240216 금요일 TIL(Today I learned) 오늘 배운 것</h3>
<h3 id="📖발표회-및-피드백">📖발표회 및 피드백</h3>
<ul>
<li>오늘은 심화챕터의 발표식이 있었다.! 이번 발표는 다른 팀원분께서 해주셨는데, 역시 발표는 잘하는 사람이 잘하는게 여러가지면에서 큰 장점들이 많은 것 같다.</li>
<li>이번 피드백은 상당히 긍정적인 반응들이 주를 이루어서 뿌듯했는데, 그 중에서도 개선점을 까먹지 않도록 적어본다.<ul>
<li>완성도 면에서 UI/UX를 꼭 챙기도록하자.</li>
<li>기능면에서, 어댑터는 데이터 처리를 완벽히 한 후 바인딩해야한다.(중복처리 등을 어댑터에서 하면 안된다.)</li>
<li>발표자료제출 시 권한 설정에 유의하자.</li>
<li>발표자료는 중요도가 높은 부분이  확실하게 어필될 수 있도록 작성하자.</li>
</ul>
</li>
<li>디자인 피드백은 감안하고 있던 부분이라 그저 아쉬운 부분 중에 하나이지만, 어댑터 처리와 발표자료 권한 설정 부분은 아차 싶은 것들이었다. 그 중 발표자료 권한 설정은 내가 관리한 부분이기도 해서 관리 미숙에 대해 팀원들에게 미안한 마음도 있었다.
놀란 부분은 발표자료에서 중요도가 안보였다는 피드백이 놀라웠다. 나는.. 나름.... 어필을..해본다고 ..생각한것이기 때문에....ㅎㅎ 어쨌든 발표라는건 청중에게 어필을 해야하는 것이기 때문에 진정한 피드백이라고 생각하고 신경써야겠다.</li>
</ul>
<br>

<h3 id="📖kpt-keepproblemtry">📖KPT (Keep/Problem/Try)</h3>
<ul>
<li><p>Keep : 잘하고 있는 점. 계속 했으면 좋겠다 싶은 점.</p>
</li>
<li><p>Problem : 뭔가 문제가 있다 싶은 점. 변화가 필요한 점.</p>
</li>
<li><p>Try : 잘하고 있는 것을 더 잘하기 위해서, 문제가 있는 점을 해결하기 위해서 우리가 시도해 볼 것들</p>
<h4 id="우리-팀의-kpt">[우리 팀의 KPT]</h4>
</li>
<li><p>Keep</p>
<ol>
<li>팀원들과의 소통이 원활하게 돼서 (하루에 2-3번씩 소회의 진행) 진행사항이나 문제점을 파악하기 수월했고, 서로의 문제점을 해결하고자 노력했던 점은 지속해서 가져가면 좋을 것 같습니다.</li>
<li>코딩 컨벤션이나 프로젝트 보드 관리를 통한 진행 과정 공유 및 일정 관리 같은 협업 전략을 잘 세웠습니다.</li>
</ol>
</li>
<li><p>Problem</p>
<ol>
<li>UI 디자인 신경 써서 개발</li>
<li>RecyclerView adapter 내에서 중복 처리 하지 말고 adapter 적용 하기 전에 처리</li>
<li>다른 사람이 편집하지 못하게 발표 자료 권한 설정</li>
</ol>
</li>
<li><p>Try</p>
<ol>
<li>기획 단계에서 UI 디자인을 구현</li>
<li>5분 기록 보드와 트러블슈팅 목록 관리 적극적으로 활용하기</li>
</ol>
</li>
</ul>
<hr>

<h3 id="📖오늘의-반성과-마무리">📖오늘의 반성과 마무리</h3>
<ul>
<li>오늘의 반성은 저번 팀프로젝트의 KPT와 비교해봤을 떄 <code>프로젝트 설계 단계에서 최대한 구체적인 부분까지 소통해서 작성하기</code> 라는 부분을 Try에 적었었는데, 오늘 돌이켜보면 시작 단계에서부터 디자인을 세세하게 정해지 못했다는 생각이 들어서 또 똑같은 KPT를 하게 되었다는 점을 크게 반성하게 되는 것 같다. </li>
<li>그래도 이번 조에서는 확실히 의사 소통의 횟수도 늘고, 그만큼 소통도 잘되어서 어떻게든 ETA를 맞출 수 있었다고 생각한다. 협업에서 의사소통은 정말 중요한 것 같다. 문제점을 혼자서 해결하지 못할 때 여러사람의 의견으로 해결할 수 있다는 것도 정말 든든한 일이라는 것을 이번 프로젝트에서 다시 한 번  더 경험할 수 있었다.</li>
<li>앞으로 시작되는 6주최종팀플은 정말 기획에서부터 배포까지 전부를 하게되는데, 이때에는 여태까지 배웠던 것들을 총합해서 아쉬운 부분 없이 KPT와 모든 피드백을 반영하여 확실히 성장된 모습을 직접 느낄 수 있는 프로젝트가 되었으면 좋겠다.! 파이팅~</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>