<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>while(true)</title>
        <link>https://velog.io/</link>
        <description>공부 노트 겸 기록장</description>
        <lastBuildDate>Sat, 19 Apr 2025 05:31:40 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>while(true)</title>
            <url>https://velog.velcdn.com/images/kk_0128_/profile/6645f7c2-2d55-4047-a57c-d4f13bf76612/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. while(true). All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/kk_0128_" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[🔐 우분투 서버에 SSH 공개키 등록해서 비밀번호 없이 접속하기]]></title>
            <link>https://velog.io/@kk_0128_/%EC%9A%B0%EB%B6%84%ED%88%AC-%EC%84%9C%EB%B2%84%EC%97%90-SSH-%EA%B3%B5%EA%B0%9C%ED%82%A4-%EB%93%B1%EB%A1%9D%ED%95%B4%EC%84%9C-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%97%86%EC%9D%B4-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kk_0128_/%EC%9A%B0%EB%B6%84%ED%88%AC-%EC%84%9C%EB%B2%84%EC%97%90-SSH-%EA%B3%B5%EA%B0%9C%ED%82%A4-%EB%93%B1%EB%A1%9D%ED%95%B4%EC%84%9C-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%97%86%EC%9D%B4-%EC%A0%91%EC%86%8D%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 19 Apr 2025 05:31:40 GMT</pubDate>
            <description><![CDATA[<p>최근 학점가방(Unibag) 프로젝트를 진행하면서, 보안성과 편의성을 높이기 위해 SSH 접속 방식을 공개키 기반 인증으로 전환하게 되었다.</p>
<p>처음에는 낯설었지만, 설정해보니 구조가 명확했고 팀원들과의 커뮤니케이션도 자연스럽게 이어졌다.</p>
<p>이 글은 그 경험을 기록한 실전 가이드다.</p>
<blockquote>
<p>✅ 서버 주소나 키 값은 보안을 위해 예시로 작성하였다.</p>
</blockquote>
<hr>
<h2 id="📌-1-ssh-접속-시도-초기-상태">📌 1. SSH 접속 시도 (초기 상태)</h2>
<pre><code class="language-bash">ssh odroid@&lt;서버_IP&gt;</code></pre>
<p><strong>사용한 이유:</strong> 서버에 기본적인 SSH 접속을 시도하기 위해 사용.</p>
<p>처음 접속 시 서버의 호스트 키가 로컬에 없기 때문에 경고 메시지가 뜬다.</p>
<pre><code>The authenticity of host &#39;&lt;서버_IP&gt;&#39; can&#39;t be established.
Are you sure you want to continue connecting (yes/no/[fingerprint])?</code></pre><p>처음 접속이므로 <code>yes</code>를 입력하고 진행한다.</p>
<hr>
<h2 id="😢-비밀번호-인증-실패">😢 비밀번호 인증 실패</h2>
<pre><code>odroid@&lt;서버_IP&gt;&#39;s password:
Permission denied, please try again.</code></pre><p>비밀번호를 모를 경우 접속이 불가능하다.</p>
<p>이 문제를 해결하기 위해 공개키 인증을 설정한다.</p>
<hr>
<h2 id="🪪-2-로컬에서-ssh-키-생성">🪪 2. 로컬에서 SSH 키 생성</h2>
<h3 id="github-actions용-자동-배포를-위해">GitHub Actions용 (자동 배포를 위해)</h3>
<pre><code class="language-bash">ssh-keygen -t rsa -b 4096 -C &quot;unibag-deploy@github-actions&quot; -f ~/.ssh/unibag_github_actions</code></pre>
<p><strong>사용한 이유:</strong> GitHub Actions에서 비밀번호 없이 서버에 자동 접속할 수 있도록 키 쌍을 생성.</p>
<ul>
<li><code>f</code> 옵션으로 키 파일 이름을 명시하고, <code>C</code>는 키 식별용 주석을 추가한다.</li>
</ul>
<h3 id="팀원용-예-팀원-키-발급">팀원용 (예: 팀원 키 발급)</h3>
<pre><code class="language-bash">ssh-keygen -t rsa -b 4096 -C &quot;kim@unibag-team&quot; -f ~/.ssh/team_kim</code></pre>
<p><strong>사용한 이유:</strong> 팀원이 키가 없다고 해서 대신 발급해드리기 위해 사용.</p>
<p>키 쌍을 발급한 뒤 공개키는 서버에 등록하고, 비공개키는 안전하게 전달한다.</p>
<hr>
<h2 id="✉️-공개키-전달-및-등록-과정">✉️ 공개키 전달 및 등록 과정</h2>
<p>액션 담당 팀원분이 서버에 접속이 필요하다고 하셔서, 대신 키를 만들어 등록을 도와드리기로 했다.</p>
<p>처음엔 “비공개키를 서버에 등록하고, 공개키로 로그인하는 건가요?”라고 물으셨는데,</p>
<p>정확한 방식은 다음과 같다:</p>
<ol>
<li><strong>내 로컬에서 팀원용 키 생성</strong><code>bash ssh-keygen -t rsa -b 4096 -C &quot;kim@unibag-team&quot; -f ~/.ssh/team_kim</code></li>
<li><strong>공개키(<code>team_kim.pub</code>)를 서버의 odroid 계정에 등록</strong><code>bash mkdir -p ~/.ssh chmod 700 ~/.ssh cat ~/team_kim.pub &gt;&gt; ~/.ssh/authorized_keys chmod 600 ~/.ssh/authorized_keys</code></li>
<li><strong>비공개키(<code>team_kim</code>)를 zip 암호화해서 팀원에게 전달</strong><code>bash zip -e team_kim.zip ~/.ssh/team_kim</code></li>
<li><strong>팀원은 받은 키를 자신의 .ssh 폴더에 복사하고 권한 설정</strong><code>bash mv ~/Downloads/team_kim ~/.ssh/ chmod 600 ~/.ssh/team_kim</code></li>
<li><strong>접속 시</strong><code>bash ssh -i ~/.ssh/team_kim -p &lt;포트번호&gt; &lt;계정명&gt;@&lt;서버_IP&gt;</code></li>
</ol>
<p>이런 흐름으로 비밀번호 없이 SSH 접속이 가능해졌다.</p>
<hr>
<h2 id="🔧-3-sshd-설정-확인-필요시">🔧 3. sshd 설정 확인 (필요시)</h2>
<pre><code class="language-bash">sudo nano /etc/ssh/sshd_config</code></pre>
<p><strong>사용한 이유:</strong> 서버가 공개키 인증을 허용하고 있는지 확인하고, 포트 등 설정을 점검하기 위함이다.</p>
<p>다음 항목들을 확인 또는 추가한다:</p>
<pre><code class="language-toml">Port &lt;포트번호&gt;PubkeyAuthentication yesAuthorizedKeysFile .ssh/authorized_keys</code></pre>
<p>설정을 변경한 경우에는 SSH 서버 재시작이 필요하다:</p>
<pre><code class="language-bash">sudo systemctl restart ssh</code></pre>
<p>단, 공개키를 등록한 경우에는 재시작을 할 필요가 없다.</p>
<hr>
<h2 id="✅-4-ssh-공개키-인증-테스트">✅ 4. SSH 공개키 인증 테스트</h2>
<pre><code class="language-bash">ssh -i ~/.ssh/team_kim -p &lt;포트번호&gt; &lt;계정명&gt;@&lt;서버_IP&gt;</code></pre>
<p><strong>사용한 이유:</strong> 공개키 기반으로 비밀번호 없이 접속이 잘 되는지 확인하기 위해 실행.</p>
<hr>
<h2 id="🧹-host-key-verification-오류-해결">🧹 Host key verification 오류 해결</h2>
<p>만약 다음과 같은 오류가 발생한다면:</p>
<pre><code>Host key verification failed.</code></pre><p>이는 이전에 저장된 서버의 호스트 키와 현재 키가 달라졌기 때문이다.</p>
<p>아래 명령어로 해결 가능하다:</p>
<pre><code class="language-bash">ssh-keygen -R &quot;[&lt;서버_IP&gt;]:&lt;포트번호&gt;&quot;</code></pre>
<p><strong>사용한 이유:</strong> 기존에 저장된 잘못된 호스트키를 제거하기 위해 사용한다.</p>
<hr>
<h2 id="🔒-ssh-비밀번호-로그인-비활성화-방법-공개키-인증만-허용">🔒 SSH 비밀번호 로그인 비활성화 방법 (공개키 인증만 허용)</h2>
<p>공개키 기반 인증을 활성화한 후에는 기존 비밀번호 로그인 방식은 비활성화하는 것이 보안에 좋다.<br>이를 위해 SSH 설정 파일을 수정하고 SSH 서비스를 재시작한다.</p>
<h3 id="📁-1-ssh-설정-파일-열기">📁 1. SSH 설정 파일 열기</h3>
<pre><code class="language-bash">sudo nano /etc/ssh/sshd_config</code></pre>
<h3 id="⚙️-2-아래-항목-수정">⚙️ 2. 아래 항목 수정</h3>
<pre><code class="language-conf">PasswordAuthentication no
ChallengeResponseAuthentication no
UsePAM no</code></pre>
<blockquote>
<p>🔹 이미 설정되어 있다면 주석(<code>#</code>)을 제거하고 <code>no</code>로 변경<br>🔹 <code>PubkeyAuthentication yes</code>가 켜져 있어야 공개키 인증이 정상 작동함</p>
</blockquote>
<h3 id="💾-3-저장하고-종료">💾 3. 저장하고 종료</h3>
<ul>
<li>Nano 기준: <code>Ctrl + O</code> → <code>Enter</code> → <code>Ctrl + X</code></li>
</ul>
<h3 id="🔁-4-ssh-서비스-재시작">🔁 4. SSH 서비스 재시작</h3>
<pre><code class="language-bash">sudo systemctl restart ssh</code></pre>
<h3 id="✅-완료">✅ 완료</h3>
<p>이제 비밀번호 방식은 차단되고, <code>~/.ssh/authorized_keys</code>에 등록된 키로만 접속이 가능하다.</p>
<hr>
<h3 id="🧪-접속-테스트-팁">🧪 접속 테스트 팁</h3>
<p>SSH 세션이 끊기면 다시 붙기 어려울 수 있으니, 설정 전 <strong>기존 세션은 그대로 둔 채 새 터미널에서 테스트 접속</strong>을 권장한다.</p>
<pre><code class="language-bash">ssh -i ~/.ssh/mykey &lt;계정명&gt;@&lt;서버_IP&gt; -p &lt;포트번호&gt;</code></pre>
<h2 id="✅-결론">✅ 결론</h2>
<ul>
<li>공개키 인증은 SSH 접속을 훨씬 편하게 만든다.</li>
<li>자동화에는 암호 없는 키, 개인용은 암호 있는 키 사용을 추천한다.</li>
<li>공개키와 비공개키의 역할을 정확히 이해하는 것이 중요하다.</li>
<li>실무에서 키를 분리하여 관리하는 습관은 보안상 매우 좋다.</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] IDE 밑줄 제거하기 - Lint 비활성화]]></title>
            <link>https://velog.io/@kk_0128_/Flutter-IDE-%EB%B0%91%EC%A4%84-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-Lint-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94</link>
            <guid>https://velog.io/@kk_0128_/Flutter-IDE-%EB%B0%91%EC%A4%84-%EC%A0%9C%EA%B1%B0%ED%95%98%EA%B8%B0-Lint-%EB%B9%84%ED%99%9C%EC%84%B1%ED%99%94</guid>
            <pubDate>Mon, 07 Apr 2025 13:13:51 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-yaml">include: package:flutter_lints/flutter.yaml

linter:
  rules:
    prefer_typing_uninitialized_variables: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors : false
    avoid_print: false</code></pre>
<p>프로젝트 디렉토리 내 <strong>analysis_options.yaml</strong> 파일에 작성하면 된다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 과일 장수 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EA%B3%BC%EC%9D%BC-%EC%9E%A5%EC%88%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-g3allun4</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EA%B3%BC%EC%9D%BC-%EC%9E%A5%EC%88%98-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-g3allun4</guid>
            <pubDate>Mon, 07 Apr 2025 13:13:03 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>과일 장수가 사과 상자를 포장하고 있습니다. 사과는 상태에 따라 1점부터 k점까지의 점수로 분류하며, k점이 최상품의 사과이고 1점이 최하품의 사과입니다. 사과 한 상자의 가격은 다음과 같이 결정됩니다.</p>
<ul>
<li>한 상자에 사과를 m개씩 담아 포장합니다.</li>
<li>상자에 담긴 사과 중 가장 낮은 점수가 p (1 ≤ p ≤ k)점인 경우, 사과 한 상자의 가격은 p * m 입니다.</li>
</ul>
<p>과일 장수가 가능한 많은 사과를 팔았을 때, 얻을 수 있는 최대 이익을 계산하고자 합니다.(사과는 상자 단위로만 판매하며, 남는 사과는 버립니다)</p>
<p>예를 들어, <code>k</code> = 3, <code>m</code> = 4, 사과 7개의 점수가 [1, 2, 3, 1, 2, 3, 1]이라면, 다음과 같이 [2, 3, 2, 3]으로 구성된 사과 상자 1개를 만들어 판매하여 최대 이익을 얻을 수 있습니다.</p>
<ul>
<li>(최저 사과 점수) x (한 상자에 담긴 사과 개수) x (상자의 개수) = 2 x 4 x 1 = 8</li>
</ul>
<p>사과의 최대 점수 <code>k</code>, 한 상자에 들어가는 사과의 수 <code>m</code>, 사과들의 점수 <code>score</code>가 주어졌을 때, 과일 장수가 얻을 수 있는 최대 이익을 return하는 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>3 ≤ <code>k</code> ≤ 9</li>
<li>3 ≤ <code>m</code> ≤ 10</li>
<li>7 ≤ <code>score</code>의 길이 ≤ 1,000,000<ul>
<li>1 ≤ <code>score[i]</code> ≤ k</li>
</ul>
</li>
<li>이익이 발생하지 않는 경우에는 0을 return 해주세요.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>k</th>
<th>m</th>
<th>score</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>3</td>
<td>4</td>
<td>[1, 2, 3, 1, 2, 3, 1]</td>
<td>8</td>
</tr>
<tr>
<td>4</td>
<td>3</td>
<td>[4, 1, 2, 2, 4, 4, 4, 4, 1, 2, 4, 2]</td>
<td>33</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p><strong>입출력 예 #1</strong></p>
<ul>
<li>문제의 예시와 같습니다.</li>
</ul>
<p><strong>입출력 예 #2</strong></p>
<ul>
<li>다음과 같이 사과 상자를 포장하여 모두 팔면 최대 이익을 낼 수 있습니다.</li>
</ul>
<table>
<thead>
<tr>
<th>사과 상자</th>
<th>가격</th>
</tr>
</thead>
<tbody><tr>
<td>[1, 1, 2]</td>
<td>1 x 3 = 3</td>
</tr>
<tr>
<td>[2, 2, 2]</td>
<td>2 x 3 = 6</td>
</tr>
<tr>
<td>[4, 4, 4]</td>
<td>4 x 3 = 12</td>
</tr>
<tr>
<td>[4, 4, 4]</td>
<td>4 x 3 = 12</td>
</tr>
</tbody></table>
<p>따라서 (1 x 3 x 1) + (2 x 3 x 1) + (4 x 3 x 2) = 33을 return합니다.</p>
<ul>
<li><a href="https://school.programmers.co.kr/learn/courses/30/lessons/135808#">solution.c</a></li>
</ul>
<p>1</p>
<pre><code>#include &lt;stdio.h&gt;</code></pre><p>2</p>
<pre><code>#include &lt;stdbool.h&gt;</code></pre><p>3</p>
<pre><code>#include &lt;stdlib.h&gt;</code></pre><p>4</p>
<p>5</p>
<pre><code>// score_len은 배열 score의 길이입니다.</code></pre><p>6</p>
<pre><code>int solution(int k, int m, int score[], size_t score_len) {</code></pre><p>7</p>
<pre><code>    int answer = 0;</code></pre><p>8</p>
<pre><code>    return answer;</code></pre><pre><code class="language-python">def solution(k, m, score):
    sortdBox = sorted(score, reverse=True) # 전체 사과를 내림차순으로 정렬
    sum = 0

    for i in range(m-1, len(score), m): # 정렬된 사과들을 한 상자에 담을 수 있는 사과의 수 간격으로 카운트
        sum += sortdBox[i] * m # 가장 작은 가격의 사과 값 * 상자 내 사과 갯수

    return sum</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 혼자 놀기의 달인 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%ED%98%BC%EC%9E%90-%EB%86%80%EA%B8%B0%EC%9D%98-%EB%8B%AC%EC%9D%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-kkn01u04</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%ED%98%BC%EC%9E%90-%EB%86%80%EA%B8%B0%EC%9D%98-%EB%8B%AC%EC%9D%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-kkn01u04</guid>
            <pubDate>Mon, 07 Apr 2025 13:12:07 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>혼자서도 잘 노는 범희는 어느 날 방구석에 있는 숫자 카드 더미를 보더니 혼자 할 수 있는 재미있는 게임을 생각해냈습니다.</p>
<p>숫자 카드 더미에는 카드가 총 100장 있으며, 각 카드에는 1부터 100까지 숫자가 하나씩 적혀있습니다. 2 이상 100 이하의 자연수를 하나 정해 그 수보다 작거나 같은 숫자 카드들을 준비하고, 준비한 카드의 수만큼 작은 상자를 준비하면 게임을 시작할 수 있으며 게임 방법은 다음과 같습니다.</p>
<p>준비된 상자에 카드를 한 장씩 넣고, 상자를 무작위로 섞어 일렬로 나열합니다. 상자가 일렬로 나열되면 상자가 나열된 순서에 따라 1번부터 순차적으로 증가하는 번호를 붙입니다.</p>
<p>그 다음 임의의 상자를 하나 선택하여 선택한 상자 안의 숫자 카드를 확인합니다. 다음으로 확인한 카드에 적힌 번호에 해당하는 상자를 열어 안에 담긴 카드에 적힌 숫자를 확인합니다. 마찬가지로 숫자에 해당하는 번호를 가진 상자를 계속해서 열어가며, 열어야 하는 상자가 이미 열려있을 때까지 반복합니다.</p>
<p>이렇게 연 상자들은 1번 상자 그룹입니다. 이제 1번 상자 그룹을 다른 상자들과 섞이지 않도록 따로 둡니다. 만약 1번 상자 그룹을 제외하고 남는 상자가 없으면 그대로 게임이 종료되며, 이때 획득하는 점수는 0점입니다.</p>
<p>그렇지 않다면 남은 상자 중 다시 임의의 상자 하나를 골라 같은 방식으로 이미 열려있는 상자를 만날 때까지 상자를 엽니다. 이렇게 연 상자들은 2번 상자 그룹입니다.</p>
<p>1번 상자 그룹에 속한 상자의 수와 2번 상자 그룹에 속한 상자의 수를 곱한 값이 게임의 점수입니다.</p>
<p>상자 안에 들어있는 카드 번호가 순서대로 담긴 배열 <code>cards</code>가 매개변수로 주어질 때, 범희가 이 게임에서 얻을 수 있는 최고 점수를 구해서 return 하도록 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><code>2</code> ≤ <code>cards</code>의 길이 ≤ <code>100</code></li>
<li><code>cards</code>의 원소는 <code>cards</code>의 길이 이하인 임의의 자연수입니다.</li>
<li><code>cards</code>에는 중복되는 원소가 존재하지 않습니다.</li>
<li><code>cards[i]</code>는 i + 1번 상자에 담긴 카드에 적힌 숫자를 의미합니다.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>cards</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>[8,6,3,7,2,5,1,4]</td>
<td>12</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>1, 4, 7, 8이 속하는 상자의 그룹과 2, 5, 6이 속하는 상자의 그룹과 3이 속하는 상자의 그룹이 존재합니다. 따라서 3번 상자를 고르지 않았을 때, 두 번의 시행에서 3과 4를 기록하며 최고 점수 12를 얻을 수 있습니다.</p>
<pre><code class="language-python">def solution(cards):
    answer = 0
    visited = [False for _ in range(len(cards))]  # 각 상자가 열렸는지 여부를 기록하는 배열
    lst = []  # 각 그룹의 크기를 저장할 리스트

    for i in range(len(cards)):  # 모든 상자를 순회하며 그룹을 형성
        if visited[i]:  # 이미 열렸다면 건너뜀
            continue
        visited[i] = True  # 현재 상자를 열었다고 표시
        cur, linked = i, 1  # 현재 상자의 인덱스와 그룹 크기를 초기화
        while cards[cur] - 1 != i:  # 다음 상자로 이동하며 그룹 형성
            cur = cards[cur] - 1  # 카드에 적힌 숫자를 인덱스로 사용하여 이동
            visited[cur] = True  # 해당 상자를 열었다고 표시
            linked += 1  # 그룹 크기를 증가시킴
        lst.append(linked)  # 그룹 크기를 리스트에 추가

    if len(lst) &lt; 2:  # 그룹이 2개 미만이면 점수는 0
        answer = 0
    else:
        lst.sort(reverse=True)  # 그룹 크기를 내림차순으로 정렬
        answer = lst[0] * lst[1]  # 가장 큰 두 그룹의 크기를 곱함

    return answer  # 최종 점수를 반환</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 소수 찾기 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%86%8C%EC%88%98-%EC%B0%BE%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%86%8C%EC%88%98-%EC%B0%BE%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</guid>
            <pubDate>Mon, 07 Apr 2025 13:09:35 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>1부터 입력받은 숫자 n 사이에 있는 소수의 개수를 반환하는 함수, solution을 만들어 보세요.</p>
<p>소수는 1과 자기 자신으로만 나누어지는 수를 의미합니다.</p>
<p>(1은 소수가 아닙니다.)</p>
<h3 id="제한-조건">제한 조건</h3>
<ul>
<li>n은 2이상 1000000이하의 자연수입니다.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>n</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>10</td>
<td>4</td>
</tr>
<tr>
<td>5</td>
<td>3</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>입출력 예 #1</p>
<p>1부터 10 사이의 소수는 [2,3,5,7] 4개가 존재하므로 4를 반환</p>
<p>입출력 예 #2</p>
<p>1부터 5 사이의 소수는 [2,3,5] 3개가 존재하므로 3를 반환</p>
<pre><code class="language-python">import math

def solution(n):
    result = []
    tmp = [True for i in range(n+1)]

    for i in range(2, int(math.sqrt(n))+1):
        if tmp[i] == True:
            j = 2
            while i * j &lt;= n:
                tmp[i*j] = False
                j+=1

    for j in range(2,n+1):
        if tmp[j]:
            result.append(j)

    return len(result)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 크기가 작은 부분 문자열 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%ED%81%AC%EA%B8%B0%EA%B0%80-%EC%9E%91%EC%9D%80-%EB%B6%80%EB%B6%84-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-gzuatjoo</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%ED%81%AC%EA%B8%B0%EA%B0%80-%EC%9E%91%EC%9D%80-%EB%B6%80%EB%B6%84-%EB%AC%B8%EC%9E%90%EC%97%B4-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-gzuatjoo</guid>
            <pubDate>Mon, 07 Apr 2025 13:08:19 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>숫자로 이루어진 문자열 <code>t</code>와 <code>p</code>가 주어질 때, <code>t</code>에서 <code>p</code>와 길이가 같은 부분문자열 중에서, 이 부분문자열이 나타내는 수가 <code>p</code>가 나타내는 수보다 작거나 같은 것이 나오는 횟수를 return하는 함수 solution을 완성하세요.</p>
<p>예를 들어, <code>t</code>=&quot;3141592&quot;이고 <code>p</code>=&quot;271&quot; 인 경우, <code>t</code>의 길이가 3인 부분 문자열은 314, 141, 415, 159, 592입니다. 이 문자열이 나타내는 수 중 271보다 작거나 같은 수는 141, 159 2개 입니다.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>1 ≤ <code>p</code>의 길이 ≤ 18</li>
<li><code>p</code>의 길이 ≤ <code>t</code>의 길이 ≤ 10,000</li>
<li><code>t</code>와 <code>p</code>는 숫자로만 이루어진 문자열이며, 0으로 시작하지 않습니다.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>t</th>
<th>p</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;3141592&quot;</td>
<td>&quot;271&quot;</td>
<td>2</td>
</tr>
<tr>
<td>&quot;500220839878&quot;</td>
<td>&quot;7&quot;</td>
<td>8</td>
</tr>
<tr>
<td>&quot;10203&quot;</td>
<td>&quot;15&quot;</td>
<td>3</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>입출력 예 #1</p>
<p>본문과 같습니다.</p>
<p>입출력 예 #2</p>
<p><code>p</code>의 길이가 1이므로 <code>t</code>의 부분문자열은 &quot;5&quot;, &quot;0&quot;, 0&quot;, &quot;2&quot;, &quot;2&quot;, &quot;0&quot;, &quot;8&quot;, &quot;3&quot;, &quot;9&quot;, &quot;8&quot;, &quot;7&quot;, &quot;8&quot;이며 이중 7보다 작거나 같은 숫자는 &quot;5&quot;, &quot;0&quot;, &quot;0&quot;, &quot;2&quot;, &quot;2&quot;, &quot;0&quot;, &quot;3&quot;, &quot;7&quot; 이렇게 8개가 있습니다.</p>
<p>입출력 예 #3</p>
<p><code>p</code>의 길이가 2이므로 <code>t</code>의 부분문자열은 &quot;10&quot;, &quot;02&quot;, &quot;20&quot;, &quot;03&quot;이며, 이중 15보다 작거나 같은 숫자는 &quot;10&quot;, &quot;02&quot;, &quot;03&quot; 이렇게 3개입니다. &quot;02&quot;와 &quot;03&quot;은 각각 2, 3에 해당한다는 점에 주의하세요</p>
<ul>
<li><a href="https://school.programmers.co.kr/learn/courses/30/lessons/147355#">solution.c</a></li>
</ul>
<p>1</p>
<pre><code>#include &lt;stdio.h&gt;</code></pre><p>2</p>
<pre><code>#include &lt;stdbool.h&gt;</code></pre><p>3</p>
<pre><code>#include &lt;stdlib.h&gt;</code></pre><p>4</p>
<p>5</p>
<pre><code>// 파라미터로 주어지는 문자열은 const로 주어집니다. 변경하려면 문자열을 복사해서 사용하세요.</code></pre><p>6</p>
<pre><code>int solution(const char* t, const char* p) {</code></pre><p>7</p>
<pre><code>    int answer = 0;</code></pre><p>8</p>
<pre><code>    return answer;</code></pre><pre><code class="language-python">def solution(t, p):
    max = len(t) - 1
    i = 0
    j = 0
    lenP = len(p)
    answer = 0

    while (j &lt;= max): 
        if (int(t[i:lenP + j]) &lt;= int(p)):
            answer += 1     
        i += 1
        j += 1
        if (i == len(t)- len(p) + 1):
            return answer</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 신고 결과 받기 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%8B%A0%EA%B3%A0-%EA%B2%B0%EA%B3%BC-%EB%B0%9B%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%8B%A0%EA%B3%A0-%EA%B2%B0%EA%B3%BC-%EB%B0%9B%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</guid>
            <pubDate>Mon, 07 Apr 2025 13:06:18 GMT</pubDate>
            <description><![CDATA[<h3 id="문제-설명">문제 설명</h3>
<p>신입사원 무지는 게시판 불량 이용자를 신고하고 처리 결과를 메일로 발송하는 시스템을 개발하려 합니다. 무지가 개발하려는 시스템은 다음과 같습니다.</p>
<ul>
<li>각 유저는 한 번에 한 명의 유저를 신고할 수 있습니다.<ul>
<li>신고 횟수에 제한은 없습니다. 서로 다른 유저를 계속해서 신고할 수 있습니다.</li>
<li>한 유저를 여러 번 신고할 수도 있지만, 동일한 유저에 대한 신고 횟수는 1회로 처리됩니다.</li>
</ul>
</li>
<li>k번 이상 신고된 유저는 게시판 이용이 정지되며, 해당 유저를 신고한 모든 유저에게 정지 사실을 메일로 발송합니다.<ul>
<li>유저가 신고한 모든 내용을 취합하여 마지막에 한꺼번에 게시판 이용 정지를 시키면서 정지 메일을 발송합니다.</li>
</ul>
</li>
</ul>
<p>다음은 전체 유저 목록이 [&quot;muzi&quot;, &quot;frodo&quot;, &quot;apeach&quot;, &quot;neo&quot;]이고, k = 2(즉, 2번 이상 신고당하면 이용 정지)인 경우의 예시입니다.</p>
<table>
<thead>
<tr>
<th>유저 ID</th>
<th>유저가 신고한 ID</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;muzi&quot;</td>
<td>&quot;frodo&quot;</td>
<td>&quot;muzi&quot;가 &quot;frodo&quot;를 신고했습니다.</td>
</tr>
<tr>
<td>&quot;apeach&quot;</td>
<td>&quot;frodo&quot;</td>
<td>&quot;apeach&quot;가 &quot;frodo&quot;를 신고했습니다.</td>
</tr>
<tr>
<td>&quot;frodo&quot;</td>
<td>&quot;neo&quot;</td>
<td>&quot;frodo&quot;가 &quot;neo&quot;를 신고했습니다.</td>
</tr>
<tr>
<td>&quot;muzi&quot;</td>
<td>&quot;neo&quot;</td>
<td>&quot;muzi&quot;가 &quot;neo&quot;를 신고했습니다.</td>
</tr>
<tr>
<td>&quot;apeach&quot;</td>
<td>&quot;muzi&quot;</td>
<td>&quot;apeach&quot;가 &quot;muzi&quot;를 신고했습니다.</td>
</tr>
</tbody></table>
<p>각 유저별로 신고당한 횟수는 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>유저 ID</th>
<th>신고당한 횟수</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;muzi&quot;</td>
<td>1</td>
</tr>
<tr>
<td>&quot;frodo&quot;</td>
<td>2</td>
</tr>
<tr>
<td>&quot;apeach&quot;</td>
<td>0</td>
</tr>
<tr>
<td>&quot;neo&quot;</td>
<td>2</td>
</tr>
</tbody></table>
<p>위 예시에서는 2번 이상 신고당한 &quot;frodo&quot;와 &quot;neo&quot;의 게시판 이용이 정지됩니다. 이때, 각 유저별로 신고한 아이디와 정지된 아이디를 정리하면 다음과 같습니다.</p>
<table>
<thead>
<tr>
<th>유저 ID</th>
<th>유저가 신고한 ID</th>
<th>정지된 ID</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;muzi&quot;</td>
<td>[&quot;frodo&quot;, &quot;neo&quot;]</td>
<td>[&quot;frodo&quot;, &quot;neo&quot;]</td>
</tr>
<tr>
<td>&quot;frodo&quot;</td>
<td>[&quot;neo&quot;]</td>
<td>[&quot;neo&quot;]</td>
</tr>
<tr>
<td>&quot;apeach&quot;</td>
<td>[&quot;muzi&quot;, &quot;frodo&quot;]</td>
<td>[&quot;frodo&quot;]</td>
</tr>
<tr>
<td>&quot;neo&quot;</td>
<td>없음</td>
<td>없음</td>
</tr>
</tbody></table>
<p>따라서 &quot;muzi&quot;는 처리 결과 메일을 2회, &quot;frodo&quot;와 &quot;apeach&quot;는 각각 처리 결과 메일을 1회 받게 됩니다.</p>
<p>이용자의 ID가 담긴 문자열 배열 <code>id_list</code>, 각 이용자가 신고한 이용자의 ID 정보가 담긴 문자열 배열 <code>report</code>, 정지 기준이 되는 신고 횟수 <code>k</code>가 매개변수로 주어질 때, 각 유저별로 처리 결과 메일을 받은 횟수를 배열에 담아 return 하도록 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>2 ≤ <code>id_list</code>의 길이 ≤ 1,000<ul>
<li>1 ≤ <code>id_list</code>의 원소 길이 ≤ 10</li>
<li><code>id_list</code>의 원소는 이용자의 id를 나타내는 문자열이며 알파벳 소문자로만 이루어져 있습니다.</li>
<li><code>id_list</code>에는 같은 아이디가 중복해서 들어있지 않습니다.</li>
</ul>
</li>
<li>1 ≤ <code>report</code>의 길이 ≤ 200,000<ul>
<li>3 ≤ <code>report</code>의 원소 길이 ≤ 21</li>
<li><code>report</code>의 원소는 &quot;이용자id 신고한id&quot;형태의 문자열입니다.</li>
<li>예를 들어 &quot;muzi frodo&quot;의 경우 &quot;muzi&quot;가 &quot;frodo&quot;를 신고했다는 의미입니다.</li>
<li>id는 알파벳 소문자로만 이루어져 있습니다.</li>
<li>이용자id와 신고한id는 공백(스페이스)하나로 구분되어 있습니다.</li>
<li>자기 자신을 신고하는 경우는 없습니다.</li>
</ul>
</li>
<li>1 ≤ <code>k</code> ≤ 200, <code>k</code>는 자연수입니다.</li>
<li>return 하는 배열은 <code>id_list</code>에 담긴 id 순서대로 각 유저가 받은 결과 메일 수를 담으면 됩니다.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>id_list</th>
<th>report</th>
<th>k</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td><code>[&quot;muzi&quot;, &quot;frodo&quot;, &quot;apeach&quot;, &quot;neo&quot;]</code></td>
<td><code>[&quot;muzi frodo&quot;,&quot;apeach frodo&quot;,&quot;frodo neo&quot;,&quot;muzi neo&quot;,&quot;apeach muzi&quot;]</code></td>
<td>2</td>
<td>[2,1,1,0]</td>
</tr>
<tr>
<td><code>[&quot;con&quot;, &quot;ryan&quot;]</code></td>
<td><code>[&quot;ryan con&quot;, &quot;ryan con&quot;, &quot;ryan con&quot;, &quot;ryan con&quot;]</code></td>
<td>3</td>
<td>[0,0]</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p><strong>입출력 예 #1</strong></p>
<p>문제의 예시와 같습니다.</p>
<p><strong>입출력 예 #2</strong></p>
<p>&quot;ryan&quot;이 &quot;con&quot;을 4번 신고했으나, 주어진 조건에 따라 한 유저가 같은 유저를 여러 번 신고한 경우는 신고 횟수 1회로 처리합니다. 따라서 &quot;con&quot;은 1회 신고당했습니다. 3번 이상 신고당한 이용자는 없으며, &quot;con&quot;과 &quot;ryan&quot;은 결과 메일을 받지 않습니다. 따라서 [0, 0]을 return 합니다.</p>
<h3 id="제한시간-안내">제한시간 안내</h3>
<ul>
<li>정확성 테스트 : 10초</li>
</ul>
<pre><code class="language-python">def solution(id_list, report, k):
    warning = {}
    result = [0] * len(id_list)  # 각 사용자가 받을 메일 수를 저장할 리스트
    report = set(report)  # 중복 신고를 제거

    # 신고 정보 처리
    for i in report:
        tmp = i.split()  # 공백을 기준으로 신고자와 대상자를 분리
        reporter, target = tmp[0], tmp[1]

        # 타겟에 대한 신고 정보가 warning에 없으면 초기화
        if target not in warning:
            warning[target] = [0, []]  # [신고 횟수, 신고한 사용자 리스트]

        # 신고자가 중복되지 않으면 추가
        if reporter not in warning[target][1]:
            warning[target][0] += 1
            warning[target][1].append(reporter)

    # 신고 횟수가 k번 이상인 경우 메일 발송
    for target, info in warning.items():
        if info[0] &gt;= k:  # 신고 횟수가 k 이상인 경우
            for reporter in info[1]:  # 해당 사용자를 신고한 사람들에게 메일 발송
                result[id_list.index(reporter)] += 1

    return result</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 숫자 짝꿍 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%88%AB%EC%9E%90-%EC%A7%9D%EA%BF%8D-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-dtgn5eih</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EC%88%AB%EC%9E%90-%EC%A7%9D%EA%BF%8D-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8-dtgn5eih</guid>
            <pubDate>Mon, 07 Apr 2025 13:05:16 GMT</pubDate>
            <description><![CDATA[<p>문제 설명</p>
<p>두 정수 <code>X</code>, <code>Y</code>의 임의의 자리에서 공통으로 나타나는 정수 k(0 ≤ k ≤ 9)들을 이용하여 만들 수 있는 가장 큰 정수를 두 수의 짝꿍이라 합니다(단, 공통으로 나타나는 정수 중 서로 짝지을 수 있는 숫자만 사용합니다). <code>X</code>, <code>Y</code>의 짝꿍이 존재하지 않으면, 짝꿍은 -1입니다. <code>X</code>, <code>Y</code>의 짝꿍이 0으로만 구성되어 있다면, 짝꿍은 0입니다.</p>
<p>예를 들어, <code>X</code> = 3403이고 <code>Y</code> = 13203이라면, <code>X</code>와 <code>Y</code>의 짝꿍은 <code>X</code>와 <code>Y</code>에서 공통으로 나타나는 3, 0, 3으로 만들 수 있는 가장 큰 정수인 330입니다. 다른 예시로 <code>X</code> = 5525이고 <code>Y</code> = 1255이면 <code>X</code>와 <code>Y</code>의 짝꿍은 <code>X</code>와 <code>Y</code>에서 공통으로 나타나는 2, 5, 5로 만들 수 있는 가장 큰 정수인 552입니다(<code>X</code>에는 5가 3개, <code>Y</code>에는 5가 2개 나타나므로 남는 5 한 개는 짝 지을 수 없습니다.)</p>
<p>두 정수 <code>X</code>, <code>Y</code>가 주어졌을 때, <code>X</code>, <code>Y</code>의 짝꿍을 return하는 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>3 ≤ <code>X</code>, <code>Y</code>의 길이(자릿수) ≤ 3,000,000입니다.</li>
<li><code>X</code>, <code>Y</code>는 0으로 시작하지 않습니다.</li>
<li><code>X</code>, <code>Y</code>의 짝꿍은 상당히 큰 정수일 수 있으므로, 문자열로 반환합니다.</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>X</th>
<th>Y</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;100&quot;</td>
<td>&quot;2345&quot;</td>
<td>&quot;-1&quot;</td>
</tr>
<tr>
<td>&quot;100&quot;</td>
<td>&quot;203045&quot;</td>
<td>&quot;0&quot;</td>
</tr>
<tr>
<td>&quot;100&quot;</td>
<td>&quot;123450&quot;</td>
<td>&quot;10&quot;</td>
</tr>
<tr>
<td>&quot;12321&quot;</td>
<td>&quot;42531&quot;</td>
<td>&quot;321&quot;</td>
</tr>
<tr>
<td>&quot;5525&quot;</td>
<td>&quot;1255&quot;</td>
<td>&quot;552&quot;</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p><strong>입출력 예 #1</strong></p>
<ul>
<li><code>X</code>, <code>Y</code>의 짝꿍은 존재하지 않습니다. 따라서 &quot;-1&quot;을 return합니다.</li>
</ul>
<p><strong>입출력 예 #2</strong></p>
<ul>
<li><code>X</code>, <code>Y</code>의 공통된 숫자는 0으로만 구성되어 있기 때문에, 두 수의 짝꿍은 정수 0입니다. 따라서 &quot;0&quot;을 return합니다.</li>
</ul>
<p><strong>입출력 예 #3</strong></p>
<ul>
<li><code>X</code>, <code>Y</code>의 짝꿍은 10이므로, &quot;10&quot;을 return합니다.</li>
</ul>
<p><strong>입출력 예 #4</strong></p>
<ul>
<li><code>X</code>, <code>Y</code>의 짝꿍은 321입니다. 따라서 &quot;321&quot;을 return합니다.</li>
</ul>
<p><strong>입출력 예 #5</strong></p>
<ul>
<li>지문에 설명된 예시와 같습니다.</li>
<li><a href="https://school.programmers.co.kr/learn/courses/30/lessons/131128#">solution.c</a></li>
</ul>
<p>1</p>
<pre><code>#include &lt;stdio.h&gt;</code></pre><p>2</p>
<pre><code>#include &lt;stdbool.h&gt;</code></pre><p>3</p>
<pre><code>#include &lt;stdlib.h&gt;</code></pre><p>4</p>
<p>5</p>
<pre><code>// 파라미터로 주어지는 문자열은 const로 주어집니다. 변경하려면 문자열을 복사해서 사용하세요.</code></pre><p>6</p>
<pre><code>char* solution(const char* X, const char* Y) {</code></pre><p>7</p>
<pre><code>    // return 값은 malloc 등 동적 할당을 사용해주세요. 할당 길이는 상황에 맞게 변경해주세요.</code></pre><p>8</p>
<pre><code>    char* answer = (char*)malloc(1);</code></pre><p>9</p>
<pre><code>    return answer;</code></pre><pre><code class="language-python">def solution(X, Y):
    answer = &#39;&#39;

    for i in range(9,-1,-1) :
        answer += (str(i) * min(X.count(str(i)), Y.count(str(i))))

    if answer == &#39;&#39; :
        return &#39;-1&#39;
    elif len(answer) == answer.count(&#39;0&#39;):
        return &#39;0&#39;
    else :
        return answer</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - 달리기 경주 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EB%8B%AC%EB%A6%AC%EA%B8%B0-%EA%B2%BD%EC%A3%BC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-%EB%8B%AC%EB%A6%AC%EA%B8%B0-%EA%B2%BD%EC%A3%BC-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</guid>
            <pubDate>Mon, 07 Apr 2025 13:03:17 GMT</pubDate>
            <description><![CDATA[<p>문제 설명</p>
<p>얀에서는 매년 달리기 경주가 열립니다. 해설진들은 선수들이 자기 바로 앞의 선수를 추월할 때 추월한 선수의 이름을 부릅니다. 예를 들어 1등부터 3등까지 &quot;mumu&quot;, &quot;soe&quot;, &quot;poe&quot; 선수들이 순서대로 달리고 있을 때, 해설진이 &quot;soe&quot;선수를 불렀다면 2등인 &quot;soe&quot; 선수가 1등인 &quot;mumu&quot; 선수를 추월했다는 것입니다. 즉 &quot;soe&quot; 선수가 1등, &quot;mumu&quot; 선수가 2등으로 바뀝니다.</p>
<p>선수들의 이름이 1등부터 현재 등수 순서대로 담긴 문자열 배열 <code>players</code>와 해설진이 부른 이름을 담은 문자열 배열 <code>callings</code>가 매개변수로 주어질 때, 경주가 끝났을 때 선수들의 이름을 1등부터 등수 순서대로 배열에 담아 return 하는 solution 함수를 완성해주세요.</p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li>5 ≤ <code>players</code>의 길이 ≤ 50,000<ul>
<li><code>players[i]</code>는 i번째 선수의 이름을 의미합니다.</li>
<li><code>players</code>의 원소들은 알파벳 소문자로만 이루어져 있습니다.</li>
<li><code>players</code>에는 중복된 값이 들어가 있지 않습니다.</li>
<li>3 ≤ <code>players[i]</code>의 길이 ≤ 10</li>
</ul>
</li>
<li>2 ≤ <code>callings</code>의 길이 ≤ 1,000,000<ul>
<li><code>callings</code>는 <code>players</code>의 원소들로만 이루어져 있습니다.</li>
<li>경주 진행중 1등인 선수의 이름은 불리지 않습니다.</li>
</ul>
</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>players</th>
<th>callings</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>[&quot;mumu&quot;, &quot;soe&quot;, &quot;poe&quot;, &quot;kai&quot;, &quot;mine&quot;]</td>
<td>[&quot;kai&quot;, &quot;kai&quot;, &quot;mine&quot;, &quot;mine&quot;]</td>
<td>[&quot;mumu&quot;, &quot;kai&quot;, &quot;mine&quot;, &quot;soe&quot;, &quot;poe&quot;]</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p>입출력 예 #1</p>
<p>4등인 &quot;kai&quot; 선수가 2번 추월하여 2등이 되고 앞서 3등, 2등인 &quot;poe&quot;, &quot;soe&quot; 선수는 4등, 3등이 됩니다. 5등인 &quot;mine&quot; 선수가 2번 추월하여 4등, 3등인 &quot;poe&quot;, &quot;soe&quot; 선수가 5등, 4등이 되고 경주가 끝납니다. 1등부터 배열에 담으면 [&quot;mumu&quot;, &quot;kai&quot;, &quot;mine&quot;, &quot;soe&quot;, &quot;poe&quot;]이 됩니다.</p>
<pre><code class="language-python">def solution(players, callings):
    result = {player: i for i, player in enumerate(players)} # 선수: 등수
    for name in callings:
        idxTmp = result[name] # 현재 등수
        result[name] -= 1 # 등수 변경
        result[players[idxTmp-1]] += 1 # 등수 Idx 뒤로 변경
        players[idxTmp-1], players[idxTmp] = players[idxTmp], players[idxTmp-1] # 위치 교환
    return players</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[🔐 코딩테스트 연습 - [PCCP 기출문제] 1번 / 붕대 감기 | 프로그래머스 스쿨]]></title>
            <link>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-PCCP-%EA%B8%B0%EC%B6%9C%EB%AC%B8%EC%A0%9C-1%EB%B2%88-%EB%B6%95%EB%8C%80-%EA%B0%90%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</link>
            <guid>https://velog.io/@kk_0128_/%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%EC%97%B0%EC%8A%B5-PCCP-%EA%B8%B0%EC%B6%9C%EB%AC%B8%EC%A0%9C-1%EB%B2%88-%EB%B6%95%EB%8C%80-%EA%B0%90%EA%B8%B0-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EC%8A%A4%EC%BF%A8</guid>
            <pubDate>Mon, 07 Apr 2025 12:59:51 GMT</pubDate>
            <description><![CDATA[<p>문제 설명</p>
<p>어떤 게임에는 <code>붕대 감기</code>라는 기술이 있습니다.</p>
<p><code>붕대 감기</code>는 <code>t</code>초 동안 붕대를 감으면서 1초마다 <code>x</code>만큼의 체력을 회복합니다. <code>t</code>초 연속으로 붕대를 감는 데 성공한다면 <code>y</code>만큼의 체력을 추가로 회복합니다. 게임 캐릭터에는 최대 체력이 존재해 현재 체력이 최대 체력보다 커지는 것은 불가능합니다.</p>
<p>기술을 쓰는 도중 몬스터에게 공격을 당하면 기술이 취소되고, 공격을 당하는 순간에는 체력을 회복할 수 없습니다. 몬스터에게 공격당해 기술이 취소당하거나 기술이 끝나면 그 즉시 <code>붕대 감기</code>를 다시 사용하며, 연속 성공 시간이 0으로 초기화됩니다.</p>
<p>몬스터의 공격을 받으면 정해진 피해량만큼 현재 체력이 줄어듭니다. 이때, 현재 체력이 0 이하가 되면 캐릭터가 죽으며 더 이상 체력을 회복할 수 없습니다.</p>
<p>당신은 <code>붕대감기</code> 기술의 정보, 캐릭터가 가진 최대 체력과 몬스터의 공격 패턴이 주어질 때 캐릭터가 끝까지 생존할 수 있는지 궁금합니다.</p>
<p><code>붕대 감기</code> 기술의 시전 시간, 1초당 회복량, 추가 회복량을 담은 1차원 정수 배열 <code>bandage</code>와 최대 체력을 의미하는 정수 <code>health</code>, 몬스터의 공격 시간과 피해량을 담은 2차원 정수 배열 <code>attacks</code>가 매개변수로 주어집니다. 모든 공격이 끝난 직후 남은 체력을 return 하도록 solution 함수를 완성해 주세요. <strong>만약 몬스터의 공격을 받고 캐릭터의 체력이 0 이하가 되어 죽는다면 -1을 return 해주세요.</strong></p>
<h3 id="제한사항">제한사항</h3>
<ul>
<li><code>bandage</code>는 [<code>시전 시간</code>, <code>초당 회복량</code>, <code>추가 회복량</code>] 형태의 길이가 3인 정수 배열입니다.<ul>
<li>1 ≤ <code>시전 시간</code> = <code>t</code> ≤ 50</li>
<li>1 ≤ <code>초당 회복량</code> = <code>x</code> ≤ 100</li>
<li>1 ≤ <code>추가 회복량</code> = <code>y</code> ≤ 100</li>
</ul>
</li>
<li>1 ≤ <code>health</code> ≤ 1,000</li>
<li>1 ≤ <code>attacks</code>의 길이 ≤ 100<ul>
<li><code>attacks[i]</code>는 [<code>공격 시간</code>, <code>피해량</code>] 형태의 길이가 2인 정수 배열입니다.</li>
<li><code>attacks</code>는 <code>공격 시간</code>을 기준으로 오름차순 정렬된 상태입니다.</li>
<li><code>attacks</code>의 <code>공격 시간</code>은 모두 다릅니다.</li>
<li>1 ≤ <code>공격 시간</code> ≤ 1,000</li>
<li>1 ≤ <code>피해량</code> ≤ 100</li>
</ul>
</li>
</ul>
<h3 id="입출력-예">입출력 예</h3>
<table>
<thead>
<tr>
<th>bandage</th>
<th>health</th>
<th>attacks</th>
<th>result</th>
</tr>
</thead>
<tbody><tr>
<td>[5, 1, 5]</td>
<td>30</td>
<td>[[2, 10], [9, 15], [10, 5], [11, 5]]</td>
<td>5</td>
</tr>
<tr>
<td>[3, 2, 7]</td>
<td>20</td>
<td>[[1, 15], [5, 16], [8, 6]]</td>
<td>-1</td>
</tr>
<tr>
<td>[4, 2, 7]</td>
<td>20</td>
<td>[[1, 15], [5, 16], [8, 6]]</td>
<td>-1</td>
</tr>
<tr>
<td>[1, 1, 1]</td>
<td>5</td>
<td>[[1, 2], [3, 2]]</td>
<td>3</td>
</tr>
</tbody></table>
<h3 id="입출력-예-설명">입출력 예 설명</h3>
<p><strong>입출력 예 #1</strong></p>
<p>몬스터의 마지막 공격은 11초에 이루어집니다. 0초부터 11초까지 캐릭터의 상태는 아래 표와 같습니다.</p>
<table>
<thead>
<tr>
<th>시간</th>
<th>현재 체력(변화량)</th>
<th>연속 성공</th>
<th>공격</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>30</td>
<td>0</td>
<td>X</td>
<td>초기 상태</td>
</tr>
<tr>
<td>1</td>
<td>30(+0)</td>
<td>1</td>
<td>X</td>
<td>최대 체력 이상의 체력을 가질 수 없습니다.</td>
</tr>
<tr>
<td>2</td>
<td>20(-10)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>3</td>
<td>21(+1)</td>
<td>1</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>22(+1)</td>
<td>2</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>5</td>
<td>23(+1)</td>
<td>3</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>6</td>
<td>24(+1)</td>
<td>4</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td>30(+6)</td>
<td>5 → 0</td>
<td>X</td>
<td>5초 연속 성공해 체력을 5만큼 추가 회복하고 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>8</td>
<td>30(+0)</td>
<td>1</td>
<td>X</td>
<td>최대 체력 이상의 체력을 가질 수 없습니다.</td>
</tr>
<tr>
<td>9</td>
<td>15(-15)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>10</td>
<td>10(-5)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>11</td>
<td>5(-5)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 마지막 공격입니다.</td>
</tr>
</tbody></table>
<p>몬스터의 마지막 공격 직후 캐릭터의 체력은 5입니다. 따라서 <code>5</code>을 return 해야 합니다.</p>
<p><strong>입출력 예 #2</strong></p>
<p>몬스터의 마지막 공격은 8초에 이루어집니다. 0초부터 8초까지 캐릭터의 상태는 아래 표와 같습니다.</p>
<table>
<thead>
<tr>
<th>시간</th>
<th>현재 체력(변화량)</th>
<th>연속 성공</th>
<th>공격</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>20</td>
<td>0</td>
<td>X</td>
<td>초기 상태</td>
</tr>
<tr>
<td>1</td>
<td>5(-15)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>2</td>
<td>7(+2)</td>
<td>1</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>3</td>
<td>9(+2)</td>
<td>2</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>18(+9)</td>
<td>3 → 0</td>
<td>X</td>
<td>3초 연속 성공해 체력을 7만큼 추가 회복하고 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>5</td>
<td>2(-16)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>6</td>
<td>4(+2)</td>
<td>1</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>7</td>
<td>6(+2)</td>
<td>2</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>8</td>
<td>0(-6)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 마지막 공격을 받아 캐릭터의 체력이 0 이하가 됩니다.</td>
</tr>
</tbody></table>
<p>몬스터의 공격을 받아 캐릭터의 체력이 0 이하가 됩니다. 따라서 <code>-1</code>을 return 해야 합니다.</p>
<p><strong>입출력 예 #3</strong></p>
<p>몬스터의 마지막 공격은 8초에 이루어집니다. 0초부터 5초까지 캐릭터의 상태는 아래 표와 같습니다.</p>
<table>
<thead>
<tr>
<th>시간</th>
<th>현재 체력(변화량)</th>
<th>연속 성공</th>
<th>공격</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>20</td>
<td>0</td>
<td>X</td>
<td>초기 상태</td>
</tr>
<tr>
<td>1</td>
<td>5(-15)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>2</td>
<td>7(+2)</td>
<td>1</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>3</td>
<td>9(+2)</td>
<td>2</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>4</td>
<td>11(+2)</td>
<td>3</td>
<td>X</td>
<td></td>
</tr>
<tr>
<td>5</td>
<td>-5(-16)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격을 받아 캐릭터의 체력이 0 이하가 됩니다.</td>
</tr>
</tbody></table>
<p>몬스터의 공격을 받아 캐릭터의 체력이 0 이하가 됩니다. 따라서 <code>-1</code>을 return 해야 합니다.</p>
<p><strong>입출력 예 #4</strong></p>
<p>몬스터의 마지막 공격은 3초에 이루어집니다. 0초부터 3초까지 캐릭터의 상태는 아래 표와 같습니다.</p>
<table>
<thead>
<tr>
<th>시간</th>
<th>현재 체력(변화량)</th>
<th>연속 성공</th>
<th>공격</th>
<th>설명</th>
</tr>
</thead>
<tbody><tr>
<td>0</td>
<td>5</td>
<td>0</td>
<td>X</td>
<td>초기 상태</td>
</tr>
<tr>
<td>1</td>
<td>3(-2)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 공격으로 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>2</td>
<td>5(+2)</td>
<td>1 → 0</td>
<td>X</td>
<td>1초 연속 성공해 체력을 1만큼 추가 회복하고 연속 성공이 초기화됩니다.</td>
</tr>
<tr>
<td>3</td>
<td>3(-2)</td>
<td>0</td>
<td>O</td>
<td>몬스터의 마지막 공격입니다.</td>
</tr>
</tbody></table>
<p>몬스터의 마지막 공격 직후 캐릭터의 체력은 3입니다. 따라서 <code>3</code>을 return 해야 합니다.</p>
<pre><code class="language-python">def solution(bandage, health, attacks):
    j = 0
    counter = 0
    max = health

    for sec in range(1, attacks[-1][0] + 1): # 가장 마지막 공격 시간까지 반복
        counter += 1

        if (health &gt;= max): # 이전에 회복한 체력이 최대 체력을 넘어섰을 경우 최대 체력으로 초기화
            health = max

        if (health &lt;= 0): # 남은 체력이 0보다 작거나 같을 때
            return -1

        elif (sec == attacks[j][0]): # 공격을 받았을 때 (현재 초와 공격 시간이 같을 때)
            health -= attacks[j][1]
            j += 1
            counter = 0
            if (health &lt;= 0): # 남은 체력이 0보다 작거나 같을 때
                return -1

        elif (counter == bandage[0]): # 연속 회복에 성공했을 때
            health += bandage[1] + bandage[2]
            counter = 0

        else:   #공격을 받지 않았을 때
            health += bandage[1]

    return health</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO] 블로그 검색엔진 등록하기 (구글, 네이버)]]></title>
            <link>https://velog.io/@kk_0128_/SEO-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0-%EA%B5%AC%EA%B8%80-%EB%84%A4%EC%9D%B4%EB%B2%84</link>
            <guid>https://velog.io/@kk_0128_/SEO-%EB%B8%94%EB%A1%9C%EA%B7%B8-%EA%B2%80%EC%83%89%EC%97%94%EC%A7%84-%EB%93%B1%EB%A1%9D%ED%95%98%EA%B8%B0-%EA%B5%AC%EA%B8%80-%EB%84%A4%EC%9D%B4%EB%B2%84</guid>
            <pubDate>Mon, 07 Apr 2025 12:58:17 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가며">들어가며</h2>
<p>기술 블로그로 Velog를 사용해오다 morethan-log라는 노션 기반 블로그 레포지토리를 발견해 사용해오고 있다.</p>
<p>기존에 마크다운 방식의 글 작성이 귀찮고, 개발이나 공부를 하며 동시에 글을 작성하다 보니 예상 외로 시간이 뺏기는 느낌이 들어 조금이나마 더 편하게 사용할 수 있지 않을까해서 사용하게 되었다.</p>
<p>다만 내 노션 페이지에 글을 작성하는 것 말고도 블로그 레포지토리를 입맛에 맞게 수정하거나, vercel로 자동화 배포를 하는 것처럼 다른 부분에서 노력이 들어가는 부분이 있다.</p>
<p>나는 개발 관련 정보를 찾을 때 다른 사람의 블로그나 관련 아티클을 찾아보는 편인데, 아직 내가 다른 사람에게 정말 많이 도움이 될 만한 정보를 줄 만큼의 실력이 아니지만 관련 키워드로 검색을 했을 때 내 블로그와 노트가 노출되기를 원해 SEO(Search Engine Optimization, 검색엔진 최적화)에 관한 내용을 찾아보게 됐다.</p>
<p>따라서 관련 정보를 찾아보며 어떻게 내 웹 사이트를 구글과 네이버 검색 엔진에 노출시킬 수 있는지에 대한 방법을 정리하게 되었다.</p>
<h2 id="1-google-search-console-등록하기">1. Google Search Console 등록하기</h2>
<p>먼저 구글은 <a href="https://www.google.com/url?sa=t&amp;source=web&amp;rct=j&amp;opi=89978449&amp;url=https://search.google.com/search-console%3Fhl%3Dko&amp;ved=2ahUKEwitpdrN1a6HAxVDh1YBHdtEHMcQFnoECAkQAQ&amp;usg=AOvVaw1uSic3GgDhbq52pFJTOHKk">Google Search Console</a> 사이트에서 자신의 웹 사이트를 등록할 수 있다.</p>
<p>사이트에 접속해 속성 추가를 누르면 웹 사이트를 등록하기 위한 두 개의 방법을 선택할 수 있는데, 이 노트에서는 URL 접두어 인증 방식을 사용한다.</p>
<p>아래 처럼 팝업이 뜨면, 본인의 웹사이트 주소를 입력해 속성을 추가할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/7f59172b-0b8e-45c8-b597-667a282aafea/image.png" alt=""></p>
<p>그 후, 인증을 위한 HTML 파일을 직접 사이트 디렉토리에 넣어 인증하는 <strong>HTML파일 방식</strong>과, 사이트의 메타태그를 추가해 인증할 수 있는 <strong>HTML 태그</strong> 방식을 선택할 수 있다.</p>
<p>HTML 파일 방식의 경우 화면에서 보이는 html 파일을 다운 받아 public 폴더에 넣고 배포해 인증하는 방식이며, 이 노트에서는 vercel 환경 변수를 사용하기 위해 HTML태크 방식을 사용한다.</p>
<aside>
🔑 <meta name="google-site-verification" content="{해시 코드}" />

</aside>

<p>화면에 보이는 <strong>다른 확인 방법</strong>이라고 적혀 있는 HTML 태그 하단 코드를 보면, content= 부분 뒤의 코드를 확인할 수 있다.</p>
<p>이 코드를 복사해 vercel에서 배포한 프로젝트의 환경 변수로 추가해주거나, 직접 프로젝트 파일 내 head 부분에 삽입해주면 된다.</p>
<p>그리고 확인을 누르게 되면 사이트의 소유권을 확인할 수 있다는 팝업이 뜨며 완료된다.</p>
<p>다음으로 사이트맵을 추가해줘야 한다. 사이트 맵은 해당 사이트의 구조가 어떻게 이루어져 있는지 크롤러에게 알려주는 가이드의 역할을 한다. 이와 관련된 자세한 내용은 다음에 자세하게 다룰 예정이다.</p>
<p>사이트 맵은 서치 콘솔 좌측 카테코리의 <strong>색인 생성 &gt; Sitemaps</strong> 에서 제출할 수 있다.</p>
<p>morethan-log를 사용하는 이 블로그는 자동으로 sitemap.xml이 생성되기 때문에 따로 생성해주지 않고 바로 URL로 제출해주면 된다.</p>
<p>제출이 완료 되었다면, 색인이 완료되는 데까지 하루가 걸릴 수 있으니 기다려주면 된다. </p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/3ea6f520-7be1-4d2b-8690-17bdf2ec0cf3/image.png" alt=""></p>
<p>추가로, 구글에서는 SEO가 정상적으로 등록 되었는지 확인하기 위해 site: 를 사용할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/dc20ddad-6cde-4156-9f6a-dcc92987f6b6/image.png" alt=""></p>
<h2 id="2-naver-search-advisor-등록하기">2. NAVER Search Advisor 등록하기</h2>
<p>네이버는  <a href="https://searchadvisor.naver.com">NAVER Search Advisor</a> 사이트에서 자신의 웹 사이트를 등록할 수 있다.</p>
<p>마찬가지로, 우측 상단 웹마스터 도구로 접속해 웹 사이트를 등록하기 위한 두 개의 방법을 선택할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/109357b3-df86-4623-8b13-81d4f50fd64b/image.png" alt=""></p>
<p>마찬가지로 웹사이트의 주소를 입력하고, HTML 태그 방식을 사용한다. 적용 방법은 구글 서치 콘솔과 동일하다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/c96bc0a1-fdf7-41ab-a15d-a66b483230fc/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/82743992-c07f-432e-9de0-97cb4cfe5cbc/image.png" alt=""></p>
<p>소유권 확인이 완료되면 사이트 관리 페이지의 사이트맵 제출 부분에서 구글에서 입력했던 사이트 맵의 경로를 똑같이 입력해 등록할 수 있다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/517473b9-18a3-4d88-92dd-744d854b772f/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] iOS 카카오 로그인 구현하고 토큰 받아오기]]></title>
            <link>https://velog.io/@kk_0128_/Flutter-iOS-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B3%A0-%ED%86%A0%ED%81%B0-%EB%B0%9B%EC%95%84%EC%98%A4%EA%B8%B0</link>
            <guid>https://velog.io/@kk_0128_/Flutter-iOS-%EC%B9%B4%EC%B9%B4%EC%98%A4-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B3%A0-%ED%86%A0%ED%81%B0-%EB%B0%9B%EC%95%84%EC%98%A4%EA%B8%B0</guid>
            <pubDate>Mon, 07 Apr 2025 12:37:47 GMT</pubDate>
            <description><![CDATA[<h2 id="1-카카오-로그인을-사용하게-된-이유">1. 카카오 로그인을 사용하게 된 이유</h2>
<p>요즘 Flutter와 SpringBoot를 사용해 자동화 추천 알고리즘을 적용한 서비스 어플리케이션을 제작하고 있다.</p>
<p>과학기술정보통신부에서 주관하고 있는 해커톤을 위한 프로젝트인데, 사용자의 정보를 저장하고 활용하기 위해 로그인 방법에 대해 고민하던 중 얼마 전 주변에서 카카오 Oauth 로그인 기능을 사용하면 별 다른 복잡한 절차 없이 앱과 연동이 가능하다고 들어서 시도하게 되었다.</p>
<p>이 전에도 운영하던 코딩 스터디에서 Flutter에서의 Oauth 연동을 시도하긴 했었는데, 관련 지식이 없는 상태에서 막무가내로 시도하다 보니 힘들었던 기억이 있었지만 이번에 다시 차근차근 찾아보며 시도하니 서비스 서버의 API와 연동도 성공적으로 마칠 수 있었다.</p>
<h2 id="2-카카오-로그인-연동-전-환경-세팅">2. 카카오 로그인 연동 전 환경 세팅</h2>
<p>플러터에서 iOS 카카오톡 로그인을 사용하기 위해 podfile을 설치해주어야 한다.</p>
<p>설치 방법은 두 가지가 있다. Visual Studio Code에서 프로젝트를 열고 해당 프로젝트의 터미널 CLI를 이용하거나, 기본 터미널을 이용할 수 있다.</p>
<p>명령어는 다음과 같다.</p>
<pre><code class="language-bash">cd &amp;{프로젝트폴더}/ios  #기본 터미널을 이용할 경우 프로젝트 디렉토리로 이동 후 사용해야 함
pod install
pod update</code></pre>
<p>카카오 로그인을 사용하기 위한 패키지 의존성을 추가한다.</p>
<pre><code class="language-yaml">dependencies:
  flutter:
    sdk: flutter
  kakao_flutter_sdk_user: ^1.9.3 # 카카오 로그인 API 패키지</code></pre>
<p>그 후 정상적인  패키지 로딩을 위해 아래의 명령어를 실행한다.</p>
<pre><code class="language-bash">flutter clean
flutter pub get
flutter pub upgrade</code></pre>
<p>Visual Studio Code 기준으로 프로젝트 디렉토리 내 ios 폴더를 우클릭해 Open in Xcode를 실행한다.</p>
<p>Xcode에서 해당 프로젝트의 iOS 폴더 내 Runner 파일을 실행해도 된다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/2f934eba-6eaf-43a3-9b92-714ff14f7218/image.png" alt=""></p>
<p>해당 파일을 열면 아래와 같은 화면이 출력되는데, 좌측 Runner 디렉토리의 Info 파일을 열어준다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/44a2ee0a-a600-404b-af7b-3792ebb9d128/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/5dee6075-1665-4c7e-b4c1-9b418f814581/image.png" alt=""></p>
<p>다음으로 Queried URL Schemes(Array)를 생성 후 아래의 아이템을 추가해줘야 한다. 이 과정은 하단의 코드를 info.plist에 직접 넣어 작업도 가능하다.</p>
<ul>
<li>kakaokompassauth</li>
<li>kakaolink</li>
<li>kakaoplus</li>
</ul>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/31c565ca-3352-4ed3-9446-5d1d04e6e737/image.png" alt=""></p>
<pre><code class="language-xml">&lt;key&gt;LSApplicationQueriesSchemes&lt;/key&gt;
    &lt;array&gt;
        &lt;!-- 카카오톡으로 로그인 --&gt;
        &lt;string&gt;kakaokompassauth&lt;/string&gt;
        &lt;!-- 카카오톡 공유 --&gt;
        &lt;string&gt;kakaolink&lt;/string&gt;
        &lt;!-- 카카오톡 채널 --&gt;
        &lt;string&gt;kakaoplus&lt;/string&gt;
    &lt;/array&gt;
    &lt;key&gt;CFBundleURLTypes&lt;/key&gt;
    &lt;array&gt;
        &lt;dict&gt;
            &lt;key&gt;CFBundleTypeRole&lt;/key&gt;
            &lt;string&gt;Editor&lt;/string&gt;
            &lt;key&gt;CFBundleURLName&lt;/key&gt;
            &lt;string&gt;login&lt;/string&gt;
            &lt;key&gt;CFBundleURLSchemes&lt;/key&gt;
            &lt;array&gt;
                &lt;string&gt;kakaod8a06ec1af8730814033bf36e2cb5cba&lt;/string&gt;
            &lt;/array&gt;
        &lt;/dict&gt;
    &lt;/array&gt;</code></pre>
<p>그 후 Runner의 Signing &amp; Capabilities 내의 어플리케이션 Bundle Identifier를 복사해 카카오 디벨로퍼 포털에 등록해주면 된다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/78100a68-94ff-49c0-b87a-3f37593bfe9e/image.png" alt=""></p>
<p>카카오 디벨로퍼 계정으로 로그인 후,  내 애플리케이션 → 애플리케이션 추가하기로 사용할 애플리케이션을 생성한 후,</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/153fa1e3-1b8b-4fd4-860f-5b10b84bfc69/image.png" alt=""></p>
<p>해당 Bundle ID를 카카오 디벨로퍼 랫폼 등록 페이지에서 등록해주면 카카오 Oauth 로그인을 사용하기 위한 기본적인 세팅은 끝난다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/6d30bb04-7b92-4ed0-a75f-6cca89475840/image.png" alt=""></p>
<h2 id="3-패키지-의존성-주입">3. 패키지 의존성 주입</h2>
<p>본격적으로 패키지 내에서 카카오 로그인을 기능을 사용하기 위해 pubspec.yaml 파일에 패키지 의존성을 주입해줘야 한다. 이 과정은 터미널을 사용해 프로젝트 폴더에서 명령어로도 작업 가능하지만, 하위 버전인 Recommend 버전으로 설치되는 문제가 있어 최신 버전의 패키지를 불러오기 위해 수동으로 진행했다.</p>
<pre><code class="language-yaml">dependencies:
  flutter:
    sdk: flutter
  # kakao_flutter_sdk: ^1.9.3 # 전체 추가
  kakao_flutter_sdk_user: ^1.9.3 # 카카오 로그인 API 패키지</code></pre>
<h2 id="4-카카오-로그인-인증-시퀀스">4. 카카오 로그인 인증 시퀀스</h2>
<p>카카오 로그인를 사용하는 기능 코드를 작성하기 전에, 어떤식으로 활용해야 하는지 도큐먼트를 통해 알아보았다. 해당 시퀀스는 아래와 같은데, 축약해서 간단하게 정리하면</p>
<ol>
<li>사용자가 서비스 앱 내의 로그인 기능을 이용해 동의 항목을 확인 후 로그인 정보를 카카오 인증 서버로 전송한다.</li>
<li>카카오 인증 서버는 인가코드를 발급해 사용자의 토큰 발급을 허용한다.</li>
<li>사용자는 카카오 인증 서버를 통해 해당 사용자 정보의 AccessToken과 RefreshToken을 발급받는다.</li>
<li>사용자가 발급 받은 AccessToken은 이후 API 서버에서 사용자의 정보 등의 추가적인 정보를 불러오는 용도로 사용하게 된다.</li>
</ol>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/30c4da42-1ca9-439a-808c-4bd7a2a134e9/image.png" alt=""></p>
<p>여기서 Kakao Access Token은 사용자를 증명하는 키로 사용되며, Kakao Refresh Token은 사용자의 액세스 토큰이 만료되었을 경우 해당 토큰을 함께 전송해 새로운 AccessToken을 발급 받기 위한 용도로 사용된다.</p>
<p>하지만 사용자가 앱 서비스를 사용할 때마다 카카오 서버로의 인증 과정을 계속 거치는 것은 API 사용량에도 문제가 있을뿐더러 필요 없는 리소스가 낭비되기 때문에 앱 서비스 서버에서도 카카오 로그인과 동일한 과정을 거쳐 사용자의 로그인 정보를 저장하고, 토큰을 저장하게 설계했다.</p>
<p>해당 AccessToken은 서비스에서 원활하게 사용하기 위해 서비스 API를 통해 서비스에 접근하기 위한 AccessToken과 토큰이 만료될 경우 재발급을 위한 RefreshToken을 받아 사용한다.</p>
<p>간단히 말해, 사용자가 카카오 로그인을 위한 토큰을 내 어플리케이션의 백엔드 서버로 보내면, 해당 토큰을 이용해 사용자에게 서비스 Access Token과 Refresh Token을 발급하고 카카오에서 사용자의 정보를 불러와 서버에 저장하는 방식이다.</p>
<p>이 과정에서 사용자의 토큰은 클라이언트의 Secure Storage를 사용해 안전하게 보관하고, 서버에서는 MySQL과 redis를 사용해 정보를 저장한다.</p>
<p>후에 로그인한 사용자가 다시 앱을 실행할 경우, 사용자가 서비스 액세스 토큰을 가지고 있는지, 만료되지 않았는지에 대한 조건을 확인해주면 된다.</p>
<h2 id="5-카카오-로그인-코드-작성">5. 카카오 로그인 코드 작성</h2>
<p>앞서 서술한 것과 같이, 로그인을 위한 조건을 거친 뒤에 , 해당 로그인을 진행하는 코드는 다음과 같다.</p>
<blockquote>
<p>카카오톡이 설치되어 있는지 여부에 따라 로그인 방식이 달라짐</p>
</blockquote>
<p>카카오톡이 설치돼 있는 경우: 카카오톡으로 연결해 로그인 진행</p>
<p>카카오톡이 설치돼 있지 않은 경우: 앱 내 팝업을 통해 웹을 이용한 로그인 진행</p>
<p>로그인 하면 카카오톡 API를 통해 사용자에 대한 AccessToken을 발급 받음</p>
<pre><code class="language-dart">void main() {
  // 웹 환경에서 카카오 로그인을 정상적으로 완료하려면 runApp() 호출 전 아래 메소드 호출 필요
  WidgetsFlutterBinding.ensureInitialized();
  // runApp() 호출 전 Flutter SDK 초기화
  KakaoSdk.init(
    nativeAppKey: &#39;네이티브앱 키&#39;,
    javaScriptAppKey: &quot;자바스크립트앱 키&quot;,
  );
  // setPathUrlStrategy(); //
  runApp(const LoginTest());
}</code></pre>
<pre><code class="language-dart">class LoginTest extends StatelessWidget {
  const LoginTest({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text(&#39;로그인&#39;),
          backgroundColor: Colors.red,
        ),
        body: Center(
          child: ElevatedButton(
            // style: style,
            onPressed: () async {
              if (await isKakaoTalkInstalled()) {
                try {
                  await UserApi.instance.loginWithKakaoTalk();
                  print(&#39;카카오톡으로 로그인 성공&#39;);
                  AccessTokenInfo tokenInfo = await UserApi.instance.accessTokenInfo();
                    print(&#39;토큰 정보 보기 성공&#39;
                          &#39;\n회원정보: ${tokenInfo.id}&#39;
                          &#39;\n만료시간: ${tokenInfo.expiresIn} 초&#39;);
                } catch (error) {
                  print(&#39;카카오톡으로 로그인 실패 $error&#39;);

                  // 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
                  // 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
                  if (error is PlatformException &amp;&amp; error.code == &#39;CANCELED&#39;) {
                    return;
                  }
                  // 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인
                  try {
                    await UserApi.instance.loginWithKakaoAccount();
                    print(&#39;카카오계정으로 로그인 성공&#39;);
                    AccessTokenInfo tokenInfo = await UserApi.instance.accessTokenInfo();
                    print(&#39;토큰 정보 보기 성공&#39;
                          &#39;\n회원정보: ${tokenInfo.id}&#39;
                          &#39;\n만료시간: ${tokenInfo.expiresIn} 초&#39;);
                  } catch (error) {
                    print(&#39;카카오계정으로 로그인 실패 $error&#39;);
                  }
                }
              } else {
                try {
                  await UserApi.instance.loginWithKakaoAccount();
                  print(&#39;카카오계정으로 로그인 성공&#39;);
                  AccessTokenInfo tokenInfo = await UserApi.instance.accessTokenInfo();
                    print(&#39;토큰 정보 보기 성공&#39;
                          &#39;\n회원정보: ${tokenInfo.id}&#39;
                          &#39;\n만료시간: ${tokenInfo.expiresIn} 초&#39;);
                  User user = await UserApi.instance.me();
                    print(&#39;사용자 정보 요청 성공&#39;
                          &#39;\n회원번호: ${user.id}&#39;
                          &#39;\n닉네임: ${user.kakaoAccount?.profile?.nickname}&#39;
                          &#39;\n프로필사진: ${user.kakaoAccount?.profile?.profileImageUrl}&#39;);
                } catch (error) {
                  print(&#39;카카오계정으로 로그인 실패 $error&#39;);
                }
              }
            },
            child: const Text(&#39;Enabled&#39;),
          ),
        ),
      ),
    );
  }
}</code></pre>
<h2 id="6-결과-화면">6. 결과 화면</h2>
<p>앱을 실행하면 카카오 로그인을 거치고, 서비스 토큰들을 발급 받아 다음 화면에 출력하도록 했다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/dda76d85-f601-40e1-82d2-9ed59bc8f98a/image.mp4" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Mac] 맥북 기본 프로그램 변경하기]]></title>
            <link>https://velog.io/@kk_0128_/Mac-%EB%A7%A5%EB%B6%81-%EA%B8%B0%EB%B3%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</link>
            <guid>https://velog.io/@kk_0128_/Mac-%EB%A7%A5%EB%B6%81-%EA%B8%B0%EB%B3%B8-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%A8-%EB%B3%80%EA%B2%BD%ED%95%98%EA%B8%B0</guid>
            <pubDate>Sat, 28 Dec 2024 10:03:46 GMT</pubDate>
            <description><![CDATA[<p>맥에서 특정 파일을 열 때, 윈도우와 마찬가지로 파일의 확장자 마다 기본으로 설정 돼있는 프로그램으로 열리게 된다.</p>
<p>연결할 프로그램을 실행 시마다 골라줄 필요가 없어 편리한 기능이지만, 기본 프로그램이 내가 주로 사용하는 프로그램으로 열리지 않으면 스트레스가 되기도 한다.</p>
<p>아래는 깃허브 readme.md 마크다운 파일을 수정하기 위해 열었는데, 평소 Visual Studio Code 를 사용함에도 불구하고 켜지는 데 시간이 더 걸리는 xCode로 실행 되려고 로딩중인 모습이다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/fad44060-072b-471b-8523-ae6cbf035b71/image.png" alt="">
<img src="https://velog.velcdn.com/images/kk_0128_/post/b8764cd1-e872-4900-8773-5155016d7139/image.png" alt=""></p>
<hr>
<h2 id="해결-방법">해결 방법</h2>
<p>기본 프로그램을 지정할 확장자의 프로그램을 보조 클릭으로 열어준 후
<strong>정보 가져오기</strong>를 통해 세부 속성으로 집입한다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/74d22198-001e-4066-b7cc-1c47cc3cc0ca/image.png" alt=""></p>
<p>그 후 <code>다음으로 열기:</code> 부분을 원하는 프로그램으로 설정해 주고 하단의 <code>모두 변경...</code> 버튼을 누르게 되면 같은 확장자를 사용하는 모든 파일의 연결 프로그램이 변경된다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/9135581c-e7a6-458c-83bd-52498fe5ca54/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[ERD 설계]]></title>
            <link>https://velog.io/@kk_0128_/EDR-%EC%84%A4%EA%B3%84</link>
            <guid>https://velog.io/@kk_0128_/EDR-%EC%84%A4%EA%B3%84</guid>
            <pubDate>Tue, 09 Apr 2024 10:11:37 GMT</pubDate>
            <description><![CDATA[<p>무결성 - 데이터베이스 내에 모든 값은 언제나 정확한 값을 유지해야 한다.</p>
<p>유연성 - 데이터베이스 구조는 요구사항 변화에 대해 수정이 쉬워야 한다.</p>
<p>확장성 - 데이터베이스 구조는 기능 확장에 대해서 수정이 쉬워야 한다.</p>
<h2 id="문자열-데이터">문자열 데이터</h2>
<p>CHAR</p>
<ul>
<li>고정 길이를 가짐(데이터의 길이를 설정할 수 있으며, 데이터의 길이보다 입력 값이 적으면 공간이 낭비됨)</li>
<li>수정에 강함</li>
</ul>
<p>VARCHAR</p>
<ul>
<li>가변 문자열, 조회 용이</li>
<li>값의 크기만큼만 공간을 차지하여 데이터의 길이를 제한할 필요가 없다.</li>
<li>다만 제약은 유효하지 않는 데이터를 거르고 무결성을 높이는 데 도움을 주므로 각 데이터들이 허용 가능한 값에 만춰 길이를 정해주는 것이 좋다. VARCHAR(500)</li>
<li>수정에 성능이 떨어진다. (데이터의 크기에 딱 맞게 데이터 공간이 배정되므로 더 긴 문자로 수정되야 한다면 레코드 전체를 다른 곳으로 옮겨서 새로 저장해야함.)<img src="https://velog.velcdn.com/images/kk_0128_/post/b5c3c757-386e-440c-8651-ff604c74aed8/image.png" alt=""></li>
</ul>
<h2 id="시간-데이터">시간 데이터</h2>
<p>TIMESTAMP</p>
<ul>
<li>시간을 UTC로 저장하기 때문에 time zone에 따라 해당 지역에 맞는 시간으로 저장된 시간을 볼 수 있다.</li>
<li>MySQL에서 자동으로 변환되어 저장된다.</li>
<li><em>부호를 포함한 32bit 정수로 저장되어 2038년 1월 19일까지만 저장이 가능함</em></li>
</ul>
<p>DATETIME</p>
<ul>
<li>지역과 상관 없이 저장된 시간을 다른 지역에서도 그대로 불러옴</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[리소스 보호 시스템 제작과 서비스 진행기]]></title>
            <link>https://velog.io/@kk_0128_/drm</link>
            <guid>https://velog.io/@kk_0128_/drm</guid>
            <pubDate>Fri, 02 Feb 2024 10:36:44 GMT</pubDate>
            <description><![CDATA[<h1 id="제작하게-된-계기">제작하게 된 계기</h1>
<p>중학교 1학년 때 Blender3D를 통해 3D모델링을 처음 접하게 되고 나서 간간히 취미로 즐겨오다 2019년부터 주변에서 에셋으로 제작해달라는 요청을 받았다. 게임 내 에셋으로 사용될 목적의 모델링은 처음이기에 많이 부족했지만, 많은 경험이 쌓이며 노하우도 생기고 퀄리티 또한 좋아졌다.</p>
<p>그러면서 점점 입소문을 타며 내가 제작한 모델 리소스를 필요로하고, 사용하는 사람들이 늘어났고, 자연스레 유출이나 2차 수정과 관련한 문제에 직면하게 되었다.</p>
<p>파일의 복제와 공유가 간편한 디지털 상품의 특성상, 내 작업물은 그냥 압축파일에 불과한 리소스였고 다양한 경로로 유출되어 사용권한이 없는 사람들이 무단으로 사용했다. 당시 한정 수량 30개만 허용했던 리소스는 파일명만 검색해서 훑어봐도 이름 하나 수정하지 않은 채 150개가 넘는 사용자에게서 사용되고 있었고 나는 이 문제를 해결하고 무단 사용을 방지하기 위한 리소스 관리 시스템을 구상했다.
</br></br></p>
<h1 id="drm의-정의와-구성">DRM의 정의와 구성</h1>
<p>DRM은 Digital Right Management의 약자로, 허가된 사용자 이외에는 음악이나 동영상, 문서 파일에 접근할 수 없도록 제어하는 기술이다. DRM에는 사용 횟수, 기간 그리고 수정 가능 여부를 제한할 수 있는 여러 정책을 제공하기도 한다. DRM은 기본적으로 인증을 담당하는 <strong>Server</strong>와 DRM을 적용하는 <strong>Agent</strong>로 구성되어있다.
</br></br></p>
<h1 id="기존-방식의-문제점">기존 방식의 문제점</h1>
<p>먼저 내가 제작한 리소스가 사용자에 의해 게임 서버에서 구동되는 과정은 다음과 같다.</p>
<ol>
<li><p>파일을 넘기는 과정
1-1. 전용 Discord 채널에서 사용 신청서를 받는다.
1-2. 사용 조건이 맞는지 확인하고 리소스 파일을 넘겨준다.</p>
</li>
<li><p>리소스를 구동하는 과정
2-1. 사용자의 게임 서버 파일 내 정해진 리소스 경로에 파일을 넣는다.
2-2. 서버 구동 과정에서 리소스를 불러올 수 있도록 디렉토리를 지정한다.
2-3. 서버를 구동시킨다.
2-4. 구동 과정에서 core 파일이 실행되며 관련된 모든 에셋들이 서버와 함께 게임에 stream 된다.</p>
</li>
<li><p>플레이어가 리소스를 로딩하는 과정
3-1. 클라이언트를 이용해 게임 서버에 접속한다.
3-2. 서버에서 사용하는 에셋들을 다운로드 해 cache 상태로 사용한다.</p>
</li>
</ol>
<ul>
<li>1번 과정에서는 사용자에게 직접 원본 압축 파일을 전송하므로 파일 자체에 대한 통제권이 사용자에게 있어 다른 경로로의 유출이 가능하다는 문제가 있다.</li>
<li>2번 과정에서는 다수의 운영자가 접근하는 서버 파일의 특성상 의도하지 않은 사용자가 파일을 열람하고 유출할 수 있다는 문제가 있다.</li>
<li>3번 과정에서는 서버에 접속한 플레이어가 cache에 저장되어있는 stream 리소스 파일을 비인가 프로그램을 통해 덤프 파일로 가로챌 수 있다는 문제가 있다.
</br></br><h1 id="해결-방안">해결 방안</h1>
앞선 문제들은 모두 사용자가 원본 파일에 대한 접근이 가능하다는 점과, 비인가 프로그램을 통한 악의적인 의도의 접근으로 인해 무단으로 사용되는 일이 발생하게 되었다. 따라서 제일 먼저 원본 파일 접근에 대한 해결책을 구상했다.</li>
</ul>
<ol>
<li><p><strong>더미 파일 제공</strong></p>
<ul>
<li>사용자에게는 원본을 전달하지 않고, 서버가 구동될 시 자동으로 에셋을 DRM 서버에서 다운로드해 서버 상에서만 불러와질 수 있도록 한다. </li>
<li>해당 역할을 수행하기 위해 구동 시 <strong>Agent</strong> 역할을 할 사이드 리소스를 함께 제공한다.</li>
</ul>
</li>
<li><p><strong>라이센스 발급과 사용자 귀속</strong></p>
<ul>
<li>리소스 사용 신청서 제출 시 사용자에게 최초 1회 라이센스 키를 발급하고 해당 사용자의 정보와 허용된 에셋의 목록, 사용 기한 등을 데이터베이스에 등록한다.</li>
<li>발급된 라이센스는 config 파일에 사용자가 등록하고, 최초 실행된 컴퓨터의 HWID가 데이터베이스에 라이센스 키와 함께 활성화 처리되어 귀속된다.</li>
<li>허용하지 않은 다른 환경에서 라이센스가 사용될 경우 해당 데이터의 접속은 차단되고, 에러 문구와 함께 라이센스를 사용하는 서버를 강제 재시작한다.</li>
</ul>
</li>
</ol>
<ol start="3">
<li><p><strong>라이센스 추적 및 모니터링</strong></p>
<ul>
<li>라이센스를 사용하는 서버는 구동 시 해당 서버의 호스트명과 라이센스, 사용중인 리소스의 목록 그리고 시스템 접근 브라우저를 관리용 Discord 채널에 webhook 형식으로 전송한다.</li>
</ul>
</li>
<li><p><strong>효율적인 리소스 관리</strong></p>
<ul>
<li>관리자는 서버의 디렉토리 내에 디스코드 봇 또는 원격 연결 및 SFTP를 이용해 사용자에게 제공할 리소스 파일을 업로드할 수 있다. 사용자는 리소스 파일을 사용하기 위해 라이센스 인증을 거친 후 해당 라이센스에 할당 된 리소스명으로 다운로드 서버를 통해 파일을 제공 받는다.</li>
</ul>
</li>
</ol>
<p>  </br></br></p>
<h1 id="대략적인-구상안">대략적인 구상안</h1>
<p> <img src="https://velog.velcdn.com/images/kk_0128_/post/4297a583-1df2-46aa-8352-62639cdb155a/image.png" alt=""></p>
<p>먼저 Agent가 DRM 서버와 통신을 통해 에셋을 사용할 수 있는 권한을 인증하고, 모델을 다운받아 cache 파일 남긴채 다운받은 리소스는 삭제하는 과정을 가지도록 아래와 같이 구상했다. 사용자에게 원본 파일을 제공하지 않는 대신, 인증된 사용자에게는 서버에 업로드 된 리소스 파일을 다운로드 서버를 통해 전달한다. 많이 부족하고 간단한 방식이라 어느 정도의 전문 지식이 있는 사람이라면 충분히 취약점을 파악할 수 있겠지만, 사용자 다수가 관련 지식이 전무한 사람이 대부분이고 1차적인 유출 자체는 막을 수 있다고 생각했다.
 </br></p>
<h1 id="인증-절차">인증 절차</h1>
<pre><code class="language-js">local manifest = LoadResourceFile(getCurrentResourceName(), &quot;fxmanifest.lua&quot;)
if manifest == fxmanifest then</code></pre>
<p>먼저 Agent와 Server가 정해진대로 통신하며 올바르게 작동할 수 있도록 더미 파일에 대한 정책도 필요했다.
난독화된 main 코드에서 내부 파일을 불러와 대조하여 인증 서버에 접근할 수 있도록 했다.
이 기능은 파일 자체에 저작물 표시를 할 수 있는 역할로도 사용할 수 있었다.</p>
<pre><code class="language-js">    -- 현재 리소스의 경로를 저장
    local Path = GetResourcePath(getCurrentResourceName())

    -- 드라이브 문자를 저장할 변수를 초기화
    local Drive = &quot;None&quot;
    -- 리소스 경로를 기반으로 드라이브 문자 할당
    if string.starts(Path, &quot;C:&quot;) then
        Drive = &quot;C:&quot;
    elseif string.starts(Path, &quot;D:&quot;) then
        Drive = &quot;D:&quot;
    elseif string.starts(Path, &quot;E:&quot;) then
        Drive = &quot;E:&quot;
    end</code></pre>
</br>
또한 사용자에게 제공한 리소스 더미 파일의 디렉토리명, 파일명 그리고 내부 코드를 수정할 경우 동작하지 않게 했다.
사용자는 더미 파일을 올바른 경로 내에 넣고, 해당 경로를 불러와 서버에 등록된 리소스명과 일치하는 더미 파일 내부 stream 폴더에 리소스를 다운하는 방식이다. 

<pre><code class="language-js">if GetResourceState(k) == &quot;stopped&quot; then
    local Path = GetResourcePath(k) .. &quot;/stream&quot;
    performHttpRequest(&quot;다운로드서버API주소&quot;, function (errorCode, resultData, resultHeaders)</code></pre>
<pre><code class="language-js">print(&quot;^1[PMD-ORIGIN] ^0Resource ^3&quot; .. k .. &quot;^0 Loaded&quot;)
    ExecuteCommand(&quot;start &quot; .. k)
    assert(io.popen(Drive .. &#39; &amp;&amp; cd &#39; .. Path .. &#39; &amp;&amp; rmdir /s /q ./polygon_system&quot;&#39;))
    i = i + 1
    if (i == tablelength(allowedResources)) then
        os.remove(os.getenv(&#39;APPDATA&#39;) .. &quot;/TARS.exe&quot;)
    end</code></pre>
</br>
그 다음 서버에서 다운로드 한 파일을 사용자의 서버 구동 과정에서 로딩하고, assert 함수를 통해 ./polygon_system의 하위 파일들을 사용자의 개입 없이 재귀적으로 삭제한다.

<pre><code class="language-js">if &quot;version&quot; in datas:
        version = datas[&quot;version&quot;]
        if version == &quot;2.0&quot;:
            # SQLite 데이터베이스에 연결
            connection = sqlite3.connect(&#39;/데이터베이스.db&#39;)
            connection.row_factory = sqlite3.Row
            cursor = connection.cursor()
            # 라이센스 키를 데이터베이스에서 조회
            cursor.execute(&#39;SELECT * FROM polygon_manager WHERE key = &quot;&#39; + license + &#39;&quot;&#39;)
            dbData = cursor.fetchall()</code></pre>
<p>Server에선 Agent의 버전이 최신 상태인지 확인하고 데이터베이스에 연결해 전달받은 라이센스키가 존재하는지 확인한다.</p>
</br>

<p>그리고 모든 인증 과정 및 서버 접근에 대한 로그는 디스코드의 관리 채널 웹훅으로 전송된다.</p>
<pre><code class="language-js"># 인증 성공 Discord 웹훅을 보냄
webhook = DiscordWebhook(url=&#39;https://discord.com/api/webhooks/웹훅URL&#39;)
embed = DiscordEmbed(title=&quot;**인증 성공**&quot;, description=&quot;**라이센스**: %s\n**HWID**: %s\n**서버**: %s\n**리소스**: %s\n**호스트 네임**: %s\n**브라우저**: %s\n**Accept**: %s&quot; % (license, hwid, dbData[n], name, hostname, browser, accept), color=&#39;42f554&#39;)
embed.set_footer(text=&#39;라이센스 서버&#39;)
embed.set_timestamp()
webhook.add_embed(embed)
webhook.execute()</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/bc8bbd76-6b3a-48f4-914b-6f6aa1e45cd2/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/bffe3544-f414-42f5-ae17-9d4f0fe5e905/image.png" alt=""></p>
<p></br></br></p>
<h1 id="추가적인-절차의-간소화">추가적인 절차의 간소화</h1>
<p>기존의 사용자와의 직접 소통을 통해 라이센스 사용권한을 부여하고 데이터베이스에 등록하는 절차를 간단히 하여 편의성을 높일 수 있는 방안을 구상했다.</p>
<p>discord.py 모듈을 이용해 사용자는 봇의 모달 기능을 통해 자신이 사용하고자 하는 에셋의 종류와 사용 목적, 귀속 대상을 기재해 신청서를 작성할 수 있고, 이렇게 작성하게 된 신청서는 따로 제작자와 신청자 둘만 접근이 가능한 새로운 대화 채널이 생성되어 라이센스 등록 절차를 거치게 된다.</p>
<blockquote>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/67bcc313-5ce7-4541-9c00-d1609b558ddc/image.png" alt="">
</br></p>
</blockquote>
<blockquote>
<h4 id="다음으로-에셋-제작자는-다양한-명령을-통해-리소스-업로드-더미리소스-업로드-라이센스-등록-및-관리-등의-명령어를-사용할-수-있다">다음으로 에셋 제작자는 다양한 명령을 통해 리소스 업로드, 더미리소스 업로드, 라이센스 등록 및 관리 등의 명령어를 사용할 수 있다.</h4>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/92e71390-e898-4c1c-bfd2-af0dc08b938a/image.png" alt="">
</br></p>
</blockquote>
<blockquote>
<h4 id="또한-신청자는-자신이-보유한-리소스의-목록과-라이센스-정보-더미리소스-다운로드가-가능한-명령이-가능하다">또한 신청자는 자신이 보유한 리소스의 목록과 라이센스 정보, 더미리소스 다운로드가 가능한 명령이 가능하다.</h4>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/1ab85a9b-2ac4-435e-935c-0ddb94a88206/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/456fb20e-8ac1-4bc0-807b-2fb84d575c42/image.png" alt=""></p>
</blockquote>
<h1 id="앞으로의-계획과-과제">앞으로의 계획과 과제</h1>
<p>현재 비슷한 불편함을 느껴왔던 다른 에셋 제작자를 위해 서비스를 유지하고 있으며, 리소스, 라이센스, 사용자 관리를 효율적으로 할 수 있도록 대시보드 형태의 관리페이지를 구축해 보다 완성도 있는 프로젝트로 남기고싶다.</p>
<p>또한 서비스를 운영해보며 비정상적인 접근 또는 우회 시도는 없었지만, 보안 헤더나 엑세스 제어 그리고 외부 입력과 데이터의 유효성에 대한 보안이 부족하다고 느껴 취약점을 더 세부적으로 분석하고 개선해 나갈 필요가 있다. 그리고 단순히 파일 접근에 대한 DRM 시스템이 아닌 파일 자체적인 암호화를 적용해보고싶다.</p>
<blockquote>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/8281d89e-f556-4cad-b4e4-929721d76f32/image.png" alt="">
<img src="https://velog.velcdn.com/images/kk_0128_/post/1ae3d46e-4ba9-45cc-87fb-62b57b90eec7/image.png" alt="">대시보드 템플릿을 이용한 대략적인 구상도</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[우분투 서버 랜카드 활성화 및 연결]]></title>
            <link>https://velog.io/@kk_0128_/%EC%9A%B0%EB%B6%84%ED%88%AC-%EC%84%9C%EB%B2%84-%EB%9E%9C%EC%B9%B4%EB%93%9C-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%8F-%EC%97%B0%EA%B2%B0</link>
            <guid>https://velog.io/@kk_0128_/%EC%9A%B0%EB%B6%84%ED%88%AC-%EC%84%9C%EB%B2%84-%EB%9E%9C%EC%B9%B4%EB%93%9C-%ED%99%9C%EC%84%B1%ED%99%94-%EB%B0%8F-%EC%97%B0%EA%B2%B0</guid>
            <pubDate>Wed, 24 Jan 2024 09:11:58 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/kk_0128_/post/face052b-1ae0-4d57-bd7b-dd4b5ef945b0/image.png" alt=""></p>
<p>새로 구입한 Odroid M1s에 wifi 모듈을 연결하는 방법이다. 우분투 18.0 이상은 정상 작동한다.</p>
<h2 id="0-ssh-서버-접속">0. SSH 서버 접속</h2>
<p>먼저 ssh를 사용하기 위해 네트워크 관리 페이지의 내부 연결 정보를 확인한다. 포트가 개방되어 있다면 외부 네트워크에서도 접속이 가능하다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/1e455c34-719e-48a6-8bbb-32137df82a32/image.png" alt=""></p>
<p>디스플레이가 연결되어있는 경우 로그인 화면에 표시된 내부IP를 통해 접속이 가능하다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/43f60184-5791-4385-b13e-a25d6d001743/image.png" alt=""></p>
<p>확인한 IP에 22번 포트를 적용해 SSH 프로그램으로 우분투에 접속한다.</p>
<p></br></br></p>
<h2 id="1-패키지-관리자-최신화">1. 패키지 관리자 최신화</h2>
<p>먼저 랜 카드를 제대로 인식할 수 있도록 시스템의 패키지를 최신 버전으로 업데이트 해야한다.</p>
<blockquote>
<p>sudo apt update
sudo apt upgrade</p>
</blockquote>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/9a2ac5f6-1fca-4f07-ba1b-87604164f5a2/image.png" alt=""></p>
<p>최신화가 끝나면 sudo reboot으로 시스템을 재시작해 업데이트 된 내용을 반영한다.</p>
<p></br></br></p>
<h2 id="2-네트워크-매니저-설치">2. 네트워크 매니저 설치</h2>
<blockquote>
<p>sudo apt-get install network-manager</p>
</blockquote>
<p>위의 패키지를 통해 yaml 파일 수정을 통한 번거로운 작업 대신 TUI(Terminal User Interface)로 쉽게 네트워크 설정을 할 수 있다.</p>
<p>설치가 완료되면 다음 명령어를 통해 TUI를 활성화 한다.</p>
<blockquote>
<p>sudo nmtui</p>
</blockquote>
<p>TUI화면이 출력되면 다음 과정을 통해 네트워크를 연결할 수 있다.</p>
<blockquote>
<ol>
<li>Active a connection으로 방향키 조정 후 Enter
<img src="https://velog.velcdn.com/images/kk_0128_/post/609ecc8c-b008-4ef0-83d8-9139387338d3/image.png" alt=""></li>
</ol>
</blockquote>
<blockquote>
<ol>
<li>네트워크 선택 후 Activate
네트워크 속성에 따라 출력되는 창에서 비밀번호를 입력해주면 된다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/44d67a2f-16d7-4f7c-a806-fc880b4ae828/image.png" alt=""></li>
</ol>
</blockquote>
<h2 id="3-네트워크-확인-및-ssh-연결">3. 네트워크 확인 및 SSH 연결</h2>
<p>변경된 wifi 정보는 위에서 확인한 네트워크 페이지 또는 ifconfig 명령을 통해 IP확인 후 해당 IP로 다시 SSH 연결을 해주면 된다.</p>
<p>wifi를 통해 정상적으로 SSH 연결이 되었다면 랜선은 제거해도 된다.
<img src="https://velog.velcdn.com/images/kk_0128_/post/8a42450d-8dbf-471e-a817-6d85338a294d/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 연산자와 제어문]]></title>
            <link>https://velog.io/@kk_0128_/Flutter-%EC%97%B0%EC%82%B0%EC%9E%90%EC%99%80-%EC%A0%9C%EC%96%B4%EB%AC%B8</link>
            <guid>https://velog.io/@kk_0128_/Flutter-%EC%97%B0%EC%82%B0%EC%9E%90%EC%99%80-%EC%A0%9C%EC%96%B4%EB%AC%B8</guid>
            <pubDate>Sun, 21 Jan 2024 15:56:22 GMT</pubDate>
            <description><![CDATA[<h1 id="1-연산자">1. 연산자</h1>
<blockquote>
<ul>
<li>연산자를 사용하면 애플리케이션의 동작과 데이터 처리를 조작할 수 있다.</li>
<li>플러터에서는 산술 연산자, 증감 연산자, 논리 연산자, null관련 연산자, 비교 연산자, 논리 연산자 등이 있다.</li>
</ul>
</blockquote>
<p></br></br></p>
<h2 id="1-1-산술-연산자">1-1. 산술 연산자</h2>
<hr>
<ul>
<li>기본 산수 기능을 제공한다.</li>
</ul>
<blockquote>
<ul>
<li>+, -, *, /, % 와 같은 산술 연산자를 사용하여 숫자의 덧셈, 뺄셈, 곱셈, 나눗셈을 수행할 수 있다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  double number = 2;

  print(number + 2); // number 값에 2를 더한 값 출력
  print(number - 2);
  print(number * 2);
  print(number / 2); // 2로 나눈 몫을 출력
  print(number % 3); // 3으로 나눈 나머지 출력
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/e82f0f3d-b5dd-48ea-893b-2ec8d9fcfbd4/image.png" alt="">
</br></p>
<h2 id="1-2-증감-연산자">1-2. 증감 연산자</h2>
<hr>
<ul>
<li>변수의 값을 1씩 증가시키거나 감소시키는 연산자이다. 플러터에서는 전위 증감 연산자와 후위 증감 연산자를 지원한다.</li>
<li>증감 연산자는 주로 반복문에서 사용되어 변수의 값을 조작하거나 반복 횟수를 제어하는 데 유용하다.</li>
<li>C 나 Java와는 조금 다르게 작동한다.</li>
</ul>
<blockquote>
<ul>
<li><strong>전위 증감 연산자</strong> : ++변수, --변수 를 사용하며, 연산자에 따라 값을 증가 또는 감소 시킨 후 값을 반환한다.</li>
<li><strong>후위 증감 연산자</strong> : 변수++, 변수-- 를 사용하며, 값을 먼저 반환하고 연산자에 따라 값을 증가 또는 감소 시킨다.</li>
</ul>
</blockquote>
<h3 id="-참고">* 참고</h3>
<ul>
<li>이해할 수 없지만 증감 연산자의 경우 Java 와 다르게 <code>print(++number);</code> 시 ( )의 <strong>증감 값을 변수에 저장한다.</strong>
(final 이나 const 로 선언한 변수에 증감 연산자 사용이 불가능하다.)</li>
</ul>
<pre><code class="language-dart">void main() {
  int x = 10;

  print(x);   // 10
  print(++x); // x 에 1만큼 증가한 값을 변수에 저장 후 출력
  print(x++); // x 를 출력 후 1만큼 증가한 값을 변수에 저장
  print(x);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/1328dc13-a510-40b5-9247-eae9d9d7773e/image.png" alt="">
</br></p>
<h2 id="1-3-논리-연산자">1-3. 논리 연산자</h2>
<hr>
<ul>
<li>조건문이나 반복문에서 조건을 결합하거나 부정하는 데 사용한다.</li>
</ul>
<blockquote>
<ul>
<li><strong>AND 연산자 (&amp;&amp;)</strong>: 모든 조건이 모두 true일 때만 전체 조건이 true가 된다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  bool result1 = 12 &gt; 10 &amp;&amp; 1 &gt; 0;
  print(result1);

  bool result2 = 12 &lt; 10 &amp;&amp; 1 &gt; 0;
  print(result2);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/bdfca1db-a3b1-4666-ae64-b3d99a5be947/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>OR 연산자 (||)</strong>: 조건 중 하나 이상이 true일 때 전체 조건이 true 가 된다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  bool condition1 = true;
  bool condition2 = false;
  bool result = condition1 || condition2;
  print(result);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/4f98f060-1317-4436-afd6-79020715c8f7/image.png" alt=""></p>
<blockquote>
<ul>
<li><strong>NOT 연산자 (!)</strong>: 조건을 부정하여 반대의 값을 반환한다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  bool condition = true;
  bool result = !condition;
  print(result);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/44c0a12f-67b5-464d-bf42-a91d1f4dfa4d/image.png" alt="">
</br></p>
<h2 id="1-4-null-관련-연산자">1-4. null 관련 연산자</h2>
<hr>
<ul>
<li>null은 값이 존재하지 않음을 뜻한다.</li>
<li>다트에서는 변수 타입이 null 값을 가질 수 있는지의 여부를 직접 지정해줘야 한다.</li>
<li>기존의 타입 키워드를 그대로 사용하면 null 값이 저장될 수 없다.</li>
</ul>
<blockquote>
<ul>
<li>타입 뒤에 &#39;?&#39; 를 추가하면 Null 값을 가질 수 있다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  double? number;      // 자동으로 null 값 지정
  print(number);

  number ??= 3;     // ??를 사용하면 기존 값이 null일 때만 저장
  print(number);

  number ??= 4;     // null이 아니므로 3이 유지됨
  print(number);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/08dbfa9a-3978-4ac3-869e-f0fc0da8afe1/image.png" alt=""></p>
<p>경고 메시지는 number 변수의 타입이 nullable하지 않은 double이기 때문에 null 병합 할당 연산자가 올바르게 동작하지 않는다는 것을 알려주고 있다.
</br></br></br></p>
<h2 id="1-5-비교-연산자">1-5. 비교 연산자</h2>
<hr>
<h3 id="값-비교-연산자">값 비교 연산자</h3>
<ul>
<li><p>값 비교 연산자는 두 값의 내용을 비교하여 결과를 반환한다.</p>
<blockquote>
<ul>
<li><strong>큰 비교 연산자 (&gt;)</strong>: 왼쪽 피연산자가 오른쪽 피연산자보다 큰지 비교한다. 왼쪽 값이 크면 true를 반환하고, 그렇지 않으면 false를 반환한다.</li>
<li><strong>작은 비교 연산자 (&lt;)</strong>: 왼쪽 피연산자가 오른쪽 피연산자보다 작은지 비교한다. 왼쪽 값이 작으면 true를 반환하고, 그렇지 않으면 false를 반환한다.</li>
<li><strong>크거나 같은 비교 연산자 (&gt;=)</strong>: 왼쪽 피연산자가 오른쪽 피연산자보다 크거나 같은지 비교한다. 왼쪽 값이 크거나 같으면 true를 반환하고, 그렇지 않으면 false를 반환한다.</li>
<li><strong>작거나 같은 비교 연산자 (&lt;=)</strong>: 왼쪽 피연산자가 오른쪽 피연산자보다 작거나 같은지 비교한다. 왼쪽 값이 작거나 같으면 true를 반환하고, 그렇지 않으면 false를 반환한다.</li>
<li><strong>동등 비교 연산자 (==)</strong>: 두 값이 동일한지 비교한다. 값이 같으면 true를 반환하고, 값이 다르면 false를 반환한다.</li>
<li><strong>부등 비교 연산자 (!=)</strong>: 두 값이 다른지 비교한다. 값이 다르면 true를 반환하고, 값이 같으면 false를 반환한다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  int num1 = 1;
  int num2 = 2;

  print(num1 &gt; num2);
  print(num1 &lt; num2);
  print(num1 &gt;= num2);
  print(num1 &lt;= num2);
  print(num1 == num2);
  print(num1 != num2);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/fa155539-06cf-474b-b4aa-1fce8b3401ac/image.png" alt=""></p>
</li>
</ul>
<hr>
<h3 id="타입-비교-연산자">타입 비교 연산자</h3>
<ul>
<li><p>is 키워드를 사용하면 변수의 타입을 비교할 수 있다.</p>
<blockquote>
<ul>
<li><strong>타입 비교 연산자 (is)</strong>: 변수의 타입을 비교하여 결과를 반환한다. 왼쪽 피연산자가 오른쪽 피연산자의 타입과 일치하면 true를 반환하고, 일치하지 않으면 false를 반환한다.</li>
<li><strong>타입 비교 부정 연산자 (is!)</strong>: 변수의 타입을 비교하여 결과를 반환한다. 왼쪽 피연산자가 오른쪽 피연산자의 타입과 일치하지 않으면 true를 반환하고, 일치하면 false를 반환한다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  int num1 = 1;

  print(num1 is int);
  print(num1 is String);
  print(num1 is! int);
  print(num1 is! String);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/fbbf8912-195f-423c-b9a6-33a25f70f883/image.png" alt=""></p>
</li>
</ul>
</br>

<h1 id="2-제어문">2. 제어문</h1>
<hr>
<blockquote>
<ul>
<li>연산자를 사용하면 애플리케이션의 동작과 데이터 처리를 조작할 수 있다.</li>
<li>플러터에서는 산술 연산자, 증감 연산자, 논리 연산자, null관련 연산자, 비교 연산자, 논리 연산자 등이 있다.</li>
</ul>
</blockquote>
<p></br></br></p>
<h2 id="2-1-if-else문">2-1. if else문</h2>
<hr>
<ul>
<li>원하는 조건을 기준으로 다른 코드를 실행하고 싶을 때 사용한다.</li>
</ul>
<blockquote>
<ul>
<li>if문, if else문, else문의 순서대로 괄호 안에 작성한 조건이 참이면 해당 조건의 코드 블록이 실행된다.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>if 문의 기본구조</strong></p>
</blockquote>
<pre><code class="language-dart">if (조건식) {
    // 조건식이 참인 경우 실행할 코드
} else {
    // 조건식이 거짓인 경우 실행할 코드
}</code></pre>
<blockquote>
<p><strong>응용 구조</strong></p>
</blockquote>
<pre><code class="language-dart">if (조건식) {
    // 조건식이 참인 경우 실행할 코드
} else if{
    // 조건식이 참인 경우 실행할 코드
} else {
    // 조건식이 거짓일 경우 실행할 코드
}</code></pre>
<pre><code class="language-dart">void main() {
  int num1 = 1;
  int num2 = -1;
  int num3 = 0;

  if (num1 &gt; 0){
    print(&#39;num1은 양수입니다.&#39;);
  } else if (num1 &lt; 0){
    print(&#39;num1은 음수입니다.&#39;);
  } else {
    print(&#39;num1은 0 입니다.&#39;);
  }

  if (num2 &gt; 0){
    print(&#39;num2은 양수입니다.&#39;);
  } else if (num2 &lt; 0){
    print(&#39;num2은 음수입니다.&#39;);
  } else {
    print(&#39;num2은 0 입니다.&#39;);
  }

  if (num3 &gt; 0){
    print(&#39;num3은 양수입니다.&#39;);
  } else if (num1 &lt; 0){
    print(&#39;num3은 음수입니다.&#39;);
  } else {
    print(&#39;num3은 0 입니다.&#39;);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/3c54fd7a-2e3a-43da-8d63-672df6c29f5b/image.png" alt=""></p>
<p></br></br></p>
<h2 id="2-2-switch-case문">2-2. switch case문</h2>
<hr>
<ul>
<li>조건에 따라 다양한 동작을 수행할 때 사용한다.</li>
<li>여러 경우를 처리해야 할 때 if-else 문 보다 가독성을 높일 수 있다.</li>
<li>변수의 값에 따라 다른 동작을 수행하는 경우에 유용하다.</li>
<li>enum 과 함께 사용하면 유용하다.</li>
</ul>
<blockquote>
<ul>
<li>주어진 변수 또는 표현식의 값을 평가하고, 값에 따라 조건에 맞는 case 블록을 수행한다.</li>
<li>break 키워드를 사용하면 switch 문 밖으로 나갈 수 있다. </li>
</ul>
</blockquote>
<blockquote>
<p><strong>switch 문의 기본구조</strong></p>
</blockquote>
<pre><code class="language-dart">if (조건식) {
    // 조건식이 참인 경우 실행할 코드
} else {
    // 조건식이 거짓인 경우 실행할 코드
}

```dart
enum Status {
  approved,
  pending,
  rejected,
}

void main() {
  Status status = Status.approved;

  switch (status){
    case Status.approved:
      print(&#39;승인 상태&#39;);
      break;
    case Status.pending:
      print(&#39;대기 상태&#39;);
      break;
    case Status.rejected:
      print(&#39;거절 상태&#39;);
      break;
    default:
      print(&#39;알 수 없음&#39;);
  }
  print(Status.values);   //Enum의 모든 수를 리스트로 반환
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/7ca5e2be-ba36-4c5a-91be-6d80eac99c16/image.png" alt=""></p>
<p></br></br></p>
<h2 id="2-3-for문">2-3. for문</h2>
<hr>
<ul>
<li>특정 조건을 만족하는 동한 작업을 여러 번 반복해서 실행할 때 사용한다.</li>
</ul>
<blockquote>
<ul>
<li>for 문은 초기화 구문을 실행한 후에 조건식을 평가한다.</li>
<li><strong>초기화</strong>: 반복문이 시작될 때 한 번 실행되는 초기화 구문이며, 반복 변수의 초기값을 설정한다.</li>
<li><strong>조건식</strong>: 반복문이 실행될 조건을 지정하는 부분이며, 조건식이 참인 경우 코드 블록이 실행, 거짓일 경우 종료된다.</li>
<li><strong>증감식</strong>: 반복문이 한 번 실행된 후에 반복 변수를 증가 또는 감소시키는 구문이며, 반복 변수를 업데이트 하는 역할을 한다.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>for 문의 기본구조</strong></p>
</blockquote>
<pre><code class="language-dart">for (초기화; 조건식; 증감식) {
    // 반복적으로 실행할 코드
}

```dart
void main() {
  for (var i = 1; i &lt;= 5; i++) {
    print(i);
  }
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/91c69541-a69e-4b26-b760-89cd7e8fa6da/image.png" alt=""></p>
<blockquote>
<p>다트 언어에서는 <strong>For in 패턴</strong>의 for문도 제공한다.
List의 모든 값을 순회할 때 사용한다.</p>
<pre><code class="language-dart">void main() {
 List&lt;int&gt; numberlist = [1,2,3];

for (int num in numberlist){
    print(num);
  }
}
</code></pre>
</blockquote>
<p>  <img src="https://velog.velcdn.com/images/kk_0128_/post/41e1bdf8-64ac-4b86-abaa-e159ac0ec6b6/image.png" alt=""></p>
<p></br></br></p>
<h2 id="2-4-while과-do-while문">2-4. while과 do while문</h2>
<hr>
<ul>
<li>반복적인 작업을 실행할 때 사용한다.</li>
<li>두 구문은 조건식을 기반으로 코드 블록을 반복적으로 실행한다.</li>
</ul>
<blockquote>
<ul>
<li>while문은 반복문 진입 전에 조건을 평가한다.</li>
<li>do while문은 반복문 진입 후에 조건을 평가한다.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>while 문의 기본구조</strong></p>
</blockquote>
<pre><code class="language-dart">while (조건식) {
// 조건식이 참인 동안 실행할 코드
}

```dart
void main() {
  int total = 1;

  while (total &lt; 10) {
    total += 1;
  }
  print(total);
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/67dba6be-dda5-40e9-ab78-d8560fcad16e/image.png" alt=""></p>
<blockquote>
<p><strong>do while 문의 기본구조</strong></p>
</blockquote>
<pre><code class="language-dart">do {
    // 실행할 코드
} while (조건식);

```dart
void main() {
  int total = 0;

  do {
    total += 1;
  } while(total &lt; 10);

  print(total);
}

</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/30cb13a4-283d-4433-a663-bb4415580156/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[전공수업에서 진행한 팀 프로젝트 첫 번째 이야기]]></title>
            <link>https://velog.io/@kk_0128_/%EC%A0%84%EA%B3%B5%EC%88%98%EC%97%85%EC%97%90%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</link>
            <guid>https://velog.io/@kk_0128_/%EC%A0%84%EA%B3%B5%EC%88%98%EC%97%85%EC%97%90%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%9C-%ED%8C%80-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8</guid>
            <pubDate>Wed, 17 Jan 2024 08:30:11 GMT</pubDate>
            <description><![CDATA[<h1 id="컴퓨터공학-기초-설계">컴퓨터공학 기초 설계</h1>
<p>우리 학교의 전공기초 과정에는 컴퓨터 공학 기초설계라는 전공 과목이 있다. 4~5명이 팀을 이뤄 프로젝트를 진행하는 수업인데, 수강했던 작년 2학기 기준으로 그 해에 신설된 교과목이어서 수강신청 전부터 강의 계획서를 수없이 조회해 보며 많은 기대를 했었다.</p>
<p>깊지 않은 전공 지식과 경험을 핑계로, 컴퓨터공학과로 전과 후 지금까지 배웠던 내용을 직접 실습할 기회가 쉽게 오지 않았다고 생각했는데 마침 전공 수업을 통해 나름 좋은 기회가 생긴 것 같았다.</p>
<p>많은 기대를 하고 수업을 듣고 프로젝트를 진행하며 힘들었던 것들과 생각대로 되지 않아 막막했던 순간들도 물론 있었지만, 결과적으로 프로젝트를 마무리한 지금으로서 생각해 보면 좋은 성적을 포함해 얻은 것도 많았고 새로운 도전에 대한 두려움도 줄어든 것 같다. </p>
<p>총 15주 간의 프로젝트를 진행하며 일궈왔던 과정들, 달성한 목표, 새롭게 알게 된 내용 그리고 앞으로의 계획에 대해 글로 남기고자 한다.
</br></br></p>
<h1 id="아이디어-선정">아이디어 선정</h1>
<p>기초설계 수준에서 진행할 수 있는 아이디어를 구상하는 단계는 생각보다 빠르게 지나갔다.</p>
<p>평소 만들고싶던 서비스나 앱들은 많았지만 실력이 받쳐주지 않아 구상만 하고 있던 아이디어들을 몇 가지 내뱉었고, 그 중 <code>택시 합승 자동매칭 서비스</code>가 주제로 굳혀졌다.
학교의 지리적인 특성상, 우리 학교는 일반적인 대학가라고는 느끼기는 애매할 수 있는 환경이었고 그에 따라 택시 이용율도 매우 높았다. 본가에 가려면 시외버스 터미널 또는 기차역을 이용해야 했는데, 둘 다 거리가 상당히 멀었다. 
무엇보다 한 번 제작해보고 버릴 내용의 프로젝트보다는 실제로 의미 있는 서비스 가능한 프로젝트를 진행하고 싶었고, 마침 재학생들을 상대로 테스트 및 서비스가 가능한 더 없이 좋은 조건을 가지고 있는 상황이 이 주제를 선정하는 데 큰 이유가 되었다.</p>
<p>그래서 우리는 비싼 택시 요금의 부담은 줄이면서 불편한 과정 없이 택시를 이용할 수 있는 방법으로 택시 합승을 떠올렸고, 기존에는 어떤 방식으로 합승이 이뤄지고 있는지 조사했다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/e2586337-8def-4acc-828f-5d40f48d7e6f/image.png" alt=""></p>
<p></br></br></p>
<h1 id="관련-서비스-조사">관련 서비스 조사</h1>
<blockquote>
<h3 id="에브리타임">에브리타임</h3>
<p> <em><strong>사용자가 많은 편이지만, 상대 찾기, 알림과 소통 면에서 불편</strong></em></p>
</blockquote>
<p>먼저 가장 먼저 알아본 것은 대학교 익명 커뮤니티인 에브리타임이었다. 각 학교별로 개설된 교과목과 여러 정보들이 연동되어있어 시간표 어플로도 사용할 수 있는 어플리케이션이다.
에브리타임은 익명 기반의 게시판 형식을 갖추고 있어 시간과 공간에 제약 없이 목적성이 분명한 글을 쉽게 올릴 수 있었다. 재학생들은 이 플랫폼을 활용해 목적지와 시간이 같은 합승 인원을 구하고 있었고, 합승을 원하는 사람들은 카카오톡 오픈채팅으로 이동해 자세한 일정을 조율해 합승하는 방식을 사용하고 있었다.</p>
<p>하지만 에브리타임은 &quot;택시&quot;와 관련된 어플리케이션이 아니다. 게시판 형태로 구성되어있어 새로운 글이 올라오는 간격이 짧았고, 그에 따라 합승 게시글은 표시 우선 순위에서 밀려났다. 내가 원하는 글에 대한 알림이 오지 않아 합승을 위해선 지속적으로 게시판을 확인해주어야 했고, 합승 상대가 여러명일 경우 다른 플랫폼인 카카오톡 오픈채팅으로 이동하는 번거로움이 존재했다.</p>
<blockquote>
<h3 id="카카오톡-오픈채팅">카카오톡 오픈채팅</h3>
<p> <em><strong>가장 편한 방법이지만, 원치 않는 알림이 오거나 필요한 알림이 오지 않음</strong></em></p>
</blockquote>
<p>다음으로 사용자가 많은 것은 택시 합승 인원을 구하는 카카오톡 오픈채팅방이었다.
재학생만 사용할 수 있도록 먼저 가입되어 있던 참여자에게 코드를 받아 입장하는 방식이었고, 약 200명의 재학생이 참여하고 있었다. 우리는 이렇게 많은 사람이 한 채팅방에서 어떤식으로 합승 상대를 구하는 지 알아봤고, 방식은 이랬다.</p>
<pre><code class="language-c">1. 톡 게시판의 일정 기능을 이용해 원하는 일정 등록
2. 채팅방에 공유된 일정을 확인하고 참/불참 여부 투표
3. 참여자들은 해당 일정 내 댓글 또는 다른 오픈채팅방으로 이동해 연락
4. 합승 성사</code></pre>
<img src="https://velog.velcdn.com/images/kk_0128_/post/b6525a9c-7a5d-4a55-9da5-ae1542fa7f67/image.png" width="50%">
이 과정에서 우리는 200명이 이용하는 채팅방에서 계속 올라오는 일정 알림과, 댓글에서는 지원되지 않는 푸시 알림이 불편하다고 느꼈다. 기존 이용자들도 그 이유로 자세한 일정 조정을 위해 다른 오픈채팅방으로 이동하는 모습을 볼 수 있었다.

<blockquote>
<h3 id="림카">림카</h3>
<p> <em><strong>구상한 아이디어와 가장 유사하지만, 익명과 게시판 형식으로 인한 불편함이 존재</strong></em></p>
</blockquote>
<p>다음으로는 한림대학교 재학생들이 개발한 &quot;림카&quot;라는 어플리케이션이다. 아이디어를 선정하면서 그려왔던 구상 내용이 거의 유사해 많이 놀랐던 서비스였다.
먼저 앱을 사용하기 위해선 대학교의 웹메일 인증을 통해 가입이 필요했다. 감사하게도, 개발자의 <a href="https://github.com/EJLee1209/HallymTaxi">깃허브</a>와 <a href="https://opposite-mandevilla-887.notion.site/400030390e5a486695c44e930347e600">노션</a>이 오픈되어있어 내부 UI를 통해 대략적인 시스템을 확인할 수 있었다.
<img src="https://opposite-mandevilla-887.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F93218072-f02d-4651-a6c2-01671884f7b2%2FKakaoTalk_Photo_2023-03-11-19-06-32.jpeg?table=block&id=5cfeb1e0-efa3-4863-aecf-129084da4b89&spaceId=f449bb0b-ae84-41f2-93fe-b34b4d9ef290&width=2000&userId=&cache=v2
" width="40%"></p>
<p>림카는 우리가 원하던 합승 옵션 선택, 즐겨찾기, 지도확인 등의 대부분의 기능들이 제공되고 있었다. 다만 게시판 방식으로 인해 이용자의 수동적인 절차가 필요했고, 익명 시스템을 채택해 안전성 면에서 다소 아쉬움이 느껴졌지만 같은 목적을 가진 유사한 선행 프로젝트가 있다는 사실에 많이 놀라고 대단하다고 느꼈던 사례였다.</p>
<blockquote>
<h3 id="반반택시-카찹">반반택시, 카찹</h3>
<p> <em><strong>택시 호출, 결제 등의 부가기능이 지원되지만 뚜렷하지 않은 타겟층과 낮은 매칭 성공률</strong></em></p>
</blockquote>
<p>이 외에 다른 서비스들로는 반반택시와 카찹이 있었다.
반반택시는 택시를 직접 호출할 수 있고 앱 자체적인 결제가 가능해 편의성은 높았지만 이용 타겟이 광범위해 경로 설정에 대한 자유도가 너무 높아 오히려 매칭이 잘 되지 않는다는 반응이 많았다. 카찹 또한 공유 모빌리티 위치나 대중교통의 혼자도 등의 여러 편의 기능을 제공했지만 가격 비교나 호출을 위해 다른 플랫폼으로의 경유가 필요해 많은 불편함이 있었다.</p>
<p>무엇보다 두 서비스 모두 전문 기업의 서비스임에도 불구하고 수동적인 매칭 방식을 지원하고 있어 기존에 재학생들이 사용하는 방식과 비교했을 때 큰 이점이 없었다.
</br></br></p>
<h1 id="이-프로젝트가-진행되어야-하는-이유">이 프로젝트가 진행되어야 하는 이유</h1>
<p>우리는 택시 합승 자동매칭 서비스를 기획하며 이 프로젝트가 진행되어야 하는 이유가 필요했다. 그래서 팀원들이 느끼는 불편함을 떠나 재학생들의 객관적인 의견을 조사하는 설문조사를 진행했다. <img src="https://velog.velcdn.com/images/kk_0128_/post/7473fd15-21d0-4651-a968-713f9f5f7766/image.png" alt=""></p>
<p>결과는 예상한 대로 인상된 택시요금에 대한 불만, 위치적인 이유로 인한 잦은 택시 이용률, 기존 합승 방식에 대한 한계에 대한 의견을 얻을 수 있었고 기존 단점들을 해결하는 어플리케이션에 대한 기대도 확인할 수 있었다.</p>
<p>이 밖에도 요금 부담 감소로 인한 재학생들의 택시 이용 횟수가 늘어나면 지역 택시 기사분 또한 긍정적인 영향을 받을 수 있을 것 같다는 기대를 <a href="https://www.ohmynews.com/NWS_Web/View/at_pg.aspx?CNTN_CD=A0002923230">관련기사</a>를 통해 예상할 수 있었다.
</br></br></p>
<h1 id="기존-서비스와의-차별화-및-계획-세분화">기존 서비스와의 차별화 및 계획 세분화</h1>
<p>우리는 재학생들이 기존에 존재하는 서비스 대신 새로운 플랫폼을 사용하게 하려면 확실한 차별점을 두어야 한다고 생각했다. 그에 따라 다음 다섯 가지의 차별점을 중심으로 계획을 세분화했다.</p>
<blockquote>
<ul>
<li>서비스에 대한 쉬운 접근</li>
<li>확실한 타겟과 이용자의 안전</li>
<li>합승 과정의 자동화, 단순화</li>
<li>신뢰할 수 있는 상대</li>
</ul>
</blockquote>
<p>먼저 쉽고 빠르게 서비스에 접근하고 사용할 수 있도록 웹페이지를 기반으로 구현해 어플리케이션과 사용중인 브라우저를 통해 쉽게 사용할 수 있는 서비스를 제공하기로 했다. 또한 재학생들이 주로 이용하는 경로를 사전에 정해두어 높은 매칭률을 이끌어 낼 수 있도록 계획했다.</p>
<p>다음으로는 실제 만남이 이루어지는 만큼 이용자에 대한 안전을 중요하게 생각했다. 익명을 사용하는 기존 방식들은 미지의 대상에 대한 두려움 또는 불안감을 빼놓을 수 없었다. 따라서 가입 과정에서 학교 웹메일 인증을 통해 재학생만 접근이 가능하게 해 서비스를 제공할 확실한 타겟을 선정했다. 그리고 익명이 아닌 학번과 실명으로 상대를 표시해 안전성을 높일 수 있도록 계획했다.</p>
<p>다음으로는 가장 중요하게 생각하는 자동화와 단순화다. 기존의 게시판 방식은 합승을 위해 지속적인 모니터링 과정이 필요했고, 상대를 구하는 과정 또한 번거로웠다. 우리는 이런 불편함을 해결하고자 매칭 옵션, 사전 제공 경로, 파티원 간의 채팅, 푸시 알림 기능을 지원하고 조건의 맞는 상대를 자동으로 매칭시켜주는 시스템을 계획했다.</p>
<p>마지막으로 우리는 합승자 간의 매너도 중요하게 생각했다. 설문조사 결과 합승 후 정산과 늦은 출발지 도착으로 인해 다른 사용자에 대한 피해 사례가 존재했고, 당일 연락 두절 등의 행위를 방지할 수단이 없어 아쉽다는 의견이 많았다. 따라서 우리는 당근마켓과 같은 온도 시스템을 도입해 상대의 신뢰도를 파악할 수 있게 하고 그에 따른 이용제한과 고객센터 신고 기능을 계획했다.</p>
<p>우리는 이러한 차별점들을 바탕으로 시스템을 개발하고, 서비스를 진행하며 실제 재학생들의 대중교통 이용 복지의 질을 향상하고, 더 나아가 지역의 대중교통을 더욱 활성화 할 수 있는 좋은 계기가 되기를 바라며 개발을 진행했다.</p>
<h1 id="시스템-설계-과정">시스템 설계 과정</h1>
<p>이 교과목에서는 유즈케이스 다이어그램과 명세서, 시스템 구성도 그리고 순차 다이어그램 등의 설계 과정을 요구했다. 그래서 기존에 접해보지 못한 세분화 된 설계 과정을 거칠 수 있어 좋은 기회가 되었지만, 따로 작성법을 배우지 않고 직접 찾아봐야해서 조금 부족한 면이 있었다. 특히 유즈케이스의 경우, 4학년 선배에게 보여주니 간결하지 못하고 복잡하다는 피드백을 받았었다. 지금 보니 정석으로 작성하지 못하고 필요 이상의 정보가 들어가있다는 느낌이 든다.</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/2f710965-c0c6-4065-8a20-753214043ff1/image.png" alt=""></p>
<p>유스케이스 다이어그램</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/4edd64ba-d5ad-412e-b925-dd495de2348d/image.png" alt=""></p>
<p>시스템 구성도</p>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/e5e8963e-c39f-4b1e-a201-29f5294c10b7/image.png" alt=""></p>
<p>시퀀스 다이어그램</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 컬렉션 - 데이터의 저장, 조작]]></title>
            <link>https://velog.io/@kk_0128_/Flutter-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EC%A1%B0%EC%9E%91</link>
            <guid>https://velog.io/@kk_0128_/Flutter-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EC%A1%B0%EC%9E%91</guid>
            <pubDate>Tue, 16 Jan 2024 11:18:23 GMT</pubDate>
            <description><![CDATA[<h3 id="시리즈-목록">시리즈 목록</h3>
<hr>
<p><a href="https://velog.io/@kk_0128_/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0">1. [Flutter] 플러터 개발환경 세팅하기</a></p>
<p><a href="https://velog.io/@kk_0128_/Flutter-%EB%8B%A4%ED%8A%B8-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95">2. [Flutter] 다트 기초 문법</a></p>
<p><a href="https://velog.io/@kk_0128_/Flutter-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EC%A1%B0%EC%9E%91">3. [Flutter] 컬렉션 - 데이터의 저장, 조작 (본문)</a>
</br></br></br></br></p>
<blockquote>
<ul>
<li>컬렉션은 여러 값을 하나의 변수에 저장할 수 있는 타입이다.</li>
<li>여러 값을 순서대로 저장하거나(<code>List</code>), 특정 키 값을 기반으로 빠르게 검색해야 하거나(<code>map</code>), 중복된 데이터를 제거할 때 사용한다(<code>set</code>).</li>
<li>컬렉션 타입은 서로의 타입으로 자유롭게 형 변환이 가능하다는 큰 장점이 있다.</li>
</ul>
</blockquote>
<h2 id="1-list-타입">1. List 타입</h2>
<hr>
<ul>
<li>한 변수에 여러 값을 순서대로 저장할 때 사용한다.</li>
</ul>
<blockquote>
<ul>
<li><p><code>List&lt;자료형&gt; 리스트명 = [&#39;데이터1&#39;, &#39;데이터2];</code> 형식으로 선언한다.</p>
</li>
<li><p>리스트명[index] 형식으로 원소에 접근이 가능하다.</p>
</li>
<li><p>index는 <code>0부터 시작</code>, 마지막 원소는 <code>리스트의 길이 -1</code> 이다. (length로 길이 확인 가능)</p>
</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  List&lt;int&gt; myNumber = [1, 2, 3, 4];

  print(myNumber);
  print(myNumber[0]);        // 0번 index의 값 출력 = &#39;1&#39;
  print(myNumber[3]);        // 3번 index의 값 출력 = &#39;4&#39;

  print(myNumber.length);    // myNumber 리스트의 길이 출력(자료의 개수)

  myNumber[3] = 5;         // myNumber의 3번 index의 값을 5로 변경
  print(myNumber[3]);        // 3번 index의 값 출력 = &#39;5&#39;
}
}


//void는 아무 값도 반환하지 않는다는 의미
//()에 입력받은 매개변수 지정 가능</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/8920ba60-e21b-4fc7-9b9b-b7a35768d5f3/image.png" alt=""></p>
<h3 id="list-타입의-기본-제공-함수">List 타입의 기본 제공 함수</h3>
<hr>
<ul>
<li><p>한 변수에 여러 값을 순서대로 저장할 때 사용한다.</p>
<blockquote>
<ol>
<li><mark style='background-color: #dcffe4'> add()</mark><ul>
<li>List의 끝에 값을 추가할 때 사용한다.</li>
</ul>
</li>
</ol>
</blockquote>
<pre><code class="language-dart">void main() {
List&lt;int&gt; numberList = [1, 2, 3, 4, 5];
List&lt;int&gt; newList = [];

for (int number in numberList) {
  if (number % 2 == 0) {
    newList.add(number);
  }
}

print(newList);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/eea9f987-3cb8-46a4-8102-3003412a5102/image.png" alt=""></p>
</li>
</ul>
<hr>
  </br>

<blockquote>
<ol start="2">
<li><mark style='background-color: #dcffe4'> where()</mark><ul>
<li>List의 값들을 순회해 조건에 맞는 값만 필터링하는 데 사용한다.</li>
<li>각 값 별로 true 반환 시 값을 유지하고, false 반환 시 값을 폐기한다.</li>
</ul>
</li>
</ol>
</blockquote>
<pre><code class="language-dart">  void main(){ 
    List&lt;String&gt; blackPinkList = [&#39;리사&#39;, &#39;지수&#39;, &#39;제니&#39;, &#39;로제&#39;];

    final newList = blackPinkList.where((name) =&gt; name==&#39;리사&#39; || name==&#39;지수&#39;,);
    // blackPinkList 리스트에서 조건을 만족하는 요소들로 새로운 Iterable을 생성
    // 각 요소의 이름을 name으로 받아와 조건을 확인
    // 조건 중 하나라도 참이면 해당 요소는 새로운 Iterable에 포함
    // newList는 blackPinkList에서 &#39;리사&#39; 또는 &#39;지수&#39;인 요소들로 구성된 Iterable임
    // 이 때 newList는 Iterable 타입
    // .toList() 메소드를 사용하면 List 타입으로 변환 가능함.

    print(newList);
    print(newList.toList()); // Iterable을 List로 다시 변환할 때 .toList() 사용
  }</code></pre>
<p>  <img src="https://velog.velcdn.com/images/kk_0128_/post/f245f2e6-ca03-4537-a8f0-1808e90b7b2a/image.png" alt=""></p>
<hr>
  </br>

<blockquote>
<ol start="3">
<li><mark style='background-color: #dcffe4'> map()</mark><ul>
<li>List의 값들을 순회해 값을 변경하는 데 사용한다.</li>
<li>매개변수에 함수를 입력해야 하며, 반환 값이 현재 값을 대체한다.</li>
<li>순회가 끝나면 Iterable이 반환된다.</li>
</ul>
</li>
</ol>
</blockquote>
<pre><code class="language-dart">  void main(){
    List&lt;String&gt; blackPinkList = [&#39;리사&#39;, &#39;지수&#39;, &#39;제니&#39;, &#39;로제&#39;];
    print(blackPinkList);

    final newList = blackPinkList.map((name) =&gt; &#39;블핑 $name&#39;,);
    // 리스트의 모든 값을 순회하며 값 앞에 &#39;블랙핑크&#39; 추가

    print(newList);
    print(newList.toList()); // Iterable을 List로 다시 변환할 때 .toList() 사용
  }</code></pre>
<p>  <img src="https://velog.velcdn.com/images/kk_0128_/post/3a23079d-237e-42ca-bb4a-5284a8755ba7/image.png" alt=""></p>
<hr>
</br>

<blockquote>
<ol start="4">
<li><mark style='background-color: #dcffe4'> reduce()</mark><ul>
<li>List에 있는 값들을 순회하며 매개변수에 입력된 함수를 실행한다.</li>
<li>순회할 때마다 값을 쌓아간다.</li>
<li>List 멤버의 타입과 같은 타입을 반환한다.</li>
</ul>
</li>
</ol>
</blockquote>
<pre><code class="language-dart">  void main(){
    List&lt;String&gt; blackPinkList = [&#39;리사&#39;, &#39;지수&#39;, &#39;제니&#39;, &#39;로제&#39;];

  final allMembers = blackPinkList.reduce((value, element) =&gt; value + &#39;, &#39;+ element);
  // 리스트를 순회하며 값들을 더함
  // 순회가 시작될 때 첫 번째 매개변수(value)는 리스트의 첫 번째 값을 받음
  // 두 번째 매개변수(element)는 두 번째 값을 받음
  // 두 번째 사이클에서 value에 전 사이클의 &#39;value, element&#39; 값이 들어감, 반복

  print(allMembers);
  }</code></pre>
<p>  <img src="https://velog.velcdn.com/images/kk_0128_/post/1e78fe9d-360e-4811-bbcc-3375ae7aae3f/image.png" alt=""></p>
<hr>
<blockquote>
<ol start="5">
<li><mark style='background-color: #dcffe4'> fold()</mark><ul>
<li>List에 있는 값들을 순회하며 매개변수에 입력된 함수를 실행한다.</li>
<li>순회할 때마다 값을 쌓아간다.</li>
<li><code>어떠한 타입이든</code> 반환 가능하다.</li>
</ul>
</li>
</ol>
</blockquote>
<pre><code class="language-dart">  void main() {
  List&lt;String&gt; black = [&#39;글자수&#39;, &#39;만큼출력&#39;];

  final all = black.fold&lt;double&gt;(0.1, (value, element) =&gt; value + element.length);
  // index의 개수만큼 사이클 순환
  // double 값으로 index의 값들을 변환함
  // 초기 value 값을 0.1로 지정, 다음 값인 &#39;글자수&#39;를 element에 지정
  // 첫 사이클 순환 시 value = 0.1 + &#39;글자수.length&#39;(3)
  // 두 번째 사이클 시 value = 3.1 + &#39;만큼출력.length&#39;(4)

  print(all);
}</code></pre>
<p>  <img src="https://velog.velcdn.com/images/kk_0128_/post/3802551f-ebb5-412a-92b5-bd6d01a025c2/image.png" alt="">
</br></br></br></p>
<h2 id="2-map-타입">2. Map 타입</h2>
<hr>
<ul>
<li>Key와 value의 짝을 저장한다.</li>
</ul>
<blockquote>
<ul>
<li><code>Map&lt;key 타입, value 타입&gt; map이름 = {키:값, 키:값};</code> 의 형식으로 선언한다.</li>
<li>key를 이용해 원하는 값을 빠르게 찾는 데 중점을 둔다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  Map&lt;int, String&gt; intmember = {
    1: &#39;해리 포터&#39;,      // int 키: &#39;String&#39; 벨류
    2: &#39;론 위즐리&#39;,
    3: &#39;헤르미온느&#39;,
  };
  print(intmember[1]); // int 키에 해당하는 벨류 출력
  print(intmember[2]);

  Map&lt;String, String&gt; stringmember = {
    &#39;1번&#39;: &#39;Harry Poter&#39;,      // &#39;String&#39; 키:&#39;String&#39; 벨류
    &#39;2번&#39;: &#39;Ron Weasley&#39;,
    &#39;3번&#39;: &#39;Hermione Granger&#39;,
  };
  print(stringmember[&#39;2번&#39;]); // &#39;String&#39; 키에 해당하는 벨류 출력
  print(stringmember[&#39;3번&#39;]);
  print(stringmember.keys);  // 키 반환, Iterable로 반환되므로 .toList()로 반환 가능
  print(stringmember.values);// 벨류 반환, Iterable로 반환되므로 .toList()로 반환 가능
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/131a11d1-cf9b-4b3f-88d2-ed44625ef210/image.png" alt="">
</br></br></br></p>
<h2 id="3-set-타입">3. Set 타입</h2>
<hr>
<ul>
<li>중복 없는 값들의 집합이며, 중복을 방지하여 유일한 값들만 존재한다.</li>
</ul>
<blockquote>
<ul>
<li><code>Set&lt;타입&gt; set이름 = {값1, 값2};</code> 의 형식으로 선언한다.</li>
<li>중복되는 값을 제거하여 한 번만 반환한다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">void main() {
  Set&lt;String&gt; black = {&#39;로제&#39;,&#39;지수&#39;,&#39;리사&#39;,&#39;제니&#39;,&#39;제니&#39;}; // 제니 중복

  print(black);
  print(black.contains(&#39;로제&#39;));  // 값의 존재 여부 T/F로 반환
  print(black.toList());        // List로 변환

  List&lt;String&gt; black2 = [&#39;로제&#39;,&#39;지수&#39;,&#39;지수&#39;];
  print(Set.from(black2));      //List 타입을 Set으로 변환, 중복 제거
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/d6e6e3a1-0138-4afe-8586-3d78bb1dab9e/image.png" alt="">
</br></br></br></p>
<h2 id="4-enum-타입">4. enum 타입</h2>
<hr>
<ul>
<li>열거형(Enumeration)의 줄임말로, 한 변수의 값을 연관된 몇 가지 상수 옵션으로 제한하는 기능이다.</li>
</ul>
<blockquote>
<ul>
<li><code>enum enum이름 {value1, value2, value3};</code> 의 형식으로 선언한다.</li>
<li>선택지가 제한적일 때 사용한다.</li>
<li>enum을 사용하면 자동 완성 기능을 통해 가능한 상수 값을 빠르게 확인할 수 있다.</li>
<li>컴파일러가 검사할 수 있는 타입이므로, 올바른 상수 값만 사용할 수 있어 잘못된 값을 사용하는 실수를 방지할 수 있다.</li>
</ul>
</blockquote>
<pre><code class="language-dart">enum Color {
  Red,
  Green,
  Blue,
}

void main() {
  Color myColor = Color.Green;

  print(myColor);

  if (myColor == Color.Red) {
    print(&quot;빨간색입니다.&quot;);
  } else if (myColor == Color.Green) {
    print(&quot;초록색입니다.&quot;);
  } else if (myColor == Color.Blue) {
    print(&quot;파란색입니다.&quot;);
  }
}
</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/fa31b65b-9251-4ed5-9d70-c30e47a4e0f3/image.png" alt="">
</br></br></br></p>
<h2 id="참고자료">참고자료</h2>
<blockquote>
<p>Must Have 코드팩토리의 플러터 프로그래밍 - 최지호(코드팩토리) [도서]</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[[Flutter] 기초 문법과 변수 선언]]></title>
            <link>https://velog.io/@kk_0128_/Flutter-%EB%8B%A4%ED%8A%B8-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95</link>
            <guid>https://velog.io/@kk_0128_/Flutter-%EB%8B%A4%ED%8A%B8-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95</guid>
            <pubDate>Tue, 16 Jan 2024 08:50:14 GMT</pubDate>
            <description><![CDATA[<h3 id="시리즈-목록">시리즈 목록</h3>
<hr>
<p><a href="https://velog.io/@kk_0128_/Flutter-%ED%94%8C%EB%9F%AC%ED%84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0">1. [Flutter] 플러터 개발환경 세팅하기</a></p>
<p><a href="https://velog.io/@kk_0128_/Flutter-%EB%8B%A4%ED%8A%B8-%EA%B8%B0%EC%B4%88-%EB%AC%B8%EB%B2%95">2. [Flutter] 다트 기초 문법 (본문)</a></p>
<p><a href="https://velog.io/@kk_0128_/Flutter-%EC%BB%AC%EB%A0%89%EC%85%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EC%9D%98-%EC%A0%80%EC%9E%A5-%EC%A1%B0%EC%9E%91">3. [Flutter] 컬렉션 - 데이터의 저장, 조작</a>
</br></br></br></br></p>
<blockquote>
<p>다트패드를 이용하는 환경의 경우 Run 버튼으로,
안드로이드 스튜디오의 경우 아래의 Terminal 탭을 누른 후 터미널에 dart lib/main.dart 명령어를 실행하면 콘솔을 통해 확인할 수 있다.</p>
</blockquote>
<h2 id="1-메인함수">1. 메인함수</h2>
<hr>
<ul>
<li>프로그램 시작점인 엔트리 함수 기호로 main()을 사용한다.</li>
</ul>
<pre><code class="language-dart">void main() {
  // 코드내용
}


//void는 아무 값도 반환하지 않는다는 의미
//()에 입력받은 매개변수 지정 가능</code></pre>
<p></br></br></p>
<h2 id="2-주석-처리">2. 주석 처리</h2>
<hr>
<pre><code>// 한 줄 주석

/*
* 여러줄
* 주석처리
* 관행상 중간에 기호 삽입
*/

/// 문서 주석 (Documentation으로 인식함)</code></pre><p></br></br></p>
<h2 id="3-print-함수">3. Print() 함수</h2>
<hr>
<pre><code class="language-dart">void main() {
  print(&#39;Hello World&#39;);
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/620c69d2-0b56-4cfa-a998-e0992004af04/image.png" alt="">
</br></p>
<h2 id="4-var를-사용한-변수-선언">4. var를 사용한 변수 선언</h2>
<hr>
<ul>
<li><p>타입을 선언하지 않아도 된다.</p>
</li>
<li><p>var 변수명 = 값; 형식으로 선언한다.</p>
<pre><code class="language-dart">void main() {
var name = &#39;VarTest&#39;; // 변수 초기화
print(name); // name 변수 출력

name = &#39;yawn&#39;; // 변수 값 변경
print(name); // 변경된 name 변수 출력

// 변수명 중복 불가
}</code></pre>
<p><img src="https://velog.velcdn.com/images/kk_0128_/post/2419c47c-32cb-4262-bb38-b86a78b9e2fb/image.png" alt=""></p>
</br>

</li>
</ul>
<h2 id="5-dynamic-사용한-변수-선언">5. dynamic 사용한 변수 선언</h2>
<hr>
<ul>
<li>변수의 타입을 추론해 유동적으로 할당한다.<pre><code class="language-dart">void main() {
dynamic name = &#39;DynamicTest&#39;; // dynamic string 타입으로 추론되어 name 변수 초기화 선언
name = 1; // dynamic int 타입으로 추론되어 name 변수에 값 저장
}</code></pre>
</br></br></br></li>
</ul>
<h2 id="6-final--const를-사용한-변수-선언">6. final / const를 사용한 변수 선언</h2>
<hr>
<ul>
<li>변수의 값을 초기화 후 변경이 불가능하다.</li>
</ul>
<blockquote>
<p><code>final</code> 과 <code>const</code> 는 런타임과 빌드타임에서 동작하는 방식에 차이가 있다.</br></p>
<ul>
<li><code>final</code> 키워드로 선언된 변수는 런타임에 값이 할당되며, 한 번 할당된 이후에는 변경할 수 없다. 이른 변수가 런타임에서 초기화되는 것을 의미하며, <code>final</code> 변수는 프로그램이 실행되는 동안에만 값이 변경되지 않는다.</br></br></li>
</ul>
</blockquote>
<ul>
<li>반면 <code>const</code> 키워드로 선언된 변수는 빌드타임(컴파일 시)에 값이 할당된다.
이는 컴파일러가 프로그램을 컴파일 하는 과정에서 상수 값으로 대체되는 것을 의미하며, <code>const</code> 변수는 프로그램이 실행되기 전에 이미 값을 가지고 있고, 런타임에는 변경되지 않는다.</li>
</ul>
<p>런타임 변수 예: 계산 결과
빌드타임 변수 예: API 키</p>
<pre><code class="language-dart">void main() {
  final String name = &#39;blping&#39;; // 런타임 상수 final
  neme = &#39;bangtan&#39;; // 에러, 선언 후 값 변경 불가

  const String name = &#39;bangtan&#39;; // 빌드타임 상수 const
  neme = &#39;blping&#39;; // 에러, 선언 후 값 변경 불가
}</code></pre>
<p></br></br></br></p>
<h2 id="7-변수-타입">7. 변수 타입</h2>
<hr>
<ul>
<li><p>변수의 타입을 명시하는 방법이다.</p>
<pre><code class="language-dart">void main() {
String name = &#39;yawn&#39;;    // 문자열
int isInt = 128;         // 정수형
double isDouble = 2.5;   // 실수형
bool isTrue = true       // 불리언 (참/거짓)

print(name);
print(isInt);
print(isDouble);
print(isTrue); 
}</code></pre>
</li>
</ul>
<p></br></br></br></p>
<h2 id="참고자료">참고자료</h2>
<blockquote>
<p>Must Have 코드팩토리의 플러터 프로그래밍 - 최지호(코드팩토리) [도서]</p>
</blockquote>
]]></description>
        </item>
    </channel>
</rss>