<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>sh_0317.log</title>
        <link>https://velog.io/</link>
        <description>코린이</description>
        <lastBuildDate>Thu, 07 Sep 2023 16:53:21 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>sh_0317.log</title>
            <url>https://velog.velcdn.com/images/sh_0317/profile/b02a2f9e-af62-4440-93d5-f8b0c28881b9/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. sh_0317.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/sh_0317" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[TIL] heap out of memory (2/2) 23.09.08]]></title>
            <link>https://velog.io/@sh_0317/TIL-heap-out-of-memory-22-23.09.08</link>
            <guid>https://velog.io/@sh_0317/TIL-heap-out-of-memory-22-23.09.08</guid>
            <pubDate>Thu, 07 Sep 2023 16:53:21 GMT</pubDate>
            <description><![CDATA[<p>아래와 같이 workflow를 작성했는데 이전에 발생했던 문제가 또 다시 발생</p>
<pre><code class="language-script">name : Build and Deploy to EC2

on:
  push:
    branches: [&quot;main&quot;]
  pull_request:
    branches: [&quot;main&quot;]

jobs:
  AUTO_DEPLOY:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: &#39;16&#39;

      - name: Run scripts in server
        uses: appleboy/ssh-action@master
        with:
          host: ${{secrets.HOST }}
          username: ${{secrets.USERNAME }}
          password: ${{secrets.PASSWORD }}
          port: ${{secrets.PORT }}
          timeout: 40s
          script: ${{secrets.SCRIPT }}</code></pre>
<p>script 파일에는 </p>
<pre><code class="language-script">cd workflow-service
git pull origin main
npm install
npm run build
sudo pm2 restart</code></pre>
<p>이렇게 작성해두었는데 stack overflow에서 찾아보니 빌드할 때 앞에서 했었던 node.js의 사용 메모리를 늘려주는 코드를 작성해야 한다고 함
<strong>수정 script</strong></p>
<pre><code class="language-script">cd workflow-service
git pull origin main
export NODE_OPTIONS=--max_old_space_size=2048
npm install
npm run build
sudo pm2 restart</code></pre>
<hr>
<p>해당 오류는 수정됐으나</p>
<pre><code class="language-script">err: From https://github.com/toy-workflow-service/workflow-service
err:  * branch            main       -&gt; FETCH_HEAD
out: Already up to date.
out: up to date, audited 1141 packages in 3s
out: 179 packages are looking for funding
out:   run `npm fund` for details
out: 13 vulnerabilities (2 moderate, 11 critical)
out: To address all issues possible (including breaking changes), run:
out:   npm audit fix --force
out: Some issues need review, and may require choosing
out: a different dependency.
out: Run `npm audit` for details.
out: &gt; work-flow@0.0.1 build
out: &gt; nest build
2023/09/07 16:24:06 Process exited with status 1
err:   error: missing required argument `id|name|namespace|all|json|stdin&#39;</code></pre>
<p>missing required argument 오류가 발생하여 해결예정,,</p>
<hr>
<h3 id="ec2-ssh-환경-비밀번호로-로그인">EC2 SSH 환경 비밀번호로 로그인</h3>
<pre><code class="language-script">ssh - i key-pair.pem ubuntu@ip_address</code></pre>
<p>키페어를 사용하여 아이피를 넣고 로그인 한 뒤</p>
<pre><code class="language-script">sudo 원하는 비밀번호 ubuntu </code></pre>
<p>원하는 비밀번호를 설정!</p>
<pre><code class="language-script">sudo vi /etc/ssh/sshd_config</code></pre>
<p>sshd_config파일을 열어 준 뒤 a or i를 눌러 입력모드로 전환 후 아래로 좀 내려가면</p>
<pre><code class="language-script">PasswordAuthentication yes</code></pre>
<p>no로 되어있는 값을 yes로 변경해주고</p>
<pre><code class="language-script">PermitRootLogin prohibit-password
-&gt;
PermitRootLogin yes</code></pre>
<p>prohibit-password를 지우고 yes로 변경하여 루트로그인을 허용함
이후 exit을 한 뒤</p>
<pre><code class="language-script">ssh ubuntu@ip_address</code></pre>
<p>를 입력하면 패스워드를 입력하고 로그인 가능!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] heap out of memory 에러 (1/2) 23.09.07 ]]></title>
            <link>https://velog.io/@sh_0317/TIL-heap-out-of-memory-%EC%97%90%EB%9F%AC-23.09.07</link>
            <guid>https://velog.io/@sh_0317/TIL-heap-out-of-memory-%EC%97%90%EB%9F%AC-23.09.07</guid>
            <pubDate>Thu, 07 Sep 2023 16:44:48 GMT</pubDate>
            <description><![CDATA[<p>EC2에서 npm run build를 하던 도중 heap out of memory에러가 발생
<img src="https://velog.velcdn.com/images/sh_0317/post/12327051-6e00-4271-9b6e-13ad39042011/image.PNG" alt=""></p>
<pre><code class="language-javascript">const used = process.memoryUsage();
console.log(`메모리 사용량: ${Math.round(used.rss / 1024 / 1024)}MB`);</code></pre>
<p>main.ts에 해당 코드를 작성 후 사용 메모리를 봤는데 123mb로 512mb보다 한참 떨어지는데 왜 저런 오류가 날까 해서 구글링 해보니 일단 build하는 과정 중에 node.js에서 사용할 수 있는 메모리양을 넘어서 그렇다는 이유, 또 t2.micro는 사용할 수 있는 기본 용량이 1gb라 swap메모리를 사용해야 한다 하여 둘 다 적용하였음</p>
<pre><code class="language-javascript">$ node -e &#39;console.log(v8.getHeapStatistics().heap_size_limit/(1024*1024))&#39;
-&gt; 499.25</code></pre>
<p>500mb를 2048mb로 늘려주고</p>
<pre><code class="language-javascript">$ export NODE_OPTIONS=--max_old_space_size=2048</code></pre>
<pre><code class="language-javascript">dd if=/dev/zero of=/swap-file bs=1k count=2048000</code></pre>
<p>swapfile을 생성해주고
dd : 크기를 가진 파일을 만드는 명령
if : 초기화할 때 사용하는 장치 파일명
of : 생성할 파일명
bs : 블록크기 지정 (단위없을 경우 : 바이트 처리)
count : bs에 설정한 블록의 개수
-&gt; 위 명령어대로라면, 현재위치에 swap-file이라는 이름의 2GB를 가진 파일이 하나 만들어진다.</p>
<p>읽기쓰기 권한 설정을 해줌</p>
<pre><code class="language-javascript">sudo chmod 600 /swapfile</code></pre>
<p>swap-file을 저장할 공간을 생성해주고</p>
<pre><code class="language-javascript">sudo mkswap /swapfile
// vi에디터를 열어 줌
sudo vi /etc/fstab
// 파일의 맨 끝 다음줄에 아래의 명령어를 작성하여 ec2 서버가 꺼져도 활성화 될 수 있도록 함
/swapfile swap swap defaults 0 0</code></pre>
<hr>
<p>ubuntu에서 정상적으로 build가 되고 있으나 git action에서 같은 오류가 발생하였음</p>
<pre><code class="language-script">name : Build and Deploy to EC2

on:
  push:
    branches: [&quot;main&quot;]
  pull_request:
    branches: [&quot;main&quot;]

jobs:
  AUTO_DEPLOY:
    runs-on: ubuntu-latest

    steps:
      - name: Check out code
        uses: actions/checkout@v2

      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: &#39;16&#39;

      - name: Run scripts in server
        uses: appleboy/ssh-action@master
        with:
          host: ${{secrets.HOST }}
          username: ${{secrets.USERNAME }}
          password: ${{secrets.PASSWORD }}
          port: ${{secrets.PORT }}
          timeout: 40s
          script: ${{secrets.SCRIPT }}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 파일 출력 및 용량계산 23.09.06]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8C%8C%EC%9D%BC-%EC%B6%9C%EB%A0%A5-%EB%B0%8F-%EC%9A%A9%EB%9F%89%EA%B3%84%EC%82%B0-23.09.06</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8C%8C%EC%9D%BC-%EC%B6%9C%EB%A0%A5-%EB%B0%8F-%EC%9A%A9%EB%9F%89%EA%B3%84%EC%82%B0-23.09.06</guid>
            <pubDate>Wed, 06 Sep 2023 14:56:36 GMT</pubDate>
            <description><![CDATA[<p>워크스페이스 디테일에서 현재 이 워크스페이스에 업로드 된 모든 파일을 가져오기 위해
쿼리빌더를 사용하여 모든 파일을 조회</p>
<pre><code class="language-typescript">// 워크스페이스에 업로드된 모든 파일 조회
  async getAllFiles(workspaceId: number): Promise&lt;File[]&gt; {
    const workspace = await this.workspaceRepository
      .createQueryBuilder(&#39;workspace&#39;)
      .innerJoinAndSelect(&#39;workspace.boards&#39;, &#39;boards&#39;)
      .innerJoinAndSelect(&#39;boards.board_columns&#39;, &#39;board_columns&#39;)
      .innerJoinAndSelect(&#39;board_columns.cards&#39;, &#39;cards&#39;)
      .where(&#39;workspace.id = :workspaceId&#39;, { workspaceId })
      .select([
        &#39;cards.id&#39;,
        &#39;cards.file_original_name&#39;,
        &#39;cards.file_url&#39;,
        &#39;cards.file_size&#39;,
        &#39;cards.created_at&#39;,
        &#39;cards.updated_at&#39;,
      ])
      .getRawMany();
    return workspace;
  }</code></pre>
<p>필요한 컬럼들을 선택해 준 뒤 프론트에서 요청 시 데이터가 넘어가는 지 확인
<img src="https://velog.velcdn.com/images/sh_0317/post/b67641ef-003d-4e95-a280-3ee06c5c4872/image.png" alt="">
원하는 정보가 넘어온 것을 확인하고 하나의 카드에 여러개의 파일이 존재하는 경우도 있으므로
다음과 같이 코드를 작성</p>
<pre><code class="language-javascript">// 전체파일 조회
async function getAllFiles() {
  try {
    await $.ajax({
      method: &#39;GET&#39;,
      url: `/workspaces/${workspaceId}/getFiles`,
      beforeSend: function (xhr) {
        xhr.setRequestHeader(&#39;Content-type&#39;, &#39;application/json&#39;);
        xhr.setRequestHeader(&#39;authorization&#39;, `Bearer ${accessToken}`);
      },
      success: async (data) =&gt; {
        let result = &#39;&#39;;
        data.forEach((file) =&gt; {
          const fileOriginalName = file.cards_file_original_name;
          const fileUrl = file.cards_file_url;
          const fileSize = file.cards_file_size;
          let fileNames = [];
          let fileSizes = [];
          let fileUrls = [];

          if (typeof fileOriginalName === &#39;string&#39;) {
            try {
              fileNames = JSON.parse(fileOriginalName);
            } catch (err) {
              fileNames = [fileOriginalName];
            }
          } else {
            fileNames = fileOriginalName;
          }

          if (typeof fileSize === &#39;string&#39;) {
            try {
              fileSizes = JSON.parse(fileSize);
            } catch (err) {
              fileSizes = [parseInt(fileSize)];
            }
          } else {
            fileSizes = fileSize;
          }

          if (typeof fileUrl === &#39;string&#39;) {
            try {
              fileUrls = JSON.parse(fileUrl);
            } catch (err) {
              fileUrls = [fileUrl];
            }
          } else {
            fileUrls = fileUrl;
          }

          if (Array.isArray(fileNames) &amp;&amp; fileNames.length &gt; 0) {
            for (let i = 0; i &lt; fileNames.length; i++) {
              const fileName = fileNames[i];
              const fileUrl = fileUrls[i];
              const imgSrc = getImgSource(fileName);
              const fileSize = getFileSize(fileSizes[i]);

              result += printFilesHtml(fileName, imgSrc, fileSize, fileUrl);
            }
          } else if (typeof fileOriginalName === &#39;string&#39;) {
            const fileName = fileOriginalName.replace(/&quot;/g, &#39;&#39;);
            const fileSize = getFileSize(fileSizes);
            const fileUrl = fileUrls;
            const imgSrc = getImgSource(fileName);

            result += printFilesHtml(fileName, imgSrc, fileSize, fileUrl);
          }
        });

        const totalSize = getTotalFileSize(data);
        const storage = await printStorageSize(totalSize);

        printStorage.innerHTML = storage;
        printFiles.innerHTML = result;
      },
    });
  } catch (err) {
    console.error(err);
  }
}</code></pre>
<p>배열이 문자열로 넘어오는 형태기때문에 문자열이라면 JSON형태로 파싱해주는 코드를 작성하고
파일의 아래 용량을 표현해주기위해 파일사이즈를 구하는 함수를 작성</p>
<pre><code class="language-javascript">// 파일 메가바이트로 변환
function getFileSize(fileSize) {
  const fileSizeInMb = fileSize / (1024 * 1024);
  return fileSizeInMb.toFixed(2);
}</code></pre>
<p>확장자에 따라 붙여주는 이미지가 다르므로 확장자 구분 함수도 작성</p>
<pre><code class="language-javascript">// 파일의 확장자 분류
function getImgSource(fileName) {
  const extension = fileName.split(&#39;.&#39;).pop().toLowerCase();
  const fileExtension = extension.replace(/&quot;/g, &#39;&#39;);
  let imgSrc = &#39;&#39;;

  switch (fileExtension) {
    case &#39;jpg&#39;:
    case &#39;jpeg&#39;:
      imgSrc = &#39;./assets/img/jpg@2x.png&#39;;
      break;
    case &#39;png&#39;:
      imgSrc = &#39;./assets/img/png@2x.png&#39;;
      break;
    case &#39;zip&#39;:
      imgSrc = &#39;./assets/img/zip@2x.png&#39;;
      break;
    case &#39;pdf&#39;:
      imgSrc = &#39;./assets/img/pdf@2x.png&#39;;
      break;
    case &#39;psd&#39;:
      imgSrc = &#39;./assets/img/psd@2x.png&#39;;
      break;
    default:
      imgSrc = &#39;./assets/img/document.png&#39;;
      break;
  }
  return imgSrc;
}</code></pre>
<p><strong>출력결과</strong>
<img src="https://velog.velcdn.com/images/sh_0317/post/3abead04-dff2-411d-8392-c5d64ad656f7/image.png" alt="">
중간의 []빈 파일은 카드 수정 시 파일을 변경하지 않으면 기존파일이 그대로 들어있어야 하는데 빈값으로 변경되는 현상이 있어서 수정필요</p>
<hr>
<p>파일출력의 구현이 끝났으면 파일 용량을 계산해줘야 하므로 getAllFiles()로 불러왔던 파일사이즈를 활용하여 전쳬용량을 계산</p>
<pre><code class="language-javascript">// 파일 전체용량 계산
function getTotalFileSize(files) {
  let totalSize = 0;
  for (const file of files) {
    const fileSize = file.cards_file_size;

    if (typeof fileSize === &#39;string&#39;) {
      const sizes = JSON.parse(fileSize);
      if (Array.isArray(sizes)) {
        for (const size of sizes) {
          totalSize += parseInt(size, 10) || 0;
        }
      } else {
        totalSize += parseInt(sizes, 10) || 0;
      }
    } else if (typeof fileSize === &#39;number&#39;) {
      totalSize += fileSize;
    }
  }

  const totalSizeInMb = totalSize / (1024 * 1024);

  return totalSizeInMb.toFixed(2);
}</code></pre>
<p>무료버전의 제한용량이 100mb이므로 mb단위로 변환, parseInt는 10진수로 고정을 해주고 여기서 합산된 사이즈를 printStorageSize()로 보내줌</p>
<pre><code class="language-javascript">// 워크스페이스 용량 조회
async function printStorageSize(totalSize) {
  try {
    const results = await $.ajax({
      method: &#39;GET&#39;,
      url: `/workspaces/${workspaceId}`,
      beforeSend: function (xhr) {
        xhr.setRequestHeader(&#39;Content-type&#39;, &#39;application/json&#39;);
        xhr.setRequestHeader(&#39;authorization&#39;, `Bearer ${accessToken}`);
      },
    });

    const { data } = results;
    const totalSizeInGb = (totalSize / 1024).toFixed(2);
    const usagePercentageGb = ((totalSizeInGb / 10) * 100).toFixed(0);
    const usagePercentageMb = ((totalSize / 100) * 100).toFixed(0);

    if (data.memberships.length) {
      return `&lt;div class=&quot;user-group-progress-bar&quot;&gt;
                    &lt;p&gt;워크스페이스 사용량&lt;/p&gt;
                    &lt;div class=&quot;progress-wrap d-flex align-items-center mb-0&quot;&gt;
                      &lt;div class=&quot;progress&quot;&gt;
                        &lt;div
                          class=&quot;progress-bar bg-success&quot;
                          role=&quot;progressbar&quot;
                          style=&quot;width: ${usagePercentageGb}%&quot;
                          aria-valuenow=&quot;${usagePercentageGb}&quot;
                          aria-valuemin=&quot;0&quot;
                          aria-valuemax=&quot;100&quot;
                        &gt;&lt;/div&gt;
                      &lt;/div&gt;
                      &lt;span class=&quot;progress-percentage&quot;&gt;${usagePercentageGb}%&lt;/span&gt;
                    &lt;/div&gt;
                    &lt;span class=&quot;&quot;&gt;10GB 중 ${totalSizeInGb}GB 사용&lt;/span&gt;
                  &lt;/div&gt;`;
    } else {
      return `&lt;div class=&quot;user-group-progress-bar&quot;&gt;
                    &lt;p&gt;워크스페이스 사용량&lt;/p&gt;
                    &lt;div class=&quot;progress-wrap d-flex align-items-center mb-0&quot;&gt;
                      &lt;div class=&quot;progress&quot;&gt;
                        &lt;div
                          class=&quot;progress-bar bg-success&quot;
                          role=&quot;progressbar&quot;
                          style=&quot;width: ${usagePercentageMb}&amp;&quot;
                          aria-valuenow=&quot;${usagePercentageMb}&quot;
                          aria-valuemin=&quot;0&quot;
                          aria-valuemax=&quot;100&quot;
                        &gt;&lt;/div&gt;
                      &lt;/div&gt;
                      &lt;span class=&quot;progress-percentage&quot;&gt;${usagePercentageMb}%&lt;/span&gt;
                    &lt;/div&gt;
                    &lt;span class=&quot;&quot;&gt;100MB 중 ${totalSize}MB 사용&lt;/span&gt;
                  &lt;/div&gt;`;
    }
  } catch (err) {
    console.error(err);
  }
}</code></pre>
<p>멤버십을 가입한 워크스페이스라면 용량제한이 10gb이므로 gb로 사이즈를 변환해주고, 불러온 데이터 중 멤버십배열을 기준으로 있으면 10gb, 없으면 100mb로 html을 붙여줌</p>
<p><strong>출력결과</strong>
무료버전인 경우
<img src="https://velog.velcdn.com/images/sh_0317/post/2f3b4dc9-003c-4f76-a4df-bd59f0740bf8/image.png" alt=""></p>
<p>멤버십에 가입된 경우
<img src="https://velog.velcdn.com/images/sh_0317/post/fee3d3b3-bd32-4f52-b7ec-7e8c73f956ff/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] OSI 7 layer, 세션 기반 인증과 토큰 기반 인증의 차이 23.09.05]]></title>
            <link>https://velog.io/@sh_0317/TIL-OSI-7-layer-%EC%84%B8%EC%85%98-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%ED%86%A0%ED%81%B0-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4-23.09.05</link>
            <guid>https://velog.io/@sh_0317/TIL-OSI-7-layer-%EC%84%B8%EC%85%98-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EA%B3%BC-%ED%86%A0%ED%81%B0-%EA%B8%B0%EB%B0%98-%EC%9D%B8%EC%A6%9D%EC%9D%98-%EC%B0%A8%EC%9D%B4-23.09.05</guid>
            <pubDate>Tue, 05 Sep 2023 13:44:10 GMT</pubDate>
            <description><![CDATA[<h2 id="osi-7-layer란">OSI 7 Layer란?</h2>
<p>OSI 7 계층은 네트워크 통신을 구성하는 요소들을 7개의 계층으로 표준화 한 것임
통신이 일어나는 과정을 단계별로 파악할 수 있어 문제 발생 시 해당 문제를 해결하기 좋음</p>
<ul>
<li>7 계층 (Application Layer, 응용 계층) : 사용자에게 통신을 위한 서비스 제공, 인터페이스 역할</li>
<li>6 계층 (Presentation Layer, 표현 계층) : 데이터의 형식을 정의하는 계층 (코드 간의 번역 담당)</li>
<li>5 계층 (Session Layer, 세션 계층) : 컴퓨터끼리 통신을 하기 위해 세션을 만드는 계층</li>
<li>4 계층 (Transport Layer, 전송 계층) : 프로그램과 프로그램의 연결, 최종 수신 프로세스로 데이터의 전송을 담당하는 계층(단위 : Segment, ex. TCP, UDP)</li>
<li>3 계층 (Network Layer, 네트워크 계층) : 패킷을 목적지까지 가장 빠른 길로 전송하기위한 계층 (단위 : Packet, ex. Router ICMP IP APR)</li>
<li>2 계층 (Datalink Layer, 데이터링크 계층) : 데이터의 물리적인 전송과 에러 검출, 흐름 제어를 담당하는 계층 (단위 : Frame, ex. 이더넷, mac)</li>
<li>1 계층 (Physical Layer, 물리 계층) : 데이터를 전기 신호로 바꿔주는 계층 (단위 : bit, 장비 : 케이블, 리피터, 허브)</li>
</ul>
<h2 id="세션-기반-인증-vs-토큰-기반-인증">세션 기반 인증 vs 토큰 기반 인증</h2>
<p>세션 기반 인증은 클라이언트로부터 요청을 받으면 클라이언트의 상태 정보를 저장하므로 Stateful한 구조를 가지고, 토큰 기반 인증은 상태 정보를 서버에 저장하지 않으므로 Stateless한 구조를 가짐.</p>
<p>세션의 경우 Cookie 헤더에 세션ID만 실어 보내면 되므로 트래픽을 적게 사용하지만 JWT는 사용자 인증 정보와 토큰의 발급, 만료시작, 토큰의 ID등 담겨있는 정보가 세션보다 많아 훨씬 더 많은 트래픽을 사용함.</p>
<p>세션의 경우 모든 인증 정보를 서버에서 관리하기 때문에 토큰 보다 보안 측면에서 조금 더 유리함. JWT의 특성상 토큰에 실린 Payload가 별도로 암호화 되어있지 않으므로 누구나 내용을 확인할 수 있고 토큰이 한 번 탈취되면 해당 토큰이 만료되기 전까지는 피해를 입을 수 밖에 없음.</p>
<p>그러나 세션에 비해 토큰 기반 인증은 확장성이 높음. 일반적으로 웹 어플리케이션의 서버 확장 방식은 수평 확장을 사용함. 즉, 한 대가 아닌 여러대의 서버가 요청을 처리하게 되는데, 이 때 별도의 작업을 해주지 않는다면, 세션 기반 인증 방식은 세션 불일치 문제를 겪게 됨. 하지만 토큰 기반 인증 방식의 경우 클라이언트가 저장하는 방식을 취하기 때문에 이런 문제로부터 자유로움.</p>
<p>마지막으로 세션 기반 인증 방식은 서비스가 세션 데이터를 직접 저장하고, 관리함. 따라서 세션 데이터의 양이 많아지면 많아질수록 서버의 부담이 증가.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 사용 로그 출력 23.09.04]]></title>
            <link>https://velog.io/@sh_0317/TIL-%EC%82%AC%EC%9A%A9-%EB%A1%9C%EA%B7%B8-%EC%B6%9C%EB%A0%A5-23.09.04</link>
            <guid>https://velog.io/@sh_0317/TIL-%EC%82%AC%EC%9A%A9-%EB%A1%9C%EA%B7%B8-%EC%B6%9C%EB%A0%A5-23.09.04</guid>
            <pubDate>Tue, 05 Sep 2023 13:28:56 GMT</pubDate>
            <description><![CDATA[<p>워크스페이스 디테일에 activity 내용을 출력해주기 위해
사용 로그를 만들기로 결정하였음</p>
<pre><code class="language-typescript">// audit-log.entity.ts
import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from &#39;typeorm&#39;;
import { User } from &#39;./user.entitiy&#39;;
import { Workspace } from &#39;./workspace.entity&#39;;
import ActionType from &#39;../utils/action-type&#39;;

@Entity(&#39;audit_logs&#39;)
export class Audit_log {
  @PrimaryGeneratedColumn({ type: &#39;bigint&#39; })
  id: number;

  @Column({ type: &#39;enum&#39;, enum: ActionType, nullable: false })
  actions: ActionType;

  @Column({ nullable: false })
  details: string;

  @CreateDateColumn()
  created_at: Date;

  @ManyToOne(() =&gt; User, (user) =&gt; user.audit_logs)
  user: User;

  @ManyToOne(() =&gt; Workspace, (workspace) =&gt; workspace.audit_logs, {
    onDelete: &#39;CASCADE&#39;,
    nullable: false,
  })
  workspace: Workspace;
}</code></pre>
<p>초반에는 board, boardColumn, card도 관계설정을 해뒀다. 어떤 부분에서 이벤트가 발생하는지 출력을 해주기 위함이였는데 다시 생각해보니 워크스페이스 디테일에 들어가는 로그기 때문에, 어떤 유저와 어떤 워크스페이스에서 이벤트가 발생했는지만 출력해주면 될 것 같다고 판단되어 변경하였음</p>
<pre><code class="language-typescript">import { Injectable } from &#39;@nestjs/common&#39;;
import { InjectRepository } from &#39;@nestjs/typeorm&#39;;
import { Audit_log } from &#39;src/_common/entities/audit-log.entity&#39;;
import ActionType from &#39;src/_common/utils/action-type&#39;;
import { Repository } from &#39;typeorm&#39;;

@Injectable()
export class AuditLogsService {
  constructor(
    @InjectRepository(Audit_log)
    private auditLogRepository: Repository&lt;Audit_log&gt;
  ) {}

  async inviteMemberLog(
    workspaceId: number,
    inviterUserId: number,
    inviterUserName: string,
    invitedUserName: string
  ): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: inviterUserId },
      actions: ActionType.INVITE,
      details: `멤버 초대 - ${inviterUserName}님이 ${invitedUserName}님을 워크스페이스에 초대하였습니다. `,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async deleteMemberLog(
    workspaceId: number,
    userId: number,
    userName: string,
    deletedUser: string
  ): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.DELETE,
      details: `멤버 삭제 - ${userName}님이 ${deletedUser}님을 워크스페이스에서 내보냈습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async roleChangeLog(
    workspaceId: number,
    userId: number,
    userName: string,
    targetUser: string,
    role: string
  ): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.ROLE_CHANGE,
      details: `역할 변경 - ${userName}님이 ${targetUser}님의 역할을 ${role}로 변경했습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async createBoardLog(workspaceId: number, boardName: string, userId: number, userName: string): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.CREATE,
      details: `보드 생성 - ${userName}님이 ${boardName} 보드를 생성하였습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async updateBoardLog(
    workspaceId: number,
    beforeBoardName: string,
    afterBoardName: string,
    userId: number,
    userName: string
  ): Promise&lt;Audit_log&gt; {
    if (beforeBoardName === afterBoardName) {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `보드 수정 - ${userName}님이 ${beforeBoardName} 보드를 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    } else {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `보드 수정 - ${userName}님이 ${beforeBoardName} 보드를 ${afterBoardName} 보드로 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    }
  }

  async deleteBoardLog(workspaceId: number, boardName: string, userId: number, userName: string): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.DELETE,
      details: `보드 삭제 - ${userName}님이 ${boardName} 보드를 삭제하였습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async createColumnLog(workspaceId: number, columnName: string, userId: number, userName: string): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.CREATE,
      details: `컬럼 생성 - ${userName}님이 ${columnName} 컬럼을 생성하였습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async updateColumnLog(
    workspaceId: number,
    beforeColumnName: string,
    afterColumnName: string,
    userId: number,
    userName: string
  ): Promise&lt;Audit_log&gt; {
    if (beforeColumnName === afterColumnName) {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `컬럼 수정 - ${userName}님이 ${beforeColumnName} 컬럼을 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    } else {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `컬럼 수정 - ${userName}님이 ${beforeColumnName} 컬럼을 ${afterColumnName} 컬럼으로 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    }
  }

  async deleteColumnLog(workspaceId: number, columnName: string, userId: number, userName: string): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.DELETE,
      details: `컬럼 삭제 - ${userName}님이 ${columnName} 컬럼을 삭제하였습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async createCardLog(workspaceId: number, cardName: string, userId: number, userName: string): Promise&lt;Audit_log&gt; {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.CREATE,
      details: `카드 생성 - ${userName}님이 ${cardName} 카드를 생성하였습니다.`,
    });

    return await this.auditLogRepository.save(newLog);
  }

  async updateCardLog(
    workspaceId: number,
    beforeCardName: string,
    afterCardName: string,
    userId: number,
    userName: string
  ) {
    if (beforeCardName === afterCardName) {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `카드 수정 - ${userName}님이 ${beforeCardName} 카드를 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    } else {
      const newLog = this.auditLogRepository.create({
        workspace: { id: workspaceId },
        user: { id: userId },
        actions: ActionType.UPDATE,
        details: `카드 수정 - ${userName}님이 ${beforeCardName} 카드를 ${afterCardName} 카드로 수정하였습니다.`,
      });

      return await this.auditLogRepository.save(newLog);
    }
  }

  async deleteCardLog(workspaceId: number, cardName: string, userId: number, userName: string) {
    const newLog = this.auditLogRepository.create({
      workspace: { id: workspaceId },
      user: { id: userId },
      actions: ActionType.DELETE,
      details: `카드 수정 - ${userName}님이 ${cardName} 카드를 삭제하였습니다.`,
    });
    return await this.auditLogRepository.save(newLog);
  }
}
</code></pre>
<p>다소 반복되는감이 없지 않아 있긴 하지만 각 이벤트별로 출력될 로그들을 미리 작성해두고 테스트</p>
<pre><code class="language-typescript">//workspace.service.ts

  // 멤버 권한 변경
  async setMemberRole(
    body: SetRoleDto,
    workspaceId: number,
    userId: number,
    loginUserName: string,
    loginUserId: number
  ): Promise&lt;IResult&gt; {
    const existMember = await this.workspaceMemberRepository.findOne({
      where: { workspace: { id: workspaceId }, user: { id: userId } },
      relations: [&#39;user&#39;],
    });
    const loginUserRole = await this.loginUserRole(loginUserId, workspaceId);

    if (!existMember) throw new HttpException(&#39;해당 멤버가 존재하지 않습니다.&#39;, HttpStatus.NOT_FOUND);

    if (loginUserRole / 1 &gt;= existMember.role)
      throw new HttpException(&#39;관리자 또는 어드민 계정은 역할 변경이 불가합니다.&#39;, HttpStatus.BAD_REQUEST);

    await this.workspaceMemberRepository.update(
      { user: { id: userId }, workspace: { id: workspaceId } },
      { role: body.role }
    );

    let role = &#39;&#39;;
    if (body.role === 2) role = &#39;Manager&#39;;
    if (body.role === 3) role = &#39;Member&#39;;
    if (body.role === 4) role = &#39;OutSourcing&#39;;

    await this.auditLogService.roleChangeLog(workspaceId, userId, loginUserName, existMember.user.name, role);

    return { result: true };
  }</code></pre>
<p>멤버권한 변경하는 코드의 하단부에 roleChangeLog 메서드를 추가해준 뒤 실행해보면</p>
<p><img src="https://velog.velcdn.com/images/sh_0317/post/709a5b43-8eaf-4ab5-b794-1b87686f2924/image.png" alt="">
2번 유저가 15번 워크스페이스에서 만든 이벤트가 audit_log 테이블에 잘 쌓이고 있는 것을 확인.</p>
<p>이것을 이제 프론트에 연결해주는 작업 및 socket도 연결하여 실시간으로 이벤트가 발생할때마다 접속해있는 인원도 확인할 수 있게 만들면 마무리 될 것 같음!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 대용량 트래픽 장애 대응방법 23.09.01]]></title>
            <link>https://velog.io/@sh_0317/TIL-%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EB%B0%A9%EB%B2%95-23.09.01</link>
            <guid>https://velog.io/@sh_0317/TIL-%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD-%EC%9E%A5%EC%95%A0-%EB%8C%80%EC%9D%91%EB%B0%A9%EB%B2%95-23.09.01</guid>
            <pubDate>Sun, 03 Sep 2023 17:40:03 GMT</pubDate>
            <description><![CDATA[<ul>
<li>로드 밸런서를 이용하여 여러대의 서버가 분산 처리하도록 함</li>
<li>클라우드 서비스 제공 업체의 오토 스케일링 기능(서버의 부하를 체크하여 서버를 생성하는 방식)을 사용</li>
<li>데이터 베이스 샤딩을 적용(DB 테이블을 수평 분할하여 물리적으로 서로 다른곳에 분산 저장)</li>
<li>데이터베이스 레플리카 적용</li>
<li>스케일 업</li>
<li>정적 컨텐츠에 대해 CDN 서비스를 이용하여 컨텐츠 다운 시간을 단축</li>
<li>API의 응답속도를 단축시키위해 코드를 리팩토링</li>
</ul>
<blockquote>
<p>참고블로그
<a href="https://limjunho.github.io/2021/06/22/traffic-handling.html">https://limjunho.github.io/2021/06/22/traffic-handling.html</a></p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Work-Flow 프로젝트 - Membership, payment (4/4) 23.08.31]]></title>
            <link>https://velog.io/@sh_0317/TIL-23.08.31</link>
            <guid>https://velog.io/@sh_0317/TIL-23.08.31</guid>
            <pubDate>Sun, 03 Sep 2023 17:38:25 GMT</pubDate>
            <description><![CDATA[<ul>
<li>무료 워크스페이스는 보드를 3개까지만 만들 수 있고 멤버 초대를 5명까지만 할 수 있으므로 기존 로직 수정</li>
</ul>
<pre><code class="language-typescript"> //보드 생성
  async CreateBoard(workspaceId: number, name: string, description: string): Promise&lt;Object&gt; {
    const workspace = await this.workspaceService.getWorkspaceDetail(workspaceId);
    console.log(workspace);
    const boardCount = await this.GetBoards(workspaceId);
    const hasMembership = workspace.memberships.length &gt; 0;

    if (boardCount.length &gt;= 3 &amp;&amp; !hasMembership)
      throw new HttpException(&#39;무료 워크스페이스는 보드를 3개까지만 생성할 수 있습니다.&#39;, HttpStatus.BAD_REQUEST);

    const board = await this.boardRepository.insert({ name, description, workspace });
    const findBoard = await this.boardRepository.findOneBy({ id: board.raw.insertId });
    await this.boardColumnRepository.insert({ name: &#39;Done&#39;, sequence: 1, board: findBoard });
    return board;
  }</code></pre>
<p>워크스페이스가 멤버십을 가지고 있는지 확인 후, 멤버십이 없고 보드의 개수가 3개 이상이라면 오류 출력</p>
<p>마찬가지로 워크스페이스 멤버초대 로직도 수정</p>
<pre><code class="language-typescript">// 워크스페이스 멤버초대
  async inviteWorkspaceMember(body: InvitationDto, workspaceId: number, userName: string): Promise&lt;IResult&gt; {
    const existWorkspace = await this.workspaceRepository.findOne({
      where: { id: workspaceId },
      relations: [&#39;memberships&#39;],
    });
    const hasMembership = existWorkspace.memberships.length &gt; 0;
    const entityManager = this.workspaceRepository.manager;

    if (!existWorkspace) throw new HttpException(&#39;해당 워크스페이스가 존재하지 않습니다.&#39;, HttpStatus.NOT_FOUND);

    const { id } = await this.userService.findUserByEmail(body.email);

    const existMember = await this.workspaceMemberRepository.findOne({
      where: { user: { id }, workspace: { id: workspaceId } },
    });

    const countMember = await this.workspaceMemberRepository.find({ where: { workspace: { id: workspaceId } } });

    if (countMember.length &gt;= 5 &amp;&amp; !hasMembership)
      throw new HttpException(&#39;무료 워크스페이스는 멤버를 5명까지만 초대 가능합니다.&#39;, HttpStatus.UNAUTHORIZED);

    if (existMember) throw new HttpException(&#39;이미 초대된 유저입니다.&#39;, HttpStatus.CONFLICT);
    try {
      await entityManager.transaction(async (transactionEntityManager: EntityManager) =&gt; {
        await this.mailService.inviteProjectMail(body.email, userName, existWorkspace.name, workspaceId);

        await transactionEntityManager.save(Workspace_Member, {
          workspace: { id: workspaceId },
          user: { id },
          role: body.role,
        });
      });
      return { result: true };
    } catch (err) {
      console.error(err);
    }
  }</code></pre>
<p>같은 방식으로 멤버십이 없고 멤버가 이미 5명이상 초대되어 있다면 오류를 출력</p>
<p><strong>출력 결과</strong>
<img src="https://velog.velcdn.com/images/sh_0317/post/263751b4-0971-448a-acbf-cf2420e0100f/image.png" alt="">
<img src="https://velog.velcdn.com/images/sh_0317/post/68f9a155-64c0-4d78-91c0-85f8b6845718/image.png" alt="">
정상작동 확인</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Work-Flow 프로젝트 - Membership, payment (3/4) 23.08.30]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-23.08.30</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-23.08.30</guid>
            <pubDate>Thu, 31 Aug 2023 00:19:05 GMT</pubDate>
            <description><![CDATA[<p><strong>이전에 발생했던 문제</strong>
<img src="https://velog.velcdn.com/images/sh_0317/post/c26e5ec6-5301-4458-9995-713a4a74c1fc/image.png" alt="">
취소된 결제가 존재할 때 새로운 멤버십을 결제하는 경우
<img src="https://velog.velcdn.com/images/sh_0317/post/c04d5e37-943b-407d-afbf-7f68d3eb8146/image.png" alt="">
아래 취소되었던 멤버십도 같이 활성화되는 문제를 발견</p>
<hr>
<p>payment 컬럼의 status를 활용하여 true인 경우에만 push를 할 수 있도록 변경</p>
<pre><code class="language-typescript">const paymentHistory = [];
    for (const payment of payments) {
      const workspaceId = payment.workspaceId;
      const status = payment.status;
      const workspace = await this.workspaceService.getWorkspaceDetail(workspaceId);

      if (workspace.memberships.length &gt; 0 &amp;&amp; status === true) {
        const membership = workspace.memberships[0];
        const paymentInfo = {
          paymentId: payment.id,
          paymentCreatedAt: payment.created_at,
          workspaceId,
          workspaceName: workspace.name,
          membershipCreatedAt: membership.created_at,
          membershipEndDate: membership.end_date,
          membershipPrice: membership.package_price,
        };
        paymentHistory.push(paymentInfo);
      } else {
        const paymentInfo = {
          paymentId: payment.id,
          paymentCreatedAt: payment.created_at,
          workspaceId,
          workspaceName: workspace.name,
        };
        paymentHistory.push(paymentInfo);
      }
    }
    return paymentHistory;</code></pre>
<p>true가 아닐 경우 취소된 결제 내역이므로 membership의 정보를 담지 않음, 프론트에서는 멤버십의 생성정보가 없을 경우 모두 취소된 결제로 볼 수 있도록 아래와 같이 코드를 작성</p>
<pre><code class="language-javascript">          const paymentDate = new Date(history.paymentCreatedAt);
          if (paymentDate &gt;= oneMonthAgo &amp;&amp; paymentDate &lt;= currentDate) {
            if (!history.membershipCreatedAt) {
              result += ` &lt;tbody&gt;
                            &lt;tr&gt;
                              &lt;td data-workspace-id=&quot;${history.workspaceId}&quot; id=&quot;workspace-name-table&quot;&gt;${history.workspaceName}&lt;/td&gt;
                              &lt;td&gt;취소된 결제입니다.&lt;/td&gt;
                              &lt;td&gt;-&lt;/td&gt;
                              &lt;td&gt;`;</code></pre>
<p><strong>출력 결과</strong>
<img src="https://velog.velcdn.com/images/sh_0317/post/79579eed-011d-4c16-b60d-0c3b729b15d4/image.png" alt=""></p>
<p>추가로 한달 이내의 결제 내역만 출력되도록 설정
52번 paymentId가 취소된 워크스페이스 결제이며 payment 생성날짜를 한달 이전으로 세팅하면
<img src="https://velog.velcdn.com/images/sh_0317/post/72064620-7438-4fbd-bf8c-e28eafd82bb5/image.png" alt="">
결제내역에 출력되지 않는 것을 확인
<img src="https://velog.velcdn.com/images/sh_0317/post/a68ac607-7f60-4920-bb5f-8fd7f1665585/image.png" alt=""></p>
<hr>
<p>추가로 아직 테스트하지 못한 내용이지만 멤버십을 이용 후 다시 결제하지 않는 경우가 있을수도 있으므로 node schedule을 사용하여 멤버십의 endDate를 확인하고 현재날짜 이전이라면 삭제하는 코드를 작성</p>
<pre><code class="language-typescript">  @Cron(&#39;0 0 * * *&#39;)
  async deleteExpiredMembership(): Promise&lt;IResult&gt; {
    return await this.membershipService.deleteExpiredMembership();
  }</code></pre>
<pre><code class="language-typescript">  // 만료된 멤버십 삭제
  async deleteExpiredMembership(): Promise&lt;IResult&gt; {
    const expiredMembership = await this.membershipRepository.find({
      where: {
        end_date: LessThan(new Date()),
      },
    });
    await this.membershipRepository.remove(expiredMembership);
    return { result: true };
  }</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Work-Flow 프로젝트 - Membership, payment (2/4) 23.08.28]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.08.28</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.08.28</guid>
            <pubDate>Tue, 29 Aug 2023 01:42:01 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-typescript"> // 결제 취소
  async cancelPurchase(workspaceId: number, paymentId: number, userId: number): Promise&lt;Object&gt; {
    const entityManager = this.paymentRepository.manager;

    const targetPayment = await this.paymentRepository.findOne({
      where: { id: paymentId, workspaceId, user: { id: userId } },
    });
    if (!targetPayment) throw new HttpException(&#39;해당 결제 내역을 찾을 수 없습니다.&#39;, HttpStatus.NOT_FOUND);

    const targetMembership = await this.membershipService.getMyMembership(workspaceId);

    const milliSecondPerDay = 24 * 60 * 60 * 1000;
    const refundRequestDate = new Date();

    // 남은기간 계산
    const remainingTime = targetMembership.end_date.getTime() - refundRequestDate.getTime();
    const remainingDays = Math.floor(remainingTime / milliSecondPerDay);

    // 멤버십 금액의 일할 계산
    const membershipPeriod = targetMembership.end_date.getTime() - targetMembership.created_at.getTime();
    const daysInMembership = Math.floor(membershipPeriod / milliSecondPerDay);

    const dailyPrice = Math.floor(targetMembership.package_price / daysInMembership);
    const refundPrice = Math.floor(remainingDays) * dailyPrice;
    const roundedRefundPrice = Math.floor(refundPrice / 100) * 100;

    await entityManager.transaction(async (transactionEntityManager: EntityManager) =&gt; {
      await this.membershipService.cancelMembership(workspaceId);

      targetPayment.status = false;
      await transactionEntityManager.save(targetPayment);

      const user = await this.userService.findUserById(userId);
      const refundPoint = roundedRefundPrice;
      const remainPoint = (user.points += refundPoint);
      await transactionEntityManager.save(User, { ...user, points: remainPoint });
    });
    return { remainingDays, roundedRefundPrice };
  }
</code></pre>
<p>생성과 마찬가지로 트랜잭션을 생성, 취소하려는 결제와 멤버십을 조회하고 해당 멤버십의 남은 기간을 계산해줌
남은 기간에 일할계산 된 금액을 곱하고 환불금액을 유저포인트에 다시 넣어준 뒤 커밋</p>
<ul>
<li>멤버십 가입 후 취소 테스트<pre><code class="language-typescript">console.log(remainingDays); // 179
console.log(dailyPrice); // 173
console.log(refundPrice); // 30967
console.log(roundedRefundPrice); // 30900</code></pre>
금액이 정확히 일치하진 않지만 정상적으로 환불되는 것을 확인</li>
</ul>
<pre><code class="language-typescript">  // 멤버십 취소
  async cancelMembership(workspaceId: number): Promise&lt;IResult&gt; {
    const targetMembership = await this.membershipRepository.findOne({ where: { workspace: { id: workspaceId } } });

    if (!targetMembership) throw new HttpException(&#39;결제된 멤버십이 없습니다.&#39;, HttpStatus.NOT_FOUND);

    await this.membershipRepository.remove(targetMembership);

    return { result: true };
  }</code></pre>
<p>결제내역 조회를 위해 payment는 status만 false로 바꿔주고 삭제하지 않음 </p>
<pre><code class="language-typescript">  // 나의 결제내역 조회
  async getMyPayments(userId: number): Promise&lt;Payment[]&gt; {
    const payments = await this.paymentRepository.find({ where: { user: { id: userId } }, relations: [&#39;user&#39;] });
    const paymentHistory = [];
    for (const payment of payments) {
      const workspaceId = payment.workspaceId;
      const workspace = await this.workspaceService.getWorkspaceDetail(workspaceId);

      if (workspace.memberships.length &gt; 0) {
        const membership = workspace.memberships[0];
        const paymentInfo = {
          paymentId: payment.id,
          paymentCreatedAt: payment.created_at,
          workspaceId,
          workspaceName: workspace.name,
          membershipCreatedAt: membership.created_at,
          membershipEndDate: membership.end_date,
          membershipPrice: membership.package_price,
        };
        paymentHistory.push(paymentInfo);
      } else {
        const paymentInfo = {
          paymentId: payment.id,
          paymentCreatedAt: payment.created_at,
          workspaceId,
          workspaceName: workspace.name,
        };
        paymentHistory.push(paymentInfo);
      }
    }
    return paymentHistory;
  }</code></pre>
<p>paymentHistory 빈배열을 만들고 워크스페이스에 멤버십이 있다면 멤버십의 정보를 출력해주고 아니라면 취소된 결제라 판단</p>
<p>** 발생한 문제 **
<img src="https://velog.velcdn.com/images/sh_0317/post/c26e5ec6-5301-4458-9995-713a4a74c1fc/image.png" alt="">
취소된 결제가 존재할 때 새로운 멤버십을 결제하는 경우
<img src="https://velog.velcdn.com/images/sh_0317/post/c04d5e37-943b-407d-afbf-7f68d3eb8146/image.png" alt="">
아래 취소되었던 멤버십도 같이 활성화되는 문제를 발견</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Work-Flow 프로젝트 - Membership, payment (1/4) 23.08.25]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-23.08.25</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94-%EC%8A%88%ED%8C%85-23.08.25</guid>
            <pubDate>Mon, 28 Aug 2023 05:20:02 GMT</pubDate>
            <description><![CDATA[<ul>
<li>프리미엄 멤버십과 무료 두 가지로 서비스를 운용하기로 함</li>
<li>무료버전의 경우 보드는 3개까지만 생성가능, 멤버는 5명까지만 초대가능</li>
</ul>
<pre><code class="language-typescript">  // 멤버십 결제
  async purchaseMembership(body: MembershipDto, workspaceId: number, userId: number): Promise&lt;IResult&gt; {
    const entityManager = this.paymentRepository.manager;

    await entityManager.transaction(async (transactionEntityManager: EntityManager) =&gt; {
      const findUserById = await this.userService.findUserById(userId);

      if (findUserById.points &lt; body.packagePrice)
        throw new HttpException(&#39;포인트가 부족합니다&#39;, HttpStatus.BAD_REQUEST);
      findUserById.points -= body.packagePrice;
      await transactionEntityManager.save(findUserById);

      const newPayment = this.paymentRepository.create({
        workspaceId,
        user: { id: userId },
      });
      await transactionEntityManager.save(newPayment);
      await this.membershipService.createMembership(body, workspaceId);
    });

    return { result: true };
  }</code></pre>
<p>entityManager를 사용하여 트랜잭션을 걸어줌. 우선 현재 접속중인 유저의 포인트를 조회한 뒤 결제에 문제가 없다면 새로운 payment를 생성해주고 멤버십을 생성</p>
<pre><code class="language-typescript">  // 멤버십 생성
  async createMembership(body: MembershipDto, workspaceId: number): Promise&lt;IResult&gt; {
    const startDate = new Date();
    const servicePeriod = body.servicePeriod * 24 * 60 * 60 * 1000;
    const endDate = new Date(startDate.getTime() + servicePeriod);

    const existMembership = await this.membershipRepository.findOne({ where: { workspace: { id: workspaceId } } });
    if (existMembership) throw new HttpException(&#39;이미 멤버십 결제가 되어있습니다.&#39;, HttpStatus.CONFLICT);

    const newMembership = this.membershipRepository.create({
      package_type: body.packageType,
      package_price: body.packagePrice,
      end_date: endDate,
      workspace: { id: workspaceId },
    });

    await this.membershipRepository.save(newMembership);

    return { result: true };
  }</code></pre>
<p>이용기간은 body값으로 30일 또는 180일만 들어오게 되어있고 시작일에 해당 body값의 기간을 더해주어 끝나는 날짜를 계산 후 생성</p>
<p>멤버십은 워크스페이스 단위로 적용되기 때문에 결제의 경우 member role이 Admin일 경우에만 가능하게 구현</p>
<pre><code class="language-typescript"> // 결제
  @Post()
  @UseGuards(AuthGuard)
  @UseInterceptors(CheckAdminInterceptor)
  async purchaseMembership(
    @Body() body: MembershipDto,
    @Param(&#39;workspaceId&#39;) workspaceId: number,
    @GetUser() user: AccessPayload
  ): Promise&lt;IResult&gt; {
    return await this.paymentService.purchaseMembership(body, workspaceId, user.id);
  }</code></pre>
<p>membe role이 Admin인지 체크하는 interceptor를 만들어서 해당 api가 실행될 때 검사</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] WebRtc를 사용한 P2P 통신 (2/2) 23.08.24]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.8.24</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.8.24</guid>
            <pubDate>Fri, 25 Aug 2023 00:24:11 GMT</pubDate>
            <description><![CDATA[<p>송수신자가 room에 입장하면 welcome으로 이동하며, chat기능을 위해 data channel을 생성하고  offer를 생성함</p>
<pre><code class="language-javascript">socket.on(&#39;welcome&#39;, async (roomName) =&gt; {
  try {
    makeConnection(roomName);
    myDataChannel = myPeerConnection.createDataChannel(&#39;chat&#39;);
    myDataChannel.addEventListener(&#39;message&#39;, addMessage);

    const offer = await myPeerConnection.createOffer();
    myPeerConnection.setLocalDescription(offer);
    socket.emit(&#39;sendOffer&#39;, { offer, roomName });
  } catch (err) {
    console.log(err);
  }
});</code></pre>
<pre><code class="language-javascript">function handleIce(data, roomName) {
  socket.emit(&#39;sendIce&#39;, { data: data.candidate, roomName });
  console.log(&#39;send ice&#39;);
}

function handleAddStream(data) {
  try {
    console.log(&#39;스트림&#39;, myPeerConnection);
    const peerVideo = document.querySelector(&#39;#peer-video&#39;);
    const peerStream = data.streams[0];
    peerVideo.srcObject = peerStream;
    console.log(&#39;peer와 스트림 연결&#39;);
    console.log(&#39;peers&#39;, data);
    console.log(&#39;peers&#39;, peerVideo.srcObject);
    console.log(&#39;my&#39;, myStream);
  } catch (err) {
    console.error(err);
  }
}

function makeConnection(roomName) {
  myPeerConnection = new RTCPeerConnection({
    iceServers: [
      {
        urls: [
          &#39;stun:stun.l.google.com:19302&#39;,
          &#39;stun:stun1.l.google.com:19302&#39;,
          &#39;stun:stun2.l.google.com:19302&#39;,
          &#39;stun:stun3.l.google.com:19302&#39;,
          &#39;stun:stun4.l.google.com:19302&#39;,
        ],
      },
    ],
  });
  myPeerConnection.addEventListener(&#39;icecandidate&#39;, (event) =&gt; {
    console.log(&#39;아이스 이벤트 발생&#39;);
    if (event.candidate) {
      handleIce(event.candidate, roomName);
    }
  });
  myPeerConnection.addEventListener(&#39;track&#39;, handleAddStream);
  myStream.getTracks().forEach((track) =&gt; myPeerConnection.addTrack(track, myStream));
}</code></pre>
<p>스트림을 추가할 때 원래는 addStream을 사용했으나 더이상 사용하지 않는 방법으로 권장하지 않는다고하여 track으로 변경하여 사용</p>
<p>송신자가 offer를 전송하면 수신자는 해당 offer의 SDP를 바탕으로 Answer를 작성하고 송신자에게 전달</p>
<pre><code class="language-javascript">socket.on(&#39;receiveOffer&#39;, async (offer) =&gt; {
  const remoteOffer = new RTCSessionDescription({
    type: &#39;offer&#39;,
    sdp: offer.payload.sdp,
  });
  if (myPeerConnection.signalingState === &#39;stable&#39;) {
    try {
      await myPeerConnection.setRemoteDescription(remoteOffer);

      const answer = await myPeerConnection.createAnswer();
      await myPeerConnection.setLocalDescription(answer);
      socket.emit(&#39;sendAnswer&#39;, { answer, roomName: offer.roomName });
    } catch (error) {
      console.error(&#39;SDP 파싱 오류&#39;, error);
    }
  } else {
    console.log(&#39;stable 상태가 아님&#39;);
  }
});</code></pre>
<hr>
<p><img src="https://velog.velcdn.com/images/sh_0317/post/5fe6c104-03b2-4f37-84fd-7fb359a0f9b2/image.png" alt="">
수신자의 mediaStream이 된것으로 보이지만 화면 출력이 진행되지 않는 문제가 발생</p>
<p><img src="https://velog.velcdn.com/images/sh_0317/post/9df364a7-9dfa-4810-b228-d0e4a2d6def9/image.png" alt="">
동영상이 로드 되고있지 않는 문제가 있어 코드를 다시 한번 살펴보았는데</p>
<p><img src="https://velog.velcdn.com/images/sh_0317/post/06578c2a-53de-4333-b748-b3bc6acb9f87/image.png" alt="">
두 브라우저간 연결이 잘 되었다면 connectionState와 iceConnetionState가 &quot;connected&quot;로 변경되어야하는데 현재 상태를 보니 연결이 되지 않은 것으로 보임</p>
<pre><code class="language-javascript">socket.on(&#39;welcome&#39;, async (roomName) =&gt; {
  try {
    makeConnection(roomName);
    myDataChannel = myPeerConnection.createDataChannel(&#39;chat&#39;);
    myDataChannel.addEventListener(&#39;message&#39;, addMessage);

    const offer = await myPeerConnection.createOffer();
    myPeerConnection.setLocalDescription(offer);
    socket.emit(&#39;sendOffer&#39;, { offer, roomName });
  } catch (err) {
    console.log(err);
  }
});</code></pre>
<p>이쪽 코드를 console로 찍어보니 offer를 peer A 즉 송신자가 peer B 수신자에게 보내야 하는데 둘 다 offer를 보내고 있음
-&gt; 아직 명확하게 개념이 잡히지 않아 이 부분이 문제인가 싶기도 함</p>
<hr>
<p>프로젝트 마감기한이 얼마 남지 않아 멤버십과 결제 기능을 먼저 도입해야 하므로 추후 다시  해결해 볼 예정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] WebRtc를 사용한 P2P 통신 (1/2) 23.08.23]]></title>
            <link>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.08.23</link>
            <guid>https://velog.io/@sh_0317/TIL-%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85-23.08.23</guid>
            <pubDate>Thu, 24 Aug 2023 00:36:52 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Socket.io를 사용하여 webRtc 기반 음성/영상통화 기능을 구현 목적</li>
</ul>
<pre><code class="language-javascript">// 음성 통화 시작
function startVoiceCall(element) {
  const receiverId = element.getAttribute(&#39;id&#39;);
  const receiverName = element.getAttribute(&#39;name&#39;);
  console.log(&#39;Starting voice call...&#39;);
  voiceCall(`/call?callerName=${userName}&amp;receiverName=${receiverName}`, &#39;음성 통화&#39;, receiverId, receiverName);
}

async function voiceCall(url, callType, receiverId, receiverName) {
  const width = 800;
  const height = 900;
  const left = (window.screen.width - width) / 2;
  const top = (window.screen.height - height) / 2;
  window.open(url, callType, `width=${width},height=${height},left=${left},top=${top}`);
  socket.emit(&#39;invite&#39;, { callerName: userName, callerId, receiverId, receiverName });
}

function startVideoCall(element) {
  const receiverId = element.getAttribute(&#39;id&#39;);
  const receiverName = element.getAttribute(&#39;name&#39;);
  console.log(&#39;Starting video call...&#39;);
  videoCall(`/call?callerName=${userName}&amp;receiverName=${receiverName}`, &#39;영상 통화&#39;, receiverId, receiverName);
}

async function videoCall(url, callType, receiverId, receiverName) {
  const width = 800;
  const height = 900;
  const left = (window.screen.width - width) / 2;
  const top = (window.screen.height - height) / 2;
  window.open(url, callType, `width=${width},height=${height},left=${left},top=${top}`);
  socket.emit(&#39;invite&#39;, { callerName: userName, callerId, receiverId, receiverName });
}</code></pre>
<ul>
<li>송신자가 먼저 음성/영상 통화를 걸고 수신자가 수락을 하면 통화가 시작되는 구조로 코드를 구성하였음</li>
</ul>
<pre><code class="language-typescript">  @SubscribeMessage(&#39;invite&#39;)
  handleInvite(client: Socket, data: any): void {
    let user = [];
    for (let key in this.connectedClients) {
      if (this.connectedClients[key] === data.receiverId / 1) user.push(key);
    }
    user.forEach((sock) =&gt; {
      this.server.to(sock).emit(&#39;response&#39;, {
        callerId: client.id,
        callerName: data.callerName,
        receiverId: data.receiverId,
        receiverName: data.receiverName,
      });
    });
  }</code></pre>
<p>invite를 보낼때 receiverId의 socket.id값을 추출하여 response를 보냄</p>
<pre><code class="language-javascript">socket.on(&#39;response&#39;, (data) =&gt; {
  responseAlert(data.callerName, data.receiverName);
});

function responseAlert(callerName, receiverName) {
  const messageHtml = `${callerName}님이 대화에 초대했습니다. &lt;br /&gt;
  &lt;button type=&quot;button&quot; class=&quot;accept&quot; data-dismiss=&quot;alert&quot; aria-label=&quot;accpet&quot;&gt;수락&lt;/button&gt;
  &lt;button type=&quot;button&quot; class=&quot;refuse&quot; data-dismiss=&quot;alert&quot; aria-label=&quot;refuse&quot;&gt;거절&lt;/button&gt;
  &lt;span aria-hidden=&quot;true&quot;&gt;&lt;/span&gt;`;

  const alt = document.getElementById(&#39;customerAlert&#39;);
  if (alt) {
    alt.innerHTML = messageHtml;
  } else {
    const htmlTemp = `&lt;div class=&quot;alert alert-sparta alert-dismissible show fade&quot; role=&quot;alert&quot; id=&quot;customerAlert&quot;&gt;${messageHtml}&lt;/div&gt;`;
    document.body.insertAdjacentHTML(&#39;beforeend&#39;, htmlTemp);
  }

  const acceptBtn = document.querySelector(&#39;.accept&#39;);
  const refuseBtn = document.querySelector(&#39;.refuse&#39;);

  acceptBtn.addEventListener(&#39;click&#39;, () =&gt; {
    acceptCall(callerName, receiverName);
    acceptBtn.style.display = &#39;none&#39;;
    refuseBtn.style.display = &#39;none&#39;;
  });

  refuseBtn.addEventListener(&#39;click&#39;, () =&gt; {
    refuseBtn.style.display = &#39;none&#39;;
    acceptBtn.style.display = &#39;none&#39;;
  });

  setTimeout(() =&gt; {
    refuseBtn.style.display = &#39;none&#39;;
    acceptBtn.style.display = &#39;none&#39;;
  }, 60000);
}

// 응답 버튼을 누를 때 호출되는 함수
function acceptCall(callerName, receiverName) {
  const width = 800;
  const height = 900;
  const left = (window.screen.width - width) / 2;
  const top = (window.screen.height - height) / 2;

  // 새 창을 열어서 WebRTC 연결을 설정
  const callWindow = window.open(
    `/call?callerName=${callerName}&amp;receiverName=${receiverName}`,
    `width=${width},height=${height},left=${left},top=${top}`
  );
  callWindow.onload = () =&gt; {};
}</code></pre>
<pre><code class="language-javascript">const params = new URLSearchParams(window.location.search);
let callerName = params.get(&#39;callerName&#39;);
let receiverName = params.get(&#39;receiverName&#39;);

socket.on(&#39;connect&#39;, async () =&gt; {
  await initCall();
  socket.emit(&#39;joinRoom&#39;, { callerName, receiverName });
  console.log(&#39;Web-RTC 소켓 연결&#39;);
});</code></pre>
<ul>
<li>새 창을 열 때 파라미터로 송수신자의 이름을 넣어주었고 해당 값을 추출하여 수신자가 수락을 누르면 양쪽 다 room에 입장</li>
</ul>
<p><img src="https://velog.velcdn.com/images/sh_0317/post/70a399a6-a3ed-4a7b-aec0-3d6d4a2d0f38/image.png" alt=""></p>
<p>수락 창을 누르면 좌측과 같은 창이 오픈되며 setTimeout을 설정하여 60초이내 수락 또는 거절을 하지않으면 사라지게 구성</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] HTTP 메서드 정리 23.08.22]]></title>
            <link>https://velog.io/@sh_0317/TIL-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC-23.08.22</link>
            <guid>https://velog.io/@sh_0317/TIL-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A0%95%EB%A6%AC-23.08.22</guid>
            <pubDate>Wed, 23 Aug 2023 00:18:40 GMT</pubDate>
            <description><![CDATA[<h3 id="http-method-종류">HTTP Method 종류</h3>
<p>HTTP 메서드란 클라이언트와 서버 사이에 이루어지는 요청과 응답데이터를 전송하는 방식을 말한다.</p>
<p><strong>주요 메서드</strong>
<strong>- GET : 리소스 조회</strong>
서버에 전달하고 싶은 데이터는 query를 통해서 전달함.  메시지 바디를 사용하여 데이터를 전달할 수는 있지만, 지원하지 않는 곳이 많아 권장하지 않음.
POST도 조회가 가능하지만, GET 메서드는 캐싱이 가능하기에 유리
<strong>- POST : 요청 데이터 처리, 주로 등록에 사용</strong>
메시지 바디를 통해 서버로 요청 데이터를 전달하면 서버는 요청 데이터를 처리하여 업데이트. 만일 데이터를 GET하는데 있어, JSON으로 조회데이터를 넘겨야 하는 애매한 경우 POST를 사용
<strong>- PUT : 리소스를 대체(덮어쓰기), 해당 리소스가 없다면 생성</strong>
<strong>- PATCH : 리소스 부분 변경 (PUT = 전체, PATCH = 일부)</strong>
PATCH를 지원하지 않는 서버에서는 POST로 대체 가능
<strong>- DELETE : 리소스 삭제</strong>
상태코드는 대부분 200을 사용하고 상황에 따라 204를 사용</p>
<p><strong>기타 메서드</strong>
<strong>- HEAD : GET과 동일하지만 메세지 부분(body)를 제외하고 상태 줄과 헤더만 반환</strong>
응답의 상태 코드만 확인할때와 같이 Resource를 받지 않고 오직 찾기만 원할때 사용 (일종의 검사 용도)
서버의 응답 헤더를 봄으로써 Resource가 수정 되었는지 확인 가능
<strong>- OPTIONS : 대상 리소스에 대한 통신 가능 옵션(메서드)을 설명 (주로 CORS에서 사용)</strong>
서버의 지원 가능한 HTTP 메서드와 출처를 응답 받아 CORS 정책Visit Website을 검사하기 위한 요청
<strong>- CONNECT : 대상 자원으로 식별되는 서버에 대한 터널을 설정</strong>
<strong>- TRACE : 대상 리소스에 대한 경로를 따라 메시지 루프백 테스트를 수행</strong>
서버에 도달 했을 때의 최종 패킷의 요청 패킷 내용을 응답 받을 수 있다.
요청의 최종 수신자는 반드시 송신자에게 200(OK) 응답의 내용(Body)로 수신한 메세지를 반송해야 한다.
최초 Client의 요청에는 Body가 포함될수 없다.</p>
<hr>
<h3 id="http-메서드의-속성">HTTP 메서드의 속성</h3>
<ul>
<li>안전</li>
<li><blockquote>
<p> 계속해서 메서드를 호출해도 리소스를 변경하지 않는다는 의미. (GET)</p>
</blockquote>
</li>
<li>멱등</li>
<li><blockquote>
<p>메서드를 계속 호출해도 결과가 똑같다는 의미, GET·PUT·DELETE는 멱등하다고 볼 수 있지만 POST나 PATCH는 볼 수 없음</p>
</blockquote>
</li>
<li>캐시가능</li>
<li><blockquote>
<p>데이터를 효율적으로 가져올 수 있다는 의미. GET, HEAD, POST, PATCH가 캐시가 가능하지만 실제로는 GET과 HEAD만 주로 캐싱이 쓰임</p>
</blockquote>
</li>
</ul>
<p><strong>참고</strong></p>
<blockquote>
<ul>
<li><a href="https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A2%85%EB%A5%98-%ED%86%B5%EC%8B%A0-%EA%B3%BC%EC%A0%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC">https://inpa.tistory.com/entry/WEB-%F0%9F%8C%90-HTTP-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%A2%85%EB%A5%98-%ED%86%B5%EC%8B%A0-%EA%B3%BC%EC%A0%95-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC</a></li>
</ul>
</blockquote>
<ul>
<li><a href="https://velog.io/@haron/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%84%EB%8A%94%EB%8C%80%EB%A1%9C-%EB%A7%90%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94">https://velog.io/@haron/HTTP-%EB%A9%94%EC%84%9C%EB%93%9C%EC%97%90-%EB%8C%80%ED%95%B4-%EC%95%84%EB%8A%94%EB%8C%80%EB%A1%9C-%EB%A7%90%ED%95%B4%EC%A3%BC%EC%84%B8%EC%9A%94</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Not found alias 오류 23.08.21]]></title>
            <link>https://velog.io/@sh_0317/TIL-Not-found-alias-%EC%98%A4%EB%A5%98-23.08.21</link>
            <guid>https://velog.io/@sh_0317/TIL-Not-found-alias-%EC%98%A4%EB%A5%98-23.08.21</guid>
            <pubDate>Mon, 21 Aug 2023 13:14:35 GMT</pubDate>
            <description><![CDATA[<p>작업을 진행하던 중 Not found alias 오류가 발생</p>
<pre><code class="language-typescript">Error: Cannot find alias for relation at workspace
    at UpdateQueryBuilder.findColumnsForPropertyPath (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\query-builder\QueryBuilder.ts:1305:27)
    at UpdateQueryBuilder.getPredicates (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\query-builder\QueryBuilder.ts:1454:26)
    at getPredicates.next (&lt;anonymous&gt;)
    at UpdateQueryBuilder.getWhereCondition (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\query-builder\QueryBuilder.ts:1621:50)
    at UpdateQueryBuilder.where (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\query-builder\UpdateQueryBuilder.ts:224:32)
    at EntityManager.update (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\entity-manager\EntityManager.ts:790:18)
    at Repository.update (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\repository\Repository.ts:361:29)
    at MembershipsService.extensionMembership (C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\memberships\memberships.service.ts:63:37)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at C:\Users\dltkd\OneDrive\바탕 화면\개발 공부\workflow-service\src\payments\payments.service.ts:64:9</code></pre>
<hr>
<h3 id="해결방법">해결방법</h3>
<ul>
<li><p>OneToOne으로 관계 설정 시 FK를 관리하지 않는 테이블에서는 참조하는 테이블의 id값 컬럼이 생성되지 않는 것을 확인 (이유는 튜터님에게 확인해봐야 할 듯)</p>
<pre><code class="language-typescript">// 멤버십 연장
async extensionMembership(body: MembershipDto, workspaceId: number): Promise&lt;IResult&gt; {
  const targetMembership = await this.membershipRepository.findOne({ where: { workspace: { id: workspaceId } } });
  const servicePeriod = body.servicePeriod * 24 * 60 * 60 * 1000;
  const newEndDate = new Date(targetMembership.end_date.getTime() + servicePeriod);

  if (!targetMembership) throw new HttpException(&#39;결제된 멤버십이 없습니다.&#39;, HttpStatus.NOT_FOUND);

  await this.membershipRepository.update({ workspace: { id: workspaceId } }, { end_date: newEndDate });

  return { result: true };
}</code></pre>
</li>
<li><p>따라서 기존처럼 update를 사용할 경우 해당 오류가 발생하는 것으로 판단</p>
</li>
<li><p>update를 save로 바꾸어서 사용</p>
<pre><code class="language-typescript">// 멤버십 연장
async extensionMembership(body: MembershipDto, workspaceId: number): Promise&lt;IResult&gt; {
  const targetMembership = await this.membershipRepository.findOne({ where: { workspace: { id: workspaceId } } });
  const servicePeriod = body.servicePeriod * 24 * 60 * 60 * 1000;
  const newEndDate = new Date(targetMembership.end_date.getTime() + servicePeriod);

  if (!targetMembership) throw new HttpException(&#39;결제된 멤버십이 없습니다.&#39;, HttpStatus.NOT_FOUND);

  await this.membershipRepository.save({ ...targetMembership, end_date: newEndDate });
  return { result: true };
}</code></pre>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Primary key duplicate 오류 23.08.18]]></title>
            <link>https://velog.io/@sh_0317/TIL-Primary-key-duplicate-%EC%98%A4%EB%A5%98-23.08.18</link>
            <guid>https://velog.io/@sh_0317/TIL-Primary-key-duplicate-%EC%98%A4%EB%A5%98-23.08.18</guid>
            <pubDate>Fri, 18 Aug 2023 20:45:11 GMT</pubDate>
            <description><![CDATA[<p>결제기능과 워크스페이스 멤버십기능을 구현하던 중 멤버십 결제를 진행하면 워크스페이스의 PK 중복 오류가 발생하였음</p>
<pre><code class="language-typescript">QueryFailedError: ER_DUP_ENTRY: Duplicate entry &#39;5&#39; for key &#39;workspaces.PRIMARY&#39;
  query: &#39;INSERT INTO `workspaces`(`id`, `name`, `type`, `description`, `created_at`, `updated_at`, `user_id`, `membership_id`) VALUES (?, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT)&#39;,
  parameters: [ &#39;5&#39; ],
  driverError: Error: ER_DUP_ENTRY: Duplicate entry &#39;5&#39; for key &#39;workspaces.PRIMARY&#39;
      {
    code: &#39;ER_DUP_ENTRY&#39;,
    errno: 1062,
    sqlMessage: &quot;Duplicate entry &#39;5&#39; for key &#39;workspaces.PRIMARY&#39;&quot;,
    sqlState: &#39;23000&#39;,
    index: 0,
    sql: &quot;INSERT INTO `workspaces`(`id`, `name`, `type`, `description`, `created_at`, `updated_at`, `user_id`, `membership_id`) VALUES (&#39;5&#39;, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT)&quot;
  },
  code: &#39;ER_DUP_ENTRY&#39;,
  errno: 1062,
  sqlMessage: &quot;Duplicate entry &#39;5&#39; for key &#39;workspaces.PRIMARY&#39;&quot;,
  sqlState: &#39;23000&#39;,
  index: 0,
  sql: &quot;INSERT INTO `workspaces`(`id`, `name`, `type`, `description`, `created_at`, `updated_at`, `user_id`, `membership_id`) VALUES (&#39;5&#39;, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT)&quot;
}</code></pre>
<p>초기에는 membership과 workspace의 관계를 1:1로 설정해주었는데 콘솔을 찍어가며 확인해보니 workspace의 membership_id를 업데이트를 해주려는 과정에서 충돌이 일어났던 것으로 추정되었음</p>
<pre><code class="language-typescript">const membership = await this.membershipService.createMembership(body, workspaceId);

const findWorkspace = await this.workspaceService.getWorkspaceDetail(workspaceId);

findWorkspace.membership = membership.id
await transactionEntityManager.save(findWorkspace);</code></pre>
<p>원래는 워크스페이스 생성 시 membership_id가 null값이어서 membership이 생성이 되면 업데이트를 해주는 구조를 생각했었는데, 해당 코드를 주석 처리 후 생성해보니 따로 업데이트를 해주지 않아도 membership_id가 업데이트 되는 것을 발견함</p>
<p>해당 코드를 살리고 실행해보면 똑같은 오류가 발생하는 것으로 보아, 이미 업데이트가 되어있는데 중복된 로직이 실행되어서 발생하는 에러인가? 싶었는데 관계 설정해두었던 CASCADE도 문제가 있는 것을 발견</p>
<pre><code class="language-typescript">@OneToOne(() =&gt; Membership, (membership) =&gt; membership.workspace, {
    onDelete: &#39;CASCADE&#39;,
    nullable: true,
  })
  @JoinColumn()
  membership: Membership;

@OneToOne(() =&gt; Workspace, (workspace) =&gt; workspace.membership, {
    // cascade: true,
    nullable: true,
  })
  workspace: Workspace;</code></pre>
<p>아래의 cascade : true를 활성화 하면 같은 중복오류가 발생을 하였고, 주석 처리 후 실행했을때 정상적으로 생성이 되는 것 까지 확인.
결제 취소를 할 경우 membership_id로 해당 membership을 찾아 삭제하는 로직을 구성했는데 취소를 하게되면 워크스페이스와 워크스페이스 멤버까지 같이 삭제되는 현상을 발견하였음</p>
<p>이것저것 시도해보다 둘의 관계를 1:N으로 변경해도 문제가 없을 것 같아 변경한 뒤 실행해보니 이번엔 멤버십만 정상적으로 삭제되는 것을 확인</p>
<pre><code class="language-typescript">@OneToMany(() =&gt; Membership, (membership) =&gt; membership.workspace, {
    onDelete: &#39;CASCADE&#39;,
    nullable: true,
  })
  memberships: Membership[];</code></pre>
<hr>
<p>일단 얼렁뚱땅 해결하긴 했지만 정확한 이유를 모르겠어서 다음주에 튜터님에게 질문 후 원인 파악이 필요</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] MVC 패턴 23.08.17]]></title>
            <link>https://velog.io/@sh_0317/TIL-MVC-%ED%8C%A8%ED%84%B4-23.08.17</link>
            <guid>https://velog.io/@sh_0317/TIL-MVC-%ED%8C%A8%ED%84%B4-23.08.17</guid>
            <pubDate>Thu, 17 Aug 2023 14:34:48 GMT</pubDate>
            <description><![CDATA[<h3 id="✔️오늘-한일">✔️오늘 한일!</h3>
<ul>
<li><input checked="" disabled="" type="checkbox"> 워크스페이스 CRUD</li>
<li><input checked="" disabled="" type="checkbox"> 워크스페이스 멤버 CRUD 및 Role 설정</li>
<li><input checked="" disabled="" type="checkbox"> 권한 확인을 위한 Interceptors 구현</li>
</ul>
<hr>
<h3 id="mvc-패턴이란">MVC 패턴이란?</h3>
<p>MVC (모델-뷰-컨트롤러) 는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴 중 하나이다. </p>
<p>디자인 패턴이란 프로그램이나 어떤 특정한 것을 개발하는 중에 발생했던 문제점들을 정리해서 상황에 따라 간편하게 적용해서 쓸 수 있는 것을 정리하여 특정한 &quot;규약&quot;을 통해 쉽게 쓸 수 있는 형태로 만든 것을 말한다.
ex)라이브러리, 프레임워크</p>
<h3 id="mvc-패턴을-사용해야-하는-이유">MVC 패턴을 사용해야 하는 이유</h3>
<ul>
<li>비즈니스 로직과 UI로직을 분리하여 유지보수를 독립적으로 수행가능</li>
<li>Model과 View가 다른 컴포넌트들에 종속되지 않아 애플리케이션의 확장성, 유연성에 유리함</li>
<li>중복 코딩의 문제점 제거</li>
</ul>
<h3 id="mvc-패턴의-한계">MVC 패턴의 한계</h3>
<p>MVC패턴에서 View는 Controller에 연결되어 화면을 구성하는 단위 요소이므로 다수의 View를 가질 수 있다. 그리고 Model은 Controller를 통해서 View와 연결되지만, Controller에 의해서 하나의 View에 연결될 수 있는 Model도 여러 개가 될 수 있어 View와 Model이 서로 의존성을 띄게 된다. 즉, Controller에 다수의 Model과 View가 복잡하게 연결되어 있는 상황이 발생할 수 도 있다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] KPT회고 및 NoSQL과 RDBMS의 차이 정리 23.08.16]]></title>
            <link>https://velog.io/@sh_0317/TIL-KPT%ED%9A%8C%EA%B3%A0-%EB%B0%8F-NoSQL%EA%B3%BC-RDBMS%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC-23.08.16</link>
            <guid>https://velog.io/@sh_0317/TIL-KPT%ED%9A%8C%EA%B3%A0-%EB%B0%8F-NoSQL%EA%B3%BC-RDBMS%EC%9D%98-%EC%B0%A8%EC%9D%B4-%EC%A0%95%EB%A6%AC-23.08.16</guid>
            <pubDate>Wed, 16 Aug 2023 12:37:58 GMT</pubDate>
            <description><![CDATA[<h3 id="★-trello-project에-대한-node-b반-4조idle에-대한-회고">★ Trello Project에 대한 Node B반 4조(IDLE)에 대한 회고</h3>
<p><strong>1. 이상훈</strong></p>
<p><strong>Keep **
팀 별 규칙을 준수하고 매일 14시에 서로의 코드 진행 상황을 알 수 있도록 스크럼 회의를 진행한 점
모르는 내용이 있으면 공유하고 서로의 지식을 공유하여 부족한 부분을 채워준 점
**Problem</strong> 
전반적은 프로젝트 진행은 문제 없이 진행됐으나 개인적으로 프론트 연결 및 프론트 관련 코드 작성이 미흡했던 점
위와 같은 문제로 시간이 부족하여 파일다운로드를 구현해보지 못한 아쉬움
이전 보다 nest.js를 좀 더 활용했으나 아직 잘 활용하지 못하고 있다고 판단
<strong>Try</strong> 
공식 문서를 참고해보고 검색을 통해 활용 예제를 많이 봐둘 것
이미지 업로드는 많이 활용해봤으니 파일 업로드 및 다운로드를 구현해볼 것!</p>
<p><strong>2. 인한별</strong></p>
<p><strong>Keep</strong> : 협업시 코드리뷰를 함으로써 로직에 대한 이해도가 높아짐을 느낌
<strong>Problem</strong> : X
<strong>Try</strong> : 깃 활용 능력이 다소 떨어지는 듯 하여 추후 깃 강의 재수강 후 트레이닝을 통해 능숙해질 수 있도록 공부 예정</p>
<p><strong>3. 이재혁</strong></p>
<p><strong>Keep</strong> : 네스트를 좀 더 알게 되었고, 협업과정에서 소통이 원할하게 이루어지면 더 효율이 올라간다는걸 느낌
<strong>Problem *<em>: 아직 DI개념이나, this, class 개념이 좀 부족해서 api 만들때 많이 어려웠음
*</em>Try</strong> : 남는시간에 틈틈히 js문법 + 타입 + 네스트 공부를 할것</p>
<p><strong>4. 박성민</strong></p>
<p><strong>Keep *<em>: 적극적인 의사소통과 상황 공유로 개발 단계의 진척이 빨랐음, nest 프레임워크와 템플릿을 이용해 작성을 하다보니 구현해야 할 기능에 좀 더 집중 할 수 있게 되어서 좋았음, 구현하고 싶었던 부분을 빠짐없이 구현해서 좋았음
*</em>Problem</strong> : 소켓에 대한 이해도가 높지 않아 실시간 대화는 금방 구축했지만 해당 대화방에 없어도 대화 내용을 저장하는 기능을 구현하는데 시간이 좀 소요됨(로직을 잘못 작성해 여러번 고쳤음)
<strong>Try</strong> : 기본적인 필수 기능만 돌아가는 코드가 아닌 디테일을 살려 실제 서비스에서 사용되는 코드처럼 여러가지의 경우를 생각해 로직을 꼼꼼히 짜는 방향으로 구현을 해볼 예정</p>
<h2 id="rdbms란">RDBMS란?</h2>
<ul>
<li>DBMS : DataBase Management System의 약자로써, 사용자와 데이터베이스 사이에서 사용자의 요청을 해석하여 데이터베이스에 저장된 정보를 관리할 수 있도록 해주는 소프트웨어이다.</li>
</ul>
<p>RDBMS는 위의 DBMS에 Relation이 붙은, 즉 관계형 데이터베이스 관리 시스템을 뜻한다.
엑셀의 형식과 유사한 2차원 테이블 형식으로 구성되며 속성<code>Attribute</code>과 값<code>Value</code>를 이용하여 데이터를 정의하고 저장·관리한다.</p>
<p>각각의 속성과 값을 가진 테이블들은 서로 관계를 맺으며 존재하고, 이러한 데이터들을 활용하기 위해서 SQL을 활용한다.</p>
<p><strong>장점</strong> </p>
<ul>
<li>Data를 Column과 Row 형태로 저장</li>
<li>데이터의 분류, 정렬, 탐색 속도가 비교적 빠름</li>
<li>SQL이라는 구조화 된 질의를 통해 데이터를 다룰 수 있음</li>
<li>작업의 완전성을 보장</li>
<li>데이터의 Update가 빠름 </li>
</ul>
<p><strong>단점</strong> </p>
<ul>
<li>스키마의 규격에 맞춰서 데이터를 다뤄야 함</li>
<li>데이터 처리에 대한 부하 발생 시 처리가 어려움</li>
</ul>
<h2 id="nosql이란">NoSQL이란?</h2>
<p>RDBMS 방식으로는 더이상 처리할 수 없을만큼의 복잡하고 큰 데이터들의 등장으로, NoSQL 방식이 부각 되기 시작했다.</p>
<p>데이터/테이블간의 관계를 정의하지 않아 정해진 스키마가 없어 보다 자유롭게 데이터를 저장할 수 있다. Key값만 가지고 데이터에 대한 입·출력을 수행할 수 있다.</p>
<p><strong>장점</strong> </p>
<ul>
<li>데이터간의 관계를 정의하지 않음</li>
<li>RDBMS보다 복잡도가 떨어져 훨씬 대용량의 데이터를 저장·관리할 수 있음</li>
<li>테이블에 스키마가 정해져있지 않아 데이터 저장이 자유로움</li>
<li>많은 양의 데이터를 저장·처리할 수 있음</li>
</ul>
<p><strong>단점</strong></p>
<ul>
<li>Key값에 대한 입·출력만 지원</li>
<li>스키마가 정해져 있지 않아 데이터에 대한 규격화가 되어있지 않음</li>
<li>데이터의 Update가 느림</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] JQueryUI를 사용한 드래그앤드랍 구현 23.08.14]]></title>
            <link>https://velog.io/@sh_0317/TIL-JQueryUI%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%93%9C%EB%9E%98%EA%B7%B8%EC%95%A4%EB%93%9C%EB%9E%8D-%EA%B5%AC%ED%98%84-23.08.14</link>
            <guid>https://velog.io/@sh_0317/TIL-JQueryUI%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%93%9C%EB%9E%98%EA%B7%B8%EC%95%A4%EB%93%9C%EB%9E%8D-%EA%B5%AC%ED%98%84-23.08.14</guid>
            <pubDate>Mon, 14 Aug 2023 11:37:10 GMT</pubDate>
            <description><![CDATA[<p>보드 순서 변경 및 컬럼 순서 변경 로직</p>
<pre><code class="language-javascript">// 보드(카드) 동일 컬럼 내 이동
  async orderBoard(body: orderBoardDto, projectId: number, boardId: number): Promise&lt;IResult&gt; {
    const { newBoardSequence } = body;
    const entityManager = this.boardRepository.manager;

    const findBoard = await this.boardRepository.findOne({ where: { id: boardId, project: { id: projectId } } });

    if (!findBoard) throw new HttpException(&#39;해당 보드를 찾을 수 없습니다&#39;, HttpStatus.NOT_FOUND);

    await entityManager.transaction(async (transactionEntityManager: EntityManager) =&gt; {
      const targetBoard = await this.boardRepository.findOne({ where: { boardSequence: newBoardSequence } });

      if (targetBoard) {
        const changeSequence = findBoard.boardSequence;
        findBoard.boardSequence = targetBoard.boardSequence;
        targetBoard.boardSequence = changeSequence;
        await transactionEntityManager.save(Board, [findBoard, targetBoard]);
      }

      findBoard.boardSequence = newBoardSequence;
      await transactionEntityManager.save(Board, findBoard);
    });

    return { result: true };
  }

  // 보드(카드) 다른 컬럼으로 이동
  async moveBoard(projectId: number, boardId: number, columnId: number): Promise&lt;IResult&gt; {
    const targetBoard = await this.boardRepository.findOne({ where: { id: boardId, project: { id: projectId } } });
    const targetColumn = await this.boardColumnRepository.findOne({ where: { id: columnId }, relations: [&#39;boards&#39;] });
    const entityManager = this.boardRepository.manager;

    if (!targetBoard || !targetColumn) throw new HttpException(&#39;해당 보드 또는 컬럼을 찾을 수 없습니다.&#39;, HttpStatus.NOT_FOUND);

    await entityManager.transaction(async (transactionEntityManager: EntityManager) =&gt; {
      const maxSequence = targetColumn.boards.reduce((max, b) =&gt; Math.max(max, b.boardSequence), 0);

      targetBoard.boardColumn = targetColumn;
      targetBoard.boardSequence = maxSequence + 1;
      await transactionEntityManager.save(Board, targetBoard);
    });

    return { result: true };
  }</code></pre>
<p>같은 컬럼 내의 보드 이동을 진행할 때는 클라이언트가 선택한 보드를 findBoard에 담아 존재하는지 검색하고 변경하고자 하는 보드를 targetBoard로 잡아 해당 보드의 sequence번호와 교환해주는 방식을 채택</p>
<p>다른 컬럼으로 이동하는 경우 이동하고자 하는 보드를 targetBoard에 담고 <code>map()</code> 과 <code>reduce()</code>를 통해 해당 보드의 maxSequence값을 구해준 뒤 + 1하여 맨 뒤에 들어갈 수 있도록 로직을 구성하였음</p>
<hr>
<p>썬더클라이언트로 정상작동 확인 후 프론트 연결 작업 시작
<code>JQueryUI</code>라이브러리 중 <code>.sortable</code>을 사용하여 drag &amp; drop을 구현하려고 함</p>
<p>오류가 생겼던 부분의 정확한 코드가 기억이 나진 않지만 구글링을 통해 드래그앤드랍을 어떻게 하는지 찾아보던 중 <code>prev()</code>와 <code>next()</code>라는게 있어서 적용 후 테스트</p>
<p>-&gt; DB에 있는 시퀀스값이 1, 2, 3, 4, 5 라한다면 1번을 선택후 5번으로 이동했을때 <code>prev()</code>는 5번에 있는 시퀀스값을 읽어오지 못하고 undefined 에러가 발생
-&gt; 반대로 <code>next()</code>의 경우 5번에서 1번으로 이동 시 undefined 발생</p>
<p>정확한 원인은 모르겠어서 콘솔을 찍어보면서 확인한 결과 첫 번째 카드 이동할 때는 숫자가 정상적으로 바뀌다가 두번 째 이동할 때 시퀀스번호가 +1되는 현상이 있어 번호가 불규칙적으로 바뀌는 것을 확인</p>
<pre><code class="language-javascript">    // 컬럼 아래에 보드 카드 추가
        const columnElement = document.querySelector(`[data-column-id=&quot;${columnId}&quot;]`);

        if (columnElement) {
          columnElement.innerHTML = columnHtml;

          $(columnElement).sortable({
            handle: &#39;.card-title&#39;,
            connectWith: `.card-body[data-column-id]`,
            opacity: 0.5,
            update: function (event, ui) {
              const targetBoard = ui.item;
              const boardId = targetBoard.find(&#39;.card-title&#39;).attr(&#39;data-board-id&#39;);
              const columnId = targetBoard.closest(&#39;.card-body&#39;).attr(&#39;data-column-id&#39;);
              // 엘리먼트 순서가 0부터시작하므로 +1하여 DB와 동기화
              const newBoardSequence = targetBoard.index() + 1;

              orderBoardSequence(boardId, newBoardSequence);
              moveBoard(boardId, columnId);
            },
          });
        }</code></pre>
<p>columnId를 받아오는 방식을 targetBoard의 부모요소의 columnId값을 가져오는것으로 변경하고 시퀀스 번호는 인덱스번호와 일치시키는 것으로 문제를 해결하였음</p>
<p>DB에서 보드나 컬럼이 생성될 때 번호를 1부터 생성하도록 로직을 구성하였기에 인덱스에 +1을 하여 순서를 일치시킨 후 매개변수로 전달</p>
<pre><code class="language-javascript">// 보드 동일 컬럼 내 이동
async function orderBoardSequence(boardId, newBoardSequence) {
  await $.ajax({
    method: &#39;PATCH&#39;,
    url: `/projects/${projectId}/boards/${boardId}/order`,
    headers: {
      Accept: &#39;application/json&#39;,
    },
    beforeSend: function (xhr) {
      xhr.setRequestHeader(&#39;Content-type&#39;, &#39;application/json&#39;);
      xhr.setRequestHeader(&#39;authorization&#39;, accessToken);
    },
    data: JSON.stringify({ newBoardSequence }),
    success: () =&gt; {},
    error: (error) =&gt; {
      console.error(error);
    },
  });
}

// 보드 다른 컬럼으로 이동
async function moveBoard(boardId, columnId) {
  await $.ajax({
    method: &#39;PATCH&#39;,
    url: `/projects/${projectId}/boards/${boardId}/${columnId}/move`,
    headers: {
      Accept: &#39;application/json&#39;,
    },
    beforeSend: function (xhr) {
      xhr.setRequestHeader(&#39;Content-type&#39;, &#39;application/json&#39;);
      xhr.setRequestHeader(&#39;authorization&#39;, accessToken);
    },
    success: () =&gt; {},
    error: (error) =&gt; {
      console.error(error);
    },
  });
}</code></pre>
<p>전달 된 매개변수는 아래의 함수에서 각자에 맞는 API를 호출</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] Nest.js Multer 23.08.09]]></title>
            <link>https://velog.io/@sh_0317/TIL-Nest.js-Multer-23.08.09</link>
            <guid>https://velog.io/@sh_0317/TIL-Nest.js-Multer-23.08.09</guid>
            <pubDate>Wed, 09 Aug 2023 13:16:08 GMT</pubDate>
            <description><![CDATA[<p>직전 타입스크립트 개인과제를 진행하면서 만들어 둔 업로드 미들웨어를 이번 팀프로젝트때 적용하려 했는데 form-data에 들어있는 값을 읽지 못하고, file에 location을 찾을 수 없다는 현상이 발생했다.</p>
<pre><code class="language-javascript">// upload-middleware.ts
import { HttpStatus, Injectable, NestMiddleware } from &#39;@nestjs/common&#39;;
import { Request, Response, NextFunction } from &#39;express&#39;;
import * as multer from &#39;multer&#39;;
import * as multerS3 from &#39;multer-s3&#39;;
import * as AWS from &#39;aws-sdk&#39;;
import * as path from &#39;path&#39;;
import { uuid } from &#39;uuidv4&#39;;

@Injectable()
export class UploadMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    try {
      const s3 = new AWS.S3({
        accessKeyId: process.env.S3_ACCESS_KEY,
        secretAccessKey: process.env.S3_ACCESS_KEY_SECRET,
        region: process.env.AWS_REGION,
      });

      const allowedExtensions = [&#39;.png&#39;, &#39;.jpg&#39;, &#39;.jpeg&#39;, &#39;.jfif&#39;, &#39;.exif&#39;, &#39;.tiff&#39;, &#39;.bmp&#39;, &#39;.gif&#39;];

      const upload = multer({
        storage: multerS3({
          s3,
          bucket: process.env.BUCKET_NAME,
          contentType: multerS3.AUTO_CONTENT_TYPE,
          shouldTransform: true,
          key: function (_, file, callback) {
            const fileId = uuid();
            const type = file.mimetype.split(&#39;/&#39;)[1];

            if (!allowedExtensions.includes(path.extname(file.originalname.toLowerCase())) || !file.mimetype.startsWith(&#39;image/&#39;)) {
              const errorMessage = &#39;이미지 파일만 업로드가 가능합니다.&#39;;
              const errorResponse = { errorMessage };
              return res.status(HttpStatus.BAD_REQUEST).json({ errorResponse });
            }

            const fileName = `${fileId}.${type}`;
            callback(null, fileName);
          },

          acl: &#39;public-read-write&#39;,
          limit: { fileSize: 5 * 1024 * 1024 },
        }),
      });
      upload.single(&#39;newFile&#39;)(req, res, next);
    } catch (error) {
      console.error(error);
      res.status(HttpStatus.INTERNAL_SERVER_ERROR).json({ errorMessage: &#39;파일 업로드 에러&#39; });
    }
  }
}</code></pre>
<p>이게 직전 프로젝트에서 사용하던 것인데 location을 찾을 수 없다는 에러는 직접 index.d.ts파일을 만들어서 type을 지정해주고 해결했었던 문제였다.</p>
<pre><code class="language-javascript">//index.d.ts
declare namespace Express {
  export interface Request {
    file: Express.Multer.File;
    user: { userId: number; isAdmin: boolean };
  }
}</code></pre>
<p>그래서 이번에도 타입을 직접 지정해줘야하나 싶어 추가해봤는데 추가를 해도 계속 Multer를 찾을 수 없다는 에러가 발생하여 이것저것 찾아보다 팀원분에게 여쭤보고 해결방법을 찾았다.</p>
<pre><code class="language-javascript">// 내가 적용했던 코드
import { Request } from &#39;express&#39;;
import { User } from &#39;../entities/user.entity&#39;;

export interface IRequest extends Request {
  user?: User;
  file: Express.Multer.File
}</code></pre>
<p>이 부분에서 처음엔 Multer를 찾을 수 없다고 나오다가 vscode를 껐다키니 Request를 확장하여 사용할 수 없다는 에러가 발생하여 아래와 같이 수정하여 해결</p>
<pre><code class="language-javascript">import { Request } from &#39;express&#39;;
import { User } from &#39;../entities/user.entity&#39;;

export interface IRequest extends Request {
  user?: User;
  file: any;
}</code></pre>
<p>먼저 file을 any타입으로 변경해주고</p>
<pre><code class="language-javascript">// upload-middleware.ts
key: function (_, file: Express.Multer.File, callback) {
            const fileId = uuid();
            const type = file.mimetype.split(&#39;/&#39;)[1];</code></pre>
<p>업로드 미들웨어의 file을 Express.Multer.File로 직접 넣어주니 req.file.location을 읽어올 수 있었다.</p>
<p>다음은 form-data의 값을 가져올 수 없는 문제가 있어서 nest-form-data 패키지를 다운받고 데코레이터를 적용했더니 unexpected end of form이라는 오류가 발생했다.
stackoverflow에 나와 같은 문제를 겪은 사람이 많아 댓글을 읽어보니 multer의 버전을 1.4.3을 사용하면 된다해서 @types/multer의 버전을 1.4.3으로 다운그레이드 후 nest-form-data패키지를 삭제하고 실행해보니 정상작동했다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[TIL] 3계층구조 트랜잭션 23.07.25]]></title>
            <link>https://velog.io/@sh_0317/TIL-3%EA%B3%84%EC%B8%B5%EA%B5%AC%EC%A1%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-23.07.25</link>
            <guid>https://velog.io/@sh_0317/TIL-3%EA%B3%84%EC%B8%B5%EA%B5%AC%EC%A1%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-23.07.25</guid>
            <pubDate>Wed, 26 Jul 2023 00:05:10 GMT</pubDate>
            <description><![CDATA[<p>트랜잭션을 걸어줄려면 레포지토리 단계에서 작업을 해야된다고 한다. 
그런데 우리조는 비즈니스로직 단계에서 이것저것 검증을 한 뒤 트랜잭션을 걸려고 서비스단계에서 작성을 했고, 역시나 실행을 해보면 트랜잭션이 작동을 안해서 오류가 났다.
한참을 고민하다 튜터님에게 질문을 드려보니 해답은 의외로 간단했다.</p>
<h4 id="servicejs-코드">service.js 코드</h4>
<pre><code class="language-javascript">isDelivered = async (orderId, res) =&gt; {
    try {
      const user = res.locals.user;
      const existStore = await this.storeRepository.getStoreInfo(user.id);
      if (!existStore) throw errorHandler.notRegistered;

      const order = await this.orderRepository.findOrder(orderId);
      if (!order) throw errorHandler.noOrder;

      if (order.order_status === &#39;delivered&#39;) throw errorHandler.completedOrder;
      else if (order.order_status === &#39;refundRequest&#39;) throw errorHandler.refundOrder;
      else if (order.order_status === &#39;cancelled&#39;) throw errorHandler.cancelledOrder;

      const total_sales = existStore.total_sales + order.total_price;
      const t = await sequelize.transaction({
        isolationLevel: Transaction.ISOLATION_LEVELS.READ_COMMITTED,
      });
      try {
        await this.orderRepository.updateDeliveryStatus(orderId, t);
        await this.storeRepository.updateStoreInSales(user.id, total_sales, t);
        await t.commit();
        return {
          code: 201,
          message: `배달이 완료되었습니다. ${order.total_price}포인트가 입금 되었습니다.`,
          data: { userId: order.user_id },
        };
      } catch (transactionError) {
        await t.rollback();
        throw transactionError;
      }
    } catch (err) {
      throw err;
    }
  };</code></pre>
<h4 id="repositoryjs">repository.js</h4>
<pre><code class="language-javascript">// order
  updateDeliveryStatus = async (orderId, t) =&gt; {
    await Order.update({ order_status: &#39;delivered&#39; }, { where: { id: orderId }, transaction: t });
    return;
  };
// store
  updateStoreInSales = async (userId, price, t) =&gt; {
    await Store.update({ total_sales: price }, { where: { user_id: userId }, transaction: t });
  };
}</code></pre>
<p>이렇게 로직을 구성한 뒤 레포지토리에 트랜잭션도 같이 보내주면 해결되는 문제였다.</p>
]]></description>
        </item>
    </channel>
</rss>