<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Readme on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/readme/</link><description>Recent content in Readme on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Fri, 03 Apr 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/readme/index.xml" rel="self" type="application/rss+xml"/><item><title>PopCon 개발기 #2 — 브랜딩, README, Docker 디버깅, 그리고 재시도 로직</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-03-popcon-dev2/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-03-popcon-dev2/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post PopCon 개발기 #2 — 브랜딩, README, Docker 디버깅, 그리고 재시도 로직" /&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/ko/posts/2026-04-02-popcon-dev1/" &gt;이전 글: PopCon 개발기 #1&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;오늘은 PopCon의 &amp;ldquo;겉모습&amp;quot;과 &amp;ldquo;안정성&amp;quot;을 모두 다듬은 세션이었다. 오전에는 Gemini가 생성한 이미지를 로고와 파비콘으로 변환하고, GitHub에 공개할 수 있는 수준의 README를 작성했다. 오후에는 Docker에서 API 키 오류가 터졌고, 파이프라인 전체를 점검하며 재시도 로직과 이모지별 에러 핸들링까지 구현했다.&lt;/p&gt;
&lt;h2 id="1-로고--파비콘--gemini-이미지를-브랜드-에셋으로"&gt;1. 로고 &amp;amp; 파비콘 — Gemini 이미지를 브랜드 에셋으로
&lt;/h2&gt;&lt;p&gt;첫 번째 작업은 Gemini가 생성한 2880×1440 이미지를 PopCon 브랜드 에셋으로 만드는 것이었다. 이미지를 중앙 기준 1:1 정사각형으로 크롭한 뒤, 여러 크기로 변환했다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;파일&lt;/th&gt;
 &lt;th&gt;크기&lt;/th&gt;
 &lt;th&gt;용도&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;logo.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;512×512&lt;/td&gt;
 &lt;td&gt;헤더 로고&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon.ico&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;16/32/48 멀티 사이즈&lt;/td&gt;
 &lt;td&gt;브라우저 탭 아이콘&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon-16x16.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;16×16&lt;/td&gt;
 &lt;td&gt;소형 파비콘&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;favicon-32x32.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;32×32&lt;/td&gt;
 &lt;td&gt;표준 파비콘&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;apple-touch-icon.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;180×180&lt;/td&gt;
 &lt;td&gt;iOS 홈 화면&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;icon-192.png&lt;/code&gt; / &lt;code&gt;icon-512.png&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;192×192 / 512×512&lt;/td&gt;
 &lt;td&gt;PWA 아이콘&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="docker에서-파비콘이-안-보이는-문제"&gt;Docker에서 파비콘이 안 보이는 문제
&lt;/h3&gt;&lt;p&gt;파비콘을 교체했는데 브라우저에 반영이 안 됐다. 원인이 두 가지였다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Next.js App Router 우선순위&lt;/strong&gt;: &lt;code&gt;app/favicon.ico&lt;/code&gt;가 &lt;code&gt;public/favicon.ico&lt;/code&gt;보다 우선한다. 이미 기본 파비콘이 &lt;code&gt;app/&lt;/code&gt; 디렉토리에 있었고, 이 파일을 교체해야 했다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker 이미지 캐시&lt;/strong&gt;: &lt;code&gt;COPY . .&lt;/code&gt;로 빌드 시점에 파일을 굽기 때문에, 파일을 수정해도 컨테이너를 재빌드하지 않으면 반영되지 않는다.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;docker compose build frontend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker compose up -d frontend
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;재빌드 후 &lt;code&gt;curl -I localhost:3000/favicon.ico&lt;/code&gt;로 HTTP 200을 확인하고 마무리했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="2-전체-제품-readme-작성"&gt;2. 전체 제품 README 작성
&lt;/h2&gt;&lt;p&gt;다음은 GitHub에 공개할 README를 제품 개요 수준으로 작성하는 작업이었다.&lt;/p&gt;
&lt;p&gt;처음에는 영어·한국어를 하나의 파일에 넣었는데, &amp;ldquo;두 언어가 구분이 안 된다&amp;quot;는 피드백이 나왔다. 결과적으로 파일을 분리했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt; — 영어, 상단에 &lt;code&gt;English | [한국어](README.ko.md)&lt;/code&gt; 토글&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.ko.md&lt;/code&gt; — 한국어, 상단에 &lt;code&gt;[English](README.md) | 한국어&lt;/code&gt; 토글&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="readme-내용이-코드와-달랐던-부분들"&gt;README 내용이 코드와 달랐던 부분들
&lt;/h3&gt;&lt;p&gt;첫 커밋 후 실제 코드를 확인하니 README와 다른 점이 여러 곳 있었다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;항목&lt;/th&gt;
 &lt;th&gt;README 작성 내용&lt;/th&gt;
 &lt;th&gt;실제 코드&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;이미지 생성 모델&lt;/td&gt;
 &lt;td&gt;Google Imagen&lt;/td&gt;
 &lt;td&gt;Gemini Flash Image&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;VEO 모드&lt;/td&gt;
 &lt;td&gt;듀얼 프레임 I2V&lt;/td&gt;
 &lt;td&gt;시작 프레임 + 모션 프롬프트만 (API 미지원)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;최소 영상 길이&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;4초 이내&amp;rdquo;&lt;/td&gt;
 &lt;td&gt;4초 고정 (API 최소값), 후처리에서 트리밍&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;전처리 단계&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;td&gt;크롭 → 정사각 패딩 → 512×512 리사이즈&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;작업 저장소&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;td&gt;Redis (24시간 TTL)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;누락 엔드포인트&lt;/td&gt;
 &lt;td&gt;없음&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;/api/job/{job_id}/emoji/{filename}&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;두 README 파일 모두 업데이트 후 푸시했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="3-docker-디버깅--api-키와-씨름한-오후"&gt;3. Docker 디버깅 — API 키와 씨름한 오후
&lt;/h2&gt;&lt;p&gt;오후 세션은 Docker 로그부터 시작했다. 서비스가 전혀 동작하지 않았는데, 원인은 &lt;code&gt;.env&lt;/code&gt; 파일의 API 키 값 뒤에 &lt;code&gt; venv&lt;/code&gt;가 붙어 있었던 것이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POPCON_GOOGLE_API_KEY=AIzaSy...-mAcuv venv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;아마 터미널에서 복사할 때 &lt;code&gt;venv&lt;/code&gt; 활성화 명령어가 함께 딸려온 것으로 추정된다. &lt;code&gt;.env&lt;/code&gt;에서 &lt;code&gt; venv&lt;/code&gt;를 제거하고 재시작했지만, 이번엔 키 자체가 만료된 것으로 확인됐다. Google AI Studio에서 새 키를 발급받아 해결했다.&lt;/p&gt;
&lt;p&gt;이 과정에서 파이프라인을 실제로 돌려보면서 여러 품질 문제를 발견했다.&lt;/p&gt;
&lt;h3 id="발견한-문제들과-수정"&gt;발견한 문제들과 수정
&lt;/h3&gt;&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 A["파이프라인 실행"] --&gt; B["출력 검토"]
 B --&gt; C["문제 발견"]
 C --&gt; D["구름 효과 &amp;lt;br/&amp;gt; 배경 노이즈"]
 C --&gt; E["중복 캐릭터 &amp;lt;br/&amp;gt; 스프라이트 시트"]
 C --&gt; F["체커보드 배경 &amp;lt;br/&amp;gt; NB2 가짜 투명"]
 C --&gt; G["VEO 엣지 라인 &amp;lt;br/&amp;gt; 좌우 경계선"]
 D --&gt; H["rembg 완전 제거 &amp;lt;br/&amp;gt; 밝기 기반 크롭으로 대체"]
 E --&gt; I["프롬프트에 &amp;lt;br/&amp;gt; 단일 캐릭터 강제"]
 F --&gt; J["#FFFFFF 명시 &amp;lt;br/&amp;gt; NOT checkerboard 프롬프트"]
 G --&gt; K["ffmpeg 2% 엣지 크롭"]&lt;/pre&gt;&lt;p&gt;가장 큰 결정은 &lt;code&gt;rembg&lt;/code&gt; 라이브러리를 완전히 제거한 것이다. 배경 제거를 시도할수록 문제가 생겼다 — &lt;code&gt;isnet-general-use&lt;/code&gt; 모델이 구름 같은 아티팩트를 남겼고, &lt;code&gt;u2net&lt;/code&gt;으로 바꿔도 마찬가지였다. 결국 VEO가 흰 배경으로 영상을 생성하도록 프롬프트를 강화하고, 밝기 기반 크롭으로 콘텐츠 영역만 추출하는 방향으로 전환했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# processor.py — 밝기 기반 콘텐츠 감지&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;content_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;brightness&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;245&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;에서 &lt;code&gt;rembg[cpu]&amp;gt;=2.0.0&lt;/code&gt;을 제거하고 &lt;code&gt;numpy&amp;gt;=1.26.0&lt;/code&gt;으로 교체하면서 Docker 이미지도 가벼워졌다.&lt;/p&gt;
&lt;h3 id="line-규격-파일-명명-수정"&gt;LINE 규격 파일 명명 수정
&lt;/h3&gt;&lt;p&gt;LINE Creators Market 가이드라인을 확인하니 파일명이 &lt;code&gt;001.png&lt;/code&gt;~&lt;code&gt;040.png&lt;/code&gt; 형식이어야 했다. 기존 코드는 액션 이름으로 저장하고 있었다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# packager.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emoji_path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emoji_paths&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;line_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;03d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;zf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emoji_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;
&lt;h2 id="4-재시도-로직--api-스로틀링"&gt;4. 재시도 로직 &amp;amp; API 스로틀링
&lt;/h2&gt;&lt;p&gt;마지막 커밋은 안정성 강화였다. 풀 세트(24개)를 생성하다 보면 VEO나 Gemini API가 간헐적으로 503이나 429를 반환했다. 하나가 실패하면 전체 작업이 멈추는 구조를 개선했다.&lt;/p&gt;
&lt;h3 id="이모지별-독립적인-에러-처리"&gt;이모지별 독립적인 에러 처리
&lt;/h3&gt;&lt;p&gt;기존 구조는 하나의 이모지가 예외를 던지면 전체 &lt;code&gt;run_emoji_generation&lt;/code&gt; 태스크가 실패했다. 이를 각 이모지별로 try/except를 적용해서 실패한 것만 &lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; 상태로 기록하고 나머지는 계속 진행하도록 변경했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# worker.py — 이모지별 에러 핸들링&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;failed_indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... 포즈 생성, 애니메이션, 후처리&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Emoji &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;failed_indices&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;save_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;작업 완료 상태도 세분화했다. &lt;code&gt;&amp;quot;done&amp;quot;&lt;/code&gt; 외에 &lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; 상태를 추가해서 일부 실패해도 ZIP을 다운로드할 수 있도록 했다.&lt;/p&gt;
&lt;h3 id="api-재시도-로직"&gt;API 재시도 로직
&lt;/h3&gt;&lt;p&gt;Gemini Image와 VEO 모두 지수 백오프 재시도를 추가했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# pose_generator.py — 재시도 로직&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_generate_image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference_image_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generate_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ServerError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="c1"&gt;# 1s, 2s, 4s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Attempt &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; failed, retrying in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="타입-시스템-동기화"&gt;타입 시스템 동기화
&lt;/h3&gt;&lt;p&gt;모델 상태 타입을 백엔드와 프론트엔드 모두 업데이트했다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;계층&lt;/th&gt;
 &lt;th&gt;변경&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;backend/models.py&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;EmojiStatus&lt;/code&gt;에 &lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; 추가, &lt;code&gt;JobStatusType&lt;/code&gt;에 &lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; 추가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/lib/api.ts&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;동일한 상태 타입 동기화&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/components/ProgressTracker.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;&amp;quot;error&amp;quot;&lt;/code&gt; 상태 빨간 카드 UI 추가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;frontend/components/EmojiPreview.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;&amp;quot;done_with_errors&amp;quot;&lt;/code&gt; 일 때도 ZIP 다운로드 버튼 표시&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id="정리"&gt;정리
&lt;/h2&gt;&lt;p&gt;오늘 작업한 커밋 4개의 흐름을 요약하면 이렇다.&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;순서&lt;/th&gt;
 &lt;th&gt;내용&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;1&lt;/td&gt;
 &lt;td&gt;로고/파비콘 생성, 브랜딩 에셋, 전체 제품 README&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2&lt;/td&gt;
 &lt;td&gt;README 영어/한국어 분리, 언어 토글&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;3&lt;/td&gt;
 &lt;td&gt;README를 실제 파이프라인 동작에 맞게 업데이트&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;4&lt;/td&gt;
 &lt;td&gt;재시도 로직, 이모지별 에러 처리, API 스로틀링&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Docker 디버깅을 하다 보니 파이프라인 품질 문제들도 많이 잡았다. 특히 &lt;code&gt;rembg&lt;/code&gt; 제거 결정은 &amp;ldquo;뭔가를 빼는 게 더 나은 경우&amp;quot;의 전형적인 사례였다 — 복잡성도 줄고, Docker 이미지도 가벼워지고, 결과물도 오히려 더 깔끔해졌다.&lt;/p&gt;
&lt;p&gt;다음 세션에서는 실제 LINE Creators Market 제출을 목표로 최종 품질 검증을 할 예정이다.&lt;/p&gt;</description></item></channel></rss>