<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>JH 개발자 일지</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Wed, 12 Mar 2025 14:48:05 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>JH 개발자 일지</title>
            <url>https://velog.velcdn.com/images/one_coin/profile/d1987013-4105-434f-8c02-ad8f32f11e11/image.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. JH 개발자 일지. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/one_coin" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[invoke에 대해 알아보자]]></title>
            <link>https://velog.io/@one_coin/invoke%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@one_coin/invoke%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 12 Mar 2025 14:48:05 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트를 진행하면서 usecase에서 invoke를 사용하여 ViewModel에서 코드를 작성하였는데 operator(연산자)와 invoke를 처음 써봐서 한번 알아보기로 하였다.</p>
<h3 id="invoke란">invoke란?</h3>
<ul>
<li>invoke는 클래스의 인스턴스가 마치 함수인 것처럼 호출될 수 있게 하는 특수 함수입니다. 주입 가능한 생성자를 사용하여 클래스를 함수로 변환하여 주는데 이를 통해 표준 함수 호출 구문을 사용하여 ViewModel에서 usecase를 계속 호출할 수 있도록 합니다.</li>
</ul>
<pre><code>class LoginUseCase @Inject constructor(private val authRepository: AuthRepository) {
    suspend operator fun invoke(email: String, password: String) : Flow&lt;String&gt; {
        return authRepository.login(email, password)
    }
}</code></pre><ul>
<li>저는 다음과 같이 로그인을 하는 usecase에서 repository를 생성자로 가지고 repository에서 login 함수를 호출하는 것으로 코드를 작성하였습니다. 다음 usecase를 ViewModel에서 다음과 같이 사용할 수 있습니다.</li>
</ul>
<pre><code>clase LoginViewModel(
    private val loginUseCase: LoginUseCase
): ViewModel() {

    fun login(email: String, password: String) {
        viewModelScope.launch() {
            val getLoginInfo = loginUseCase(email, password)
            ...
        }
    }
}</code></pre><ul>
<li>이렇게 LoginUseCase의 invoke 메서드를 이름을 생략하여 호출할 수 있었습니다.
원래는 &quot;객체명.함수명()&quot;의 형태로 호출하여야 하지만 &quot;객체명()&quot; 형태로도 그 객체의 함수를 호출할 수 있다는 것이 편하고 코드의 가독성을 올려주는 것 같았습니다.</li>
</ul>
<h3 id="결론">결론</h3>
<ul>
<li><p>operator fun과 invoke 키워드는 모두 연산자 오버로딩을 사용하기 위한 키워드입니다.
(연산자 오버로딩이란 다양한 연산자들(+, -, *, / 등등)을 재정의해서 사용할 수 있게 해주는 방법이라고 생각하시면 됩니다. )</p>
</li>
<li><p>코드의 간결함과 가독성을 높여주고 호출의 유연성이 생깁니다.
(loginUseCase() vs loginUseCase.invoke)</p>
</li>
</ul>
<p>.
.
.</p>
<h4 id="참고">참고</h4>
<p><a href="https://onlyfor-me-blog.tistory.com/837">https://onlyfor-me-blog.tistory.com/837</a></p>
<p><a href="https://velog.io/@spdlqjfire/Kotlin-invoke">https://velog.io/@spdlqjfire/Kotlin-invoke</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Web Socket과 HTTP 프로토콜 두 가지의 차이점]]></title>
            <link>https://velog.io/@one_coin/Web-Socket%EA%B3%BC-HTTP-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EB%91%90-%EA%B0%80%EC%A7%80%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</link>
            <guid>https://velog.io/@one_coin/Web-Socket%EA%B3%BC-HTTP-%ED%94%84%EB%A1%9C%ED%86%A0%EC%BD%9C-%EB%91%90-%EA%B0%80%EC%A7%80%EC%9D%98-%EC%B0%A8%EC%9D%B4%EC%A0%90</guid>
            <pubDate>Tue, 11 Mar 2025 15:32:37 GMT</pubDate>
            <description><![CDATA[<p>이번에 실시간 채팅을 구현하면서 저는 Firebase로 실시간 갱신을 시켜주는 형태를 띄는 채팅을 구현했지만 원래는 서버에서 어떻게 통신을 할까하여 궁금한 점을 구글링을 통해 이를 정리해 보겠습니다.</p>
<h3 id="서버와의-통신">서버와의 통신</h3>
<p>웹 애플리케이션 개발에서 클라이언트와 서버 간의 통신은 필수적인 요소인데 이를 위해 주로 사용되는 두 가지 프로토콜이 HTTP(HyperText Transfer Protocol)와 웹소켓(WebSocket)입니다.</p>
<h3 id="http-프로토콜-통신">HTTP 프로토콜 통신</h3>
<ul>
<li><p>기존에는 HTTP 프로토콜 통신이란 HTML 파일을 전송하는 프로토콜의 의미를 가졌다.(Hyper Text Transfer Protocol)</p>
</li>
<li><p>웹 브라우저에서의 통신은 초기 대부분 HTML 파일을 전송하는 목적이였기 때문이었는데 현재는 JSON, Image 등 다양한 형식의 파일들을 전송할 수 있습니다.</p>
</li>
<li><p>HTTP 통신은 클라이언트에서 서버로 요청을 보내고, 서버가 이에 응답하는 방식으로 통신이 이루어집니다. 여기서 응답은 클라이언트의 요청에 따른 결과를 반환한 것입니다. (단방향 통신)</p>
</li>
<li><p>서버의 응답에는 응답 코드(202, 404 ...)가 같이 전송되며 사용자는 응답 코드와 메시지 응답으로부터 오는 메시지 바디를 통해 요청 값을 전달 받습니다.</p>
</li>
</ul>
<hr>
<h3 id="websocket웹소켓">WebSocket(웹소켓)</h3>
<ul>
<li><p>웹소켓(Web Socket) 프로토콜은 HTTP와는 다른 통신 프로토콜로 웹 서버와 웹 브라우저가 서로 실시간 메시지를 교환하는 데 사용됩니다. </p>
</li>
<li><p>웹소켓은 연결을 위한 첫 번째 핸드셰이크를 주고 받은 이후 지속적으로 연결이 유지되는 것이 특징이며 매번 메시지 전송 시에 새롭게 연결을 맺을 필요가 없어 빠르고 효율적입니다.</p>
</li>
<li><p>웹소켓을 이용한 실시간 통신 구현은 클라이언트와 서버 모두에서 웹소켓 API를 사용하여 이루어집니다. 클라이언트는 JavaScript의 WebSocket 객체를 사용하여 서버와의 연결을 수립하고 서버는 웹소켓 프로토콜을 지원하는 라이브러리를 이용하여 이를 처리합니다.</p>
</li>
<li><p>웹소켓은 TCP 소켓과 이름만 유사할 뿐 브라우저의 소켓이며 웹소켓 프로토콜은 HTTP와 동일하게 애플리케이션 계층에서 동작합니다. 그리고 평문 메시지 전송 방식이므로 SSL/TLS 보안 계층으로 암호화되어야 데이터 탈취를 방지할 수 있습니다.</p>
</li>
<li><p>웹소켓은 사용할 때는 네트워크 지연시간, 데이터 압축, 보안 등을 고려하여 애플리케이션의 요구 사항에 맞게 세심하게 설계되어야 합니다.</p>
</li>
</ul>
<h3 id="두-가지-프로토콜의-차이점">두 가지 프로토콜의 차이점?</h3>
<p>여기서 두 가지 프로토콜의 차이점이 있는데 HTTP는 요청/응답(Request/Response) 모델을 기반으로 하는 비연결성(단방향) 프로토콜 입니다. 클라이언트가 서버에 요청을 보내고, 서버는 이에 대한 응답을 보내줍니다. HTTP는 각 요청이 독립적이며 상태를 유지하지 않기 때문에 이러한 형태를 띄고 있다고 합니다. 반면 웹소켓은 클라이언트와 서버 간에 지속적인 연결을 유지하는 양방향 통신 프로토콜입니다. 이를 통해 실시간으로 데이터를 주고 받을 수 있습니다. (아무래도 채팅을 구현하기에는 웹소켓이 더 적당하지 않을까 생각합니다.)</p>
<hr>
<h3 id="차이점-정리">차이점 정리</h3>
<h4 id="http">HTTP</h4>
<ul>
<li>비연결성(단방향) 프로토콜로 각 요청이 독립적이며 상태를 유지하지 않음</li>
<li>매번 연결을 맺고 끊는 과정의 비용이 많이듭니다.</li>
<li>요청(Request) - 응답(Response)의 구조를 지닙니다.</li>
<li>기본적인 웹 애플리케이션에 적합</li>
</ul>
<h4 id="웹소켓">웹소켓</h4>
<ul>
<li>연결성(양방향) 프로토콜로 클라이언트와 서버 간에 지속적인 연결을 유지하는 통신 프로토콜</li>
<li>한번 연결을 맺은 뒤 계속 유지되는 실시간성을 보장합니다.</li>
<li>연결지향적인 특징을 지닙니다.</li>
<li>실시간 통신이 필요한 애플리케이션에 필수적</li>
</ul>
<hr>
<p>.
.
.</p>
<h3 id="번외-tcpip-소켓과-웹소켓의-차이">(번외) TCP/IP 소켓과 웹소켓의 차이</h3>
<h4 id="tcpip-socket">TCP/IP Socket</h4>
<ul>
<li><p>네트워크상 서버와 클라이언트 두 개의 프로그램이 특정 포트를 통해 양방향 통신이 가능하도록 만들어주는 소프트웨어 장치입니다.</p>
</li>
<li><p>intranet 바운더리(조직 내 네트워크)에서 작업하는 경우에는 해당 네트워크의 컴퓨터를 제어하고 TCP 연결에 적합한 포트를 열 수 있기 때문에 TCP 소켓을 통해 통신하는 것이 더 쉽습니다.</p>
</li>
<li><p>TCP/IP Socket은 인터넷을 통한 통신을 위한 프로토콜이기 때문에 소켓을 이용하여 양 끝단의 IP/Port 번호를 이용해 연결을 수립하고 데이터를 주고 받고 데이터의 신뢰성을 보장합니다. </p>
</li>
<li><p>하나의 TCP 접속에 있는 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜입니다.</p>
</li>
</ul>
<h4 id="web-socket">Web Socket</h4>
<ul>
<li><p>HTTP 프로토콜 위에 실시간 양방향 통신 기능을 추가한 프로토콜로 ws/wss 프로토콜 위에서 동작합니다.</p>
</li>
<li><p>웹소켓은 HTTP에서 실시간 통신을 할 수 없다는 문제를 해결하기 위해 나온 기술로 HTTP의 헤더를 업그레이드해서 동작하기 때문에 서버나 클라이언트는 전이중 양방향 통신을 수행하며 언제든지 메시지를 보낼 수 있다.</p>
</li>
<li><p>웹소켓은 연결지향으로 한 번 연결을 맺은 뒤 유지되며 핸드쉐이크 과정에서는 헤더의 비중이 크지만, 한 번 연결이 되면 간단한 메시지들만 오고가기에 굉장히 경제적입니다.</p>
</li>
<li><p>HTTP나 HTTPS 위에서 동작하도록 설계되었으며 포트를 80번, 443번 사용합니다.</p>
</li>
</ul>
<hr>
<h4 id="참고">참고</h4>
<p><a href="https://velog.io/@sangje112/http-vs-WebSocket-http-%ED%86%B5%EC%8B%A0%EA%B3%BC-WebSocket%EC%9D%98-%EC%9E%A5%EB%8B%A8%EC%A0%90">https://velog.io/@sangje112/http-vs-WebSocket-http-%ED%86%B5%EC%8B%A0%EA%B3%BC-WebSocket%EC%9D%98-%EC%9E%A5%EB%8B%A8%EC%A0%90</a></p>
<p><a href="https://jerecord.tistory.com/155">https://jerecord.tistory.com/155</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[List, Map, Set 차이 (자료구조)]]></title>
            <link>https://velog.io/@one_coin/List-Map-Set-%EC%B0%A8%EC%9D%B4-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@one_coin/List-Map-Set-%EC%B0%A8%EC%9D%B4-%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Fri, 07 Mar 2025 20:12:32 GMT</pubDate>
            <description><![CDATA[<p>Java에서 Collection은 데이터의 집합, 그룹을 의미합니다. </p>
<p>Collection의 주요 인터페이스에는 List와 Map, Set이 있는데 이 세가지 자료구조의 특징을 한번 알아보겠습니다.</p>
<h3 id="list">List</h3>
<ul>
<li><p>순서가 있으며 데이터의 중복을 허용하는 자료구조입니다.</p>
</li>
<li><p>크기가 가변적이며 비어있는 데이터가 없습니다.</p>
</li>
<li><p>인덱스를 통해 저장 데이터에 접근이 가능합니다.</p>
</li>
<li><p>만약 원하는 데이터가 뒤쪽에 있을 경우 속도가 떨어집니다. (리스트를 순회해야 하기 때문에)</p>
</li>
</ul>
<h3 id="list-인터페이스의-주요-구현체">List 인터페이스의 주요 구현체</h3>
<h4 id="arraylist">ArrayList</h4>
<ul>
<li><p>단방향 포인터 구조로 데이터 순차적 접근에 강점을 가집니다.</p>
</li>
<li><p>배열을 기반으로 데이터를 저장합니다.</p>
</li>
<li><p>데이터 삽입, 삭제가 느리고 데이터 검색이 빠릅니다.</p>
</li>
</ul>
<h4 id="linkedlist">LinkedList</h4>
<ul>
<li><p>양방향 포인터 구조로 데이터 삽입, 삭제가 빠릅니다.</p>
</li>
<li><p>ArrayList 보다 검색이 느립니다.</p>
</li>
</ul>
<p>.
.
.</p>
<h4 id="array와-arraylist의-차이">Array와 ArrayList의 차이</h4>
<ul>
<li><p>Array의 경우 길이가 고정되어 변경이 불가능하지만 ArrayList의 경우 Array를 이용해 만든 List형 자료구조입니다. Array와 다르게 길이를 할당하지 않지만 데이터 추가시 배열의 최대 크기가 넘으면 2배 크기의 배열을 만들고 원본을 복사하여 재생성 합니다.</p>
</li>
<li><p>ArrayList는 배열을 이용하여 리스트를 구현한 것 입니다. 다만 배열을 가변적으로 사용하며 리스트가 가지는 특성인 저장순서 유지와 중복 허용의 특징을 가지고 있습니다. ArrayList는 클래스이기 때문에 배열을 추가, 삭제 할 수 있는 기능을 함께 포함하고 있습니다.</p>
</li>
<li><p>Array는 동적으로 사이즈 변경이 안되지만 ArrayList의 경우에는 가능합니다.</p>
</li>
</ul>
<hr>
<h3 id="map">Map</h3>
<ul>
<li><p>Key와 Value의 한쌍으로 이루어진 데이터의 집합 입니다.</p>
</li>
<li><p>Key에 대한 중복이 없으며 순서를 보장하지 않습니다. (데이터는 중복을 허용합니다.)</p>
</li>
<li><p>뛰어난 검색 속도를 가지고 인덱스가 따로 존재하지 않습니다.</p>
</li>
</ul>
<h4 id="hashmap">HashMap</h4>
<ul>
<li><p>내부적으로 배열을 만들어 관리하며 Key 값으로 넘겨준 객체의 해시 코드를 계산하여 배열의 접근 인덱스로 사용합니다.</p>
</li>
<li><p>Key와 Value 값으며 NULL을 허용합니다.</p>
</li>
<li><p>동기화가 보장되지 않습니다.</p>
</li>
<li><p>검색에 가장 뛰어난 성능을 가집니다.</p>
</li>
</ul>
<h4 id="treemap">TreeMap</h4>
<ul>
<li><p>Key와 Value 쌍을 내부적으로 RedBack Tree (이진 탐색 트리)로 저장하여 관리합니다.</p>
</li>
<li><p>Key 값을 기준으로 오름차순 정렬되고 빠른 검색이 가능합니다.</p>
</li>
<li><p>저장 시 정렬을 하기 때문에 시간이 다소 오래 걸립니다.</p>
</li>
</ul>
<h4 id="linkedhashmap">LinkedHashMap</h4>
<ul>
<li>데이터의 입력 순서를 기억합니다.</li>
</ul>
<hr>
<h3 id="set">Set</h3>
<ul>
<li><p>데이터의 집합이며 순서가 없고 중복된 데이터를 허용하지 않습니다.</p>
</li>
<li><p>중복되지 않은 데이터를 구할 때 유용합니다.</p>
</li>
<li><p>빠른 검색 속도를 가지고 인덱스가 따로 존재하지 않습니다.</p>
</li>
</ul>
<h4 id="hashset">HashSet</h4>
<ul>
<li><p>HashSet은 객체들을 순서없이 저장하고 동일한 객체는 중복 저장하지 않습니다.</p>
</li>
<li><p>NULL 값을 허용하고 TreeSet 보다 삽입, 삭제가 빠릅니다.</p>
</li>
</ul>
<h4 id="treeset">TreeSet</h4>
<ul>
<li><p>이진 탐색 트리(Red Black Tree)를 기반으로 합니다.</p>
</li>
<li><p>데이터들이 오름차순으로 정렬됩니다.</p>
</li>
<li><p>데이터 삽이브 삭제에는 시간이 걸리지만 검색과 정렬이 빠릅니다.</p>
</li>
</ul>
<h4 id="linkedhashset">LinkedHashSet</h4>
<ul>
<li>HashSet과 동일하지만 입력된 순서를 저장하고 관리합니다.</li>
</ul>
<hr>
<h4 id="정리해보기">정리해보기</h4>
<ul>
<li><p><strong>List</strong>는 데이터들이 순서대로 저장되며 데이터의 중복이 허용됩니다.</p>
</li>
<li><p><em>(순서 O, 데이터 중복 O)*</em></p>
</li>
<li><p><strong>Set</strong>은 순서가 보장되지 않는 데이터의 집합으로 데이터의 중복이 허용되지 않습니다.</p>
</li>
<li><p><em>(순서 X, 데이터 중복 X)*</em></p>
</li>
<li><p><strong>Map</strong>은 순서가 보장되지 않고 Key 값의 중복은 허용하지 않지만 Value 값의 중복은 허용됩니다.</p>
</li>
<li><p><em>(순서 X, 키는 중복 X, 값은 중복 O)*</em></p>
</li>
</ul>
<hr>
<h3 id="참고">참고</h3>
<p><a href="https://velog.io/@kwj2435/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-list-map-set-%EC%B0%A8%EC%9D%B4">https://velog.io/@kwj2435/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-list-map-set-%EC%B0%A8%EC%9D%B4</a></p>
<p><a href="https://studywithus.tistory.com/68">https://studywithus.tistory.com/68</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Firebase 채팅 구현하기]]></title>
            <link>https://velog.io/@one_coin/Firebase-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@one_coin/Firebase-%EC%B1%84%ED%8C%85-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Thu, 06 Mar 2025 19:58:55 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트를 진행하면서 Firebase로 실시간 채팅을 구현해보았습니다.</p>
<p>생각보다 신경쓸 부분이 많아서 오래걸렸지만 차근차근 작성해 보겠습니다.</p>
<h3 id="데이터-모델-설정">데이터 모델 설정</h3>
<pre><code>data class ChatRoomDataEntity(
    val chatRoomId: String,
    val lastMessageData: LastMessageDataEntity,
    val unReadMessage: Int,
    val participant: List&lt;String&gt;,
    val chatRoomSession: List&lt;Map&lt;String, Boolean&gt;&gt;
)

data class LastMessageDataModel(
    val lastMessage: String,
    val lastSendAt: String,
    val lastMessageSender: String
)</code></pre><ul>
<li><p>채팅방에 대한 데이터는 채팅방 고유 ID, 마지막으로 받은 메시지(시간, 내용), 안읽은 메시지 갯수, 참여자, 채팅방에 지금 들어와 있는지 이렇게 구성되어 있습니다.</p>
</li>
<li><p>마지막으로 받은 메시지에 대한 내용을 가지고 오는 것보다 더 간단할 것 같아서 메시지를 보낼 때 Firebase에서 update 할 수 있도록 구성했고, 채팅방에 현재 상대방이 들어와 있는지 없는지 Boolean 타입으로 나타내어 false일 경우에는 안읽은 메시지로 나타내고 true일 경우에는 읽은 메시지로 나타낼 수 있도록 하였습니다.</p>
</li>
<li><p>채팅방 List에서 안읽은 메시지를 나타낼 수 있도록 Firebase의 snapshotListener를 이용하여 실시간으로 갱신되어 update 할 수 있도록 unReadMessage를 구성했습니다.</p>
<pre><code>chatRoomDB.collection(COLLECTION_CHAT_MESSAGE)
  .whereArrayContains(&quot;read&quot;, mapOf(recipientUid to false))
  .addSnapshotListener { unReadMessage, e -&gt;
      chatRoomDB.update(&quot;unReadMessage&quot;, unReadMessage?.size())
}
</code></pre></li>
</ul>
<p>// * unReadMessage 갯수 갱신</p>
<pre><code>----</code></pre><p>data class MessageDataModel(
    val uid: String,
    val chatRoomId: String,
    val messageId: String,
    val message: String?,
    val imageList: List<ImageDataEntity>?,
    val sendAt: String,
    val read: List&lt;Map&lt;String, Boolean&gt;&gt;,
    val type: MessageViewType
)</p>
<p>enum class MessageViewType(val messageType: Int) {
    TEXT_MESSAGE(0),
    IMAGE_MESSAGE(1)
}</p>
<pre><code>- 메시지는 보낸 사람의 uid(고유 id를 통해서 그 사람의 프로필 정보를 가져옴) 메시지 고유 id, 보낸 시간, 읽음 여부, 메시지 타입(imageType or textType)을 담았습니다.

- 이미지 타입과 텍스트 타입 두가지 타입의 메시지를 보낼 수 있는데 메시지 타입일 때는 enum class로 타입을 나누어 multiView로 recyclerView를 나타낼 수 있도록 하였고 imageType일 때는 message 내용을 null로, textType일 때는 imageList를 null로 Firebase 채팅방에 보내줍니다.

----
채팅방과 채팅방을 구성하는 메시지에 대한 데이터 모델은 이렇게 설정하였습니다. 생각보다 처음 만들어보는 채팅이라서 그런지 생각해야할 부분이 많아서 데이터 모델만 해도 계속 추가하고 수정했었던 것 같습니다.
.
.
.
### 코드 구성

- 메시지를 보낼 때 생각할 경우의 수가 있는데 기존에 첫번째 메시지를 보내서 채팅방이 존재하는지 아닌지 체크를 해서 채팅방이 존재한다면 메시지만 보내야 하는 경우의 수가 있고 채팅방이 없다면 메시지를 보내기 전에 채팅방을 형성해주고 보내야 한다는 경우의 수가 있습니다.

#### 기존 채팅방이 없을 경우 (처음 메시지를 보낼 때)
![](https://velog.velcdn.com/images/one_coin/post/9380628d-7463-4c6b-a645-45f8e9bae1bf/image.png)
- 기존 채팅방이 없는 경우 서로 친구일 경우에 다음과 같이 상단에 친구 목록이 나타나게 되는데 채팅을 시작할 친구를 눌러서 채팅을 시작할 수 있습니다.

- 채팅방을 들어가면 먼저 채팅방이 있는지 없는지 Boolean으로 확인을 하고 채팅방이 없다면 false를 반환, 있다면 true를 반환하게 합니다.

- 채팅방이 있는지 없는지 체크하는 메서드는 LiveData로 observe하여 첫 메시지를 보내고 난 후 채팅방이 감지되면 바로 true를 반환합니다.

- 체크 메서드가 false로 결과 값을 반환하는 경우에 첫 메시지를 보내면 다음과 같이 Firebase에 채팅방에 대한 데이터가 set 됩니다.

![](https://velog.velcdn.com/images/one_coin/post/70688f0a-7936-4a99-aa1f-594450fdbce6/image.png)

---

#### 기존 채팅방이 있는 경우

- 기존 채팅방이 있는 경우는 간단합니다. 기존 채팅방의 고유 id를 받아와서 하위 document에 데이터를 set 해주는 것으로 처리합니다.

![](https://velog.velcdn.com/images/one_coin/post/f1e482c6-3ff3-463b-b0a1-64eb444734e3/image.png)

- 여기서 신경써야할 부분은 상대방이 지금 채팅방 안에 들어와 있는지 체크하여 Boolean 타입으로 반환시켜 줍니다. true일 경우는 상대방이 채팅방에 들어와 있는 경우로 생각하여 read의 value 값을 true로 서버에 업로드 시켜줍니다. 그럼 상대방은 메시지를 바로 읽었다고 값을 받게 되므로 바로 읽은 상태의 메시지가 채팅방에 나타납니다. 반대의 경우에는 value가 false이므로 읽지 않은 메시지로 채팅방에 표시됩니다.

---

#### 신경쓰지 못한 점

- 상대방이 채팅을 읽는 경우를 생각하지 못해서 데이터 모델도 다시 고치고 시간이 좀 걸렸던 것 같습니다. 

- 원래는 시간을 현재 사용하는 기기 시간인 LocalDateTime을 사용하여 작업을 진행하였는데 sortedDescending 이용하여 메시지 리스트를 adapter에 submitList하였을 때 순서대로 나오지 않는 경우가 발생하였습니다. 이 부분을 고치고자 한국 표준시를 이용하였고 이 부분에서 조금 아쉬웠던 점은 UTC를 활용하여 하지 못한 것 같아 다음에는 UTC+09와 같이 적용을 해볼까 생각하고 있습니다.</code></pre><p>fun chatDateFormat() : String {
    val formatter = SimpleDateFormat(&quot;yyyy-MM-dd HH:mm:ss:SSS&quot;, Locale.KOREAN)
    formatter.timeZone = TimeZone.getTimeZone(&quot;Asia/Seoul&quot;)
    return formatter.format(Date().time)
}
// * 한국 표준 시를 가져오는 코드</p>
<p>```</p>
<ul>
<li>개인적으로 채팅이 정말 신경쓸 부분이 많은 작업이라고 들었는데 실제로 멀티뷰타입, 채팅방이 존재하는지, 상대방이 채팅방에 들어와 있는지 등등 생각할 부분이 많았다고 생각합니다. 덕분에 조금 좋은 경험이었다고 생각합니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[안드로이드 4대 컴포넌트]]></title>
            <link>https://velog.io/@one_coin/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-4%EB%8C%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</link>
            <guid>https://velog.io/@one_coin/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-4%EB%8C%80-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8</guid>
            <pubDate>Thu, 06 Mar 2025 11:46:00 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/one_coin/post/0d54a7af-97ca-4c46-b08a-2f46f4463fa8/image.png" alt=""></p>
<h3 id="컴포넌트component란">컴포넌트(Component)란?</h3>
<ul>
<li>먼저 컴포넌트란 독립된 모듈로, 사용자 인터페이스의 기본적인 형태를 말합니다. 즉, 안드로이드에서의 컴포넌트란 안드로이드 앱을 구성하는 필요한 4개의 요소를 뜻합니다. 독립된 모듈이기 때문에 재사용이 가능하고 추후에 교환될 수도 있습니다.</li>
</ul>
<h3 id="그럼-안드로이드의-4대-컴포넌트는">그럼 안드로이드의 4대 컴포넌트는?</h3>
<ul>
<li>안드로이드의 4대 컴포넌트에는 <strong>액티비티(Activity)</strong>, <strong>서비스(Service)</strong>, <strong>방송 수신자(BroadCast Receiver)</strong>, <strong>콘텐츠 제공자(Content Provider)</strong>가 있습니다. 각 컴포넌트는 독립적인 형태로 존재하며 인텐트(Intent)를 통해서 상호작용합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/one_coin/post/a3f795b0-fd3b-4c48-a4b6-cb427335918e/image.png" alt=""></p>
<h3 id="각-컴포넌트는-어떤-역할을-하나요">각 컴포넌트는 어떤 역할을 하나요?</h3>
<blockquote>
</blockquote>
<h3 id="액티비티-activity">액티비티 (Activity)</h3>
<ul>
<li>액티비티는 사용자가 Application과 상호작용하며 실제로 사용자에게 보이는 화면을 의미합니다. Application에 화면이 없다면 사용자와 상호작용 할 수 없다는 의미와 같으므로 반드시 하나의 액티비티는 존재해야 합니다. <blockquote>
</blockquote>
</li>
<li>다른 Application의 액티비티 역시 인텐트를 통해 불러올 수 있으며 액티비티는 <strong>생명주기(Life Cycle)</strong> 관련 메서드를 재정의하여 원하는 기능들을 구현할 수 있습니다. <blockquote>
</blockquote>
</li>
<li>2개 이상의 액티비티를 동시에 Display 할 수 없고 1개 이상의 View 또는 ViewGroup을 포함합니다.</li>
</ul>
<blockquote>
</blockquote>
<h3 id="서비스-service">서비스 (Service)</h3>
<ul>
<li>서비스는 액티비티처럼 사용자와 직접적으로 상호작용하는 요소는 아닙니다. 
백그라운드(Background)에서 어떠한 작업을 처리하기 위해서 서비스를 사용하는데, 만약 Application이 종료되어도 백그라운드(Background)에서 동작하는 컴포넌트가 <strong>서비스</strong>이다.<blockquote>
</blockquote>
</li>
<li>대표적으로 음악 앱과 타이머 앱처럼 Background에서 계속 음악이 재생되거나 타이머가 작동되기 때문에 이 기능은 서비스에 해당됩니다.<blockquote>
</blockquote>
</li>
<li>서비스 같은 경우가 사용자의 인터페이스(UI, 화면)를 방해하지 않고 눈에 보이지 않는 곳에서 작업을 처리하기 때문에 별도의 스레드(Thread)에서 동작한다고 생각할 수 있지만, 서비스는 엄연히 <strong>메인 스레드(Main Thread)</strong> 에서 동작하기 때문에 서비스 내에서 별도의 스레드를 생성하여 작업을 처리해야 합니다. <blockquote>
</blockquote>
</li>
<li>서비스는 네트워크와 연동이 가능하고 별도의 UI를 가지지 않으며 액티비티와 서비스는 UI스레드라고 불리는 동일한 애플리케이션 스레드(Thread)로 실행됩니다.</li>
</ul>
<blockquote>
</blockquote>
<h3 id="방송-수신자-broadcast-receiver">방송 수신자 (BroadCast Receiver)</h3>
<ul>
<li>방송 수신자 (BroadCast Receiver)는 안드로이드 OS로부터 발생하는 각종 이벤트와 정보를 받아와 핸들링하는 컴포넌트입니다. 간단하게 말하면 안드로이드 시스템이나 다른 앱으로부터 전송되는 브로드캐스트 메시지를 수신하는 컴포넌트입니다.<blockquote>
</blockquote>
</li>
<li>사용자 안드로이드 디바이스의 시스템 부팅시 앱 초기화, 네트워크 끊김 등 특수한 이벤트에 대한 처리나 배터리 부족 알림, 문자 수신과 같은 정보를 받아 처리를 해야 할 필요가 있을 때 동작합니다. 즉, 안드로이드 OS에서 메신저 앱 또는 문자 메시지가 오면 모든 앱에 메시지에 대한 정보를 방송(BroadCast)을 합니다. 이 메시지를 받기 위해 <strong>브로드캐스트 리시버(BroadCast Receiver)</strong>를 구현하면 되고 해당 정보가 오면 특정 이벤트를 처리할 수 있습니다.<blockquote>
</blockquote>
</li>
<li><strong>브로드캐스트 리시버(BroadCast Receiver)</strong>는 대부분 UI를 가지지 않으며 안드로이드 디바이스의 특수한 상황에 대응하기 위해 사용됩니다.<blockquote>
</blockquote>
<ul>
<li>Broadcast (브로드 캐스트란)?
브로드 캐스트(Broadcast)는 컴퓨터 네트워크에서 전송되는 메시지를 일방적으로 모든 연결된 장치에게 보내는 통신 방식입니다.. 이 방식은 단일 송신자가 여러 수신자에게 동시에 메시지를 보낼 수 있도록 합니다.</li>
</ul>
</li>
</ul>
<blockquote>
</blockquote>
<h3 id="콘텐트-제공자-content-provider">콘텐트 제공자 (Content Provider)</h3>
<ul>
<li>콘텐트 제공자(Content Provider)는 데이터를 관리하고 다른 애플리케이션의 데이터를 제공하는데 사용되는 컴포넌트입니다. 특정한 애플리케이션이 사용하고 있는 데이터베이스(DB)를 공유하기 위해 사용하며 애플리케이션 간의 데이터 공유를 위해 표준화된 인터페이스를 제공합니다.<blockquote>
</blockquote>
</li>
<li>외부 애플리케이션이 현재 실행 중인 애플리케이션 내에 있는 데이터베이스(DB)에 함부로 접근하지 못하게 할 수 있으면서 나 자신이 공개하고 공유하고 싶은 데이터만 공유할 수 있도록 도와줍니다.<blockquote>
</blockquote>
</li>
<li>작은 데이터들은 인텐트(Intent)로 애플리케이션끼리 데이터를 서로 공유가 가능하지만 콘텐트 제공자(Content Provider)는 음악 또는 사진 파일 등과 같이 용량이 큰 데이터들을 공유하는데 적합합니다.<blockquote>
</blockquote>
</li>
<li>데이터베이스에서 흔히 사용되는 CRUD(Create, Read, Update, Delete) 원칙을 준수합니다.</li>
</ul>
<p>.
.
.</p>
<blockquote>
</blockquote>
<h3 id="인텐트-intent">인텐트 (Intent)</h3>
<ul>
<li>인텐트란 애플리케이션 컴포넌트 간에 작업 수행을 위한 정보를 전달하는 역할을 하며 통신수단이라고 보면 됩니다. 인텐트를 가장 많이 사용하는 예로는 액티비티 간의 화면 전환(이동)이 있습니다.<blockquote>
</blockquote>
</li>
<li>서로 독립적으로 동작하는 4가지 컴포넌트들 간의 상호 통신을 위한 장치로 컴포넌트에 액션(Action), 데이터(Data) 등을 전달합니다. 또한 인텐트(Intent)를 통하여 다른 애플리케이션의 컴포넌트를 활성화시킬 수 있습니다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[지도 api 비교해보기]]></title>
            <link>https://velog.io/@one_coin/%EC%A7%80%EB%8F%84-api-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@one_coin/%EC%A7%80%EB%8F%84-api-%EB%B9%84%EA%B5%90%ED%95%B4%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Wed, 16 Oct 2024 04:46:46 GMT</pubDate>
            <description><![CDATA[<p>이번 프로젝트를 진행하면서 카카오 지도 api와 네이버 지도 api를 사용하였습니다.
장소에 대한 정보는 카카오 지도 api로, 지도 자체는 네이버 지도 api를 사용하였는데 두 가지 지도 api에 관련하여 각자의 장단점을 한번 비교해볼까 합니다.</p>
<h3 id="카카오-지도-api">카카오 지도 api</h3>
<ul>
<li>카카오 지도 api는 다음과 같은 기능을 지원합니다.</li>
</ul>
<table>
<thead>
<tr>
<th><center>API 및 기능</center></th>
<th><center>설명</center></th>
</tr>
</thead>
<tbody><tr>
<td>주소 검색하기</td>
<td>특정 주소에 해당하는 장소 정보의 좌표를 검색합니다.</td>
</tr>
<tr>
<td>좌표로 행정구역정보 받기</td>
<td>좌표 값을 행정동, 법정동으로 변환합니다.맛집, 날씨 등 위치에 맞는 정보 제공 서비스에 활용할 수 있습니다.</td>
</tr>
<tr>
<td>좌표로 주소 변환하기</td>
<td>특정 좌표의 지번 주소 및 도로명 주소를 제공합니다.</td>
</tr>
<tr>
<td>좌표계 변환하기</td>
<td>특정 체계의 좌표 값을 다른 체계의 좌표 값으로 변환합니다. (지원 좌표계: WGS84, WCONGNAMUL, CONGNAMUL, WTM, TM, KTM, UTM, BESSEL, WKTM, WUTM)</td>
</tr>
<tr>
<td>키워드 검색하기</td>
<td>키워드로 관련 장소 및 상세 정보를 검색합니다. 각 장소 상세 페이지로 연결되는 URL을 제공합니다.</td>
</tr>
<tr>
<td>카테고리 검색하기</td>
<td>카테고리로 관련 장소 및 상세 정보 검색합니다. 각 장소 상세 페이지로 연결되는 URL을 제공합니다.</td>
</tr>
</tbody></table>
<p>여기서 쿼리에 맞는 검색 결과를 가져오기 위해서는 키워드 검색하기 기능을 통해서 정보를 받아올 수 있습니다.</p>
<p>키워드로 장소를 검색할 때는 다음과 같이 친절하게 안내가 되어있습니다.
<img src="https://velog.velcdn.com/images/one_coin/post/9970f5bd-5623-476c-bcfa-d1b39dc80ad3/image.png" alt=""></p>
<ul>
<li>GET 메서드를 통해서 URL에 자료형만 정해주면 됩니다. (저의 경우엔 JSON으로)</li>
</ul>
<p>요청 단계에서는 다음과 같습니다.
<img src="https://velog.velcdn.com/images/one_coin/post/61d5b9a6-2710-4ab0-a409-18be17523446/image.png" alt=""></p>
<ul>
<li>HEADER에 카카오 REST_API_KEY를 Authorization에 넣어주고
<img src="https://velog.velcdn.com/images/one_coin/post/815c7fa5-73b6-4aad-a2c5-29bd40bc6e76/image.png" alt=""></li>
<li>다음과 같은 쿼리 파라미터를 통해 결과를 요청할 수 있습니다.
저는 크게 query(키워드)와 page, size 정도 사용하여 결과 값을 받아왔습니다.</li>
</ul>
<h4 id="응답-결과">응답 결과</h4>
<blockquote>
</blockquote>
<pre><code>HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
  &quot;meta&quot;: {
    &quot;same_name&quot;: {
      &quot;region&quot;: [],
      &quot;keyword&quot;: &quot;카카오프렌즈&quot;,
      &quot;selected_region&quot;: &quot;&quot;
    },
    &quot;pageable_count&quot;: 14,
    &quot;total_count&quot;: 14,
    &quot;is_end&quot;: true
  },
  &quot;documents&quot;: [
    {
      &quot;place_name&quot;: &quot;카카오프렌즈 코엑스점&quot;,
      &quot;distance&quot;: &quot;418&quot;,
      &quot;place_url&quot;: &quot;http://place.map.kakao.com/26338954&quot;,
      &quot;category_name&quot;: &quot;가정,생활 &gt; 문구,사무용품 &gt; 디자인문구 &gt; 카카오프렌즈&quot;,
      &quot;address_name&quot;: &quot;서울 강남구 삼성동 159&quot;,
      &quot;road_address_name&quot;: &quot;서울 강남구 영동대로 513&quot;,
      &quot;id&quot;: &quot;26338954&quot;,
      &quot;phone&quot;: &quot;02-6002-1880&quot;,
      &quot;category_group_code&quot;: &quot;&quot;,
      &quot;category_group_name&quot;: &quot;&quot;,
      &quot;x&quot;: &quot;127.05902969025047&quot;,
      &quot;y&quot;: &quot;37.51207412593136&quot;
    },
    ...
  ]
}</code></pre><ul>
<li>카카오 지도 api에서 예시로 보여준 응답 결과입니다. query로 카카오 프렌즈를 넣어서 요청하였을 때의 결과로 저는 여기서 phone, place_url, address_name, place_name, x, y 값을 사용하였습니다. 
x, y는 경도와 위도 값으로 지도 위에 마커를 찍기위해 필요한 부분이라서 가져왔습니다.</li>
</ul>
<h4 id="요금">요금</h4>
<ul>
<li>카카오 지도 api의 경우에는 1일 300,000회 무료로 사용 가능한데 혼자 프로젝트를 진행하거나 개발 공부를 하실 때는 부담없이 사용 가능 할 것 같습니다.</li>
</ul>
<h4 id="장점">장점</h4>
<ul>
<li>개인적으로 이번에 프로젝트에서 카카오 지도 검색 api를 사용한 이유는 제가 필요한 정보를 제공해주는 점(장소에 대한 url, 전화번호 등)이 제일 컸습니다. 좀 다양한 정보를 받아서 사용자에게 보여주고 싶었던 점이 컸기 때문에 선택하게 되었습니다.</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li><p>네이버 지도 api에 비해서 검색 결과가 사실 그렇게 정확하게 나오지는 않았던 것 같습니다. 테스트를 하는 과정에서 문제가 있었을 수도 있겠지만 네이버 지도 api에 비해서 결과 값의 정확도, 상위 결과 값의 선정 기준?? 을 잘 모르겠었습니다. 그 외에는 다 만족했습니다!</p>
</li>
<li><p>이건 조금 다른 이야기지만 지금 실 기기가 아닌 에뮬레이터에서 테스트를 해보고 있다 보니
<a href="https://devtalk.kakao.com/t/x86-x86-64/42582">https://devtalk.kakao.com/t/x86-x86-64/42582</a>
위의 링크와 같은 오류가 생겼습니다. 그래서 문제 해결 방법으로 카카오 지도 api에서 검색 결과 값을, 네이버 지도 api를 화면에 나타내 주는 방법을 선택한 이유도 있습니다.</p>
</li>
</ul>
<h3 id="네이버-지도-api">네이버 지도 api</h3>
<ul>
<li>네이버 지도 api는 다음과 같은 기능을 제공합니다.</li>
</ul>
<table>
<thead>
<tr>
<th><center>API</center></th>
<th><center>설명</center></th>
</tr>
</thead>
<tbody><tr>
<td>Static Map</td>
<td>정적 지도 이미지 생성</td>
</tr>
<tr>
<td>Directions 5</td>
<td>경로 탐색(경유지 최대 5개)</td>
</tr>
<tr>
<td>Directions 15</td>
<td>경로 탐색(경유지 최대 15개)</td>
</tr>
<tr>
<td>Geocoding</td>
<td>주소 검색</td>
</tr>
<tr>
<td>Reverse Geocoding</td>
<td>좌표를 주소로 변환</td>
</tr>
</tbody></table>
<p>네이버 지도는 키워드를 검색할 수 없고 주소를 검색해서 장소에 대한 정보를 얻을 수 있었습니다.
정확도는 네이버 지도 api가 더 좋았지만 다양한 결과를 얻을 수 있었던 것은 카카오 지도 api였던 이유가 이런 점도 있었습니다. </p>
<p><img src="https://velog.velcdn.com/images/one_coin/post/2a308a0d-7126-4d67-9a44-6ceed90eaad9/image.png" alt=""></p>
<ul>
<li>네이버 지도 api의 요청 예시는 다음과 같습니다. 카카오와 비슷하게 GET 메서드를 통해서 받을 수 있으며 카카오와 다른 점은 JSON 형식의 자료형만 지원합니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/one_coin/post/0bba6c92-480f-4599-8576-8d22eb8b1906/image.png" alt=""></p>
<ul>
<li>헤더에서 응답 데이터의 형식을 정해주는 것으로 보이고 api key도 헤더로 요청하는 것으로 보입니다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/one_coin/post/6008fe6a-4e22-4a1d-8e7d-7d334087a20e/image.png" alt=""></p>
<ul>
<li>요청 쿼리 파라미터로는 다음과 같이 요청할 수 있고 카카오와 마찬가지로 query, page, count 세 가지를 요청해보았습니다.</li>
</ul>
<p>(개인적으로 공식 문서 자체는 카카오가 조금 더 잘 알려주는 느낌?? 이 있었던 것 같습니다.)</p>
<h4 id="응답-결과-1">응답 결과</h4>
<blockquote>
</blockquote>
<pre><code>{
    &quot;status&quot;: &quot;OK&quot;,
    &quot;meta&quot;: {
        &quot;totalCount&quot;: 1,
        &quot;page&quot;: 1,
        &quot;count&quot;: 1
    },
    &quot;addresses&quot;: [
        {
            &quot;roadAddress&quot;: &quot;경기도 성남시 분당구 불정로 6 NAVER그린팩토리&quot;,
            &quot;jibunAddress&quot;: &quot;경기도 성남시 분당구 정자동 178-1 NAVER그린팩토리&quot;,
            &quot;englishAddress&quot;: &quot;6, Buljeong-ro, Bundang-gu, Seongnam-si, Gyeonggi-do, Republic of Korea&quot;,
            &quot;addressElements&quot;: [
                {
                    &quot;types&quot;: [
                        &quot;SIDO&quot;
                    ],
                    &quot;longName&quot;: &quot;경기도&quot;,
                    &quot;shortName&quot;: &quot;경기도&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;SIGUGUN&quot;
                    ],
                    &quot;longName&quot;: &quot;성남시 분당구&quot;,
                    &quot;shortName&quot;: &quot;성남시 분당구&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;DONGMYUN&quot;
                    ],
                    &quot;longName&quot;: &quot;정자동&quot;,
                    &quot;shortName&quot;: &quot;정자동&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;RI&quot;
                    ],
                    &quot;longName&quot;: &quot;&quot;,
                    &quot;shortName&quot;: &quot;&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;ROAD_NAME&quot;
                    ],
                    &quot;longName&quot;: &quot;불정로&quot;,
                    &quot;shortName&quot;: &quot;불정로&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;BUILDING_NUMBER&quot;
                    ],
                    &quot;longName&quot;: &quot;6&quot;,
                    &quot;shortName&quot;: &quot;6&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;BUILDING_NAME&quot;
                    ],
                    &quot;longName&quot;: &quot;NAVER그린팩토리&quot;,
                    &quot;shortName&quot;: &quot;NAVER그린팩토리&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;LAND_NUMBER&quot;
                    ],
                    &quot;longName&quot;: &quot;178-1&quot;,
                    &quot;shortName&quot;: &quot;178-1&quot;,
                    &quot;code&quot;: &quot;&quot;
                },
                {
                    &quot;types&quot;: [
                        &quot;POSTAL_CODE&quot;
                    ],
                    &quot;longName&quot;: &quot;13561&quot;,
                    &quot;shortName&quot;: &quot;13561&quot;,
                    &quot;code&quot;: &quot;&quot;
                }
            ],
            &quot;x&quot;: &quot;127.1054328&quot;,
            &quot;y&quot;: &quot;37.3595963&quot;,
            &quot;distance&quot;: 0.0
        }
    ],
    &quot;errorMessage&quot;: &quot;&quot;
}</code></pre><ul>
<li>네이버 지도 api 공식 문서에 있는 응답 결과 예시입니다. 주소에 대한 내용은 자세하게 되어있지만 그 외에 부가적인 사항은 받을 수 없더라구요 ㅠㅠ 
(물론 제가 잘 못 찾아본 것일 수도 있습니다!) <blockquote>
</blockquote>
</li>
<li>그래서 검색에 대한 조금 부가사항을 사용하고 싶어서 장소에 대한 검색은 카카오 지도 api를 사용하기로 한 것이었습니다.</li>
</ul>
<h4 id="요금-1">요금</h4>
<p>네이버 지도에서 방금 사용한 Geocoding의 경우에는 요금이 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th><center>서비스 구분</center></th>
<th><center>과금 기준</center></th>
<th><center>과금 구간</center></th>
<th><center>요금</center></th>
<th><center>비고</center></th>
</tr>
</thead>
<tbody><tr>
<td>Geocoding</td>
<td>이용 횟수</td>
<td>3,000,000건 이하</td>
<td>무료</td>
<td>※ 대표 계정 1개에 한해 월 3,000,000건의 무료 이용량을 제공합니다.</td>
</tr>
<tr>
<td>Geocoding</td>
<td>이용 횟수</td>
<td>3,000,000건 초과</td>
<td>0.5원</td>
<td>※ 대표 계정 1개에 한해 월 3,000,000건의 무료 이용량을 제공합니다.</td>
</tr>
</tbody></table>
<ul>
<li>카카오와 마찬가지로 혼자서 공부를 하거나 작은 규모의 프로젝트라면 전혀 문제되지 않을 이용 횟수 입니다.</li>
</ul>
<h4 id="장점-1">장점</h4>
<ul>
<li>주소를 기반으로 하는 프로젝트라면 훨씬 좋을 것 같습니다. 검색 결과가 정확하기도 하고 주소를 세분화 하여 분류 할 수도 있기 때문에 지도가 메인 기능인 지도 기반 프로젝트일 경우에는 네이버 지도 사용도 고려해 볼 수 있을 것 같습니다.</li>
</ul>
<h4 id="단점-1">단점</h4>
<ul>
<li>단순히 서브의 느낌으로 사용하기에는 내용이 조금 부족한 느낌이 있습니다. 네이버 검색 엔진을 사용하면 더 좋을 수도 있겠지만 지도 api안에서 비교를 해보자면 단순히 주소만 보여주는 느낌이라서 활용성이 적을 수도 있을 것 같다는 느낌이 있습니다.</li>
</ul>
<p>.
.
.
.
.</p>
<h4 id="결론">결론</h4>
<ul>
<li><p>예전에 구글 지도를 사용했을 때는 정보는 좋았지만 국내 서비스에는 조금 불편한 점이 있었고 국내 서비스를 목표로 지도 api를 사용한다면 카카오 api와 네이버 api를 고려해 볼 수 있는데  </p>
<p>정확한 내용과 장소에 대한 세분화를 원한다면 네이버 지도 api를, 부가적인 정보의 활용을 원한다면 카카오 지도 api의 사용이 좋을 것 같습니다. 물론 개인적인 견해이긴 하지만...사용해보니 그런 느낌을 받은 것 같습니다. </p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[Join Two Collections (Firebase)]]></title>
            <link>https://velog.io/@one_coin/Join-Two-Collections-Firebase</link>
            <guid>https://velog.io/@one_coin/Join-Two-Collections-Firebase</guid>
            <pubDate>Mon, 07 Oct 2024 16:06:12 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Firebase에서 collection 안에 정보를 직접 저장하는 것이 아닌 id를 통해 가져오는 방법으로 데이터 구조를 변경해보려고 합니다.</p>
<h3 id="1-데이터-구조-변경">1. 데이터 구조 변경</h3>
<blockquote>
</blockquote>
<p><strong>변경전</strong></p>
<pre><code>data class PostDataModel(
    val uid: String,                ─┐
    val profileImage: String,        │
    val name: String,                │  → user data
    val email: String,               │
    val postId: String,             ─┘
    val imageList = imageList,            ─┐
    val postText: String,                  │
    val mapData = MapDataModel(),          │   → post data
    val createdAt: String                 ─┘   
)</code></pre><ul>
<li>원래는 다음과 같이 포스트에 대한 data class 안에 고유 아이디 값, 이름, 이메일, 프로필 사진 등 유저의 정보를 직접 참조 할 수 있도록 데이터 구조를 구성했었는데, post data를 가져올 때 user라는 collection을 참조하여 uid 값에 맞는 user 정보를 가져오는 것으로 데이터 구조를 변경해 보려고 합니다. 그렇게 되면 post data 안에는 고유 아이디 값인 uid만 넣어주면 되는 것이므로 한층 간결해 질 것으로 예상됩니다.<blockquote>
</blockquote>
↓
↓
↓<blockquote>
</blockquote>
</li>
<li><em>변경후*</em><pre><code>data class PostDataModel(
   val uid: String,
   val postId: String,
   val imageList = imageList,
   val postText: String,
   val mapData = MapDataModel(),
   val createdAt: String
)
&gt;
data class UserDataModel(
   val uid: String,
   val name: String,
   val email: String,
   val profileImage: String?,
   val createdAt: String,
   val intro: String?
)
&gt;
// join two collections
data class PostModel(
   val userData: UserDataModel,
   val postData: PostDataModel
)</code></pre></li>
<li>user data와 post data를 분리하여 주고 post data를 작성한 유저의 uid만 post data 안에 넣어줍니다. 게시글을 작성하는건 현재 유저만 가능한 일이므로 current user의 uid가 들어가면 되겠습니다. 여기서 기존과 다른 점은 data class를 하나 더 생성하여 post data와 user data를 같이 받아올 수 있는 data class를 하나 만들어줘야 합니다. 예시에서 PostModel이<br>그 data class 입니다. 이제 repository에서 로직을 작성해 보겠습니다.</li>
</ul>
<h3 id="2-repositoryimpl">2. repositoryImpl</h3>
<p>현재 작성중인 프로젝트의 repositoryImpl의 구문을 paging 작업 빼고 가져왔습니다.</p>
<blockquote>
</blockquote>
<pre><code>db.collection(&quot;post&quot;)
    .orderBy(&quot;createdAt&quot;, Query.Direction.DESCENDING)
    .get()
    .addOnSuccessListener { postData -&gt;
        val documents = postData.documents
        val postResponse = postData.toObjects(PostDataResponse::class.java)
        postResponse.map { postEntity -&gt;
            db.collection(&quot;user&quot;).document(postEntity.uid).get()
            .addOnSuccessListener { userData -&gt;
                val userEntity = userData.toObject(UserDataResponse::class.java)?.toEntity()
                if (userEntity != null) {
                    postList.addAll(listOf(PostEntity(userEntity, postEntity)))
                }
                postDocuments.addAll(documents)
                trySend(postList)
        }
    }
}</code></pre><hr>
<pre><code>1. post data 가져오기
&gt;
db.collection(&quot;post&quot;).orderBy(&quot;createdAt&quot;, Query.Direction.DESCENDING).get()
&gt;
- 먼저 fireStore database의 post collection에서 정보를 가져옵니다. </code></pre><hr>
<pre><code>2. 가져온 post data를 data class로 변환시켜주기
&gt;
.addOnSuccessListener { postData -&gt;
    val documents = postData.documents
    val postResponse = postData.toObjects(PostDataResponse::class.java)
&gt;
- addOnSuccessListener는 firebase에서 지원해주는 로직으로 데이터를 받아오는 것이 성공했을 때를 의미합니다.
- 받아온 data의 documents를 data class로 변환(toObjects) 해줍니다.</code></pre><hr>
<pre><code>3. user data 가져오기
&gt;
postResponse.map { postEntity -&gt;
    db.collection(&quot;user&quot;).document(postEntity.uid).get()
&gt;
- toObjects 시켜준 데이터에서 각 data 마다의 uid를 가져와서 uid에 맞는 user data를 가져와 줍니다.
- 네이밍이 조금 이상하게 되어있긴합니다...ㅠㅠ</code></pre><hr>
<pre><code>4. list 전달하기
&gt;
.addOnSuccessListener { userData -&gt;
    val userEntity = userData.toObject(UserDataResponse::class.java)?.toEntity()
    if (userEntity != null) {
        postList.addAll(listOf(PostEntity(userEntity, postEntity)))
    }
    postDocuments.addAll(documents)
    trySend(postList)
        }
    }
&gt;
- 가져온 user data도 data class로 toObject 해주고 postList를 만들어서 안에 addAll 해주었습니다.
- 이때 list에 들어가야 하는 data는 아까 전에 만들어둔 PostModel 입니다!
&gt;
                    ↓
&gt;
data class PostModel(
    val userData: UserDataModel,
    val postData: PostDataModel
)</code></pre><ul>
<li>좀 뒤죽박죽 작성한 것 같긴한데 핵심은 이렇습니다.</li>
</ul>
<ol>
<li>user data와 post data를 분리한다.</li>
<li>post data를 가져올 때 각 post data의 uid를 참조하여 uid 값에 맞는 user data를 가져온다.</li>
<li>두가지 데이터를 참조하는 data class를 만들어 list로 받아준다.</li>
</ol>
<h3 id="3-완">3. 완</h3>
<p>이렇게 하여 data를 받아주면 collection을 따로 관리할 수 있어서 더 편리하기도 하고 나중에 프로젝트의 규모가 커질수록 더 효율적일 것 같습니다! 규모가 커진다는 것은 그만큼 관리해야 하는 데이터가 늘어난다는 것인데 그때도 모든 유저의 데이터를 각 댓글, 게시글, 대댓글 마다 넣어준다면 쓸모없는 데이터를 계속하여 넣어주는 느낌이지 않을까 싶습니다.</p>
<p>처음 data 구조를 구성할 때도 이게 맞나 긴가민가 했지만 좋은 선생님 덕분에 방법을 알아간 것 같아서 좋습니다!</p>
<p>더 깊게 공부하고 data 구조를 공부하려면 mongoDB, SQL Join 등 키워드로 검색 해봐야 겠습니다!
.
.
.</p>
<h4 id="참고문헌">참고문헌</h4>
<p><a href="https://stackoverflow.com/questions/52696311/cloud-firestore-how-to-get-relational-data-from-two-collections">https://stackoverflow.com/questions/52696311/cloud-firestore-how-to-get-relational-data-from-two-collections</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[FireStore로 게시물 만들기]]></title>
            <link>https://velog.io/@one_coin/FireStore%EB%A1%9C-%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@one_coin/FireStore%EB%A1%9C-%EA%B2%8C%EC%8B%9C%EB%AC%BC-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sun, 01 Sep 2024 17:11:09 GMT</pubDate>
            <description><![CDATA[<p>오늘은 Firebase의 FireStore로 게시물을 만드는 법을 알아보겠습니다.</p>
<ul>
<li><h3 id="firebase-설정">Firebase 설정</h3>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/98dfe3ab-bc91-4722-bf68-0da8354b63e1/image.png" alt=""><blockquote>
</blockquote>
먼저 FireStore Storage를 생성해주면 되는데 위치를 설정해주고<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/9117b43c-09c1-4ca8-8287-cc6241627a6e/image.png" alt=""><blockquote>
</blockquote>
테스트 모드로 시작해줍니다.
(프로덕션 모드에서 시작해서 읽기/쓰기 권한을 true로 변경해줘도 될 것 같습니다!)</li>
</ul>
<ul>
<li><h3 id="프로젝트-설정">프로젝트 설정</h3>
<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/ec42cdcf-534a-4481-a481-99c103aef6ed/image.png" alt=""><blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/3f085af9-cf96-40e8-8c24-011e61afd58d/image.png" alt=""><blockquote>
</blockquote>
프로젝트에서 Tools를 통해 Cloud Firestore를 추가하셔도 되고</li>
</ul>
<pre><code>dependencies {
    implementation(&quot;com.google.firebase:firebase-firestore:25.0.0&quot;)
}</code></pre><blockquote>
</blockquote>
<p>다음과 같이 의존성을 추가해도 됩니다.</p>
<ul>
<li><h2 id="게시물-생성">게시물 생성</h2>
</li>
</ul>
<h3 id="ui-layer">UI layer</h3>
<p>먼저 데이터를 받아줍니다.</p>
<blockquote>
</blockquote>
<pre><code>private fun initData() {
        val auth = CurrentUser.userData
        val uid = auth?.uid.toString()
        val profileImage = auth?.profileImage
        val name = auth?.name.toString()
        val email = auth?.email.toString()
        val postText = binding.etMakeText.text.toString()
        val time = LocalDateTime.now()
&gt;
        val data = PostDataModel(
            uid = uid,
            postId = UUID.randomUUID().toString(),
            profileImage = profileImage,
            name = name,
            email = email,
            imageList = imageList,
            postText = postText,
            mapData = MapDataModel(placeName, addressName, lat?.toDouble(), lng?.toDouble()),
            createdAt = dateFormat(time)
        )
}</code></pre><blockquote>
</blockquote>
<ul>
<li>여기서 CurrnetUser는 MainFragment가 onCreated 될 때 값을 초기화 해준 object 입니다.
(data class CurrentUserModel을 반환값으로 가짐)<blockquote>
</blockquote>
</li>
<li>게시물 생성은 현재 유저의 uid, name, email, profileImage 등을 보여주므로 현재 유저의 정보를 담아줍니다.<blockquote>
</blockquote>
</li>
<li>postId는 uid와 마찬가지로 각 게시물마다 고유의 값이 존재해야 수정, 삭제 등에서 혼선이 없고 식별이 가능하기 때문에 randomUUID를 통해 값을 지정해주었습니다.<blockquote>
</blockquote>
</li>
<li>createdAt 즉, 생성 시기는 게시물을 생성된 시기에 따라 순차적으로 보여주기 위해 받았고 추후에 게시물 생성 몇 초전, 몇 분전과 같이 생성 시간에 따른 로직을 추가할 예정이기 때문에 넣어주었습니다. 
(여기서 dateFormat은 util로 빼준 함수입니다.)
```
fun dateFormat(time: LocalDateTime) : String {
  return time.format(DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;))
} <blockquote>
</blockquote>
</li>
<li>시간만 넣으면 변환 (편안) -
```</li>
<li>mapData는 data class MapDataModel로 받고 있는데 장소 이름과 주소, 경도, 위도를 받고 있습니다.
현재 장소 이름은 null이 아닐 경우에 게시물에 표시되고 있고 추후에 naver 지도를 통해 받은 경도, 위도를 입력해서 지도에 위치를 나타내줄 생각으로 경도, 위도까지 같이 받고 있습니다.<blockquote>
</blockquote>
</li>
</ul>
<blockquote>
</blockquote>
<p>사실 mapData를 받는 과정에서 viewModel을 사용했는데 data가 들어오지않아 새로 배운 기술을 사용해 보았습니다. 
(후에 알았지만 fragment 생명주기의 영향을 받는 by viewModels를 사용했을 때는 안되던 것이 
by activityViewModels를 사용하니 받아지더라구요...이 부분도 공부해서 다음에 포스트 작성해야겠습니다.)</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/fc094612-72bb-45ee-a012-6f1bddffe230/image.png" alt=""><blockquote>
</blockquote>
현재 다음과 같이 바텀시트를 이용해서 검색 결과를 보여주고 아이템 클릭 리스너를 통해 클릭 시 데이터를 bottomSheetFragment.popBackStack() 하면서 전달해주어야 하는데 viewModel을 사용하지 않고 적용하려하니 좀 어려웠던 것 같다 ㅠㅠ...<blockquote>
</blockquote>
검색하면서 알아보다보니 Fragment Result API를 이용해서 값을 전달해 줄 수 있다는 것을 알았고 한번 사용해보았다!<blockquote>
</blockquote>
Fragment 간 result를 전달 하기 위해 사용하는데 간단하다.
전달하고 싶은 데이터를 담아서 클릭 리스너에 연결시켜주었다.</li>
</ul>
<p><strong>MapBottomSheetFragment -&gt; SendData</strong></p>
<blockquote>
</blockquote>
<pre><code>Recyclerview ClickListener -&gt;
&gt;
&gt;
val mapAdapter = KakaoMapListAdapter { data -&gt;
            sendMapData(data)
            findNavController().popBackStack()
        }
        with(binding.rvMap) {
            LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
            adapter = mapAdapter
        }
    }
&gt;
&gt;
&gt;
sendMapData -&gt; 
&gt;
&gt;
private fun sendMapData(mapData: KakaoDocumentsModel) {
        val bundle = Bundle()
        bundle.putString(&quot;placeName&quot;, mapData.placeName)
        bundle.putString(&quot;addressName&quot;, mapData.addressName)
        bundle.putString(&quot;lat&quot;, mapData.lat)
        bundle.putString(&quot;lng&quot;, mapData.lng)
        setFragmentResult(&quot;data&quot;, bundleOf(&quot;mapData&quot; to bundle))
    }</code></pre><p>.
.
mapData를 번들로 담아서 recyclerview item 클릭 시 Fragment Result API로 넘겨주었다.</p>
<p><strong>MakePostFragment -&gt; RecieveData</strong></p>
<blockquote>
</blockquote>
<pre><code>private fun getMapData() {
&gt;
        setFragmentResultListener(&quot;data&quot;) { data, bundle -&gt;
            val mapData = bundle.getBundle(&quot;mapData&quot;)
            if (mapData != null) {
                binding.clMakeLocationText.visibility = View.VISIBLE
                binding.tvMakeLocationName.text = mapData.getString(&quot;placeName&quot;)
                binding.tvMakeLocationInfo.text = mapData.getString(&quot;addressName&quot;)
&gt;
                placeName = mapData.getString(&quot;placeName&quot;)
                addressName = mapData.getString(&quot;addressName&quot;)
                lat = mapData.getString(&quot;lat&quot;)
                lng = mapData.getString(&quot;lng&quot;)
&gt;
            } else {
                binding.clMakeLocationText.visibility = View.GONE
            }
&gt;
            binding.ivMakeMapDelete.setOnClickListener {
                mapData?.clear()
                binding.clMakeLocationText.visibility = View.GONE
            }
        }
    }</code></pre><p>데이터를 전달해줬으면 받는 쪽에서는 setFragmentResultListener를 통해 key값으로 data를 받을 수 있다. </p>
<blockquote>
</blockquote>
<p>지금은 해결방법을 알아서 viewModel을 통해서 데이터를 전달하고 사용하지만 Fragment Result API를 통해서 데이터를 전달하고 받는 과정도 공부해봐서 좋았던 것 같다.
.
.
.
이제 데이터가 준비됐다면 Firebase FireStore로 데이터를 올려줄 차례이다.
domain layer에서 먼저 작업을 해보자!</p>
<hr>
<h3 id="domain-layer">Domain layer</h3>
<blockquote>
</blockquote>
<p>먼저 repository에서 함수를 작성해준다.</p>
<blockquote>
</blockquote>
<p><strong>DataRepository</strong></p>
<pre><code>interface DataRepository {
    suspend fun uploadPost(postData: PostDataEntity): Flow&lt;Boolean&gt;
}</code></pre><blockquote>
</blockquote>
<p>그 다음 useCase를 작성하고 data layer로 넘어가보자</p>
<blockquote>
</blockquote>
<p><strong>UploadPostUseCase</strong></p>
<pre><code>class UploadPostUseCase @Inject constructor(private val dataRepository: DataRepository) {
    suspend operator fun invoke(postData: PostDataEntity) : Flow&lt;Boolean&gt; {
        return dataRepository.uploadPost(postData)
    }
}</code></pre><p>(invoke에 관해서도 한번 공부할 필요가 있어보인다...)</p>
<hr>
<h3 id="data-layer">Data layer</h3>
<blockquote>
</blockquote>
<p><strong>DataRpositoryImpl</strong>
impl에서 먼저 constructor로 auth, firestore, storage를 생성해준다.</p>
<pre><code>class DataRepositoryImpl @Inject constructor(
    private val auth: FirebaseAuth,
    private val db: FirebaseFirestore,
    private val storage: FirebaseStorage
): DataRepository</code></pre><p>storage는 따로 소개가 되지 않았지만 이미지는 firestore에 바로 올릴 수 없기 때문에 storage에 저장한 다음 downloadUrl을 통해 가져와야 부를 수 있습니다!</p>
<blockquote>
</blockquote>
<p>(storage도 firestore 처럼 종속성 추가하면 됩니다!! 과정이 같아서 생략...ㅎ)</p>
<hr>
<p>그 다음 아까 작성해둔 uploadPost를 override하여 여기서 구현하면 된다.</p>
<blockquote>
</blockquote>
<pre><code>override suspend fun uploadPost(postData: PostDataEntity): Flow&lt;Boolean&gt; {
        return flow {
            try {
                val currentUser = auth.currentUser?.uid
                val imageList = arrayListOf&lt;ImageDataEntity&gt;()
                if (currentUser != null) {
                    val data = hashMapOf(
                        &quot;postId&quot; to postData.postId,
                        &quot;uid&quot; to currentUser,
                        &quot;profileImage&quot; to postData.profileImage,
                        &quot;name&quot; to postData.name,
                        &quot;email&quot; to postData.email,
                        &quot;postText&quot; to postData.postText,
                        &quot;mapData&quot; to postData.mapData,
                        &quot;createdAt&quot; to postData.createdAt
                    )
                    db.collection(&quot;post&quot;).document(postData.postId).set(data).await()
                    &gt;
                    if (postData.imageList != null) {
                        for (i in 0 until postData.imageList.count()) {
                            val imageToUri = postData.imageList[i].imageUri.toUri()
                            val storageRef =
                                storage.getReference(&quot;image&quot;)
                                    .child(&quot;${currentUser}/${postData.postId}/${postData.createdAt}_${i}&quot;)
                            if (imageToUri.pathSegments?.contains(&quot;video&quot;) == true) {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri -&gt;
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                &quot;video&quot;
                                            )
                                        )
                                        val imageData =
                                            mapOf(&quot;imageList&quot; to imageList.sortedBy { it.imageUri })
                                        db.collection(&quot;post&quot;).document(postData.postId)
                                            .update(imageData)
                                    }
                                }
                            } else {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri -&gt;
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                &quot;image&quot;
                                            )
                                        )
                                        val imageData =
                                            mapOf(&quot;imageList&quot; to imageList.sortedBy { it.imageUri })
                                        db.collection(&quot;post&quot;).document(postData.postId)
                                            .update(imageData)
                                    }
                                }
                            }
                        }
                    }
                    emit(true)
                }
&gt;
            } catch (e: Exception) {
                emit(false)
            }
        }
    }</code></pre><p>코드가 좀 길긴한데 코드를 더럽게 못짜서 그런가 싶다...하나씩 살펴보자</p>
<blockquote>
</blockquote>
<p>먼저 아래 코드로 데이터를 firestore에 올려준다.
db.collection(&quot;post&quot;).document(postData.postId).set(data).await()</p>
<blockquote>
</blockquote>
<p><strong>여기서!</strong> 굳이 데이터를 hashMap의 형태로 바꿔줄 필요는 없고 나는 image 데이터를 따로 올려주려고 하다보니 한번 다시 하나하나 직접 바꿔주었다. data class 형태 그대로 올려도 무방하다.</p>
<blockquote>
</blockquote>
<p>그리고 image 데이터가 사실 거의 메인인데...
for 문을 통해 0 부터 imageList.count() or imageList.size까지 imageData의 downloadUrl을 해당 post에 updata 시켜준다. storage에 있는 이미지들을 한번에 변환시켜줄 수 있는 방법이 있는 지는 모르겠지만...내가 알아본 정보로는 없어서...그렇다 ㅠㅠ</p>
<blockquote>
</blockquote>
<p>다음 구문을 통해 내가 저장하고 싶은 image의 이름과 경로를 지정해준다.</p>
<pre><code>storage.getReference(&quot;image&quot;).child(&quot;${currentUser}/${postData.postId}/${postData.createdAt}_${i}&quot;)</code></pre><p>그 다음 나는 video와 image 두 가지 타입을 구분해줘야 했기 때문에 다음 구문을 작성했다.</p>
<pre><code>  if (imageToUri.pathSegments?.contains(&quot;video&quot;) == true) {
                                storageRef.putFile(imageToUri).addOnSuccessListener {
                                    storageRef.downloadUrl.addOnSuccessListener { downloadUri -&gt;
                                        imageList.add(
                                            ImageDataEntity(
                                                downloadUri.toString(),
                                                &quot;video&quot;
                                            )
                                        )
                                        val imageData =
                                            mapOf(&quot;imageList&quot; to imageList.sortedBy { it.imageUri })
                                        db.collection(&quot;post&quot;).document(postData.postId)
                                            .update(imageData)
                                    }
                                }</code></pre><p>if 문을 통해 만약 imageUri에 &quot;video&quot;가 포함되어 있다면
(이 부분은 Log로 직접 image와 video의 다른 점을 찾아야 합니다! 제가 사용한 imagePicker는 달라서...)</p>
<blockquote>
</blockquote>
<p>putFile 후 addOnSuccessListener로 putFile에 성공했을 때 storageRef.downloadUrl에서 downloadUri를 가져옵니다. (이 친구가 중요합니다!)</p>
<blockquote>
</blockquote>
<p>가져온 uri를 imageList에 담아줍니다. 저는 추후에 구분을 편하게 하기 위해 type: String과 Uri: String으로 값을 받아줬습니다.</p>
<blockquote>
</blockquote>
<p>이제 imageData를 아까 다른 postData를 set해준 경로에 update 시켜주면 됩니다.
(이미지도 같은 방법으로 pathSegments에 contain(&quot;image&quot;)일 경우에 다음과 같이 진행해주면 됩니다!)</p>
<h3 id="결과화면">결과화면</h3>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/5ef9bd7e-3ce4-4d54-86a2-3a1974e4f9fd/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/2003296b-f079-4c38-a28b-2d5957b8b5e5/image.png" alt=""></li>
</ul>
<p>다음과 같이 데이터가 잘 들어오는 모습을 볼 수 있습니다.
.
.
.
만약 hilt를 사용하고 계신다면 di module을 통해 firebase @Provides 해야합니다!
repository는 @Binds!
.
.
.</p>
<h4 id="참고문헌">참고문헌</h4>
<blockquote>
</blockquote>
<hr>
<blockquote>
</blockquote>
<p><a href="https://moon-i.tistory.com/entry/Fragment-Result-API">https://moon-i.tistory.com/entry/Fragment-Result-API</a></p>
<hr>
<blockquote>
</blockquote>
<p><a href="https://rkdrkd-history.tistory.com/3">https://rkdrkd-history.tistory.com/3</a></p>
<hr>
<blockquote>
</blockquote>
<p><a href="https://velog.io/@simsubeen/Android-Kotlin-Firebase-Storage-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C%EC%82%AD%EC%A0%9C">https://velog.io/@simsubeen/Android-Kotlin-Firebase-Storage-%EC%9D%B4%EB%AF%B8%EC%A7%80-%ED%8C%8C%EC%9D%BC-%EC%97%85%EB%A1%9C%EB%93%9C%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C%EC%82%AD%EC%A0%9C</a></p>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[Firebase 사용해서 회원가입/로그인]]></title>
            <link>https://velog.io/@one_coin/Firebase-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@one_coin/Firebase-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Wed, 31 Jul 2024 12:51:30 GMT</pubDate>
            <description><![CDATA[<p>Firebase를 이용해서 회원가입을 하고 회원정보를 FireStore Storage에 저장할 수 있도록 설정해보았다.</p>
<h3 id="사전설정">사전설정</h3>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/one_coin/post/cc4ea49b-2a46-4aab-b166-f43dedef4a93/image.png" alt=""></p>
<ul>
<li>먼저 firebase에서 프로젝트의 이름을 정하고 프로젝트를 설정해준다.<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/0422ced0-f25e-4080-a685-56e008ba491a/image.png" alt=""></li>
<li>저는 Firebase Crashlytics를 사용할 것이기 때문에 Google 애널리틱스를 사용하도록 설정했습니다. (추후에 Crash가 난 이유와 개선점을 Firebase에서 관리할 수 있도록!)<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/6af055e2-af36-43f8-80f5-5637bb2bfb28/image.png" alt=""></li>
<li>계정은 default로 설정해주고<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/310368e5-56b4-44b5-a06f-3c7a3f83c64c/image.png" alt=""></li>
<li>프로젝트가 생성되면 다음 화면에서 자신의 프로젝트에 맞게 앱에 Firebase를 추가해주면 되는데 저는 안드로이드를 사용하고 있기 때문에 안드로이드를 추가해주겠습니다.<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/87f8c550-35ab-45fc-93dc-3d30e8f4c76d/image.png" alt=""></li>
<li>앱 등록에서 패키지명을 추가해줍니다. 
(저는 임의로 Test라는 프로젝트를 생성해서 만들어 줬습니다.)<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/ff59feed-54ac-4be4-8dcb-5ef31ee6d3a6/image.png" alt=""></li>
<li>그 다음 google-services.json 파일을 Project에 app 디렉토리에 넣어줍니다.<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/79e866c6-aab7-4ee8-a64c-fbf0fdd1c704/image.png" alt=""></li>
<li>해당 plugin과 dependencies는 파이어 베이스의 설명대로 복사 해주시면 됩니다!<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/ca165065-30f6-4613-9d26-dd9173c8d203/image.png" alt=""></li>
<li>그러면 사전 설정은 끝납니다!<blockquote>
</blockquote>
(추후에 편하게 firebase SDK를 추가하고 싶으시면 tools에서 추가할 수 있습니다.)
<img src="https://velog.velcdn.com/images/one_coin/post/7b0fa357-06ea-4354-8ee6-380f5e10f0b3/image.png" alt=""><blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/51ae8274-8652-461b-82d1-dc633f9e58b9/image.png" alt=""><blockquote>
</blockquote>
</li>
<li>Add the...을 눌러서 편하게 SDK를 app에 추가해줄 수 있습니다.</li>
</ul>
<h3 id="회원가입--로그인">회원가입 / 로그인</h3>
<blockquote>
</blockquote>
<h4 id="domain-layer">Domain layer</h4>
<blockquote>
</blockquote>
<ul>
<li>먼저 layer를 data, domain, ui 영역으로 나누어놨기 때문에 domain 영역에서 AuthRepository를 생성하여 회원 정보에 관한 메서드를 suspend fun으로 설정해놨습니다.
<img src="https://velog.velcdn.com/images/one_coin/post/02274672-27e6-4b95-b8cb-5a80d73efa59/image.png" alt=""><blockquote>
</blockquote>
</li>
<li>이후 suspend operator fun invoke를 통해 AuthRepository의 login과 signup을 return 하도록 UseCase를 만들어 주었습니다.<blockquote>
</blockquote>
</li>
<li><em>LoginUseCase*</em>
<img src="https://velog.velcdn.com/images/one_coin/post/624607e5-a3ab-41e9-81da-b6d5ae47996f/image.png" alt=""><blockquote>
</blockquote>
</li>
<li><em>SignUpUseCase*</em>
<img src="https://velog.velcdn.com/images/one_coin/post/258c1a0e-d3e4-4154-b5e5-79e60252a91d/image.png" alt=""><blockquote>
</blockquote>
(반환 값을 사실 Result로 굳이 두지 않아도 됩니다. 그저 성공했을 때와 실패했을 때 예외처리를 두고 싶어서 만들어뒀습니다!)</li>
</ul>
<blockquote>
</blockquote>
<h4 id="data-layer">Data layer</h4>
<blockquote>
</blockquote>
<ul>
<li>data 영역의 AuthRepositoryImpl를 통해 메서드를 실체화 해주었습니다.<pre><code>class AuthRepositoryImpl @Inject constructor(
  private val db: FirebaseFirestore,
  private val auth: FirebaseAuth
) : AuthRepository {
  override suspend fun signUp(
      email: String,
      password: String,
      data: FirebaseUserData
  ): Result&lt;String&gt; {
      return try {
          val result = auth.createUserWithEmailAndPassword(email, password).await()
          val user = result.user
&gt;
          if (user != null) {
              db.collection(&quot;user&quot;).document(user.uid).set(data)
                  .addOnSuccessListener {
                      Log.d(&quot;debug_signup&quot;, &quot;success&quot;)
                  }
                  .addOnFailureListener {
                      Log.d(&quot;debug_signup&quot;, &quot;fail&quot;)
                  }
              Result.success(&quot;Success&quot;)
          } else {
              Result.failure(Exception(&quot;Fail&quot;))
          }
      } catch (e: FirebaseAuthException) {
          Result.failure(Exception(&quot;FirebaseAuthException: ${e.message}&quot;))
      } catch (e: FirebaseFirestoreException) {
          Result.failure(Exception(&quot;FirebaseFirestoreException: ${e.message}&quot;))
      } catch (e: Exception) {
          Result.failure(Exception(&quot;Exception: ${e.message}&quot;))
      }
  }
&gt;
  override suspend fun logIn(email: String, password: String): Result&lt;String&gt; {
      return try {
          auth.signInWithEmailAndPassword(email, password).await()
          Result.success(&quot;success&quot;)
      } catch (e: Exception) {
          return Result.failure(Exception(&quot;Exception: ${e.message}&quot;))
      }
  }</code></pre><blockquote>
</blockquote>
</li>
<li>여기서 FirebaseAuth에서 제공해주는 createUserWithEmailAndPassword() 메서드를 통해서 파라미터로 받은 email과 password를 통해 FirebaseAuth에 계정을 생성할 수 있다.<blockquote>
</blockquote>
(await() 함수는 코루틴 함수로 suspend 함수 내에서만 사용이 가능한데 await() 함수가 실행되면 코루틴은 결과가 반환될 때까지 기다리게 되면서 생성된 계정의 uid를 가져올 수 있게 해주었습니다.)</li>
</ul>
<blockquote>
</blockquote>
<h4 id="ui-layer">Ui layer</h4>
<blockquote>
</blockquote>
<ul>
<li>ui layer에서는 viewmodel을 만들어 constructor로 LoginUseCase와 SignUpUseCase를 받아주고 fragment에서는 view에 대한 설정만 해줄 수 있도록 해주었습니다.<blockquote>
</blockquote>
</li>
<li><em>ViewModel*</em><pre><code>@HiltViewModel
class LoginViewModel @Inject constructor(
   private val logInUseCase: LogInUseCase
) : ViewModel() {
&gt;
   private val _loginEvent = Channel&lt;CheckLogin&gt; { }
   val loginEvent = _loginEvent.receiveAsFlow()
&gt;
   fun login(email: String, password: String) {
       viewModelScope.launch {
           val loginData = logInUseCase(email = email, password = password)
           if (loginData.isSuccess) {
               _loginEvent.send(CheckLogin.LoginSuccess(&quot;환영합니다! ${email}님\uD83D\uDC4D&quot;))
           } else {
               _loginEvent.send(CheckLogin.LoginFail(&quot;이메일 및 비밀번호를 확인해주세요.&quot;))
           }
       }
   }
}</code></pre><blockquote>
</blockquote>
</li>
<li><em>Fragment*</em><pre><code>private fun initView() {
&gt;
       binding.btnLogin.setOnClickListener {
           initLogin()
           collectFlow()
       }
   }
&gt;
   private fun collectFlow() {
&gt;
       viewLifecycleOwner.lifecycleScope.launch {
           repeatOnLifecycle(state = Lifecycle.State.STARTED) {
               loginViewModel.loginEvent.collect { checkLogin -&gt;
                   when (checkLogin) {
                       is CheckLogin.LoginSuccess -&gt; {
                           Toast.makeText(requireContext(), checkLogin.message, Toast.LENGTH_SHORT)
                               .show()
                           findNavController().navigate(R.id.action_loginFragment_to_mainFragment)
                       }
&gt;
                       is CheckLogin.LoginFail -&gt; {
                           Toast.makeText(requireContext(), checkLogin.message, Toast.LENGTH_SHORT)
                               .show()
                       }
                   }
               }
           }
       }
   }
&gt;
   private fun initLogin() {
       val email = binding.etLoginEmail.text.toString()
       val password = binding.etLoginPassword.text.toString()
&gt;
       viewLifecycleOwner.lifecycleScope.launch {
           loginViewModel.login(email, password)
       }
   }</code></pre></li>
<li>예전 팀 프로젝트를 많이 참고하면서 진행하였는데 가장 좋았던 부분이 sealed interface를 사용하여 로그인 성공했을 때와 실패했을 때의 data class를 나누어 동작을 다르게 설정해 줄 수있었던 점이 좋았다.
<img src="https://velog.velcdn.com/images/one_coin/post/8affb49e-d4df-45cb-abee-0eae72bb0db3/image.png" alt=""><blockquote>
</blockquote>
</li>
<li>viewmodel에서 loginEvent에 send한 예외처리 상황을 통해 fragment에서는 when문을 이용하여 loginEvent가 받은 LoginSuccess와 LoginFail일 때의 Toast 메세지를 다르게 설정해주어 sealed interface를 이용해보았다.<blockquote>
</blockquote>
내 실력으로는 사실 생각하기 어려웠을 것 같은데 예전 팀 프로젝트를 복기하면서 좋은 아이디어를 얻어 적용해 볼 수 있어서 편했던 것 같다.<blockquote>
</blockquote>
특히 회원가입 단계에서 예외처리가 굉장히 많이 들어갔어야 했는데 정말 유용했다.<blockquote>
</blockquote>
<img src="https://velog.velcdn.com/images/one_coin/post/79c7ca89-d23e-4346-95d2-e0c658120a4a/image.png" alt=""></li>
<li>회원가입 실패했을 때 각 상황에 맞게 message를 넣어주어 이 메세지를 view에서 Toast 메세지로 사용할 수 있도록 해주었다.</li>
</ul>
<h3 id="결과">결과</h3>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/one_coin/post/e010ecde-9f3a-4850-acee-6c577e25bf25/image.png" alt=""></p>
<ul>
<li>test용으로 이메일을 설정하여 회원가입을 하면 다음과 같이 Firebase 프로젝트에 계정이 생성되는 것을 볼 수 있다.</li>
</ul>
<p>.
.
.
.
.
오늘은 Firebase를 통해 회원가입과 로그인을 구현하는 법을 작성해보았습니다.
아직 코드적으로나 여러 방면으로 부족해서 좀 더 심도있게 공부해보고 계속 열심히 해야겠다는 생각이 많이 듭니다 ㅠㅠ </p>
<p>FireStore에 데이터를 전송하는 부분도 작성은 되어있지만 이 부분은 다음에 작성해보겠습니다!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[프래그먼트에서 뷰 바인딩 사용하기]]></title>
            <link>https://velog.io/@one_coin/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%A1%9D</link>
            <guid>https://velog.io/@one_coin/%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EA%B8%B0%EB%A1%9D</guid>
            <pubDate>Wed, 17 Jul 2024 07:55:33 GMT</pubDate>
            <description><![CDATA[<p>프래그먼트에서 ViewBinding을 사용하는 중에 binding null 처리를 통해서 메모리 누수를 방지하는 것이 좋다는 글을 보고 이유에 대해서 알아보았다.</p>
<p>ViewBinding을 프래그먼트에서 사용하게 될 경우에 메모리 누수(Memory leak)과 관련하여 문제가 생길 수 있다. 이는 구글 문서에 나와있다.</p>
<p>(아래는 구글 문서 원본)</p>
<ul>
<li><h4 id="프래그먼트에서-뷰-결합-사용">프래그먼트에서 뷰 결합 사용</h4>
<blockquote>
</blockquote>
프래그먼트와 함께 사용할 결합 클래스 인스턴스를 설정하려면 프래그먼트의 onCreateView() 메서드에서 다음 단계를 따르세요.</li>
</ul>
<blockquote>
</blockquote>
<ol>
<li>생성된 결합 클래스에 포함된 정적 inflate() 메서드를 호출합니다. 그러면 프래그먼트에서 
사용할 결합 클래스 인스턴스가 생성됩니다.<blockquote>
</blockquote>
</li>
<li>getRoot() 메서드를 호출하거나 Kotlin 속성 문법을 사용하여 루트 뷰 참조를 가져옵니다.<blockquote>
</blockquote>
</li>
<li>onCreateView() 메서드에서 루트 뷰를 반환하여 화면의 활성 뷰로 만듭니다.</li>
</ol>
<pre><code>private var _binding: ResultProfileBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    _binding = ResultProfileBinding.inflate(inflater, container, false)
    val view = binding.root
    return view
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null
}</code></pre><p>이렇게 프래그먼트에서 binding = null 처리를 해주는 이유는 아래 참고사항에 나와있다.</p>
<p>프래그먼트는 뷰보다 오래 지속되므로 프래그먼트의 onDestroyView() 메서드에서 결합 클래스 인스턴스 참조를 정리해야 한다고 한다.</p>
<p>이러한 현상때문에 메모리를 회수하려고 하여도 프래그먼트 뷰가 살아있게 되면서 회수하지 못하여 메모리 누수 현상이 일어나게 되는 것이라고 한다.</p>
<p>다음에는 baseFragment를 통해 반복되는 binding 코드를 줄일 수 있는 방법을 작성해봐야겠다.</p>
<h4 id="참고-">참고 :</h4>
<p><a href="https://developer.android.com/topic/libraries/view-binding?hl=ko">https://developer.android.com/topic/libraries/view-binding?hl=ko</a>
<a href="https://bignerdranch.tistory.com/65">https://bignerdranch.tistory.com/65</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 72일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-72%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-72%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 01 Jul 2024 12:40:10 GMT</pubDate>
            <description><![CDATA[<p>오늘은 튜토리얼 페이지 마무리했다.</p>
<p>어플을 처음 사용하는 분들이 사용법을 잘 모를 수 있기 때문에 어플 사용법을 따로 만들어서 sharedPreference를 이용해 어플을 처음 실행하면 페이지를 띄워주고 그 이후에는 세팅 페이지에서 다시 볼 수 있도록 설정했다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/7e12f554-3021-4bd2-ac20-37457c4ebf14/image.png" alt=""></li>
<li>앱 사용 설명서 페이지를 누르면
<img src="https://velog.velcdn.com/images/one_coin/post/b4904692-1abd-4650-b305-6fef6a49e61c/image.png" alt=""></li>
<li>피그마에서 만든 이미지를 통해서 안내해 줄 수 있도록 해주었다.
<img src="https://velog.velcdn.com/images/one_coin/post/131d0b83-3cee-4828-8850-9bf196471d62/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/05b0bfdb-51cb-4bc5-a47d-4fa32c121c95/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/7ec26689-a6dc-4729-a5bc-c106779e4af5/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/beb868d0-7309-4933-9bb9-10fe301b2285/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/c46d4dc5-466e-4a6b-9e40-563628da3b5e/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/b6e1a493-a3fc-4f34-a70c-292896b21d73/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/4a9046be-66c0-41b4-833f-76e4a2ae4553/image.png" alt=""></li>
<li>원래 레이아웃으로 작업하려고 했었는데 생각보다 프래그먼트랑 너무 많아져서 viewpager에 이미지를 넣어주는 방향으로 변경하였다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>이제 4일 남았다 ㅠㅠ 얼마 안남았다 화이팅</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 71일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-71%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-71%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 27 Jun 2024 08:26:41 GMT</pubDate>
            <description><![CDATA[<p>오늘은 프로젝트 제출 단계에서 추가된 내용을 적어보았다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/7cc4288d-c26a-4cce-9ef1-060dfe5d2d82/image.png" alt=""></li>
<li>채팅방 리스트를 보여주고
<img src="https://velog.velcdn.com/images/one_coin/post/6dbba1e2-c8ac-42f6-982c-2e0a4336368a/image.png" alt=""></li>
<li>채팅방을 들어가면 실시간으로 채팅을 주고 받을 수 있다.
<img src="https://velog.velcdn.com/images/one_coin/post/85c47141-eecb-4e8b-a124-7073816f32f1/image.png" alt=""></li>
<li>상단 공지를 클릭하면 채팅방에 대한 내용을 볼 수 있다.
<img src="https://velog.velcdn.com/images/one_coin/post/1fa61705-edc7-4373-98b9-a0f8e5feb2af/image.png" alt=""></li>
<li>날짜와 제목을 입력하고
<img src="https://velog.velcdn.com/images/one_coin/post/1b4fda80-0247-425e-8482-de1290086c57/image.png" alt=""></li>
<li>정산하기 기능에서는 벌금과 벌금 납부 인원을 기입하면
<img src="https://velog.velcdn.com/images/one_coin/post/45bfae78-745b-4f5f-9972-86ca2e10f50c/image.png" alt=""></li>
<li>지출해야할 정산 내역 결과를 볼 수 있다.
<img src="https://velog.velcdn.com/images/one_coin/post/cf044dc3-7c34-489a-9553-e92dd0790015/image.png" alt=""></li>
<li>공유 버튼을 누르면 정산 내용을 공유 받을 수 있고
<img src="https://velog.velcdn.com/images/one_coin/post/126ecca8-a077-4b43-8325-46743dfabeed/image.png" alt=""></li>
<li>내역을 문자로 공유해 보았다.
<img src="https://velog.velcdn.com/images/one_coin/post/6bf00bf5-601c-4fe8-9d75-ad72e554b841/image.png" alt=""></li>
<li>정산완료를 누르지 않고 나가기를 누르면 정산 중에 입력되어있고
<img src="https://velog.velcdn.com/images/one_coin/post/cadda4b7-bf11-4369-a519-261b44210ab5/image.png" alt=""></li>
<li>완료로 누르면 정산 내역으로 상태가 변경된다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>어플 제출하고 나서야 오랜만에 블로그 쓰는 것 같다 ㅠㅠ 이제 다시 자주 써야겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[늦지마 개인정보 처리방침]]></title>
            <link>https://velog.io/@one_coin/%EB%8A%A6%EC%A7%80%EB%A7%88-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</link>
            <guid>https://velog.io/@one_coin/%EB%8A%A6%EC%A7%80%EB%A7%88-%EA%B0%9C%EC%9D%B8%EC%A0%95%EB%B3%B4-%EC%B2%98%EB%A6%AC%EB%B0%A9%EC%B9%A8</guid>
            <pubDate>Wed, 26 Jun 2024 11:43:05 GMT</pubDate>
            <description><![CDATA[<p>늦지마컴퍼니는 개인정보 보호법 제30조에 따라 정보주체의 개인정보를 보호하고
이와 관련한 고충을 신속하고 원활하게 처리할 수 있도록 하기 위하여
다음과 같이 개인정보 처리방침을 수립·공개합니다.</p>
<p>○ 이 개인정보처리방침은 2023년 1월 1부터 적용됩니다.</p>
<p>제1조(개인정보의 처리 목적)</p>
<p>① 늦지마컴퍼니는 다음의 목적을 위하여 개인정보를 처리합니다.
처리하고 있는 개인정보는 다음의 목적 이외의 용도로는 이용되지 않으며,
이용 목적이 변경되는 경우에는 개인정보 보호법 제18조에 따라 별도의 동의를 받는 등 필요한 조치를 이행할 예정입니다.</p>
<ol>
<li>회원 가입 및 관리</li>
</ol>
<ul>
<li>회원 가입의사 확인, 각종 고지·통지 등을 목적으로 개인정보를 처리합니다.\n\n</li>
</ul>
<p>제2조(개인정보의 처리 및 보유 기간)</p>
<p>① 늦지마 컴퍼니는 법령에 따른 개인정보 보유·이용기간 또는 정보주체로부터 개인정보를 수집 시에
동의받은 개인정보 보유·이용기간 내에서 개인정보를 처리·보유합니다.</p>
<p>② 각각의 개인정보 처리 및 보유기간은 다음과 같습니다.</p>
<ol>
<li>홈페이지 회원 가입 및 관리 : 홈페이지 탈퇴시까지 다만, 다음의 사유에 해당하는 경우에는 해당 사유 종료시까지
1) 관계 법령 위반에 따른 수사·조사 등이 진행중인 경우에는 해당 수사·조사 종료시까지\n\n</li>
</ol>
<p>제3조(개인정보의 제3자 제공)</p>
<p>① 늦지마컴퍼니는 정보주체의 동의, 법률의 특별한 규정 등 개인정보 보호법 제17조 및 제18조에 해당하는 경우에만 개인정보를 제3자에게 제공하며,
정보 주체의 사전 동의 없이는 본래의 범위를 초과하여 처리하거나 제3자에게 제공하지 않습니다.</p>
<p>제4조(개인정보처리의 위탁에 관한 사항(해당되는 경우에만 정한다.))</p>
<p>제5조(정보주체와 법정대리인의 권리ㆍ의무 및 그 행사방법에 관한 사항)
① 정보주체는 늦지마컴퍼니에 대해 언제든지 개인정보 열람·정정·삭제·처리정지 요구 등의 권리를 행사할 수 있습니다.
② 제1항에 따른 권리 행사는 늦지마컴퍼니에 대해 개인정보 보호법 시행령 제41조제1항에 따라 서면, 전자우편, 모사전송(FAX)
등을 통하여 하실 수 있으며, 늦지마컴퍼니는 이에 대해 지체없이 조치하겠습니다.</p>
<ul>
<li>[시행규칙 별지 제8호] 개인정보(열람, 정정·삭제, 처리정지) 요구서
③ 제1항에 따른 권리 행사는 정보주체의 법정대리인이나 위임을 받은 자 등 대리인을 통하여 하실 수 있습니다.
이 경우 개인정보 보호법 시행규칙 별지 제11호 서식에 따른 위임장을 제출하셔야 합니다.<ul>
<li>[시행규칙 별지 제11호] 위임장 
④ 개인정보 열람 및 처리정지 요구는 개인정보보호법 제35조 제5항, 제37조 제2항에 의하여 정보주체의 권리가 제한 될 수 있습니다.
⑤ 개인정보의 정정 및 삭제 요구는 다른 법령에서 그 개인정보가 수집 대상으로 명시되어 있는 경우에는 그 삭제를 요구할 수 없습니다.
⑥ 늦지마컴퍼니는 정보주체 권리에 따른 열람의 요구, 정정·삭제의 요구,
처리정지의 요구 시 열람 등 요구를 한 자가 본인이거나 정당한 대리인인지를 확인합니다.</li>
</ul>
</li>
</ul>
<p>제6조(처리하는 개인정보 항목)
① 늦지마컴퍼니는 다음의 개인정보 항목을 처리하고 있습니다.</p>
<ol>
<li>개인정보파일 명칭 : 홈페이지 회원정보</li>
<li>개인정보 항목 : (필수항목) 아이디, 비밀번호, 이름(or nick-name), 이메일
② 인터넷 서비스 이용과정에서 아래 개인정보 항목이 자동으로 생성되어 수집될 수 있습니다.</li>
</ol>
<ul>
<li>IP주소, 쿠키, MAC주소, 서비스 이용기록, 방문기록, 불량 이용기록 등</li>
</ul>
<p>제7조(개인정보의 파기)
① 늦지마컴퍼니는 개인정보 보유기간의 경과, 처리목적 달성 등 개인정보가 불필요하게 되었을 때에는 지체없이 해당 개인정보를 파기합니다.
② 정보주체로부터 동의받은 개인정보 보유기간이 경과하거나 처리목적이 달성되었음에도 불구하고 다른 법령에 따라 개인정보를 계속 보존하여야 하는 경우에는,
해당 개인정보(또는 개인정보파일)을 별도의 데이터베이스(DB)로 옮기거나 보관장소를 달리하여 보존합니다.
③ 개인정보 파기의 절차 및 방법은 다음과 같습니다.</p>
<ol>
<li>파기절차</li>
</ol>
<ul>
<li>늦지만컴퍼니는 파기하여야 하는 개인정보(또는 개인정보파일)에 대해 개인정보 파기계획을 수립하여 파기합니다.</li>
<li>늦지만컴퍼니는 파기 사유가 발생한 개인정보(또는 개인정보파일)을 선정하고,
개인정보 보호총괄책임자의 승인을 받아 개인정보(또는 개인정보파일)을 파기합니다.</li>
</ul>
<ol start="2">
<li>파기방법</li>
</ol>
<ul>
<li>늦지만컴퍼니는 전자적 파일 형태로 기록·저장된 개인정보는 기록을 재생할 수 없도록 파기하며,
종이 문서에 기록·저장된 개인정보가 있다면 분쇄기로 분쇄하거나 소각하여 파기합니다.</li>
</ul>
<p>제8조(개인정보의 안전성 확보조치)
① 늦지마컴퍼니는 개인정보의 안전성 확보를 위해 다음과 같은 조치를 취하고 있습니다.</p>
<ol>
<li>정기적인 자체 감사 실시</li>
</ol>
<ul>
<li>개인정보 취급 관련 안정성 확보를 위해 정기적(분기 1회)으로 자체 감사를 실시하고 있습니다.</li>
</ul>
<p>제9조(개인정보 자동 수집 장치의 설치·운영 및 거부에 관한 사항)
① 늦지마컴퍼니는 정보주체의 이용정보를 저장하고 수시로 불러오는 <![CDATA["쿠키(cookie)"]]]]><![CDATA[>를 사용하지 않습니다.</p>
<p>제10조(개인정보 보호책임자)
① 늦지마컴퍼니는 개인정보 처리에 관한 업무를 총괄해서 책임지고,
개인정보 처리와 관련한 정보주체의 불만처리 및 피해구제 등을 위하여 아래와 같이 개인정보 보호책임자를 지정하고 있습니다.</p>
<ul>
<li>개인정보 보호책임자
부서명 : 늦지마 컴퍼니
성명 : 김재현, 임채명, 장규식, 최지원, 박혜란
직급 : 리더, 부리더, 팀원, 팀원, 팀원
연락처 : <a href="mailto:onecoin789@gmail.com">onecoin789@gmail.com</a>, <a href="mailto:limchaem0318@gmail.com">limchaem0318@gmail.com</a>, <a href="mailto:jgs288@gmail.com">jgs288@gmail.com</a>, <a href="mailto:sp202401jw@gmail.com">sp202401jw@gmail.com</a>, <a href="mailto:tnfl2644@gmail.com">tnfl2644@gmail.com</a></li>
</ul>
<p>제11조(개인정보 열람청구)
① 정보주체는 개인정보 보호법 제35조에 따른 개인정보의 열람 청구를 아래의 부서에 할 수 있습니다.
늦지마컴퍼니는 정보주체의 개인정보 열람청구가 신속하게 처리되도록 노력하겠습니다.
② 정보주체께서는 모든 개인정보 보호 관련 문의, 불만처리, 피해구제 등에 관한 사항을 개인정보 보호책임자 및 담당부서로 문의하실 수 있습니다.
늦지마컴퍼니는 정보주체의 문의에 대해 지체 없이 답변 및 처리해드릴 것입니다.</p>
<ul>
<li>개인정보 열람청구 접수·처리 부서
(개인정보 보호책임자와 동일)</li>
</ul>
<p>제12조(권익침해 구제방법)
① 정보주체는 개인정보침해로 인한 구제를 받기 위하여 개인정보분쟁조정위원회,
한국인터넷진흥원 개인정보침해신고센터 등에 분쟁해결이나 상담 등을 신청할 수 있습니다. 이 밖에 기타 개인정보침해의 신고,
상담에 대하여는 아래의 기관에 문의하시기 바랍니다.</p>
<ol>
<li>개인정보분쟁조정위원회 : (국번없이) 1833-6972 (<a href="http://www.kopico.go.kr">www.kopico.go.kr</a>)</li>
<li>개인정보침해신고센터 : (국번없이) 118 (privacy.kisa.or.kr)</li>
<li>대검찰청 : (국번없이) 1301 (<a href="http://www.spo.go.kr">www.spo.go.kr</a>)</li>
<li>경찰청 : (국번없이) 182 (ecrm.cyber.go.kr)</li>
</ol>
<p>제13조(개인정보 처리방침 변경)
① 이 개인정보 처리방침은 2022. 08. 22부터 적용됩니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 70일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-70%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-70%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 19 Jun 2024 12:35:30 GMT</pubDate>
            <description><![CDATA[<p>오늘은 세팅 페이지에서 토스트로 준비중이라고 띄웠던 부분을 전부 채워넣었다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/a106642e-9ca7-4335-9c88-b7b673f0a3d1/image.png" alt=""></li>
<li>문의하기 페이지로 들어가면 E-mail로 문의하는 기능과 오픈 채팅방이 있다.
<img src="https://velog.velcdn.com/images/one_coin/post/b419f8e3-9479-4c0a-9cc7-578137e90592/image.png" alt=""></li>
<li>E-mail은 개발자 계정으로 등록된 gmail을 볼 수 있도록 설정해두었다.
<img src="https://velog.velcdn.com/images/one_coin/post/5c8485ff-7e9a-4858-bb2f-21d6ae508222/image.png" alt=""></li>
<li>개인정보 처리방침, 약관 등 동의 사항도 따로 마련해두었다.
원래는 앱 버전 항목을 따로 설정해두었는데 공간이 너무 많이 남아서 오른쪽 하단에 표시하도록 해두었다
<img src="https://velog.velcdn.com/images/one_coin/post/9618a77b-f7e6-44fe-9f43-bda420878c34/image.png" alt=""></li>
<li>오픈 채팅방은 카카오톡 오픈 채팅방과 디스코드 서버를 만들어서 공유해두었다.
<img src="https://velog.velcdn.com/images/one_coin/post/b11fb370-0034-43bf-abb1-4684e17a8a76/image.png" alt=""></li>
<li>앱을 만들면서 사용한 오픈소스 라이선스를 따로 넣었고
<img src="https://velog.velcdn.com/images/one_coin/post/e5a973c3-abc1-45e3-8ca4-1af8f6180edd/image.png" alt=""></li>
<li>클릭하면 해당 페이지로 이동할 수 있다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>이제 거의 다 채워간다 ㅠㅠ 내일은 recyclerview 만들어야한다 ㅠㅠ</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 69일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-69%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-69%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 17 Jun 2024 10:01:50 GMT</pubDate>
            <description><![CDATA[<p>오랜만에 쓰는 것 같은 TIL이다 ㅠㅠ</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/ca352d7b-cd6c-490c-8707-1f44d7575a66/image.png" alt=""></li>
<li>사이드 시트를 이용해서 설정화면을 보여줄까 했는데 이 부분은 프래그먼트로 보여주기로 수정하였다.
<img src="https://velog.velcdn.com/images/one_coin/post/0ef33efe-77d6-4167-a9f3-5198fb6786b1/image.png" alt=""></li>
<li>약속에 대한 정보를 입력하지 않으면 토스트 메세지를 통해 입력하도록 설정해주었다.
<img src="https://velog.velcdn.com/images/one_coin/post/aaaf0ac2-5ab3-4479-a45f-cdf9e1e47195/image.png" alt=""></li>
<li>목적지에 대한 정보도 동일!
<img src="https://velog.velcdn.com/images/one_coin/post/d052a1ed-00fd-4da1-80df-9f7fd2b2d859/image.png" alt=""></li>
<li>채팅방에서는 오른쪽 위 지도 표시를 누르면 현재 위치부터 목적지까지의 거리, 소요시간, 도착시간을 볼 수 있도록 설정하였다.
<img src="https://velog.velcdn.com/images/one_coin/post/32bae20b-98eb-4907-b10d-3416f0cab87a/image.png" alt=""></li>
<li>친구 요청에서는 내가 보낸 친구 요청을 볼 수 있도록 설정해 주셨다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>중간 발표때문에 정신이 없어서 한동안 TIL을 못썼다 ㅠㅠ 다시 열심히 써봐야겠다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 68일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-68%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-68%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Thu, 13 Jun 2024 13:41:45 GMT</pubDate>
            <description><![CDATA[<p>오늘은 파이어베이스에 데이터를 넘겨주는 작업을 했다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/f20adb62-780c-4b92-87b7-0e3eddd118c4/image.png" alt=""></li>
<li>약속 제목, 날짜, 시간, 내용을 입력하면
<img src="https://velog.velcdn.com/images/one_coin/post/38fb43c9-069c-4da9-8b88-7e5cc314a470/image.png" alt=""></li>
<li>목적지를 검색하여 정할 수 있는 화면으로 이동하고
<img src="https://velog.velcdn.com/images/one_coin/post/cca5cca5-7712-406e-9b56-cb4ccc074889/image.png" alt=""></li>
<li>검색하면 바텀시트를 통해 리스트를 보여주도록 설정했다.
<img src="https://velog.velcdn.com/images/one_coin/post/e38aaab9-16da-4693-b306-95d10d006add/image.png" alt=""></li>
<li>부산역을 선택하였는데 해당 위도, 경도에 맞는 위치에 마커가 찍힌다.
<img src="https://velog.velcdn.com/images/one_coin/post/434b6da5-9b84-4979-8f98-2dd2fed2ce9d/image.png" alt="">
<img src="https://velog.velcdn.com/images/one_coin/post/d424eadc-ccac-4be7-ae2e-fd0955d45fbd/image.png" alt=""></li>
<li>약속을 과정을 완료하면 결과 페이지를 통해 약속 내용을 다시 한번 확인한다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>코드가 좀 뒤죽박죽 안좋다고 생각되는 코드가 많아서 튜터님께 여쭤보고 수정해야한다 ㅠㅠ</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 67일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-67%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-67%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Tue, 11 Jun 2024 12:55:45 GMT</pubDate>
            <description><![CDATA[<p>오늘은 google maps directions api를 통해 목적지까지의 정보를 받아보았다.</p>
<ul>
<li>api 통신 연결하고 뭔가 잘못된 곳이 없었다고 생각했는데 값이 안받아와졌다 ㅠㅠ<pre><code>java.lang.IllegalArgumentException: Unable to create converter for class com.example.donotlate.feature.searchPlace.data.model.Directions for method Goo</code></pre></li>
<li>계속 다음과 같은 에러 코드만 떠서 구글링을 해도 원하는 대답이 안떴는데...
팀원분께서 구글링 해주셔서 data class 한번 확인해 보라고 하셔서 확인했더니
<img src="https://velog.velcdn.com/images/one_coin/post/2c41c349-6443-4bed-be0c-3106a25cbc90/image.png" alt=""></li>
<li>멍청한 놈이 아닐 수 없다...
<img src="https://velog.velcdn.com/images/one_coin/post/34a4c582-6235-4b69-a040-eba7260e692d/image.png" alt=""></li>
<li>고치고 코드 돌렸더니
<img src="https://velog.velcdn.com/images/one_coin/post/06122f12-9a25-4af1-a658-82d90b6adbd2/image.png" alt=""></li>
<li>테스트 삼아 받아보려고 했던 코드 받아와지고...
<img src="https://velog.velcdn.com/images/one_coin/post/60eb9715-2843-4bea-b67e-979919d40329/image.png" alt=""></li>
<li>테스트 삼아 만든 EditText에 해당 값 출발장소, 도착장소 입력해주면
<img src="https://velog.velcdn.com/images/one_coin/post/e360faf6-d7ad-465e-ac7a-681a9235b721/image.png" alt=""></li>
<li>다음과 같이 출발시간, 도착시간, 거리, 걸리는 시간을 받아올 수 있다!</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>오늘의 3시간을 날린 교훈 : 좀 더 침착하게 일을 해보자...ㅠㅠ</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 66일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-66%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-66%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 10 Jun 2024 13:09:44 GMT</pubDate>
            <description><![CDATA[<p>오늘은 detail로 정보 넘겨주고 위도 경도 값 marker로 표시해주었다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/6d529e56-d7cb-41b8-a04d-79ad4b0d5c0c/image.png" alt=""></li>
<li>놀거리로 들어오면 다음과 같은 화면이 보이고
<img src="https://velog.velcdn.com/images/one_coin/post/a45c988f-3c5c-4cbd-b20a-c987ab54a890/image.png" alt=""></li>
<li>EditText가 비어있는채로 검색을 누르면 다음과 같은 이미지와 텍스트가 나온다.
<img src="https://velog.velcdn.com/images/one_coin/post/f2dac132-5afc-4aba-bf50-d23de782faf5/image.png" alt=""></li>
<li>리스트가 나오고 리스트를 누르면
<img src="https://velog.velcdn.com/images/one_coin/post/864c3795-ce6e-497c-8f94-3a4bf24462f4/image.png" alt=""></li>
<li>리스트에 대한 가게 이름, 주소, 번호, 영업시간, 위치 정보를 받아온다.
<img src="https://velog.velcdn.com/images/one_coin/post/1b61ee3a-8e01-49bb-9666-73f097dbd6ac/image.png" alt=""></li>
<li>위치 정보를 보면 해당 가게에 대한 위도 경도 값으로 마커를 찍어놓았다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li><p>데이터 넘겨주는 파트는 금방 끝났는데 마커 찍는데 오래걸렸다 흑 ㅠㅠ</p>
<p>내일은 길찾기 파트를 끝내봐야겠다.</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 65일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-65%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-65%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Wed, 05 Jun 2024 12:20:55 GMT</pubDate>
            <description><![CDATA[<p>오늘은 맵 데이터 받아오고 리스트 형태로 나타내주었다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/9f51478a-75c4-4ec0-893f-198bd14cec46/image.png" alt=""></li>
<li>일단 main 디자인은 이렇게 살짝 바꿨다
<img src="https://velog.velcdn.com/images/one_coin/post/bc757ba9-e330-4dce-8fd1-9a3c9bee8c6e/image.png" alt=""></li>
<li>놀거리를 누르면 다음과 같은 화면이 되고
<img src="https://velog.velcdn.com/images/one_coin/post/9bebb05e-c636-4442-a9a0-46dbf4d851db/image.png" alt=""></li>
<li>검색하면 다음과 같이 연관 키워드에 대한 검색결과를 가져올 수 있었다.</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>바쁘다 바빠 ㅠㅠ</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[내일배움캠프 64일차]]></title>
            <link>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-64%EC%9D%BC%EC%B0%A8</link>
            <guid>https://velog.io/@one_coin/%EB%82%B4%EC%9D%BC%EB%B0%B0%EC%9B%80%EC%BA%A0%ED%94%84-64%EC%9D%BC%EC%B0%A8</guid>
            <pubDate>Mon, 03 Jun 2024 14:55:35 GMT</pubDate>
            <description><![CDATA[<p>오늘은 DatePicker와 TimePicker를 통해 날짜, 시간을 받아왔다.</p>
<ul>
<li><img src="https://velog.velcdn.com/images/one_coin/post/f1e38681-7ca9-4999-811c-eadfc7bd7905/image.png" alt=""></li>
<li>현재 날짜와 현재 시각을 받아오고
<img src="https://velog.velcdn.com/images/one_coin/post/77b295c6-2618-4438-90c8-ab3e30fd317c/image.png" alt=""></li>
<li>datePicker를 통해 날짜를 선택하면
<img src="https://velog.velcdn.com/images/one_coin/post/ffed44b5-d799-4583-9d0b-fec94a267995/image.png" alt=""></li>
<li>다음과 같이 날짜가 바뀐다.
<img src="https://velog.velcdn.com/images/one_coin/post/fd3efe32-9ea6-4c32-92ed-ada4550ecd1a/image.png" alt=""></li>
<li>timePicker를 통해 시간을 선택하면
<img src="https://velog.velcdn.com/images/one_coin/post/97cba669-77f4-499a-80d1-0e8682629e08/image.png" alt=""></li>
<li>시간도 바꿀 수 있다!
<img src="https://velog.velcdn.com/images/one_coin/post/7732c9d7-f377-473e-b26c-88ec9fb1b104/image.png" alt=""></li>
<li>바뀐 data값도 확인했다. 변수에 담아 놨으니 넘겨주기만 하면 끝!</li>
</ul>
<h4 id="하루의-마무리">하루의 마무리</h4>
<ul>
<li>button 속성을 ripple effect를 주고 싶어서 meterial을 사용했는데 튜터님께서 안된다고 하셔서 style에서 설정해줘서 background로 넣어주는 것까지만 마무리하고 자야겠다 ㅠㅠ</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>