<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>🏠woori_zip</title>
        <link>https://velog.io/</link>
        <description>할 건 해야지</description>
        <lastBuildDate>Fri, 23 Aug 2024 01:22:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>🏠woori_zip</title>
            <url>https://velog.velcdn.com/images/woori_dec/profile/94d5b3c7-e92a-4937-80f3-0e1ae3e8fe55/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. 🏠woori_zip. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/woori_dec" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA['좋아요'를 취소해보자]]></title>
            <link>https://velog.io/@woori_dec/%EC%A2%8B%EC%95%84%EC%9A%94%EB%A5%BC-%EC%B7%A8%EC%86%8C%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@woori_dec/%EC%A2%8B%EC%95%84%EC%9A%94%EB%A5%BC-%EC%B7%A8%EC%86%8C%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Fri, 23 Aug 2024 01:22:45 GMT</pubDate>
            <description><![CDATA[<p>지난번에 &#39;좋아요&#39; 기능을 구현하는 데까지 성공했다.
그런데, 지금 와서 생각해보니까 &#39;좋아요&#39; 취소는 구현을 안 했다.. 🥲
그래서 다른 팀원이 눈치채기 전에 부랴부랴 추가 중이다.</p>
<hr>
<h1 id="1-canclelike-핸들러-추가">1. CancleLike 핸들러 추가</h1>
<pre><code class="language-js">&lt;FavoriteIcon className={styles.icon} onClick={handleoCancelLike}/&gt;

const handleoCancelLike = async () =&gt; {
  try {
    const response = await axios.post(&#39;/api/postlike/cancel&#39;, {
      userId: userId,
      postId: postId
    });
    console.log(response.data);
    setIsLiked(false); // 좋아요 상태를 false로 설정
  } catch (error) {
    console.error(&#39;Error unliking the post:&#39;, error);
  }
};</code></pre>
<hr>
<h1 id="2-api-처리">2. API 처리</h1>
<pre><code class="language-java">
// controller
Mapping(&quot;/cancel&quot;)
public ResponseEntity&lt;String&gt; cancleLikePost(@RequestBody PostLikeDTO dto) {
    boolean isCancled = postLikeService.cancleLikePost(dto.getUserId(), dto.getPostId());
    if (isCancled) {
        return ResponseEntity.ok(&quot;Post unliked successfully&quot;);
    } else {
        return ResponseEntity.badRequest().body(&quot;Failed to unlike post&quot;);
    }
}


// service
public boolean cancleLikePost(String userId, Integer postId) {
    try {
        // 1. userId와 postId로 PostLikeEntity를 찾기
        PostLikeEntity postLike = postLikeRepository.findByUserIdAndPostId(userId, postId);
        if (postLike != null) {
            // 2. 찾은 엔티티를 삭제
            postLikeRepository.delete(postLike);
            return true;
        }
        return false; // 좋아요가 존재하지 않는 경우
    } catch (Exception e) {
        return false;
    }
}


// repostiory
PostLikeEntity findByUserIdAndPostId(String userId, Integer postId);</code></pre>
<hr>
<img src="https://velog.velcdn.com/images/woori_dec/post/9865d12b-7784-41fd-b6c9-0c1d88de89f8/image.png" width="500px">
<img src="https://velog.velcdn.com/images/woori_dec/post/a6b6526f-6648-449e-a06e-3e3e94c0f483/image.png" width="500px">

<p>데이터가 정상적으로 삭제되는 것을 확인할 수 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL : SQL 구문 기초(1)]]></title>
            <link>https://velog.io/@woori_dec/PostgreSQL-SQL-%EA%B5%AC%EB%AC%B8-%EA%B8%B0%EC%B4%88</link>
            <guid>https://velog.io/@woori_dec/PostgreSQL-SQL-%EA%B5%AC%EB%AC%B8-%EA%B8%B0%EC%B4%88</guid>
            <pubDate>Thu, 22 Aug 2024 15:02:13 GMT</pubDate>
            <description><![CDATA[<p>SQL 구문은 그동안 접했던 다른 SQL 데이터베이스와 동일하다.</p>
<hr>
<h1 id="1-select">1. SELECT</h1>
<p>SELECT 문은 테이블에서 원하는 데이터를 <strong>조회</strong>할 때 사용된다.</p>
<pre><code class="language-sql">SELECT 컬럼명 FROM 테이블명;
SELECT 컬럼명1, 컬럼명2, ... FROM 테이블명;
SELECT * FROM 테이블명;</code></pre>
<blockquote>
<p>컬럼명을 나열할 때는, 나열한 순서대로 출력된다.</p>
</blockquote>
<h2 id="실전-문제">실전 문제</h2>
<blockquote>
<p>이메일에 작성할 고객의 성과 이름, 이메일이 필요한 경우</p>
</blockquote>
<pre><code class="language-sql">SELECT first_name, last_name, email FROM customer;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/dc4589a0-e24e-4ea6-a899-27bd02ec2660/image.png" width="700px">

<hr>
<h1 id="2-select-distinct">2. SELECT DISTINCT</h1>
<p>테이블에에서 중복되지 않는 값만 조회하고 싶을 때 사용한다.</p>
<pre><code class="language-sql">SELECT DISTINCT(컬럼명) FROM 테이블명;

SELECT DISTINCT 컬럼명 FROM 테이블명;</code></pre>
<h2 id="실전-문제-1">실전 문제</h2>
<blockquote>
<p>어떤 등급의 영화들을 가지고 있는지 조회할 경우</p>
</blockquote>
<pre><code class="language-sql">SELECT DISTINCT rating FROM film;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/7ea5bc1d-c610-43bf-bd84-1de5112a7da5/image.png" width="220x">

<hr>
<h1 id="3-count">3. COUNT</h1>
<p>특정 쿼리 조건에 맞는 입력 행의 개수를 구하고 싶을 때 사용한다.</p>
<pre><code class="language-sql">SELECT COUNT(쿼리명) FROM 테이블명;
SELECT COUNT(*) FROM 테이블명;</code></pre>
<p>예를 들어,</p>
<pre><code class="language-sql">SELECT COUNT (DISTINCT rating) FROM film;</code></pre>
<h1 id="4-select-where">4. SELECT WHERE</h1>
<p><strong>WHERE</strong> 문은 열에 조건을 지정하여 그에 맞는 행이 반환되도록 한다.</p>
<pre><code class="language-sql">SELECT 컬럼명1, 컬럼명2 FROM 테이블명
WHERE 조건</code></pre>
<blockquote>
<ol>
<li><strong>비교 연산자</strong> : =, &lt; , &gt;, &gt;=, &lt;=, &lt;&gt; OR !=</li>
<li><strong>논리 연산자</strong> : AND, OR, NOT</li>
</ol>
</blockquote>
<p>예를 들어,</p>
<pre><code class="language-sql">SELECT * FROM customer
WHERE first_name=&#39;Jared&#39;;

SELECT COUNT(*) FROM film
WHERE rental_rate &gt; 4 AND replacement_cost &gt;= 19.99
AND rating=&#39;R&#39;;

SELECT * FROM film
WHERE rating != &#39;R&#39;;</code></pre>
<h2 id="실전-문제-2">실전 문제</h2>
<blockquote>
<p>한 고객이 매장이 지갑을 두고 갔다. 
&#39;Nancy Thomas&#39; 고객의 이메일 주소를 찾아서 이메일을 보내야한다. 
그의 이메일 주소를 조회해보자.</p>
</blockquote>
<pre><code class="language-sql">SELECT email FROM customer
WHERE first_name = &#39;Nancy&#39; AND last_name = &#39;Thomas&#39;;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/25027b7b-3f19-429f-a883-b28867dd4027/image.png" width="250px">

<blockquote>
<p>한 고객이 영화 &#39;Outlaw Hanky&#39;의 내용(description)을 궁금해한다.
영화의 내용을 조회해보자.</p>
</blockquote>
<pre><code class="language-sql">SELECT description FROM film
WHERE title=&#39;Outlaw Hanky&#39;;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/d05e9cad-53af-46fd-9745-7957271aa7d4/image.png" width="700px">

<blockquote>
<p>한 고객이 영화 반납을 연체했다.
고객의 &#39;259 Ipoh Drive&#39;주소로 우편을 보냈고, 전화로도 이 내용을 알려야 한다. 
해당 주소에 살고있는 고객의 전화번호를 조회해보자.</p>
</blockquote>
<pre><code class="language-sql">SELECT phone FROM address
WHERE address = &#39;259 Ipoh Drive&#39; ;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/49838ae6-479f-4f5c-b4a1-73d064b09e1b/image.png" width="250x">

<h1 id="5-order-by">5. ORDER BY</h1>
<p>열의 값에 따라 오름차순(ASC)/내림차순(DESC)으로 정렬한다.</p>
<pre><code class="language-sql">SELECT * FROM customer
ORDER BY first_name ASC;</code></pre>
<p>여러 칼럼에 각기 다른 정렬을 적용할 수도 있다.</p>
<pre><code class="language-sql">SELECT store_id, first_name, last_name FROM customer
ORDER BY store_id DESC, first_name ASC;

👉 1. store_id를 기준으로 내림차순 정렬
   2. first_name을 기준으로 오름차순 정렬 (A to Z)</code></pre>
<h1 id="6-limit">6. LIMIT</h1>
<p>쿼리에 대해 반환되는 행의 개수를 제한할 수 있다.
쿼리의 <strong>가장 마지막에 실행</strong>된다.</p>
<pre><code class="language-sql">SELECT * FROM payment
WHERE amount != 0.00
ORDER BY payment_date DESC
LIMIT 5;</code></pre>
<blockquote>
<p>빠르게 테이블 구조만 확인하고 싶을 때, ```<strong>SELECT * FROM table LIMIT 1`</strong>``을 쓰기도 한다.</p>
</blockquote>
<h2 id="실전-문제-3">실전 문제</h2>
<blockquote>
<p>처음으로 결제한 10명의 고객에게 리워드를 지급하고 싶다.
처음으로 결제를 생성한 고객 10명의 ID는?</p>
</blockquote>
<pre><code class="language-sql">SELECT customer_id FROM payment
ORDER BY payment_date ASC
LIMIT 10;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/8567ce57-c02a-4998-a184-93ca03d0090c/image.png" width="250px">

<blockquote>
<p>고객이 점심 시간에 볼 짧은 영화를 대여하고 싶어한다.
상영시간이 가장 짧은 영화 5편의 제목은?</p>
</blockquote>
<pre><code class="language-sql">SELECT title, length FROM film
ORDER BY length ASC
LIMIT 5;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/5b24d71b-47cb-436b-834b-09098c292a18/image.png" width="250px">


<p> <em>🚨 상영 시간이 46분인 영화가 5개일 뿐이라는 보장은 없다.
6분짜리 영화가 6개 이상일수도 있다.</em></p>
<blockquote>
<p>위의 고객이 50분 이하의 영화를 찾는다면, 제안할 수 있는 영화는 총 몇 편인가?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*) FROM film
WHERE length &lt;= 50;</code></pre>
<img src="https://velog.velcdn.com/images/woori_dec/post/fdacbd20-a94f-4516-bf0f-5c4db2963260/image.png" width="250px">

<hr>
<h1 id="7-between">7. BETWEEN</h1>
<p>값의 범위를 제어한다.
범위 안에 존재하는 값만 가져오는 게 아니라,
<strong>NOT BETWEEN</strong> 연산자로 범위 밖에 존재하는 값을 조회할 수도 있다.</p>
<pre><code class="language-sql">SELECT * FROM payment
WHERE amount NOT BETWEEN 8 AND 9;

SELECT * FROM payment
WHERE payment_date BETWEEN &#39;2007-02-01&#39; AND &#39;2007-02-15&#39;;</code></pre>
<p>시간 범위를 지정할 땐, <strong>TIMESTAMP</strong>를 고려해야 한다.</p>
<p><code>BETWEEN &#39;2007-02-01&#39; AND &#39;2007-02-14&#39;</code> 로 범위를 지정하면, 
14일에 결제한 데이터는 출력되지 않는다.</p>
<hr>
<h1 id="8-in">8. IN</h1>
<pre><code class="language-sql">SELECT color FROM table
WHERE color IN (&#39;red&#39;,&#39;blue&#39;,&#39;green&#39;);</code></pre>
<p>색상이 red <strong>OR</strong> blue <strong>OR</strong> green 인인 행을 조회한다.</p>
<p><strong>NOT</strong> 연산자와 결합해서 사용할 수도 있다.</p>
<pre><code class="language-sql">SELECT color FROM table
WHERE color NOT IN (&#39;red&#39;,&#39;blue&#39;);</code></pre>
<pre><code class="language-sql">SELECT COUNT(*) FROM payment
WHERE amount IN (0.99,1.98,1.99);

SELECT COUNT(*) FROM payment
WHERE amount NOT IN (0.99,1.98,1.99);

SELECT * FROM customer
WHERE first_name IN (&#39;John&#39;,&#39;Jake&#39;,&#39;Julie&#39;);</code></pre>
<hr>
<h1 id="9-like-and-ilike">9. LIKE and ILIKE</h1>
<p>@gmail.com 으로 끝나는 이메일 주소를 찾는 경우처럼,
문자열 내의 일반 패턴에 매칭하고 싶을 때 사용한다.</p>
<p><code>WHERE name LIKE &#39;A%&#39;</code> : 대문자A로 시작하는 모든 문자열
<code>WHERE name LIKE &#39;%a&#39;</code> : 소문자a로 끝나는 모든 문자열</p>
<p><code>WHERE title LIKE &#39;Mission Impossible _&#39;</code> 
: Missino Impossible 뒤에는 문자 <strong>하나만</strong> 올 수 있다. <em>(1, 2, 3, 4, ...)</em></p>
<p><code>WHERE name LIKE &#39;_her%&#39;</code> 의 경우</p>
<ul>
<li><span style="color:red">C</span>her<span style="color:green">yl</span></li>
<li><span style="color:red">T</span>her<span style="color:green">esa</span></li>
<li><span style="color:red">S</span>her<span style="color:green">ri</span>
를 포함할 수 있다.</li>
</ul>
<p>더 자세한 내용은 <a href="https://www.postgresql.org/docs/current/functions-matching.html">PostgreSQL 공식문서</a> 참조.</p>
<pre><code class="language-sql">SELECT * FROM customer
WHERE first_name LIKE &#39;J%&#39; AND last_name LIKE &#39;S%&#39;;</code></pre>
<p>성의 첫 글자가 <strong>대문자 J</strong>로 시작하고, 이름의 첫 글자가 <strong>대문자 S</strong>로 시작하는 고객을 조회한다.
👉 <em>LIKE 연산자는 <strong>대소문자를 구분한다.</strong></em></p>
<pre><code class="language-sql">SELECT * FROM customer
WHERE first_name ILIKE &#39;j%&#39; AND last_name ILIKE &#39;s%&#39;;</code></pre>
<p>성의 첫 글자가 <strong>J 혹은 j</strong>로 시작하고, 이름의 첫 글자가 <strong>S 혹은 s</strong>로 시작하는 고객을 조회한다.
👉 <em>ILIKE 연산자는 <strong>대소문자를 구분하지 않는다.</strong></em></p>
<pre><code class="language-sql">SELECT * FROM customer
WHERE first_name LIKE &#39;_her%</code></pre>
<hr>
<h1 id="마무리-문제">마무리 문제</h1>
<blockquote>
<p>5달러보다 큰 금액을 결제한 거래는 몇 건입니까?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*) FROM payment 
WHERE amount &gt; 5;</code></pre>
<blockquote>
<p>성이 P로 시작하는 배우는 몇 명입니까?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*) FROM actor
WHERE first_name LIKE &#39;P%&#39;;</code></pre>
<blockquote>
<p>고객 주소에서 중복되지 않는 고유한 지역(district)은 몇 개입니까?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(DISTINCT(district)) FROM address;</code></pre>
<blockquote>
<p>위에서 조회한 중복되지 않는 지역의 목록을 조회하세요.</p>
</blockquote>
<pre><code class="language-sql">SELECT DISTINCT(district) FROM address;</code></pre>
<blockquote>
<p>R등급이고 교환 비용이 5달러에서 15달러 사이인 영화는 몇 개입니까?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*) FROM film
WHERE rating = &#39;R&#39; 
AND replacement_cost BETWEEN 5 AND 15;</code></pre>
<blockquote>
<p>제목에 &#39;Truman&#39;이 포함되는 영화는 몇 편인가요?</p>
</blockquote>
<pre><code class="language-sql">SELECT COUNT(*) FROM film
WHERE title LIKE &#39;%Truman%&#39;;</code></pre>
<hr>
<p>SQLD 공부할 때 같아서 재밌었다.
ILIKE는 처음 보는 연산자라서 배워가는 것도 있었다.
아님 내가 공부를 덜 했던 걸지도...😉</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL 설치 및 세팅]]></title>
            <link>https://velog.io/@woori_dec/PostgreSQL-%EC%9E%85%EB%AC%B8-gx2b1eyr</link>
            <guid>https://velog.io/@woori_dec/PostgreSQL-%EC%9E%85%EB%AC%B8-gx2b1eyr</guid>
            <pubDate>Thu, 22 Aug 2024 06:55:57 GMT</pubDate>
            <description><![CDATA[<a href="https://www.udemy.com/course/best-sql-2022/?couponCode=SKILLS4SALEB">
<img src="https://velog.velcdn.com/images/woori_dec/post/1e1181c8-4727-4e10-af20-a984c7b92cbd/image.png"/>
</a>

<p><em>(↑ 이미지 클릭 시 강의 페이지로 넘어감)</em></p>
<p><strong>PostgreSQL</strong> 에 관해 조금 더 찾아보고 있었는데, 마침 <strong>Udemy</strong> 에서 세일을 하고 있었다.
무슨 면접 한 번을 위해서 강의까지 찾아듣나 싶지만, 이게 나다... 🥲</p>
<p>하지만, 굳이 면접이 아니더라도 이런 기초 강의를 통해
학원에서 제대로 사용해보지 않은 쿼리문을 복습하는 기회라고 생각하면
꽤 도움이 될 것 같다.</p>
<hr>
<h1 id="1-설치">1. 설치</h1>
<p><a href="https://www.postgresql.org/">https://www.postgresql.org/</a> 에 들어가서 PostgreSQL을 설치하고,</p>
<img src="https://velog.velcdn.com/images/woori_dec/post/d8503b2e-4667-4c5d-ba07-e6dc517dfce7/image.png" width="500px"/>

<p><a href="https://www.pgadmin.org/">https://www.pgadmin.org/</a> 에서 PgAdmin을 설치했다.</p>
<p>그리고 컴퓨터를 껐다가 켜면,</p>
<img src="https://velog.velcdn.com/images/woori_dec/post/03642cb7-406b-4599-a33f-6a751d4b31cc/image.png" width="500px"/>

<p>준비 끝!</p>
<hr>
<h1 id="2-데이터베이스-생성">2. 데이터베이스 생성</h1>
<img src="https://velog.velcdn.com/images/woori_dec/post/a9a080f1-0d1a-4e8f-badb-5b30cd680ba7/image.png" width="500px"/>

<p><strong>Databases</strong> 우클릭 -&gt; Create -&gt; Database</p>
<img src="https://velog.velcdn.com/images/woori_dec/post/83513091-b8ca-4259-a86c-1c48f75b98ff/image.png" width="500px"/>

<p>강좌에서 설명하는대로 <strong>dvdrental</strong> 데이터베이스를 생성했다.</p>
<details>
  <summary>기존 파일 복구 (Restore)</summary>
강사가 제공하는 데이터베이스를 사용하기 위해 Restore를 진행했다.

<img src="https://velog.velcdn.com/images/woori_dec/post/75eb26aa-5f89-409c-98e7-4d3e24057e7e/image.png" width="500px"/>

<p>복구를 진행하기 전에 Data Options 탭에서 Sections 설정을 몇 가지 수정했다.</p>
<img src="https://velog.velcdn.com/images/woori_dec/post/fecb6d26-7a08-4b08-85fa-3ece844925fd/image.png" width="500px"/>

<p><em>(Pre-data / Data / Post-data 세 가지 수정했음)</em></p>
</details>

<blockquote>
<p>다크모드 :  File &gt; Preferences &gt; Miscellaneous &gt; Themes</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/686ba5c2-720f-4dde-be80-288399dd95ec/image.png" alt=""></p>
<blockquote>
<p>대시보드의 <strong>Refresh rates</strong> 등의 속성을 바꾸고 싶을 때에도, File &gt; Preferences</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA['좋아요'를 구현해보자]]></title>
            <link>https://velog.io/@woori_dec/%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B6%81%EB%A7%88%ED%81%AC-%EA%B5%AC%ED%98%84-1</link>
            <guid>https://velog.io/@woori_dec/%EC%A2%8B%EC%95%84%EC%9A%94-%EB%B6%81%EB%A7%88%ED%81%AC-%EA%B5%AC%ED%98%84-1</guid>
            <pubDate>Wed, 21 Aug 2024 04:44:41 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/woori_dec/post/1aa1e4b1-55e1-4d8c-9264-ee689df2861e/image.png" alt=""></p>
<p>해당 부분을 <strong>postActions</strong> 컴포넌트로 분리했다.</p>
<pre><code class="language-js">import React from &quot;react&quot;;
import FavoriteBorder from &quot;@mui/icons-material/FavoriteBorder&quot;;
import ChatBubbleOutlineIcon from &quot;@mui/icons-material/ChatBubbleOutline&quot;;
import TelegramIcon from &quot;@mui/icons-material/Telegram&quot;;
import BookmarkBorderIcon from &quot;@mui/icons-material/BookmarkBorder&quot;;
import styles from &quot;../styles/post.module.css&quot;;

function PostActions() {
  return (
    &lt;div className={styles.icons_container}&gt;
      &lt;div&gt;
        &lt;FavoriteBorder className={styles.icon} /&gt;
        &lt;ChatBubbleOutlineIcon className={styles.icon} /&gt;
        &lt;TelegramIcon className={styles.icon} /&gt;
      &lt;/div&gt;
      &lt;div&gt;
        &lt;BookmarkBorderIcon className={styles.icon} /&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default PostActions;</code></pre>
<hr>
<h1 id="1-좋아요-상태를-저장해보자">1. &#39;좋아요&#39; 상태를 저장해보자</h1>
<p>아이콘을 클릭하면, <strong>handlePostLike</strong> 이벤트가 발생한다.</p>
<pre><code class="language-js">&lt;FavoriteBorder className={styles.icon} onClick={handlePostLike} /&gt;</code></pre>
<p><strong>handlePostLike</strong> 이벤트 메서드는 다음과 같이 구현했다.</p>
<pre><code class="language-js">const handlePostLike = async () =&gt; {
  try {
    const response = await axios.post(&#39;/api/postlike&#39;, {
      userId: userId, 
      postId: postId
    });
    console.log(response.data);
  } catch (error) {
    console.error(&#39;Error liking the post:&#39;, error);
  }
};</code></pre>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/40b2911e-bcfe-48d9-9872-efd38b748717/image.png" alt=""></p>
<p>데이터베이스에 <strong>사용자id</strong> 와 <strong>포스트id</strong>가 정상적으로 저장되는 것을 확인했다.</p>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/e489053d-48ca-4d6d-910d-310a1f8a1989/image.png" alt=""></p>
<p>그런데, 좋아요를 눌러도 icon이 변하지 않았다. 😕</p>
<p>당연하다.
아이콘은 여전히 <code>&lt;FavoriteBorder /&gt;</code> 이거 하나다.</p>
<hr>
<h1 id="2-좋아요-상태를-가져와보자">2. &#39;좋아요&#39; 상태를 가져와보자</h1>
<p>사용자가 해당 포스트에 좋아요를 눌렀는지 확인하는 메서드를 구현했다.</p>
<pre><code class="language-js">const [isLiked, setIsLiked] = useState(false);

useEffect(() =&gt; {
  const fetchLikeStatus = async () =&gt; {
    try {
      const response = await axios.get(&#39;/api/postlike/status&#39;, {
        params: { userId: userId, postId: postId }
      });
      setIsLiked(response.data);
    } catch (error) {
      console.error(&#39;Error fetching like status:&#39;, error);
    }
  };

  fetchLikeStatus();
}, [userId, postId]);</code></pre>
<p><code>setIsLiked(response.data);</code> 에서 응답이 true라면, <code>isLinked = true</code>가 된다.
<strong>isLinked = true</strong>일때, <code>&lt;FavoriteBorder /&gt;</code> 대신 <code>&lt;FavoriteIcon /&gt;</code>가 출력되어야한다.</p>
<pre><code class="language-js">isLiked ? (
  &lt;FavoriteIcon className={styles.icon} /&gt;
) : (
  &lt;FavoriteBorder className={styles.icon} onClick={handlePostLike} /&gt;
)}</code></pre>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/0e53670b-d492-4c89-a22a-b3a6dc89d2dd/image.png" alt=""></p>
<p>그러면 이렇게 좋아요 아이콘이 바뀌지 않는다.
여전히 <code>&lt;FavoriteBorder /&gt;</code> 가 출력된다.</p>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/3937e7f6-de78-486f-afe6-e7b0b8b7acac/image.png" alt=""></p>
<p>모달창을 껐다가 켜야 <code>&lt;FavoriteIcon /&gt;</code> 가 출력된다.</p>
<h1 id="🚨왜-즉시-변경되지-않지">🚨왜 즉시 변경되지 않지?</h1>
<pre><code class="language-js">useEffect(() =&gt; {
  const fetchLikeStatus = async () =&gt; {
    try {
      const response = await axios.get(&#39;/api/postlike/status&#39;, {
        params: { userId: userId, postId: postId }
      });
      setIsLiked(response.data); // 백엔드에서 받아온 좋아요 상태를 설정
    } catch (error) {
      console.error(&#39;Error fetching like status:&#39;, error);
    }
  };

  fetchLikeStatus();
}, [userId, postId]);</code></pre>
<p><code>useEffect</code> 가 <strong>처음 로드될 때</strong> 좋아요 상태를 가져오기 때문이다.</p>
<hr>
<h1 id="🍀상태를-즉시-변경해보자">🍀상태를 즉시 변경해보자</h1>
<p>좋아요 아이콘을 클릭하면, <strong>isLiked</strong> 상태가 즉시 변경되도록 구현하변 되지 않을까?</p>
<pre><code class="language-js">const handlePostLike = async () =&gt; {
  try {
    const response = await axios.post(&#39;/api/postlike&#39;, {
      userId: userId,
      postId: postId
    });
    console.log(response.data);
    setIsLiked(true); // 좋아요 상태를 즉시 true로 만든다
  } catch (error) {
    console.error(&#39;Error liking the post:&#39;, error);
  }
};</code></pre>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/8d4be722-dd43-43a2-9a28-2198ea433c58/image.png" alt=""></p>
<p><code>&lt;FavoriteBorder /&gt;</code>  아이콘을 클릭하면, 아이콘이 바로 <code>&lt;FavoriteIcon /&gt;</code> 로 바뀐다. 굿👍</p>
<p><strong>하지만,</strong> 모달창을 켤 때, 
<code>&lt;FavoriteBorder /&gt;</code> 가 보였다가, <code>&lt;FavoriteIcon /&gt;</code>로 바뀌는 것을 확인하고 말았다.</p>
<h1 id="🚨초기-ui가-노출된다">🚨초기 UI가 노출된다</h1>
<p>async 데이터를 가져오는 시간이 필요하기 때문인 것 같다.
모달창을 띄울 때, useEffect로 좋아요 상태를 호출하기 때문이다.
모달창을 띄울 때, 이미 좋아요 상태를 가지고 있어야할 것 같다.</p>
<hr>
<h1 id="🍀부모에서-상태를-받아오자">🍀부모에서 상태를 받아오자</h1>
<p>그럼, 부모 컴포넌트에서 <strong>isLiked</strong> 상태를 가져와야하나? <span style="font-size:12px; color:grey;"><em>테스트 해보면 되겠지...</em></span></p>
<p>부모 컴포넌트에 <strong>좋아요 상태를 가져오는 메서드</strong>를 추가했다.</p>
<pre><code class="language-js">useEffect(() =&gt; {
  const fetchLikeStatus = async () =&gt; {
    if (loggedInUser &amp;&amp; postModal) {
      try {
        const response = await axios.get(&#39;/api/postlike/status&#39;, {
          params: { userId: loggedInUser.id, postId: postModal.postId }
        });
        setIsLiked(response.data);
      } catch (error) {
        console.error(&#39;Error fetching like status:&#39;, error);
      }
    }
  };

  fetchLikeStatus();
}, [loggedInUser, postModal]);

&lt;PostActions postId={postModal.postId} userId={loggedInUser.id} isLiked={isLiked} setIsLiked={setIsLiked} /&gt;</code></pre>
<p>❗부모 컴포넌트에서 상태를 받아오므로, 자식 컴포넌트에서는 axios 함수를 뺐다.</p>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/5209a2c4-8dd0-4003-9a15-cdde54948de5/image.png" alt=""></p>
<p>됐다. 진짜 됐는데, 사진으로만 첨부하니까 잘 모르겠네...
다음에는 영상으로 따오던지 해야겠다.</p>
<hr>
<p>북마크도 동일한 방식으로 진행하면 될 것 같다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[PostgreSQL 입문]]></title>
            <link>https://velog.io/@woori_dec/PostgreSQL-%EC%9E%85%EB%AC%B8</link>
            <guid>https://velog.io/@woori_dec/PostgreSQL-%EC%9E%85%EB%AC%B8</guid>
            <pubDate>Tue, 20 Aug 2024 11:38:23 GMT</pubDate>
            <description><![CDATA[<p>지원한 회사에서 <strong>PostgreSQL</strong> 을 사용한단다.
이런 불시장에서 면접 기회를 얻은 것만 해도 감사한 일이다. 
그리고 이 귀한 기회를 놓치고 싶지 않다.</p>
<p><em>그래서 <strong>PostgreSQL</strong> 이 뭔데? 🤔</em></p>
<p>PostgreSQL은 <strong>오픈 소스 관계형 데이터베이스 관리 시스템(RDBMS)</strong> 이다.
이건 기존에 사용했던 <strong>MySQL, MariaDB, Oracle</strong> 과 같다.</p>
<p>간단하게 각각의 특징을 살펴보자면,</p>
<ul>
<li><strong>MySQL, MariaDB</strong> : 속도와 성능에 중점을 둔 경량화된 RDBMS <em>(오픈 소스)</em></li>
<li><strong>Oracle</strong> : 고성능과 보안성, 데이터 복구 기능 등이 뛰어난 RDBMS <em>(상용)</em></li>
<li><strong>PostgreSQL</strong> : 안정성과 확장성에서 독보적인 RDBMS <em>(오픈 소스)</em></li>
</ul>
<p>이렇게 정리할 수 있겠다.</p>
<hr>
<h1 id="데이터-형식">데이터 형식</h1>
<p><strong>PostgreSQL</strong> 의 확장성에 대해 조금 더 알아보자면, 
PostgreSQL은 <strong>NoSQL</strong> 기능도 함께 제공한다.</p>
<p><em><strong>NoSQL</strong>? 그건 또 뭔데? 🤔</em></p>
<p>NoSQL은 데이터베이스의 한 종류다.
<strong>데이터베이스</strong>란? 정보를 저장하고 관리하는 장소라고 생각하면 된다. </p>
<p>이전에 사용했던 MySQL, Oracle 같은 데이터베이스는 정보를 테이블에 보관한다.
그런데, 최근에는 테이블에 딱 맞지 않는 <strong>비정형 데이터</strong>라는 게 많아졌다. 
비정형 데이터는 쉽게 말해, 정해진 틀이 없는 정보다. </p>
<p><strong>NoSQL 데이터베이스</strong>는 이런 비정형 데이터를 유연하게 다루기 위해 만들어졌다. 
테이블 구조가 아니라, JSON 같은 형식을 사용해 원하는 대로 정보를 저장할 수 있다.
JSON은 간단히 말해, 정보를 <strong>키-값 쌍</strong>으로 저장하는 방식이다.</p>
<p>예를 들어, 사용자의 프로필 정보는 사람마다 다를 수 있다.
어떤 사람은 취미를, 어떤 사람은 위치를 추가할 수도 있잖나!</p>
<p>비교를 해보자.</p>
<p>☑️<strong>테이블</strong>로 저장하는 방식이다.</p>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Hobby</th>
<th>Location</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>김민지</td>
<td><a href="mailto:minji@example.com">minji@example.com</a></td>
<td>노래</td>
<td>서울</td>
</tr>
<tr>
<td>2</td>
<td>팜하니</td>
<td><a href="mailto:pham@example.com">pham@example.com</a></td>
<td>춤</td>
<td>NULL</td>
</tr>
<tr>
<td>3</td>
<td>다니엘</td>
<td><a href="mailto:danielle@example.com">danielle@example.com</a></td>
<td>NULL</td>
<td>호주</td>
</tr>
</tbody></table>
<blockquote>
<p>데이터가 깔끔하게 정리되지만, 모든 사용자에게 동일한 필드를 적용해야 하므로, 
<strong>필드가 비어 있는 경우</strong>가 생기거나, <strong>테이블을 자주 변경</strong>해야 할 수 있다.</p>
</blockquote>
<p>☑️다음은 <strong>JSON</strong> 형식으로 저장하는 방식이다.</p>
<pre><code class="language-json">{
  &quot;users&quot;: [
    {
      &quot;id&quot;: 1,
      &quot;name&quot;: &quot;김민지&quot;,
      &quot;email&quot;: &quot;minji@example.com&quot;,
      &quot;hobby&quot;: &quot;노래&quot;,
      &quot;location&quot;: &quot;서울&quot;
    },
    {
      &quot;id&quot;: 2,
      &quot;name&quot;: &quot;팜하니&quot;,
      &quot;email&quot;: &quot;pham@example.com&quot;,
      &quot;hobby&quot;: &quot;춤&quot;
    },
    {
      &quot;id&quot;: 3,
      &quot;name&quot;: &quot;다니엘&quot;,
      &quot;email&quot;: &quot;danielle@example.com&quot;,
      &quot;location&quot;: &quot;호주&quot;
    }
  ]
}
</code></pre>
<blockquote>
<p>각 사용자가 자신에게 필요한 정보만을 저장할 수 있다. 추가적인 필드가 필요할 때에도 <strong>테이블 구조를 변경할 필요 없이</strong>, JSON 구조에 해당 필드를 추가하면 된다. → <strong>유연성</strong>이 높다.</p>
</blockquote>
<p><strong>PostgreSQL</strong>은 이런 NoSQL 방식도 지원한다. 
즉, 전통적인 테이블 구조는 물론이고 비정형 데이터도 유연하게 다룰 수 있다는 것이다. </p>
<p>그래서 <strong>PostgreSQL</strong>은 기존의 RDBMS보다 더 다양한 데이터를 처리할 수 있다는 장점이 있다.</p>
<hr>
<h1 id="쿼리문">쿼리문</h1>
<p>다른 형식의 데이터를 처리하기 위해서는 작성하는 <strong>쿼리문의 형식</strong>도 다를 것이다.</p>
<p>☑️아래는 전통적인 <strong>테이블 구조로 저장된 데이터</strong>를 조회하는 쿼리문이다.</p>
<pre><code class="language-sql">SELECT name, email, hobby, location
FROM users
WHERE hobby IS NOT NULL;</code></pre>
<p>☑️다음은 <strong>JSON 데이터</strong>를 다룰 때의 쿼리문이다.</p>
<pre><code class="language-sql">SELECT json_data-&gt;&gt;&#39;name&#39; AS name,
       json_data-&gt;&gt;&#39;email&#39; AS email,
       json_data-&gt;&gt;&#39;hobby&#39; AS hobby,
       json_data-&gt;&gt;&#39;location&#39; AS location
FROM users_json
WHERE json_data-&gt;&gt;&#39;hobby&#39; IS NOT NULL;</code></pre>
<p style="color:gray">
    (_users_json_ 테이블에는 JSON 데이터를 저장하는 _json_data_ 필드가 존재)
</p>

<p><em>위의 쿼리문은 너무나 익숙하지만, 아래의 쿼리문은 익숙하지 않다.
어떤 느낌인지 알겠지만, 명확하게는 모르겠달까? 🤔
그래서 한 줄씩 뜯어보겠다.</em></p>
<ul>
<li><p><code>json_data-&gt;&gt;&#39;name&#39;</code></p>
<ul>
<li>PostgreSQL에서 JSON 데이터 타입을 다룰 때 사용하는 연산자. </li>
<li><code>-&gt;&gt;</code> 연산자는 JSON 객체에서 문자열을 추출할 때 사용된다.</li>
</ul>
</li>
<li><p><code>AS name</code></p>
<ul>
<li>추출한 값을 name이라는 별칭으로 표시하게 한다.</li>
</ul>
</li>
<li><p><code>FROM users_json</code></p>
<ul>
<li>users_json이라는 테이블에서 데이터를 조회한다.</li>
</ul>
</li>
<li><p><code>WHERE json_data-&gt;&gt;&#39;hobby&#39; IS NOT NULL</code></p>
<ul>
<li>JSON 데이터에서 hobby 키의 값이 존재하는 행만 필터링한다.</li>
</ul>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[모달창을 만들어보자]]></title>
            <link>https://velog.io/@woori_dec/%EB%AA%A8%EB%8B%AC%EC%B0%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@woori_dec/%EB%AA%A8%EB%8B%AC%EC%B0%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Tue, 20 Aug 2024 04:10:55 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/woori_dec/post/ef47d616-9ddb-429c-8b06-0aac8a3e633d/image.png" alt=""></p>
<p>피드의 썸네일을 눌렀을 때, 출력되는 모달창이다.
어느 프로젝트든 모달창을 꼭 만드는 것 같은데, 만들 때마다 헷갈려서 정리해두기로 했다.</p>
<hr>
<h1 id="1-기본-모달창">1. 기본 모달창</h1>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/7104dc85-331c-4ea1-840c-3de73a652590/image.png" alt=""></p>
<details>
  <summary>코드보기</summary>

  <div markdown = "1">
    ```js
    return (
    <div className={styles.modalBackground}>
      <div className={styles.modalBox}>
        모달창입니다
      </div>
    </div>
    );


<pre><code>// css
.modalBackground {
z-index: 10000;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgb(0 0 0 / 80%);
display: flex;
align-items: center;
margin: auto;
}

.modalBox {
background-color: white;
border-radius: 8px;
margin: auto;
height: 50%;
}
```</code></pre>  </div>
</details>

<p>이제 모달창 내부를 두 섹션으로 나눈다.</p>
<ol>
<li>이미지 출력부</li>
<li>내용 및 댓글 출력부</li>
</ol>
<hr>
<h1 id="2-모달창-섹션-분리">2. 모달창 섹션 분리</h1>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/08446219-a59e-43d4-b367-4859b33c73e9/image.png" alt=""></p>
<p>그리드 시스템을 사용하기 위해 부모 컨테이너에 <code>display:grid</code>를 적용했다.</p>
<details>
  <summary>코드보기</summary>

<pre><code>```js
return (
  &lt;div className={styles.modalBackground}&gt;
    &lt;div className={styles.modalBox}&gt;
      {/* 이미지 출력 */}
      &lt;div className={styles.sectionImg}&gt;
      &lt;/div&gt;
      {/* 내용 출력 */}
      &lt;div className={styles.sectionContent}&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
);

// css
.modalBox {
  background-color: white;
  border-radius: 8px;
  margin: auto;
  height: 50%;
  display: grid;
  grid-template-columns: 1fr 1fr; /* 이미지와 콘텐츠가 각각 50%씩 차지 */
}

.sectionImg {
  background: pink;
  grid-row: 1;
  grid-column: 1;
}

.sectionContent {
  grid-row: 1;
  grid-column: 2;
  background: skyblue;
}

```</code></pre></details>

<hr>
<h1 id="3-이미지-넣기">3. 이미지 넣기</h1>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/fafef519-1789-4f69-b105-8f0d3817e54e/image.png" alt=""></p>
<p>이미지는 임의로 넣었다.
sectionImg 안에 가득 차도록 구현했다.</p>
<details>
<summary>코드보기</summary>
<div>

<pre><code class="language-js">return (
  &lt;div className={styles.modalBackground}&gt;
    &lt;div className={styles.modalBox}&gt;
      {/* 이미지 출력 */}
      &lt;div className={styles.sectionImg}&gt;
        &lt;img src={이에로} /&gt;
      &lt;/div&gt;
      {/* 내용 출력 */}
      &lt;div className={styles.sectionContent}&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
);

// css
.sectionImg {
  grid-column: 1;
  height: 100%;
  aspect-ratio: 1/1;
  overflow: hidden;
  position: relative;
}

.sectionImg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}</code></pre>
</div>
</details>


<h1 id="4-섹션-나누기">4. 섹션 나누기</h1>
<img src="https://velog.velcdn.com/images/woori_dec/post/0e414515-1861-4f11-9739-bc717eeecbd8/image.png" width="500px">

<p>섹션은 6개로 나눴다.</p>
<ol>
<li>프로필</li>
<li>내용 및 댓글</li>
<li>좋아요/댓글/공유/북마크 버튼</li>
<li>좋아요 개수</li>
<li>작성일</li>
<li>댓글 작성</li>
</ol>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/bbd232b8-6693-4583-aa08-0e8481f18c42/image.png" alt=""></p>
<p>마찬가지로 그리드 시스템을 사용하기 위해 부모 컨테이너에 <code>display:grid</code>를 적용했다.</p>
<details>
<summary>코드보기</summary>
<div>

<pre><code class="language-js">return (
  &lt;div className={styles.modalBackground}&gt;
    &lt;div className={styles.modalBox}&gt;
      {/* 이미지 출력 */}
      &lt;div className={styles.sectionImg}&gt;
        &lt;img src={이에로}  alt=&#39;이에로 고양이&#39;/&gt;
      &lt;/div&gt;
      {/* 내용 출력 */}
      &lt;div className={styles.sectionContent}&gt;
        &lt;div className={styles.sectionProfile}&gt;
          {/* 프로필 */ }
        &lt;/div&gt;
        &lt;div className={styles.sectionComment}&gt;
          {/* 내용 및 댓글 */}
        &lt;/div&gt;
        &lt;div className={styles.sectionButtons}&gt;
          {/* 좋아요/댓글/공유/북마크 */}
        &lt;/div&gt;
        &lt;div className={styles.sectionLikes}&gt;
          {/* 좋아요 개수 */}
        &lt;/div&gt;
        &lt;div className={styles.sectionDate}&gt;
          {/* 작성일 */}
        &lt;/div&gt;
        &lt;div className={styles.addComment}&gt;
          {/* 댓글 작성 */}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
);

// css
.sectionContent {
  grid-column: 2;
  display: flex;
  flex-direction: column;
  padding: 16px;
}

.sectionProfile {
  display: flex;
  align-items: center;
  margin-bottom: 16px;
}</code></pre>
</div>
</details>

<hr>
<h1 id="5-모달창-닫기">5. 모달창 닫기</h1>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/73cb2e4c-1009-4227-837c-909197619036/image.png" alt=""></p>
<p>모달창을 열었으니, 닫기 위한 버튼이 필요하다.
모달창 닫기 버튼은 첨부한 사진처럼 배경의 우측 상단에 위치하게 했다.</p>
<p>또한, <strong>modalBox</strong> 바깥은 배경 부분을 클릭해도 모달창이 닫히게 구현하기 위해서
<strong>modalBackground</strong> 에도 <code>onClick</code>이벤트를 추가했다.</p>
<pre><code class="language-js">const closeModal = () =&gt; {  
  handleCloseModal(); // 모달을 닫기 위해 handleCloseModal 호출
};

return (
  &lt;div className={styles.modalBackground} onClick={closeModal}&gt;
    &lt;div className={styles.modalBox}&gt;
      {/* 이미지 출력 */}
      &lt;div className={styles.sectionImg}&gt;
        ...
      &lt;/div&gt;
      {/* 내용 출력 */}
      &lt;div className={styles.sectionContent}&gt;
        ...
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;button className={styles.closeBtn} onClick={closeModal}&gt;
      &lt;CloseIcon /&gt;
    &lt;/button&gt;
  &lt;/div&gt;
);</code></pre>
<p>🚨하지만, 이렇게 구현하면 <strong>modalBox</strong>를 클릭해도 모달창이 닫히는 현상이 발생한다.
부모 컨테이너에 걸린 이벤트가 전파되기 때문이다.</p>
<p>🍀이걸 해결하기 위해선, 자식 컨테이너에 <span style="color:red">이벤트 전파를 막는 메서드</span>를 추가해야한다.</p>
<pre><code class="language-js">const closeModal = () =&gt; {  
  handleCloseModal(); 
};

const stopPropagation = (e) =&gt; {
  e.stopPropagation(); // 이벤트 전파 방지
};

return (
  &lt;div className={styles.modalBackground} onClick={closeModal}&gt;
    &lt;div className={styles.modalBox} onClick={stopPropagation}&gt;
      {/* 이미지 출력 */}
      &lt;div className={styles.sectionImg}&gt;
        ...
      &lt;/div&gt;
      {/* 내용 출력 */}
      &lt;div className={styles.sectionContent}&gt;
        ...
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;button className={styles.closeBtn} onClick={closeModal}&gt;
      &lt;CloseIcon /&gt;
    &lt;/button&gt;
  &lt;/div&gt;
);</code></pre>
<hr>
<blockquote>
<p>여기서부터는 실제 데이터를 매핑해서 진행했다.</p>
</blockquote>
<h1 id="6-이미지-슬라이더">6. 이미지 슬라이더</h1>
<p>인스타그램에 게시할 수 있는 이미지는 최대 10장이다.
그 말인즉슨, 모달창에서도 이미지 목록을 확인할 수 있어야 한다는 의미다.</p>
<p>구현해야할 기능은 다음과 같다.</p>
<ol>
<li>최대 10장의 이미지를 차례대로 넘겨볼 수 있어야한다.</li>
<li>이전 이미지가 존재할 때에만 이전 버튼 출력</li>
<li>다음 이미지가 존재할 때에만 다음 버튼 출력</li>
</ol>
<h2 id="1-이미지-슬라이더">(1) 이미지 슬라이더</h2>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/582e75f2-2526-413a-b6ef-d17efb17bc0f/image.png" alt=""></p>
<pre><code class="language-js">const handleNext = () =&gt; {
  if (currentIndex &lt; postModal.images.length - 1) {
    setCurrentIndex(currentIndex + 1);
  }
};

const handlePrev = () =&gt; {
  if (currentIndex &gt; 0) {
    setCurrentIndex(currentIndex - 1);
  }
};

{/* 이미지 출력 */}
&lt;div className={styles.sectionImg}&gt;
  &lt;img 
    src={`http://localhost:8080${postModal.images[currentIndex].url}`} 
    alt={postModal.images[currentIndex].alt || &#39;게시물 이미지&#39;} 
  /&gt;
  {postModal.images.length &gt; 1 &amp;&amp; ( // 이미지가 한 장 이상일 때만 버튼이 출력되도록 
    &lt;&gt;
      &lt;button className={`${styles.Btn} ${styles.prevBtn}`} onClick={handlePrev}&gt;
        &lt;NavigateBeforeIcon /&gt;
      &lt;/button&gt;
      &lt;button className={`${styles.Btn} ${styles.nextBtn}`} onClick={handleNext}&gt;
        &lt;NavigateNextIcon /&gt;
      &lt;/button&gt;
    &lt;/&gt;
  )}
&lt;/div&gt;</code></pre>
<details>
  <summary>css 코드보기</summary>
  <div>


<pre><code>    .Btn {
      position: absolute;
      background-color: rgba(0, 0, 0, 0.7);
      color: white;
      border-radius: 50%;
      border: none;
      cursor: pointer;
      transform: translateY(-50%);
      width: 30px;
      height: 30px;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .Btn svg {
      width: 100%;
      height: 100%;
    }

    .prevBtn {
      top: 50%;
      left: 10px;
    }

    .nextBtn {
      top: 50%;
      right: 10px;
    }</code></pre>  </div>
</details>  

<h2 id="2-index로-버튼-출력-제어">(2) index로 버튼 출력 제어</h2>
<p>(1)에서는 이전, 다음 이미지의 존재 여부 관계없이 버튼이 출력되었다.
이젠, currentIndex를 이용해 버튼 출력을 제어한다.</p>
<pre><code class="language-js">{postModal.images.length &gt; 1 &amp;&amp; (
  &lt;&gt;
    // 현재 index가 0보다 크다면(첫번째 이미지가 아니라면)
    {currentIndex &gt; 0 &amp;&amp; ( 
      &lt;button className={`${styles.Btn} ${styles.prevBtn}`} onClick={handlePrev}&gt;
        &lt;NavigateBeforeIcon /&gt;
      &lt;/button&gt;
    )}
    // 현재 index가 이미지의 개수 - 1 보다 작다면(마지막 이미지라면)
    {currentIndex &lt; postModal.images.length - 1 &amp;&amp; ( 
      &lt;button className={`${styles.Btn} ${styles.nextBtn}`} onClick={handleNext}&gt;
        &lt;NavigateNextIcon /&gt;
      &lt;/button&gt;
    )}
  &lt;/&gt;
)}</code></pre>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/5bcc45d1-be1f-45fb-aa8d-c4d4e33886fd/image.png" alt=""><img src="https://velog.velcdn.com/images/woori_dec/post/f4c8793f-8d5a-4873-91ff-6e530d2d436d/image.png" alt=""></p>
<p>첫번째 이미지에서는 다음 버튼만, 마지막 이미지에서는 이전 버튼만 출력된다.</p>
<hr>
<p>이제 content 부분만 매핑하면 모달은 끝이다👍</p>
<p><img src="https://velog.velcdn.com/images/woori_dec/post/e480fefa-e777-48ef-851b-34491a7e10de/image.png" alt=""></p>
<p>아직 진행 중~</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Spring Boot 저장 경로 설정]]></title>
            <link>https://velog.io/@woori_dec/%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%B6%9C%EB%A0%A5%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94</link>
            <guid>https://velog.io/@woori_dec/%EC%97%85%EB%A1%9C%EB%93%9C%ED%95%9C-%EC%9D%B4%EB%AF%B8%EC%A7%80%EA%B0%80-%EC%B6%9C%EB%A0%A5%EB%90%98%EC%A7%80-%EC%95%8A%EC%95%84%EC%9A%94</guid>
            <pubDate>Mon, 19 Aug 2024 05:53:14 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>✨ 한줄 요약 : 저장 경로를 내부 → 외부로 변경했다</p>
</blockquote>
<img src="https://velog.velcdn.com/images/woori_dec/post/903c68d0-7b94-40ec-8612-4b1a9a76a1dd/image.png" width="500px">
<img src="https://velog.velcdn.com/images/woori_dec/post/13416e0e-1caf-4c5c-88f6-6e41c411e75d/image.png" width="500px">

<p>포스트를 업로드하는 데까지는 성공했다.
그런데, 피드에서 포스팅한 이미지를 불러오는 데에서 문제가 생겼다.</p>
<img src="https://velog.velcdn.com/images/woori_dec/post/76287f4e-b63b-4ddf-abf4-42006eaf8305/image.png" width="500px">
엑박이 뜬다🫤

<hr>
<h1 id="🚨문제-발생">🚨문제 발생</h1>
<h3 id="원인-분석">원인 분석</h3>
<img src="https://velog.velcdn.com/images/woori_dec/post/1ecd7a1c-49aa-4b78-b422-cedffa182461/image.png" width="500px">
DB에는 다른 이미지들과 같은 형식으로 경로 설정이 되어있다.
<img src="https://velog.velcdn.com/images/woori_dec/post/4045e3f5-6443-40f0-aff9-5b85ead8237d/image.png" width="500px">

<p>업로드 경로로 설정해둔 <code>/src/resources/static/uploads</code>에도 이미지가 업로드 되어있다.
그런데 왜 출력이 안 될까?</p>
<p><em>그런데 사실 아예 안 되는 건 아니다... 🤔</em></p>
<img src="https://velog.velcdn.com/images/woori_dec/post/c89585cf-76ea-433a-ac0f-75d158f79045/image.png" width="500px">

<p>서버를 껐다가 재가동하면, 이미지가 출력된다. <span style="color:grey"><del><em>오...</em></del></span></p>
<img src="https://velog.velcdn.com/images/woori_dec/post/b65bffcf-2462-4a10-a041-333a115b1dbd/image.png">

<p>Network 탭을 확인해보면 이런식으로 <span style="color:red">403 에러</span>가 떠있다.</p>
<p>서치 결과, 대부분의 문제는 경로 때문이라는 것을 알게 되었다.</p>
<p><span style="color:grey"><em>나는 경로 설정을 어떻게 했더라...?</em></span></p>
<pre><code class="language-java">private final String uploadDir = System.getProperty(&quot;user.dir&quot;) + &quot;/src/main/resources/static/uploads&quot;;

private String saveImage(MultipartFile image) {
    try {
        // 업로드 폴더가 존재하지 않으면 생성
        Path uploadPath = Paths.get(uploadDir);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath);
        }

        // 파일 저장
        String fileName = UUID.randomUUID().toString() + &quot;_&quot; + image.getOriginalFilename();
        Path filePath = uploadPath.resolve(fileName);
        image.transferTo(filePath);

        // 파일의 상대 URL 반환 (예: /uploads/파일이름)
        return &quot;/uploads/&quot; + fileName;
    } catch (IOException e) {
        throw new RuntimeException(&quot;Failed to save image&quot;, e);
    }
}</code></pre>
<p>👉 <code>/src/main/resources/static/uploads</code>로 경로를 설정했다.</p>
<h3 id="분석-결과">분석 결과</h3>
<p><code>/src/main/resources/static</code>는 Spring Boot 애플리케이션이 빌드 될 때 포함된다.</p>
<p>즉, 업로드한 파일을 사용하려면 애플리케이션을 다시 빌드하고 <span style="color:red">재시작</span>해야 한다는 의미다. </p>
<hr>
<h1 id="🍀해결-방법">🍀해결 방법</h1>
<h3 id="1-경로-변경">1. 경로 변경</h3>
<p>애플리케이션 내부로 설정되어있던 경로를 <code>user.dir</code>로 변경했다.</p>
<pre><code class="language-java">// 파일 저장 디렉토리 경로 설정
private final String uploadDir = System.getProperty(&quot;user.dir&quot;) + &quot;/uploads&quot;;

private String saveImage(MultipartFile image) {
    try {

        // 업로드 폴더가 존재하지 않으면 생성
        Path uploadPath = Paths.get(uploadDir);
        if (!Files.exists(uploadPath)) {
            Files.createDirectories(uploadPath); // 디렉토리 생성
        }

        // 파일 이름 생성
        String fileName = UUID.randomUUID().toString() + &quot;_&quot; + image.getOriginalFilename();
        Path filePath = uploadPath.resolve(fileName);

        // 파일을 지정된 경로에 저장 (임시 파일을 사용하지 않음)
        Files.write(filePath, image.getBytes()); // 파일을 직접 디스크에 기록

        // 파일의 상대 URL 반환 (예: /uploads/파일이름)
        String fileUrl = &quot;/uploads/&quot; + fileName;
        return fileUrl;
    } catch (IOException e) {
        throw new RuntimeException(&quot;이미지 저장 실패&quot;, e);
    }
}</code></pre>
<p>이렇게 외부에 경로를 구성하면, 
<strong>새로 빌드를 할 필요 없이,</strong> 애플리케이션 실행 중 첨부된 파일을 사용할 수 있다.</p>
<p>단, 외부 경로 파일에 접근하기 위한 <strong>핸들러</strong>가 필요하다.</p>
<h3 id="2-외부-경로-파일-접근">2. 외부 경로 파일 접근</h3>
<pre><code class="language-java">@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    // /uploads/** 경로를 루트 경로의 /uploads 폴더와 매핑
    registry.addResourceHandler(&quot;/uploads/**&quot;)
            .addResourceLocations(&quot;file:&quot; + System.getProperty(&quot;user.dir&quot;) + &quot;/uploads/&quot;);
}</code></pre>
<p>외부 경로에 있는 파일을 Spring Boot에서 정적 리소스로 사용하기 위해,
WebMvcConfigurer의 <span style="color:red"><strong>addResourceHandlers</strong></span> 메서드를 사용하여 경로를 매핑했다.</p>
<h3 id="3-결과">3. 결과</h3>
<img src="https://velog.velcdn.com/images/woori_dec/post/12145c0b-c37b-4a6d-a1d4-523e60e6c7df/image.png" width="500px">

<p>서버를 재시작하는 과정 없이, 첨부된 이미지가 출력된다.</p>
<hr>
<p>참고</p>
<ul>
<li><a href="https://wildeveloperetrain.tistory.com/41">https://wildeveloperetrain.tistory.com/41</a> (addResourceHandlers)</li>
</ul>
<hr>
<h1 id="🤯반성의-시간">🤯반성의 시간</h1>
<p>애초에 왜 내부 경로로 경로를 설정했는지도 의문이다.
정말 생각 없이 코딩한 것 같아서 부끄러울 따름이다... </p>
]]></description>
        </item>
    </channel>
</rss>