<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>I'll never know if I never commit</title>
    <link>https://moonheekim-code.tistory.com/</link>
    <description>행복한 개발자지망생</description>
    <language>ko</language>
    <pubDate>Fri, 8 May 2026 13:00:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>무니웜테일패드풋프롱스</managingEditor>
    <image>
      <title>I'll never know if I never commit</title>
      <url>https://tistory1.daumcdn.net/tistory/3639664/attach/e8bb0a1a41294de4b9f8e60e34e9b690</url>
      <link>https://moonheekim-code.tistory.com</link>
    </image>
    <item>
      <title>[JavaScript] 자바스크립트 실행컨텍스트</title>
      <link>https://moonheekim-code.tistory.com/138</link>
      <description>&lt;h1&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행컨텍스트 (execution context)&lt;/span&gt;&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행할 코드에 제공할 환경 정보들을 모아놓은 객체&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동일한 환경(하나의 실행컨텍스트) 의 코드들을 실행 시, 필요한 환경정보들을 모아 컨텍스트를 구성하고 이를 콜 스택에 쌓아놨다가, &lt;b&gt;가장 위에 쌓여있는 컨텍스트와 관련있는 코드들을 실행&lt;/b&gt;함&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;동일한 환경이 구성되는 조건: eval 함수, 함수, 전역 공간&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바스크립트를 실행하는 동시에 전역 컨텍스트가 콜 스택에 담김&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최상단은 브라우저에서 자동으로 실행되므로, 자바스크립트 파일이 열림과 동시에 전역 컨텍스트가 활성화 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 실행 컨텍스트가 활성화 될 때 자바스크립트 엔진은, 해당 컨텍스트에 관련된 &lt;b&gt;코드들을 실행하는데 필요한 환경 정보들을 수집&lt;/b&gt;해서 &lt;b&gt;실행 컨텍스트 객체에 저장&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 컨텍스트 객체&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;VariableEnvironment&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;LexicalEnvironment&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;This binding&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. VariableEnvironment&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;environment Record (snapshot) - 초기 상태 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;outerEnvironmentReference (snapshot) - 초기 상태 유지&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 컨텍스트 실행 시 Variable Environment에 가장 먼저 정보를 담음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그 후에 이를 그대로 복사하여 Lexical Environment 생성&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전역 실행 컨텍스트는 변수 객체를 생성하는 대신, 자바스크립트 구동환경이 별도로 제공하는 객체(global object)를 활용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. LexicalEnvironment&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;environment Record - 함수 실행도중에 변경되는 사항이 즉시 반영&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;outerEnvironmentReference - 함수 실행도중에 변경되는 사항이 즉시 반영&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨텍스트를 구성하는 환경정보들을 모아놓음 (식별자, 참조 등)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;environmentRecord&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 컨텍스트와 관련된 코드의 식별자 정보들이 저장 ( 함수에 지정된 매개변수 식별자, 함수 자체, var 선언 변수 식별자 등..)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;컨텍스트 내부 전체를 처음-끝 까지 훑어가며 순서대로 식별자 정보 수집&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 environmentRecord에서 코드가 실행되기 전에 해당 환경에 속한 변수명을 모두 수집함 == '호이스팅'&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉, 자바스크립트 엔진이 실제적으로 '끌어올리지는' 않고 enviromentRecord가 변수 정보를 미리 수집하는 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;function a() {
    var x = 1;
    console.log(x); 
    var x ;
    console.log(x);
    var x =2; 
    console.log(x); 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;호이스팅이 없다면 위의 결과는 1, undfined, 2 여야 할 것이다. 하지만 호이스팅의에 의해서 결과는 1,1,2 가 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;environmentRecord&lt;/b&gt; - 호이스팅의 규칙&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수는 선언부와 할당부를 나누어 선언부만 끌어올리는 반면, 함수 선언은 함수 전체를 끌어올린다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수의 할당부는 원래 자리에 남겨둔다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;environmentRecord&lt;/b&gt; - 함수 선언문 ,함수 표현식의 호이스팅 차이점&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;function a() {} // 함수 선언문
var b = function(){} // 익명 함수 표현식 &lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;fortran&quot;&gt;&lt;code&gt;console.log(sum(1,2));
console.log(multiply(1,2));

function sum(a, b){
    return a+b;
}

var multiply = function(a,b){
    return a*b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;environment record에 의해 호이스팅 후&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;function sum(a,b){
    return a+b;
} // 함수 선언문은 함수 정보 전체가 수집됨

var multipy ; // 함수 표현식은 함수명만 수집되고 함수 선언은 그 자리에 남음

console.log(sum(1,2)); // 3 
console.log(muliply(1,2)); // mutiply is not a function 에러 

var multiply = function(a,b){
    return a*b;
}&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;outerEnvironmentReference&lt;/b&gt; - 스코프 ,스코프 체인&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스코프(Scope) : 식별자에 대한 유효범위 (함수 스코프, 블럭 스코프)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스코프 체인 (Scope chain) : 식별자의 유효범위를 안에서 바깥으로 차례대로 검색해가는 것&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스코프와 스코프체인은 outerEnvironmentReference에 의해 가능&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;outerEnvironmentReference&lt;/b&gt; - 스코프 체인&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;outerEnvironmentReference는 현재 호출된 함수가 선언될 당시의 LexicalEnvironment를 참조&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;A함수 내부에서 선언된 B함수의 outerEnvironmentReference는 함수 A의 LexicalEnvironment참조 .. 이렇게 타고 올라가다보면 전역 컨텍스트의 LexicalEnvironment가지 가게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 연결리스트 형태를 띄게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각 outerEnvironmentReference는 자신이 선언된 시점의 LexicalEnvironemt만 참조하므로, &lt;b&gt;가장 가까운 요소부터 차례대로 접근&lt;/b&gt; 가능 ==&amp;gt; 여러 스코프에서 동일한 식별자 선언한 경우 &lt;b&gt;스코프 체인 상에서 가장 먼저 발견 된 식별자에게만 접근 가능&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;var a = 1; 
var outer = function(){
    var inner= function(){
        console.log(a); // # 1번 
        var a = 3; 
    }
    inner();
    console.log(a); // #2 번 
};

outer();&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1번에서는 식별자 정보 a를 수집하여 호이스팅했으나 할당은 console.log보다 아래있으므로 undefined 출력&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 변수 은닉화 참조&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2번에서는 활성화된 실행 컨텍스트 LexicalEnvironmnet에 접근하여 environmentRecord에 a 가 있는지 찾아보고 없으면 &lt;b&gt;outerEnvironmentReference가 가리키고 있는 environmentRecord (여기서는 전역 LexicalEnvironment)로 넘어가 a 가 있는지 찾아봄&lt;/b&gt;. a가 있으므로 a에 저장된 값 1 출력&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;outerEnvironmentReference&lt;/b&gt; - 변수 은닉화 ( variable shadowing)&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스코프 체인 상 있는 변수라고 해서 모두 접근 가능하지 않음&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;스코프 체인 상 첫번째 인자만 검색 가능, 식별자 발견시 더이상 체인 검색을 하지 않음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. This Binding&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;this로 지정된 객체가 저장&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 컨텍스트 활성화 당시 this가 지정되지 않는다면, this에는 &lt;b&gt;전역 객체&lt;/b&gt;가 저장됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>programming language/JavaScript</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/138</guid>
      <comments>https://moonheekim-code.tistory.com/138#entry138comment</comments>
      <pubDate>Sun, 26 Jul 2020 10:27:29 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 깊은복사와 얕은복사</title>
      <link>https://moonheekim-code.tistory.com/136</link>
      <description>&lt;h2&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;깊은복사(deep Copy) 와 얕은복사(Shallow Copy)&lt;/span&gt;&lt;/b&gt;&lt;/h2&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;var num1 =10;
var num2 = num1;
var num2 = 20;
console.log(num1) // 10
console.log(num2) // 20
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 immutable 한 변수(기본형 데이터) 를 copy하면 나타는게 &lt;b&gt;깊은 복사&lt;/b&gt;이다. &lt;b&gt;깊은 복사는 내부의 값(기본형 데이터)까지 복사&lt;/b&gt;하는 것이다. 물론 여기서 num2=num1을 했을 때, num2와 num1은 같은 주소 (정수 10이 담겨있는 메모리)를 가리키게 되지만 num2가 다른 주소를 가리킨다고 해서 num1도 덩달아 다른 주소를 가리키게 되지 않는다. 왜냐하면 당연하다! 기본형 데이터는 &lt;b&gt;불변&lt;/b&gt;하기 때문이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;var obj1 ={
    a:1,
    b:2
}

var obj2 = obj1;
obj2.a=3;

console.log(obj1.a) // 3
console.log(obj2.a) // 3&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 객체, 즉 참조형 데이터와 같이 mutable한 변수를 copy하고, obj2의 프로퍼티 값을 바꾸면 기존 값의 프로퍼티 역시 바뀐다. 이게 바로 &lt;b&gt;얕은 복사&lt;/b&gt;이다. 얕은 복사는 복사 대상의 내부의 값(기본형 데이터)을 복사하는게 아니라, 참조형 데이터 즉, &lt;b&gt;기본형 데이터를 가리키고 있는 주소만 복사&lt;/b&gt;한다. 따라서 obj2.a 의 값을 바꾸는 것은 &lt;b&gt;obj2.a의 기본형 데이터 자체를 바꾸는게 아니라, obj2.a가 가리키고 있는 기본형 데이터의 주소를 바꾸는 것&lt;/b&gt;으로, 당연히 obj1.a 값도 바뀌게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;따라서 obj2와 obj1은 하나를 바꾸면 나머지도 바뀌게 된다. 왜냐! 참조형 데이터는 &lt;b&gt;가변&lt;/b&gt;하기 때문이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참조형에 대해 깊은 복사를 하는 법 ?&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;간단히 말해서 해당 데이터를 복사하되, 참조형 데이터 각각을 '내부 값'까지 완전히 복사하면 된다. 쉽게 떠올릴 수 있는 방법은 재귀 함수를 통한 복사 방법이다.&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;재귀함수를 통한 참조형 데이터 깊은 복사&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;var deepCopy =function(target){
    var result={}
    if (typeof target === 'object' &amp;amp;&amp;amp; target !== null){
        for(var prop in target){
            result[prop]=deepCopy(target[prop]);
        }
    }else{ // 기본형 데이터까지 내려가면 그제서야 복사를 한다. 
        result = target;
    }
    return target;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또 다른 방법은 JSON을사용하는 것이다. 객체를 JSON 문법으로 표현된 문자열로 전환하고, 다시 JSON 객체로 바꾸는 것이다. 다만 해당 방법은 메서드나 숨겨진 프로퍼티는 모두 무시한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;JSON을 활용한 깊은 복사 ( - 코어 자바스크립트 p.28 참조)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;var deepCopyViaJSON=function(target){
    return JSON.parse(JSON.stringify(target));
};

var obj = {
    a:1,
    b:[1,2]
}

var obj2=deepCopyViaJSON(obj);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>programming language/JavaScript</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/136</guid>
      <comments>https://moonheekim-code.tistory.com/136#entry136comment</comments>
      <pubDate>Sat, 25 Jul 2020 09:54:17 +0900</pubDate>
    </item>
    <item>
      <title>[ node js - express 쇼핑몰 웹] 7. 상품 검색 기능 구현</title>
      <link>https://moonheekim-code.tistory.com/135</link>
      <description>&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  2020 07 24&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전체 상품 중에서 특정 키워드가 title에 들어간 상품 검색 기능 구현&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;몽고 db에서는 searching 기능을 제공해주는데, 먼저 검색의 대상이 되고자 하는 컬렉션 필드를 text index로 등록해줘야 한다. 아래와 같이 등록해주면 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Schema.index({필드명: 'text'});&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;const mongoose =require('mongoose');
const Schema = mongoose.Schema;

const ProductSchema = new Schema({
    title:{
        type:String,
        required:true
    },
    price:{
        type:Number,
        required:true
    },
    imageUrl:{
        type:String,
        required:true
    },
    description:{
        type:String,
        required:true
    },
    userId:{
        type:Schema.Types.ObjectId,
        required:true
    }
});
ProductSchema.index({title: 'text'}); // text index로 등록 
module.exports=mongoose.model('Product',ProductSchema);&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제 사용자가 검색한 키워드를 서버에 받아오기 위해 form 을 만들어주고, method는 GET으로 해주어 해당 키워드를 쿼리로 담아온다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt; &amp;lt;form action=&quot;/search&quot; method=&quot;GET&quot;&amp;gt;
    &amp;lt;label&amp;gt;title search&amp;lt;/label&amp;gt;
    &amp;lt;input class =&quot;searchBox&quot; type=&quot;text&quot; name=&quot;searchWord&quot;&amp;gt;
    &amp;lt;button class=&quot;btn&quot; type=&quot;submit&quot;&amp;gt;search&amp;lt;/button&amp;gt;
&amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;searchWord 쿼리에 담아온 데이터를 이용해서 Product 스키마에게 search 쿼리를 적용하여 특정 상품만 받아오도록 하자.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Product.find({ $text : {$search: searchWord}})&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 입력해주면 해당하는 product만 가져올 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;추가 : 나는 searching Result 페이지에도 pagination을 구현했기 때문에 부가적인 요소있다.&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;searchWord쿼리와 함께 page쿼리에는 page number를 담아왔다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 주의 할점은 아래 예시처럼 쿼리를 두개이상 보낼 때는 각 쿼리들을 '&amp;amp; '으로 연결시키고, 뒤에오는 쿼리문에는 ? 을 붙여선 안된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;http://localhost:3000/search?searchWord=text&amp;amp;page=1&lt;/code&gt;&lt;/pre&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.getSearch=(req,res,next)=&amp;gt;{ // 상품 찾기
    const searchWord = req.query.searchWord; // 쿼리에 담아온 
    console.log(searchWord);
    let pageNum=1;
    if(req.query.page){  pageNum=+req.query.page; } // 페이지 번호 담아오기 
    console.log(req.query.page);
    let totalItems; 
    Product.find({ $text : {$search: searchWord}}).countDocuments() // 전체 아이템 수 
    .then(itemsNum=&amp;gt;{ 
        totalItems=itemsNum;
        return Product.find({ $text : {$search: searchWord}}) 
        .skip((pageNum-1)*POST_PER_PAGE) // 앞 페이지 product skip  
        .limit(POST_PER_PAGE) // 현재 페이지에 와야할 product 수 
    })
    .then(products=&amp;gt;{
        res.render('shop/searchingResult', {
            pageTitle: 'searching result',
            path:'/search?searchWord='+searchWord,
            prods: products,
            currentPage:pageNum, // 현재 페이지 
            hasNextPage: POST_PER_PAGE*pageNum &amp;lt; totalItems, // 다음 페이지가 있는가? 
            hasPreviousPage: pageNum&amp;gt;=2, // 이전 페이지가 있는가? 
            nextPage: pageNum+1, // 다음페이지 
            previousPage:pageNum-1, //이전페이지 
            lastPage: Math.ceil(totalItems/POST_PER_PAGE) //마지막 페이지 
        })
        })
    .catch(err =&amp;gt;console.log(err));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;몽고 db의 serach 기능에 대한 더 많은 정보는 &lt;a href=&quot;https://docs.mongodb.com/manual/text-search/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;docs.mongodb.com/manual/text-search/&lt;/a&gt; 에 있다!&lt;/p&gt;</description>
      <category>Project/첫번째 프로젝트 쇼핑몰 웹</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/135</guid>
      <comments>https://moonheekim-code.tistory.com/135#entry135comment</comments>
      <pubDate>Fri, 24 Jul 2020 14:57:44 +0900</pubDate>
    </item>
    <item>
      <title>[JavaScript] 자바스크립트 데이터타입</title>
      <link>https://moonheekim-code.tistory.com/134</link>
      <description>&lt;h2&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바스크립트 데이터 타입&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;기본형 : 숫자, 불리언, null, undefiend, Symbol&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참조형 : 객체, 함수, 날짜 , 배열&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바스크립트가 변수를 선언/할당 하는 과정 - 기본형&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수 : 변할 수 있는 데이터&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;식별자: 해당 변수를 가리키는 이름&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var a = 1; &lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같은 코드를 썼을 때, 자바스크립트 엔진은 다음과 같이 작동한다.&lt;/span&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;선언된 변수를 위한 비어있는 메모리 공간을 확보한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 메모리 공간의 식별자를 'a' 로 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;a에 할당할 데이터 1이 메모리 영역에 존재하는지 살펴 본다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;있다면 해당 주소를 'a' 공간에 대입하고, 없다면 새로운 메모리 공간에 데이터 1을 저장한 후 해당 공간의 주소를 'a' 공간에 대입한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;왜 자바스크립트는 변수 a 공간에 바로 데이터를 할당하지 않고 해당 데이터의 '주소값'을 할당하는가 ?&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;형변환이 필요 없다.&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;C/C++ 자바에서는 변수를 선언할 때부터 해당 변수 타입에 따라 메모리를 다르게 할당한다 . ( 정수는 4 byte, 소수는 2byte..) 이에 따라서 해당 변수의 값을 바꾸고자 할 때 형변환 과정이 필수이다. 하지만 자바스크립트에서 변수의 값을 변경하고자 하면 해당 변수가 가리키는 주소값만 변경하면 되므로 형변환 과정이 필요 없다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바스크립트에서 아래 예제와 같이 변수 a가 가리키는 문자열 &quot;abc&quot;에 &quot;def&quot;를 추가한다고 할때, 자바스크립트는 기존 &quot;abc&quot;가 저장된 메모리 공간에서 값 자체를 바꾸는게 아니라, &lt;b&gt;아예 새로운 메모리 공간을 확보하여 &quot;abcdef&quot; 값을 할당&lt;/b&gt;하고, &lt;b&gt;변수 a 가 가리키는 주소값을 변경&lt;/b&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;var a =&quot;abc&quot;;
a+=&quot;def&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메모리의 효율적인 사용&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만약 같은 값을 가진 변수가 몇천개, 몇만개씩 필요하다면 , 우리는 해당 변수마다 값을 일일이 할당할 필요 없이 해당 값을 저장한 메모리 영역을 하나 만들고, 모든 변수가 하나의 주소를 가리키도록 하면 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;변수 선언/할당 과정으로 알 수 있는 기본형의 불변성&lt;/span&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 예제에서도 보았듯, 변수가 가리키고 있는 &lt;b&gt;메모리 공간에 저장된 '값 자체'는 변할 수 없다.&lt;/b&gt;&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메모리 공간에 저장된 값 1을 2로 바꿀 수 없음&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다만, 변수가 가리키는 &lt;b&gt;주소값&lt;/b&gt;을 변경 할 수 있을 뿐이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2&gt;&lt;span style=&quot;color: #000000;&quot;&gt;자바스크립트가 변수를 선언/ 할당하는 과정 - 참조형&lt;/span&gt;&lt;/h2&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var obj = {
    a: 1,
    b: 'abc'
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같은 코드를 썼을 때 자바스크립트 엔진은 다음과 같이 작동한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;dd.jpg&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;960&quot; width=&quot;450&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMcS3s/btqFWYoTzGC/5KxOZjyqE02VKGBU9yvKXk/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMcS3s/btqFWYoTzGC/5KxOZjyqE02VKGBU9yvKXk/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMcS3s/btqFWYoTzGC/5KxOZjyqE02VKGBU9yvKXk/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMcS3s%2FbtqFWYoTzGC%2F5KxOZjyqE02VKGBU9yvKXk%2Fimg.jpg&quot; data-filename=&quot;dd.jpg&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;960&quot; width=&quot;450&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;선언된 변수 obj 를 위한 비어있는 메모리 공간을 하나 확보한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 변수의 식별자를 'obj'로 지정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 변수에 할당된 값을 메모리 영역에서 찾으려고 보니 여러개의 프로퍼티로 이루어진 데이터 그룹이다. 일단, &lt;b&gt;해당 데이터 그룹 ( 프로퍼티들 ) 의 주소를 저장할 메모리 공간을 확보&lt;/b&gt;해준다. ( 4 에서 확보된 메모리 공간의 주소값을 저장하게 된다.)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그룹 내부의 프로퍼티 들을 위한 메모리 공간을(여기서는 두개의 공간) 확보해주고 식별자를 프로퍼티 이름으로 (a, b)지정해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;각각의 프로퍼티를 위한 메모리 공간은 또 &lt;b&gt;프로퍼티에 저장된 값을 가리키는 메모리 공간의 주소값을 저장&lt;/b&gt;해야한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;즉 참조형은 변수 obj 는 데이터 그룹(여러개의 프로퍼티)이 할당된 주소를 값으로 가지는 메모리 영역의 주소를 값으로 가진다. 그리고 그 데이터 그룹이 저장된 메모리 영역 역시, 해당 프로퍼티들의 값이 저장된 메모리 영역의 주소를 값으로 가진다. (..복잡..)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참조형은 가변한가 ?&lt;/span&gt;&lt;/h3&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;var obj = {
    a:1,
    b: 'abc'
}
obj.a = 2; &lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참조형이 '가변하다'라는 것은 &lt;b&gt;내부의 프로퍼티 값을 바꿀 수 있기 때문&lt;/b&gt;이다. 하지만 프로퍼티 값을 바꾸는 것 역시 &lt;b&gt;결국 프로퍼티가 가리키는 주소를 바꾼다는 것&lt;/b&gt;이므로, 주관적인 해석으로는 완벽히 가변하다고 할 수 없을것이라 생각한다. 물론 객체 전체로 봤을 때, 프로퍼티 값만 변경 할 수 있으므로 객체의 입장에서는 가변하다.&lt;br /&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 obj의 프로퍼티 a의 값을 바꾼 다는 것은 a가 가리키고 있는 값의 주소를 변경한다는 것이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;var obj = {
    a:1,
    b: 'abc'
}

obj = { a: 5, b :'def'}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같이 변수 obj 자체를 바꿔버리면 어떻게 될까. 이는 결국 기본형에서 변수 값을 변경하는 것과 같은 동작을 띈다. 기존의 obj가 가리키고 있던 obj 변수 -&amp;gt; 데이터 그룹 - &amp;gt; 프로퍼티 - &amp;gt; 값 ...을 변경하는게 아니라, 아예 새로운 메모리 영역을 생성하여 obj에 할당하게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;- 잘못된 부분이나 궁금한점 있으시면 언제든 댓글 환영입니다 &lt;/span&gt;&lt;/p&gt;</description>
      <category>programming language/JavaScript</category>
      <category>JavaScript</category>
      <category>자바스크립트</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/134</guid>
      <comments>https://moonheekim-code.tistory.com/134#entry134comment</comments>
      <pubDate>Fri, 24 Jul 2020 09:16:16 +0900</pubDate>
    </item>
    <item>
      <title>[ node js - express 쇼핑몰 웹] 6. Cart 와 Order 수량 변경 기능 구현  / Cart 내에서 체크한 항목만 Order하는 기능 구현</title>
      <link>https://moonheekim-code.tistory.com/131</link>
      <description>&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  2020 07 22&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart와 Order에서 수량 조절 기능을 구현&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제까지는 수량조절하려면 수동으로 해당 Product를 다시 add to cart 혹은 order해야 했는데 이를 개선함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart에서 check한 Product만 Order로 넘어가도록 수정함&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이제까지는 Cart에 있는 모든 Product를 Order로 넘길 수 밖에 없었는데 이를 개선함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart / Order 수량조절&lt;/span&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;수량 조절 버튼에 onClick 이벤트가 발생하면 해당 product의 수량을 변경하는 라우팅으로 fetch시키는 자바스크립트 함수가 실행되도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 get 방식으로 전송하므로 product Id와 변경될 수량 (qty) 는 각각 파라미터와 쿼리에 담아서 전달해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전달받은 데이터를 통해 user 모델의 cart에서 해당 상품의 수량을 변경시켜준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;cart view 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;li class=&quot;cart__item&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; value=&quot;&amp;lt;%= p.productId._id %&amp;gt;&quot; name=&quot;productId&quot;&amp;gt;
     &amp;lt;input type=&quot;hidden&quot;  name=&quot;_csrf&quot;  value=&quot;&amp;lt;%= csrfToken%&amp;gt;&quot;&amp;gt;
     &amp;lt;label for=&quot;qty&quot;&amp;gt;quantity: &amp;lt;/label&amp;gt;
     &amp;lt;input type=&quot;number&quot; name=&quot;qty&quot; value=&quot;&amp;lt;%= p.quantity%&amp;gt;&quot; onclick=&quot;cartChangeQty(this)&quot;&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; class=&quot;orderCheck&quot; name=&quot;order&quot; checked=&quot;true&quot; onclick=&quot;orderChecked(this)&quot;&amp;gt;
     &amp;lt;button class=&quot;btn danger&quot; type=&quot;button&quot; onclick=&quot;deleteFromCart(this)&quot;&amp;gt;Delete&amp;lt;/button&amp;gt;
&amp;lt;/li&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 라우팅으로 fetch 시키는 코드 : this를 매개변수로 받아옴으로써 hidden 값으로 넘겨받은 인자들에 접근 가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const cartChangeQty=(btn)=&amp;gt;{ // 데이터베이스에 수량 바꾸기 등록 
    const prodId = btn.parentNode.querySelector('[name=productId]').value;
    const qty=btn.parentNode.querySelector('[name=qty]').value;
    fetch('/cart-qty/'+prodId+'/?qty='+qty, { // 파라미터로 prod Id넘겨주고 쿼리로 qty넘겨주기 
        method:'GET'
    })
    .then(result=&amp;gt;{
        return result.json(); //전달받은 json 데이터
    })
    .then(data=&amp;gt;{
        console.log(data); // json데이터 콘솔에 띄우기, 성공이면 success 실패면 fail
    }).catch(err=&amp;gt;next(err));
    ;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/cart-qty/:productId get요청을 처리하는 라우터 &lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.cartChangeQty =(req,res,next)=&amp;gt;{ // 카트 qty 변경 
    const productId = req.params.productId; // 파라미터로 받아온 productId
    const qty = req.query.qty; // 쿼리로 받아온 qty 
    req.user.changeQty(productId,qty)  // 현재 로그인된 유저의 cart 변경 
    .then(result=&amp;gt;{
        res.status(200).json({message:'success'});
    })
    .catch(err=&amp;gt;{
        res.status(500).json({message:'fail'});
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;User 모델의 cart 내 해당 아이템 수량 변경 메서드 : 받아온 product Id에 해당하는 인덱스를 뽑은 후, 해당 인덱스의 qty를 변경해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;userSchema.methods.changeQty=function(productId,qty){
    const cartProductIndex= this.cart.items.findIndex(cp=&amp;gt;{
        return cp.productId.toString()==productId.toString();
    });
    this.cart.items[cartProductIndex].quantity=qty;
    return this.save();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart에서 check 한 product만 Order에 추가하기&lt;/span&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 cart에 담긴 모든 Product의 check 값을 true로 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;user모델의 해당 cart item 영역의 unorderd를 추가해주어 해당 product가 주문 리스트에 올랐는지 확인해주도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 checkbox의 onclick 이벤트에 자바스크립트 함수를 등록한다. 자바스크립트 함수는 checked == true라면 unorderd를 undefined로 변경 or 유지 해주고, checked == false 라면 true로 변경해주는 라우팅으로 fetch 해준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Order를 생성 할 때, cart에 있는 product중 unorderd 가 undefined 인 product만 order로 옮겨준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart를 비울 때, cart에 있는 product중 unorderd가 true 인 product만 남겨준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Cart view 코드&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;&amp;lt;li class=&quot;cart__item&quot;&amp;gt;
    &amp;lt;input type=&quot;hidden&quot; value=&quot;&amp;lt;%= p.productId._id %&amp;gt;&quot; name=&quot;productId&quot;&amp;gt;
     &amp;lt;input type=&quot;hidden&quot;  name=&quot;_csrf&quot;  value=&quot;&amp;lt;%= csrfToken%&amp;gt;&quot;&amp;gt;
     &amp;lt;label for=&quot;qty&quot;&amp;gt;quantity: &amp;lt;/label&amp;gt;
     &amp;lt;input type=&quot;number&quot; name=&quot;qty&quot; value=&quot;&amp;lt;%= p.quantity%&amp;gt;&quot; onclick=&quot;cartChangeQty(this)&quot;&amp;gt;
    &amp;lt;input type=&quot;checkbox&quot; class=&quot;orderCheck&quot; name=&quot;order&quot; checked=&quot;true&quot; onclick=&quot;orderChecked(this)&quot;&amp;gt;
     &amp;lt;button class=&quot;btn danger&quot; type=&quot;button&quot; onclick=&quot;deleteFromCart(this)&quot;&amp;gt;Delete&amp;lt;/button&amp;gt;
&amp;lt;/li&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 라우팅으로 fetch 시키는 코드 : productId와 unorderd 결과를 각각 파라미터와 쿼리에 담아서 전달해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;const orderChecked=(btn)=&amp;gt;{
    const prodId=btn.parentNode.querySelector('[name=productId]').value;
    const orderd=btn.parentNode.querySelector('[name=order]').checked; // 체크되었으면 true, 안되었으면 false
    fetch('/cart-orderd/'+prodId+'/?orderd='+orderd, {
        method:'GET',
    }).then(result=&amp;gt;{
        console.log(result);
        return result.json();
    }).then(data=&amp;gt;{
        console.log(data);
    }).catch(err=&amp;gt;console.log(err));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;/cart-orderd/:productId get요청을 처리하는 라우터&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.cartOrderd=(req,res,next)=&amp;gt;{ // orderd or not check해주기 --&amp;gt; order 에 들어갈 cart목록 체크 
    const productId = req.params.productId;
    const orderd = req.query.orderd;
    req.user.orderCheck(productId,orderd) // 현재 로그인된 유저 모델의 카트에 결과를 반영해준다.
    .then(result=&amp;gt;{
        res.status(200).json({message:'success'});
    })
    .catch(err=&amp;gt;{
        console.log(err);
        res.status(500).json({message:'fail'});
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;User 모델에서 unorderd를 등록해주는 메서드 : 매개변수로 받은 orderd가 true이면 , 즉 주문 리스트에 포함되면 cart 내 unorderd항목을 undefined로 변경해주고, orderd가 false라면 true로 변경해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;userSchema.methods.orderCheck=function(productId,orderd){ // order에 들어갈 product인지 등록해준다.
    const cartProductIndex=this.cart.items.findIndex(cp=&amp;gt;{
        return cp.productId.toString()==productId.toString(); // 해당 product의 인덱스 찾기 
    })
    if(orderd=='true'){ // orderd에 들어갈 product라면 
        this.cart.items[cartProductIndex].unorderd=undefined;
    }
    else{ // orderd에 들어가지 않을 product라면 
        this.cart.items[cartProductIndex].unorderd='true'
    }
    return this.save();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Order에 추가해주는 post 라우터 : 해당 메서드의 req.user의 cart.items를 매개변수로 전달해준다. 원래는 req.user._id만 전달해줘서 해당 메서드에서 다시 User.findOne()메서드를 통해서 user를 찾았는데, 어차피 로그인 된 유저의 cart정보이므로 굳이 해당 User 모델을 찾을 필요 없을 것이라 생각하여 req.user로 접근하여 cart.items를 전달해주는 방향으로 바꾸었다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;exports.postAddToOrder=(req,res,next)=&amp;gt;{ // Cart에서 Order로 추가 
    Order.findOne({'user.userId':req.user._id})
    .then(order=&amp;gt;{
        if(!order){
            order=new Order({ // 해당  user의 order가 없다면 생성해주기 
                products: {items:[]},
                user:{userId:req.user._id}
            });
        }
        order.addOrder(req.user.cart.items) // order 추가 
        .then(result=&amp;gt;{
            req.user.adjustCart(); // 카트 비우기 
            res.redirect('/orders');
        })
    }).catch(err=&amp;gt;{
        console.log(err);
        const error = new Error(err);
        error.httpStatusCode = 500;
        return next(error);
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Order를 추가해주는 메서드 : 매개변수로 받은 cart items를 반복문으로 보며 unorderd == true인 product는 Order 에서 제외해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;OrderSchema.methods.addOrder=function(cart_items){ // order에 상품 추가- 이미 order에 있는 상품은 수량만 늘리기 
    let updatedOrderItem=[...this.products.items];
    for(item of cart_items){
        if(item.unorderd=='true') continue;// 주문 리스트에 포함되지 않으므로 넘어간다. 
        const itemIndex = this.products.items.findIndex(cp=&amp;gt;{
            return cp.productId.toString()==item.productId.toString();
        })
        let newQuantity = 1;
        if(itemIndex&amp;gt;=0){ // 이미 order에 있는 경우 
            newQuantity+=this.products.items[itemIndex].quantity;
            updatedOrderItem[itemIndex].quantity=newQuantity;
        }
        else{ // 없는 경우 
            updatedOrderItem.push({productId:item.productId, quantity: item.quantity});
        }
    }
    this.products.items = updatedOrderItem;
    return this.save();

}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Order 후에 남은 Cart를 재정비해주는 메서드 : 주문 리스트에 올라가지 않은 product만 남겨주도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;userSchema.methods.adjustCart=function(){ // 카트에서 order로 넘겨준후 모두 지우지 않고 checked된 product만 지우기 
    // undefined 아닌것만 남겨주기
    let updatedCartitems = this.cart.items.filter(item=&amp;gt;{
        return item.unorderd=='true' // true로 표시된 prodcut만 남겨주기 
    });
    this.cart.items=updatedCartitems;
    return this.save();
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Project/첫번째 프로젝트 쇼핑몰 웹</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/131</guid>
      <comments>https://moonheekim-code.tistory.com/131#entry131comment</comments>
      <pubDate>Wed, 22 Jul 2020 23:13:36 +0900</pubDate>
    </item>
    <item>
      <title>[ node js - express 쇼핑몰 웹] 5. Seller 등록, 취소 구현</title>
      <link>https://moonheekim-code.tistory.com/130</link>
      <description>&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;  2020 07 21&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Seller 등록을 구현하여 Seller로 등록된 유저에게만 Admin Product와 add Product 항목이 보이도록 수정함&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Amazon 웹에서 착안한 아이디어 !&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Seller 등록 및 취소 flow&lt;/span&gt;&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;login이 되었을 때만 가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Seller가 되기 위해서는 Seller name을 입력하도록 한다. 입력한 Seller name은 해당 User 데이터베이스에 저장된다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Seller인 회원에게는 Seller를 취소할 것인가에 대한 항목을 보여준다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;취소하기위해서는 비밀번호로 인증해야한다. Seller를 취소하면 그동안 post 했던 모든 Product가 사라진다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;isLoggedIn과 함께 isSeller도 req.locals에 저장해준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arcade&quot;&gt;&lt;code&gt;app.use((req,res,next)=&amp;gt;{
   res.locals.isAuthenticated = req.session.isLoggedIn;
   res.locals.isSeller=undefined; // 먼저 undefined 할당해준다.
   if(req.user){ // 로그인이 되었다면 
    res.locals.isSeller = req.user.Seller; } // 로그인한 유저의 Seller정보로 할당해준다.
   res.locals.csrfToken=req.csrfToken();
   next();
});&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;seller authentication 라우팅 : seller만 접근할 수 있는 add product, admin product로의 접근을 차단한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;exports.sellerCheck =(req,res,next)=&amp;gt;{
    if(!req.session.isLoggedIn){
        return res.redirect('/login');
    }
    else if(!req.user.Seller){
        return res.redirect('/sell');
    }
    next();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;seller 등록 post 라우팅 : 유저 데이터에 입력받은 sellerName을 저장해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postSell=(req,res,next)=&amp;gt;{
    const sellerName = req.body.sellerName;
    User.findById(req.user._id)
    .then(user=&amp;gt;{
        user.Seller=sellerName; // 유저 데이터에 저장 
        return user.save();
    })
    .then(result=&amp;gt;{
        res.redirect('/admin/add-product'); 
        sendMail(email,'you are a Seller now!', SellerMessage); // 이메일 전송 
    })
    .catch(err=&amp;gt;next(err));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;seller 취소 post 라우팅 : 먼저 비밀번호를 확인하고 비밀번호가 맞을 경우 Seller를 다시 undefined로 수정한뒤, 해당 user가 등록한 모든 Product를 삭제해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postSellDelete=(req,res,next)=&amp;gt;{ // seller 취소 
    const password=req.body.password;
    User.findById(req.user._id)
    .then(user=&amp;gt;{
        return bcrypt.compare(password,user.password) // 비밀번호 확인 
        .then(doMatch=&amp;gt;{ 
            if(doMatch){ // 비밀번호 맞을 경우
                user.Seller=undefined;
                return user.save()
                .then(result=&amp;gt;{
                    return Product.deleteMany({userId:user._id}); // 해당 유저가 등록한 상품 모두 삭제 
                })
            }
        })
    })
    .then(result=&amp;gt;{
        res.redirect('/sell');
    })
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/첫번째 프로젝트 쇼핑몰 웹</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/130</guid>
      <comments>https://moonheekim-code.tistory.com/130#entry130comment</comments>
      <pubDate>Wed, 22 Jul 2020 22:36:28 +0900</pubDate>
    </item>
    <item>
      <title>[node js  express] Multer를 이용하여 웹에 이미지 업로드 기능 추가</title>
      <link>https://moonheekim-code.tistory.com/129</link>
      <description>&lt;h1&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Multer를 이용하여 웹에 이미지 업로드 기능 추가하기&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/h1&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. Multer 패키지 설치&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;npm install --save multer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;2. 이미지 업로드 기능을 추가하고자 하는 view에 아래와 같이 추가해준다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;enctype = &quot;multipart/form-data&quot;&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;enctype은 POST로 파일이나 용량이 큰 데이터를 전송할 때 전송되는 데이터의 인코딩 방식을 지정한다. 전송되는 파일은 데이터 크기가 일정하지 않고 여러 복잡한 정보가 필요하다보니 URL 인코딩 방식 (application/x-www-urlencoded)이 부적합하다. 이 때 &quot;multipart/form-data&quot;로 인코딩 방식을 적용해줌으로써 폼 태그 내에 있는 파일의 데이터를 전송 할 수 있게 된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;type 이 file 인 input 을 생성해주어 파일을 업로드 받는다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; &amp;lt;form class=&quot;product-form&quot; action=&quot;/admin/add-product&quot; method=&quot;POST&quot; enctype=&quot;multipart/form-data&quot;&amp;gt; 
     &amp;lt;div class=&quot;form-control&quot;&amp;gt;
          &amp;lt;label for=&quot;image&quot;&amp;gt;Image&amp;lt;/label&amp;gt;
          &amp;lt;input type=&quot;file&quot;name=&quot;image&quot; id=&quot;image&quot; &amp;gt;
     &amp;lt;/div&amp;gt;
 &amp;lt;/form&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;3. 엔트리 파일에&amp;nbsp; multer 미들웨어를 추가해준다.&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 때 multer 프로퍼티를 통해서 저장방식이나 저장하는 파일을 필터링 할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 fileStorage 는 저장 방식 ( 저장되는 공간 , 저장되는 이름) 을 지정하고 있고 fileFilter는 mimetype에 따라서 저장 할 것인지 안할 것인지를 구분 하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt; const multer = require('multer');
 const fileStorage = multer.diskStorage({ // 저장 방식
     destination: (req,file,cb)=&amp;gt;{ // 저장되는 곳 지정 
         cb(null, 'images');
     },
     filename: (req,file,cb)=&amp;gt;{ // 저장되는 이름 지정 
         cb(null, file.filename+'-'+file.originalname);
     }
 });

 const fileFilter = (req,file,cb) =&amp;gt; { // 확장자 필터링 
     if(file.mimetype === 'image/png' || file.mimetype === 'image/jpg' || file.mimetype === 'image/jpeg'){
         cb(null,true); // 해당 mimetype만 받겠다는 의미 
     }
     else{ // 다른 mimetype은 저장되지 않음 
         cb(null,false);
     }
 };
 app.use(multer({storage :fileStorage, fileFilter:fileFilter}).single('image')); // 라우터 &lt;/code&gt;&lt;/pre&gt;
&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 업로드 된 이미지를 &lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터베이스에 저장하기&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저 post request로 담아온 데이터를 추출해준다&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;const image =req.file;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;​ 이렇게 추출된 파일 데이터는 아래와 같은 형식을 가진다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;{
  fieldname: 'image',
  originalname: '72a4daa7ab57cdf086fbb85287c1a7b2.jpg',
  encoding: '7bit',
  mimetype: 'image/jpeg',
  destination: 'images',
  filename: 'undefined-72a4daa7ab57cdf086fbb85287c1a7b2.jpg',
  path: 'images\\undefined-72a4daa7ab57cdf086fbb85287c1a7b2.jpg',
  size: 732453
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이 중에서 데이터베이스에 저장할때 유의미한 데이터는 현재 이미지 리소스가 저장된 곳을 나타내는 path이다. 따라서 받아온 파일을 데이터베이스에 저장할 때에는 path를 추출해서 저장해주도록 한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;    const imageUrl = image.path; // db 저장용 
       const product = new Product({
           title:title,
           price:price,
           description:description,
           imageUrl:imageUrl,
           userId:req.user._id
       });&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이렇게 데이터에 path를 담으면 html 페이지에서 어떻게 해당 이미지를 불러올 수 있을까 ? 이는 CSS파일을 html 파일에 정적으로 제공하는 것과 같은 방식으로 해주면 된다. 즉, 엔트리 파일에서 해당 이미지 폴더를 정적으로 제공해주는 미들웨어를 추가해주면 된다. 아래와 같이 추가하면 데이터베이스에 저장된 path를 통해서 이미지가 제대로 전달 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;app.use('/images',express.static(path.join(__dirname,'images')));&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>web : back-end/node js</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/129</guid>
      <comments>https://moonheekim-code.tistory.com/129#entry129comment</comments>
      <pubDate>Mon, 20 Jul 2020 13:04:18 +0900</pubDate>
    </item>
    <item>
      <title>[ node js - express 쇼핑몰 웹 ] 4. input data validation 추가</title>
      <link>https://moonheekim-code.tistory.com/128</link>
      <description>&lt;h3&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 2020 07 19&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;log in, sign up, add product, edit product에 input data에 대한 validation을 추가했음&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;express validator third party package 사용&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;500 error page 구현&lt;/span&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;데이터베이스 문제 혹은 permission 문제에 대해 stuck 되지 않고 500 error 페이지로 보내줌&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨ Login Validation&lt;/span&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원래는 user databse를 탐색하여 user id 일치여부, password 일치 여부를 확인해주는 custom validation도 라우터에서 함께 구현하였으나 , 어차피 컨트롤러에서 로그인된 user 를 session에 저장하는 과정에서 Userdatabse. findOne('user') 연산이 들어가서 해당 validation은 컨트롤러로 옮겨줌.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;auth.js ( routes)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;const { body } = require('express-validator/check');
router.post('/login', 
[
    body('email').isEmail().withMessage('email을 입력해주세요') // email 형식 검사
    body('password').isLength({min:6, max:12}).withMessage('비밀번호를 잘못 입력하셨습니다.') // 비밀번호 형식 검사 
],authController.postLogin);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;auth.js (controller)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;const { validationResult } = require('express-validator/check');
exports.postLogin=(req,res,next)=&amp;gt;{
    const email = req.body.email;
    const password=req.body.password;
    const error = validationResult(req);
    if(!error.isEmpty()){  // error 가 넘어온 경우 
        return res.status(422).render('auth/login',{ //다시 login 페이지로 렌더링해줌 
            path:'/login',
            pageTitle:'login',
            ErrorMessage : error.array()[0].msg, // 에러메시지 
            oldInput : {email: email, password: password}, // form에 old Input을 value로 띄워주기 위해서
            validationError: error.array() // input data중 어느곳에 error가 있는지 알려줘서, error가 있는 곳에 css class 적용하여 붉은색 border color 설정 
        });
    }
    User.findOne({email:email})
    .then(user=&amp;gt;{
        if(!user){  // 이메일 불일치 
            return res.status(422).render('auth/login', {  
                path:'/login',
                pageTitle:'login',
                ErrorMessage : '입력하신 email은 존재하지 않습니다.',
                oldInput : {email: email, password: password},
                validationError:[{param:'email'}]
            })
        }
        bcrypt.compare(password,user.password)
        .then(doMatch=&amp;gt;{
            if(!doMatch){ // 비밀번호 불일치 
                    res.status(422).render('auth/login', {
                    path:'/login',
                    pageTitle:'login',
                    ErrorMessage : '비밀번호가 일치하지 않습니다.',
                    oldInput : {email: email, password: password},
                    validationError:[{param:'password'}]
                });
            }
            else{ // 이메일과 비밀번호 일치 ==&amp;gt; 로그인 성공! 
                req.session.isLoggedIn= true; // 로그인 되었음 
                req.session.user = user; // 유저 정보 저장 
                req.session.save(err=&amp;gt;{ // 세션 저장 
                console.log(err);
                res.redirect('/'); // password 체크하는 flow 추가해줘야함 
                })
            }
        })
    })
    .catch(err=&amp;gt;{
        const error = new Error(err);
        error.httpStatusCode = 500;
        return next(error);
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨ Sign up Validation&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;sign up validation에서는 로그인과 같이 email, password형식을 검사한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;email custom validator를 추가하여 User database를 탐색하여 이메일 중복 여부를 검사한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;confirmPassword custom validator 를 추가하여 2차 비밀번호 일치 여부를 검사한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;auth.js (routes)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot;&gt;&lt;code&gt;router.post('/signUp', [
    body('email').isEmail().withMessage('올바른 email을 입력해주세요') // 이메일 형식 검사 
    .custom((value, {req})=&amp;gt;{ // custom validator : 해당 이메일이 이미 존재하는지 여부 
        return User.findOne({email:value})
        .then(user=&amp;gt;{
            if(user){
               return Promise.reject('이미 존재하는 이메일 입니다.'); 
            };
        }).catch(err=&amp;gt;{
        const error = new Error(err);
        error.httpStatusCode = 500;
        return next(error);
    });
    }).normalizeEmail(), // 소문자로 변경 

    // 비밀번호 형식 검사
    body('password').isAlphanumeric().withMessage('비밀번호는 숫자와 문자로만 생성가능합니다.') 
    .isLength({min:6, max:12}).withMessage('비밀번호는 최소 6자 최대 12자 입니다.').trim(),          
    body('confirmPassword').custom((value,{req})=&amp;gt;{ // custom validator : 2차 비밀번호 일치 여부 
        if(value !== req.body.password){
           throw new Error('비밀번호가 일치하지 않습니다.');
        }
        else return true;
    }).trim()
],authController.postSignUp);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;auth.js (controller)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postSignUp=(req,res,next)=&amp;gt;{
    const email = req.body.email;
    const password = req.body.password;
    const error = validationResult(req);
    if(!error.isEmpty()){ // error가 넘어온 경우 
        return res.status(422).render('auth/signUp',{
            path:'/signUp',
            pageTitle:'sign up',
            ErrorMessage : error.array()[0].msg,
            oldInput : {email: email, password: password},
            validationError: error.array()
        });
    }
// routes에서 모든 검사를 마쳤으니 바로 db에 저장해준다
    bcrypt.hash(password,12)
    .then(hashedPassword=&amp;gt;{ // hashed password로 변경
        const user = new User({
            email:email,
            password:hashedPassword,
            cart:{items:[]}
        }); // 새로운 User 데이터 저장 
        user.save()
        .then(result=&amp;gt;{
            res.redirect('/login');
            sendMail(email,'welcome to Amadoo!', 
            welcomeMessage);
        })
    }).catch(err=&amp;gt;{
        const error = new Error(err);
        error.httpStatusCode = 500;
        return next(error);
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨ Add product &amp;amp;&amp;amp; Edit Product Validation&lt;/span&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;add와 edit 모두 제목, 이미지 url, 가격, 설명에 대한 형식 검사 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;rendering 시 isError 데이터를 넘겨주어서 isError == true 이거나 editing == true 일 때 input value를 띄워주도록 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;admin.js (routes )&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;router.post('/add-product', AuthRouting,[
body('title').isString().withMessage('제목을 올바르게 입력해주세요.') // 제목 형식 검사
.isLength({min:2,max:15}).withMessage('제목은 2글자 이상, 15글자 이하여야 합니다.').trim(),
body('imageUrl').isURL().withMessage('올바른 형식의 이미지 주소를 입력해주세요.').trim(), // 이미지 url 형식 검사 
body('price').isNumeric().withMessage('숫자만 입력해주세요'), // 가격 형식 검사 
body('description').isLength({min:5, max:100}).withMessage('상품 설명은 최소 5글자여야합니다.') // 설명 형식 검사 
] ,
AdminController.postAddProduct);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;admin.js (controller)&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;maxima&quot;&gt;&lt;code&gt;exports.postAddProduct=(req,res,next)=&amp;gt;{
    const title = req.body.title;
    const imageUrl =req.body.imageUrl;
    const price = req.body.price;
    const description=req.body.description;
    const error = validationResult(req);
    if(!error.isEmpty()){ // error가 넘어온 경우 
        return res.status(422).render('admin/add-product',{
            path:'/add-product',
            pageTitle:'add product',
            ErrorMessage : error.array()[0].msg,
            validationError: error.array(),
            editing:false,
            isError:true, // 에러 여부 ==&amp;gt; isError || editing 일 경우 value 띄워주도록 함 
            product:{title:title,imageUrl:imageUrl,price:price,description:description} 
            // value에 띄울 값 product 객체에 담아주기 
        })
    }
    const product = new Product({
        title:title,
        price:price,
        description:description,
        imageUrl:imageUrl,
        userId:req.user._id
    });
    product.save()
    .then(result=&amp;gt;{
        res.redirect('/');
    }).catch(err=&amp;gt;{
        const error = new Error(err);
        error.httpStatusCode = 500;
        return next(error);
    });
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Project/첫번째 프로젝트 쇼핑몰 웹</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/128</guid>
      <comments>https://moonheekim-code.tistory.com/128#entry128comment</comments>
      <pubDate>Sun, 19 Jul 2020 12:26:26 +0900</pubDate>
    </item>
    <item>
      <title>[node js - express 쇼핑몰 웹] 3. 로그인, 로그아웃, 회원가입 구현하기</title>
      <link>https://moonheekim-code.tistory.com/125</link>
      <description>&lt;h3&gt; &lt;span style=&quot;color: #000000;&quot;&gt;2020 07 16&lt;/span&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;login, sign up, 그리고 resetting password 구현&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;sign up과 resetting password에서는 gmail api를 이용하여 user에게 email을 보내도록 함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨how &quot;sign up&quot; works&lt;/span&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자에게 email과 password를 입력받는다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;email이 이미 존재하면 다른 페이지로 리다이렉트 해준다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;email 존재하지 않는다면 새로운 User 모델을 생성하고 데이터베이스에 반영해준다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;sign up이 완료되면 gmail api 를 이용하여 User에게 sign up이 되었다고 알려준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postSignUp=(req,res,next)=&amp;gt;{
 const email = req.body.email;
 const password = req.body.password;
 // 해당 email 이 존재하면 fail
 User.findOne({email:email})
 .then(user=&amp;gt;{
     if(!user){ // 이메일 사용 가능 
         return bcrypt.hash(password,12)
         .then(hashedPassword=&amp;gt;{ // hashed password로 변경
             const user = new User({
                 email:email,
                 password:hashedPassword,
                 cart:{items:[]}
             }); // 새로운 User 데이터 저장 
             user.save()
             .then(result=&amp;gt;{
                 res.redirect('/login');
                 sendMail(email,'welcome to Amadoo!', 
                 `&amp;lt;h1&amp;gt;Thank you for signing in our shopping mall 'AMADOO' &amp;lt;/h1&amp;gt;
                 &amp;lt;p&amp;gt; we hope you'll get bunch of great exprience in our shop&amp;lt;/p&amp;gt;
                 &amp;lt;p&amp;gt; if you have any troubles while shopping, please let us know by this email! &amp;lt;/p&amp;gt;
                 `);
             })
         }).catch(err=&amp;gt;console.log(err));
     }
     else{
         res.redirect('/signUp');
     }
 })
 .catch(err=&amp;gt;console.log(err));
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨how &quot; login &quot; works&lt;/span&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자로부터 email 과 password를 입력받는다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;받은 email로 해당하는 User데이터를 찾고, password를 bcrypt.compare()로 비교해준다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일치할 시 로그인하고 req.session.user에 user를 저장해준다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postLogin=(req,res,next)=&amp;gt;{
const email = req.body.email;
const password=req.body.password;
User.findOne({email:email})
.then(user=&amp;gt;{
    if(!user){ //유저 아이디 맞지 않는 경우 
        res.redirect('/login');
    }
    else{
        bcrypt.compare(password,user.password) 
        .then(doMatch=&amp;gt;{
            if(!doMatch){ // 비밀번호가 맞지 않는 경우 
               res.redirect('/login'); 
            }
            else{
                req.session.isLoggedIn= true; // 로그인 되었음 
                req.session.user = user; // 유저 정보 저장 
                req.session.save(err=&amp;gt;{ // 세션 저장 
                    console.log(err);
                    res.redirect('/'); // password 체크하는 flow 추가해줘야함 
                })
            }
        })
        .catch(err=&amp;gt;console.log(err));
    }
}).catch(err=&amp;gt;console.log(err));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol start=&quot;4&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;어플리케이션 전역에서 User 모델에 접근 할 수 있어야 하므로 req.session.user!== undefined라면 req.user에 해당 User모델을 담아준다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;app.use((req,res,next)=&amp;gt;{
   if(req.session.user){ // session.user가 저장되었다면 == 로그인 되었다면 
       User.findOne(req.session.user._id) // 해당user찾기 
       .then(user=&amp;gt;{
           req.user = user; // req.user에 저장해서 전역에서 접근 가능하도록 
           next();
       })
       .catch(err=&amp;gt;console.log(err));
   }
   else{
       next();
   }
})&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨logout&lt;/span&gt;&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;로그아웃을 하면 session이 destroy 된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;moonscript&quot;&gt;&lt;code&gt;exports.getLogout=(req,res,next)=&amp;gt;{ // 로그아웃 
  req.session.destroy();
  res.redirect('/');
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;&lt;span style=&quot;color: #000000;&quot;&gt;✨how resetting password works&lt;/span&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사용자로부터 resetting password를 할 email을 입력받아 온다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 email로 난수 token을 쿼리값으로 준 path를 전송한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;typescript&quot;&gt;&lt;code&gt;exports.postNewPassword=(req,res,next)=&amp;gt;{ // password 재설정 post
 // 토큰 값을 포함한 path를 email 로 발송해준후 리다이렉트.
 const email = req.body.email;
 crypto.randomBytes(32, (err,Buffer)=&amp;gt;{ // 토큰 생성 
     if(err){
         console.log(err);
         return res.redirect('/new-password');
     }
     const token = Buffer.toString('hex');
     User.findOne({email:email}) // 유저 찾기 
     .then(user=&amp;gt;{
         if(!user){
             return res.redirect('/reset');
         }
         user.resetToken=token; // 해당 유저에 토큰 및 만기일 저장 
         user.resetTokenExpiration= Date.now()+3600000;
         return user.save();
     })
     .then(result=&amp;gt;{
         res.redirect('/');
       // 이메일 보내기 
         return sendMail(email, 'Reset your password', ` 
         follow &amp;lt;a href=&quot;http://localhost:3000/new-password/${token}&quot;&amp;gt;this link&amp;lt;/a&amp;gt; to reset your password `);
     })
     .catch(err=&amp;gt;console.log(err));
 })
}&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 path에는 파라미터 값으로 담아온 토큰을 추출하여 해당 토큰을 저장한 User모델을 찾도록 한다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;User모델을 찾았으면 post request로 새로 입력 받은 password와 함께 토큰 값, User Id를 넘긴다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.getResetPage=(req,res,next)=&amp;gt;{
 const token = req.params.token; // 파라미터
 User.findOne({resetToken:token, resetTokenExpiration:{$gt: Date.now()}}) // User 모델 찾기 
 .then(user=&amp;gt;{
     if(!user){ res.redirect('/');} // User가 존재하지 않는다면 리다이렉트 
     else{
         res.render('auth/reset-password', {
             path:'/reset-password',
             pageTitle:'reset password',
             userId:user._id, 
             resetToken:token
         })
     }
 })
 .catch(err=&amp;gt;console.log(err));
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;post 라우팅에서는 req.body에 담아온 user Id와 token을 통해 해당 User의 token이 아직 만료되지 않았는지 다시한번 확인한다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;만료되지 않았고 정삭적으로 User 모델을 찾으면 req.body에 담아온 password를 hash값으로 변경하여 User모델에 저장해주고 데이터베이스에 반영해준다.&lt;/span&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;exports.postResetPage=(req,res,next)=&amp;gt;{
    const token  = req.body.token;
    const userId= req.body.userId;
    const password =req.body.password;
    let foundUser;
    User.findOne({_id:userId, resetToken:token, resetTokenExpiration:{$gt: Date.now()}})
    .then(user=&amp;gt;{
        if(!user){ // user 존재하지 않음 
            res.redirect('/');
        }
        else{
            foundUser = user;
            return bcrypt.hash(password,12)
            .then(hashedPassword=&amp;gt;{
                foundUser.password=hashedPassword;
                foundUser.resetToken=undefined;
                foundUser.resetTokenExpiration=undefined;
                return foundUser.save();
            })
        }
    })
    .then(result=&amp;gt;{
        res.redirect('/login');
    })
    .catch(err=&amp;gt;console.log(err));
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>Project/첫번째 프로젝트 쇼핑몰 웹</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/125</guid>
      <comments>https://moonheekim-code.tistory.com/125#entry125comment</comments>
      <pubDate>Thu, 16 Jul 2020 15:38:56 +0900</pubDate>
    </item>
    <item>
      <title>nodemailer와 google gmail api로 node js에서 메일 보내기</title>
      <link>https://moonheekim-code.tistory.com/123</link>
      <description>&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;현재 듣고 있는 노드 js 강의에서 진행중인 authentication 수업에서는 node js에서 email 보내는 것을 SendGrid라는 api를 통해 진행했습니다 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만 자꾸 SendGrid에서 제 계정을 block 하는 탓에.. 제대로 이메일 발송을 할 수 없었고 그래서 저는 구글에서 제공하는 gmail api로 변경하여 구현하였습니다 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일단 이를 위해서는 npm과 최신버전 node js가 설치되어있는 환경이 필요 합니다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;0. 먼저 노드 js에 nodemailer를 설치해주세요.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594805356325&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;npm install --save nodemailer&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. 구글 계정 설정 -&amp;gt; 보안 -&amp;gt; 덜 안전한 app 접근 허용을 해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. &lt;a style=&quot;color: #000000;&quot; href=&quot;https://accounts.google.com/DisplayUnlockCaptcha&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;accounts.google.com/DisplayUnlockCaptcha&lt;/a&gt; 에 들어가 continue를 눌러줍니다. 이는 Captcha 프로그램을 일시적으로 unlcok해줍니다. 이걸 하지 않으면 이메일을 발송할 때 error: connect ETIMEDOUT가 떠요&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. 구글 개발자 콘솔에 들어가서 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://console.developers.google.com/apis/dashboard?project=credible-skill-283403&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;console.developers.google.com/apis&lt;/a&gt; 프로젝트를 생성해주세요. 이름은 임의로 지으셔도 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. 그 다음 개발자 콘솔 -&amp;gt; 라이브러리로 가서 Gmail API를 검색해 찾아주시고 활성화 시켜주세요!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목 없음.png&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ewzar4/btqFEj74Eu5/GT17PQMgp993iH0S8AUAMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ewzar4/btqFEj74Eu5/GT17PQMgp993iH0S8AUAMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ewzar4/btqFEj74Eu5/GT17PQMgp993iH0S8AUAMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fewzar4%2FbtqFEj74Eu5%2FGT17PQMgp993iH0S8AUAMk%2Fimg.png&quot; data-filename=&quot;제목 없음.png&quot; data-origin-width=&quot;695&quot; data-origin-height=&quot;376&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 클라이언트와 클라이언트 시크릿을 받기 위해서 사용자 인증정보 탭 -&amp;gt; 사용자 인증정보 만들기 -&amp;gt; OAuth 클라이언트 ID를 선택해주세요&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목 2.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;363&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCH3Mh/btqFIh1Yozl/jXJb4eQ4wBZYMk1C9AjrUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCH3Mh/btqFIh1Yozl/jXJb4eQ4wBZYMk1C9AjrUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCH3Mh/btqFIh1Yozl/jXJb4eQ4wBZYMk1C9AjrUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCH3Mh%2FbtqFIh1Yozl%2FjXJb4eQ4wBZYMk1C9AjrUk%2Fimg.png&quot; data-filename=&quot;제목 2.png&quot; data-origin-width=&quot;855&quot; data-origin-height=&quot;363&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. 아래와 같이 어플리케이션 타입을 웹 어플리케이션으로 정해주시고 승인된 리다이렉션 URI 에는 구글 OAuth의 플레이 그라운드의 주소를 입력해주셔야 합니다.&amp;nbsp; 그러지 않으면 나중에 본인의 개발자 콘솔과 연동이 되지 않습니다. &lt;a style=&quot;color: #000000;&quot; href=&quot;https://developers.google.com/oauthplayground&quot;&gt;https://developers.google.com/oauthplayground&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목3.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;746&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brGkXQ/btqFHabcqtu/TzX0xXhVgUdlQQxQA5MkcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brGkXQ/btqFHabcqtu/TzX0xXhVgUdlQQxQA5MkcK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brGkXQ/btqFHabcqtu/TzX0xXhVgUdlQQxQA5MkcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrGkXQ%2FbtqFHabcqtu%2FTzX0xXhVgUdlQQxQA5MkcK%2Fimg.png&quot; data-filename=&quot;제목3.png&quot; data-origin-width=&quot;638&quot; data-origin-height=&quot;746&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 이렇게 생성하면 클라이언트 ID와 클라이언트 시크릿이 생성됩니다. 그러면 이를 복사해줍니다. ( JSON 파일로도 저장해놓을 수 있습니다.)&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;8. 이제 인증을 위해서 refresh token과 access token을 발급받아야 합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://developers.google.com/oauthplayground&quot;&gt;https://developers.google.com/oauthplayground&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위의 주소로 가시면 오른쪽 상단에 보이는 톱니바퀴 모양을 누릅니다. 그리고 Use your own OAuth credentials를 체크하고 앞서 발급받은 클라이언트 ID와 클라이언트 시크릿을 입력해주고 close 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목4.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;732&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buNHDR/btqFGM9uq9a/hRV1ftHV3N3zQWZI2lojT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buNHDR/btqFGM9uq9a/hRV1ftHV3N3zQWZI2lojT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buNHDR/btqFGM9uq9a/hRV1ftHV3N3zQWZI2lojT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuNHDR%2FbtqFGM9uq9a%2FhRV1ftHV3N3zQWZI2lojT1%2Fimg.png&quot; data-filename=&quot;제목4.png&quot; data-origin-width=&quot;604&quot; data-origin-height=&quot;732&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;9.&amp;nbsp; 그리고 왼쪽의 Select &amp;amp; Authorize API 항목을 클릭한 후 gmail api를 찾아주고 Authorize API를 클릭해줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그러면 본인의 계정의 개발자 콘솔과 연동되어서 진행이 될 것이고, 신뢰할 수 없는 앱이라는 경고창이 뜰 것입니다. 이 때 그냥 무시하고 진행해주세요.&amp;nbsp; &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목5.png&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;706&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Df55a/btqFGOfblNb/kLbZ87kllohWzZDn291Fa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Df55a/btqFGOfblNb/kLbZ87kllohWzZDn291Fa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Df55a/btqFGOfblNb/kLbZ87kllohWzZDn291Fa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDf55a%2FbtqFGOfblNb%2FkLbZ87kllohWzZDn291Fa1%2Fimg.png&quot; data-filename=&quot;제목5.png&quot; data-origin-width=&quot;615&quot; data-origin-height=&quot;706&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 개발자 콘솔과 연동되지 않는다면 개발자 콘솔로 돌아가서 OAuth 클라이언트 정보를 다시 확인해주세요! &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;특히나 승인된 리다이렉션 URI에 구글 OAuth 플레이 그라운드의 주소를 넣었는지 잘 확인해주세요! ( 대쉬'/' 하나라도 더 들어가면 mismatch 됩니다) &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10. 이제 step 2에 들어와서 Exchange authorization code for tokens 를 눌러주시면 refresh token 과 access token 이 생성됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-filename=&quot;제목 없음 6.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;679&quot; data-ke-mobilestyle=&quot;widthContent&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhxvjV/btqFIGmUMQk/Ak1Fzjl7wP9DbxIKp9Sc30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhxvjV/btqFIGmUMQk/Ak1Fzjl7wP9DbxIKp9Sc30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhxvjV/btqFIGmUMQk/Ak1Fzjl7wP9DbxIKp9Sc30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhxvjV%2FbtqFIGmUMQk%2FAk1Fzjl7wP9DbxIKp9Sc30%2Fimg.png&quot; data-filename=&quot;제목 없음 6.png&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;679&quot; data-ke-mobilestyle=&quot;widthContent&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;11. 이제 노드 js 파일에 아래와 같이 구현을 해주세요&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1594789994761&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;const nodemailer = require('nodemailer');

const sendMail = async (to, subject, html) =&amp;gt; {
    const googleTransporter = await nodemailer.createTransport({
        host:  'smtp.gmail.com',
        port: 465, 
        secure: true,
        auth:{
            type:'OAuth2',
            user: '&amp;lt;본인의 구글 계정  &amp;gt;',
            clientId:'&amp;lt;발급받은 client ID&amp;gt;',
            clientSecret:'&amp;lt;발급받은 client secret&amp;gt; ',
            refreshToken: '&amp;lt;발급받은 refreshtoken&amp;gt;',
            accessToken: '&amp;lt;발급받은 accessToken&amp;gt;',
            expirse: 3600
        }
    }),
    mailOptions = {
        from: '&amp;lt;보내는 사람 이름, &amp;lt;test@test.gmail.com&amp;gt;&amp;gt;',
        to,
        subject,
        html
    }
    try{
        await googleTransporter.sendMail(mailOptions);
        googleTransporter.close();
        console.log(`mail have sent to ${ to }`);
    } catch(err){
        console.log(err);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1594790038652&quot; class=&quot;javascript&quot; data-ke-language=&quot;javascript&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;sendMail('randomuser@gmail.com', 'Welcome to our shop!', '&amp;lt;p&amp;gt;hi!&amp;lt;/p&amp;gt;');&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 이렇게 보내면 됩니다 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참조 : &lt;a style=&quot;color: #000000;&quot; href=&quot;https://blog.eunsatio.io/develop/nodemailer%EC%99%80-gmail%EB%A1%9C-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1%ED%95%98%EA%B8%B0-%E3%85%A1-OAuth2&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;blog.eunsatio.io/develop/nodemailer%EC%99%80-gmail%EB%A1%9C-%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1%ED%95%98%EA%B8%B0-%E3%85%A1-OAuth2&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;span style=&quot;color: #000000;&quot;&gt;메일보낼때 ETIMEDOUT 에러가 계속 뜬다면 아래 포스팅을 참조해보세요! firewall의 문제일 수도 있어서 포트 번호를 바꾸거나 PC를 재부팅하면 해결된다기도 합니다. 저도 이 에러가 계속 떴는데, 저는 captcha 를 제대로 unblock 하지 않은 문제였습니다 &lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nodemailer/nodemailer/issues/749&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;github.com/nodemailer/nodemailer/issues/749&lt;/a&gt;&lt;/p&gt;</description>
      <category>web : back-end/node js</category>
      <author>무니웜테일패드풋프롱스</author>
      <guid isPermaLink="true">https://moonheekim-code.tistory.com/123</guid>
      <comments>https://moonheekim-code.tistory.com/123#entry123comment</comments>
      <pubDate>Wed, 15 Jul 2020 14:18:18 +0900</pubDate>
    </item>
  </channel>
</rss>