<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>neptun-29.log</title>
        <link>https://velog.io/</link>
        <description>Wonderland</description>
        <lastBuildDate>Mon, 03 Mar 2025 06:46:45 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>neptun-29.log</title>
            <url>https://velog.velcdn.com/images/neptun-29/profile/802ce2f9-2447-4f49-b7c4-896d5e545212/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. neptun-29.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/neptun-29" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[TIL: mysql day7]]></title>
            <link>https://velog.io/@neptun-29/TIL-mysql-day7</link>
            <guid>https://velog.io/@neptun-29/TIL-mysql-day7</guid>
            <pubDate>Mon, 03 Mar 2025 06:46:45 GMT</pubDate>
            <description><![CDATA[<h3 id="인덱스">인덱스</h3>
<ol>
<li>클러스터형 인덱스</li>
</ol>
<ul>
<li>클러스터형 인덱스를 기본키로 지정하면 자동으로 생성된다.</li>
<li>새로운 열이 입력되어도 자동으로 정렬된다.(기본키에 따라)</li>
<li>데이터가 자동으로 인덱스에 따라 정렬된다.</li>
<li>테이블 당 1개만 생성가능<ul>
<li>열을 묶어서 가능 (복합키 composite key)</li>
</ul>
</li>
</ul>
<ol start="2">
<li>비클러스터형 인덱스</li>
</ol>
<ul>
<li>인덱스 페이지에 데이터 주소를 정렬해 저장
<code>CREATE INDEX ix_tablename ON tablename (colname);</code></li>
<li>조회 시 데이터가 정렬되어있지 않음</li>
</ul>
<h3 id="뷰">뷰</h3>
<p>사용이유</p>
<ol>
<li>복잡한 쿼리를 단축</li>
<li>보안성: 테이블, 열 이름을 숨기거나 권한에 따라 필요한 열만 구성가능</li>
</ol>
<ul>
<li>그러나, 인덱스를 가질 수 없고, 변경 불가<pre><code class="language-sql">CREATE VIEW viewName
AS 
  SELECT 필요한 열 나열.. FROM table;
SELECT * FROm viewName;
</code></pre>
</li>
</ul>
<pre><code>- view 확인: `DESCRIBE viewName`, `SHOW CREATE VIEW viewName`
- 뷰보다 원본테이블을 변경하거나 데이터 추가하는 것이 나음

### 스토어드 함수
```sql
SET GLOBAL log_bin_trust_function_creators = 1;

DELIMITER $$
CREATE FUNCTION functionName(인수)
    RETURNS 반환 데이터 유형
BEGIN
    RETURN ;
END $$
DELIMITER ;</code></pre><h3 id="커서">커서</h3>
<p>테이블의 데이터를 1행씩 처리, 스토어드 프로시저 내부에서 사용</p>
<pre><code class="language-sql">DELIMITER $$
CREATE PROCEDUER procedureName()
BEGIN
    DECLARE endOfRow BOOLEAN DEFAULT FALSE;
    DECLARE idCursor() CURSOR FOR #커서 선언

    DECLARE CONTINUE HANDLER
    FOR NOT FOUND SET endOfRow;

    OPEN idCursor;
    ...
    FETCH idCursor INTO user_payment_id;
    IF endOfRow THEN
        LEAVE ..
    END IF;
    ...

    CLOSE idCursor;
END $$
DELIMITER ;</code></pre>
<h3 id="트리거">트리거</h3>
<ul>
<li>이전 값의 상태를 별도의 테이블에 보관하고 싶을 때 사용될 수 있음</li>
<li>MYSQL에서는 DDL X, DML O</li>
<li>트리거 실행 시기를 정할 수 있음 -&gt; BEFORE &amp; AFTER</li>
<li>매개변수 사용 불가</li>
<li>뷰에 트리거 사용 불가</li>
<li>행, 문장 트리거가 있음<pre><code class="language-sql">delimiter $$
CREATE TRIGGER triggerName
AFTER[BEFORE] UPDATE
ON tableName1
FOR EACH ROW
</code></pre>
</li>
</ul>
<p>BEGIN
    트리거 내용
END $$
DELIMITER ;</p>
<p>```</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL: MYSQL day2]]></title>
            <link>https://velog.io/@neptun-29/TIL-MYSQL-day2</link>
            <guid>https://velog.io/@neptun-29/TIL-MYSQL-day2</guid>
            <pubDate>Tue, 18 Feb 2025 05:36:10 GMT</pubDate>
            <description><![CDATA[<h3 id="기억해두기">기억해두기</h3>
<h4 id="1-조회">1. 조회</h4>
<ul>
<li><p>전체 열 조회는 불필요한 열의 데이터를 포함하므로 CPU나 디스크에 많은 부담, 네트워크를 통해 전송되기 때문에 네트워크에도 부담</p>
</li>
<li><p>조회 시 정렬이 중요한 데이터는 ORDER BY 문을 사용하는 것이 좋음</p>
</li>
<li><p>논리 연산자</p>
<table>
<thead>
<tr>
<th>연산자</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>ALL</td>
<td>모든 비교 집합이 참</td>
</tr>
<tr>
<td>SOME</td>
<td>비교 집합 중 일부가 참</td>
</tr>
<tr>
<td>ANY</td>
<td>하나라도 참</td>
</tr>
<tr>
<td>EXISTS</td>
<td>하위 쿼리에 행이 포함</td>
</tr>
<tr>
<td>IN</td>
<td>리스트 중 하나라도 포함</td>
</tr>
<tr>
<td>LIKE</td>
<td>패턴과 일치하는 경우</td>
</tr>
</tbody></table>
</li>
<li><p><code>LIMIT</code></p>
<ul>
<li><code>SELECT * FROM 테이블명 ORDER BY 컬럼명 LIMIT n,m</code>: n번째 행부터 m만큼 조회</li>
<li><code>LIMIT n OFFSET m</code> : m개 건너뛰고 n 개수만큼 </li>
</ul>
</li>
<li><p>와일드 카드</p>
<ul>
<li><code>LIKE &#39;%A%</code><ul>
<li><code>LIKE &#39;%#%%&#39; ESCAPE &#39;#&#39;</code><ul>
<li><code>LIKE &#39;_A_&#39;</code>, <code>LIKE &#39;A___</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><p>정규 표현식 <strong>REGEXP</strong></p>
<table>
<thead>
<tr>
<th>표현식</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>.</td>
<td>임의의 한 문자</td>
</tr>
<tr>
<td>*</td>
<td>문자 패턴 0번 이상 반복</td>
</tr>
<tr>
<td>+</td>
<td>문자 패턴 1번 이상 반복</td>
</tr>
<tr>
<td>^</td>
<td>문자열의 처음</td>
</tr>
<tr>
<td>$</td>
<td>문자열의 끝</td>
</tr>
<tr>
<td>[...]</td>
<td>대괄호 안에 있는 문자</td>
</tr>
<tr>
<td>[^...]</td>
<td>대괄호 안에 있지 않은 문자</td>
</tr>
<tr>
<td>{n}</td>
<td>반복 횟수</td>
</tr>
<tr>
<td>{m,n}</td>
<td>반복 횟수 최소-최대</td>
</tr>
</tbody></table>
<p>ex1. K뒤에 R~T사이의 글자가 오는
<code>REGEXP &#39;A[R-T]&#39;</code></p>
</li>
<li><p>GROUP BY</p>
<ul>
<li>SELECT문에 사용된 열이 GROUP BY에 명시되어야함</li>
<li>HAVING: 필터링(where)</li>
</ul>
</li>
<li><p>DISTINCT</p>
<ul>
<li>같은 열에 있는 중복 데이터 제거</li>
<li>GROUP BY처럼 중복을 제거해주나 집계연산은 안됨. Ex. count연산</li>
</ul>
</li>
</ul>
<h4 id="2-테이블-생성-및-조작">2. 테이블 생성 및 조작</h4>
<ul>
<li>auto increment는 primary key 필수</li>
<li><code>SELECT LAST_INSERT_ID();</code></li>
<li><code>SET @AUTO_INCREMENT_INCREMENT = 5;</code> : 두번째 insert부터 5씩 증가</li>
<li><code>CREATE TABLE talbe_name AS (SELECT * FROM table2_name);</code></li>
</ul>
<h4 id="3-데이터-유형">3. 데이터 유형</h4>
<ul>
<li>부동소수점 float, double</li>
<li>고정소수점 decimal, numeric</li>
<li>mysql은 하나의 행에서 열 전체 크기가 64KB를 초과할 수 없음 </li>
<li>콜레이션 주의 (ex. <code>col_latin1_general_ci</code> ...)</li>
<li>DATETIMEdms,TIMESTAMP (문자형, 숫자형) (8byte, 4byte) (직접, 자동)</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MGP] 프로젝트 4. 필터링 기능: 몽고디비의 쿼리문으로 코드(성능)개선]]></title>
            <link>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4.-%ED%95%84%ED%84%B0%EB%A7%81-%EA%B8%B0%EB%8A%A5-%EB%AA%BD%EA%B3%A0%EB%94%94%EB%B9%84%EC%9D%98-%EC%BF%BC%EB%A6%AC%EB%AC%B8%EC%9C%BC%EB%A1%9C-%EC%BD%94%EB%93%9C%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0</link>
            <guid>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-4.-%ED%95%84%ED%84%B0%EB%A7%81-%EA%B8%B0%EB%8A%A5-%EB%AA%BD%EA%B3%A0%EB%94%94%EB%B9%84%EC%9D%98-%EC%BF%BC%EB%A6%AC%EB%AC%B8%EC%9C%BC%EB%A1%9C-%EC%BD%94%EB%93%9C%EC%84%B1%EB%8A%A5%EA%B0%9C%EC%84%A0</guid>
            <pubDate>Mon, 02 Sep 2024 06:28:24 GMT</pubDate>
            <description><![CDATA[<h2 id="개요">개요</h2>
<p>성능개선보다는 코드 개선이 맞겠다.</p>
<p>endpoint</p>
<pre><code class="language-json">주소/stores?location={location}&amp;search={search}&amp;minPrice={minPrice}&amp;maxPrice={maxPrice}&amp;category={category}</code></pre>
<p>이렇게 쿼리 파라미터를 이용해서 프론트에서 보내준 정보를 토대로 조건에 맞는 가게를 다시 response body에 담아 보내주는 기능 구현을 진행했다.</p>
<blockquote>
</blockquote>
<p>이 부분에서 생겼던 어려움은 바로 minPrice&amp;maxPrice이다. 메뉴가 다음과 같이 배열 안의 객체들로 구성되어있었는데 JSON에 대해 가볍게 생각한 나는 몽고디비에 string으로 저장해버렸고 이로인해 하드코딩을 해버렸다.</p>
<h3 id="기본-쿼리문">기본 쿼리문</h3>
<pre><code class="language-ts">async getStoresBy(queryParams: GetStoresQueryDTO): Promise&lt;StoresReturnDTO[]&gt; {
    const {
      category,
      location,
      search,
      minPrice,
      maxPrice,
    } = queryParams;

    const query: any = {};
    let stores = [];

    if (category !== &quot;null&quot;) {
      query.category = category;
    } 
    if (location !== &quot;null&quot;) {
      const state = location.split(&#39; &#39;)[0]
      const city = location.split(&#39; &#39;)[1]
      query.state = state;
      query.city = city;
    }
    if (search !== &quot;null&quot;) {
      query.name = new RegExp(search, &#39;i&#39;);
    }

      const results = await this.storesRepository.find(query);
      stores =  plainToInstance(StoresReturnDTO, results);
</code></pre>
<p>mysql의 sql문처럼 몽고디비도 query를 작성하면 알아서 조회를 시켜줬다.
<a href="https://www.mongodb.com/ko-kr/docs/manual/tutorial/project-fields-from-query-results/">https://www.mongodb.com/ko-kr/docs/manual/tutorial/project-fields-from-query-results/</a>
이 문서를 참고하면 좋다.</p>
<h3 id="금액-범위-필터링---하드코딩">금액 범위 필터링 - 하드코딩</h3>
<p>처음에 메뉴 배열을 string으로 저장했기 때문에 string을 parse해서 forEach문과 filter로 하드코딩을 했다. 데이터가 8000개이기 때문에 서버에 배포하면 10초나 걸리는 문제가 발생했다.
&lt;데이터 형식&gt; 은 이러하다</p>
<img src='https://velog.velcdn.com/images/neptun-29/post/b61e8f46-0550-4899-b858-c3126c0c8694/image.png' width='50%'>

<h4 id="코드">코드</h4>
<pre><code class="language-ts">    if (minPrice !== &quot;null&quot; &amp;&amp; maxPrice !== &quot;null&quot;) {
      const prevStores = stores;
      const new_stores = []
      prevStores.forEach((store)=&gt; {
        if(store.menu.length !== 0) {
          const new_menu = store.menu.filter((menu)=&gt; {
            return (menu[&#39;price&#39;] &lt;= maxPrice &amp;&amp; menu[&#39;price&#39;] &gt;= minPrice)
          })
          if(new_menu.length !== 0){
            new_stores.push({name: store.name, state:store.state, city:store.city,
              category: store.category, address: store.address, tel:store.tel,
              menu: new_menu 
                })
            }
          }
        }
      )
      stores= new_stores
    }
</code></pre>
<p>이런 비효율적인 코드를 작성하다니..!!!</p>
<h4 id="조회결과">조회결과</h4>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/59242053-4283-4660-ab30-77a2f3df9c3c/image.png" alt=""></p>
<p><code>Time</code>을 보면 10s인 것을 볼 수 있다.
1초도 아니고 10초는 넘길 수가 없어서 이건 반드시 고쳐야겠다는 생각을 했고</p>
<h3 id="개선된-코드">개선된 코드</h3>
<p>코드를 개선하기 이전에 string으로 저장해놓았던 데이터를 변경해야했다.
파이썬으로 메뉴 데이터 형식을 바꿀 때 json으로 저장하는 것으로 다시 가공했고 결국 몽고디비에 들어갈 때 <code>EJSON</code>형식으로 들어가 배열과 객체로 인식될 수 있게 하였다.</p>
<ol>
<li><p>파이썬 코드 변경</p>
<pre><code class="language-py">df.at[i, &#39;menu&#39;] = json.dumps(menu_list, ensure_ascii=False)</code></pre>
<p>그냥 저장한게 아닌 json으로 저장될 수 있게 하였다.</p>
</li>
<li><p>쿼리문 작성 가능</p>
<pre><code class="language-ts">if (minPrice !== &quot;null&quot; &amp;&amp; maxPrice !== &quot;null&quot;){
   query.menu = { 
     $elemMatch: { 
         price: { 
             $lte: maxPrice_new, 
             $gte: minPrice_new 
         }}}}</code></pre>
<p>배열로 인식 되기 때문에!! 쿼리문을 적용할 수 있었다.</p>
</li>
</ol>
<h2 id="개선된-결과">개선된 결과</h2>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/958ffeee-e8aa-4bc5-a875-87e8144f1869/image.png" alt="">
 18ms 실화??</p>
<p> <span style ="color:orange">10s =&gt; 18ms는 정말 커다란 개선이라고 생각된다.</span></p>
<h3 id="몽고디비의-쿼리-왜-빠를까">몽고디비의 쿼리, 왜 빠를까?</h3>
<p>몽고디비뿐만 아니라 데이터베이스 시스템에서 조회를 하면 인덱스를 사용한다고 한다. B-트리로 인덱스를 관리해서 검색 성능을 높일 수 있다고 한다!</p>
<h3 id="회고">회고</h3>
<p>json을 그냥 데이터 파일 그 자체로 생각했기 때문에 이런 참사가 일어났던 것 같다. 개념공부와 더불어 앞으로는 성급히 빨리 진행시키는 것보다는 이렇게 진행했을 때 무슨일이 일어날지 생각해보고, 더 나은 방법이 없는지 고민해보고 해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MGP] 프로젝트 3. 지오코딩]]></title>
            <link>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-%EC%A7%80%EC%98%A4%EC%BD%94%EB%94%A9</link>
            <guid>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-3.-%EC%A7%80%EC%98%A4%EC%BD%94%EB%94%A9</guid>
            <pubDate>Sat, 31 Aug 2024 06:20:45 GMT</pubDate>
            <description><![CDATA[<p>마지막 데이터 처리는 위도, 경도를 추가하는 일이었다.</p>
<p>공공데이터가 제공하는 위치 정보는 “주소”만 존재했다. 그러나 지도에 가게 정보들을 표시해주려면 위도, 경도가 필요했다.</p>
<p>따라서 </p>
<ol>
<li><p>카카오</p>
</li>
<li><p>네이버</p>
</li>
<li><p>구글</p>
</li>
</ol>
<p>maps API들에 대해 찾아봤다.</p>
<h3 id="카카오">카카오</h3>
<p>카카오는 하루 제공량은 넉넉했지만 1분동안 30건만 호출할 수 있었다. 8000개를 변경해야하는데 1분마다 제한을 해서 코드를 짜기에는 더 좋은 API가 있을 것 같았다.</p>
<h3 id="구글클라우드">구글클라우드</h3>
<p>두번째는 구글 클라우드에서 제공하는 것을 이용해봤다.</p>
<p>다음 코드와 같이 변경이 잘 되었다. 해당코드는 한가지 주소만 변경되는 것이지 전체 코드를 변경하는 것은 아니다</p>
<pre><code class="language-py">import googlemaps

# API키 입력
mykey = 

def change(address):
    try:
        geo_location = maps.geocode(address)[0].get(&#39;geometry&#39;)
        lat = geo_location[&#39;location&#39;][&#39;lat&#39;]
        lng =  geo_location[&#39;location&#39;][&#39;lng&#39;]
        return [lat,lng]
    except:
        return [0,0]


print(change(&#39;서울특별시 종로구 삼일대로 428 (낙원동, 4층)&#39;))</code></pre>
<p>그러나 공공데이터에 적혀있는 주소를 인지못하거나 위도경도가 조금 떨어진 곳으로 잡히는 등의 오류가 아주 많았다.</p>
<h2 id="최종">최종</h2>
<h3 id="3-네이버클라우드">3. 네이버클라우드</h3>
<p>서비스는 한국 가게들만 제공하기 때문에 마지막으로 네이버 클라우드에서 제공하는 것을 살펴보았고 결론적으로 말하자면 변경이 아주 잘되고 하루 제공량도 넉넉했다.</p>
<p>아래 코드는 종로구 삼일대로에 대한 위도경도를 반환하는 코드이다.</p>
<pre><code class="language-py">import requests
import pandas as pd

client_id = 
client_secret = 

# 주소 텍스트
query = &quot;서울특별시 종로구 삼일대로 428 (낙원동, 4층)&quot;

endpoint = &quot;https://naveropenapi.apigw.ntruss.com/map-geocode/v2/geocode&quot;
url = f&quot;{endpoint}?query={query}&quot;

# 헤더
headers = {
    &quot;X-NCP-APIGW-API-KEY-ID&quot;: client_id,
    &quot;X-NCP-APIGW-API-KEY&quot;: client_secret,
}

url = f&quot;{endpoint}?query={query}&quot;
res = requests.get(url, headers=headers)
if res.status_code == 200 and res.json().get(&quot;addresses&quot;):
    print(res.json()[&quot;addresses&quot;][0][&#39;y&#39;])
    print(res.json()[&quot;addresses&quot;][0][&#39;x&#39;])</code></pre>
<p>전체 데이터는 df로 데이터 프레임으로 바꾼 뒤 tqdm으로 진행상황을 보면서 데이터 가공을 완료했다.</p>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/5357c8f7-8cfd-4543-901d-c9bc69606fb8/image.png" alt="">
<img src="https://velog.velcdn.com/images/neptun-29/post/16078d4a-0239-4db8-b7f2-664e0cd75f11/image.png" alt=""></p>
<p>이렇게 약 8000개의 가게를 변환시켰다. 각종 테스트로 인해 총 9000번정도를 API를 사용했다.</p>
<p>공공데이터 자체에 문제가 있는 주소</p>
<p>“서울특별시 종로구 종로구 ~~” 이런식으로 종로구가 2번 입력된 것같은 주소들 빼고는 변경이 잘 되어서 약 95%는 위도경도로 변경이 되었다.</p>
<p>이렇게 3개의 작업을 마무리 한 뒤 몽고DB에 csv파일을 import해주었다. (csv파일을 utf8로 저장하고 import하는 것 빼먹으면 안된다.)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MGP] 프로젝트 2. 데이터처리 과정]]></title>
            <link>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-2.-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%B2%98%EB%A6%AC-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 31 Aug 2024 06:08:57 GMT</pubDate>
            <description><![CDATA[<p>이제 서비스에서 필요한 데이터를 위해 파이썬을 위해서 데이터를 약간 변형시켜주었다.이전에 머신러닝에서 데이터 전처리했던 경험을 활용해 파이썬 판다스를 이용했다.(아주 간단한 파이썬 코드...)
백엔드 api를 만들면서 데이터를 가공하는 방법도 있겠지만 아직 초보자인 나는 정제된 데이터가 필요하다고 생각되어 이렇게 진행했다.</p>
<p>크게 두가지 필드를 변경시켰고 한가지(정확히 하자면 2개) 필드를 추가시켰다.</p>
<h2 id="1-category-수-줄이기">1. category 수 줄이기</h2>
<p>처음 제공되는 카테고리는 다음과 같았다</p>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/d8aca759-4a52-4343-b697-73da02aa6449/image.png" alt=""></p>
<p>그런데 카테고리를 이대로 사용하면 화면에 카테고리가 넘칠 것 같았다. 또한 음식 위주로 보여주고 싶었고 사용자도 한눈에 카테고리를 알아볼 수 있도록 다음과 같이 구성했다</p>
<p>중식
양식
일식
한식
기타</p>
<pre><code class="language-py">import pandas as pd
import numpy as np
import json

file_path = &#39;./goodprice08.csv&#39;
df = pd.read_csv(file_path)

# 카테고리 정제
for i,cate in enumerate(df[&#39;업종&#39;]):
    category = cate[:2]
    if category == &quot;한식&quot;:
        df.loc[i,&#39;업종&#39;] = &quot;한식&quot; 
    elif category not in [&quot;일식&quot;, &quot;중식&quot;, &quot;양식&quot;]:
        df.loc[i,&#39;업종&#39;] = &#39;기타&#39;

output_file_path = &#39;./mygoodprice2.csv&#39;
df.to_csv(output_file_path, index=False)</code></pre>
<p>한식_찌개류,.. 이런식으로 되어있기 때문에 그냥 한식으로 시작하면 한식으로 통일 시켜주고</p>
<p>한식, 일식, 중식, 한식, 양식으로 시작하지않으면 모두 기타로 바꿔버렸다.</p>
<h2 id="2-menu-배열로-바꾸기">2. menu 배열로 바꾸기</h2>
<p>프론트 단에서 이런식의 메뉴를 원했다</p>
<pre><code class="language-jsx">
        &quot;name&quot;: &quot;먹고갈래지고갈래&quot;,
        &quot;state&quot;: &quot;서울특별시&quot;,
        &quot;city&quot;: &quot;종로구&quot;,
        &quot;category&quot;: &quot;한식&quot;,
        &quot;address&quot;: &quot;서울특별시 종로구 수표로 117 (낙원동)&quot;,
        &quot;tel&quot;: &quot;02-367-1239&quot;,
        **&quot;menu&quot;: [
            {
                &quot;id&quot;: 1,
                &quot;menu&quot;: &quot;삼계탕&quot;,
                &quot;price&quot;: 13000
            },
            {
                &quot;id&quot;: 2,
                &quot;menu&quot;: &quot;산채비빔밥&quot;,
                &quot;price&quot;: 7000
            }
                ],**</code></pre>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/0f142e6e-c354-43c3-a140-dfa9cc88d3a8/image.png" alt="">
이런 메뉴에서
<img src="https://velog.velcdn.com/images/neptun-29/post/6a0b6598-0764-4049-bed1-155754c2c56e/image.png" alt="">
위 처럼 변경시킨 것이다.</p>
<p>다음과 같이 코드를 작성했다.</p>
<pre><code class="language-py">for i in range(len(df)):
    menu_list = []
    if not pd.isna(df.loc[i, &#39;메뉴1&#39;]):
        menu_list.append({
            &quot;id&quot;: 1,
            &quot;menu&quot;: df.loc[i, &#39;메뉴1&#39;],
            &quot;price&quot;: int(float(df.loc[i, &#39;가격1&#39;])) if not np.isnan(df.loc[i, &#39;가격1&#39;]) else &quot;&quot;
        })
    if not pd.isna(df.loc[i, &#39;메뉴2&#39;]):
        menu_list.append({
            &quot;id&quot;: 2,
            &quot;menu&quot;: df.loc[i, &#39;메뉴2&#39;],
            &quot;price&quot;: int(float(df.loc[i, &#39;가격2&#39;])) if not np.isnan(df.loc[i, &#39;가격2&#39;]) else &quot;&quot;
        })
    if not pd.isna(df.loc[i, &#39;메뉴3&#39;]):
        menu_list.append({
            &quot;id&quot;: 3,
            &quot;menu&quot;: df.loc[i, &#39;메뉴3&#39;],
            &quot;price&quot;: int(float(df.loc[i, &#39;가격3&#39;])) if not np.isnan(df.loc[i, &#39;가격3&#39;]) else &quot;&quot;
        })

    # SON 문자열로 변환
    df.at[i, &#39;menu&#39;] = json.dumps(menu_list, ensure_ascii=False)

df = df.drop(columns=[&#39;메뉴1&#39;,&#39;메뉴2&#39;,&#39;메뉴3&#39;,&#39;가격1&#39;,&#39;가격2&#39;,&#39;가격3&#39;])

output_file_path = &#39;./mygoodprice2.csv&#39;
df.to_csv(output_file_path, index=False)
</code></pre>
<p>하지만 여기서 문제가 많이 생겼다..
몽고디비와.. 정확히 말하자면 EJSON을 잘 몰랐기 때문일 것이다.</p>
<p>데이터 csv 파일을 처음에 몽고디비에 import 했을 때 EJSON을 잘 몰라서 배열은 안되는구나 string으로 해야하는 구나 생각했다. 
(이래서 기초가 중요하다-EJSON 대한 글을 따로 작성하겠다)</p>
<p><code>df.at[i, &#39;menu&#39;] = json.dumps(menu_list, ensure_ascii=False)</code></p>
<p>를 추가해주지 않으면 string형식이 되고</p>
<p>nest js에서 string을 다시 파싱해서 배열로 변환해야하며</p>
<p>배열 타입 자체로 변환하는 법을 몰랐기 때문에 쿼리문을 써야하는 필터링 작업에서 꽤나 애를 먹었다.(메뉴필터링 - 트러블슈팅에서 확인가능하다)</p>
<p>아무튼 이렇게해서 변환해야하는 두가지 필드는 이렇게 준비가 완료되었다.</p>
<p>다음 게시글은 위도, 경도를 추가한 것에 관한 글이다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MGP] 프로젝트 1. DB에 대한 고민]]></title>
            <link>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1.-DB%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC</link>
            <guid>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-1.-DB%EC%97%90-%EB%8C%80%ED%95%9C-%EA%B3%A0%EB%AF%BC</guid>
            <pubDate>Sat, 31 Aug 2024 05:55:36 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/neptun-29/post/95e6a340-39cb-49fa-9345-a0b83554eaa6/image.png" alt=""></p>
<p>정부에서 제공하는 착한가게 업소 공공데이터를 활용하기로 했다.
<a href="https://www.data.go.kr/data/3045247/fileData.do">https://www.data.go.kr/data/3045247/fileData.do</a></p>
<h2 id="첫번째-고민">첫번째 고민</h2>
<h3 id="오픈-api를-활용해-프론트앤드에서-매번-오픈-api를-불러올-것인가">오픈 API를 활용해 프론트앤드에서 매번 오픈 API를 불러올 것인가?</h3>
<p>총 가게의 개수는 약 8000개 정도로</p>
<p>1) 첫 화면에서 페이지네이션을 구현한다고 해도 매번 사용자가 접속할 때마다 오픈 API를 호출하는 것과</p>
<p>2) 차라리 우리 DB에 한번에 데이터를 저장후 필요한 API를 보내주는 것</p>
<p>두개 방법이 존재했고 어떤 것이 더 적합한지 고민했다. 결국 두번째 방법을 선택하게 되었다.</p>
<h4 id="2번을-선택한-이유는">2번을 선택한 이유는</h4>
<ul>
<li>제공되는 데이터가 빈번하게 변한다면 1번 방법이 맞다고 생각하지만  한달에 한번 변경되며</li>
<li>데이터가 많이 변경되지않고 서비스에서 사용하기 위한 데이터 전처리과정도 어렵지 않다고 생각해</li>
<li>또한, 위도-경도 데이터를 추가해야하며 여러 필드 데이터를 변경시켜야했으므로 
  : 물론 오픈 API를 불러와 데이터를 가공할 수 도 있지만 더 번거로울 것 같았다.</li>
</ul>
<p>우리 DB에 데이터를 모두 저장하기로 했다.</p>
<h2 id="두번째-고민">두번째 고민</h2>
<h3 id="어떤-데이터베이스를-사용할-것인가">어떤 데이터베이스를 사용할 것인가?</h3>
<ol>
<li>많은 레퍼런스가 존재하고 이전에 사용해봤던 MYSQL</li>
<li>NOSQL사용해보기 + 불규칙한 데이터 이용에 적합한 몽고 DB</li>
</ol>
<p>먼저 이용한 공공데이터를 보여주자면</p>
<p>필드가 이런식으로 구성되어있다</p>
<pre><code>시도-구-업종-업소명-주소-연락처-메뉴1-가격1-메뉴2-가격2-메뉴3-가격3</code></pre><p>그런데 이 필드가 모두 채워져있는 것이 아니고 가게마다 메뉴가 아예 없거나 1~3개 사이에서 다양하게 존재했다.</p>
<p>처음에는 메뉴를 배열로 묶어서 생각하지 않았고 이러한 데이터는 불규칙하다고 생각했기 때문에 <strong>몽고 디비를 선택했다.</strong> 경험이 부족해서 데이터가 불규칙한게 어디까지 불규칙한지, 관계형 디비와 NoSQL 디비에 불규칙이 어디까지 적용되는지 몰랐던 것 같다. <em>사실 이정도 데이터면 없는 데이터는 Null값으로 채우며 그냥 관계형을 써도 괜찮았을 것 같다.</em></p>
<p>그러나 이러한 선택으로 인해..
-&gt; 한달이라는 시간동안 개발을 진행했는데 nest js도 처음인데 디비까지 처음인 것을 쓰다보니 공부+구현까지 시간이 오래 걸렸다. 다음에는 이러한 것도 고려해야겠다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MGP] 프로젝트 0. 개요]]></title>
            <link>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-0.-%EA%B0%9C%EC%9A%94</link>
            <guid>https://velog.io/@neptun-29/MGP-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-0.-%EA%B0%9C%EC%9A%94</guid>
            <pubDate>Sat, 31 Aug 2024 05:42:59 GMT</pubDate>
            <description><![CDATA[<h2 id="서비스-기획-배경">서비스 기획 배경</h2>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/b71c6ae5-0beb-429c-859e-ad8fd386884b/image.png" alt="">
<a href="https://www.goodprice.go.kr/index.do">https://www.goodprice.go.kr/index.do</a></p>
<hr>
<p>배경: 물가상승으로 인해 정부가 착한가격업소를 선정하고 운영하고 있습니다.하지만 현재 행정안전부가 운영하는 공식사이트는 소비자 입장에서 적극적으로 이용하기 불편해보입니다.</p>
<p>따라서 저희는 소비자, 사용자 친화적인 착한가격업소 서비스를 개발하기로 했습니다. 개발할 기능을 고려했을 때 소비자 입장에서 스마트폰으로 사용할 수 있도록 화면은 무신사처럼 웹 앱 모바일뷰를 활용했습니다.</p>
<h3 id="계획한-기능">계획한 기능</h3>
<ol>
<li>착한가격업소 소개: 여러 필터링 기능을 가지고 검색할 수 있도록<pre><code>* 검색어
* 카테고리
* 가격 상하한선
* 지역</code></pre><ol start="2">
<li>제공된 메뉴와 함께</li>
</ol>
</li>
<li>위치 서비스를 제공<ul>
<li>현재 위치 기반과 함께 주위에 있는 가게 확인</li>
</ul>
</li>
<li>로그인 기능</li>
<li>마이가게관리 기능<ul>
<li>찜한 가게를 확인</li>
<li>내가 이용하고 찜한 가게들을 분석한 통계 제공</li>
</ul>
</li>
</ol>
<h3 id="나의-역할">나의 역할</h3>
<p>백엔드를 맡았고 팀원과 협의 후 Nest.js와 mongoDB를 사용하기로 했다.</p>
<ul>
<li>필터링 기능 개발</li>
<li>데이터 처리</li>
<li>위치 기능 개발</li>
</ul>
<h3 id="앞으로의-목차">앞으로의 목차</h3>
<p>DB선택이유 -&gt; 데이터 처리 → 필터링 기능 →  위치 기능 →  회고</p>
<p>개발 구현보다는 왜 이렇게 했고 겪었던 어려움들에 대해 작성해보려한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[에러해결] AxiosError: Network Error]]></title>
            <link>https://velog.io/@neptun-29/%EC%97%90%EB%9F%AC%ED%95%B4%EA%B2%B0-AxiosError-Network-Error</link>
            <guid>https://velog.io/@neptun-29/%EC%97%90%EB%9F%AC%ED%95%B4%EA%B2%B0-AxiosError-Network-Error</guid>
            <pubDate>Fri, 19 Jul 2024 07:26:33 GMT</pubDate>
            <description><![CDATA[<h2 id="상황">상황</h2>
<p>백엔드에서 작성한 API를 <code>axios</code>로 받아오는 과정에서 cors에러가 발생했다. 이전에 <code>app.use(cors())</code>만 서버에 추가해주면 해결되길래 이번에도 그렇게 했는데 여전히 에러인 것이다.</p>
<hr>
<p>ERROR
Network Error
AxiosError: Network Error
    at XMLHttpRequest.handleError (<a href="http://localhost:3000/static/js/bundle.js:51796:14">http://localhost:3000/static/js/bundle.js:51796:14</a>)
    at Axios.request (<a href="http://localhost:3000/static/js/bundle.js:52234:41">http://localhost:3000/static/js/bundle.js:52234:41</a>)
    at async fetchCategory (<a href="http://localhost:3000/static/js/bundle.js:141:20">http://localhost:3000/static/js/bundle.js:141:20</a>)</p>
<hr>
<h2 id="해결해보기">해결해보기</h2>
<ol>
<li><p>클라이언트</p>
<pre><code class="language-ts">export const createClient = (config?: AxiosRequestConfig) =&gt; {
const axiosInstance = axios.create({
 baseURL : BASE_URL,
 timeout : DEFAULT_TIMEOUT,
 headers: {
   &#39;content-type&#39; : &#39;application/json&#39;,
 },
 withCredentials : true,
 ...config,
});</code></pre>
<p>클라이언트에서는 <code>withCredentials:true</code>로 설정하라했지만 이미 되어있었다.</p>
</li>
<li><p>서버</p>
<pre><code class="language-js">const cors = require(&#39;cors&#39;);
app.use(express.json());
app.use(cors({credentials: true, origin: &#39;http://localhost:3000&#39;}));</code></pre>
<p>더 찾아본 결과 cors안에 코드를 저렇게 넣어주어야 한다.
위의 코드 잘 작동!!</p>
</li>
</ol>
<p>&lt;해결&gt;</p>
<h3 id="참조">참조</h3>
<p><a href="https://sennieworld.tistory.com/49">https://sennieworld.tistory.com/49</a></p>
<h3 id="더-공부해야할-내용">더 공부해야할 내용</h3>
<ul>
<li>cors 내용 알아보기!</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[REACT 공부하기 기록 : 좌석 예약 화면 만들기 (1)]]></title>
            <link>https://velog.io/@neptun-29/REACT-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0-%EA%B8%B0%EB%A1%9D-%EC%A2%8C%EC%84%9D-%EC%98%88%EC%95%BD-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</link>
            <guid>https://velog.io/@neptun-29/REACT-%EA%B3%B5%EB%B6%80%ED%95%98%EA%B8%B0-%EA%B8%B0%EB%A1%9D-%EC%A2%8C%EC%84%9D-%EC%98%88%EC%95%BD-%ED%99%94%EB%A9%B4-%EB%A7%8C%EB%93%A4%EA%B8%B0-1</guid>
            <pubDate>Thu, 04 Jul 2024 16:41:57 GMT</pubDate>
            <description><![CDATA[<p><a href="https://github.com/estiemoon/Spaceship/tree/main/space-proto">https://github.com/estiemoon/Spaceship/tree/main/space-proto</a></p>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/a5f42a37-5d8c-4405-b7fe-67e3629823ab/image.gif" alt=""></p>
<h2 id="공부목적">공부목적</h2>
<p>리액트는 완전 처음인데 강의가 react-redux toolkit으로 강의가 진행되니 혼란스러워서
일단 리액트 타입스크립트를 간단하게 만들어보고 싶었다. 
props, useState, 컴포넌트 기본 개념을 이해하기 위해서!!</p>
<h2 id="기능">기능</h2>
<ol>
<li>인원 수를 설정한다</li>
<li>인원 수에 맞는 좌석을 선택한다</li>
<li>인원 수보다 많이 선택은 불가능</li>
<li>결제하기 버튼을 누른다</li>
<li>인원 수만큼 좌석이 선택되어야 결제가 이루어짐</li>
</ol>
<h2 id="개선사항">개선사항</h2>
<ol>
<li><p>아직 사실 css는 위치만 조정했다. 
우주선 좌석 예약 컨셉이라 디자인은 시간이 걸릴 것 같다(심심할때하려는데 시간이 안나는)</p>
</li>
<li><p>코드적으로는 좌석이 첫번째 두번째 열이 컴포넌트가 같은 것을 두번 출력한 것이라 인원 수 조정할 때 나누기를 한다던가 코드가 좀 더럽게 됐다. </p>
</li>
</ol>
<p>갑자기 11시에 불이 붙어서 새벽 2시에 1차를 끝내고 자게됐는데 (내일 아침 모각코 큰일이네)
조만간 고민해서 이 2개를 다시 고쳐봐야겠다. </p>
]]></description>
        </item>
        <item>
            <title><![CDATA[MYSQL WorkBench에서 엑셀 파일 데이터 삽입하기 오류]]></title>
            <link>https://velog.io/@neptun-29/MYSQL-WorkBench%EC%97%90%EC%84%9C-%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%EC%98%A4%EB%A5%98</link>
            <guid>https://velog.io/@neptun-29/MYSQL-WorkBench%EC%97%90%EC%84%9C-%EC%97%91%EC%85%80-%ED%8C%8C%EC%9D%BC-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%82%BD%EC%9E%85%ED%95%98%EA%B8%B0-%EC%98%A4%EB%A5%98</guid>
            <pubDate>Fri, 28 Jun 2024 02:02:36 GMT</pubDate>
            <description><![CDATA[<h2 id="문제-상황">문제 상황</h2>
<p>한글이 포함된 엑셀 데이터를 mysql workbench의 wizard import를 이용해서 삽입하려 하는데 자꾸 이런 오류가 났다.</p>
<p>Unhandled exception: &#39;ascii&#39; codec can&#39;t decode byte 0xed in position 2: ordinal not in range(128)</p>
<h3 id="첫번째-시도">첫번째 시도</h3>
<p>position뒤의 숫자가 바뀌어가면서 오류가 났는데.. 첫번째 행의 데이터를 바꿀 때 position 뒤 숫자가 바뀌길래 엔터 오류라고 생각했다.
<code>/n</code>이 안 먹히나?</p>
<p>그래서 파이썬을 이용해 <code>/n</code>을 삽입해 보았다.</p>
<pre><code class="language-python">import csv

input_file = &#39;canne_awards_1.csv&#39;
output_file = &#39;canne_awards_2.csv&#39;

with open(input_file, &#39;r&#39;, encoding=&#39;utf-8&#39;) as infile, open(output_file, &#39;w&#39;, newline=&#39;\n&#39;, encoding=&#39;utf-8&#39;) as outfile:
    reader = csv.reader(infile)
    writer = csv.writer(outfile)
    for row in reader:
        writer.writerow(row)</code></pre>
<p>해결 ❌</p>
<h3 id="두번째-시도">두번째 시도</h3>
<p>utf-8로 인코딩도 다 했는데 안되길래
뭔가 맥북이 문제인 것 같아 키워드 &#39;맥북 엑셀 mysql&#39;를 포함해 검색 결과
json파일로 변환해서 import하면 문제가 안생긴다라는 것을 발견했다.</p>
<p><a href="https://www.convertcsv.com/csv-to-json.htm">https://www.convertcsv.com/csv-to-json.htm</a></p>
<p>여기서 변환하면 된다.
그렇게 json으로 변환 후 import 성공 !</p>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/4dceb1c5-12be-4417-b4dd-86b213118911/image.png" alt=""></p>
<p>참조 : <a href="https://www.codeit.kr/community/questions/UXVlc3Rpb246NjJkYTM0YTMwZTFhMzgxOGMxMTNlM2Rh">https://www.codeit.kr/community/questions/UXVlc3Rpb246NjJkYTM0YTMwZTFhMzgxOGMxMTNlM2Rh</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Refresh Token 구현하기]]></title>
            <link>https://velog.io/@neptun-29/Refresh-Token-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@neptun-29/Refresh-Token-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0</guid>
            <pubDate>Wed, 19 Jun 2024 06:03:38 GMT</pubDate>
            <description><![CDATA[<p>로그인 마지막 시리즈이다</p>
<h2 id="0-개요">0. 개요</h2>
<ol>
<li>회원가입과 암호화방식 알아보기</li>
<li>JWT와 로그인</li>
</ol>
<p><strong>3. Refresh Token을 이용한 로그인, Authorization</strong></p>
<h2 id="왜-refresh-token">왜 Refresh Token?</h2>
<p>Access Token으로만 인증을 하다보면 보안이 취약하기 때문에 Refresh Token도 같이 발행해준다는 이야기를 지난 2편에서 했다. 이번에는 어떤 방식으로 흘러가는지 정리하고 코드로 구현해보려한다.</p>
<h2 id="access-token--refresh-token-작동방식">Access Token &amp; Refresh Token 작동방식</h2>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/f42865b2-04f0-4636-958e-5da65f5de750/image.jpeg" alt=""></p>
<h2 id="코드-구현">코드 구현</h2>
<h3 id="access-token만-발행할-때와-달라지는-수정사항">Access Token만 발행할 때와 달라지는 수정사항</h3>
<p>users/userService.js</p>
<ol>
<li><p>login 모듈에서 refresh Token도 함께 발행해주어야한다.</p>
</li>
<li><p>Refresh Token 발행 후 <code>redis</code> 모듈을 이용해 서버에 저장한다.</p>
<pre><code class="language-javascript">if(user &amp;&amp; currentPwd == user.password) { 

     const token = sign(user);
     const refreshToken = refresh();

     const redisClient = redis.createClient(process.env.REDIS_PORT);
     await redisClient.connect();
     redisClient.set(toString(user.id), refreshToken);

     res.cookie(&#39;token&#39;, {token: token, refreshToken: refreshToken}, {httpOnly : true})
     res.status(StatusCodes.OK).json(result)
 }      </code></pre>
</li>
<li><p>refresh.js 파일 생성 후 access 재발급 요청에 대한 모듈 작성추가</p>
</li>
</ol>
<pre><code class="language-jsx">const { StatusCodes } = require(&#39;http-status-codes&#39;);
const {sign, verify, refreshVerify} = require(&#39;./jwt-utils&#39;);
const jwt = require(&#39;jsonwebtoken&#39;);

const excRefresh = async (req,res) =&gt; {
    const accessToken = req.headers.authorization;
    const refreshToken = req.headers.refresh;

    if(accessToken &amp;&amp; refreshToken){

        accessResult = verify(accessToken);
        const user = accessResult
        refreshResult = await refreshVerify(refreshToken, user.id);

        //access 만료시
        if(!accessResult.ok) {
            if(!refreshResult.ok){ // refresh 만료시
                res.status(StatusCodes.UNAUTHORIZED).json({
                    message : &quot;로그인 다시&quot;
                })
            } else { // refresh 유효
                //access token 새로 발급
                const newToken = sign(user);
                res.cookie(&#39;token&#39;, {token: newToken, refreshToken: refreshToken}, {httpOnly : true});
                res.status(StatusCodes.OK).json({message : &quot;새로운 토큰 발급 완료&quot;});
            }
        } else {
            //access not expired
            res.status(StatusCodes.BAD_REQUEST).json({message : &quot;access Token이 만료되지 않았습니다.&quot;})
        }
    } else {
        res.status(StatusCodes.BAD_REQUEST).json({
            message : &quot;No Access Token and Refresh Token&quot;
        })
    }
}

module.exports = excRefresh;</code></pre>
<h3 id="코드의-흐름">코드의 흐름</h3>
<ol>
<li>Refresh Token &amp;&amp; Access Token이 모두 헤더에 있으면<ol>
<li>access token 만료시<ol>
<li>refresh token 유효시 : 토큰 재발급</li>
<li>refresh token 만료시 : 로그인 요청</li>
</ol>
</li>
<li>access token 유효시 : “잘못된 요청 - 만료되지 않았습니다”</li>
</ol>
</li>
<li>토큰이 존재하지 않습니다.</li>
</ol>
<h2 id="수정해야할-사항">수정해야할 사항</h2>
<ol>
<li>access token이 만료 시 위에 login.js, refresh.js를 구현하고
user.js 라우터 파일 안에 밑에 처럼<pre><code class="language-javascript">app.get(&#39;/refresh&#39;, refresh);</code></pre>
새로운 요청하는 방식으로 작성하고 postman에서 요청해 잘 작동하는지 확인했는데 실제로는 어떻게 작동되는지 알아보아야 한다.</li>
</ol>
<hr>
<ol start="2">
<li>두개의 token을 Cookie로 보냈는데 <code>set-cookie</code>에 분리되어있지 않아 확인하기가 어려웠다. <code>cookie-parser</code>을 이용해보려 했는데 아직 에러가 나서 재시도 해보아야겠다.</li>
</ol>
<p>참조 :  <a href="https://velog.io/@kshired/Express%EC%97%90%EC%84%9C-JWT%EB%A1%9C-%EC%9D%B8%EC%A6%9D%EC%8B%9C%EC%8A%A4%ED%85%9C-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0-Access-Token%EA%B3%BC-Refresh-Token">https://velog.io/@kshired/Express에서-JWT로-인증시스템-구현하기-Access-Token과-Refresh-Token</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[객체지향] 객체지향과 클래스에 대해 알아보기]]></title>
            <link>https://velog.io/@neptun-29/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@neptun-29/%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5-%EA%B0%9D%EC%B2%B4%EC%A7%80%ED%96%A5%EA%B3%BC-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Sun, 16 Jun 2024 05:32:44 GMT</pubDate>
            <description><![CDATA[<h2 id="목차">목차</h2>
<ol>
<li>메모리 영역과 변수타입</li>
<li>클래스의 기초개념</li>
<li>객체지향</li>
<li>클래스 개념과 특징</li>
</ol>
<ul>
<li>변수와 메모리에 대한 기본 이해를 위해 1번 목차를</li>
<li>클래스를 위해 2~4번 목차를 구성해봤다.</li>
</ul>
<hr>
<h2 id="1-메모리-영역과-변수-타입">1. 메모리 영역과 변수 타입</h2>
<h3 id="메모리-영역">메모리 영역</h3>
<p>메모리 영역은 총 4가지로 구성된다.</p>
<ol>
<li><strong>코드 영역</strong> : 실행할 명령어들을 순서대로 쌓이고 CPU는 이 명령어들을 하나씩 처리한다.</li>
<li>** 스택 영역** : 매개변수 및 지역변수를 저장한다.</li>
<li><strong>힙 영역</strong> : 이벤트 발생 시 메모리가 Queue에 저장된다. 컴파일이 아닌 실행 시 사용자로부터 할당 메모리를 입력받는다.</li>
<li><strong>데이터 영역</strong> : static과 전역변수가 저장되는 공간이다.</li>
</ol>
<h3 id="변수타입">변수타입</h3>
<p>데이터는 원시타입과 참조타입으로 나눌 수 있는데</p>
<h4 id="원시타입">원시타입</h4>
<ul>
<li>원시타입에는 string, int, float, ... 등이 있고</li>
<li>재정의가 일어나거나 데이터 복사가 일어날 때 메모리 공간을 새로 확보해서 값을 저장한다.<h4 id="참조타입">참조타입</h4>
</li>
<li>참조타입은 배열, 클래스 등이라고 볼 수 있다.</li>
<li>데이터 복사가 일어날 때 주소값을 가져간다.</li>
</ul>
<h2 id="2-클래스의-기초개념">2. 클래스의 기초개념</h2>
<h3 id="구조체">구조체</h3>
<p>서로 다른 종류들의 변수들을 묶어 새로운 데이터 타입을 정의한 것으로 &quot;데이터 타입&quot;임을 기억하자.</p>
<pre><code class="language-c">struct person {
    char name[30];
    int age;
}
//구조체 변수
person p1;
person p2;

p1.name;
p2.age;</code></pre>
<h3 id="공용체">공용체</h3>
<p>사용자 정의 자료형으로, 구조체와 차이점으로 메모리 공간을 공유한다는 특징을 가진다</p>
<pre><code class="language-c">union person{
  char name[30];
  int age;
};
</code></pre>
<h3 id="동적할당">동적할당?</h3>
<ul>
<li>동적할당은 <strong>객체를 생성할 때 동작하는 개념의 기초!</strong> <code>new</code></li>
<li>정적으로 메모리를 할당하면 상황이 바뀔 때마다 컴파일을 해줘야함.</li>
<li>고정하지말고 실행 시 결정하는 동적할당을 이용해보자</li>
</ul>
<p>예시
<code>#2 student = #1 (int*)malloc(sizeof(int)*num)</code> 
(2) 스택메모리에 주소값을 전달  (1) 힙 메모리에 할당
--&gt; 힙 메모리에 할당된 메모리는 반드시 스택 메모리의 포인터 변수를 사용해서만 가능하다.</p>
<h2 id="3-객체지향">3. 객체지향</h2>
<p>객체지향 = 추상화 + 캡슐화</p>
<ul>
<li>공통되는 특성 및 속성을 추출하는 추상화</li>
<li>외부에서 내부를 볼 수 없게 은닉하는 캡슐화 ( 외부로부터 데이터를 조작할 수 있도록 인터페이스가 필요하다)</li>
</ul>
<h2 id="4-클래스">4. 클래스</h2>
<p>클래스 = <em>프로퍼티 _+ 메서드
클래스 = _은닉된 데이터</em> + 출입 인터페이스</p>
<pre><code class="language-cs">class Dog {
    private int height, weight, type;
    public void bark(){Console.WriteLine(&quot;bark!&quot;);}

}
&lt;객체의 생성&gt;
class Helloworld {
    static void Main(){
        (#1) Dog a = (#2) new Dog(); 
        a.bark(); </code></pre>
<p>(1) stack 영역 = (2)Heap 영역 
-&gt; heap 영역에 동적메모리 할당 후 stack영역이 참조 
-&gt; 변수가 3개이므로 3 * 4 = 12 bytes</p>
<p><code>new</code>가 동적할당해줌 <code>malloc</code>의 역할</p>
<h3 id="생성자">생성자</h3>
<ul>
<li>모든 변수는 선언 시 초기화를 시켜주어야 하는데 객체도 변수이기 때문에 초기화가 필요하다.</li>
<li>이 초기화하는 하무삭 생성자, constructor<pre><code class="language-cs">class Dog {
  private int height, weight, type;
  public void bark(){Console.WriteLine(&quot;bark!&quot;);}
  public Dog(){ //클래스이름과 동일하게만 하면 생성자 함수 만족
      height = 0;
      weight = 0;
      type = 1;
      }    
}</code></pre>
</li>
</ul>
<h3 id="클래스의-특징-1-상속성">클래스의 특징 1. 상속성</h3>
<ul>
<li>공통된 속성을 가진 자식들 - 중복을 피하기 위해</li>
<li>부모 - 자식 관계를 만들어서 부모의 속성을 자식이 받아오고 <code>extends</code>한다.<pre><code class="language-cs">class Pudle : Dog {
  public Pudle(){
      base.height = 80; 
      Console.WriteLine(height); &gt;&gt; 80
  }
}
</code></pre>
</li>
</ul>
<p>main() {
    Pudle pd = new Pudle(); 
    }</p>
<pre><code>
### 클래스의 특징 2. 다형성
1. 오버로딩
* &quot;적재하다&quot;
* 이름은 동일하지만 전달인자의 타입이나 개수가 다른 경우
* `overloading` : 전달인자를 받고 알아서 맞는 함수를 호출해줌

```cs

public class Cat{
    public void Overload(int a){
        Console.WriteLine(&quot;{0}&quot;,a);
        }
    public void Overload(int a, int b){
        Console.WriteLine(&quot;{0} + {0}&quot;,a,b);  //전달인자 개수가 다른 경우
    }
    public void Overload(double a){
        Console.WriteLine(&quot;{0&quot;,a); //전달인자 타입이 다른 경우
    }
}</code></pre><ol start="2">
<li>오버라이딩</li>
</ol>
<ul>
<li>기존의 것을 덮어버리다 : 자식 클래스에서 부모 속성을 재정의</li>
<li>함수포인터 개념을 객체포인터로 확장</li>
</ul>
<pre><code class="language-cs">class Dog {
    private int height, weight, type;
    virtual public void bark(){Console.WriteLine(&quot;bark!&quot;);}
    public Dog(){ 
        height = 0;
        weight = 0;
        type = 1;
        }    
}
class Pudle : Dog{
    public Pudle(){
        base.height = 80; 
        Console.WriteLine(height); &gt;&gt; 80
    }
    public override void bark(){Console.WriteLine(&quot;bark 재정의!!&quot;);} //override
}

class HelloWorld {
    static void Main() {
        Dog dog = new Dog();
        Pudle pudle = new Pudle();
        pudle.bark(); &gt;&gt; &quot;bark 재정의!!&quot;

        &lt;함수포인터를 객체 포인터로 확장.&gt; : 함수를 갈아끼운다..!
        dog = new Pudle(); //부모에 자식을..
        dog.bark(); &gt;&gt; &quot;bark 재정의!!&quot; 
    }
    }
</code></pre>
<h3 id="함수포인터">함수포인터?</h3>
<p><strong>[함수포인터 예]</strong> </p>
<pre><code class="language-c">int Add(int a, int b) {
    return a + b;
}
int Minus(int a, int b) {
    return a - b;
}

int main() {
    int a = 100;
    int b = 50;

    int (*fd)(int pa, int pb);
    fd = Add;
    int result = fd(a,b); &gt;&gt; 150
    fd = Minus;
    result = fd(a,b); &gt;&gt; 50

    return 0;
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[forEach문을 쓰면 비동기처리가 안된다.]]></title>
            <link>https://velog.io/@neptun-29/forEach%EB%AC%B8%EC%9D%84-%EC%93%B0%EB%A9%B4-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC%EA%B0%80-%EC%95%88%EB%90%9C%EB%8B%A4</link>
            <guid>https://velog.io/@neptun-29/forEach%EB%AC%B8%EC%9D%84-%EC%93%B0%EB%A9%B4-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%B2%98%EB%A6%AC%EA%B0%80-%EC%95%88%EB%90%9C%EB%8B%A4</guid>
            <pubDate>Thu, 13 Jun 2024 11:50:33 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/076788d6-12ba-4208-ab2b-80df67c3dbcd/image.png" alt=""></p>
<ol>
<li>현재 주문 관련 API를 만드는 과정에서 1개의 post 요청에서 총 5개의 sql문을 INSERT하는 코드를 작성중이었다.</li>
<li>순서는 FK참조에 따라 이렇게 구성된다.
배달주소테이블 삽입 &gt; 주문테이블 삽입 &gt; 주문당-책테이블 삽입 &gt; 장바구니 제거</li>
<li><strong>그런데 장바구니 제거에서 <code>book_id</code>가 <code>null</code>이라며 자꾸 오류가 나는 것이었다.</strong></li>
<li><code>aysnc</code>와 <code>await</code> 다 잘 작성한 것 같은데 왜그러지 ???</li>
</ol>
<h2 id="원인">원인</h2>
<p>의심이 갔던 바로 한가지 forEach문!</p>
<pre><code class="language-javascript">        items.forEach(async(item)=&gt;{
          let sql3 = `INSERT INTO orderedbooks (order_id, book_id, quantity) 
VALUES (?, 
(SELECT book_id FROM cartItems WHERE id = ?),
(SELECT quantity FROM cartItems WHERE id = ?));`;
          val3 = [order_id,item,item];
          result3 = await addData(sql3,val3,res)
        };
        }) </code></pre>
<p>반복문 중에 for of문이 있었기 때문에 코드를 for of으로 고쳐보았고</p>
<h3 id="arrayforeach-vs-forof">Array.forEach() vs for..of</h3>
<h2 id="해결">해결</h2>
<pre><code class="language-javascript">        for (const item of items) {
            let sql3 = `INSERT INTO orderedbooks (order_id, book_id, quantity) 
            VALUES (?, 
            (SELECT book_id FROM cartItems WHERE id = ?),
            (SELECT quantity FROM cartItems WHERE id = ?));`;
            val3 = [order_id,item,item];
            result3 = await addData(sql3,val3,res)
        };</code></pre>
<p>결국 정상 작동!!!
<img src="blob:https://velog.io/085965f7-e35e-40ef-83ec-223154c0ed14" alt=""></p>
<h2 id="생각">생각</h2>
<h3 id="foreach문은-비동기-처리를-못하고-for-in문은-비동기처리를-해주는데-왜-그런걸까">forEach문은 비동기 처리를 못하고 for in문은 비동기처리를 해주는데 왜 그런걸까??</h3>
<ul>
<li>먼저 for of는 iterable한 Object들의 속성값을 반복해주는 것
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach">https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach</a> 에 따르면..</li>
</ul>
<blockquote>
<p>forEach() expects a synchronous function — it does not wait for promises. Make sure you are aware of the implications while using promises (or async functions) as forEach callbacks.</p>
</blockquote>
<p>promise를 기다려주지 않는다고 한다.
또한,forEach의 콜백함수는 호출만 하고 반환은 기다리지 않기 때문에 거의 동시에 호출된다고 생각해보면 비동기가 적용되지 않는다.</p>
<p>참조: <a href="https://velog.io/@0709/forEach%EC%99%80-Promise.all">https://velog.io/@0709/forEach%EC%99%80-Promise.all</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[JS] 객체 비교하기]]></title>
            <link>https://velog.io/@neptun-29/JS-%EA%B0%9D%EC%B2%B4-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@neptun-29/JS-%EA%B0%9D%EC%B2%B4-%EB%B9%84%EA%B5%90%ED%95%98%EA%B8%B0</guid>
            <pubDate>Mon, 10 Jun 2024 14:34:03 GMT</pubDate>
            <description><![CDATA[<h2 id="문제">문제</h2>
<p>** <code>if (req.body != {} )</code>  &gt;&gt; true    :      req.body는 {}를 출력하는데 왜 true일까?**</p>
<p>req.body도 {}이고 비교하는 것도 {}인데 왜 true가 뜨며
vsCode에서 아래와 같은 주의문을 주었다</p>
<pre><code class="language-jsx">if (req.body !== {} ){~}</code></pre>
<blockquote>
<p>&quot;JavaScript는 값이 아닌 참조로 객체를 비교하므로 이 조건은 항상 &#39;true&#39;을(를) 반환합니다.&quot;</p>
</blockquote>
<p>→  즉 객체끼리는 항상 다를 수 밖에 없다. 객체를 비교할 때는 <code>!==</code> 나 <code>!=</code> 를 잘 쓰지 않음.</p>
<h2 id="해결방법">해결방법</h2>
<h3 id="1-순서가-같은-객체-비교">1. 순서가 같은 객체 비교</h3>
<pre><code class="language-javascript">const obj1 = { name : &quot;kim&quot;, job : &quot;teacher&quot; };
const obj2 = { name : &quot;kim&quot;, job : &quot;teacher&quot; };

if (Object.entries(obj1).toString() == Object.entries(obj2).toString()  ) {
    console.log(&quot;true&quot;)}</code></pre>
<p><code>Object.entries(객체)</code> : [key,value] 쌍이 반환됨.
<code>[ [ &#39;name&#39;, &#39;kim&#39; ], [ &#39;job&#39;, &#39;teacher&#39; ] ]</code></p>
<p>이렇게 변환한 string끼리 비교하는 것</p>
<p>이런 방법도 가능하다</p>
<pre><code class="language-javascript">if (JSON.stringify(obj) == JSON.stringify(com)) &gt;&gt; true</code></pre>
<h3 id="2-그런데-객체는-순서가-안-중요하잖아">2. 그런데 객체는 순서가 안 중요하잖아?</h3>
<pre><code class="language-javascript">const obj1 = { name : &quot;kim&quot;, job : &quot;teacher&quot; };
const obj2 = { job : &quot;teacher&quot;, name: &quot;kim&quot; };

const sortObj1 = Object.keys(obj1).sort().reduce( (obj,key)=&gt; ((obj[key] = obj1[key], obj),{} }
const sortObj2 = Object.keys(obj2).sort().reduce((obj,key) =&gt; ((obj[key] = obj1[key], obj), {} }

if (JSON.stringify(sortObj1) == JSON.stringify(sortObj2)) &gt;&gt; true

</code></pre>
<p><code>const sortObj1 = Object.keys(obj1).sort().reduce( (obj,key)=&gt; ((obj[key] = obj1[key], obj),{} }</code>
이 코드를 자세히 알아보자</p>
<ol>
<li>먼저 keys값으로 [&#39;name&#39;, &#39;job&#39;]으로 정렬한 후 이 배열을 가지고 reduce함수를 실행한다.</li>
<li><code>obj</code>는 accumulator, <code>key</code>는 currentValue 인자로  <code>obj[key] = obj1[key]</code>는 새로운 obj 객체에 key값을 가지고 다시 담는 것이다.</li>
<li>그 뒤에 오는 <code>,obj</code>는 쉼표연산자로 obj를 반환한다.</li>
</ol>
<h3 id="피드백">피드백</h3>
<p>JS의 기본적인 문법 조차 모르고 있어 이해도 안되고 처음에 vscode가 저런 문구를 날려주는데 이해를 못한 것이다.
항상 기초와 기본은 충실히 꾸준히 하자!!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[JWT와 로그인]]></title>
            <link>https://velog.io/@neptun-29/JWT%EC%99%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8</link>
            <guid>https://velog.io/@neptun-29/JWT%EC%99%80-%EB%A1%9C%EA%B7%B8%EC%9D%B8</guid>
            <pubDate>Sun, 09 Jun 2024 02:33:34 GMT</pubDate>
            <description><![CDATA[<h2 id="0-개요">0. 개요</h2>
<ol>
<li>회원가입과 암호화방식 알아보기</li>
</ol>
<p><strong>2. JWT와 로그인</strong></p>
<ol start="3">
<li>Refresh Token을 이용한 로그인과 Authorization</li>
</ol>
<hr>
<h2 id="1-인증과-인가란">1. 인증과 인가란?</h2>
<p><strong>인증(Authentication)</strong>은 사이트에 가입된(등록된) 사용자임을 증명하는 것이다.</p>
<p><strong>인가(Authorization)</strong>은 인증이 확인된 유저가 리소스에 엑세스 할 수 있는 권한을 부여하는 것이다.</p>
<p>1). 로그인을 하면 인증(Authentication)을 통해 TOKEN을 발행 받는다.</p>
<p>2). 인증을 받은 사용자는 리소스에 접근할 때 request Header에 Authorization에 token정보를 같이 보낸다.</p>
<p>3).  토큰을 통해 권한이 있는지 없는지 확인한다.</p>
<h2 id="2-쿠키와-세션-그리고-jwt">2. 쿠키와 세션, 그리고 JWT</h2>
<h3 id="cookie-쿠키">Cookie 쿠키</h3>
<ul>
<li><p>로그인 시 서버가 정보를 쿠키 안에 넣어서 전달한다.</p>
</li>
<li><p>유저가 로그인이 필요한 시점에(리소스에 접근 시) 쿠키를 보여준다.</p>
</li>
<li><p>서버가 저장하는 것이 아니기 때문에 저장 공간이 좋으며 RESTful하다</p>
</li>
</ul>
<p>= 서버가 상태를 저장하지 않음 ; Stateless</p>
<h3 id="세션">세션</h3>
<ul>
<li><p>중요한 정보는 세션에 저장하고 그 정보의 위치를 쿠키에 적는다.</p>
</li>
<li><p>로그인 시 서버가 중요한 정보를 저장해 번호를 전달한다. 클라이언트와 서버는 번호를 이용해서만 대화한다.</p>
</li>
<li><p>보안이 비교적 강해지지만 서버의 저장공간을 사용한다 ; Not Stateless</p>
</li>
</ul>
<h3 id="jwt--json-web-token">JWT : JSON Web Token</h3>
<ul>
<li><p>보안이 강하고 Staeless하며 서버 부담을 줄여준다.</p>
</li>
<li><p>구조</p>
<p>Header.Payload.VerifySignature</p>
<p><code>Header</code>: 토큰을 암호화하는 알고리즘 + 토큰의 형태(타입)</p>
<p><code>Payload</code>: 사용자 정보, 중요한 정보는 입력하지 않음</p>
<p><code>Verify Signature</code>: 페이로드 값이 바뀌면 서명 값도 바뀌기 때문에 인증 가능(JWT가 원본임을 확인)</p>
</li>
<li><p>절차</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/neptun-29/post/d5ba7507-996a-450a-bed2-feb829a0d4a2/image.png" alt=""></p>
<h2 id="3-로그인-구현하기">3. 로그인 구현하기</h2>
<h3 id="유저정보-확인-후-토큰-발행해서-보내주기">유저정보 확인 후 토큰 발행해서 보내주기</h3>
<pre><code class="language-javascript">app.post(&#39;/prac/users/login&#39;, (req,res)=&gt;{
    const {email, password} = req.body;

    const sql = `SELECT * FROM users WHERE users.email = ?`
    const val = [email]
    conn.query(sql,val,(err,results)=&gt;{
        if(err){
            console.log(&#39;로그인 conn.query 오류 : &#39;, err);
            res.status(400).json(err);
        } else {
            let user = results[0];
            let thisPwd = user.password;
            let salt = user.salt;
            let hashReceivedPwd = crypto.pbkdf2Sync(password,salt,10000,10,&#39;sha512&#39;).toString(&#39;base64&#39;);
          ...**token**

})</code></pre>
<ol>
<li>사용자 정보를 받아온다</li>
<li>DB에서 이메일과 같은 유저정보를 가져온 후</li>
<li>암호가 일치하는지 확인한다.</li>
<li>일치하면 .. <strong>token</strong> 아래 코드</li>
</ol>
<pre><code class="language-javascript">**token**
 if(user &amp;&amp; thisPwd == hashReceivedPwd){//비밀번호가 일치하면 토큰 발행
   const accessToken = jwt.sign(
     {email: email, username: user.username},
     &#39;accessPrivateKey&#39;,
     {expiresIn : &quot;1 min&quot;,
      issuer : &quot;estie&quot;
     }
   )

   res.cookie(&#39;accessToken&#39;,accessToken,{httpOnly:true});
   res.json({
     message : `${user.username}님 로그인되셨습니다.`,
     token : accessToken
   });

    })</code></pre>
<ol>
<li><code>jwt.sign</code> 을 이용해 토큰 발행 후</li>
<li><code>res.cookie</code>로 쿠키에 담아 토큰을 보내준다.</li>
</ol>
<h2 id="4-access-token의-한계">4. Access Token의 한계?</h2>
<p>하지만 토큰의 유효기간이 길면 중간에 정보를 탈취당하는 문제가 생길 수 있다.
그럼 토큰의 유효기간을 짧게 할 수 있지만 1분마다 로그인하면 사용자 입장에서 매우 불편할 것이다. 따라서 등장한 것이 Refresh Token!
Access Token이 만료되었을 때 Refresh Token으로 확인 후 재발행해주는 방법이다.</p>
<p>기존 코드에서 많이 변경되기 때문에 3장에서 Refresh Token을 구현하고 특정 라우터에서 인가를 받는 코드까지 작성해보겠다.<img src="https://velog.velcdn.com/images/neptun-29/post/75a92f21-fc14-48f0-8058-d3b8d5585ad8/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[회원가입과 암호화방식 알아보기]]></title>
            <link>https://velog.io/@neptun-29/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EA%B3%BC-%EC%95%94%ED%98%B8%ED%99%94%EB%B0%A9%EC%8B%9D-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</link>
            <guid>https://velog.io/@neptun-29/%ED%9A%8C%EC%9B%90%EA%B0%80%EC%9E%85%EA%B3%BC-%EC%95%94%ED%98%B8%ED%99%94%EB%B0%A9%EC%8B%9D-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0</guid>
            <pubDate>Fri, 07 Jun 2024 06:17:42 GMT</pubDate>
            <description><![CDATA[<h2 id="0-개요">0. 개요</h2>
<p>백엔드 공부를 하면서 &quot;회원가입 &amp; 로그인 &amp; 인증+인가&quot; 기능이 자주 쓰이면서도 정리가 필요할 것 같아 작성하게 되었다.</p>
<p>앞으로 3개로 나누어서 글을 작성해보려한다.</p>
<p><strong>1. 회원가입과 암호화방식 알아보기</strong>
2. JWT와 로그인
3. Refresh Token을 이용한 로그인과 Authorization</p>
<hr>
<h2 id="1회원가입-절차-및-코드-구현">1.회원가입 절차 및 코드 구현</h2>
<h3 id="절차">절차</h3>
<ol>
<li>username, email, password를 받아온다.</li>
<li>비밀번호를 암호화 한다.</li>
<li>users 테이블에 회원정보 삽입한다.</li>
</ol>
<pre><code class="language-jsx">const join = 
    (req,res) =&gt; {
        let {username, email, password} = req.body;
                //1. 암호화
        const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
        const hashPwd = crypto.pbkdf2Sync(password,salt,10000,10,&#39;sha512&#39;).toString(&#39;base64&#39;)

        const sql = `INSERT INTO users (email, password, salt, username)
                    VALUES (?,?,?,?)`;
        const val = [email, hashPwd, salt, username];
        conn.query(sql,val,
            (err,results) =&gt; {
                if(err) {
                    console.log(&quot;회원가입 conn.query 오류&quot;, err);
                } 
                else{
                    res.status(201).json({
                        message : &quot;회원가입이 완료되었습니다&quot;,
                        result : results
                    })
                }
        })
}</code></pre>
<h3 id="암호화">암호화</h3>
<p>방법 : salt를 이용해 해싱된 비밀번호를 만들어 DB에 salt와 해싱된 비밀번호를 저장한다.</p>
<p>⇒ 로그인 시 <code>로그인에서 받은 비밀번호</code> 를 저장된 salt로 똑같이 해싱해서 비교한다.</p>
<hr>
<h3 id="유효성-검사-추가">유효성 검사 추가</h3>
<p>node js에서 사용할 수 있는     <code>express-validator</code> 모듈을 이용하였다.</p>
<pre><code class="language-jsx">app.post(&#39;/prac/users/join&#39;,
    [
        body(&#39;email&#39;).notEmpty().isString().withMessage(&#39;이메일 필요&#39;),
        body(&#39;password&#39;).notEmpty().isString().withMessage(&#39;비밀번호 필요&#39;),
        body(&#39;username&#39;).notEmpty().isString().withMessage(&#39;사용자이름 필요&#39;),
        (req,res,next) =&gt; {
            const err = validationResult(req);
            if (err.isEmpty()) {
                return next()
            } else {
                res.status(400).json(err.array())
            }
        }
    ] 
 ,join)</code></pre>
<h2 id="2-암호화-방식">2. 암호화 방식</h2>
<p>회원가입 시 비밀번호를 받아 암호화시켜 데이터베이스에 저장하게 되는데
암호화가 어떤 방식으로 작동되고 종류들을 알아보자</p>
<h3 id="단방향-해시-함수">단방향 해시 함수</h3>
<ul>
<li>복원이 불가능</li>
<li>단점 : Rainbow Table (; 전처리된 Digest(최종 암호화 값)들을 찾아놓은 테이블) 을 이용해  해킹</li>
</ul>
<p>⇒ 해결법 : salt와 여러번 해싱을 돌려줌 (<strong>salt &amp; key strectching</strong>)</p>
<h3 id="1-bcrypt">1. bcrypt</h3>
<ul>
<li>blowfish 암호화 기반 ( 키 방식의 대칭형 블록 암호, 자원 효율적)</li>
<li>강력한 해시 메커니즘</li>
</ul>
<pre><code class="language-jsx">const bcrypt = require(&#39;bcrypt&#39;);

// 비밀번호 해시화
saltRounds = 10
bcrypt.genSalt(saltRounds, (err,salt)=&gt;{
    bycrpt.hash(myPlaintextPwd, salt, (err,hash) =&gt;{
        //store hash in DB
    }
}

// 비밀번호 검증
async function verifyPassword(password, hash) {
  const isMatch = await bcrypt.compare(password, hash,
  (err,result)=&gt;{
      // result == true or false
  };
  return isMatch;
}</code></pre>
<h3 id="2-scrypt">2. scrypt</h3>
<ul>
<li>brute force 공격에 대해 강력</li>
<li>많은 자원낭비 (메모리 집약적, CPU)</li>
<li><code>password salt keylen options callback</code></li>
</ul>
<pre><code class="language-jsx">const {scryptSync} = require(&#39;node:crypto&#39;);
//비동기는 공식문서 참조

const key1 = scryptSync(&#39;password&#39;, &#39;salt&#39;, 64);
console.log(key1.toString(&#39;hex&#39;));  // &#39;3745e48...08d59ae&#39;
// Using a custom N parameter. Must be a power of two.
const key2 = scryptSync(&#39;password&#39;, &#39;salt&#39;, 64, { N: 1024 });
console.log(key2.toString(&#39;hex&#39;));  // &#39;3745e48...aa39b34&#39;</code></pre>
<h3 id="3-pbkdf2--password-based-key-derivation-function">3. PBKDF2 : Password-Based Key Derivation Function</h3>
<pre><code class="language-jsx">const crypto = require(&#39;crypto&#39;);
//동기식
const salt = crypto.randomBytes(10).toString(&#39;base64&#39;);
const hashPwd = crypto.pbkdf2Sync(password,salt,10000,10,&#39;sha512&#39;).toString(&#39;base64&#39;)

//비동기식
DIGEST = crypto.pbkdf2(password,salt,iterations,keylen,digest,callback(err,derivedKey))</code></pre>
<ul>
<li><p><code>crypto.randomBytes</code> The number of bytes to generate.</p>
<p>-&gt;  <code>&lt;Buffer 03 c7 62 54&gt;</code></p>
</li>
<li><p><code>crypto.pbkdf2Sync</code> 비밀번호를 salt를 이용해 해싱해줌</p>
</li>
<li><p>password, salt, iterations(반복횟수), keylen(결과길이), digest(해시 알고리즘)</p>
<p>= 솔트를 적용 후 임의로 선택한 수로 반복횟수로 해시함수를 돌린다</p>
</li>
</ul>
<h3 id="4-argon2">4. argon2</h3>
<ul>
<li>최근에 나온 알고리즘, 병렬적 접근가능</li>
<li>메모리와 계산 비용을 유연하게 조절가능</li>
</ul>
<pre><code class="language-jsx">const argon2 = require(&#39;argon2&#39;);

// 비밀번호 해시화
try {
  const hash = await argon2.hash(&quot;password&quot;);
} catch (err) {
  //...
}
//verify
try {
  if (await argon2.verify(&quot;&lt;big long hash&gt;&quot;, &quot;password&quot;)) {
    // password match
  } else {
    // password did not match
  }
} catch (err) {
  // internal failure
}</code></pre>
<p>참고 <a href="https://nodejs.org/api/crypto.html">https://nodejs.org/api/crypto.html</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MYSQL] BULK INSERT, 벌크 인서트 : 여러 데이터를 한번에 INSERT 하기]]></title>
            <link>https://velog.io/@neptun-29/MYSQL-BULK-INSERT-%EB%B2%8C%ED%81%AC-%EC%9D%B8%EC%84%9C%ED%8A%B8-%EC%97%AC%EB%9F%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%95%9C%EB%B2%88%EC%97%90-INSERT-%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@neptun-29/MYSQL-BULK-INSERT-%EB%B2%8C%ED%81%AC-%EC%9D%B8%EC%84%9C%ED%8A%B8-%EC%97%AC%EB%9F%AC-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%ED%95%9C%EB%B2%88%EC%97%90-INSERT-%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 01 Jun 2024 12:29:48 GMT</pubDate>
            <description><![CDATA[<h2 id="문제상황">문제상황</h2>
<p>주문했을 때 주문id - (장바구니 아이템 id, 개수)로 연결해주는 테이블을 만들고 있었다. 한번 주문할 때 여러개의 장바구니 아이템이 담기기 때문에
<strong>여러 데이터를 SELECT해서 INSERT해야 하는 상황</strong></p>
<h3 id="insert-values에-여러-select-넣기">INSERT VALUES에 여러 SELECT 넣기</h3>
<p>먼저 내가 작성한 코드는 이러하다.</p>
<pre><code class="language-javascript">let order_id = 1;
let items = [
  {
    cart_id : 1,
    item_id : 1,
    quantity : 2
    }, 
  {
    cart_id: 2,
    item_id: 4,
    quantity :1
    }
]

 items.forEach(async (item) =&gt; {
    let sql = `INSERT INTO orderedbooks (order_id, book_id, quantity) 
                VALUES (?, 
                        (SELECT item_id FROM cartItems WHERE id = ?),
                        (SELECT quantity FROM cartItems WHERE id = ?))`;
      val = [order_id,item.cart_id,item.cart_id];
      await conn.execute(sql, val);
</code></pre>
<p>여기서 주의할 것은 </p>
<pre><code class="language-javascript">VALUES (order_id, (SELECT item_id, quantity FROM cartItems WHERE id = ?)</code></pre>
<p> 이런 식으로 SELECT를 한번에 여러개하면 <code>error</code>가 났다.</p>
<p> 첫번째 코드로 작성하면 잘 INSERT가 되었다.
 for문으로 INSERT를 item개수만큼 실행한 것.
 여기서 고민한 것이 많은 수의 데이터를 SELECT해서 INSERT할 때는 첫번째 방법이 복잡할 것 같은데 좀 더 쉬운 방법이 없을까였다.(for문을 너무 많이 돌리는 것이 아닐까..)</p>
<p> 결국 알아낸 것이</p>
<h3 id="벌크인서트bulk-insert">벌크인서트(BULK INSERT)</h3>
<pre><code class="language-javascript">let order_id = 1;
let items = [
  {
    cart_id : 1,
    item_id : 1,
    quantity : 2
    }, 
  {
    cart_id: 2,
    item_id: 4,
    quantity :1
    }
]

let sql = `INSERT INTO orderedItem (order_id, item_id, quantity) VALUES ?`;

let val = [];
items.forEach((item) =&gt; {
  val3.push([order_id, item.item_id, item.quantity])
        })
let [result3] = await conn.query(sql, [val]);
} 
</code></pre>
<p>sql문은 한번만 돌리면 되는 방법! 벌크 인서트</p>
<ol>
<li>배열을 [values]형태로 <code>conn.query(sql, [values])</code>에 넣어주면 된다.</li>
<li>execute는 적용이 안된다.</li>
</ol>
<p>결과 sql = <code>INSERT INTO orderedbooks (order_id, book_id, quantity) VALUES (1,1,2),(1,4,1)</code></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[SQL문(SELECT, INSERT)과 예외처리]]></title>
            <link>https://velog.io/@neptun-29/SQL%EB%AC%B8SELECT-INSERT%EA%B3%BC-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@neptun-29/SQL%EB%AC%B8SELECT-INSERT%EA%B3%BC-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Thu, 23 May 2024 08:09:03 GMT</pubDate>
            <description><![CDATA[<h1 id="데이터가-없는데-sql문을-사용할-때-고려해야-하는-것">데이터가 없는데 SQL문을 사용할 때 고려해야 하는 것<img src="https://velog.velcdn.com/images/neptun-29/post/ff642c7b-7281-4bc3-b7d3-390941164834/image.png" alt=""></h1>
<h2 id="문제-배경">문제 배경:</h2>
<p>API를 설계하는 연습을 하는 도중 빈 값이거나 없는 값인데 그냥 빈 배열을 반환하면서 오류가 나지 않는 sql문들이 있어서 코드를 작성할 때 여러 문제가 발생했었다. 이를 정리해보려 한다.</p>
<h2 id="탐구">탐구:</h2>
<h3 id="select와-조회">SELECT와 조회</h3>
<ol>
<li>개별 책 조회 API를 만들어서 GET을 보내줬는데 없는 아이디임에도 불구하고 Postman에 빈 배열과 함께 200 OK가 떴다.</li>
</ol>
<pre><code class="language-javascript">router.get(&#39;/books/:id&#39;, (req,res)=&gt;{
    let {id} = req.params
    id = parseInt(id)
    let sql = `SELECT * FROM books WHERE id = ?`
    let values = id

    conn.query(sql,values, 
        function(err,results) {
            res.json(results)
    })
})
&gt;&gt; results = []


이런 results를 가지고 코드를 짤 때 값이 비어있으므로 오류가 발생한다.
또한, `conn.query`의 콜백함수에 들어있는 err와는 상관이 없다!!
 sql문이 오류인 것인가 vs 아닌가를 생각해봐야 하는 것.

따라서 SELECT 쿼리문은 이런 식으로 **예외처리**를 해주어야한다.
핵심은 if문의 조건이다. **results[0]값이 있으면 ~ 없으면 ~**


```javascript
    const curBook = results[0];
        if(curBook){
            res.status(200).json(`관련내용`);
        } else {
            res.status(400).end();
        }

or 
        if(results.length){
            ...동일
        }</code></pre>
<hr>
<h3 id="insert와-등록">INSERT와 등록</h3>
<ol start="2">
<li><p>이와 반대로 책을 등록하는 POST API에서 사용하는 INSERT 경우에..</p>
<pre><code class="language-javascript">   app.post(&#39;/books/:id&#39;, (req,res)=&gt;{  //채널 생성

       const {name, user_id} = req.body
       let sql = `INSERT INTO channels (name,user_id) VALUES (?,?)`
       let values = [name,user_id]
       conn.query(
           sql, values,
           function(err,results){
               //sql의 에러: 없는 userId사용할때
               if(err){
                   console.log(err)
                   return res.status(400).end()
               }
               res.status(201).json(results)
           }
       )
</code></pre>
</li>
</ol>
<p>user_id가 FK로 설정이 되어있기 때문에 없는 데이터베이스에 없는 user_id를 사용하면 insert에서 SQL문 오류가 난다.  따라서 이런 경우에는 <code>conn.query</code> 의 콜백함수의 err에서 검출이 된다.</p>
<hr>
<p>  cf. 추가로 UPDATE나 DELETE에서도 주의사항이 존재한다.
  update하거나 delete를 요청했지만 변경된 데이터가 없을경우
  results를 반환하지 않기 때문에 <code>results.affectedRows</code>를 사용할 수 있다.</p>
<h2 id="정리">정리</h2>
<ol>
<li>SELECT 쿼리문은 WHERE 조건이 만족되지 않아도, 혹은 없는 데이터를 url에서 요청해도 빈 배열을 반환하기 때문에 코드 작성 시 유의해야한다.</li>
<li>INSERT 쿼리문은 없는 아이디를 사용하면 sql문 자체에서 오류가 발생한다.</li>
<li><strong>그러나 사실 SELECT냐 INSERT가 중요한 것이 아니라 sql문이 오류가 나는 것인지 아닌지가 중요한 것이다. sql문이 오류가 나면 query에서 err가 나오지만 빈 배열을 반환할 때는 오류가 없기 때문에 따로 관리해주어야 한다.</strong></li>
</ol>
]]></description>
        </item>
    </channel>
</rss>