<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>wklee0607_.log</title>
        <link>https://velog.io/</link>
        <description>github: https://github.com/WKlee0607</description>
        <lastBuildDate>Tue, 14 Feb 2023 13:42:59 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>wklee0607_.log</title>
            <url>https://velog.velcdn.com/images/wklee0607_/profile/12b474c3-32fd-4209-8f92-52d470341f34/social_profile.png</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. wklee0607_.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/wklee0607_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[7. [KhuCv] - FaceTracking, Performance Improvement.]]></title>
            <link>https://velog.io/@wklee0607_/7.-KhuCv-FaceTracking-Performance-Improvement</link>
            <guid>https://velog.io/@wklee0607_/7.-KhuCv-FaceTracking-Performance-Improvement</guid>
            <pubDate>Tue, 14 Feb 2023 13:42:59 GMT</pubDate>
            <description><![CDATA[<h1 id="1-performance-improvement">1. Performance Improvement.</h1>
<h2 id="1-1-facetracking-improvement-요약">1-1. FaceTracking Improvement 요약.</h2>
<pre><code class="language-cpp">if(maxSimilarity &gt; 0.945) identified = true;
else if(maxSimilarity &gt; 0.93 &amp;&amp; maxTracker-&gt;UnTracked &gt; 5) identified = true;</code></pre>
<p>이전 코드(6번 게시물 참조 - 위의 코드처럼)는 수치를 코드에 넣음으로써, 일반적인 경우에 적용하기 힘든 코드였다. 또한 FaceSimilarity가 mobile_net DNN중 사람들간의 얼굴을 구별해주는 DNN이 아니라 사람, 개, 고양이 등 종류를 분류해주는 기능을하는 DNN파일이다. 따라서 해당 FaceSimilariry의 수치를 이용하면 정확도가 많이 떨어져 제대로 매칭되지 않는 경우가 허다했다. 이를 보완하고자, FaceSimilariry 수치는 이용하지 않으면서 가장 유사도가 높은 FaceRect가 매칭되도록 코드를 작성하였다.</p>
<p>전체적으로 첫번째 Tracking은 IOU판별에 의해 이뤄지며, 여러 FaceRect가 Face에 겹칠 경우, Face와 가장 유사도가 높은 FaceRect가 Face에 부여된다.</p>
<p>만약 IOU가 겹치는 FaceRect가 없다면, 일정 거리를 만족하는 FaceRect중 가장 유사도가 높은 것을 Face에 매칭해준다. 여기서 일정거리는 해당 Face와 FaceRect 사이의 거리가 FaceRect의 offset길이와 UnTracked된 횟수 만큼 곱한 값보다 작아야 한다는 조건이다. 이는 멀리 떨어진 FaceRect가 엉뚱한 Face에 매칭되는 것을 막으며, 기준 거리에 UnTracked를 곱해주어 UnTracked된 프레임 수에 따라 거리를 조절하여 FaceRect를 찾아 유사도를 비교하여 매칭시킬 수 있다.</p>
<br>

<h2 id="1-2-최종-코드">1-2. 최종 코드</h2>
<h3 id="--projectcpp---github">- <a href="https://github.com/WKlee0607/KhuCv-FaceTracking/blob/main/KhuCvApp/Project.cpp">Project.cpp - Github</a></h3>
<h3 id="--projecth---github">- <a href="https://github.com/WKlee0607/KhuCv-FaceTracking/blob/main/KhuCvApp/Project.h">Project.h - Github</a></h3>
<br>

<h2 id="1-3-결과-영상">1-3. 결과 영상</h2>
<p>[결과 영상] : <a href="https://github.com/WKlee0607/KhuCv-FaceTracking/blob/main/Previews.gif">Github - KhuCv FaceTracking Project Preview Video.</a></p>
<br>

<h2 id="1-4-코드-설명">1-4. 코드 설명</h2>
<h3 id="--iou판별">- IOU판별</h3>
<pre><code class="language-cpp">// IOU가 0.15넘는 FaceRect(Tracker)는 iouVectors에 넣음.
if(m_idTrackers[i].rt.iou(currentRt) &gt; 0.15) {
    iouVectors.push_back(&amp;m_idTrackers[i]);
}

// IOU가 0.15넘는 FaceRect(Tracker)중 가장 유사도가 큰 걸 부여.
if(iouVectors.size() &gt; 0){
    identified = true;
    double maxSim = 0;
    if(iouVectors.size() == 1) maxTracker = iouVectors[0];
    else{
        for(int i = 0; i &lt; iouVectors.size(); ++i){
            double sim = iouVectors[i]-&gt;GetCosineSimilarity(cvFeature);
            if(sim &gt; maxSim) {
                maxSim = sim;
                maxTracker = iouVectors[i];
            }
        }
    }
}</code></pre>
<p>해당 Face와 IOU가 0.15넘는 FaceRect(Tracker)는 iouVectors에 넣음. 만약 IOU &gt; 0.15를 만족하는 Tracker가 한 개라면 그 FaceRect를 Face에 부여하고, 아니라면 IOU가 0.15넘는 FaceRect(Tracker)중 가장 유사도가 큰 걸 부여.</p>
<h3 id="--face유사도-판별">- Face유사도 판별</h3>
<pre><code class="language-cpp">// 거리 정의
// distance : 이전 프레임과 현재 프레임의 FaceRect의 거리
// threshold : FaceRect이동속도(offset)에 UnTracked된 횟수 곱한 거리.
double distance = fabs((m_idTrackers[i].rt.center() - currentRt.center()).mag());
double threshold = fabs((m_idTrackers[i].offset).mag() * m_idTrackers[i].UnTracked);

// 유사도 판별
double similarity = m_idTrackers[i].GetCosineSimilarity(cvFeature);

// 거리를 만족하고, Face와 유사도가 제일 높은 FaceRect(maxTracker) 뽑기
else if(distance &lt;= threshold &amp;&amp; similarity &gt; maxSimilarity) { // FaceRect주변에서만 조사
    maxSimilarity = similarity;
    maxTracker = &amp;m_idTrackers[i];
}</code></pre>
<p>Face와 FaceRect사이의 거리가 추적한 거리(threshold)보다 작으면 유사도 판별을 진행함. 즉, Face를 추적하고 있는 FaceRect가 Face와 어느 정도 가까이 있을 경우에만 유사도를 측정하여 Face에 FaceRect를 매칭하도록 함.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[6. [KhuCv] - FaceTracking, Using DeepSORT.]]></title>
            <link>https://velog.io/@wklee0607_/6.-KhuCv-FaceTracking-Using-DeepSORT</link>
            <guid>https://velog.io/@wklee0607_/6.-KhuCv-FaceTracking-Using-DeepSORT</guid>
            <pubDate>Thu, 09 Feb 2023 17:18:35 GMT</pubDate>
            <description><![CDATA[<h1 id="1-using-deepsort-in-facetracking">1. Using DeepSORT in FaceTracking.</h1>
<h2 id="1-1-deepsort">1-1. DeepSort?</h2>
<p>DeepSort란 가장 널리 사용되고 객체 추적 프레임워크 중 하나로, SORT(Simple Online and Realtime Tracking - 예시: IOU)을 보완 확장한 기술이다. 이전 KhuCv는 SORT만을 사용하여 오직 IOU로만 id를 부여하여 object를 추적하였는데, 이는 object를 Detection하지 못 할 경우 IOU추적을 어렵게 만들어 객체에 새로운 id를 부여하는 오류를 범한다. 또한 객체가 다른 객체에 의해 가려지면 다른 객체에 FaceRect가 옮겨져가는 오류도 범한다. 이러한 오류들을 해결하고 Tracking 성능을 높이기 위해 DeepSORT프레임 워크를 사용한다.</p>
<h2 id="1-2-deepsort-applied-in-khucv-facetracking">1-2. DeepSORT applied in KhuCv FaceTracking.</h2>
<p>사용할 DeepSORT프레임워크는에는 다음의 3가지 특징을 포함한다.</p>
<ol>
<li>IOU.</li>
<li>FaceSimilarity(<a href="https://en.wikipedia.org/wiki/Cosine_similarity">Cosine Similarity</a> - 참고).</li>
<li>이동 속도를 측정하여 다음 FaceRect위치 예상하기.</li>
</ol>
<p>현재 Detection된 FaceRect를 이전 Frame에서 Detection된 FaceRect와 비교하여 측정한다. 먼저, 이전 FaceRect와 현재 FaceRect의 IOU(또는 FaceSimilarity)를 측정하고, 측정이 안 될 경우 FaceSimilarity(또는 IOU)로 측정을 한다. 측정이 된다면 Tracker객체의 Rect의 좌표와 이외에 필요한 정보들을 업데이트한다.</p>
<p>이 두 가지 모두 측정이 안 될 경우, 이전 FaceRect의 이동 속도를 가지고 다음 FaceRect의 위치를 추적하도록 한다. 만약 15~20번 정도 추적이 안 될 경우, Frame에서 사라진 것으로 간주하고 해당 FaceRect에 대해 추적하는 Tracker객체를 삭제한다.</p>
<p><br><br><br></p>
<h1 id="2-how-to-get-face-feature">2. How to get Face feature?</h1>
<h2 id="2-1-dnn">2-1. DNN?</h2>
<p>심층 신경망(Deep Neural Network, DNN)은 입력층(input layer)과 출력층(output layer) 사이에 여러 개의 은닉층(hidden layer)들로 이뤄진 인공신경망(Artificial Neural Network, ANN)이다. 심층 신경망은 일반적인 인공신경망과 마찬가지로 복잡한 비선형 관계(non-linear relationship)들을 모델링할 수 있다.</p>
<p>예를 들어, 사물 식별 모델을 위한 심층 신경망 구조에서는 각 객체가 이미지 기본 요소들의 계층적 구성으로 표현될 수 있다. 이때, 추가 계층들은 점진적으로 모여진 하위 계층들의 특징들을 규합시킬 수 있다. 심층 신경망의 이러한 특징은, 비슷하게 수행된 인공신경망에 비해 더 적은 수의 유닛(unit, node)들 만으로도 복잡한 데이터를 모델링할 수 있게 해준다.</p>
<p>출처 : <a href="http://www.tcpschool.com/deeplearning/deep_algorithm1">심층 신경망(Deep Neural Network, DNN)</a></p>
<h2 id="2-2-how-to-get-face-feature">2-2. How to get Face feature.</h2>
<p>컴퓨터 비전에서는 활용되는 DNN은 미리 몇 천장의 사진으로 훈련되어 input layers로 받은 이미지에 있는 동물이 각각 어떤 동물인지 판별할 수 있다. 여기서 동물을 판별할 때, input layers에서 동물을 감지하고 각각의 특징들을 잡아내는데, 우린 이 특징을 잡아내는 기능을 이용하여 FaceRect의 특징을 찾아낼 것이다.</p>
<p>이후 FaceRect의 특징을 해당 Frame에서 Detection된 FaceRect와 cosine similarity를 이용하여 해당 Rect가 일치하는 비율을 확인할 것이다.</p>
<ul>
<li><a href="https://github.com/onnx/models/blob/main/vision/classification/mobilenet/model/mobilenetv2-7.onnx">DNN파일 다운로드</a></li>
<li><a href="https://docs.opencv.org/3.4/db/d30/classcv_1_1dnn_1_1Net.html">cv::dnn::NET reference</a></li>
<li><a href="https://netron.app/">onnx(dnn) struct 확인하는 Tool</a></li>
</ul>
<ol>
<li>해당 DNN(onnx)파일을 다운받고, Run폴더에 넣어준다.</li>
<li>CProject::CProject() - 생성자 에 해당 코드를 넣어주어 dnn의 onnx파일을 읽어온다.<pre><code class="language-cpp">// Project.cpp
CProject::CProject() {
 strcat(m_ExePt, &quot;/mobilenetv2-7.onnx&quot;);// m_ExePt는 현재 작업 경로
 m_MobileNet = cv::dnn::readNetFromONNX(m_ExePt);
}
</code></pre>
</li>
</ol>
<p>// Project.h
class CProject{
    cv::dnn::Net m_MobileNet;
}</p>
<pre><code>3. Detection된 faceRect마다 해당 코드를 실행시켜, faceRect특징을 가져온다. 즉, DNN의 foward함수를 이용하여 DNN(mobilenetv2-7.onnx) 파일의 &quot;onnx_node!mobilenetv20_features_pool0_fwd&quot; layer까지만 실행한 FaceRect의 특징을 가져온다. 가져온 특징을 cv::Mat(matrix) 형태로 저장하고 40*32(1280pixel)의 크기로 resize하여 통일성 있게 faceRect의 크기를 조정한다.
- 해당 코드는 detected된 faceRect의 특징을 가져오는 기능을 수행한다.
- dnn.foward는 해당 DNN을 입력된 파일명까지만 실행하여, cv::Mat형태로 반환하며, 우리가 사용하는 mobilenetv2-7.onnx의 &quot;onnx_node!mobilenetv20_features_pool0_fwd&quot; 파일은 input된 cv::Mat에서 특징을 집어내 데이터로 반환한다.

```cpp
for(auto list : detected_faceList) {
    cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
    cv::Mat DnnInput;
    cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
    cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
    m_MobileNet.setInput(inputBlob);
    // features cv::Mat 받아들이기
    #ifndef  __APPLE__
            cv::Mat features = m_MobileNet.forward(&quot;mobilenetv20_features_pool0_fwd&quot;);
    #else
            cv::Mat features = m_MobileNet.forward(&quot;onnx_node!mobilenetv20_features_pool0_fwd&quot;);
    #endif

    cv::Mat cvFeature(40, 32, CV_32FC1);
    memcpy(cvFeature.data, features.data, 1280 * sizeof(float));
}</code></pre><p>각 FaceRect의 특징이 담긴 cvFeature에 CosineSimilarity를 적용하여 Face Similarity를 검사한다.</p>
<p><br><br><br></p>
<h1 id="3-errors-and-how-to-fix-them">3. Errors and How to fix them.</h1>
<ul>
<li>Project.h - 고정(Fixed)<pre><code class="language-cpp">#pragma once
#include &quot;cv_dnn_ultraface.h&quot;
#include &lt;iostream&gt;
#include &lt;sstream&gt;
#include &lt;algorithm&gt;
#include &lt;vector&gt;
</code></pre>
</li>
</ul>
<p>class CProject
{
    cv::Mat m_PreviousImage;
public:
    char m_ExePath[256];
    wchar_t m_ExePathUnicode[256];</p>
<pre><code>CProject();
~CProject();
void GetExecutionPath();
void Run(cv::Mat Input, cv::Mat&amp; Output, bool bFirstRun, bool bVerbose);

UltraFace *m_pUltraface;
cv::dnn::Net m_MobileNet;</code></pre><p>};</p>
<p>class Point{
public:
    float x,y;
    Point(){}
    Point(float a, float b): x(a), y(b){}
    Point operator-(Point other){return {x-other.x, y-other.y};}
    Point operator+(Point other){return {x+other.x, y+other.y};}
    Point operator*(float a){return {x * a, y * a};}
    bool operator==(Point other){return (other.x==x &amp;&amp; other.y==y) ? true : false;}
};</p>
<p>class Rect{
public:
    Point LT,RB;
    Rect(){}
    Rect(Point lt, Point rb): LT(lt), RB(rb){}
    float iou(Rect other){ // iou가 0을 가질 경우, 이 사각형은 inter을 갖지 않음. 즉, 겹치지 않음.
        Rect inter = this-&gt;intersection(other);
        float thisArea = this-&gt;width() * this-&gt;height();
        float otherArea = other.width() * other.height();
        float interArea = inter.width() * inter.height();
        return interArea/(thisArea+otherArea-interArea);
    }
    Rect intersection(Rect other){
        float x1 = std::max(LT.x, other.LT.x);
        float y1 = std::max(LT.y, other.LT.y);
        float x2 = std::min(RB.x, other.RB.x);
        float y2 = std::min(RB.y, other.RB.y);
        return {{x1,y1},{x2,y2}};
    }
    float width(){
        return RB.x-LT.x &gt;= 0 ? RB.x-LT.x : 0;
    }
    float height(){
        return RB.y-LT.y &gt;= 0 ? RB.y-LT.y : 0;
    }
    Point center(){
        return {(LT.x + RB.x)/2, (LT.y + RB.y)/2};
    }
};</p>
<p>class Tracker{
public:
    static int cnt;
    int T_id;
    int UnTracked = 0;</p>
<pre><code>Point offset;
Rect rt;
std::vector&lt;cv::Mat&gt; featureList;

Tracker(){}
Tracker(Rect current, cv::Mat cvFeature):T_id(cnt++), rt(current), offset(0,0){
    featureList.push_back(cvFeature);
    matched = true;
}
double GetCosineSimilarity(cv::Mat feature){
    double maxSimilarity = 0;
    for(int i = featureList.size() - 1; i &gt;= 0 &amp;&amp; i &gt;= featureList.size() - 5;--i){
        double Similarity = 0;
        double A = 0, B = 0;
        for(int row = 0; row &lt; featureList[i].rows; ++row)
            for(int col = 0; col &lt; featureList[i].cols; ++col){
                Similarity += featureList[i].at&lt;float&gt;(row,col) * feature.at&lt;float&gt;(row,col); // 내적
                A += featureList[i].at&lt;float&gt;(row, col) * featureList[i].at&lt;float&gt;(row, col); // A크기
                B += feature.at&lt;float&gt;(row, col) * feature.at&lt;float&gt;(row,col); // B크기
            }
        if(sqrt(A) * sqrt(B) &gt; 0) Similarity /= sqrt(A) * sqrt(B);
        else Similarity = 0;

        if(Similarity &gt; maxSimilarity) maxSimilarity = Similarity;
    }
    return maxSimilarity;
}</code></pre><p>};</p>
<pre><code>&lt;br&gt;&lt;br&gt;

## 3-1. First Error - One Person Two faceRect Error.


### - 오류 이미지
&lt;img src=&quot;https://velog.velcdn.com/images/wklee0607_/post/765f16a9-446d-473c-b412-0957ca0d8988/image.png&quot; width=&quot;560&quot; height=&quot;310&quot;/&gt;

### - 설명
&lt;p&gt;: 맨 앞의 남자를 보면 ID 2번과 4번이 겹쳐지는 One Person Two faceRect Error가 발생함을 볼 수 있다. 해당 오류는 현재 Frame의 Detected된 Rect에 대해서, 이전 Frame의 조건을 만족하는 모든 Rect에 현재 Rect를 부여해줌으로써 발생한 문제이다.&lt;/p&gt;

### - 오류 발생 코드(초기 코드)
: Project.cpp - CProject::Run()
-&gt; 밑의 &lt;문제 발생 코드&gt; 참조

```cpp
// &lt;문제 발생 코드&gt;

// tracking vector initialize
std::vector&lt;Tracker&gt; m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat&amp; Output, bool bFirstRun, bool bVerbose) {
    std::vector&lt;FaceInfo&gt; faceList;
    m_pUltraface-&gt;detect(Input, faceList);

    cv::Mat OutImage = Input.clone();

    for(auto list : faceList) {

        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward(&quot;mobilenetv20_features_pool0_fwd&quot;);
#else
        cv::Mat features = m_MobileNet.forward(&quot;onnx_node!mobilenetv20_features_pool0_fwd&quot;);
#endif

        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));


        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;

        for(int i = 0; i &lt; m_idTrackers.size(); ++i){
            bool identified = false;


            if(m_idTrackers[i].GetCosineSimilarity(cvFeature) &gt; 0.98) identified = true;
            else if(m_idTrackers[i].rt.iou(currentRt) &gt; 0.15) identified = true;


            if(identified){
                has_id = true;
                m_idTrackers[i].featureList.push_back(cvFeature);
                m_idTrackers[i].UnTracked = (m_idTrackers[i].UnTracked &gt; 1) ? -1 : 0;
                m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (currentRt.center() - m_idTrackers[i].rt.center()) * 0.5;
                m_idTrackers[i].rt = currentRt;
            }
        }

        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }

    for(int i = 0; i &lt; m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked &gt; 15) m_idTrackers.erase(m_idTrackers.begin() + i);

        // 그리기
        cv::Scalar color;
        std::stringstream m_id;
        m_id &lt;&lt; m_idTrackers[i].T_id &lt;&lt; &quot;-&quot; &lt;&lt; m_idTrackers[i].UnTracked;

        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;
            m_idTrackers[i].UnTracked++;
        }
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }

    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();

    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}</code></pre><p><br><br></p>
<h2 id="3-1-문제-해결">3-1) 문제 해결</h2>
<h3 id="--해결-이미지">- 해결 이미지</h3>
<img src="https://velog.velcdn.com/images/wklee0607_/post/ba47b1a2-1d8f-473f-b065-cf8d11d95fdb/image.png" width="560" height="310"/>

<h3 id="--설명">- 설명</h3>
<p>: 현재 Detected된 FaceRect에 대하여 이전 프레임에서 검출된 FaceRect와의 Iou혹은 similarity가 일치할 경우, 비교 과정을 계속 진행하지 않고 break를 넣어 줌으로써 끝내어 하나의 Detected FaceRect에 한 개의 Rect만 대입하도록 한다. 해당 오류는 밑의 코드처럼 break를 작성함으로써 조건을 만족하는 하나의 Rect를 인지할 경우, FaceRect의 비교 과정을 끝낸다.</p>

<h3 id="--수정-코드">- 수정 코드</h3>
<p>: Project.cpp - parts of CProject::Run()
-&gt; 밑의 코드 참조</p>
<pre><code class="language-cpp">// 수정 코드 (parts of CProject::Run())
if(identified){
    has_id = true;
    m_idTrackers[i].featureList.push_back(features);
    m_idTrackers[i].UnTracked = (m_idTrackers[i].UnTracked &gt; 1) ? -1 : 0;
    m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (currentRt.center() - m_idTrackers[i].rt.center()) * 0.5;
    m_idTrackers[i].rt = currentRt;
    break; // break 작성
}
</code></pre>
<p><br><br></p>
<h2 id="3-2-second-error---late-tracking">3-2. Second Error - late Tracking</h2>
<h3 id="--오류-영상">- 오류 영상</h3>
<img src="https://velog.velcdn.com/images/wklee0607_/post/528b1ffa-7d92-4947-9314-1e019f86bb10/image.gif" width="560" height="315"/>

<h3 id="--설명-1">- 설명</h3>
<p> : 3-1 one person two faceRect의 문제를 해결한 뒤, late Detection문제에 직면하였다. 해당 그림의 3-0 여성을 보면, 1-0의 남성의 뒤를 지난 후 ID 3번이 제 때에 FaceDetection을 하지 못하여 ID 5번이 할당된 후, 뒤늦게 FaceDetection하여 3번으로 교체되는 late Tracking현상이 발생하였다. <br><br>
이 오류는 3번 여성이 가려졌다가 나타났을 때 밑의 코드에서처럼 인물의 유사도를 판별하는 유사도가 0.98을 넘지 못하여 발생한다.</p>

<pre><code class="language-cpp">// 오류 설명 참조 코드
if(m_idTrackers[i].GetCosineSimilarity(cvFeature) &gt; 0.98) identified = true;
else if(m_idTrackers[i].rt.iou(currentRt) &gt; 0.15) identified = true;</code></pre>
<p><br><br></p>
<h2 id="3-2-문제-해결">3-2) 문제 해결</h2>
<h3 id="--해결-영상">- 해결 영상</h3>
<img src="https://velog.velcdn.com/images/wklee0607_/post/71929db0-bb14-45f0-b7f9-9c66f6801e8c/image.gif"/>

<h3 id="--설명-2">- 설명</h3>
<p> : late Tracking을 해결하기 위해선 유사도 판별의 임계치를 0.98 밑으로 설정을 해줘야 하는데, 그렇게 되면 임계치 유사도를 만족하는 FaceRect가 여러 개가 되어 제대로 된 Tracking을 못할 수 있다. 따라서 가장 유사도가 높은 FaceRect를 선정하여 원하는 FaceRect가 매칭되도록 해준다. 이 때, FaceRect가 화면에서 사라질 경우를 대비하여 유사도의 임계치를 0.945로 설정해준다. 또한, FaceRect가 5번 동안 matched되지 않았다면 유사도 값이 더 낮을 것이므로 임계치를 0.93으로 설정해주어 match되도록 한다.</p>

<h3 id="--수정-코드-1">- 수정 코드</h3>
<p>: Project.cpp - CProject::Run()
-&gt; 밑의 코드 참조</p>
<pre><code class="language-cpp">// 수정 코드 (Project.cpp - CProject::Run())

// tracking vector initialize
std::vector&lt;Tracker&gt; m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat&amp; Output, bool bFirstRun, bool bVerbose) {
    if(bFirstRun){
        Tracker::cnt = 0;
        m_idTrackers.clear();

        for(auto layer : m_MobileNet.getLayerNames()){
            DlgPrintf(&quot;%s&quot;, layer.c_str());
        }
    }

    std::vector&lt;FaceInfo&gt; faceList;
    m_pUltraface-&gt;detect(Input, faceList);

    cv::Mat OutImage = Input.clone();

    for(auto list : faceList) {
        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward(&quot;mobilenetv20_features_pool0_fwd&quot;);
#else
        cv::Mat features = m_MobileNet.forward(&quot;onnx_node!mobilenetv20_features_pool0_fwd&quot;);
#endif

        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));


        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;
        bool identified = false;

        double maxSimilarity = 0;
        Tracker *maxTracker;

        for(int i = 0; i &lt; m_idTrackers.size(); ++i){
            if(m_idTrackers[i].rt.iou(currentRt) &gt; 0.15) {
                identified = true;
                maxTracker = &amp;m_idTrackers[i];
                break;
            }
            else{
                double similarity = m_idTrackers[i].GetCosineSimilarity(cvFeature);
                if(similarity &gt; maxSimilarity) {
                    maxSimilarity = similarity;
                    maxTracker = &amp;m_idTrackers[i];
                }
            }
        }
        if(maxSimilarity &gt; 0.945) identified = true;
        else if(maxSimilarity &gt; 0.93 &amp;&amp; maxTracker-&gt;UnTracked &gt; 5) identified = true;

        if(identified){
            has_id = true;
            maxTracker-&gt;featureList.push_back(cvFeature);
            maxTracker-&gt;UnTracked = (maxTracker-&gt;UnTracked &gt; 1) ? -1 : 0;
            maxTracker-&gt;offset = maxTracker-&gt;offset * 0.5 + (currentRt.center() - maxTracker-&gt;rt.center()) * 0.5;
            maxTracker-&gt;rt = currentRt;
        }

        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }

    for(int i = 0; i &lt; m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked &gt; 20 || (m_idTrackers[i].offset == Point{0,0} &amp;&amp; m_idTrackers[i].UnTracked &gt; 5)) m_idTrackers.erase(m_idTrackers.begin() + i);

        // id 부여
        cv::Scalar color;
        std::stringstream m_id;
        m_id &lt;&lt; m_idTrackers[i].T_id &lt;&lt; &quot;-&quot; &lt;&lt; m_idTrackers[i].UnTracked;

        // 초기화
        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;
            m_idTrackers[i].UnTracked++;
        }

        // rect, id 그리기
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }

    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();

    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}</code></pre>
<p><br><br><br></p>
<h1 id="4-performance-improving">4. Performance improving.</h1>
<h2 id="4-1-first-improving">4-1. First improving.</h2>
<p>기존의 UnTracked시에 FaceRect를 추적하여 다음 위치를 예상하기 위한 이동 속도를 조절해주었다. 기존의 FaceRect 추적 속도는 현재 offset과 이전 프레임의 offset의 비율이 5대 5였으나 개선 후에 그 비율을 6.5대 3.5로 변경해주었다. </p>
<pre><code class="language-cpp">// 변경 전 = 5:5
maxTracker-&gt;offset = maxTracker-&gt;offset * 0.5 + (currentRt.center() - maxTracker-&gt;rt.center()) * 0.5;
m_idTrackers[i].offset = m_idTrackers[i].offset * 0.5 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.5;

// 변경 후 = 3.5: 6.5
maxTracker-&gt;offset = maxTracker-&gt;offset * 0.35 + (currentRt.center() - maxTracker-&gt;rt.center()) * 0.65;
m_idTrackers[i].offset = m_idTrackers[i].offset * 0.35 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.65;</code></pre>
<br>

<h3 id="--before-improving">- Before improving.</h3>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c51e37f9-182f-474b-8787-fb4355905554/image.gif" alt=""></p>
<ol>
<li>20번 남성을 제대로 Tracking하지 못하여 22번 ID를 새로 부여하고 있다.<br></li>
</ol>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c2671155-3998-4e39-8f90-88382e1b4a6e/image.gif" alt=""></p>
<ol start="2">
<li>기존의 8번 남성이 위치 추적중인 6번의 FaceRect를 가져간다.</li>
</ol>
<br>

<h3 id="--after-improving">- after improving.</h3>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/bbfe17c8-1c93-4f77-ac19-d4288338a5a3/image.gif" alt=""></p>
<ol>
<li>개선 전보다 20번 남성의 속도에 맞추어 FaceRect가 움직임을 볼 수 있다. 따라서 제대로된 Tracking이 가능해졌다.<br></li>
</ol>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/74731e90-b3d7-49d4-a8f8-4187a254a4ee/image.gif" alt=""></p>
<ol start="2">
<li>추적 속도를 조절해주어 8번 남성이 6번 FaceRect를 가져가지 못하도록 해주었다. 즉, 두 FaceRect가 만나지 못하도록 하여 두 사각형 간의 IOU를 0으로 만들어주었다.</li>
</ol>
<p><br><br><br></p>
<h1 id="5-final-code">5. Final code.</h1>
<h2 id="5-1-projecth">5-1. Project.h</h2>
<ul>
<li>&#39;3. Errors and How to fix them.&#39; 의 Project.h 참조</li>
</ul>
<h2 id="5-2-projectcpp">5-2. Project.cpp</h2>
<pre><code class="language-cpp">// 1. Project.cpp
//  Project.cpp: implementation of CProject (main project class you will write)
//    Dept. Software Convergence, Kyung Hee University
//    Prof. Daeho Lee, nize@khu.ac.kr
//
#include &quot;KhuCvApp.h&quot;
#include &quot;Project.h&quot;

#ifdef _MSC_VER
#ifdef _DEBUG
#define _CRTDBG_MAP_ALLOC
#include &lt;stdlib.h&gt;
#include &lt;crtdbg.h&gt;
#define DEBUG_NEW new(_NORMAL_BLOCK, __FILE__, __LINE__)
#define new DEBUG_NEW
#endif
#endif

CProject::CProject() {
    GetExecutionPath();

    char m_ExePt[256];
    strcat(m_ExePt, m_ExePath);

    strcat(m_ExePath, &quot;/version-RFB-320_without_postprocessing.onnx&quot;);
    strcat(m_ExePt, &quot;/mobilenetv2-7.onnx&quot;);

    m_pUltraface = new UltraFace(m_ExePath, 320, 240);
    m_MobileNet = cv::dnn::readNetFromONNX(m_ExePt);
}

CProject::~CProject() {
    delete m_pUltraface;
}

void CProject::GetExecutionPath() {
    wxFileName f(wxStandardPaths::Get().GetExecutablePath());
    wxString appPath(f.GetPath());

    wcscpy(m_ExePathUnicode, appPath);
    strcpy(m_ExePath, appPath.c_str());
}

// tracking vector initialize
std::vector&lt;Tracker&gt; m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat&amp; Output, bool bFirstRun, bool bVerbose) {
    if(bFirstRun){
        Tracker::cnt = 0;
        m_idTrackers.clear();

        for(auto layer : m_MobileNet.getLayerNames()){
            DlgPrintf(&quot;%s&quot;, layer.c_str());
        }
    }

    std::vector&lt;FaceInfo&gt; faceList;
    m_pUltraface-&gt;detect(Input, faceList);

    cv::Mat OutImage = Input.clone();

    for(auto list : faceList) {
        cv::Mat Roi = Input(cv::Rect(cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2)));
        cv::Mat DnnInput;
        cv::resize(Roi, DnnInput, cv::Size(224, 224), 0, 0, cv::INTER_AREA);
        cv::Mat inputBlob = cv::dnn::blobFromImage(DnnInput, 1 / 255., cv::Size(224, 224), cv::Scalar(128, 128, 128), false);
        m_MobileNet.setInput(inputBlob);
        // features cv::Mat 받아들이기
#ifndef  __APPLE__
        cv::Mat features = m_MobileNet.forward(&quot;mobilenetv20_features_pool0_fwd&quot;);
#else
        cv::Mat features = m_MobileNet.forward(&quot;onnx_node!mobilenetv20_features_pool0_fwd&quot;);
#endif

        cv::Mat cvFeature(40, 32, CV_32FC1);
        memcpy(cvFeature.data, features.data, 1280 * sizeof(float));

        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;
        bool identified = false;

        double maxSimilarity = 0;
        Tracker *maxTracker;

        for(int i = 0; i &lt; m_idTrackers.size(); ++i){
            if(m_idTrackers[i].rt.iou(currentRt) &gt; 0.15) {
                identified = true;
                maxTracker = &amp;m_idTrackers[i];
                break;
            }
            else{
                double similarity = m_idTrackers[i].GetCosineSimilarity(cvFeature);
                if(similarity &gt; maxSimilarity) {
                    maxSimilarity = similarity;
                    maxTracker = &amp;m_idTrackers[i];
                }
            }
        }
        if(maxSimilarity &gt; 0.945) identified = true;
        else if(maxSimilarity &gt; 0.93 &amp;&amp; maxTracker-&gt;UnTracked &gt; 5) identified = true;


        if(identified){
            has_id = true;
            maxTracker-&gt;featureList.push_back(cvFeature);
            maxTracker-&gt;UnTracked = (maxTracker-&gt;UnTracked &gt; 1) ? -1 : 0;
            maxTracker-&gt;offset = maxTracker-&gt;offset * 0.65 + (currentRt.center() - maxTracker-&gt;rt.center()) * 0.35;
            maxTracker-&gt;rt = currentRt;
        }

        if(!has_id){
            Tracker tracker(currentRt, cvFeature);
            m_idTrackers.push_back(tracker);
        }
    }

    for(int i = 0; i &lt; m_idTrackers.size(); ++i){
        // 삭제
        if(m_idTrackers[i].UnTracked &gt; 20 || (m_idTrackers[i].offset == Point{0,0} &amp;&amp; m_idTrackers[i].UnTracked &gt; 5)) m_idTrackers.erase(m_idTrackers.begin() + i);

        // id 부여
        cv::Scalar color;
        std::stringstream m_id;
        m_id &lt;&lt; m_idTrackers[i].T_id &lt;&lt; &quot;-&quot; &lt;&lt; m_idTrackers[i].UnTracked;

        // 초기화
        if(m_idTrackers[i].UnTracked == -1) {
            color = cv::Scalar(255,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else if(m_idTrackers[i].UnTracked == 0) {
            color = cv::Scalar(0,0,255);
            m_idTrackers[i].UnTracked = 1;
        }
        else {
            color = cv::Scalar(255,255,0);
            Rect currentRt = m_idTrackers[i].rt;
            m_idTrackers[i].rt.LT = m_idTrackers[i].rt.LT + m_idTrackers[i].offset;
            m_idTrackers[i].rt.RB = m_idTrackers[i].rt.RB + m_idTrackers[i].offset;
            m_idTrackers[i].offset = m_idTrackers[i].offset * 0.35 + (m_idTrackers[i].rt.center()-currentRt.center()) * 0.65;
            m_idTrackers[i].UnTracked++;
        }

        // rect, id 그리기
        cv::rectangle(OutImage, cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y), cv::Point(m_idTrackers[i].rt.RB.x,m_idTrackers[i].rt.RB.y),color, 2);
        cv::putText(OutImage, m_id.str(), cv::Point(m_idTrackers[i].rt.LT.x,m_idTrackers[i].rt.LT.y-10),1,2,color,3);
    }

    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();

    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}</code></pre>
<p>해당 프레임에서 detection된 FaceRect에 대하여 먼저 이전 프레임의 FaceRect와의 IOU값을 판별하여 Tracking을 시도한다. 만약 IOU로 Tracking이 되지 않을 경우, FaceSimilarity로 Tracking을 시도한다. 임계치의 IOU 혹은 FaceSimilarity를 만족하는 FaceRect가 발견될 경우 Tracker의 Rect, offset, UnTracked 등을 업데이트 해준다. </p>
<p>그러나 만약 OU 혹은 FaceSimilarity를 만족하는 FaceRect가 없을 경우, 새로운 객체로 판단하여 새로운 Tracker객체를 만들어준다. </p>
<p>판별 과정이 끝나면 각 Tracker의 UnTracked에 따라 색깔을 달리하여 Rectangle을 그려준다. UnTracked는 기본적으로 해당 FaceRect정보가 담긴 Tracker가 연속적으로 몇 번 Tracking하지 못하였는지를 의미한다.  UnTracked가 0일 경우 이전 프레임에서 UnTracked된 적 없이 연속적으로 Tracking됐음을 뜻하며, -1일 경우 추적을 놓친 FaceRect를 찾았다는 것을 의미한다. 그리고 UnTracked가 1이상일 경우 이전 프레임에서 x번 이상 Tracking되지 못하였음을 뜻하고 이 UnTracked가 18번 이상이면 해당 Tracker는 삭제한다. </p>
<p><br><br></p>
<h1 id="6-느낀-점">6. 느낀 점</h1>
<p>외부 FaceDetection DNN을 활용하여 FaceTracking하는 활동을 해보았다. 알고리즘을 짜는 데에만 일주일을 쓴 거 같다. 어떤 식으로 코드를 짜야 간결하고 잘 작동할까?라는 생각에 더 시간을 오래 쓴 거 같다. 솔직히 대략적인 기능을 어찌저찌 구현만 하면 이후에는 수월할 것이라 생각했는데, 위에 언급한 Error들에 직면했다. </p>
<p>3-1 Error는 한 3일간 고민을 했는데, 그 방법을 모르겠어서 교수님께 여쭤보았다. 교수님은 Tracking된 object는 따로 표시를 하여 2번 중복되지 않도록 하라고 하셨다. 그러나 나는 이 방법보다 더 간결한 방법이 있을 것이라 생각하였고, 3-1의 해결방법에 언급한 방법으로 해결하였다. 이후 3-2의 에러를 만났는데, 이 에러는 하루 정도 고민하여 해결하였다. 이전의 교수님이 설명해주신 maxSimilarity 아이디어에 조금 코드를 추가하여 에러를 해결할 수 있었다. </p>
<p>이번 프로젝트를 통해 원하는 기능을 내 방식대로 구현해보는 값진 경험을 할 수 있었으며, 아직은 부족하지만 코드를 간결하게 짜는 스킬도 조금은 터득할 수 있었다. 그리고 마주한 에러들을 혼자 생각해보고 (주변의 도움을 조금 받긴 했지만) 스스로 해결해보며, 문제를 마주했을 때 어떤 부분이 문제인지 정확히 인지하는 방식과 문제마다 어떤 식으로 해결해 나아가야 하는지에 대한 방식을 배울 수 있었다. </p>
<p>아직 Face Tracking 성능이 많이 부족하긴 하지만, 처음 한 것 치곤 만족스러운 결과를 낸 프로젝트였다. 추후, 컴퓨터 비전 및 딥러닝 관련 수업을 들으며, 이 성능을 계속 보완해나갈 계획이다.</p>
<ul>
<li>[Github Address] : <a href="https://github.com/WKlee0607/KhuCv-FaceTracking">KhuCv-FaceTracking github Code</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[5. [KhuCv] - FaceTracking, Using SORT.]]></title>
            <link>https://velog.io/@wklee0607_/5.-KhuCv-Tracking-Using-IOU</link>
            <guid>https://velog.io/@wklee0607_/5.-KhuCv-Tracking-Using-IOU</guid>
            <pubDate>Thu, 19 Jan 2023 15:20:47 GMT</pubDate>
            <description><![CDATA[<h1 id="1-how-to-track-using-only-iou">1. How to Track, Using only IOU.</h1>
<h2 id="1-1-sort">1-1. SORT?</h2>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/9c3a2e07-eec1-4220-a68d-ae08026c08db/image.png" alt=""></p>
<p>(위의 사진은 SORT의 흐름도이다.) SORT는 실시간 추적을 위해 object들을 효율적으로 연관지어주는 MOT(Multi Object Tracking)이다. 이 때 MOT란 다수의 객체들을 추적하기 위해 detection 결과 간 연관(association)을 수행하는 과정이다.</p>
<p>SORT는 Simple Online and Realtime Tracking의 약자이다. 여기서 나타나는 Online Tracking 방식은 미래 프레임에 대한 정보 없이 과거와 현재 프레임의 객체 detection 정보만을 사용하여 연관 관계에 대한 Tracking을 수행하는 방식이다. 따라서 과거 혹은 현재 프레임에 대한 detection이 부족하여 제대로된 association이 이뤄지지 못하면, 제대로된 Tracking이 이루어지지 못한다는 단점을 가지고 있다.</p>
<h2 id="1-2-iou">1-2. IOU?</h2>
<p>IOU(Intersection over Union)란 두 겹치는 사각형의 교집합 사각형의 넓이를 두 사각형의 합집합 사각형의 넓이로 나눈 값. 즉, 겹치는 두 사각형의 교집합과 합집합의 비율. 겹치는 영역이 커질 수록 IOU 값이 높다.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c0c12863-6fa5-43a6-a296-b699cdda0384/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/0d0b4d9b-45a4-47a2-8138-efa18ae2ff4b/image.png" alt=""></p>
<p>Target Association은 MOT 방법을 기반으로 한 tracking-by-detection의 핵심 단계이다. IOU를 Metric으로 사용하여 그림에서는 IOU Match라고 나타낸다. 이 단계에서는 IOU 유사도를 구한 후, 추적되고 있던 개체와 아닌 개체를 분류한다. 이 때 추적되고 있지 않던 개체는 사라진 개체, 새로 등장한 개체 이 두 가지로 나뉠 수 있다. </p>
<p>실제 상황에서 발생하는 Occulusion이나 Different View Point, ID switching은 지속적인 추적을 방해하며, 한 번 사라진 개체(추적을 놓친 개체)로 하여금 추적 불가능하게 만든다. SORT는 한 번 추적을 놓치면, 다시 동일한 객체가 나타났을 때 새로운 객체로 인식하는 한계를 가지므로 추후 DeepSORT방식을 이용하여 SORT의 한계를 보완해야 한다.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/2e7bf43b-329b-4f9b-9310-f51241160097/image.png" alt=""></p>
<p>[참고] : <a href="https://www.waytoliah.com/1491">IOU란?</a></p>
<h2 id="1-3-how-to-track">1-3. How to Track.</h2>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/928fcdd5-d2fd-4ca6-94bb-80bcaf133059/image.png" alt=""></p>
<p>Project.cpp의 Run함수를 살펴보면, 현재 프레임에서 detection된 FaceRect객체들은 FaceInfo객체로 나타남을 볼 수 있다. FaceInfo는 얼굴을 인식한 Rect(사각형)의 좌표를 저장한 객체로써 (x1,y1)은 LeftTop, (x2,y2)는 RightBottom의 좌표를 의미한다.</p>
<p>현재 프레임에서 detection된 각 FaceRect와 이전 프레임의 FaceRect의 IOU를 판별하여 특정 IOU값 이상을 만족하는 Rect가 있다면 즉, 겹치는 사각형이 있다면, 이는 동일한 인물이 움직여서 생긴 Rect의 움직임으로 판단할 수 있으므로 별도의 Vector(m_idTrackers)에 넣어준다. 화면상에 존재하는 Rect에 대한 IOU판별을 끝내면 m_idTrackers Vector내의 객체 각각에 대하여 FaceRect와 id를 화면에 그려준다.
이 과정을 반복하면 오직 IOU를 이용하여 해당 FaceRect의 움직임을 추적할 수 있다.</p>
<h2 id="1-4-결과">1-4. 결과</h2>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/b9057515-445a-4408-83c0-e4dd47528b57/image.png" alt=""></p>
<h3 id="projecth">Project.h</h3>
<pre><code class="language-cpp">// tracking

class Point{
public:
    float x,y;
    Point(){}
    Point(float a, float b): x(a), y(b){}
};

class Rect{
public:
    Point LT,RB;
    Rect(){}
    Rect(Point lt, Point rb): LT(lt), RB(rb){}
    float iou(Rect other){ // iou가 0을 가질 경우, 이 사각형은 inter을 갖지 않음. 즉, 겹치지 않음.
        Rect inter = this-&gt;intersection(other);
        float thisArea = this-&gt;width() * this-&gt;height();
        float otherArea = other.width() * other.height();
        float interArea = inter.width() * inter.height();
        return interArea/(thisArea+otherArea-interArea);
    }
    Rect intersection(Rect other){
        float x1 = std::max(LT.x, other.LT.x);
        float y1 = std::max(LT.y, other.LT.y);
        float x2 = std::min(RB.x, other.RB.x);
        float y2 = std::min(RB.y, other.RB.y);
        return {{x1,y1},{x2,y2}};
    }
    float width(){
        return RB.x-LT.x &gt;= 0 ? RB.x-LT.x : 0;
    }
    float height(){
        return RB.y-LT.y &gt;= 0 ? RB.y-LT.y : 0;
    }
};

class Tracker{
public:
    static int cnt;
    int T_id;
    Rect rt;
    int time = 0;
    Tracker(){}
    Tracker(Rect current):T_id(cnt++), rt(current){}
    void update_rt(Rect other){
        rt = other;
    }
    void drawText(cv::Mat OutImage){
        std::stringstream m_id;
        m_id &lt;&lt; T_id;
        cv::putText(OutImage, m_id.str(), cv::Point(rt.LT.x,rt.LT.y-10),1,2,cv::Scalar(0,0,255),3);
    }
};</code></pre>
<p>현재 프레임의 FaceRect를 추적하기 위해 Tracker라는 class를 만든다. 이 Tracker객체는 추적하는 각 개체마다 id를 부여하여 구분하기 위해 m_id멤버 변수를 static으로 선언하였으며, 각 객체가 생성될 때마다 1씩 증가된 id가 부여된다. 이 외에도 추적하는 Rect를 저장하기 위한 Rect, 그리고 화면 상에 몇 번 연속적으로 나타나지 않았는지를 세는 time instance를 갖는다.</p>
<h3 id="projectcpp">Project.cpp</h3>
<pre><code class="language-cpp">#include &lt;sstream&gt;

// tracking vector initialize
std::vector&lt;Tracker&gt; m_idTrackers;
int Tracker::cnt = 0;

void CProject::Run(cv::Mat Input, cv::Mat&amp; Output, bool bFirstRun, bool bVerbose) {
    std::vector&lt;FaceInfo&gt; faceList;
    m_pUltraface-&gt;detect(Input, faceList);

    cv::Mat OutImage = Input.clone();

    for(auto list : faceList) {
       cv::rectangle(OutImage, cv::Point(list.x1, list.y1), cv::Point(list.x2, list.y2), cv::Scalar(0, 0, 255), 2);

    /// tracking - start
        Rect currentRt({list.x1,list.y1},{list.x2,list.y2});
        bool has_id = false;

        for(auto &amp;tracker : m_idTrackers){
            float c_iou = currentRt.iou(tracker.rt);
            if(c_iou &gt;= 0.25){
                tracker.update_rt(currentRt);
                has_id = true;
                tracker.time = 0;
                break;
            }
        }

        if(!has_id){ /// 현재 Rect가 vector안에 없다면
            Tracker tracker(currentRt);
            m_idTrackers.push_back(tracker);
        }
    }
    for(int i = 0; i &lt; m_idTrackers.size(); ++i)
        if(m_idTrackers[i].time &gt; 4) m_idTrackers.erase(m_idTrackers.begin() + i);
        if(m_idTrackers[i].time == 0) m_idTrackers[i].drawText(OutImage);
        m_idTrackers[i].time++;
    }
    /// m_idTrackers Vector의 capacity 맞춰주기
    m_idTrackers.shrink_to_fit();

    /// tracking - end

    if(bVerbose)
        DisplayImage(OutImage, Input.cols, 0, false, true);
}
</code></pre>
<p>현재 프레임의 FaceRect마다 이전 프레임의 FaceRect정보가 담긴 m_idTrackers(vector)안의 모든 Tracker객체와 IOU를 비교한다. 만약 임계치 이상의 IOU값을 만족하는 갖는 Tracker가 있다면, Tracker의 Rect를 업데이트 해주고 time = 0으로 초기화해준다. 만약 m_idTrackers 안에 IOU를 만족하는 Tracker Rect가 없다면, 이 FaceRect를 새로운 객체로 인식하고 해당 Rect를 갖는 Tracker를 m_idTrackers에 추가한다.</p>
<p>화면의 모든 Rect에 대한 IOU판별이 끝나면, m_idTrackers에 존재하는 Tracker의 id를 화면에 그련준다(drawText). 이 때, 화면에 3번 이상 나타나지 않는 Rect Tracker는 m_idTrackers에서 제거한다. 이후, m_idTrackers vector의 capacity를 줄여주기 위해 shrink_to_fit()함수를 이용한다.</p>
<ul>
<li>vector.erase(주소값) : 특정값을 만족하는 값을 vector내에서 삭제해준다. vector의 size는 줄지만, capacity자체는 줄지 않는다. 이는 <a href="https://cplusplus.com/reference/vector/vector/shrink_to_fit/">shrink_to_fit()</a> 함수를 이용하여 capacity를 맞추어준다.</li>
</ul>
<h1 id="2-conclusion">2. Conclusion.</h1>
<p>위의 과정을 반복함으로써, IOU를 이용하여 얼굴 Tracking이 가능해진다. 하지만 얼굴이 장애물에 가려짐으로써 3번 이상 나타나지 않을 경우 동일한 사람에 대한 Tracking이 불가능해지며, 다른 사람에게 FaceRect가 옮겨가는 현상까지 발생한다.</p>
<p>이는 추후 얼굴 특징을 이용하여 DeepSORT Tracking 방식을 이용함으로써 어느 정도 극복 가능할 것으로 보인다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[4. [KhuCv] - Face Detection DNN Build]]></title>
            <link>https://velog.io/@wklee0607_/4.-KhuCv-Face-Detecting-Build</link>
            <guid>https://velog.io/@wklee0607_/4.-KhuCv-Face-Detecting-Build</guid>
            <pubDate>Thu, 19 Jan 2023 10:35:47 GMT</pubDate>
            <description><![CDATA[<h1 id="1-how-to-build-face-detecting-dnn">1. How to build Face Detecting DNN.</h1>
<h3 id="1-download-ultrafacedetecting-filesone-cpp-one-header-file">1. Download UltraFaceDetecting files(one cpp, one header file).</h3>
<p><a href="https://github.com/Linzaer/Ultra-Light-Fast-Generic-Face-Detector-1MB/tree/master/opencv_dnn">Download UltraFaceDetecting files</a> &lt;- 해당 링크를 참조하여 KhuCv Folder(Build 할 폴더)에 포함시킨다.</p>
<h3 id="2-editing-cmakelists-and-project-file">2. Editing CMakeLists and Project File.</h3>
<p>UltraFaceDetecting files가 build되도록 CMakeLists를 수정한다. 이후, Project.cpp 파일과 Project.h 파일을 <a href="https://github.com/NizeLee/KhuCv_mdi/tree/main/Samples/01_Face_detection_opencv">참고</a> 링크를 참조하여 수정한다.</p>
<h3 id="3-build">3. Build</h3>
<h3 id="4-add-onnx-file-on-run-folder">4. Add onnx File On Run Folder.</h3>
<p><a href="https://github.com/Linzaer/Ultra-Light-Fast-Generic-Face-Detector-1MB/tree/master/models/onnx">onnx File</a> &lt;- 여기서 onnx File을 다운받아, Run Folder에 추가시킨다. 이 때, Project.cpp 파일에 적힌 onnx파일과 동일한 파일을 다운받는다.</p>
<p>&lt;주의사항&gt; : onnx파일에 대한 선언 및 정의가 Ultraface와 CProject에서 중복 선언된 경우가 있을 수 있으므로, 이를 잘 확인하여 수정한다.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/1b4b20d8-d354-415e-aafb-ad8d4d478ff5/image.png" alt=""></p>
<h3 id="5-run-khucv">5. Run KhuCv.</h3>
<p>KhuCv Application을 실행하여 Face Detection이 잘 작동하는지 확인한다.
<img src="https://velog.velcdn.com/images/wklee0607_/post/bbfae1ca-ff66-45de-bf01-3edcb122c83a/image.png" alt=""></p>
<p>[참고] : <a href="https://github.com/NizeLee/KhuCv_mdi/tree/main/Samples/01_Face_detection_opencv">KhuCv-Face Detecting</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [Python] 2. os.walk]]></title>
            <link>https://velog.io/@wklee0607_/Python-2.-os.walk</link>
            <guid>https://velog.io/@wklee0607_/Python-2.-os.walk</guid>
            <pubDate>Sat, 07 Jan 2023 10:04:27 GMT</pubDate>
            <description><![CDATA[<h1 id="oswalkpath">os.walk(path)</h1>
<p>os.walk(path) 는 인자로 받은 경로에 대해 하위 디렉토리를 검색한다.
디렉토리 구조는 다음과 같을 때, </p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/ee9736b4-da2c-4870-86ee-1b6fbeb74be3/image.png" alt=""></p>
<p>예제 1): dirs(폴더) &amp; files 구하기</p>
<ul>
<li>dirs: 해당 path가 가지고 있는 폴더 list형식으로 반환</li>
<li>files: 해당 path내에 존재하는 files를 list형식으로 반환</li>
</ul>
<pre><code class="language-python">import os

PATH = os.path.dirname(os.path.abspath(__file__))
dir_name = &#39;data&#39;

for path, dirs, files in os.walk(os.path.join(PATH,dir_name)):
    print(path, dirs, files)</code></pre>
<p>결과 : 
<img src="https://velog.velcdn.com/images/wklee0607_/post/78dac158-86eb-4027-af7e-658f855eb755/image.png" alt=""></p>
<p>예제 2): 폴더 내의 모든 경로 구하기</p>
<pre><code class="language-python">import os

PATH = os.path.dirname(os.path.abspath(__file__))
dir_name = &#39;data&#39;
dir_path = os.path.join(PATH, dir_name)
for path, dirs, files in os.walk(dir_path):
    print(path)
    for file in files:
        file_path = os.path.join(path, file)
        print(file_path)</code></pre>
<p>결과 :
<img src="https://velog.velcdn.com/images/wklee0607_/post/0298a0ac-85d7-4eef-9114-ccc024779ece/image.png" alt=""></p>
<p>예제 3): 특정 확장자 추출</p>
<pre><code class="language-python">import os

PATH = os.path.dirname(os.path.abspath(__file__))
dir_name = &#39;data&#39;

for path, dirs, files in os.walk(os.path.join(PATH,dir_name)):
    for file in files:
        name, ext = os.path.splitext(file)
        if ext == &#39;.png&#39;:
            tile_path = os.path.join(os.path.join(PATH,dir_name), file)
            print(&#39;find file :&#39;,file_path)</code></pre>
<p>결과 : </p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/07ae5268-52ee-4728-a60d-03b35a046d0d/image.png" alt=""></p>
<h2 id="참고--httpsjvvptistorycom986">참고 : <a href="https://jvvp.tistory.com/986">https://jvvp.tistory.com/986</a></h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[# [Python] 1. Match-Case ]]></title>
            <link>https://velog.io/@wklee0607_/Python-Match-Case</link>
            <guid>https://velog.io/@wklee0607_/Python-Match-Case</guid>
            <pubDate>Sat, 07 Jan 2023 07:30:14 GMT</pubDate>
            <description><![CDATA[<h1 id="match-case문">Match-Case문</h1>
<p>C++의 Switch-Case문과 비슷함.</p>
<pre><code class="language-python">match obj:
    case A:
        A code ...
    case B:
        B code ..
    case default:
        default code</code></pre>
<p>이런 식으로 obj가 A를 만족하면 A의 A code가 실행되고, B를 만족하면 B코드가 실행됨. 아무것도 만족하지 않을 시 default code가 실행됨.</p>
<p>예시 0) : 기본 사용 형태</p>
<pre><code class="language-python">def number_to_string(agrument):
    match agrument:
        case 0:
            return &quot;zero&quot;
        case 1:
            return &quot;one&quot;
        case 2:
            return &quot;two&quot;
        case default:
            return &quot;nothing&quot;


print(number_to_string(0))
print(number_to_string(1))
print(number_to_string(2))
print(number_to_string(3))
print(number_to_string(4))</code></pre>
<p>예시 1) : </p>
<pre><code class="language-python">def http_status(status):
    match status:
        case 400:
            return &quot;Bad request&quot;
        case 401 | 403:
            return &quot;Unauthorized&quot;
        case 404:
            return &quot;Not found&quot;
        case _: # default , or case default:
            return &quot;Other error&quot;</code></pre>
<p>예시 2) : 아래와 같이 리스트로 match case 구문을 사용할 수 있다. *names 처럼 고정되지 않은 개수의 인자들을 받아서 처리할 수 있다.</p>
<pre><code class="language-python">def greeting(message):
    match message:
        case [&quot;hello&quot;]:
            print(&quot;Hello!&quot;)
        case [&quot;hello&quot;, name]:
            print(f&quot;Hello {name}!&quot;)
        case [&quot;hello&quot;, *names]:
            for name in names:
                print(f&quot;Hello {name}!&quot;)
        case _:
            print(&quot;nothing&quot;)


greeting([&quot;hello&quot;])
greeting([&quot;hello&quot;, &quot;John&quot;])
greeting([&quot;hello&quot;, &quot;John&quot;, &quot;Doe&quot;, &quot;MAC&quot;])</code></pre>
<p>예시 3) : tuple 형식에 대한 경우도 다음과 같이 처리할 수 있다.</p>
<pre><code class="language-python">for n in range(1, 101):
    match (n % 3, n % 5):
        case (0, 0):
            print(&quot;FizzBuzz&quot;)
        case (0, _):
            print(&quot;Fizz&quot;)
        case (_, 0):
            print(&quot;Buzz&quot;)
        case _:
            print(n)</code></pre>
<h2 id="참고-">참고 :</h2>
<h4 id="1-httpswikidocsnet173398">1. <a href="https://wikidocs.net/173398">https://wikidocs.net/173398</a></h4>
<h4 id="2-httpscodechachacomkopython-switch-case">2. <a href="https://codechacha.com/ko/python-switch-case/">https://codechacha.com/ko/python-switch-case/</a></h4>
]]></description>
        </item>
        <item>
            <title><![CDATA[3. CMake (KhuCv Build)]]></title>
            <link>https://velog.io/@wklee0607_/3.-CMake</link>
            <guid>https://velog.io/@wklee0607_/3.-CMake</guid>
            <pubDate>Tue, 03 Jan 2023 14:43:09 GMT</pubDate>
            <description><![CDATA[<h1 id="1-cmake">1. <a href="https://cmake.org/">CMake</a></h1>
<p> CMake(Cross Platform Make)는 멀티플랫폼으로 사용할 수 있는 <a href="https://ko.wikipedia.org/wiki/Make_(%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4)">Make</a>(소프트웨어 개발을 위해 Unix운영체제에서 주로 사용되는 프로그램 빌드 도구)의 빌드 관리 시스템을 만들기 위한 오픈소스 프로젝트로 키트웨어와 인사이트 콘솔티엄에서 만들었다.
 스스로 기존의 Make의 과정을 수행하지는 않고, <strong>지정한 운영 체제에 맞는 Make파일의 생성 및 관리만을 수행하기 때문에 Meta Make라고도 불리우는 빌드 자동화 시스템이다.</strong> 가장 큰 이점은 Unix게열 OS중심이던 기존의 Make의 빌드 시스템과는 달리 한 번 작성해 두면 유닉스 계열은 물론 MS Window계열의 프로그래밍 도구도 지원함.
 즉, 간략히 말해서 c/c++등 프로젝트는 빌드하기 위해서는 MakeFile을 작성해야 하는데, 이를 보다 쉽고 편리하게 작성할 수 있도록 도와주는 툴이 바로 CMake이다. 즉, c/c++ 언어 빌드 도와주는 툴.</p>
<p> CMake is an open-source, cross-platform family of tools designed <strong>to build, test and package software.</strong> CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice. </p>
<h2 id="1-1-cmake로-원하는-라이브러리-build하기">1-1. CMake로 원하는 라이브러리 build하기</h2>
<ul>
<li>여기서 모든 진행은 terminal(CLI)에서 진행한다.</li>
</ul>
<ol>
<li>$ CMakeLists가 있는 해당 파일로 들어간다. Ex) cd build-mac</li>
<li>$ cmake . : makefile을 만들어라.</li>
<li>$ make : CMakeLists를 참조하여 source file들을 build하기(실행 가능한 파일로 컴파일 하기) !</li>
</ol>
<p>** build 과정에서 오류 해결: ls: .: Operation not permitted -&gt; Mac 터미널에서 발생한 에러
해결법 참조 :<a href="https://saurus2.tistory.com/entry/ls-Operation-not-permitted-mac-OS-%EB%A7%A5%EB%B6%81-%ED%84%B0%EB%AF%B8%EB%84%90-%EC%97%90%EB%9F%AC">tistory ls operation not permitted 해결</a> </p>
<p>1 . 환경설정 &gt; 보안 및 개인 정보 보호로 들어간다.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/a4047de6-6d91-4a48-bb5e-a9af051a3196/image.png" alt=""></p>
<p>2 . 전체 디스크 접근 권한에서 terminal에 접근을 허락하면 오류 해결 !</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/cbcc82f9-2028-456b-88e7-68142ad3aa71/image.png" alt=""></p>
<h1 id="2-예제-khucv-실행하기">2. 예제) KhuCv 실행하기</h1>
<ol>
<li>OpenCV build</li>
<li>wxWidigets build </li>
<li>cmake로 KhuCv파일 build(CmakeLists.txt 참조하여)</li>
<li>3에서 생성된 파일에서 Run &gt; KhuCv 파일 들어가면 다음과 같이 실행가능</li>
</ol>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/b0025b6d-a528-4496-a75c-fd11163c9d80/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/75fb2052-bddf-45b1-a358-bfd0fbbd5084/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[2. WxWidgets]]></title>
            <link>https://velog.io/@wklee0607_/2.-WxWidgets</link>
            <guid>https://velog.io/@wklee0607_/2.-WxWidgets</guid>
            <pubDate>Tue, 03 Jan 2023 14:42:46 GMT</pubDate>
            <description><![CDATA[<h1 id="1-wxwidgets">1. <a href="https://www.wxwidgets.org/">WxWidgets</a></h1>
<p> WxWidgets는 <strong>크로스 플랫폼</strong> 응용 프로그램을 위한 <strong>그래픽 사용자 인터페이스(GUI)</strong> 를 만들어 주는 위젯 톨킷이다. wxWidgets는 특별한 코드 변경 없이도 프로그램의 GUI 코드를 여러 컴퓨터의 운영 체제에서 컴파일하고 동작할 수 있게 도와준다. 마이크로소프트 윈도우, OS X, 리눅스/유닉스 (X11, 모티프, GTK+), 오픈VMS, OS/2, 아미가OS와 같은 운영 체제를 지원한다. 즉, 이걸 이용하면 Windows,  Linux,  MacOS, iOS, Android 기타등등 .. 운영체제에 구애받지 않고 GUI 프로그램을 작성 할 수 있다는 이야기이다.
 즉, 운영체제에 구애받지 않고 동일한 GUI코드(그래픽 사용자 인터페이스)를 여러 컴퓨터의 운영체제에서 컴파일하고 동작할 수 있게 도와준다.</p>
<h2 id="1-1-wxwidgets-for-macos-installation">1-1. <a href="https://docs.wxwidgets.org/3.2/plat_osx_install.html">wxWidgets for macOS installation</a></h2>
<p> wxWidgets for macOS installation은 터미널에서 진행.
0. Download wxWidgets source code from <a href="https://www.wxwidgets.org/downloads/">https://www.wxwidgets.org/downloads/</a></p>
<ol>
<li>terminal -&gt; cd wxWidgets-3.x.x : 다운받은 wxWidget 폴더로 들어간다.</li>
<li>mkdir build-cocoa-build-cocoa : wxWidgets 폴더 내에서 build-cocoa-debug 라는 폴더를 생성한다.</li>
<li>cd build-cocoa-debug : build-cocoa-debug 폴더로 들어간다.</li>
<li>../configure –-enable-debug <strong>--with-libtiff=builtin</strong> : wxWidgets파일의 configure terminal에서 –-enable-debug  --with-libtiff=builtin를 동작시킨다는 뜻이다. </li>
</ol>
<p>** 오류 해결 : 
 <img src="https://velog.velcdn.com/images/wklee0607_/post/6ad2cc3d-5251-479d-8a66-3954a2aaac01/image.png" alt="">
그림을 보면 fatal error : &#39;tiff.h&#39; files not found라는 오류가 발생한다. 이는 configure가 tiff.h file을 찾지 못했다는 뜻인데, 이는 tiff.h파일이 builtin이라 발생한 문제이다. 따라서 나는 이를 configure terminal에서 debug(?)를 할 때, tiff.h 파일은 builtin이라고 알려주어 오류를 해결하였다.</p>
<p>5 sudo su
6. make install
7. exit
8. cd smaples
9. make
10. cd ../demos
11. make</p>
<ul>
<li>make 명령어: Makefile을 만든 후 컴파일하기 위해서는 make라는 명령어를 실행하면 된다. </li>
</ul>
<p><strong>참고영상</strong> : <a href="https://www.youtube.com/watch?v=BBv3FkFcPwI">https://www.youtube.com/watch?v=BBv3FkFcPwI</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[1. OpenCV]]></title>
            <link>https://velog.io/@wklee0607_/1.-OpenCV</link>
            <guid>https://velog.io/@wklee0607_/1.-OpenCV</guid>
            <pubDate>Tue, 03 Jan 2023 14:41:59 GMT</pubDate>
            <description><![CDATA[<h1 id="1-opencv">1. OpenCV</h1>
<p>openCV란 Open Source Computer Vision의 약자로, 영상 처리에 사용할 수 있는 오픈 라이브러리이다. 즉, 실시간 컴퓨터 비전을 목적으로 한 프로그래밍 라이브러리이다. 컴퓨터가 사람의 눈처럼 인식을 할 수 있게 처리해주는 역할을 하기도, 우리가 사용하는 카메라 어플에서도 OpenCV가 사용되기도 한다.
그 외 사용 예시 :</p>
<ul>
<li>공장 제품 검사</li>
<li>의료 영상 처리 및 보정 &amp; 판단</li>
<li>CCTV영상</li>
<li>로보틱스
여기에 머신 러닝과 A.I를 활용하여 그 활용도를 넓혀가는 중이다.</li>
</ul>
<p>OpenCV가 더욱 인기있는 이유는 오픈소스이지만 BSD(Berkely Software Distribution)라이선스를 따르기 때문에, 상업적 목적으로 사용해도 좋다. 그리고 소스코드 공개 의무가 없다는 점에서 강점을 가지고 있다. OpenCV의 기원은 인텔에서부터 시작되는데, 컴퓨터 비전과 인공지능의 발달 시키고자 하는 바램으로 OpenCV를 출시한 것이다.
 OpenCV는 실시간 처리에 중점을 두고 설계되서 빠른 속도와 효율성을 자랑한다. 기반 언어는 C++로 멀티 코어 프로세서를 활용할 수 있다. </p>
<br>

<h2 id="1-1-opencv-사용-언어">1-1. OpenCV 사용 언어</h2>
<p> OpenCV와 사용할 수 있는 언어로는 C/C++, 파이썬, Java등이 있다. 요새 많이 쓰이는 언어로는 파이썬이 많이 사용되고 있다. 빅데이터와 머신 러닝에서 강점을 보이고 있기에 파이썬이 많이 쓰인다. </p>
<br>


<h2 id="1-2-opencv-기본-구조">1-2. OpenCV 기본 구조</h2>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/fe4bdb9e-6d91-4453-99e5-f54c8f395b68/image.png" alt="">
1.CV: Computer Vision으로 기본 구성 요소에서는 <strong>기본 이지미 처리 &amp; 고급 컴퓨터 비전 알고리즘</strong>을 포함하고 있다.
2.ML(Machine Learning): 머신러닝이 주를 이루므로 이를 활용할 수 있는 <strong>기본적인 통계 분류기 &amp; 클러스터링 도구</strong> 를 포함하고 있는 라이브러리들이 있다.
3.HighGUI: OpenCV에 기본으로 포함되어 있는 <strong>GUI</strong>이다. 여기에는 비디오와 이미지 저장 그리고 이를 로드하기 위한 I/O 루틴을 포함하고 있다. 이 모듈을 활용해서 사용자의 입력을 받고 그에 따른 출력하는 기능을 하는 유저 인터페이스 기능을 하고 있다. 기본 기능으로 지원하고 있지만 조금 더 범용적으로 Qt를 사용한다. 
4.CXCORE: 이곳에는 기본 데이터 구조와 여러 content가 구성되어 있다. </p>
<p>** GUI: 그래픽 사용자 인터페이스(graphical user interface)는 사용자가 편리하게 사용할 수 있도록 입출력 등의 기능을 알기 쉬운 아이콘 따위의 그래픽으로 나타낸 것이다. 컴퓨터를 사용하면서, 화면 위의 물체나 틀, 색상과 같은 그래픽 요소들을 어떠한 기능과 용도를 나타내기 위해 고안된 사용자를 위한 컴퓨터 인터페이스이다.
 GUI의 동작은 일반적으로 그래픽 요소의 직접 조작을 통해 수행된다. 컴퓨터를 넘어 GUI는 MP3 플레이어 등, 포터블 미디어 플레이어, 게이밍 장치, 스마트폰, 소형 가전, 사무 및 산업 제어 등 수많은 휴대용 모바일 장치에 사용된다. 
 GUi 예시 사진: 이 사진은 mac바탕화면인데, 이 자체가 GUI이다.
 <img src="https://velog.velcdn.com/images/wklee0607_/post/3f2f0ee4-f246-44ce-bc5d-e1dc8b2e2680/image.png" alt=""></p>
<p>** CLI: 명령 줄 인터페이스(영어: command-line interface, CLI, 커맨드 라인 인터페이스) 또는 명령어 인터페이스는 가상 터미널 또는 터미널을 통해 사용자와 컴퓨터가 상호 작용하는 방식을 뜻한다. 즉, 작업 명령은 사용자가 컴퓨터 키보드 등을 통해 문자열의 형태로 입력하며, 컴퓨터로부터의 출력 역시 문자열의 형태로 주어진다. 
<img src="https://velog.velcdn.com/images/wklee0607_/post/999293d0-c98b-4909-9681-7c1ca13868bc/image.png" alt=""></p>
<br>

<h2 id="1-3-opencv-environment-settingopencv-build">1-3. OpenCV environment setting(OpenCV build)</h2>
<ul>
<li>모든 구성은 terminal(CLI)에서 이루어진다. OpenCV(4.x)</li>
</ul>
<ol>
<li>$ /bin/bash -c &quot;$(curl -fsSL <a href="https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&quot;">https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&quot;</a></li>
<li>$ brew install cmake</li>
<li>$ brew install ffmpeg@4</li>
<li>$ brew link ffmpeg@4</li>
<li>$ mkdir opencv</li>
<li>$ cd opencv</li>
<li>$ git clone <a href="https://github.com/opencv/opencv.git">https://github.com/opencv/opencv.git</a></li>
<li>$ git clone <a href="https://github.com/opencv/opencv_contrib.git">https://github.com/opencv/opencv_contrib.git</a></li>
<li>$ mkdir build</li>
<li>$ cd build</li>
<li>$ cmake -D BUILD_OPENEXR=ON -D WITH_FFMPEG=ON -D OPENCV_EXTRA_MODULES_PATH=../opencv_contrib/modules ../opencv</li>
<li>$ cmake --build .</li>
<li>$ sudo make install</li>
<li>$ make</li>
</ol>
<ul>
<li>mkdir : 폴더 생성</li>
<li>cd : 폴더 들어가기</li>
<li>make : make is GNU make utility to maintain groups of programs, make는 source를 컴파일 하도록 명령하는 것이다. makefile을 참조하여 source file들을 컴파일한다. 여기서 컴파일이란 소스파일을 사용자가 실행 가능한 파일로 만들어 주는 과정을 말한다. make 과정이 끝나고 나면 설치파일(Setup 파일 같은 것)이 생성된 상태라고 볼 수 있다. </li>
<li>makefile : 어떤 프로그램을 컴파일하고 링크해야 하는지 그 방법을 설명한 파일. 이 파일이 있으면, sourcecode에서 변경 사항이 있어도 금방 수정하고 컴파일 할 수 있다. 큰 규모의 프로젝트에서 이 파일이 없다면 파일 하나하나를 컴파일 해야하는 불편함이 있다.  </li>
<li>git clone &#39;url&#39;: 해당 git url의 repository의 파일을 복사해옴.</li>
<li>brew link : 해당 패키지와 연결</li>
<li>cmake --build : cmake tool로 build한다는 뜻</li>
<li>sudo make install : make install은 make를 통해 만들어진 설치파일(setup)을 설치를 하는 과정이다. 한마디로 build된 프로그램을 실행 할 수 있게 파일들을 알맞은 위치에다가 복사를 한다.</li>
</ul>
<p><a href="https://itdexter.tistory.com/325">configure, make, make install에 대한 설명 link</a></p>
<p><br><br></p>
<h1 id="2-computer-vision">2. Computer Vision</h1>
<h2 id="2-1-computer-vision">2-1. Computer Vision?</h2>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/2db82f4e-4530-43bb-ac9b-c47cf3c6be31/image.png" alt=""></p>
<p>&lt;컴퓨터는 위의 사진과 같이 숫자의 행렬로 구성된 사진 및 여러 형태를 인식한다.&gt;</p>
<p>컴퓨터 비전은 인공지능(AI)의 한 분야로, 컴퓨터와 시스템을 통해 위의 행렬처럼 구성된 디지털 이미지, 비디오 및 기타 시각적 입력에서 의미 있는 정보를 추출한 다음 이러한 정보를 바탕으로 작업을 실행하고 추천할 수 있도록 한다. AI를 통해 컴퓨터가 생각할 수 있다면 컴퓨터 비전을 통해서는 컴퓨터가 보고, 관찰하고 이해할 수 있다.</p>
<p>컴퓨터 비전은 인간이 앞서 출발했다는 점을 제외하면 인간의 시각과 메커니즘이 거의 동일하다. 인간의 시력은 사물이 얼마나 멀리 떨어져 있는지, 사물이 움직이는지 여부, 이미지에 문제가 있는지 여부 등으로 사물을 구분하고, 이러한 구분 방법을 평생 학습한다. </p>
<p>컴퓨터 비전은 구분을 수행하도록 머신을 훈련하지만 망막, 시신경, 시각 피질이 아닌 카메라, 데이터 및 알고리즘을 사용하여 훨씬 더 짧은 시간에 수행해야 한다. 제품을 검사하거나 생산 자산을 관찰하도록 훈련된 시스템은 수천 개의 제품 또는 프로세스를 짧은 시간 동안 분석하여 감지할 수 없는 결함이나 문제를 찾아낼 수 있기 때문에 인간의 능력을 금방 앞지를 수 있다.</p>
<p>컴퓨터 비전은 에너지 및 유틸리티부터 제조, 자동차에 이르기까지 다양한 산업에서 사용되며, 시장은 계속 성장할 것으로 예상된다.</p>
<br>


<h2 id="2-2-how-computer-vision-works">2-2. How Computer Vision Works.</h2>
<p>컴퓨터 비전에는 많은 데이터가 필요하며, 차이를 구분하고 궁극적으로 이미지를 인식할 때까지 데이터 분석을 반복적으로 실행한다. 예를 들어 자동차 타이어를 인식하도록 컴퓨터를 훈련시키려면 특히 결함이 없는 타이어를 인식하고 차이점을 학습하기 위해 방대한 양의 타이어 이미지와 타이어 관련 데이터를 공급해야 한다.</p>
<p>이를 위해 두 가지 필수 기술이 사용되는데, 하나는 딥 러닝이라고 하는 일종의 머신 러닝이고 다른 하나는 <a href="https://ko.wikipedia.org/wiki/%ED%95%A9%EC%84%B1%EA%B3%B1_%EC%8B%A0%EA%B2%BD%EB%A7%9D">컨볼루션(합성곱) 신경망(CNN)</a>이다.</p>
<p>머신 러닝은 컴퓨터가 시각적 데이터의 컨텍스트에 따라 스스로 학습할 수 있도록 하는 알고리즘 모델을 사용한다. 이 모델을 통해 충분한 데이터가 공급되면 컴퓨터가 데이터를 &quot;보고&quot;, 이미지를 서로 구별할 수 있도록 스스로 학습한다. 알고리즘을 사용하면 누군가가 이미지를 인식하도록 프로그래밍하는 대신 컴퓨터가 스스로 학습할 수 있다.</p>
<p>CNN은 이미지를 태그 또는 레이블이 지정된 픽셀로 분해하여 머신 러닝 또는 딥 러닝 모델이 &quot;볼 수 있는&quot; 형태로 만든다. 그런 다음 레이블을 사용하여 컨볼루션(세 번째 함수를 생성하는 두 함수에 대한 수학적 연산)을 수행하고 &quot;보고 있는&quot; 것에 대해 예측을 수행한다. 신경망은 컨볼루션을 실행하는데, 예측이 실현되기 전까지 일련의 반복을 통해 예측의 정확성을 확인한다. 그런 다음 인간과 유사한 방식으로 이미지를 인식하거나 보게 된다.</p>
<p>인간이 적당한 거리를 두고 이미지를 만드는 것처럼 CNN은 먼저 명확한 가장자리(hard edge)와 단순한 도형을 구분한 다음 예측을 반복하면서 정보를 채운다. CNN은 단일 이미지를 구분하는 데 사용된다. 순환 신경망(RNN)은 비디오 애플리케이션에서 이와 유사한 방식으로 사용되는데, 컴퓨터가 여러 프레임으로 구성된 그림이 서로 어떻게 관련되어 있는지 이해하는 데 도움이 된다.</p>
<p>[참고] : </p>
<ul>
<li><a href="https://m.blog.naver.com/msnayana/220776380373">CNN 이해하기(개요)</a></li>
<li><a href="https://m.blog.naver.com/msnayana/220781235541">CNN 이해하기(역사)</a></li>
</ul>
<p>[출처] : <a href="https://www.ibm.com/kr-ko/topics/computer-vision">컴퓨터 비전이란? - IBM</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[챌린지]]></title>
            <link>https://velog.io/@wklee0607_/%EC%B1%8C%EB%A6%B0%EC%A7%80-%EA%B3%BC%EC%A0%9C</link>
            <guid>https://velog.io/@wklee0607_/%EC%B1%8C%EB%A6%B0%EC%A7%80-%EA%B3%BC%EC%A0%9C</guid>
            <pubDate>Sat, 13 Aug 2022 14:53:01 GMT</pubDate>
            <description><![CDATA[<h3 id="챌린지-과제">챌린지 과제</h3>
<h2 id="1-heroku일-때-avatar를-바꾸면-이전-파일은-aws에서-삭제하기">1. Heroku일 때, avatar를 바꾸면 이전 파일은 AWS에서 삭제하기</h2>
<p>-middlewares.js</p>
<pre><code class="language-javascript">const isHeroku = process.env.NODE_ENV === &quot;production&quot;;

export const s3DeleteAvatar = (req, res, next) =&gt; {
    if(!isHeroku){
        return next();
    }
    const {session: {user: {avatarUrl}}, file} = req;
    const avatar = file ? (isHeroku ? file.location : file.path) :avatarUrl;
    if(avatarUrl === avatar){
        return next();
    }
    s3.deleteObject({
        Bucket:&quot;wkitube&quot;,
        Key: `images/${avatarUrl.split(&quot;/&quot;)[4]}`
    },(err, data) =&gt; {
        if(err){
            throw err;
        }
        console.log(&quot;s3 deleteObject&quot;, data);
    })
    next();
};</code></pre>
<p>-userRouter</p>
<pre><code class="language-javascript">userRouter.route(&quot;/edit&quot;).all(protectorMiddleware).get(getEdit).post(avatarUpload.single(&quot;avatar&quot;), s3DeleteAvatar,postEdit);</code></pre>
<h2 id="2-heroku일-때-video를-삭제하면-이전-파일은-aws에서-삭제하기">2. Heroku일 때, video를 삭제하면 이전 파일은 AWS에서 삭제하기</h2>
<p>-middlewares.js</p>
<pre><code class="language-javascript">import Video from &quot;./models/Video&quot;;
const isHeroku = process.env.NODE_ENV === &quot;production&quot;;

export const s3DeleteVideo = async (req, res, next) =&gt; {
    if(!isHeroku){
        return next();
    }
    const {session: {user: {_id}}, params:{id}} = req;
    const video = await Video.findById(id);
    if(!video){
        return next();
    }
    if(String(video.owner) !== String(_id)){
        return next();
    }
    s3.deleteObjects({
        Bucket:&quot;wkitube&quot;,
        Delete: {
            Objects: [{Key: `videos/${video.fileUrl.split(&quot;/&quot;)[4]}`},{Key: `videos/${video.thumbUrl.split(&quot;/&quot;)[4]}`}],
        }, 
    },(err, data) =&gt; {
        if(err){
            throw err;
        }
        console.log(&quot;s3 deleteObject&quot;, data);
    })
    next();
};</code></pre>
<p>-videoRouter.js</p>
<pre><code class="language-javascript">videoRouter.route(&quot;/:id([0-9a-f]{24})/delete&quot;).all(protectorMiddleware).get(s3DeleteVideo,deleteVideo);</code></pre>
<ol start="3">
<li>localhost일 때, avatar를 바꾸면 이전 파일은 uploads/avatars파일에서 삭제하기</li>
<li>localhost일 때, video를 삭제하면 이전 파일은 uploads/videos파일에서 삭제하기<h2 id="5-video-좋아요-좋아요-취소-기능-만들기">5. video 좋아요, 좋아요 취소 기능 만들기</h2>
</li>
</ol>
<p>-&gt; 비디오 watch템플릿에 좋아요 버튼을 만들고, 누르면 fake number를 이용하여 숫자가 오르도록 만들었다.</p>
<p>-&gt; video model에 좋아요 누른 사용자의 id가 들어갈 수 있는 array를 만들고, user model에도 좋아요 누른 비디오의 id가 들어갈 수 있는 array를 만듦.</p>
<p>-&gt; 좋아요를 누르면 fetch를 이용해 요청이 백엔드에 보내지고, 백엔드에서 video array에는 user의 id가, user의 array에는 video의 id가 들어가도록 설정해 줌. 그리고 만약 video의 array에 이미 user의 id가 있다면 아이디를 삭제하는 요청을 함(좋아요 취소 기능도 같이 넣은 것임). 이후 프론트엔드로 video의 좋아요 array를 반환해줌. 프론트엔드에선 이 받은 array의 length를 이용해 좋아요 개수를 표현하도록 해줌. 이 때, 새로고침을 하지 않으면 숫자가 바로 보이지 않기 때문에, fake number를 이용해 바로 보이도록 해줬음. fake num은 likeIcon의 classname에 따라 좋아요, 좋아요 취소가 발동되도록 함.</p>
<p>-watch.pug</p>
<pre><code class="language-javascript">if loggedIn
    div.like__box
        span.like Like
            if video.meta.likes.find((id) =&gt; id !== String(loggedInUser._id))
                i.fas.fa-thumbs-up
            else 
                  i.far.fa-thumbs-up
                 span.likeNum=video.meta.likes.length
else 
  span Like
      i.far.fa-thumbs-up
    span=video.meta.likes.length</code></pre>
<p>-likeSection.js - frontend</p>
<pre><code class="language-javascript">import { async } from &quot;regenerator-runtime&quot;;

const videoContainer = document.getElementById(&quot;videoContainer&quot;);
const like = document.querySelector(&quot;.like&quot;);
const likeIcon = like.querySelector(&quot;i&quot;);

const fakeLikeNum = (likes) =&gt; {
    const likeNum = like.querySelector(&quot;.likeNum&quot;);
    likeNum.remove();
    likeIcon.className = &quot;fas fa-thumbs-up&quot;
    const span = document.createElement(&quot;span&quot;);
    span.className = &quot;likeNum&quot;;
    span.innerText = likes.length;
    like.append(span);
};

const fakeDislikeNum = () =&gt; {
    const likeNum = like.querySelector(&quot;.likeNum&quot;);
    const currentLikeNum = likeNum.innerText;
    likeNum.remove();
    likeIcon.className = &quot;far fa-thumbs-up&quot;
    const span = document.createElement(&quot;span&quot;);
    span.className = &quot;likeNum&quot;;
    span.innerText = currentLikeNum - 1;
    like.append(span);
};


const handleLikeClick = async () =&gt; {
    console.log(&quot;like&quot;);
    const { videoid } = videoContainer.dataset;
    const response = await fetch(`/api/videos/${videoid}/like`, {
        method:&quot;POST&quot;,
    }); 
    if(response.status === 200){
        const {likes}= await response.json();
        return fakeLikeNum(likes);
    };
    if(response.status === 404){
       return window.location.reload();
    };
    if(response.status === 204){
        return fakeDislikeNum();
    };
};

if(like){
    like.addEventListener(&quot;click&quot;, handleLikeClick);
}
</code></pre>
<p>-apiRouter</p>
<pre><code class="language-javascript">apiRouter.post(&quot;/videos/:id([0-9a-f]{24})/like&quot;, videoLike);</code></pre>
<p>-videoController - backend</p>
<pre><code class="language-javascript">export const videoLike = async (req, res) =&gt; {
    const {params:{id}, session:{user:{_id}}} = req;
    const user = await User.findById(_id);
    const video = await Video.findById(id);
    const found = video.meta.likes.find((element) =&gt; element !== _id);
    if(!user || !video){
        req.flash(&quot;error&quot;, &quot;video or user is not exists&quot;);
        return res.sendStatus(404);
    }
    if(found){
        user.likes.splice(user.likes.indexOf(id),1);
        video.meta.likes.splice(video.meta.likes.indexOf(_id),1);
        user.save();
        video.save();
        return res.sendStatus(204);
    }
    user.likes.push(id);
    user.save();
    video.meta.likes.push(_id);
    video.save();
    return res.status(200).json({likes : video.meta.likes});
};</code></pre>
<p>-&gt; postㄱ는 같은 Router를 이용하지만, 백엔드에서 좋아요, 좋아요 취소 기능 둘 다 구현할 수 있도록 설정해줬음.</p>
<h2 id="6-kakao-login-구현---restapi로-구현">6. kakao Login 구현 - REST.API로 구현</h2>
<p><a href="https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api">kakao RESTAPI 로그인</a></p>
<p>-&gt; 버튼 구현 (social-login.pug)</p>
<pre><code class="language-javascript">a(href=&quot;/users/kakao/start&quot;).social__btn.social__btn--kakao
        i.fas.fa-comment
        |  Continue with Kakao &amp;rarr;</code></pre>
<p>-&gt; Router구현(userRouter)</p>
<pre><code class="language-javascript">userRouter.get(&quot;/kakao/start&quot;, publicOnlyMiddleware, startKakaoLogin);
userRouter.get(&quot;/kakao/callback&quot;, publicOnlyMiddleware, callbackKakaoLogin);</code></pre>
<p>-&gt; userController로 인가코드(code) 받기 -&gt; 토큰 받기 -&gt; 토큰으로 사용자 정보 받기
client_id &amp; secret code는 env파일에 저장 -&gt; heroku에서도 마찬가지</p>
<pre><code class="language-javascript">export const startKakaoLogin = (req, res) =&gt; {
    const baseUrl = &quot;https://kauth.kakao.com/oauth/authorize&quot;;
    const host = req.host
    let redirect_uri = &quot;&quot;
    if(host === &quot;localhost&quot;){
        redirect_uri = &quot;http://localhost:4000/users/kakao/callback&quot;;
    }
    else {
        redirect_uri = &quot;https://itube-by-wk.herokuapp.com/users/kakao/callback&quot;;
    }
    const config = {
        response_type:&quot;code&quot;,
        client_id:process.env.KAKAO_CLIENT,
        redirect_uri:redirect_uri,
    };
    const params = new URLSearchParams(config).toString();
    const finalUrl = `${baseUrl}?${params}`;
    return res.redirect(finalUrl);
};

export const callbackKakaoLogin = async (req, res) =&gt; {
    const baseUrl = &quot;https://kauth.kakao.com/oauth/token&quot;;
    const host = req.host
    let redirect_uri = &quot;&quot;
    if(host === &quot;localhost&quot;){
        redirect_uri = &quot;http://localhost:4000/users/kakao/callback&quot;;
    }
    else {
        redirect_uri = &quot;https://itube-by-wk.herokuapp.com/users/kakao/callback&quot;;
    }
    const config = {
        grant_type:&quot;authorization_code&quot;,
        client_id:process.env.KAKAO_CLIENT,
        client_secret: process.env.KAKAO_SECRET,
        redirect_url:redirect_uri,
        code: req.query.code,
    };
    const params = new URLSearchParams(config).toString();
    const finalUrl = `${baseUrl}?${params}`;
    const tokenRequest = await (
        await fetch(finalUrl, {
            method:&quot;POST&quot;,
            headers: {
                &quot;Content-Type&quot;: &quot;application/x-www-form-urlencoded;charset=utf-8&quot;,
            }
        })
        ).json();
    if(&quot;access_token&quot; in tokenRequest){
        const {access_token} = tokenRequest;
        const apiUrl = &quot;https://kapi.kakao.com/v2/user/me&quot;;
        const userDataAll = await (await fetch(`${apiUrl}`, {
            headers: {
                Authorization: `Bearer ${access_token}`,
                &quot;Content-type&quot;: &quot;application/x-www-form-urlencoded;charset=utf-8&quot;,
            },
            secure_resource: true,
        })).json();
        const userData = userDataAll.kakao_account;
        let userEmail = userData.email;
        if(!userEmail){
            await fetch(&quot;https://kapi.kakao.com/v1/user/unlink&quot;, {
                headers: {
                    &quot;Content-Type&quot;:&quot;application/x-www-form-urlencoded&quot;,
                    Authorization: `Bearer ${access_token}`,
                },
            });
            req.flash(&quot;error&quot;, &quot;Please agree to collect your email&quot;);
            return res.status(404).redirect(&quot;/login&quot;);
        }
        let user = await User.findOne({ email: userData.email });// github에서 찾은 이메일을 우리의 DB에서 찾는것임.
        if(!user){
            user = await User.create({
                avatarUrl: userData.profile.profile_image_url,
                name: userData.profile.nickname ? userData.profile.nickname : &quot;Unknown&quot;,
                username: userData.profile.nickname ? userData.profile.nickname : &quot;Unknown&quot;,
                email: userEmail ? userEmail : &quot;Unknown&quot;,
                password: &quot;&quot;,
                socialOnly: true,//socialOnly:true -&gt;  소셜 계정으로 만들어진 계정이란 뜻임. 따라서 이 사람은 password가 없으므로 login form을 이용할 수 없음.
                location: &quot;Unknown&quot;,
            });
        }
        req.session.loggedIn = true;
        req.session.user = user;
        return res.redirect(&quot;/&quot;);
    } else {
        return res.redirect(&quot;/login&quot;);
    }
};</code></pre>
<p>오류 발생 : cors 오류 발생. image의 origin이 cors의 origin에 등록되지 않아 발생한 오류. 고치는 법 모르겠음 ㅠㅠ</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/30341679-9f74-42c9-901b-817d7ce96476/image.png" alt=""></p>
<h1 id="7카카오-공유하기-기능-구현">7.카카오 공유하기 기능 구현</h1>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/4a50193c-9b55-4128-a2d3-68feca0380bd/image.png" alt=""></p>
<p><a href="https://developers.kakao.com/docs/latest/ko/message/common#intro">이해하기</a></p>
<p><a href="https://velog.io/@987412563/%EC%B9%B4%EC%B9%B4%EC%98%A4%ED%86%A1-%EA%B3%B5%EC%9C%A0%ED%95%98%EA%B8%B0-API-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B01-%EA%B3%B5%ED%86%B5%EB%A7%81%ED%81%AC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0">참고한 벨로그 -&gt; 템플릿 작성</a></p>
<p><a href="https://developers.kakao.com/docs/latest/ko/message/js-link#custom-template-msg">kakao 공유하기 JS SDK 이용</a></p>
<p>템플릿 작성 후, kakao javascript SDK 다운로드 후 client/js에 kakaoJsSDK파일을 만듦. 이 때 webpack에도 이 파일을 생성한다고 알려주는 코드 작성.</p>
<p>이후 watch 템플릿에 kakaoSDK파일을 추가해준다.</p>
<p>이후 client/js에서 videoShare파일 생성 후 webpack에 동일하게 생성되었다고 알리는 코드 작성. 이후 videoShare.js에 </p>
<pre><code class="language-javascript">const shareBox = document.querySelector(&quot;.share__box&quot;);

//shareVideo
const handleShaerboxClick = () =&gt; {
    const shareContainer = document.querySelector(&quot;#shareContainer&quot;);
    shareContainer.classList.remove(&quot;hidden&quot;);

    //link에 주소 넣기
    const input = shareContainer.querySelector(&quot;.share__link input&quot;);
    input.value = window.location.href;

    //닫기 버튼
    const exit = shareContainer.querySelector(&quot;.share__header i&quot;);
    exit.addEventListener(&quot;click&quot;, () =&gt; {
        shareContainer.classList.add(&quot;hidden&quot;);
    })

    // 공유하기
    const kakaoShare = shareContainer.querySelector(&quot;.share__icons-kakao&quot;);
    if(kakaoShare){
        Kakao.init(&#39;a49bae0fc61fcf4a825e3df56b72ff6d&#39;);
        Kakao.isInitialized();
        kakaoShare.addEventListener(&quot;click&quot;, () =&gt; {
            Kakao.Share.createCustomButton({
                container: kakaoShare,
                templateId: 82088,
                templateArgs: {
                    &#39;title&#39;: &#39;itube&#39;,
                    &#39;description&#39;: &#39;개발 공부중인 이원규가 NodeJS로 구현한 첫 사이트 itube&#39;
            }
      });
        })
    }
}


if(shareBox){
    shareBox.addEventListener(&quot;click&quot;, handleShaerboxClick);
}</code></pre>
<p>코드 작성 후, watch 템플릿에 script추가 ㄱ ㄱ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 17.3 Deploying to Heroku]]></title>
            <link>https://velog.io/@wklee0607_/17.3-Deploying-to-Heroku</link>
            <guid>https://velog.io/@wklee0607_/17.3-Deploying-to-Heroku</guid>
            <pubDate>Sun, 07 Aug 2022 10:59:30 GMT</pubDate>
            <description><![CDATA[<h1 id="173-deploying-to-heroku">17.3 Deploying to <a href="https://www.heroku.com/">Heroku</a></h1>
<p>-&gt; Heroku는 아주 멋진 웹사이트임. 서버를 아주아주 빠르게 배포할 수 있음. Heroku 계정을 만들고 나의 Dashboard에서 새 앱을 만들 것임.
id: <a href="mailto:fovert@khu.ac.kr">fovert@khu.ac.kr</a>
password: github와 동일함.</p>
<ol>
<li><p>create new app 클릭
<img src="https://velog.velcdn.com/images/wklee0607_/post/f09946bb-40f1-4c72-a732-427df7ae64bb/image.png" alt=""></p>
</li>
<li><p>뜨는 창에서 app name, region등을 적고 생성</p>
</li>
<li><p>Heroku에 백엔드 서버를 업로드 해주기.(2가지 방법이 있음)
 3-1. Heroku git이용한 방법. 
 3-2. github를 이용한 방법.
 <img src="https://velog.velcdn.com/images/wklee0607_/post/6622c79b-0672-4888-9c58-036e86e446bc/image.png" alt=""></p>
</li>
</ol>
<h4 id="3-1-heroku-git이용한-방법">3-1. Heroku git이용한 방법.</h4>
<ol>
<li><p><a href="https://devcenter.heroku.com/articles/heroku-cli">Ubantu 설치</a> -&gt;  brew tap heroku/brew &amp;&amp; brew install heroku
설치가 잘 됐는지 확인하려면 터미널에 heroku login 치면 됨.</p>
</li>
<li><p>vscode 터미널에 heroku login치면 로그인 할지 물어봄. 이 때 아무 키나 누르면 웹브라우저에 로그인 창이 뜰거고 그걸 로그인 하면 heroku-cli에 연결됨.
<img src="https://velog.velcdn.com/images/wklee0607_/post/65908119-ec14-4ce9-91c9-f35709b665dd/image.png" alt=""></p>
</li>
</ol>
<p>이게 heroku-cli에 연결됐다는 뜻임.
<img src="https://velog.velcdn.com/images/wklee0607_/post/acc2c77e-5ac8-4b21-891d-d2098ff68e56/image.png" alt=""></p>
<ol start="3">
<li>내 프로젝트 안으로 이동하기.(프로젝트가 git repository가 있게 하고 이를 heroku와 연결해주기 : heroku git:remote -a itube-by-wk). 내 프로젝트는 git repository가 필요함. 그래서 git init을 해야함. 아무튼 git repository가 있는지 확인하고 
heroku git:remote -a itube-by-wk을 터미널에 ㄱ ㄱ 하셈.<pre><code>만약 repostory가 있다면
heroku git:remote -a itube-by-wk 고고
</code></pre></li>
</ol>
<p>없다면 이거 고고
git add .
$ git commit -am &quot;make it better&quot;</p>
<p>위에 꺼 두개 중 하나 한 뒤, 이거 실행 -&gt; 5번에서 고고
$ git push heroku master</p>
<pre><code>이게 뭔 의미냐면, git에서 add, commit등등을 하고 push를 heroku에 할 수 있다는 의미임.

git을 사용할 때 우린 보통 이걸 사용함.</code></pre><p>git init
git commit 하고
git push origin master 고고함.</p>
<p>```</p>
<p>쨋든 거두절미하고, heroku git:remote -a itube-by-wk까지 완료하고 git push heroku master 하기 전에 알아둬야 할 것. heroku는 우리의 git history만 보기 때문에, commit을 하지 않으면 서버에 반영되지 않음. 따라서 반영하려면 commit &amp; push해줘서 서버에 반영해야함.</p>
<ol start="4">
<li>git push heroku master 하기 전에 서버를 만들었을 때, 터미널 창을 하나 더 만들어서 heroku logs --tail을 실행함. 이는 server나 heroku의 로그를 볼 수 있게 해줌.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/d6ada6f2-4d4c-463f-9e47-82164d611365/image.png" alt=""></p>
<ol start="5">
<li>git push heroku master 실행 고고.(zsh에서) 이러면 에러가 날 텐데, 이는 heroku가 git의 repostory만 보기 때문에 그럼. env에 mongoDB url이 있는데 이 env파일은 .gitignore해버려서 mongoDB url을 heroku가 볼 수 없음.</li>
</ol>
<p>mongo가 연결되지 않아서 그럼.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/cd935ef0-42d5-4d0b-a909-8f258a83dc6a/image.png" alt=""></p>
<h2 id="다음-수업에서는-mongodb-url을-heroku에-전달해줄-것임">다음 수업에서는 mongoDB url을 heroku에 전달해줄 것임.</h2>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 17.2 Building the Frontend]]></title>
            <link>https://velog.io/@wklee0607_/17.2-Building-the-Frontend</link>
            <guid>https://velog.io/@wklee0607_/17.2-Building-the-Frontend</guid>
            <pubDate>Sun, 07 Aug 2022 09:02:53 GMT</pubDate>
            <description><![CDATA[<h1 id="172-building-the-frontend">17.2 Building the Frontend</h1>
<p>=&gt; webpack은 2가지 모드가 있음.</p>
<ol>
<li>development 2. production. 이 때 production코드가 훨씬 작음. 그래서 assets(frontend)을 빌드하고 싶음.</li>
</ol>
<h4 id="assetsfrontend을-빌드">assets(frontend)을 빌드</h4>
<ul>
<li><ol>
<li>webpack.config.js<pre><code class="language-javascript">mode: &quot;development&quot;, -&gt; 삭제하기(build, development일 때 각자 설정해주기 위함.)
watch: true, -&gt; 삭제하기 (이거는 mode가 development일 때만 작동해야함. package.json에서 -w로 개발자모드일 때만 watch:true 설정해주기)</code></pre>
</li>
</ol>
</li>
<li><ol start="2">
<li>package.json<pre><code class="language-javascript">//추가
&quot;build:assets&quot;: &quot;webpack&quot;,
</code></pre>
</li>
</ol>
</li>
</ul>
<p>//수정
&quot;dev:assets&quot;: &quot;webpack --mode=development&quot;</p>
<p>//최종
&quot;scripts&quot;: {
    &quot;start&quot;: &quot;node build/init.js&quot;,
    &quot;build&quot;: &quot;npm run build:server &amp;&amp; npm run build:assets&quot;, //server, assets을 동시에 build해줌 -&gt; 이렇게 build된 코드는 어느 브라우저에서도 이해 ㄱㄴ
    &quot;build:server&quot;: &quot;babel src -d build&quot;,
    &quot;build:assets&quot;: &quot;webpack --mode=production&quot;,//추가 -&gt; build할 떄
    &quot;dev:server&quot;: &quot;nodemon&quot;,
    &quot;dev:assets&quot;: &quot;webpack --mode=development -w&quot;//수정 -&gt; development할 때/ -w: watch모드 true라는 뜻임.
  },</p>
<pre><code>
참고 : [webpack/api/cli](https://webpack.js.org/api/cli/)

npm run build:assets 하면 assets 폴더 내의 파일들의 코드가 production용 코드로 바뀔 것이고

![](https://velog.velcdn.com/images/wklee0607_/post/ee919c36-50fc-467a-9798-ae8345bfee8e/image.png)

npm run dev:server 하면 assets 폴더 내의 파일들의 코드가 development코드로 바뀔 것임.
![](https://velog.velcdn.com/images/wklee0607_/post/e50e4be3-ca80-44b4-af60-c3a674b2d7e2/image.png)

상대적으로 build의 코드가 더 짧다.
이렇게 설정해주면 development이든 production이든 변경없이 같은 webpack파일을 사용할 수 있음. 두 모드의 차이점은 1.모드 2.watch 설정이라 이 두개만 package.json에서 작동할 때 따로 설정해두고, 다른 건 같이 사용하기.

-&gt; 
문제: bulid:server할 때, src폴더를 변환했는데 이 때, 백엔드의 폴더만 변환해줘야 하는데 client폴더까지 변환해버림.(client코드는 webpack으로 변환해주기 때문에, babel로 하면 안됨.)

해결: 이걸 해결할 수 있음.이제 build:sever를 하면 build:sever는 build:assets을 보게 될 것임.
build:assets을 실행하고 build:server를 실행해준 뒤, npm start 하면 localhost:4000이 실행될 것임. npm run build만 해도 두개가 동시에 실행됨.

### 이 다음 수업에서는 Heroku의 내부에 이 서버를 둘 것임. 아마 에러가 발생할 ㅡ텐데 DB가 없는 서버로 이동했기 떄문일 것임.
</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[# 17. 0-1 Building the Backend - 백엔드를 실제 서버로 배포]]></title>
            <link>https://velog.io/@wklee0607_/17.0-Building-the-Backend-%EB%B0%B1%EC%97%94%EB%93%9C%EB%A5%BC-%EC%8B%A4%EC%A0%9C-%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%B0%B0%ED%8F%AC</link>
            <guid>https://velog.io/@wklee0607_/17.0-Building-the-Backend-%EB%B0%B1%EC%97%94%EB%93%9C%EB%A5%BC-%EC%8B%A4%EC%A0%9C-%EC%84%9C%EB%B2%84%EB%A1%9C-%EB%B0%B0%ED%8F%AC</guid>
            <pubDate>Sat, 06 Aug 2022 02:42:25 GMT</pubDate>
            <description><![CDATA[<p><a href="https://jbee.io/etc/Everything-about-babel/">babel의 모든 것</a></p>
<h1 id="170-building-the-backend">17.0 Building the Backend</h1>
<p>-&gt; Heroku를 통해 배포.
-&gt; 배포 하려면 어떤 Node.js 환경에서도 실행될 수 있도록 설정을 바꿔야함.
-&gt; DB도 바꿔야함. DB가 지금은 localhost에서만 작동함.
-&gt; 그리고 파일들을 우리 서버가 아니라 아마존에 올려야함.
-&gt; webpack.config: 우리 코드를 production 방법으로 빌드해야 되고, 코드를 압축해야함.</p>
<p> 이번 섹션에서는 실제 서버에서 백엔드를 실행하는 모든 단계를 해볼 것임.</p>
<ol>
<li>우리가 만든 코드를 실행하려면, nodemon을 사용해서 babel-node를 실행함. babel-node는 실제로 서비스 되는 곳이 아니라 개발할 때만 사용되는 목적으로 씀. 왜냐면 babel-node는 나의 최신 JS코드를 실행할 수 있게 도와주기 떄문임. 코드를 바꾸지 않고 babel-node가 우리 코드를 실행해줌. 그치만 babel-node를 사용하면 우리 performance에 문제가 생김. babel-node는 그렇게 빠르지 않음. 그래서 이 init.js를 다른 babel을 이용하여 일반적인 javascript코드로 바꿔야함. 
<strong>즉, 백엔드의 코드를 완전 안 멋진 옛 코드로. 그래서 이걸 위해서-&gt; babel-CLI를 사용할 것임.(모든 브라우저가 이해할 수 있도록 옛 코드로 변환)</strong></li>
</ol>
<h2 id="1-babel-cli">1. <a href="https://babeljs.io/setup#installation">babel-CLI</a></h2>
<ol>
<li><p>설치</p>
<pre><code>npm install --save-dev @babel/core @babel/cli</code></pre></li>
<li><p>script 만들기</p>
</li>
</ol>
<ul>
<li>예시
```
{
  &quot;name&quot;: &quot;my-project&quot;,
  &quot;version&quot;: &quot;1.0.0&quot;,</li>
<li>&quot;scripts&quot;: {</li>
<li>&quot;build&quot;: &quot;babel src -d lib&quot;</li>
<li>},
&quot;devDependencies&quot;: {
  &quot;@babel/cli&quot;: &quot;^7.0.0&quot;
}
}<pre><code></code></pre></li>
</ul>
<p>(전)</p>
<ul>
<li>package.json<pre><code class="language-javascript">&quot;scripts&quot;: {
  &quot;build:server&quot;: &quot;babel src/init.js -d build&quot;, // src가 아니라 우리는 src/init.js의 코드를 변환하고자 해서 이렇게 적음. -d로 directory지정(빌드한 코드를 어디에 저장할 건지).
  &quot;dev:server&quot;: &quot;nodemon&quot;,
  &quot;dev:assets&quot;: &quot;webpack&quot;
},</code></pre>
</li>
</ul>
<p>-&gt; npm run build:server 를 하면 init.js의 변환된 코드가 들어간 bulid폴더 생성됨.
-&gt; babel-CLI는 예를 들면 babel-node처럼 실행하지 않아. nodemon은 파일을 실행하고 그 파일이 모든 걸 실행함. babel의 경우, 한 파일만 실행하는 것이 아니라, 모든 폴더를 빌드해서 실행해야함.
즉, babel-node는 init만 변환해서 실행하면 init이 관련된 모든 파일을 전부 실행시켰지만, babel-CLI는 init만 변환해서 실행함. 따라서 babel build를 src/init이 아니라 src전체로 해줘야함.</p>
<p>(후)</p>
<ul>
<li>package.json<pre><code class="language-javascript">&quot;scripts&quot;: {
  &quot;build:server&quot;: &quot;babel src -d build&quot;, // src 전체 코드를 변환해줘야함. -d로 directory지정(빌드한 코드를 어디에 저장할 건지).
  &quot;dev:server&quot;: &quot;nodemon&quot;,
  &quot;dev:assets&quot;: &quot;webpack&quot;
},</code></pre>
</li>
</ul>
<p>-&gt; npm run build:server 를 하면 build 폴더에 변환된 src폴더의 파일들이 생성될 것임. &amp; 이 build는 크기가 커서 .gitignore에 등록해야함 </p>
<ul>
<li>gitignore <pre><code>/build</code></pre></li>
</ul>
<ol start="3">
<li>bulid:server할 때, src폴더를 변환했는데 이 때, 백엔드의 폴더만 변환해줘야 하는데 client폴더까지 변환해버림.(client코드는 webpack으로 변환해주기 때문에, babel로 하면 안됨.)</li>
</ol>
<p>-&gt; 나중에 고칠 거임.</p>
<ol start="4">
<li>변환된 build의 init.js파일 실행하는 script추가. (=babel-node일 때, init.js 실행한 것과 같은 역할임. dev:server와 같은 역할이라 보면됨.)</li>
</ol>
<pre><code class="language-javascript">&quot;scripts&quot;: {
    &quot;start&quot;: &quot;node build/init.js&quot;, // 이거 추가 -&gt; 변환된 코드라 굳이 babel-node로 해주지 않아도 됨. babel을 사용하지 않아도 됨.
    &quot;build:server&quot;: &quot;babel src -d build&quot;,
    &quot;dev:server&quot;: &quot;nodemon&quot;,
    &quot;dev:assets&quot;: &quot;webpack&quot;
  },</code></pre>
<p>-&gt; npm start</p>
<h2 id="수정사항">수정사항</h2>
<ol>
<li>bulid:server할 때, src폴더를 변환했는데 이 때, 백엔드의 폴더만 변환해줘야 하는데 client폴더까지 변환해버림.(client코드는 webpack으로 변환해주기 때문에, babel로 하면 안됨.)</li>
</ol>
<ol start="2">
<li><p>-&gt; npm start하면  </p>
<h4 id="regeneratorruntime-is-not-defined-에러가-날-것임--이-에러는-async와-await을-사용하려고-했을-때-발생한-에러와-같음">regeneratorRuntime is not defined 에러가 날 것임. ( 이 에러는 async와 await을 사용하려고 했을 때 발생한 에러와 같음.)</h4>
<p>즉, 우리 코드가 async와 await을 사용할 때 regenerator가 필요한데 없음. 
이 문제는 우리 코드가 async와 await을 사용할 수 있도록 알려줘야함</p>
</li>
<li><p>build파일에 pug파일이 없어서 넣어줘야함.</p>
</li>
</ol>
<h1 id="171-building-the-backend-part-two">17.1 Building the Backend part Two</h1>
<h4 id="문제-1-regeneratorruntime-is-not-defined-에러-고치기">문제 1: regeneratorRuntime is not defined 에러 고치기</h4>
<p>=&gt; 
 await, async를 사용할 수 없을 때, 나타나는 에러임. 예전에도 이런 오류가 났었음. clent/js에서 났었음. 이에 따라 우리는 client/js/main.js에 </p>
<pre><code class="language-javascript">import  &quot;regenerator-runtime/runtime.js&quot; ;</code></pre>
<p>를 해줬음.</p>
<p> 이번에는 client폴더가 아닌 src폴더 전체에(정확히는 src를 옛코드로 변환해준 build폴더) await, async가 먹히질 않아서 그럼(최신 코드에서는 뭔가를 import하지 않아도 사용가능하지만 옛 코드로 바꾸면서 뭔가를 import해줘야 함.) 따라서 우리는 init.js에</p>
<pre><code class="language-javascript">import  &quot;regenerator-runtime/runtime.js&quot; ;</code></pre>
<p>를 import해주면 오류 해결 !</p>
<p> 이후, 다시 npm run build:server로 새로 추가된 코드가 포함된 뒤, 옛코드로 변환된 src폴더를 만들고, npm run start하면 됨.</p>
<p>이제 babel이 우리 서버에서 실행되지 않고. node.js가 우리 서버에서 실행되고 있게됨. 이제 babel의 도움 필요 없이, node.js가 이 코드를 모두 이해하고 있음.</p>
<h4 id="문제-2-build된-src폴더-즉-새로-생긴-build폴더에는-views-파일이-생성되지-않음">문제 2: build된 src폴더 즉, 새로 생긴 build폴더에는 views 파일이 생성되지 않음.</h4>
<p>=&gt; 
답은 server.js에 있음. server.js에서 views의 directory경로를 보면</p>
<pre><code class="language-javascript">app.set(&quot;views&quot;,process.cwd() + &quot;/src/views&quot;);//process.cwd: 현재 작업중인 파일 위치.</code></pre>
<p>이렇게 되어있음. views폴더는 현재 working directory(=cwd)에서 인데, 그건 node를 실행한 위치를 말함. package.json을 가진 폴더의 위치를 의미하고 root폴더를 말함. build와 src의 바깥 폴더를 말함. 그 뒤에 &quot;/src/views&quot; 이렇게 되어있는데, build의 views는 src/views를 가져와서 사용하고 있음. 따라서 우리는 src/views폴더를 잘 보존하면 됨. 따로 build폴더에 안 넣어줘도 됨.</p>
<p>!) 만약 build폴더에 넣고 싶다면 src/views폴더를 복사하고 build폴더에 넣고, </p>
<pre><code class="language-javascript">app.set(&quot;views&quot;,process.cwd() + &quot;/build/views&quot;);</code></pre>
<p>로 수정하면 됨.</p>
<p>----------------------------backend build 완료 ---------------------------</p>
<h2 id="">\\</h2>
<p>----------------------------client코드(assets) build 해야함 -----------------------
다음 영상에서는 클라이언트 코드를 빌드하고 좀 더 프로답게 만들 것임. production을 위해 만들 것임. 왜냐하면 assets/js의 파일들의 코드가 압축되지 않았기 떄문임. production용으로 만들고, 코드도 압축해야ㅕ함. 또한 우리의 빌드 서버는 환경 변수에 접근할 수 있음.(.env에 접근 가능하다는 소리임)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 16.0-8 댓글창 만들기]]></title>
            <link>https://velog.io/@wklee0607_/16.0-7-%EB%8C%93%EA%B8%80%EC%B0%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</link>
            <guid>https://velog.io/@wklee0607_/16.0-7-%EB%8C%93%EA%B8%80%EC%B0%BD-%EB%A7%8C%EB%93%A4%EA%B8%B0</guid>
            <pubDate>Sat, 30 Jul 2022 12:44:23 GMT</pubDate>
            <description><![CDATA[<h1 id="댓글창-만들기">댓글창 만들기</h1>
<p>-&gt; mongoose로 모델 만들기, 컨트롤러, route, 바닐라JS, status code, populate, mongoose relationship 등 여태 배운걸 이용할 것임.</p>
<h2 id="161-comment-models--relation-ship-with-user-video">16.1. Comment Models &amp; relation ship with User, Video</h2>
<ol>
<li>Comment.js 만들기 in models폴더</li>
<li>Comment.js 작성</li>
</ol>
<p>-&gt; text, video, createdAt은 자동으로 그 내용이 추가될 것임. owner에 대해서는 session을 이용하여 누가 댓글의 주인인지 추가해줄 것임.</p>
<pre><code class="language-javascript">import mongoose from &quot;mongoose&quot;;

const commentSchema = new mongoose.Schema({
    text: {type:String, required:true },
    owner: { type: mongoose.Schema.Types.ObjectId, required: true, ref:&quot;User&quot; }, // User와 mongoose relationship
    video: { type: mongoose.Schema.Types.ObjectId, required: true, ref:&quot;Video&quot; }, // Video와 mongoose relationship
    createdAt: {type:Date, required:true, default:Date.now },
});

const Comment = mongoose.model(&quot;Comment&quot;, commentSchema); //&quot;Comment&quot;는 다른 Schema 등에서 ref로 사용되는 곳과 일치하도록 써줘야함.

export default Comment;</code></pre>
<ol start="3">
<li>Video model에서 video가 많은 댓글을 가지도록 Video Schema 수정</li>
</ol>
<p>-&gt; 비디오는 해당 비디오에 달린 댓글들을 comments array로 가짐.</p>
<pre><code class="language-javascript">//추가
comments: [{type: mongoose.Schema.Types.ObjectId, required: true, ref:&quot;Comment&quot;}], // ref는 const Comment = mongoose.model(&quot;Comment&quot;, commentSchema); 여기에서의 &quot;Comment&quot; 즉, 첫번째 인수와 같아야 함. -&gt; Comment와 mongoose relationship</code></pre>
<ol start="4">
<li>User가 많은 댓글을 가질 수 있도록 User Schema 수정</li>
</ol>
<p>-&gt; 사용자는 자기가 달은 댓글들을 comments array로 가지는 것임.</p>
<pre><code class="language-javascript">//추가
comments :[{type:mongoose.Schema.Types.ObjectId, ref:&quot;Comment&quot; }],  //-&gt; Comment와 mongoose relationship</code></pre>
<ol start="5">
<li>init.js에 comment model import하기<pre><code>import Comment from &quot;./models/Comment&quot;;</code></pre></li>
</ol>
<h2 id="162-comment-box">16.2 Comment Box</h2>
<ol>
<li>client/js/commentSection.js 파일 만들기</li>
<li>webpack이 인식하도록 만들기 (entry, output)</li>
</ol>
<ul>
<li>webpack.config.js<pre><code class="language-javascript">const BASE_JS = &quot;./src/client/js/&quot;;
</code></pre>
</li>
</ul>
<p>//entry 추가
commentSection: BASE_JS + &quot;commentSection.js&quot;,</p>
<p>// output은 이전의 설정으로 인해 자동 추가  </p>
<pre><code>
3. watch.pug에 commentSection.js import해주기 &amp; 댓글html 만들기
```javascript
block content
    if loggedIn
      div.video__comments
          form.video__comment-form#commentForm
              textarea(cols=&quot;30&quot;, rows=&quot;10&quot;, placeholder=&quot;Write a nice comment..&quot;)
              button Add Comment

if loggedIn 
        script(src=&quot;/assets/js/commentSection.js&quot;)</code></pre><ol start="4">
<li>commentSection.js 작성<pre><code class="language-javascript">const videoContainer = document.getElementById(&quot;videoContainer&quot;);
const form = document.getElementById(&quot;commentForm&quot;);


</code></pre>
</li>
</ol>
<p>const handleSubmit = (event) =&gt; {
    event.preventDefault();
    const textarea = form.querySelector(&quot;textarea&quot;);
    const text = textarea.value;
    const video = videoContainer.dataset.videoid</p>
<p>};</p>
<p>if(form){
    form.addEventListener(&quot;submit&quot;, handleSubmit);
} // form이 로그인 한 상태에서만 보이기 떄문임.</p>
<pre><code>

-&gt; comment에 대해서 text, video, createdAt은 자동으로 그 내용이 추가될 것임. owner에 대해서는 session을 이용하여 누가 댓글의 주인인지 추가해줄 것임.

![](https://velog.velcdn.com/images/wklee0607_/post/162aa06d-18f5-45fb-bc0b-fa1b4e111ee0/image.png)

![](https://velog.velcdn.com/images/wklee0607_/post/c760a9f7-5145-4f87-8830-dc62ef207e7d/image.png)

![](https://velog.velcdn.com/images/wklee0607_/post/322058b3-52ce-4b94-9677-cac034c53ba4/image.png)
-&gt; const video = videoContainer.dataset.videoid 로 불러서 쓸 수 있음.


## 16.3-4 API Router
#### comment api router 만들기 &amp; fetch로 api로 데이터 보내기

1. comment api router 만들기
- apiRouter.js
```javascript
//추가
apiRouter.post(&quot;/videos/:id([0-9a-f]{24})/comment&quot;, createComment); // comment api router추가</code></pre><ul>
<li>videoController - createComment<pre><code class="language-javascript">export const createComment = (req, res) =&gt; {
  console.log(req.params); // 이거는 결과값이 잘 나오는데
  console.log(req.body);// 이게 결과값이 안 나옴 -&gt; 몇가지 고쳐줘야함 ! 서버가 이해할 수 있도록 고쳐줘야함.
  res.end();
};</code></pre>
</li>
</ul>
<ol start="2">
<li>fetch로 api로 데이터 보내기</li>
</ol>
<ul>
<li>commentSection.js<pre><code class="language-javascript">const videoContainer = document.getElementById(&quot;videoContainer&quot;);
const form = document.getElementById(&quot;commentForm&quot;);
</code></pre>
</li>
</ul>
<p>const handleSubmit = (event) =&gt; {
    event.preventDefault();
    const textarea = form.querySelector(&quot;textarea&quot;);
    const text = textarea.value;
    const videoId = videoContainer.dataset.videoid
    fetch(<code>/api/videos/${videoId}/comment</code>, {
        method: &quot;POST&quot;,
        body: {// req.body를 만드는 과정, 해당 url로 req.body형태로 text의 값을 post한다는 뜻임.
            text,
        }
    })
      textarea.value =&quot;&quot;;
};</p>
<p>if(form){
    form.addEventListener(&quot;submit&quot;, handleSubmit);
} // form이 로그인 한 상태에서만 보이기 떄문임.</p>
<pre><code>
## 16.4 API Router - api로 보낸 value comment를 백엔드가 이해하기
-&gt; (commentSection) handleSubmit으로 프론트엔드에서 api로 fetch를 통해 데이터(JSON 형태) POST하기 -&gt; 미드웨어를 이용해 보낸 데이터(JSON형태) 백엔드에서 이해하기 -&gt; Router를 이용하여 해당 api url에 대한 Route를 만든 뒤, 컨트롤러를 이용하여 받아온 data를 사용하기.

### -&gt; 문제: 1번의 console.log(req.body)가 빈 창으로 나오는 오류를 볼 수 있음.
![](https://velog.velcdn.com/images/wklee0607_/post/2a8a2cf4-be7b-44f7-82de-e47a90524d27/image.png)

알아둘 거:
1. **웹사이트로 들어오는 form을 이해하도록 만들어주는 미드웨어**
server.js에서 app.use(express.urlencoded({extended:true}));
//이 미드웨어는 form의 내용을 js의 array object 형태로 바꿔줌-&gt; req.body를 생성함! [urlencoded에 관한 내용](https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&amp;blogId=awesomedad&amp;logNo=220748168859)

#### 해결책 1: 우리가 프론트엔드에서 fetch로 보내는 데이터를 text형태로 만들고, 우리의 백엔드가 이를 이해하도록 하는 미드웨어를 추가해줘야함. 이 때는 한 가지의 데이터만 보낼 수 있음.

- commentSection.js
```javascript
fetch(`/api/videos/${videoId}/comment`, {
        method: &quot;POST&quot;,
        body:text, // 이런 식으로 {} 없이 데이터를 보내주면 백엔드는 이를 text형식으로 이해함.
    })</code></pre><ul>
<li>server.js<pre><code class="language-javascript">app.use(express.text());// 우리의 백엔드는, 누군가 text를 보내면 그걸 이해하고, 그걸 req.body에 넣어줄 것임.</code></pre>
</li>
</ul>
<p>참고</p>
<ul>
<li><p><strong><a href="https://expressjs.com/ko/api.html">express api</a> 너무 중요함 !</strong></p>
</li>
<li><p><a href="https://expressjs.com/ko/api.html#express.text">express.text([options])</a>
Express에 내장된 미들웨어 기능입니다.
body-parser를 기반으로 request payload로 전달한 문자열을 파싱합니다.</p>
</li>
<li><p><a href="https://gomakethings.com/how-to-send-data-to-an-api-with-the-vanilla-js-fetch-method/#sending-data-as-a-json-object">fetch()를 이용해서 JSON객체 보내기</a></p>
</li>
</ul>
<h4 id="해결책-2-우리가-앞으로-백엔드에-보낼게-text뿐만이-아니라-rating등도-보낼-수-있음-즉-object형태로-보내줄-수-있기-때문에-보내줄-때는-jsonstringify로-object를-string의-형태로-백엔드로-보내주고-백엔드에서는-이를-jsonparse를-실행하도록-하는-미드웨어를-선언해줘서-이-object를-이해할-수-있도록-해줘야함-이-때-백엔드로-보내주는-data가-json형식인-것도-알려줘야함-이-때는-여러가지의-데이터를-보낼-수-있음object형식이라서">해결책 2: 우리가 앞으로 백엔드에 보낼게 text뿐만이 아니라 rating등도 보낼 수 있음. 즉, object형태로 보내줄 수 있기 때문에 보내줄 때는 json.stringify로 object를 string의 형태로 백엔드로 보내주고, 백엔드에서는 이를 json.parse를 실행하도록 하는 미드웨어를 선언해줘서 이 object를 이해할 수 있도록 해줘야함. 이 때 백엔드로 보내주는 data가 json형식인 것도 알려줘야함. 이 때는 여러가지의 데이터를 보낼 수 있음(object형식이라서)</h4>
<p>-&gt; fetch로 data를 보낼 때, 보통 object data를 json형태로 보내는 것이 국룰임.
-&gt; frontend에서 backend로 매우 큰 object를 보내야 할 때 쓰는 방법임. </p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c9ebe283-95e8-4757-8f41-a86d9536ae84/image.png" alt=""></p>
<p><strong>1. 프론트엔드에서 json.stringify를 이용하여 object를 string의 형태로 백엔드에 보냄. 이 때 보내는 data가 json형태인 것을 알려줘야함(Content-Type: application/json). 왜냐하면 express는 기본적으로 데이터를 text형태로 인식하기 때문임.</strong></p>
<ul>
<li>commentSection.js<pre><code class="language-javascript">const handleSubmit = (event) =&gt; {
  event.preventDefault();
  const textarea = form.querySelector(&quot;textarea&quot;);
  const text = textarea.value;
  const videoId = videoContainer.dataset.videoid
  if(text === &quot;&quot;){
      return; //사용자가 아무것도 입력하지 않으면 req를 보내지 않음.
  }
  fetch(`/api/videos/${videoId}/comment`, {
      method: &quot;POST&quot;,
      headers : {// header: 기본적으로 requests에 대한 정보를 담고 있음. 따라서 request에 추가할 수 있는 정보임. 이를 이용하여 Express에게 보내는 데이터가 json형태라고 알림.
          &quot;Content-Type&quot; : &quot;application/json&quot;, // Content-Type을 해주는 이유: 우리가 백엔드로 보내는 데이터 Type이 json형식임을 알려주는 것임. -&gt; 이걸 처리해주지 않으면 백엔드는 여전히 text를 보낸다고 알 것임.
      },
      body: JSON.stringify({ // 보내줄 때 JSON.stringify형태로 보내줌.
          text,
      })
  })
    textarea.value =&quot;&quot;;
};
</code></pre>
</li>
</ul>
<p>if(form){
    form.addEventListener(&quot;submit&quot;, handleSubmit);
} // form이 로그인 한 상태에서만 보이기 떄문임.</p>
<pre><code>
**2. 백엔드에서는 json형태로 온 data를 json.parse를 통해 이해하도록 해주는 미드웨어를 사용하여 이를 이해해야함**

- server.js
```javascript
app.use(express.json()); // express.json: 이건 프론트엔드에서 json.stringify로 보내준 object data를 json형태로 이해시켜서 object로 이해시키고, 이를 req.body에 넣어줌.</code></pre><p>참고</p>
<ul>
<li><a href="https://expressjs.com/ko/api.html#express.json">express.json([options])</a>
Express에 내장된 미들웨어 기능입니다.
body-parser를 기반으로 request payload로 전달한 JSON을 파싱합니다.
문자열을 받아서 json으로 바꿔줍니다.
주의할 점은 express.json()은 header에 Content-Type이 express.json()의 기본 값인 &quot;application/json&quot;과 일치하는 request만 보는 미들웨어를 반환합니다.
다시 말해, headers: { &quot;Content-type&quot;: &quot;application/json&quot; }인 request만 express.json()을 실행한다.</li>
</ul>
<h2 id="165-commenting---comment한-걸-백엔드에서-컨트롤러를-이용해-comment-생성-in-mongodb">16.5 Commenting -&gt; comment한 걸 백엔드에서 컨트롤러를 이용해 comment 생성 in mongoDB</h2>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c998d6bc-7bff-4c92-929d-774a8929379d/image.png" alt="">
우리가 댓글을 쓰고 POST를 하면, 쿠키와 함께 comment가 보내지는 걸 알 수 있음.
-&gt; 백엔드에 쿠키를 보내면, 백엔드는 세션을 찾아봄. 그 말은 우리가 그냥 api로 fetch requsets를 해서 POST하기만 해도 우리의 쿠키는 자동으로 브라우저에 의해 전송되고, 백엔드는 세션을 찾음. 그 말은 Controller에서 우리가 쓸 수 있는 뭔가(req.session)가 있다는 것임. </p>
<p>원리: 브라우저가 localhost:4000 <del>~ 의 프론트엔드에서 localhost:4000 ~</del>의 백엔드로 보내는 것을 알기 때문에 쿠키의 원칙에 의해 우리는 자동으로 쿠키를 받을 수 있음.</p>
<h4 id="controller에서-댓글을-만들고-비디오에-댓글-넣어주기">controller에서 댓글을 만들고, 비디오에 댓글 넣어주기</h4>
<ul>
<li>videoController - createComment<pre><code class="language-javascript">import Comment from &quot;../models/Comment&quot;;
</code></pre>
</li>
</ul>
<p>export const createComment = async (req, res) =&gt; {
    const {session : {user}, body : {text}, params : {id}} = req;
    const video = await Video.findById(id);
    if(!video){
        return res.sendStatus(404);
    }
    const comment = await Comment.create({
        text,
        owner: user._id,
        video:id,
    });
      video.comments.push(comment._id); // 만들 댓글의 ObjId를  비디오의 comments array에 넣어줌
      video.save(); // 비디오 저장 ㄱ ㄱ
    res.sendStatus(201);
};</p>
<pre><code>-&gt; 원한다면, req.session 이용해 사용자에게도 댓글을 넣어주어도 됨. User model에 comments array에 넣어주면됨. video와 동일하게

1. mongo에 comment생김
![](https://velog.velcdn.com/images/wklee0607_/post/703333d6-f5c4-4c2d-aa9e-574210f181cf/image.png)

2. video의 comments array에 comment생김. (watch에서 populate해줘서 저렇게 object형태로 다 보이는 것임. 애초에 array에 넣어줄 때는 comment의 id만 넣어줬음)
![](https://velog.velcdn.com/images/wklee0607_/post/804974bd-719a-4d68-bb79-a73421ed7ade/image.png)

## 16.6 Rendering Comments

#### 단 comment를 화면에 보이도록 할 것임
- videoController - watch
```javascript
const video = await Video.findById(id).populate(&quot;owner&quot;).populate(&quot;comments&quot;);// 댓글을 만들 때, 비디오의 comments array에 댓글의 id를 넣어줬음. 이 id를 이용해 각각의 comment의 내용을 가져온다는 것임. -&gt; array의 내용이 더 이상 id가 아닌, obj형태로 정보를 담고있음. populat값은 video의 array명이 되야함.</code></pre><ul>
<li>watch.pug<pre><code class="language-javascript">// 업로드된 댓글 추가
div.video__comments
      ul 
          each comment in video.comments.reverse() // 최신 댓글이 위로 오도록 array를 뒤집어줌. reverse()만 해도 되는 이유: pug는 javascript언어를 실행하기 떄문.
             li.video__comment
                  i.fas.fa-comment
                  span  #{comment.text}</code></pre>
</li>
</ul>
<p>-&gt; 과제: </p>
<ol>
<li>댓글에 사용자도 나타나도록 하기!</li>
<li>사용자가 댓글 삭제하도록 하기 -&gt; db에서 삭제해야겠죠? 사용자 array에서 삭제 (filter이용), comment db에서 삭제.</li>
<li>댓글창 이쁘게 수정하기</li>
</ol>
<ul>
<li>watch.scss -&gt; client/scss/screens/watch.scss<pre><code class="language-javascript">.video__comments {
  display: flex;
  flex-direction: column;
  max-width: 320px;
  width: 100%;
  margin: 0 auto;
  margin-top: 20px;
  .video__comment {
    padding: 10px;
    border-radius: 9999px;
    background-color: white;
    color: black;
    margin-bottom: 10px;
  }
}</code></pre>
</li>
</ul>
<h2 id="167-realtime-comments">16.7 RealTime comments</h2>
<p>-&gt; 실시간으로 댓글이 달리면 화면에 보이도록 해줄것임.</p>
<h4 id="commentsectionfrontend에서-작업해주기---직접-html에-더해주는-것임">commentSection(frontend)에서 작업해주기 -&gt; 직접 html에 더해주는 것임.</h4>
<ul>
<li>commentSection.js</li>
</ul>
<ol>
<li><p>async, fetch에 await추가 -&gt; fetch에 await해주면 backend에서 DB랑 뭔가를 하고, statusCode를 return하고 다시 backend가 우리에게 돌아오는데 이 과정을 전부 기다려줌. 즉, 백엔드에서 모든 과정을 기다려줌.</p>
</li>
<li><p>fetch를 response로 정의해쥼 -&gt; fetch는 console.log해주면 response를 출력함.<br><img src="https://velog.velcdn.com/images/wklee0607_/post/30a091a3-ff04-4263-9c00-41c047bda435/image.png" alt="">
여기서 우리는 댓글이 잘 업로드 되었는지 status를 통해 알 수 있음. -&gt; 201 즉, 댓글이 생성되었을 때만, html을 추가해줘서 댓글이 화면에 보이도록 만들기(이 댓글은 새로고침하면 사라짐 ! 대신 db에서 댓글을 가져오기 때문에 똑같이 보여질 것임). 
지린다 진짜...</p>
</li>
</ol>
<p><strong>즉, 댓글을 단 이용자에게는 단지 html을 추가해서 댓글을 보여주고, 다른 이용자는 그 댓글을 실시간으로는 볼 수 없으나, 새로고침하면 db에서 가져오기 때문에 볼 수 있음. 새로고침 하면 html을 추가해준 것은 없어지고, db에서 가져오는 것들을 보게됨.</strong></p>
<p>서로 다른 A, B라는 사람이 같은 페이지에 접속해있을 때 A가 댓글을 다는 것이 B에게 실시간으로 보여지는 것은 아님!</p>
<pre><code class="language-javascript">const addComment = (text) =&gt; {
    const videoComments = document.querySelector(&quot;.video__comments ul&quot;);
    const newComment = document.createElement(&quot;li&quot;);
    newComment.className = &quot;video__comment&quot;;
    const icon = document.createElement(&quot;i&quot;)
    icon.className = &quot;fas fa-comment&quot;;
    const span = document.createElement(&quot;span&quot;);
    span.innerText = ` ${text}`;
    newComment.appendChild(icon);
    newComment.appendChild(span);
    videoComments.prepend(newComment); // 새 댓글이 위로 오도록!
};

const handleSubmit = async (event) =&gt; {
    event.preventDefault();
    const textarea = form.querySelector(&quot;textarea&quot;);
    const text = textarea.value;
    const videoId = videoContainer.dataset.videoid
    if(text === &quot;&quot;){
        return; //사용자가 아무것도 입력하지 않으면 req를 보내지 않음.
    }
    const response = await fetch(`/api/videos/${videoId}/comment`, { // await을 하면 백엔드에서의 모든 과정을 기다려줌.
        method: &quot;POST&quot;,
        headers : {// header: 기본적으로 requests에 대한 정보를 담고 있음. 따라서 request에 추가할 수 있는 정보임. 이를 이용하여 Express에게 보내는 데이터가 json형태라고 알림.
            &quot;Content-Type&quot; : &quot;application/json&quot;, // Content-Type을 해주는 이유: 우리가 백엔드로 보내는 데이터 Type이 json형식임을 알려주는 것임. -&gt; 이걸 처리해주지 않으면 백엔드는 여전히 text를 보낸다고 알 것임.
        },
        body: JSON.stringify({ // 보내줄 때 JSON.stringify형태로 보내줌.
            text,
        })
    })
    const status = response.status;
      if(status === 201){
        addComment(text);
    }
    console.log(response);
      textarea.value =&quot;&quot;;
};

if(form){
    form.addEventListener(&quot;submit&quot;, handleSubmit);
} // form이 로그인 한 상태에서만 보이기 떄문임.</code></pre>
<h2 id="168-comment-ids">16.8 Comment Ids</h2>
<h4 id="댓글-삭제-기능-만들기">댓글 삭제 기능 만들기</h4>
<ol>
<li><p>watch.pug에서 댓글의 주인만 삭제 버튼을 볼 수 있도록 기능 설정 &amp; 댓글에 dataset으로 comment의 id를 넣음</p>
<pre><code class="language-javascript">if (String(loggedInUser._id) === String(comment.owner))
                     span.removeBtn(data-commentId=comment._id)  ❌</code></pre>
</li>
<li><p>javascript에서 버튼을 누르면 fetch를 통해 remove api로 post하도록 만들고 이 때, 댓글의 dataset을 백엔드로 보내줌.</p>
<pre><code class="language-javascript">const removeBtn = document.querySelector(&quot;.removeBtn&quot;);
</code></pre>
</li>
</ol>
<p>const handleRemoveComment = async (event) =&gt; {
    const videoId = videoContainer.dataset.videoid
    const child = event.target.parentElement;
    const commentid = event.target.dataset.commentid;
    if(!commentid){
        return child.remove();
    }
    const response = await fetch(<code>/api/videos/${videoId}/commentRemove</code>, {
        method : &quot;POST&quot;,
        headers : {
            &quot;Content-Type&quot; : &quot;application/json&quot;,
        },
        body: JSON.stringify({commentid}),
    });
    window.location.reload();
};</p>
<p>// add comment(fake comment)도 수정해줌 
const addComment = (text) =&gt; {
    const videoComments = document.querySelector(&quot;.video__comments ul&quot;);
    const newComment = document.createElement(&quot;li&quot;);
    newComment.className = &quot;video__comment&quot;;
    const icon = document.createElement(&quot;i&quot;)
    icon.className = &quot;fas fa-comment&quot;;
    const span = document.createElement(&quot;span&quot;);
    span.innerText = <code>${text}</code>;
    const rmvBtn = document.createElement(&quot;span&quot;);
    rmvBtn.innerText = &quot; ❌&quot;
    rmvBtn.className = &quot;removeBtn&quot;;
    rmvBtn.addEventListener(&quot;click&quot;, handleRemoveComment);
    newComment.appendChild(icon);
    newComment.appendChild(span);
    newComment.appendChild(rmvBtn);
    videoComments.prepend(newComment);
};</p>
<pre><code>
-&gt; 한계점: fake comment를 이용할 때는, comment에 바로 dataset이 생기지 않으므로 handleRemoveComment의 fetch가 제대로 작동하지 않음. 따라서 이를 보완해주는 작업이 필요함. fake comment 상태에서도 바로 지울 수 있도록 만들기

3. api Router 생성
```javascript
apiRouter.post(&quot;/videos/:id([0-9a-f]{24})/commentRemove&quot;, removeComment);</code></pre><ol start="4">
<li>controller로 백엔드에서 댓글db에서 댓글 지우고, video의 comments arr에서 filter를 이용해 댓글을 삭제해줌<pre><code class="language-javascript">export const removeComment = async (req, res) =&gt; {
 const {body :{commentid}, params :{id}, session} = req;
 const comment = await Comment.findById(commentid);
 const video = await Video.findById(id);
 if(!comment){
     req.flash(&quot;error&quot;, &quot;Comment Not Found.&quot;);
     return res.sendStatus(404);
 }
 if(!video){
     req.flash(&quot;error&quot;, &quot;Video Not Found.&quot;);
     return res.sendStatus(404);
 }
 if(String(comment.owner) !== session.user._id){
     req.flash(&quot;error&quot;, &quot;You are not this comment&#39;s owner&quot;);
     return res.sendStatus(404);
 }
 await Comment.findByIdAndDelete(commentid);
 const newarr = video.comments.filter((comment) =&gt; String(comment) !== String(commentid));
 video.comments = newarr
 video.save();
 return res.sendStatus(200);
};</code></pre>
</li>
</ol>
<h2 id="보완할-점">보완할 점</h2>
<p>-&gt; fake comment를 이용할 때는, comment에 바로 dataset이 생기지 않으므로 handleRemoveComment의 fetch가 제대로 작동하지 않음. 따라서 이를 보완해주는 작업이 필요함. fake comment 상태에서도 바로 지울 수 있도록 만들기</p>
<p>-&gt; 원하는 기능: fake comment상태에서도 삭제를 하면 백엔드에서 삭제 됨과 동시에, child.remove()로만 삭제할 수 있도록 구현하고 싶음</p>
<h4 id="보완완료">보완완료</h4>
<p>-&gt; fakecomment를 만들 때, fetch를 이용해 비디오 comment arr를 가져와 그 id를 fakecomment dataset에 넣어주었음.</p>
<ul>
<li><p>commentsection</p>
<pre><code class="language-javascript">const addComment = async (text) =&gt; {
  const videoId = videoContainer.dataset.videoid
  const videoComments = document.querySelector(&quot;.video__comments ul&quot;);
  const newComment = document.createElement(&quot;li&quot;);
  newComment.className = &quot;video__comment&quot;;
  const icon = document.createElement(&quot;i&quot;)
  icon.className = &quot;fas fa-comment&quot;;
  const span = document.createElement(&quot;span&quot;);
  span.innerText = ` ${text}`;
  const rmvBtn = document.createElement(&quot;span&quot;);
  rmvBtn.innerText = &quot; ❌&quot;
  rmvBtn.className = &quot;removeBtn&quot;;
    //추가한 부분
  const response = await fetch(`/api/videos/${videoId}/comment`);
  const data = await response.json();
  const commentid = data[0];
  rmvBtn.dataset.commentid = commentid;
    //
  rmvBtn.addEventListener(&quot;click&quot;, handleRemoveComment);
  newComment.appendChild(icon);
  newComment.appendChild(span);
  newComment.appendChild(rmvBtn);
  videoComments.prepend(newComment);
};</code></pre>
</li>
<li><p>apiRouter</p>
<pre><code class="language-javascript">apiRouter.route(&quot;/videos/:id([0-9a-f]{24})/comment&quot;).post(createComment).get(getComment); // comment api router추가</code></pre>
</li>
<li><p>controller</p>
<pre><code class="language-javascript">export const getComment = async (req, res) =&gt; {
  const { body , params: {id} } = req;
  const video = await Video.findById(id);
  if(!video){
      res.sendStatus(404);
  }
  res.status(200).send(video.comments.reverse());
};</code></pre>
</li>
<li><p>---- 이게 내가 한 방법이고 니콜라스가 한 방법 --------</p>
</li>
</ul>
<p>-&gt; 애초에 comment를 만들고 백엔드에서 프론트엔드로 돌아올 때, commentid를 넘겨줬음. 또한 html에서 dataset-id:comment._id를 버튼에 넣지 않고, comment li에 넣었음 !
그리고 fetch를 할 때, params에  videoid를 넣고 body로 commentid를 넘겨주는 것이 아니라 commentid를 params에 넣어서 fetch를 실행함. 이렇게 해도 comment를 이용해 video를 찾을 수 있음. 백엔드에서 comment.video를 이용해 비디오를 찾음 !</p>
<ul>
<li>videoController<pre><code class="language-javascript">export const createComment = async (req, res) =&gt; {
  const {session : {user}, body : {text}, params : {id}} = req;
  const video = await Video.findById(id);
  if(!video){
      return res.sendStatus(404);
  }
  const comment = await Comment.create({
      text,
      owner: user._id,
      video:id,
  });
  video.comments.push(comment._id); // 만들 댓글의 ObjId를  비디오의 comments array에 넣어줌
  video.save(); //comments array에 변경사항 생겨서 저장해줌 !
  return res.status(201).json({ newCommentId: comment._id });
};
</code></pre>
</li>
</ul>
<p>export const removeComment = async (req, res) =&gt; {
    const { params :{id}, session} = req;
    const comment = await Comment.findById(id);
    const videoid = String(comment.video);
    const video = await Video.findById(videoid);
    if(!comment){
        req.flash(&quot;error&quot;, &quot;Comment Not Found.&quot;);
        return res.sendStatus(404);
    }
    if(!video){
        req.flash(&quot;error&quot;, &quot;Video Not Found.&quot;);
        return res.sendStatus(404);
    }
    if(String(comment.owner) !== session.user._id){
        req.flash(&quot;error&quot;, &quot;You are not this comment&#39;s owner&quot;);
        return res.sendStatus(404);
    }
    await Comment.findByIdAndDelete(id);
    const newarr = video.comments.filter((comment) =&gt; String(comment) !== String(id));
    video.comments = newarr
    video.save();
    return res.sendStatus(200);
};</p>
<pre><code>
- commentSection
```javascript
const handleSubmit = async (event) =&gt; {
    event.preventDefault();
    const textarea = form.querySelector(&quot;textarea&quot;);
    const text = textarea.value;
    const videoId = videoContainer.dataset.videoid
    if(text === &quot;&quot;){
        return; //사용자가 아무것도 입력하지 않으면 req를 보내지 않음.
    }
    const response =  await fetch(`/api/videos/${videoId}/comment`, {
        method: &quot;POST&quot;,
        headers : {
            &quot;Content-Type&quot; : &quot;application/json&quot;,
        },
        body: JSON.stringify({
            text,// text(value값)만 보내면 object형태가 아닌 string형식의 text만 보내는 것임.
        })
    });
    if(response.status === 201){
        textarea.value =&quot;&quot;;
        const { newCommentId } = await response.json(); // 백엔드로부터 보내온 걸 쓸려면 이렇게 const data = await response.json()을 해야 쓸 수 있음.
        addComment(text, newCommentId);
    }
};


const addComment = async (text, id) =&gt; {
    const videoComments = document.querySelector(&quot;.video__comments ul&quot;);
    const newComment = document.createElement(&quot;li&quot;);
    newComment.dataset.id = id;
    newComment.className = &quot;video__comment&quot;;
    const icon = document.createElement(&quot;i&quot;)
    icon.className = &quot;fas fa-comment&quot;;
    const span = document.createElement(&quot;span&quot;);
    span.innerText = ` ${text}`;
    const rmvBtn = document.createElement(&quot;span&quot;);
    rmvBtn.innerText = &quot; ❌&quot;
    rmvBtn.className = &quot;removeBtn&quot;;
    rmvBtn.addEventListener(&quot;click&quot;, handleRemoveComment);
    newComment.appendChild(icon);
    newComment.appendChild(span);
    newComment.appendChild(rmvBtn);
    videoComments.prepend(newComment);
};


const handleRemoveComment = async (event) =&gt; {
    //const videoId = videoContainer.dataset.videoid
    const child = event.target.parentElement;
    const commentid = child.dataset.id;
    const response = await fetch(`/api/comment/${commentid}/remove`, {
        method : &quot;DELETE&quot;,
    });
    if(response.status === 200){
        child.remove();
    };
};
</code></pre><ul>
<li>apiRouter<pre><code class="language-javascript">apiRouter.delete(&quot;/comment/:id([0-9a-f]{24})/remove&quot;, removeComment);</code></pre>
</li>
</ul>
<h2 id="또-다른-문제">또 다른 문제</h2>
<p>-&gt; 댓글을 쓴 후 새로고침 한 담에, 2개 연속으로 삭제하려 그려면 첫번째는 잘 삭제되는데 2번부터는 삭제가 안 돼서 새로고침 후 삭제해야 된다. </p>
<h4 id="해결">해결</h4>
<ul>
<li>commentSection<pre><code class="language-javascript">const removeBtns = document.querySelectorAll(&quot;.removeBtn&quot;);
</code></pre>
</li>
</ul>
<p>Array.from(removeBtns).forEach(btn =&gt; btn.addEventListener(&quot;click&quot;, handleRemoveComment)); // rmvBtn이 여러개니까 queryselectAll로 다 선언하고,(이 선언된 것의 type은 object이므로 이를 array로 바꾸고) 이 array를 forEach를 통해 각각의 btn에다가 이벤트 리스너 부여함.</p>
<pre><code>
#### fetch

fetch로 백 -&gt; 프론트 or 프론트 -&gt; 백엔드로 뭔가 데이터를 보낼 때는, json의 형태로 보내야 한다. 그리고 이 둘의 json 데이터를 주고 받기 위해 다음의 미드웨어가 반드시 필요함

미드웨어: app.use(express.json()); : 프 -&gt; 백으로 데이터를 넘겨 받을 때, 프론트엔드에서 보낸 데이터는 JSON.stringify형태이다. 이 데이터를 백엔드에서 json()함수를 실행시켜줘서 백엔드에서 사용할 수 있도록 데이터 형태를 바꿔주는 미드웨어임. 즉 json.string을 받아서 js object로 바꿔주는 역할을 함. 이게 있어야 밑의 2번처럼 사용가능함.

1. 프 -&gt; 백 : headers : {&quot;Content-Type&quot; : &quot;application/json&quot;,}, body:JSON.stringify(data) -&gt; req.body 로 사용
2. 백 -&gt; 프 : res.json({ data: comment._id}) -&gt; const datas = await response.json()  / const data = datas.data 으로 사용하기


**fetch는 프론트엔드에서 백엔드로 요청을 보내고, 백엔드에서 대답을 해주는 모든 과정을 fetch의 과정이라 할 수 있다.**


## 챌린지 과제
챌린지 과제
#### - 댓글 삭제하기 (삭제시 비디오나 유저 도큐먼트에서도 삭제 필요)
#### - 댓글 추가 및 삭제시 실시간으로 댓글 갯수 변경 ✓

추가로 구현해볼 만한 기능들
#### - 댓글 수정하기 ✓
=&gt; 
- commentSection
```javascript
const editBtns = document.querySelectorAll(&quot;.editBtn&quot;);

const handleEditSubmit = async (event) =&gt; {
    event.preventDefault();
    const commentid = event.target.parentElement.dataset.id;
    const span = event.target.parentElement.querySelector(&quot;span&quot;);
    const input = event.target.querySelector(&quot;input&quot;);
    const text = input.value.trim();
    if(text === &quot;&quot;){
        return; //사용자가 아무것도 입력하지 않으면 req를 보내지 않음.
    }
    const response =  await fetch(`/api/comment/${commentid}/edit`, {
        method: &quot;POST&quot;,
        headers : {
            &quot;Content-Type&quot; : &quot;application/json&quot;,
        },
        body: JSON.stringify({
            text,// text(value값)만 보내면 object형태가 아닌 string형식의 text만 보내는 것임.
        })
    });
    if(response.status === 200){
        span.innerText = input.value;
        input.value =&quot;&quot;;
    }
};

const handleExitForm = (event) =&gt; {
    const li = event.target.parentElement.parentElement;
    const form = event.target.parentElement;
    form.remove();
};

const showEditComment = (event) =&gt; {
    const li = event.target.parentElement;
    const alreadyForm = li.querySelector(&quot;form&quot;);
    const form = document.createElement(&quot;form&quot;);
    form.addEventListener(&quot;submit&quot;, handleEditSubmit); 
    const input = document.createElement(&quot;input&quot;);
    input.type = &quot;text&quot;;
    input.value = li.querySelector(&quot;span&quot;).innerText;
    const btn = document.createElement(&quot;button&quot;);
    btn.innerText = &quot;Edit&quot;;
    const exit = document.createElement(&quot;span&quot;);
    exit.className = &quot;far fa-times-circle&quot;;
    exit.addEventListener(&quot;click&quot;, handleExitForm);
    form.appendChild(input);
    form.appendChild(btn);
    form.appendChild(exit);
    li.appendChild(form);
};

if(editBtns){
    Array.from(editBtns).forEach((editBtn) =&gt; editBtn.addEventListener(&quot;click&quot;, showEditComment)); 
};</code></pre><ul>
<li><p>apiRouter</p>
<pre><code class="language-javascript">apiRouter.post(&quot;/comment/:id([0-9a-f]{24})/edit&quot;, editComment);</code></pre>
</li>
<li><p>videoController</p>
<pre><code class="language-javascript">export const editComment = async (req, res) =&gt; {
  const {body, params:{id}, session:{user : {_id}}} = req;
  const comment = await Comment.findById(id);
  if(!comment){
      req.flash(&quot;error&quot;, &quot;Comment Not Found.&quot;);
      return res.sendStatus(404);
  }
  if(String(comment.owner) !== String(_id)){
      req.flash(&quot;error&quot;, &quot;You are not this comment&#39;s owner&quot;);
      return res.sendStatus(404);
  }
  comment.text = body.text;
  comment.save();
  return res.sendStatus(200);
};</code></pre>
<h4 id="--좋아요">- 좋아요</h4>
<h4 id="--좋아요-취소">- 좋아요 취소</h4>
<h4 id="--해시태그-클릭시-비디오-찾기">- 해시태그 클릭시 비디오 찾기</h4>
</li>
</ul>
<p>Element.remove()
Element.remove() 메서드는 해당 요소가 속한 트리에서 요소를 제거합니다.
(remove대신 removeChild를 사용해서 엘리먼트 삭제도 가능)
<a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/remove">https://developer.mozilla.org/en-US/docs/Web/API/Element/remove</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 14.1 -2 Transcode Video]]></title>
            <link>https://velog.io/@wklee0607_/14.1-Transcode-Video</link>
            <guid>https://velog.io/@wklee0607_/14.1-Transcode-Video</guid>
            <pubDate>Sat, 16 Jul 2022 16:52:42 GMT</pubDate>
            <description><![CDATA[<h1 id="transcode-video">Transcode Video</h1>
<p>object url: 영상의 모든 정보를 담음. // event.datad에 binary data가 있는데 파일일 수도 있는 binary data에 createObjectURL을 이용해서 접근할 수 있어야함. </p>
<p>-&gt; 밑의 사진은 videoFile = URL.createObjectURL(event.data); 의 videoFile 콘솔임.
<img src="https://velog.velcdn.com/images/wklee0607_/post/5369bf63-c555-4155-bb1d-36bc620fd972/image.png" alt=""></p>
<p>우리가 원하는건 handelDownload를 누르면 영상을 눌러서 변환하는 것임.</p>
<h2 id="ffmpeg사용법"><a href="https://ffmpeg.org/ffmpeg.html">ffmpeg</a>사용법</h2>
<pre><code class="language-javascript">const fs = require(&#39;fs&#39;);
const { createFFmpeg, fetchFile } = require(&#39;@ffmpeg/ffmpeg&#39;);

const ffmpeg = createFFmpeg({ log: true });

(async () =&gt; {
  await ffmpeg.load();
  ffmpeg.FS(&#39;writeFile&#39;, &#39;test.avi&#39;, await fetchFile(&#39;./test.avi&#39;));
  await ffmpeg.run(&#39;-i&#39;, &#39;test.avi&#39;, &#39;test.mp4&#39;);
  await fs.promises.writeFile(&#39;./test.mp4&#39;, ffmpeg.FS(&#39;readFile&#39;, &#39;test.mp4&#39;));
  process.exit(0);
})();</code></pre>
<h1 id="transcode-video-1">Transcode Video</h1>
<h2 id="1-recoderjs작성">1. recoder.js작성</h2>
<pre><code class="language-javascript">import { createFFmpeg, fetchFile } from &quot;@ffmpeg/ffmpeg&quot;;


const handleDownload = async() =&gt; {
      //1 단계
    const ffmpeg = createFFmpeg({corePath: &quot;/convert/ffmpeg-core.js&quot; ,log: true});
    await ffmpeg.load(); // await사용: 사용자가 소프트웨어를 사용할 것이기 때문. 사용자가 JS가 아닌 코드를 사용하는 거임, 무언가를 설치해서. 우리 웹사이트에서 다른 소프트웨어를 사용하는 거임. 소프트웨어가 무거울 수 있기 때문에 기다려야함.

      //2단계: ffmpeg에 파일 만들기 = 백엔드의 multer같은 존재. 실존하진 않아도 프론트엔드에 파일이 생김
      ffmpeg.FS(&quot;writeFile&quot;, &quot;recording.webm&quot;, await fetchFile(videoFile)); // 파일 생성 -&gt; recording.webm은 생성하는 파일 이름, fetchFile -&gt; 우리가 만든 videoFile(Blob파일에 접근할 수 있는 url로 만든 것임 즉, 이 데이터는 Blob이므로 binary data형식임.) 
    await ffmpeg.run(&quot;-i&quot;, &quot;recording.webm&quot;, &quot;-r&quot;, &quot;60&quot;, &quot;output.mp4&quot;); // 파일 변환 과정. 가상 컴퓨터에 이미 존재하는 파일(방금 바로 윗줄 파일 생성해준 파일임.)(recording.webm)을 input으로 받는 것임. -&gt; input을 output으로 변형하겠다. -i: input으로 받겠다는 뜻임.

      // 3단계: output.mp4 가져오기
      const mp4File = ffmpeg.FS(&quot;readFile&quot;, &quot;output.mp4&quot;); // output파일 가져오기
    console.log(mp4File.buffer);
    const mp4Blob = new Blob([mp4File.buffer], {type:&quot;video/mp4&quot;});// 변환된 output파일의 ArrayBuffer(raw binary data buffer를 나타내는 object)를 아용하여 video의 mp4형태의 Blob(binary파일 형식)으로 만들어준다. blob으로 만들거기 때문에 input data를 binary형식으로 해줌.
    const mp4Url = URL.createObjectURL(mp4Blob); // 위에서 만든 Blob에 접근할 수 있도록 url부여

      //변경
      a.href = mp4Url;
    a.download = &quot;MyRecording.mp4&quot;;
};
</code></pre>
<p>-&gt; 실행하면 output파일 생성(메모리에 output.mp4라는 파일 생성)</p>
<p>2단계 : 우리가 브라우저 안에 있다는 생각을 멈춤. 눈을 감고 폴더와 파일로 가득찬 컴퓨터 안에 있다 생각하면서 이들을 맘대로 조종할 수 있는 컴퓨터를 브라우저에서 실행하는 신이 된다 생각. -&gt; ffmpeg에 파일 만들기 = 백엔드의 multer같은 존재. 프론트엔드의 ffmpeg</p>
<p>3단계 : output.mp4 가져오기. 일단 output파일을 읽기 형식으로 가져온다. 그런 뒤, 해당 파일의 buffer중 raw binary data buffer를 나타내는 object인 즉, 한 마디로 우리 영상을 나타내는 bytes의 배열인 ArrayBuffer에 접근하는 것을 목적(mp4File.buffer하면 ArrayBuffer접근됨.)으로, 해당 파일의 버퍼를 video/mp4의 형태로 즉 binary데이터를 가진 형태인 Blob형태로 변환시켜준다. 이후 이 Blob을 createObjectUrl을 이용하여 브라우저 메모리 상에 저장을 해두고, 우리가 그 파일에 접근할 수 있는 URL을 준다.</p>
<hr>
<p><a href="https://ffmpeg.org/ffmpeg.html#Video-Options">ffmpeg.documedation.videoOptions</a></p>
<p>ffmpeg.FS(method, ...args): any/ ffmpeg.FS(&quot;파일 작성&quot;, &quot;파일 이름&quot;, binarydata);
ffmpeg.wasm의 입출력 파일은 ffmpeg.wasm이 소비할 수 있도록 먼저 MEMFS에 저장해야 합니다.
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#ffmpegfsmethod-args-any">https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#ffmpegfsmethod-args-any</a></p>
<p>fetchFile(media): Promise
다양한 리소스에서 파일을 가져오기 위한 도우미 기능입니다. 때로는 처리하려는 비디오 / 오디오 파일이 원격 URL과 로컬 파일 시스템의 어딘가에 있을 수 있습니다.이 도우미 함수는 파일로 가져오고 ffmpeg.wasm이 사용할 Uint8Array 변수를 반환하는 데 도움이 됩니다. fetch = url파일 실행(get / post), file: 파일 형태로 된 url fetch처리 한다는 뜻임.
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#fetchfilemedia-promise">https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#fetchfilemedia-promise</a></p>
<p>ffmpeg.load
ffmpeg.load()를 호출하면 기본적으로 <a href="http://localhost:3000/node_modules/@ffmpeg/core/dist/%EB%A5%BC">http://localhost:3000/node_modules/@ffmpeg/core/dist/를</a> 검색하여 필수 파일을 다운로드합니다. (ffmpeg-core.js, ffmpeg-core.wasm, ffmpeg-core.worker.js). 해당 파일이 거기에 제공되었는지 확인해야 합니다. 해당 파일이 다른 위치에 있는 경우 호출할 때 기본 동작을 다시 작성할 수 있습니다.
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm#why-it-doesnt-work-in-my-local-environment">https://github.com/ffmpegwasm/ffmpeg.wasm#why-it-doesnt-work-in-my-local-environment</a></p>
<p>binarydata: 이진수(0과 1)로 이루어진 데이터</p>
<p>fmpeg.run : 가상 컴퓨터에 이미 존재하는 파일을 input으로 받는 것임. -&gt; input을 output으로 변형하겠다. -i: input으로 받겠다는 뜻임. / &quot;r&quot;, &quot;60&quot; : 영상을 초당 60프레임으로 인코딩 해주는 명령어.</p>
<hr>
<p>MEMFS에서 데이터 읽기
ffmpeg.FS(&#39;readFile&#39;, &#39;video.mp4&#39;);
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#ffmpegfsmethod-args-any">https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#ffmpegfsmethod-args-any</a></p>
<p>buffr</p>
<pre><code>Uint8Array (양의 정수 8비트 배열)
Uint8Array 형식 배열은 8비트 부호 없는 정수 배열을 나타냅니다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array

ArrayBuffer
ArrayBuffer 객체는 raw binary data buffer를 나타내는 object이다. 한 마디로 우리 영상을 나타내는 bytes의 배열임. 다른 언어에서는 종종 &quot;byte array&quot;이라고 하는 byte array입니다. arrayBuffer를 사용하고 싶다면 buffer에 접근해야 된다.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer</code></pre><p>Blob
Blob: binary정보를 가지고 있는 파일.
자바스크립트 세계의 파일과 같은거. 파일같은 객체를 만듦. 객체는 파일류의 불변하는 미가공 데이터를 나타냅니다. 텍스트와 이진 데이터의 형태로 읽을 수 있으며, ReadableStream으로 변환한 후 그 메서드를 사용해 데이터를 처리할 수도 있습니다.
<a href="https://developer.mozilla.org/ko/docs/Web/API/Blob">https://developer.mozilla.org/ko/docs/Web/API/Blob</a></p>
<h2 id="2-error해결">2. error해결</h2>
<ol>
<li><a href="http://localhost:4000/node_modules/@ffmpeg/core/dist/ffmpeg-core.js">http://localhost:4000/node_modules/@ffmpeg/core/dist/ffmpeg-core.js</a> 404 (Not Found) 또는
createFFmpegCore is not defined 오류 해결 방법</li>
</ol>
<p>-&gt;
1️⃣ server.js
app.use(&quot;/convert&quot;, express.static(&quot;node_modules/@ffmpeg/core/dist&quot;));
2️⃣ recorder.js
const ffmpeg = createFFmpeg({
corePath: &quot;/convert/ffmpeg-core.js&quot;,
log: true,
});</p>
<ol start="2">
<li>Uncaught (in promise) ReferenceError: SharedArrayBuffer is not defined 오류 해결 방법
FFmpeg를 실행했을 때, 콘솔창에 위와 같은 오류가 난다면 server.js에 app.set()아래에 함수를 추가해주시면 됩니다.</li>
</ol>
<p>-&gt; 
오류 원인 : SharedArrayBuffer는 cross-origin isolated된 페이지에서만 사용할 수 있습니다. 따라서 ffmpeg.wasm을 사용하려면 Cross-Origin-Embedder-Policy: require-corp 및 Cross-Origin-Opener-Policy: same-origin를 header에 설정해 자체 서버를 호스팅해야 합니다.
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm/issues/263">https://github.com/ffmpegwasm/ffmpeg.wasm/issues/263</a></p>
<pre><code class="language-javascript">// server.js
app.use((req, res, next) =&gt; {
    res.header(&quot;Cross-Origin-Embedder-Policy&quot;, &quot;require-corp&quot;);
    res.header(&quot;Cross-Origin-Opener-Policy&quot;, &quot;same-origin&quot;);
next();
});</code></pre>
<p>FFmpeg Usage
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm#usage">https://github.com/ffmpegwasm/ffmpeg.wasm#usage</a></p>
<p>FFmpeg API
<a href="https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#api">https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md#api</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 14.0 WEBASSEMBLY VIDEO TRANSCODE]]></title>
            <link>https://velog.io/@wklee0607_/14.0-WEBASSEMBLY-VIDEO-TRANSCODE</link>
            <guid>https://velog.io/@wklee0607_/14.0-WEBASSEMBLY-VIDEO-TRANSCODE</guid>
            <pubDate>Sat, 16 Jul 2022 09:05:27 GMT</pubDate>
            <description><![CDATA[<ol>
<li>비디오를 찍고 webm파일을 만들면 이 webM파일을 가지고 mp4로 변환할 거임.(변환 이유: 모든 기기들이 webm을 이해하지 못하기 떄문. 그래서 모두 이해 가능한 mp4로 변환)</li>
<li>비디오에서 스크린 샷을 찍어 썸네일로 추가할 것임.</li>
</ol>
<h1 id="introduction">Introduction</h1>
<ol>
<li><p>이 섹션에서 우리는 FFmpeg로 우리 webM비디오를 mp4로 변환시킬 것임.</p>
</li>
<li><p>FFmpeg를 이용해 비디오 썸네일을 추출할 것임.</p>
</li>
<li><p>되게 실험적인 부분이 많은 section임.</p>
</li>
</ol>
<h2 id="ffmpeg">FFmpeg</h2>
<p>-&gt; 세계 최고의 소프트웨어 중 하나. 비디오나 오디오 같은 어떤 종류의 미디어 파일을 다룰 수 있음.
예를 들어 비디오를 압축하거나, 비디오 포맷을 변환해야 하거나 아니면 비디오에서 오디오를 추출하고 싶거나, 비디오에서 스크린샷을 찍고 싶을 때 등 사용. 비디오를 가지고 할 수 있는 것들은 모두 할 수 있음.</p>
<p>-&gt; C언어로 만들어졌고, 거의 모든 언어에서 사용 가능함. 또한 나의 콘솔에(터미널에) 설치 ㄱㄴ. 일반적으로 FFmpeg는 mongo처럼 터미널에서 실행돼야함. 무슨 의미냐면 컴퓨터 즉, 백엔드에서ㅜ 실행해야함. 우리가 webM파일을 다운로드할 때마다 변환하여 압축하려면 자체의 좋은 서버가 필요한데, 이는 돈이 들음.</p>
<p>-&gt; 프로그램을 이용하기 때문에 FS(파일 시스템)을 이용할 수 있고, 여기에 파일을 작성하거나 불러올 수 있음</p>
<p>-&gt; 실제 유튜브에서는 업로드된 비디오를 그들의 비싼 서버에서 변환할 것임.</p>
<h2 id="webassembly">webAssembly</h2>
<p>-&gt; 좋은 서버를 이용하면 돈이 드는 걸 방지위한 대비책. 개방형 표준이고, 기본적으로 웹사이트가 매우 빠른 코드를 실행할 수 있게 해줌. 이 모든 것들은 원래는 백엔드에서 쓰는 것임. 
이 웹에섬블리는 프론트엔드에서 매우 빠른 코드를 실행할 수 있게 해줌(webassembly 덕분에 프론트엔드에서 사용 ㄱㄴ). JS를 이용하지 않고, 다른 종류의 프로그램을 사용할 수 있음. 대부분 webAssembly를 직접 작성하지 않고, webAssembly로 컴파일 되는 go 또는 Rust를 작성하게 될 것임. 실행 비용이 큰 프로그램들을 브라우저에서 실행할 수 있는 거임.</p>
<h2 id="우리가-할-것">우리가 할 것</h2>
<ol>
<li>webAssembly와 FFmpeg 두 가지 개념을 결합시키는 것임. </li>
<li>우리가 할 것은 ffmpeg.wasm을 사용하는 것임. ffmpeg.wasm은 비디오를 변환하기 위해 사용자의 컴퓨터를 사용함.</li>
<li>지금 우리가 할 것은 사용자의 브라우저에서 비디오를 변환하는 것임. 우리는 컴퓨터의 처리 능력을 사용할 것임.</li>
<li>우리는 webAssembly를 이용해 FFmpeg를 실행하도록 할 것임.(FFmpef는 C언어 프로그렘이고, webAssembly를 사용하면 브라우저에서 FFmpeg를 사용할 수 있게 해줌.)</li>
</ol>
<h4 id="ffmpegwasm--설치"><a href="https://github.com/ffmpegwasm/ffmpeg.wasm">ffmpeg.wasm</a>  설치</h4>
<pre><code>npm install @ffmpeg/ffmpeg @ffmpeg/core</code></pre><p><a href="https://www.npmjs.com/package/@ffmpeg/ffmpeg">ffmpeg.wasm</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 13.0 -5 Recorder ]]></title>
            <link>https://velog.io/@wklee0607_/13.0-5-Recorder</link>
            <guid>https://velog.io/@wklee0607_/13.0-5-Recorder</guid>
            <pubDate>Wed, 13 Jul 2022 14:55:07 GMT</pubDate>
            <description><![CDATA[<p>-&gt; 사람들이 버튼을 누르면 비디오 녹음이 시작됨. 5초로 제한을 둘거고, 5초가 지나면 녹화된 비디오를 다운받을 수 있게됨.</p>
<h2 id="1-setup---clientjsrecoderjs생성---프론트엔드에서-vanillajs로-만들-것임">1. Setup - client/js/recoder.js생성 - 프론트엔드에서 vanillaJS로 만들 것임.</h2>
<h4 id="11-clientjsrecoderjs생성">1.1 client/js/recoder.js생성</h4>
<h4 id="12-webpack에-추가">1.2 webpack에 추가</h4>
<ul>
<li>webpack<pre><code class="language-javascript">// entry에 추가
recorder : &quot;./src/client/js/recorder.js&quot;,
</code></pre>
</li>
</ul>
<pre><code>
#### 1.3 upload.pug에 script추가 &amp; 녹화 버튼 추가

```javascript
// script선언해주기
block scripts 
    script(src=&quot;/assets/js/recorder.js&quot;)

//버튼 추가
block content
    div 
        video#preview.hidden 
        button#startBtn.hidden Start Recording
        button#initBtn if u record video, Click here </code></pre><h4 id="14-clientjsrecoderjs작성">1.4 client/js/recoder.js작성</h4>
<ul>
<li>recoder.js<pre><code class="language-javascript">const startBtn = document.getElementById(&quot;startBtn&quot;);
const video = document.getElementById(&quot;preview&quot;)
</code></pre>
</li>
</ul>
<p>const handleStart = async() =&gt; {
    const stream = await navigator.mediaDevices.getUserMedia({
        audio: true,
        video: {width:500, height:500},
    });
    video.srcObject = stream; // video에 stream넣어주기 -&gt; stream이 object라서 video.src가 아니라 video.Object임.
    video.play();
};</p>
<p>startBtn.addEventListener(&quot;click&quot;, handleStart);</p>
<pre><code>
1. [MediaDevices.getUserMedia()](https://developer.mozilla.org/ko/docs/Web/API/MediaDevices/getUserMedia)
MediaDevices 인터페이스의 getUserMedia() 메서드는 사용자에게 미디어 입력 장치 사용 권한을 요청하며, 사용자가 수락하면 요청한 미디어 종류의 트랙을 포함한 MediaStream (en-US)을 반환합니다. 스트림은 카메라, 비디오 녹화 장치, 스크린 공유 장치 등 하드웨어와 가장 비디오 소스가 생성하는 비디오 트랙과, 마이크, A/D 변환기 등 물리적과 가상 오디오 장치가 생성하는 오디오 스트림, 그리고 그 외의 다른 종류의 스트림을 포함할 수 있습니다.
보통, MediaDevices 싱글톤 객체는 다음과 같이 navigator.mediaDevices를 사용해 접근합니다.
navigator.mediaDevices.getUserMedia(constraints);

기본 사용법: 

![](https://velog.velcdn.com/images/wklee0607_/post/fb16b4cd-f257-4299-b7a5-6df874b93336/image.png)

![](https://velog.velcdn.com/images/wklee0607_/post/6cb9c15f-3051-4d05-acea-8d4a4c460ead/image.png)

![](https://velog.velcdn.com/images/wklee0607_/post/35ac2c36-5eb8-4804-82b3-a4852b3a9cd5/image.png)


2. constraints
요청할 미디어 유형과 각각에 대한 요구사항을 지정하는 MediaStreamConstraints 객체. constraints 매개변수는 두 개의 구성 요소, video와 audio를 가지는 객체로, 요청할 미디어 유형에 대해 설명합니다. 둘 중 적어도 하나는 지정해야 합니다.
{ audio: true, video: true }

3. [regenerator-runtime](https://www.npmjs.com/package/regenerator-runtime)
Regenerator로 컴파일된 생성기 및 비동기 함수를 위한 독립 실행형 런타임입니다. -&gt; async, await을 사용하기 위해 설치 및 import해줘야함.
npm i regenerator-runtime

-&gt; **main.js에 import regeneratorRuntime from &quot;regenerator-runtime&quot;; 해주고 base.pug에 script(src=&quot;/assets/js/main.js&quot;) 추가**

4. HTMLMediaElement [srcObject](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/srcObject)

HTMLMediaElement 인터페이스의 srcObject 속성은 HTMLMediaElement와 연결된 미디어의 소스 역할을 하는 객체를 설정하거나 반환합니다.
그 객체는 MediaStream, MediaSource, Blob 또는 파일(Blob에서 상속됨)일 수 있습니다.

사용 예시
이 예에서 카메라의 MediaStream은 새로 생성된 요소에 할당됩니다.</code></pre><p>const mediaStream = await navigator.mediaDevices.getUserMedia({video: true});
const video = document.createElement(&#39;video&#39;);
video.srcObject = mediaStream;</p>
<pre><code>

## 2. Recording File &amp; download

#### 2.1 사용할 API들
1. [MediaRecorder](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder)
MediaStream Recording API의 MediaRecorder 인터페이스는 미디어를 쉽게 녹화할 수 있는 기능을 제공합니다. MediaRecorder() 생성자를 사용하여 생성됩니다.


MediaRecorder()
기록할 MediaStream이 지정된 새 MediaRecorder 개체를 만듭니다.

2. stream
기록될 MediaStream입니다. 이 소스 미디어는 navigator.mediaDevices.getUserMedia()를 사용하여 생성된 스트림이나 audio, video 또는 canvas 요소에서 가져올 수 있습니다.

3. MediaRecorder.start()
미디어 녹화를 시작합니다. 이 메서드는 선택적으로 밀리초 단위의 값을 가진 타임슬라이스 인수를 전달할 수 있습니다.

4. MediaRecorder.stop()
저장된 데이터의 최종 Blob을 포함하는 dataavailable 이벤트가 발생하는 시점에서 기록을 중지합니다.

5. [MediaRecorder ondataavailable](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/ondataavailable)
=&gt; **MediaRecorder.stop()이 실행될 때 발생하는 이벤트이다. recording된 데이터에 접근할 수 있도록 해준다.**


6. URL.createObjectURL() : 브라우저 메모리에서만 가능한 URL을 만들어준다. 이 URL은 웹 상에 실제로 존재하지 않고, 브라우저의 메모리를 가리키기만 하는 URL임. =&gt; 그냥 파일을 가리키고 있는 URL. 즉 파일은 브라우저의 메모리 상에 있다는 것임.
요약: 브라우저 메모리 상에 저장을 해두고, 우리가 그 파일에 접근할 수 있는 URL을 준거. 즉, 이 URL은 브라우저가 파일을 보여주는 형식.

URL.createObjectURL() 정적 메서드는 주어진 객체를 가리키는 URL을 DOMString으로 반환합니다. 해당 URL은 자신을 생성한 창의 document가 사라지면 함께 무효화됩니다.

=&gt; 결과: 우리는 ondataavailable의 event.data를 이용할 것임.- &gt; 여기에 파일이 들어가 있음.

![](https://velog.velcdn.com/images/wklee0607_/post/f5ebeac3-7a7d-4095-9634-b71e012c0333/image.png)

7. MediaStream.getTracks()

인터페이스의 getTracks()메서드는 에 관계없이 이 스트림의 모든 개체 MediaStream를 나타내는 시퀀스를 반환합니다.

#### 2.2 client/scss/screens/upload.scss 만들기
```javascript
.hidden {
    display: none;
}</code></pre><h4 id="23-recorderjs">2.3 recorder.js</h4>
<p>-&gt; 가짜 버튼 이용: a 안에 download가 있으면 href(url)로 이동하는게 아니라 href를 다운
<a href="https://developer.mozilla.org/ko/docs/Web/HTML/Element/a#attr-download">a - download</a></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/d0445dc3-3c31-4c64-980f-a6908fddf692/image.png" alt=""></p>
<pre><code class="language-javascript">const startBtn = document.getElementById(&quot;startBtn&quot;);
const initBtn = document.getElementById(&quot;initBtn&quot;);
const video = document.getElementById(&quot;preview&quot;);


let stream;
let recorder;
let videoFile;

const handleDownload = () =&gt; {
    const a = document.createElement(&quot;a&quot;);
    a.href = videoFile;
    a.download = &quot;MyRecording.webm&quot;; // href(url)을 다운로드 함. =&gt; 다운할 때 파일명을 넣어주면 됨.
    document.body.appendChild(a);
    a.click();
    const tracks = stream.getTracks();
    tracks.forEach((track) =&gt; {
        track.stop();
    });
    stream = null;
    video.src = null;
    startBtn.innerText = &quot;Start Recording&quot;;
    startBtn.removeEventListener(&quot;click&quot;,handleDownload);
    startBtn.addEventListener(&quot;click&quot;, handleStart);
    startBtn.classList.add(&quot;hidden&quot;);
    video.classList.add(&quot;hidden&quot;);
    initBtn.classList.remove(&quot;hidden&quot;);
};

const handleStop = () =&gt; {
    startBtn.innerText = &quot;Dowload Recording&quot;;
    startBtn.removeEventListener(&quot;click&quot;,handleStop);
    startBtn.addEventListener(&quot;click&quot;, handleDownload);
    recorder.stop();
};

const handleStart = () =&gt; {
    startBtn.innerText = &quot;Stop Recording&quot;;
    startBtn.removeEventListener(&quot;click&quot;, handleStart);
    startBtn.addEventListener(&quot;click&quot;, handleStop);
    recorder = new window.MediaRecorder(stream);//stream을 record가능한 형태로 만들어줌.
    recorder.ondataavailable = (event) =&gt; {// recorder data에 접근함. / event를 감지함. event가 발생하면 다음의 것들이 실행됨. 이건 stop()일 떄 발생하므로 stop()되면 event발생 -&gt; ondataavailable발생
        videoFile = URL.createObjectURL(event.data); // 브라우저 메모리 상에 저장을 해두고, 우리가 그 파일에 접근할 수 있는 URL을 준거.
        video.srcObject = null;
        video.src = videoFile;
        video.loop = true;
        video.play();
    };
    recorder.start();
};

const init = async() =&gt; {
    startBtn.classList.remove(&quot;hidden&quot;);
    video.classList.remove(&quot;hidden&quot;);
    initBtn.classList.add(&quot;hidden&quot;);
    stream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: true,
    });
    video.srcObject = stream; // video에 stream넣어주기 -&gt; stream이 object라서 video.src가 아니라 video.Object임.
    video.play();
};

initBtn.addEventListener(&quot;click&quot;, init);
startBtn.addEventListener(&quot;click&quot;, handleStart);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 11.8 -9 Controls Events]]></title>
            <link>https://velog.io/@wklee0607_/11.8-9-Controls-Events</link>
            <guid>https://velog.io/@wklee0607_/11.8-9-Controls-Events</guid>
            <pubDate>Mon, 11 Jul 2022 00:38:33 GMT</pubDate>
            <description><![CDATA[<h1 id="controls-events">Controls Events</h1>
<h2 id="1-비디오에-있는-버튼을-숨겼다가-비디오-위에-마우스를-놓으면-나타나는-걸-설정해줄-것임--마우스-올려놓은-상태에서-몇-초-있으면-컨트롤러-없어지도록-해줄-거--유저가-비디오-위에서-마우스를-멈추고-몇-초-있다가-컨트롤들-사라지게-하기">1. 비디오에 있는 버튼을 숨겼다가, 비디오 위에 마우스를 놓으면 나타나는 걸 설정해줄 것임. &amp; 마우스 올려놓은 상태에서 몇 초 있으면 컨트롤러 없어지도록 해줄 거. &amp; 유저가 비디오 위에서 마우스를 멈추고 몇 초 있다가 컨트롤들 사라지게 하기.</h2>
<h4 id="watchpug">watch.pug</h4>
<pre><code class="language-javascript">div#videoControls //아이디 추가
            button#play Play 
            button#mute Mute 
            input(type=&quot;range&quot;,step=&quot;0.1&quot;,value=0.5 ,min=&quot;0&quot;, max=&quot;1&quot;)#volume
            div
                span#currenTime 00:00
                span  / 
                span#totalTime 00:00
            div 
                input(type=&quot;range&quot;,step=&quot;0.5&quot;,value=0 ,min=&quot;0&quot;)#timeline
            div 
                button#fullScreen Enter Full Screen
</code></pre>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/c24fc521-fb03-4bbb-9b54-c2d58f640f0c/image.png" alt=""></p>
<h4 id="videoplayerjs">videoPlayer.js</h4>
<pre><code class="language-javascript">const videoControls = document.getElementById(&quot;videoControls&quot;);
let controlsTimeout = null;// 외부에 이 let값을 놓아줘서 timeout을 취소할 수 있도록 해줌.

const handelMouseMove = () =&gt; {
      if(controlsTimeout){
        clearTimeout(controlsTimeout);//이러면 timeout이 취소될 것임.
        controlsTimeout = null;
    }
    videoControls.classList.add(&quot;showing&quot;);// 마우스가 위에 있을 때, 컨트롤러들 보여주기
};

const handleMouseLeave = () =&gt; {
    const handleMouseLeave = () =&gt; {
    controlsTimeout = setTimeout(() =&gt; {
        videoControls.classList.remove(&quot;showing&quot;);
    }, 2000); //2초 뒤에 컨트롤러 사라짐. / 
    //console.log(controlsTimeout); Timeout의 id를 반환함.
};


video.addEventListener(&quot;mousemove&quot;, handelMouseMove);
video.addEventListener(&quot;mouseleave&quot;, handleMouseLeave);</code></pre>
<p>-&gt; 위에서 비디오에 마우스가 있다가 나가고 classList에서 showing이 setTimeout으로 인해 리스트가 사라져갈 때, 다시 마우스를 비디오 위에 놓아도 classList가 사라짐. 이를 방지하기 위해 timeout중에 마우스가 올려지면 timeout을 취소하도록(clearTimeout) 해줬음. 이 때 받는 인수값은 setTimeout의 id임.</p>
<p>참고 : <a href="https://developer.mozilla.org/en-US/docs/Web/API/clearTimeout">clearTimeout API</a></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/e223b068-fe90-4290-b7c6-b680f342ed97/image.png" alt=""></p>
<h2 id="2-유저가-비디오-위에서-마우스를-멈추고-몇-초-있다가-컨트롤들-사라지게-하기">2. 유저가 비디오 위에서 마우스를 멈추고 몇 초 있다가 컨트롤들 사라지게 하기.</h2>
<p>-&gt; mousestop은 없으므로, setTimeout과 cleartimeout을 이용. 즉, 마우스를 움직일 때마다 timeout을 발생시키면 됨. 한 마디로, 마우스를 움직일 때 setTimeot과 clearTimeout을 계속 발생시켜 주는 것임.</p>
<pre><code class="language-javascript">let controlsMovementTimeout = null;

const hideControls = () =&gt; videoControls.classList.remove(&quot;showing&quot;);

const handelMouseMove = () =&gt; {
    if(controlsTimeout){
        clearTimeout(controlsTimeout);//이러면 timeout이 취소될 것임.
        controlsTimeout = null;
    }
    if(controlsMovementTimeout){
        clearTimeout(controlsMovementTimeout);//이건 계속 hidecontrols를 취소시킴. -&gt; 매 움직임마다 이전의 움직임에 대한 timeout은 취소시키고, 마지막에 남은 timeout은 발생시키게 됨. 아하 !
        controlsMovementTimeout = null;
    }
    videoControls.classList.add(&quot;showing&quot;);
    controlsMovementTimeout = setTimeout(hideControls, 2000);//계속 hidecontrols를 timeout시킴.
};

const handleMouseLeave = () =&gt; {
    controlsTimeout = setTimeout(hideControls, 2000); //2초 뒤에 컨트롤러 사라짐. / Timeout의 id=39임. 즉, return값이 39라는 뜻임.
    //console.log(controlsTimeout);
};
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 11.7 video Full Screens]]></title>
            <link>https://velog.io/@wklee0607_/11.7-video-Full-Screens</link>
            <guid>https://velog.io/@wklee0607_/11.7-video-Full-Screens</guid>
            <pubDate>Sun, 10 Jul 2022 23:42:08 GMT</pubDate>
            <description><![CDATA[<h1 id="full-screen"><a href="https://developer.mozilla.org/ko/docs/Web/API/Fullscreen_API">Full Screen</a></h1>
<p>Fullscreen API
Fullscreen API 는 특정 요소 Element(와 해당 자손들을)를 full-screen mode로 표시하고, 더 이상 필요하지 않으면 full-screen mode를 종료하는 메서드를 추가합니다.</p>
<ul>
<li><p>Element.requestFullscreen() (en-US) ❗️
유저 에이전트가 지정한 요소(그리고 그 자손들까지)를 full-screen mode로 설정하고, 브라우저의 모든 UI 요소와 다른 모든 애플리케이션을 화면에서 제거하도록 요구합니다. full-screen mode가 활성화되면 Promise resolved를 반환합니다.</p>
</li>
<li><p>Document.exitFullscreen() (en-US) ❗️
user agent 가 full-screen mode에서 창 모드로 다시 전환되도록 요청합니다. full-screen mode가 완전히 종료되면 Promise resolved 를 반환합니다.</p>
</li>
</ul>
<p>DocumentOrShadowRoot.fullscreenElement (en-US) (사용 추천)
fullscreenElement 속성은 DOM(혹은 shadow DOM)에서 현재 full-screen mode로 표시되는 요소Element를 알려줍니다. 이것이 null인 경우, document는 full-screen mode가 아닙니다.</p>
<p>Document.fullscreen (en-US) (더 이상 사용되지 않는 속성)
(fullscreenElement처럼 풀스크린을 감지할 수 있지만 사용 비추천)
문서에 현재 full-screen mode로 표시되는 요소가 있는 경우 true, 그렇지 않으면 false의 Boolean 값입니다.</p>
<p><a href="https://developer.mozilla.org/ko/docs/Web/API/Fullscreen_API">https://developer.mozilla.org/ko/docs/Web/API/Fullscreen_API</a></p>
<h2 id="1-watchpug">1. watch.pug</h2>
<pre><code class="language-javascript">div#videoContainer
        video(src=&quot;/&quot; + video.fileUrl)
        div 
            button#play Play 
            button#mute Mute 
            input(type=&quot;range&quot;,step=&quot;0.1&quot;,value=0.5 ,min=&quot;0&quot;, max=&quot;1&quot;)#volume
            div
                span#currenTime 00:00
                span  / 
                span#totalTime 00:00
            div 
                input(type=&quot;range&quot;,step=&quot;0.5&quot;,value=0 ,min=&quot;0&quot;)#timeline
            div 
                button#fullScreen Enter Full Screen</code></pre>
<h2 id="2-videoplayerjs">2. videoPlayer.js</h2>
<pre><code class="language-javascript">const fullScreenBtn = document.getElementById(&quot;fullScreen&quot;);
const videoContainer = document.getElementById(&quot;videoContainer&quot;);// fullScreen으로 만들 때 videoContainer를 full스크린으로 해야 내가 만든 버튼까지도 풀스크린에 포함할 수 있음. video만 풀 스크린 하면 비디오만 커짐.

const handleFullscreen = () =&gt; {
    const fullscreen = document.fullscreenElement;//현재 fullScreen이 적용된 개체를 알려줌. -&gt; fullScreenContainer / 만약 fullScreen이 없으면 null을 return
    if(fullscreen){
        document.exitFullscreen();// document.exitFullscreen() : document method임. 풀스크린 나가기임.
        fullScreenBtn.innerText =&quot;Enter Full Screen&quot;;
    } else{
        videoContainer.requestFullscreen();//elemet.requestFullscreen() : fullscreen으로 만들어줌.
        fullScreenBtn.innerText =&quot;Exit Full Screen&quot;;
    }
};


fullScreenBtn.addEventListener(&quot;click&quot;, handleFullscreen);</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[# 11.5-6 Time Formatting & Timeline]]></title>
            <link>https://velog.io/@wklee0607_/11.5-6-Time-Formatting-Timeline</link>
            <guid>https://velog.io/@wklee0607_/11.5-6-Time-Formatting-Timeline</guid>
            <pubDate>Sun, 10 Jul 2022 15:20:21 GMT</pubDate>
            <description><![CDATA[<h1 id="1-time-formatting-with-속임수">1. Time Formatting with 속임수</h1>
<h2 id="date-constructor-이용">date constructor 이용</h2>
<p>new Date(ms) : new Date는 1970년 1월 1일부터 09시부터(제로 타임 = new Date(0)) 날짜를 세기 시작하고, ms에 숫자를 넣으면 그 밀리세컨드 이후의 날짜 혹은 시간을 return함.</p>
<p>우리가 할 건 밑의 사진 처럼, 시, 분, 그리고 시간 부분을 이용하여 비디오의 시간 부분을 나타낼 것임.</p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/90b28f24-eb03-4d68-95bb-5391b39b6a44/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/3c6a7c37-c40c-4eda-bcec-f60b57d141ed/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/wklee0607_/post/b9c5bb97-c7e9-4552-b6a2-27ccb8499390/image.png" alt=""></p>
<p>substr: str에서 원하는 시작위치에서 원하는 길이만큼 잘라서 return해줌. -&gt; 현재는 subString(시작 인덱스, 종료 인덱스) 이걸 이용하셈.</p>
<ul>
<li>videoPlayer.js<pre><code class="language-javascript">const formatTime = (seconds) =&gt; {
  if(seconds &lt; 3600){
      return new Date(seconds * 1000).toISOString().substring(14,19);
  } 
  return new Date(seconds * 1000).toISOString().substring(11,19);
}
</code></pre>
</li>
</ul>
<p>const handleLoadedMetadata = () =&gt; {
    totalTime.innerText = formatTime(Math.floor(video.duration));
};</p>
<p>const handleTimeUpdate = () =&gt; {
    currenTime.innerText = formatTime(Math.floor(video.currentTime));
};</p>
<pre><code>

# 2. Timeline

## 2.1 watch.pug수정
```javascript
//추가
div 
    input(type=&quot;range&quot;,step=&quot;1&quot;,value=0 ,min=&quot;0&quot;)#timeline

// 최종
video(src=&quot;/&quot; + video.fileUrl)
    div 
        button#play Play 
        button#mute Mute 
        input(type=&quot;range&quot;,step=&quot;0.1&quot;,value=0.5 ,min=&quot;0&quot;, max=&quot;1&quot;)#volume
        div
            span#currenTime 00:00
            span  / 
            span#totalTime 00:00
        div 
            input(type=&quot;range&quot;,step=&quot;0.5&quot;,value=0 ,min=&quot;0&quot;)#timeline</code></pre><h2 id="22-videoplayerjs">2.2 videoPlayer.js</h2>
<pre><code class="language-javascript">const timeline = document.getElementById(&quot;timeline&quot;);

const handleLoadedMetadata = () =&gt; {
    totalTime.innerText = formatTime(Math.floor(video.duration));
    timeline.max = Math.floor(video.duration);//추가 -&gt; 비디오 총 길이 timeline에 넣음.
};

const handleTimeUpdate = () =&gt; {
    currenTime.innerText = formatTime(Math.floor(video.currentTime));
    timeline.value = Math.floor(video.currentTime);//추가 -&gt; 비디오의 현재 진행시간을 타임라인에 나타내줌.
};

const handleTimelineChange = (event) =&gt; {
    const { target: {value}} = event;
    video.currentTime = value;
};

timeline.addEventListener(&quot;input&quot;, handleTimelineChange);</code></pre>
<p>실제 유튜브처럼 타임라인 옮기는 중에는 재생이 멈추고, 다 옮긴 후에 재생이 되도록 수정하였습니다! 타임라인 옮기기 전 재생 상태를 유지합니다. 재생 중이었다면 재생, 일시정지 중이었다면 일시정지.</p>
<p>let videoPlayStatus = false;
let setVideoPlayStatus = false;</p>
<p>const handleTimelineChange = (event) =&gt; {
const {
target: { value },
} = event;
if (!setVideoPlayStatus) {
videoPlayStatus = video.paused ? false : true;
setVideoPlayStatus = true;
}
video.pause();
video.currentTime = value;
};</p>
<p>const handleTimelineSet = () =&gt; {
videoPlayStatus ? video.play() : video.pause();
setVideoPlayStatus = false;
};</p>
<p>timeline.addEventListener(&quot;change&quot;, handleTimelineSet);</p>
]]></description>
        </item>
    </channel>
</rss>