<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyeo71.log</title>
        <link>https://velog.io/</link>
        <description></description>
        <lastBuildDate>Tue, 21 Oct 2025 07:22:19 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyeo71.log</title>
            <url>https://velog.velcdn.com/images/hyeo_71/profile/0afe0dd9-10b0-4572-9d33-17e17ba0b0bb/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyeo71.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyeo_71" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[DB Index]]></title>
            <link>https://velog.io/@hyeo_71/DB-Index</link>
            <guid>https://velog.io/@hyeo_71/DB-Index</guid>
            <pubDate>Tue, 21 Oct 2025 07:22:19 GMT</pubDate>
            <description><![CDATA[<h1 id="db-index">DB Index</h1>
<blockquote>
<p>데이터베이스 테이블의 <strong>검색(SELECT) 속도를 획기적으로 향상</strong>시켜주는 <strong>자료구조</strong></p>
</blockquote>
<h4 id="책의-목차에-비유한-인덱스-설명">책의 목차에 비유한 인덱스 설명</h4>
<ul>
<li><p>인덱스가 없을 때 
책에서 특정 단어를 찾으려면 첫 페이지부터 끝까지 <strong>모든 페이지(Full Scan)</strong>를 확인 </p>
</li>
<li><blockquote>
<p>데이터베이스에서는 테이블의 모든 행을 확인</p>
</blockquote>
</li>
<li><blockquote>
<p>느림</p>
</blockquote>
</li>
<li><p>인덱스가 있을 때
책의 <strong>목차나 색인</strong>을 통해 원하는 내용이 몇 페이지에 있는지 확인하고, 그 페이지로 <strong>바로 이동</strong></p>
</li>
<li><blockquote>
<p>데이터베이스에서는 인덱스에서 데이터의 <strong>물리적 주소</strong>를 찾아 바로 접근</p>
</blockquote>
</li>
<li><blockquote>
<p>빠름</p>
</blockquote>
</li>
</ul>
<hr>
<h1 id="index의-자료구조">Index의 자료구조</h1>
<h3 id="a-btreeb-plus-tree---가장-일반적--표준">A. B+Tree(B-Plus Tree) - 가장 일반적 &amp; 표준</h3>
<ul>
<li><p>특징
데이터를 정렬된 상태로 유지</p>
</li>
<li><p><em>리프 노드*</em>에만 실제 데이터를 저장하거나 데이터의 포인터를 저장하며, 이 리프 노드들은 <strong>링크드 리스트로 연결</strong></p>
</li>
<li><p>사용 목적</p>
</li>
<li><p><em>범위 검색*</em>(<code>&lt;</code>, <code>&gt;</code>, <code>BETWEEN</code>) 및 <strong>정렬</strong>(<code>ORDER BY</code>)에 매우 효율적이며, 대부분의 관계형 데이터베이스(RDB) 인덱스의 기본값</p>
</li>
</ul>
<h3 id="b-hash-index해시-인덱스">B. Hash Index(해시 인덱스)</h3>
<ul>
<li><p>특징
키 값에 해시 함수를 적용하여 해시 테이블 형태로 저장
데이터가 정렬되어 있지 않음</p>
</li>
<li><p>사용 목적</p>
</li>
<li><p><em>동등 비교 검색*</em>(<code>=</code>)시 주소를 <strong>O(1)</strong>로 <strong>가장 빠르게</strong> 검색</p>
</li>
<li><p>제한
데이터가 정렬되어 있지 않아 <strong>범위 검색이나 정렬에는 사용 불가</strong>(DBMS에 따라 지원 여부 및 활용 범위가 다름)</p>
</li>
</ul>
<hr>
<h1 id="index의-유형">Index의 유형</h1>
<h3 id="a-클러스터드-인덱스clustered-index">A. 클러스터드 인덱스(Clustered Index)</h3>
<ul>
<li><p>역할
인덱스의 리프 노드가 <strong>실제 데이터 행 자체</strong>
데이터의 물리적인 저장 순서가 이 인덱스 순서대로 정렬</p>
</li>
<li><p>개수
테이블당 <strong>오직 1개</strong>만 생성 가능, 주로 <strong>Primary Key(기본 키)</strong> 지정 시 자동으로 생성</p>
</li>
<li><p>조회
인덱스 검색 후 데이터에 바로 접근(1단계) -&gt; <strong>가장 빠른 조회</strong> 제공</p>
</li>
</ul>
<h3 id="b-세컨더리-인덱스secondary-index-보조-인덱스">B. 세컨더리 인덱스(Secondary Index, 보조 인덱스)</h3>
<ul>
<li><p>역할
데이터와 별도로 생성
리프 노드에는 <strong>컬럼 값</strong>과 해당 데이터의 <strong>클러스터드 키 값</strong>(또는 물리적 주소)만 저장</p>
</li>
<li><p>개수
테이블당 여러 개 생성 가능</p>
</li>
<li><p>조회
세컨더리 인덱스 검색 -&gt; 클러스터드 키 획득 -&gt; 클러스터드 인덱스를 통해 테이블 접근(2단계)이 필요</p>
</li>
</ul>
<hr>
<h1 id="장단점-및-생성-기준">장단점 및 생성 기준</h1>
<h3 id="장단점">장단점</h3>
<h4 id="장점">장점</h4>
<ul>
<li>조회
검색(SELECT) 속도 및 시스템 전반의 성능 향상</li>
</ul>
<h4 id="단점">단점</h4>
<ul>
<li>저장/변경</li>
<li><em>추가 저장 공간*</em> 필요
데이터 <strong>변경(CUD) 시 성능 저하</strong>(인덱스 갱신 필요)</li>
</ul>
<h3 id="생성-기준">생성 기준</h3>
<ol>
<li><strong>규모가 큰 테이블</strong>: 데이터 양이 적으면 Full Scan이 더 빠를 수 있음</li>
<li><code>WHERE</code>, <code>ORDER BY</code>, <code>JOIN</code> <strong>절에 자주 사용되는 컬럼</strong></li>
<li><strong>카디널리티(Cardinality)가 높은</strong> 컬럼: 데이터의 <strong>중복도가 낮은</strong> 컬럼일수록 효율적(ex. ID, 주민번호, 이메일)</li>
</ol>
<hr>
<h1 id="생성-및-활용">생성 및 활용</h1>
<table>
<thead>
<tr>
<th align="left"></th>
<th align="center">컬럼</th>
<th align="center">인덱스 유형</th>
</tr>
</thead>
<tbody><tr>
<td align="left">member_id</td>
<td align="center">INT</td>
<td align="center">Primary Key(클러스터드 인덱스 자동 생성)</td>
</tr>
<tr>
<td align="left">memeber_name</td>
<td align="center">VARCHAR</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">email</td>
<td align="center">VARCHAR</td>
<td align="center"></td>
</tr>
<tr>
<td align="left">join_date</td>
<td align="center">DATE</td>
<td align="center"></td>
</tr>
</tbody></table>
<h3 id="생성">생성</h3>
<pre><code class="language-sql">-- (기본값) B+Tree 세컨더리 인덱스 생성
CREATE INDEX idx_members_join_date ON members (join_date); 

-- 특정 자료구조 명시 (DBMS가 지원하는 경우)
CREATE INDEX idx_members_email ON members (email) USING HASH;</code></pre>
<h3 id="커버링-인덱스를-활용한-최적화">커버링 인덱스를 활용한 최적화</h3>
<p>쿼리가 요구하는 <strong>모든 컬럼이 인덱스에 포함</strong>되도록 인덱스를 설계하여, 실제 테이블 접근(Table Fetch) 없이 인덱스만으로 조회를 완료</p>
<pre><code class="language-sql">-- 복합 인덱스 생성 (이름 검색 후 가입일 조회에 사용)
CREATE INDEX idx_members_name_date ON members (member_name, join_date);

-- 쿼리: 이름으로 검색하고 가입일만 조회
-- 이 경우, 인덱스(member_name, join_date)만 읽어 결과를 반환 (최적의 성능)
EXPLAIN SELECT member_name, join_date FROM members 
WHERE member_name = &#39;홍길동&#39;;</code></pre>
<h3 id="옵티마이저의-역할">옵티마이저의 역할</h3>
<p>인덱스를 만들더라도, 데이터베이스 내부의 <strong>옵티마이저(Optimizer)</strong>가 쿼리 실행 시 비용을 계산하여 인덱스 스캔보다 Full Scan이 빠르다고 판단하면 인덱스를 사용하지 않을 수 있음 (ex. 조회하려는 데이터 비율이 전체의 10~20%를 초과할 때)</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[노마드코더] AI Agents 마스터클래스]]></title>
            <link>https://velog.io/@hyeo_71/%EB%85%B8%EB%A7%88%EB%93%9C%EC%BD%94%EB%8D%94-AI-Agents-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%81%B4%EB%9E%98%EC%8A%A4</link>
            <guid>https://velog.io/@hyeo_71/%EB%85%B8%EB%A7%88%EB%93%9C%EC%BD%94%EB%8D%94-AI-Agents-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%81%B4%EB%9E%98%EC%8A%A4</guid>
            <pubDate>Sun, 12 Oct 2025 13:02:38 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeo_71/post/dd8bbdc4-2ef6-4d41-b6da-1dab17aced27/image.png" alt=""></p>
<p>풀스택 GPT를 재밌게 수강중인 수강생입니다
새로운 AI 강의도 재밌을 것 같아서 구매합니다
배울게 많아보여서 두근두근하네요 ㅎㅎ</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[DB 비교하기]]></title>
            <link>https://velog.io/@hyeo_71/Sqlite-MySQL-PostgreSQL</link>
            <guid>https://velog.io/@hyeo_71/Sqlite-MySQL-PostgreSQL</guid>
            <pubDate>Mon, 30 Sep 2024 14:32:23 GMT</pubDate>
            <description><![CDATA[<p>SQL DB는 모두 SQL 언어를 사용하지만, 각 DB는 지원하는 기능이 다르고 호환되지 않는 경우가 많다. 그래서 프로젝트에 적합한 DB를 선택하는 것이 중요하다.</p>
<p>아래 3개의 DB를 성능, 편의성 두 가지 측면에서 비교해보도록 하자.</p>
<ul>
<li><p><strong>성능</strong>: 지원 기능, 작업 처리 능력, 확장성, 한계점(한계에 부딫힐까 걱정되는 부분이 존재하는가)</p>
</li>
<li><p><strong>편의성</strong>: 설치, 백업 및 유지 관리</p>
</li>
</ul>
<hr>
<h2 id="1-sqlite">1. SQLite</h2>
<ul>
<li>성능: 3/5</li>
<li>편의성: 5/5</li>
</ul>
<h3 id="주요-특징">주요 특징</h3>
<ul>
<li>&#39;Lite&#39;는 성능과는 아무 관련이 없으며 실제로 얼마나 가벼운지, 리소스를 얼마나 적게 사용하는지, 설치, 백업 및 사용이 얼마나 쉬운지를 나타내며, 모든 휴대폰, 비행기, 스마트워치 및 IoT 장치에 내장되어 있을 정도로 가벼움.</li>
<li>가벼운 파일 기반 DB로, 설치 및 백업이 매우 간단함.</li>
<li>수백만 개의 행과 기가바이트 단위의 데이터를 처리할 수 있음.</li>
<li>테이블 열 유형이 간단하며, MySQL과 PostgreSQL에 필요한 모든 유지 관리 작업이 필요 없는 점이 장점.</li>
<li>서버 없이 애플리케이션 코드에 통합 가능.</li>
<li>단일 서버에 적합하며, 다중 서버 환경에서는 부적합.</li>
</ul>
<h3 id="요약">요약</h3>
<p>SQLite는 소규모 프로젝트나 단일 서버에서 쉽게 사용할 수 있으며, 유지 보수가 간단한 DB이다. 하지만 다중 서버 환경에서는 한계가 있다.</p>
<hr>
<h2 id="2-mysql">2. MySQL</h2>
<ul>
<li>성능: 4/5</li>
<li>편의성: 4/5</li>
</ul>
<h3 id="주요-특징-1">주요 특징</h3>
<ul>
<li>오랫동안 인기가 많은 견고한 DB.</li>
<li>많은 SQL 기능을 지원하며, 특히 <code>ALTER TABLE</code> 명령, 날짜 및 시간 데이터 유형, JSON 등 SQL 기능이 강점.</li>
<li>Oracle의 인수 이후에도 지속적인 업데이트와 버그 수정이 이루어짐.</li>
<li><strong><a href="https://jake-seo-dev.tistory.com/169">PlanetScale</a></strong>과 같은 강력한 도구로 개발자 경험이 우수함.</li>
<li>백업, 업그레이드, 마이그레이션 등에서 SQLite보다 복잡함.</li>
</ul>
<h3 id="요약-1">요약</h3>
<p>MySQL은 많은 기능과 안정성을 제공하며 대규모 프로젝트에 적합하지만, 관리가 다소 복잡할 수 있다. PlanetScale 같은 툴을 활용하면 편의성이 증가한다.</p>
<hr>
<h2 id="3-postgresql">3. PostgreSQL</h2>
<ul>
<li>성능: 5/5</li>
<li>편의성: 3/5</li>
</ul>
<h3 id="주요-특징-2">주요 특징</h3>
<ul>
<li>가장 진보된 오픈 소스 DB로, 많은 개발자가 선호.</li>
<li>확장 기능이 매우 강력하며, NoSQL 키-값 저장소로 전환(<code>hstore</code>)하거나, DB 트리거(<code>plpython3u</code>: SQL 쿼리에 python 코드를 작성할 수 있음)와 같은 고급 기능을 제공.</li>
<li>다양한 확장 기능으로 DB의 기능을 크게 확장할 수 있음(<code>pgvector</code>: 벡터 DB로 변환, <code>PostGIS</code>: 지리공간 데이터, <code>pg_cron</code>: DB 내에서 작업을 예약 등 <a href="https://pgxn.org/">pgxn.org</a> 같은 웹사이트 참고)</li>
<li>확장 기능을 많이 사용할 경우 시스템이 복잡해질 수 있음.</li>
</ul>
<h3 id="요약-2">요약</h3>
<p>PostgreSQL은 강력한 기능과 확장성을 제공하지만, 복잡한 시스템 구조로 인해 관리가 어려울 수 있다. 고성능과 다양한 기능을 필요로 하는 프로젝트에 적합하다.</p>
<hr>
<h2 id="결론">결론</h2>
<ul>
<li><p><strong>SQLite</strong>: 유지 보수와 간편한 관리가 중요한 소규모 프로젝트에 적합.</p>
</li>
<li><p><strong>MySQL</strong>: 대중적인 선택으로, 기능이 풍부하고 대규모 프로젝트에도 적합.</p>
</li>
<li><p><strong>PostgreSQL</strong>: 고성능과 확장성이 필요한 프로젝트에 이상적이지만, 관리가 복잡할 수 있음.</p>
</li>
</ul>
<hr>
<h4 id="reference">Reference</h4>
<p><a href="https://www.youtube.com/watch?v=ocZid4g4UpY&amp;t=2s">nomad coders - DB 고민 끝내드림 💥</a>
<a href="https://jake-seo-dev.tistory.com/169">Jake Seo tistory blog - PlanetScale 정리</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240612]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240612</link>
            <guid>https://velog.io/@hyeo_71/TIL-240612</guid>
            <pubDate>Wed, 12 Jun 2024 11:34:11 GMT</pubDate>
            <description><![CDATA[<p>작성중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240604]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240604</link>
            <guid>https://velog.io/@hyeo_71/TIL-240604</guid>
            <pubDate>Tue, 04 Jun 2024 13:44:05 GMT</pubDate>
            <description><![CDATA[<h1 id="aws">AWS</h1>
<h2 id="aws-관련-오늘-한-일">AWS 관련 오늘 한 일</h2>
<ul>
<li>회원 가입</li>
<li>EC2 인스턴스 생성</li>
<li>인스턴스 진입 및 update, python, gunicorn, nginx 설치</li>
</ul>
<h2 id="내일-할일">내일 할일</h2>
<ul>
<li>requirements.txt 최신 버전으로 다시 한 번 freeze 하기</li>
<li>.gitignore 설정</li>
<li>.env 작성</li>
<li>가상 환경 폴더 제외하기</li>
<li>dj_rest_auth library accounts/user url 주석처리하기</li>
<li>포트 열기는 보안-보안 그룹-인바운드 규칙에서 편집</li>
<li>django ALLOWED_HOSTS 설정하기</li>
<li>정적 파일 모으기? (해야 되는지 모르겠음)</li>
<li>Nginx 설정하기</li>
<li>gunicorn 설정하기</li>
<li>개발 모드 종료하기</li>
<li>postman으로 동작확인</li>
<li>media 파일(유저가 올리는 이미지, 소설 표지) S3 ? 로 처리하는 방법 알아보기</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240531]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240530-nbenc0hf</link>
            <guid>https://velog.io/@hyeo_71/TIL-240530-nbenc0hf</guid>
            <pubDate>Fri, 31 May 2024 11:39:27 GMT</pubDate>
            <description><![CDATA[<h1 id="ai-로직-변경안">AI 로직 변경안</h1>
<p>현재 사용중인 기본적인 로직
<img src="https://velog.velcdn.com/images/hyeo_71/post/6a301f49-a7e6-4d07-b900-59d298651c35/image.png" alt=""></p>
<p>변경할 로직
<img src="https://velog.velcdn.com/images/hyeo_71/post/484b3347-0f40-4b0a-b4c5-0fb06c499c90/image.png" alt=""></p>
<p>이전 로직은 시놉시스라는 전체적인 줄거리를 생성한 뒤 이를 참고하여 이어지는 내용을 작성했지만 이렇게 할 시 기본적인 사건 등 소설의 주요 갈등이 요약해서 등장하기 때문에 소설의 분기점을 나누려는 해당 프로젝트의 로직으로 사용하기에 맞지 않다고 판단했다.</p>
<p>따라서, 소설의 구성 요소를 생성한 뒤 이를 참고하여 프롤로그를 시작으로 발단, 전개, 위기, 절정, 결말의 순서대로 소설의 구현하는 것이 분기점을 넣는 것, 소설의 일관성을 유지하는데 더 맞다고 판단하여 로직을 변경할 예정이다.</p>
<hr>
<p><img src="https://velog.velcdn.com/images/hyeo_71/post/04a92a64-27ee-435e-82e0-c969f11acb44/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240530]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240530</link>
            <guid>https://velog.io/@hyeo_71/TIL-240530</guid>
            <pubDate>Thu, 30 May 2024 17:08:23 GMT</pubDate>
            <description><![CDATA[<p>작성중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240529]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240529</link>
            <guid>https://velog.io/@hyeo_71/TIL-240529</guid>
            <pubDate>Wed, 29 May 2024 14:49:49 GMT</pubDate>
            <description><![CDATA[<h1 id="최종-프로젝트-mvp-발표-및-피드백">최종 프로젝트 MVP 발표 및 피드백</h1>
<blockquote>
<ol>
<li>README 작성 시, Frontend, Backend 로 나눠 작성되어 있어 내용을 확인하기에 좋았습니다. 지금까지 진행한 프로젝트 현황에 대한 내용 보충이 필요할 거 같습니다.</li>
<li>현재 소설 생성 결과가 영어로만 나오는데, 번역 기능을 추가할 생각할 생각이 있으신지?</li>
<li>웹페이지 시연 중에 페이지 로딩시간이 오래 걸리는 것에 대한 문제 <ul>
<li>CPU 점유율 높아서 생기는 문제로 웹페이지의 문제는 아님.    </li>
</ul>
</li>
<li>소설을 생성하는 과정에서 AI prompt 요청했을 때 소설 전체를 생성하는지 레이아웃으로 나눠 생성되는지를 알 수가 없습니다. 어떻게 작업이 진행되는지? 또한 README 내에 어떤 방식으로 나오는지에 대한 추가 설명이 필요합니다.</li>
</ol>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240528]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240528</link>
            <guid>https://velog.io/@hyeo_71/TIL-240528</guid>
            <pubDate>Tue, 28 May 2024 12:55:59 GMT</pubDate>
            <description><![CDATA[<h1 id="profile-api">Profile API</h1>
<p>dj_rest_auth를 사용하면서 profile은 이전에 <code>UserDetailSerializer</code>를 상속하여 만든 <code>CustomUserDetailSerializer</code>를 <code>accounts/user</code>에서 사용했지만 해당 serializer를 <code>LoginSerializer</code>가 같이 사용하는 것을 느꼈다.</p>
<p>Login을 하는 경우 access, refresh, user를 반환하는데 user모델의 필드를 전부 가져오기 때문에 로그인을 했을 때만 보여주고 싶은 user의 field(id, email, nickname)를 따로 구현하는 것이 쉽지 않았다.</p>
<p><code>LoginSerializer</code>를 상속해서 이를 해결하려했지만 유저를 받아오는 과정에서 settings.py에서 설정한 <code>USER_DETAILS_SERIALIZER</code>를 사용하여 가져오는 듯하다.</p>
<pre><code class="language-python">{
    &quot;access&quot;: &quot;(access_token)&quot;,
    &quot;refresh&quot;: &quot;(refresh_token)&quot;,
    &quot;user&quot;: {
        &quot;id&quot;: 2,
        &quot;email&quot;: &quot;admin@test.com&quot;,
        &quot;nickname&quot;: &quot;&quot;
          &quot;books&quot;: []
    }
}</code></pre>
<p>고민하다가 결국 기존에 존재하는 <code>accounst/user</code>를 profile로 사용하지 않고 <code>ProfileAPIView</code>를 새로 구현하기로 했다.</p>
<pre><code class="language-python">class ProfileAPIView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request):
        user = request.user
        if not user:
            return Response(
                {&quot;message&quot;: &quot;해당 유저는 존재하지 않습니다.&quot;},
                status=status.HTTP_400_BAD_REQUEST,
            )
        serializer = ProfileSerializer(user)
        return Response(serializer.data, status=status.HTTP_200_OK)

    def put(self, request):
        user = request.user
        serializer = ProfileSerializer(user, data=request.data, partial=True)
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(serializer.data, status=status.HTTP_200_OK)

    def delete(self, request, *args, **kwargs):
        user = self.request.user

        try:
            refresh_token = request.data.get(&quot;refresh_token&quot;)
            if refresh_token:
                token = RefreshToken(refresh_token)
                token.blacklist()
            else:
                return Response(
                    {&quot;message&quot;: &quot;refresh_token이 제공되지 않았습니다.&quot;},
                    status=status.HTTP_400_BAD_REQUEST,
                )
        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST)

        try:
            # 사용자 비활성화
            # user.is_active=False
            # user.save()
            user.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)
        except Exception as e:
            return Response(status=status.HTTP_400_BAD_REQUEST)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240527]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240527</link>
            <guid>https://velog.io/@hyeo_71/TIL-240527</guid>
            <pubDate>Mon, 27 May 2024 11:31:40 GMT</pubDate>
            <description><![CDATA[<h1 id="synopsis-summary-refactor">Synopsis, Summary refactor</h1>
<h2 id="synopsis">Synopsis</h2>
<p>기존에 만든 SynopsisAPIView를 삭제하고 해당 기능을 BookListAPIView의 post method로 조정</p>
<p>Synopsis는 책의 뼈대가 되는 배경과 소설책의 제목을 반환하기 때문에 책의 제목을 저장하는 Book table에 넣는 것이 올바르다고 판단하여 BookListAPIView의 API endpoint를 사용</p>
<pre><code class="language-python">class BookListAPIView(ListAPIView):
    # 전체 목록 조회
    queryset = Book.objects.all().order_by(&quot;-created_at&quot;)
    serializer_class = BookSerializer

    # 새 소설 책 생성
    def post(self, request):
        user_prompt = request.data.get(&quot;prompt&quot;)
        if not user_prompt:
            return Response(
                {&quot;error&quot;: &quot;Missing prompt&quot;}, status=status.HTTP_400_BAD_REQUEST
            )

        content = synopsis_generator(user_prompt)  # ai로 title, synopsis 생성
        title = content[&quot;title&quot;]
        synopsis = content[&quot;synopsis&quot;]
        serializer = BookSerializer(
            data={&quot;title&quot;: title, &quot;user_id&quot;: request.user.pk}
        )  # db에 title, user_id 저장
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            return Response(
                data={&#39;book_id&#39;:serializer.data[&#39;id&#39;],&quot;content&quot;: synopsis},  # FE에 content 응답
                status=status.HTTP_201_CREATED,
            )</code></pre>
<h2 id="summary">Summary</h2>
<p>소설책의 내용을 생성하는 SummaryAPIView는 생성하는 소설의 내용이 Book table의 소설과 같은 book_id를 가져야하기 때문에 이를 url에서 받는 BookDetailView의 post method에 구현하는 것으로 조정</p>
<pre><code class="language-python">def post(self, request, book_id):
        summary = request.data.get(&quot;summary&quot;)
        if not summary:
            return Response(
                {&quot;error&quot;: &quot;Missing summary prompt&quot;}, status=status.HTTP_400_BAD_REQUEST
            )
        result = summary_generator(summary)
        book=get_object_or_404(Book, id=book_id)
        serializer = ChapterSerializer(
            data={&quot;content&quot;: result[&quot;final_summary&quot;], &quot;book_id&quot;: book.pk}
        )
        if serializer.is_valid(raise_exception=True):
            serializer.save()
            result[&#39;book_id&#39;]=book.pk
            return Response(data=result, status=status.HTTP_201_CREATED)</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240524]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240524</link>
            <guid>https://velog.io/@hyeo_71/TIL-240524</guid>
            <pubDate>Fri, 24 May 2024 08:54:45 GMT</pubDate>
            <description><![CDATA[<h1 id="chapter-model">Chapter model</h1>
<p>기존 Book model의 content를 지우고 해당 테이블에는 id, 책 제목, 작성자, 좋아요, 작성날짜, 수정날짜만을 가진 생성한 소설책을 저장하고 이에 대한 내용은 chapter를 사용하여 저장하는 방법을 사용</p>
<table>
<thead>
<tr>
<th align="center">book_id</th>
<th align="center">chapter_num</th>
<th align="center">content</th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center">1</td>
<td align="center">xxx</td>
</tr>
<tr>
<td align="center">1</td>
<td align="center">2</td>
<td align="center">xxx</td>
</tr>
<tr>
<td align="center">1</td>
<td align="center">3</td>
<td align="center">xxx</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">1</td>
<td align="center">xxx</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center">2</td>
<td align="center">xxx</td>
</tr>
</tbody></table>
<p>위와 같은 형식
chapter_num는 id 처럼 자동으로 증가하되 book_id가 같을 경우에만 자동으로 증가하고 새로운 book_id가 생성되면 1로 초기화</p>
<pre><code class="language-python">class Chapter(models.Model):
    chapter_num = models.PositiveIntegerField(editable=False)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    book_id = models.ForeignKey(Book, on_delete=models.CASCADE, related_name=&quot;chapters&quot;)

    def save(self, *args, **kwargs):
        if not self.id:
            last_index = (
                Chapter.objects.filter(book_id=self.book_id).order_by(&quot;index&quot;).last()
            )
            if last_index:
                self.chapter_num = last_index.chapter_num + 1
            else:
                self.chapter_num = 1
        super(Chapter, self).save(*args, **kwargs)
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240523]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240523</link>
            <guid>https://velog.io/@hyeo_71/TIL-240523</guid>
            <pubDate>Thu, 23 May 2024 12:13:23 GMT</pubDate>
            <description><![CDATA[<h1 id="summary-구현하기">Summary 구현하기</h1>
<p>이전에 구현한 synopsys를 토대로 소설의 줄거리를 구현하고 출력된 소설의 다음 방향성을 결정하는 선택지 3개를 생성하여 프론트엔드에 데이터 넘겨주기</p>
<pre><code class="language-python">def summary_generator(summary):
    llm=ChatOpenAI(model=&#39;gpt-3.5-turbo&#39;, api_key=os.getenv(&#39;OPENAI_API_KEY&#39;))

    memory=ConversationSummaryBufferMemory(llm=llm, max_token_limit=20000, memory_key=&#39;chat_history&#39;, return_messages=True)

    summary_template = ChatPromptTemplate.from_messages([
        (&quot;system&quot;, &quot;You are an experienced novelist. Write a concise, realistic, and engaging summary based on the provided theme and previous context. Develop the characters, setting, and plot with rich descriptions. Ensure the summary flows smoothly, highlighting both hope and despair. Make the narrative provocative and creative. Avoid explicit reader interaction prompts or suggested paths.&quot;),
        MessagesPlaceholder(variable_name=&quot;chat_history&quot;),
        (&quot;human&quot;, &quot;{prompt}&quot;)
    ])

    recommend_template = ChatPromptTemplate.from_messages([
        (&quot;system&quot;, &quot;Based on the current summary prompt, provide three compelling recommendations for the next part of the summary. Your recommendations should emphasize hopeful, tragically hopeless, and starkly realistic choices, respectively. Be extremely contextual and realistic with your recommendations. Each recommendation should have &#39;Title&#39;: &#39;Description&#39;. For example: &#39;Title&#39;: &#39;The Beginning of a Tragedy&#39;,&#39;Description&#39;: &#39;The people are kind to the new doctor in town, but under the guise of healing their wounds, the doctor slowly conducts experiments.&#39; The response format is exactly the same as the frames in the example.&quot;),
        MessagesPlaceholder(variable_name=&quot;chat_history&quot;),
        (&quot;human&quot;, &quot;{current_story}&quot;)
    ])

    def load_memory():
        return memory.load_memory_variables({})[&quot;chat_history&quot;]

    def parse_recommendations(recommendation_text):
        recommendations = []
        try:
            rec_lines = recommendation_text.split(&#39;\n&#39;)
            title, description = None, None
            for line in rec_lines:
                if line.startswith(&quot;Title:&quot;):
                    if title and description:
                        recommendations.append(
                            {&quot;Title&quot;: title, &quot;Description&quot;: description})
                    title = line.split(&quot;Title:&quot;, 1)[1].strip()
                    description = None
                elif line.startswith(&quot;Description:&quot;):
                    description = line.split(&quot;Description:&quot;, 1)[1].strip()
                    if title and description:
                        recommendations.append(
                            {&quot;Title&quot;: title, &quot;Description&quot;: description})
                        title, description = None, None
                if len(recommendations) == 3:
                    break
        except Exception as e:
            logging.error(f&quot;Error parsing recommendations: {e}&quot;)

        return recommendations

    def generate_recommendations(chat_history, current_story):
        formatted_recommendation_prompt = recommend_template.format(
            chat_history=chat_history, current_story=current_story)
        recommendation_result = llm.invoke(formatted_recommendation_prompt)
        recommendations = parse_recommendations(recommendation_result.content)
        return recommendations

    def remove_recommendation_paths(final_summary):
        pattern = re.compile(r&#39;Recommended summary paths:.*$&#39;, re.DOTALL)
        cleaned_story = re.sub(pattern, &#39;&#39;, final_summary).strip()
        return cleaned_story


    chat_history = load_memory()
    prompt = f&quot;&quot;&quot;
    Story Prompt: {summary}
    Previous Story: {chat_history}
    Write a concise, realistic, and engaging summary based on the above information. Highlight both hope and despair in the narrative. Make it provocative and creative.
    &quot;&quot;&quot;

    formatted_final_prompt = summary_template.format(chat_history=chat_history, prompt=prompt)
    result = llm.invoke(formatted_final_prompt)
    memory.save_context({&quot;input&quot;: prompt}, {&quot;output&quot;: result.content})

    cleaned_story = remove_recommendation_paths(result.content)
    recommendations = generate_recommendations(chat_history, result.content)

    return {&quot;final_summary&quot;: cleaned_story, &quot;recommendations&quot;: recommendations}</code></pre>
<p>memory를 사용하여 소설의 내용이 이어지도록 다음 summary를 생성하도록 한다. ConversationSummaryBufferMemory를 사용하여 일정 토큰까지는 결과값을 그대로 저장하고 토큰의 값보다 많이 저장되기 시작하면 오래된 데이터를 요약하여 저장하면서 소설의 내용이 뜬금없는 값이 되지 않도록 한다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240522]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240522</link>
            <guid>https://velog.io/@hyeo_71/TIL-240522</guid>
            <pubDate>Wed, 22 May 2024 11:32:00 GMT</pubDate>
            <description><![CDATA[<h1 id="시놉시스-생성하기">시놉시스 생성하기</h1>
<p>AI를 사용하여 소설을 만들기 위해 소설의 기본적인 배경을 생성하고 그에 맞게 소설을 생성하기 위해 시놉시스를 AI가 생성하도록 하려한다.</p>
<p>본인이 평소에 관심이 있는 소설과 원하는 장르를 입력하여 AI가 적당한 시놉시스를 만드는 것을 목표로 한다.</p>
<p>랭체인을 사용하여 example예시를 주고 AI에게 예시에 맞는 대답을 하도록 설정하고 시놉시스를 생성한다.</p>
<pre><code class="language-python">llm = ChatOpenAI(
    model=&quot;gpt-3.5-turbo&quot;, api_key=os.getenv(&quot;OPENAI_API_KEY&quot;), max_tokens=800
)

examples = [
    {
        &quot;novel&quot;: &quot;Harry Potter&quot;,
        &quot;genre&quot;: &quot;fantasy&quot;,
        &quot;answer&quot;: &quot;&quot;&quot;
            (The answer of the AI you want)
        &quot;&quot;&quot;,
    },
    {
        &quot;novel&quot;: &quot;Miracles of the Namiya General Store&quot;,
        &quot;genre&quot;: &quot;mystery&quot;,
        &quot;answer&quot;: &quot;&quot;&quot;
            (The answer of the AI you want)
        &quot;&quot;&quot;,
    },
]

example_prompt = ChatPromptTemplate.from_messages(
    [
        (
            &quot;human&quot;,
            &quot;I&#39;m going to make a new novel by referring to the novel {novel}. I&#39;m going to make the genre {genre}. Please make a synopsis.&quot;,
        ),
        (&quot;ai&quot;, &quot;{answer}&quot;),
    ]
)

example_prompt = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

final_prompt = ChatPromptTemplate.from_messages(
    [
        (&quot;system&quot;, &quot;You are an expert in fiction&quot;),
        example_prompt,
        (
            &quot;human&quot;,
            &quot;I&#39;m going to make a new novel by referring to the novel {novel}. I&#39;m going to make the genre {genre}. Please make a synopsis.&quot;,
        ),
    ]
)

chain = final_prompt | llm

chain.invoke({&quot;novel&quot;: &quot;twilight&quot;, &quot;genre&quot;: &quot;Romance&quot;})</code></pre>
<blockquote>
<p>In the small town of Moonlight Falls, a new student named Luna arrives at Moonlight High School. Luna is mysterious and keeps to herself, but she captures the attention of the popular and enigmatic student, Blake. As they get to know each other, Luna reveals that she is not entirely human - she is a descendant of a powerful supernatural bloodline that has been kept secret for generations. Blake is drawn to Luna&#39;s otherworldly charm and the two develop a deep connection despite the dangers that come with Luna&#39;s true identity. However, their forbidden love is put to the test when a rival supernatural clan threatens to expose Luna&#39;s secret and tear them apart. Luna and Blake must navigate the treacherous world of supernatural politics and fight for their love against all odds.</p>
</blockquote>
<blockquote>
<p>Moonlight Falls라는 작은 마을에서 루나라는 이름의 새로운 학생이 Moonlight High School에 도착합니다. 루나는 신비롭고 비밀에 부쳐지지만, 그녀는 인기 있고 수수께끼 같은 학생인 블레이크의 관심을 사로잡습니다. 그들이 서로에 대해 알게 되면서, 루나는 완전히 인간이 아니라는 것을 드러냅니다. 블레이크는 루나의 세속적인 매력에 매료되고, 루나의 진짜 정체성에 따르는 위험에도 불구하고 두 사람은 깊은 관계를 발전시킵니다. 그러나, 그들의 금지된 사랑은 라이벌 초자연적인 일족이 루나의 비밀을 폭로하고 그들을 찢어버리겠다고 위협할 때 시험대에 오릅니다. 루나와 블레이크는 초자연적인 정치의 위험한 세계를 탐색하고 모든 역경에 맞서 그들의 사랑을 위해 싸워야 합니다.```</p>
</blockquote>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240521]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240521</link>
            <guid>https://velog.io/@hyeo_71/TIL-240521</guid>
            <pubDate>Tue, 21 May 2024 12:12:12 GMT</pubDate>
            <description><![CDATA[<p>작성중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240520]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240520</link>
            <guid>https://velog.io/@hyeo_71/TIL-240520</guid>
            <pubDate>Mon, 20 May 2024 11:37:52 GMT</pubDate>
            <description><![CDATA[<p>작성중</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240517]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240517</link>
            <guid>https://velog.io/@hyeo_71/TIL-240517</guid>
            <pubDate>Fri, 17 May 2024 11:52:48 GMT</pubDate>
            <description><![CDATA[<p>git diff &gt; test.patch
git apply </p>
<p>patch -p1 --dry-run &lt; test.patch</p>
<p>$ patch -p1 &lt; test.patch</p>
<p>패치 적용하기
곧바로 패치를 적용해도 되지만, 문제가 없는지 테스트부터 하기를 권합니다.</p>
<p>$ patch -p1 --dry-run &lt; test.patch
실패하지 않으면 실제로 적용합니다.</p>
<p>$ patch -p1 &lt; test.patch
※ -p1은 패치 파일에 파일명이 등장할 때마다 prefix가 있음을 알려주는 옵션입니다. git diff에서 파일명에 prefix를 붙이는 게 기본값이기 때문입니다. prefix를 안 붙이려면 git diff에 --no-prefix 옵션을 주면 되고요, 이렇게 만든 패치 파일은 적용할 때 -p1이 아니라 -p0 옵션을 쓰면 됩니다.</p>
<p>나중에 정리예정</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240516]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240516</link>
            <guid>https://velog.io/@hyeo_71/TIL-240516</guid>
            <pubDate>Thu, 16 May 2024 11:25:48 GMT</pubDate>
            <description><![CDATA[<h1 id="langchain">Langchain</h1>
<h2 id="메모리를-chain에-꽂는-방법-2">메모리를 chain에 꽂는 방법 2</h2>
<h3 id="langchain-express-사용하기변경작업을-할-때-권장하는-방법">langchain express 사용하기(변경작업을 할 때 권장하는 방법)</h3>
<p><img src="https://velog.velcdn.com/images/hyeo_71/post/550a7f57-2815-4b1d-b3a8-1a701b9fec26/image.png" alt=""></p>
<p>LLMChain은 메모리를 자동으로 저장해줘서 메모리 저장에 필요한 추가 코드를 작성할 필요가 없는 것이 장점이자 단점(사용자가 필요하지 않은 부분도 저장할 가능성이 있음)
(반자동, 자동으로 LLM으로부터 응답값을 가져오고 memory 업데이트, prompt에 기록을 넣는 건 수동)</p>
<p>LCEL은 메모리를 저장하기 위한 코드를 작성해야 하지만 그 외의 다양한 메세지를 전달하는 등의 작업의 자유도가 높아 확장성이 좋음
(수동)</p>
<p><a href="https://github.com/Juunsik/FULLSTACK-GPT/blob/3b633247a0b8b56fdd55946b8a96d5b0390448b8/notebook.ipynb">Github</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240514]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240515</link>
            <guid>https://velog.io/@hyeo_71/TIL-240515</guid>
            <pubDate>Tue, 14 May 2024 11:17:50 GMT</pubDate>
            <description><![CDATA[<h1 id="langchain">Langchain</h1>
<h2 id="메모리를-chain에-꽂는-방법-1">메모리를 chain에 꽂는 방법 1</h2>
<p>대화 기록을 추가하는 방식</p>
<h2 id="llm-chain">LLM chain</h2>
<blockquote>
<p>off-the-shelf chain(일반적인 목적을 가진 chain)</p>
</blockquote>
<p>스스로 무언가를 만들 때는 off-the-shelf chain보다는 우리가 직접 커스텀해서 만든 chain을 활용하기를 선호</p>
<p>off-the-shelf chain은 빠르게 시작할 수 있어 좋긴 하지만 off-the-shelf을 커스텀하기보다
직접 만들고 싶을 땐 langchain expression 언어를 활용해서 우리의 것을 만들 수 있음</p>
<p>memory class는 memory를 두가지 방식으로 출력할 수 있다.(문자열 형태, 메세지 형태)</p>
<h3 id="string">string</h3>
<p><a href="https://github.com/Juunsik/FULLSTACK-GPT/blob/c50376d6392f36e3a8a1c0b08a085d51397a2f2d/notebook.ipynb">string</a></p>
<h3 id="message">message</h3>
<p>메세지를 기반으로 한 사람과 ai의 대화 기록을 추가하는 방법
<a href="">message</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240513]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240513</link>
            <guid>https://velog.io/@hyeo_71/TIL-240513</guid>
            <pubDate>Mon, 13 May 2024 13:40:26 GMT</pubDate>
            <description><![CDATA[<h1 id="langchain">Langchain</h1>
<h2 id="conversationkgmemory">ConversationKGMemory</h2>
<blockquote>
<p>Conversation Knowledge Graph Memory
대화 중 entity의 knwoledge graph를 만듬
가장 중요한 것들만 뽑아내는 요약본</p>
</blockquote>
<p>knowledge graph에서 히스토리를 가져오지 않고 엔티티를 가져옴</p>
<p><img src="https://velog.velcdn.com/images/hyeo_71/post/0fbd563e-5d5a-4326-afed-29fb5a0d84eb/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[TIL 240510]]></title>
            <link>https://velog.io/@hyeo_71/TIL-240510</link>
            <guid>https://velog.io/@hyeo_71/TIL-240510</guid>
            <pubDate>Fri, 10 May 2024 11:33:27 GMT</pubDate>
            <description><![CDATA[<h1 id="kpt">KPT</h1>
<h2 id="keeping">Keeping</h2>
<ul>
<li>팀원 간의 불화가 일어나지 않아 좋았다고 생각함</li>
<li>기본적인 CRUD, 해당 기능에 대한 권한처리가 잘 되었다고 생각함</li>
</ul>
<h2 id="problem">Problem</h2>
<ul>
<li><p>branch 명을 모두가 알아보기 쉽게 앱 or 기능별로 나눠서 branch명을 지었으면 좋았겠다고 생각함</p>
</li>
<li><p>피드백 과정에서 나왔지만 AI News 생성에서 class로 구현한 기능을 파일을 나눠 사용하는 과정에서 함수로 바꾸면서 함수명을 코드 컨벤션에 맞게 “snake_case”로 바꾸지 못한게 아쉬움</p>
</li>
<li><p>의사 소통이 더 원활히 됐다면 기능을 더 추가하거나 기존 코드, ERD의 보완을 더 할 수 있었을 거라는 아쉬움이 있음</p>
</li>
</ul>
<h2 id="try">Try</h2>
<ul>
<li><p>Django에서 기본적으로 지원하는 password validation을 custom하여 password validation을 만들어보기</p>
</li>
<li><p>다른 AI 모델을 사용해서 AI News 생성 기능 개선하기</p>
</li>
</ul>
]]></description>
        </item>
    </channel>
</rss>