<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Torch on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/torch/</link><description>Recent content in Torch 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/torch/index.xml" rel="self" type="application/rss+xml"/><item><title>Hybrid Image Search 개발기 #8 — 톤/앵글 S3 마이그레이션, EC2 배포 수정, Hex 컬러 추출</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-03-hybrid-search-dev8/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-03-hybrid-search-dev8/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Hybrid Image Search 개발기 #8 — 톤/앵글 S3 마이그레이션, EC2 배포 수정, Hex 컬러 추출" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/ko/posts/2026-04-02-hybrid-search-dev7/" &gt;이전 글: Hybrid Image Search 개발기 #7&lt;/a&gt;에서 LLM 기반 톤/앵글 카테고리 자동 주입을 구현했다. 이번 스프린트에서는 그 구현을 실제 배포 환경에서 안정적으로 작동하게 다듬는 데 집중했다.&lt;/p&gt;
&lt;p&gt;크게 세 축으로 작업이 진행됐다. 첫째, 로컬 파일시스템에 남아있던 카테고리 이미지 읽기 로직을 완전히 S3로 전환했다. 둘째, EC2 프로덕션 인스턴스에서 torch의 CUDA 의존성이 충돌하는 문제를 CPU-only 인덱스 핀으로 해결했다. 셋째, 톤 레퍼런스 이미지에서 지배색 hex 코드를 추출해 DB에 저장하고, 프롬프트 상세 UI에 컬러 스와치로 렌더링했다.&lt;/p&gt;
&lt;h2 id="톤앵글-카테고리-이미지--s3-읽기로-전환"&gt;톤/앵글 카테고리 이미지 — S3 읽기로 전환
&lt;/h2&gt;&lt;p&gt;이전 구현에서 &lt;code&gt;injection.py&lt;/code&gt;의 &lt;code&gt;_list_category_images()&lt;/code&gt;는 로컬 &lt;code&gt;data/tone_angle_image_ref/{category}/&lt;/code&gt; 폴더를 &lt;code&gt;os.listdir()&lt;/code&gt;으로 읽고 있었다. EC2 인스턴스에는 이 폴더가 없으므로 항상 빈 리스트가 반환되어 주입이 무력화되는 버그가 있었다.&lt;/p&gt;
&lt;p&gt;수정 방향은 명확했다. &lt;code&gt;S3Storage&lt;/code&gt; 인스턴스를 &lt;code&gt;select_auto_injection()&lt;/code&gt;에 전달하고, 카테고리 폴더 목록을 &lt;code&gt;s3.list_objects(prefix)&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;# 변경 전: 로컬 폴더를 직접 읽음&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_list_category_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;folder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TONE_ANGLE_IMAGE_DIR&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;category&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;folder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iterdir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;...&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# 변경 후: S3에서 prefix 기반으로 조회&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_list_category_images&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;S3Storage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;prefix&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;refs/tone_angle_image_ref/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&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;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_objects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&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;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;basename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IMAGE_EXTS&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;동시에 S3 키 캐시(&lt;code&gt;build_ref_key_cache&lt;/code&gt;) 구조도 수정해, &lt;code&gt;data/tone_angle_image_ref/a(natural,film)&lt;/code&gt; 같은 중첩 경로가 &lt;code&gt;refs/tone_angle_image_ref/a(natural,film)/{filename}&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;# storage.py — Path.relative_to(&amp;#34;data&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;ref_subdir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relative_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;data&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="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ref_key_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&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;refs/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ref_subdir&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;f&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;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="ec2-배포--torch-cpu-only-핀"&gt;EC2 배포 — torch CPU-only 핀
&lt;/h2&gt;&lt;p&gt;프로덕션 EC2 인스턴스에서 서버 기동 시 &lt;code&gt;libcudnn.so.9&lt;/code&gt; 파일을 찾지 못해 임베딩 모델 로드에 실패하는 문제가 발생했다. &lt;code&gt;sentence-transformers&lt;/code&gt;가 &lt;code&gt;torch&lt;/code&gt;를 의존성으로 끌어오는데, &lt;code&gt;uv&lt;/code&gt;가 CUDA 지원 torch를 설치하면서 실제로는 없는 CUDA 라이브러리를 참조하게 된 것이다.&lt;/p&gt;
&lt;p&gt;개발 환경에서는 &lt;code&gt;nvidia-cudnn-cu12&lt;/code&gt;와 &lt;code&gt;nvidia-cudnn-cu13&lt;/code&gt;이 모두 설치되어 있어 우연히 동작했지만, 프로덕션에는 &lt;code&gt;cu13&lt;/code&gt;만 있어 충돌이 발생했다.&lt;/p&gt;
&lt;p&gt;해결책은 개별 cudnn 패키지를 추적하는 대신, &lt;code&gt;pyproject.toml&lt;/code&gt;의 S3 인덱스 설정에서 CPU-only torch를 명시적으로 핀하는 것이었다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;# pyproject.toml — CPU-only torch 인덱스 추가&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 class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;index&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="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytorch-cpu&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;https://download.pytorch.org/whl/cpu&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;explicit&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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 class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sources&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="nx"&gt;torch&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pytorch-cpu&amp;#34;&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;uv sync&lt;/code&gt; 시 항상 CPU-only 빌드가 설치되어, EC2 인스턴스의 CUDA 환경 유무와 무관하게 안정적으로 동작한다.&lt;/p&gt;
&lt;h2 id="hex-컬러-추출--지배색-분석--db-저장"&gt;Hex 컬러 추출 — 지배색 분석 &amp;amp; DB 저장
&lt;/h2&gt;&lt;p&gt;톤 레퍼런스 이미지가 어떤 색감을 대표하는지 시각적으로 확인할 수 있도록, 이미지에서 지배색 hex 코드를 추출해 &lt;code&gt;generation_logs&lt;/code&gt; 테이블에 함께 저장하는 기능을 추가했다.&lt;/p&gt;
&lt;p&gt;아래 다이어그램은 데이터 흐름을 보여준다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 A["이미지 생성 요청"] --&gt; B["LLM 카테고리 분류"]
 B --&gt; C["S3에서 카테고리 이미지 목록 조회"]
 C --&gt; D["랜덤 이미지 선택 &amp;lt;br/&amp;gt; (tone + angle)"]
 D --&gt; E["지배색 hex 추출 &amp;lt;br/&amp;gt; (PIL + K-Means)"]
 E --&gt; F["generation_logs에 &amp;lt;br/&amp;gt; hex_colors 저장"]
 F --&gt; G["Gemini API 이미지 생성"]
 G --&gt; H["프론트엔드에 hex_colors 포함 응답"]
 H --&gt; I["구조화된 프롬프트 UI &amp;lt;br/&amp;gt; 컬러 스와치 렌더링"]&lt;/pre&gt;&lt;p&gt;지배색 추출은 &lt;code&gt;scikit-learn&lt;/code&gt;의 &lt;code&gt;KMeans&lt;/code&gt;를 사용해 이미지를 N개의 클러스터로 나누고, 각 클러스터의 중심색을 hex 형식으로 변환한다.&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;# hex_color_extractor.py (개념)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;sklearn.cluster&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_dominant_hex_colors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_colors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_bytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;RGB&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;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&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="n"&gt;pixels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&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 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="n"&gt;km&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;KMeans&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_clusters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;n_colors&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_init&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="n"&gt;km&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixels&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;centers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;km&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster_centers_&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;int&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;return&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;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;02x&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;02x&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;02x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;centers&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;추출된 hex 값은 &lt;code&gt;generation_logs.hex_colors&lt;/code&gt;(JSON 컬럼)에 저장되고, API 응답의 &lt;code&gt;InjectedReference.hex_colors&lt;/code&gt; 필드를 통해 프론트엔드로 전달된다.&lt;/p&gt;
&lt;h2 id="구조화된-프롬프트-ui--hex-스와치-포함"&gt;구조화된 프롬프트 UI — hex 스와치 포함
&lt;/h2&gt;&lt;p&gt;기존에 이미지 상세 모달의 &amp;ldquo;작업 프롬프트&amp;rdquo; 섹션은 &lt;code&gt;getFullPrompt()&lt;/code&gt;가 반환하는 원시 텍스트(마크다운 스타일 &lt;code&gt;###&lt;/code&gt;, &lt;code&gt;===&lt;/code&gt; 구분자, 날것의 JSON hex 배열)를 &lt;code&gt;whitespace-pre-wrap&lt;/code&gt;으로 그대로 뿌리고 있었다. 가독성이 매우 낮았다.&lt;/p&gt;
&lt;p&gt;이번에 &lt;code&gt;renderStructuredPrompt()&lt;/code&gt; 함수를 추가해, 같은 데이터를 구조화된 형태로 렌더링하도록 교체했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;###&lt;/code&gt; 헤딩 → 앰버/스카이 컬러 섹션 헤더&lt;/li&gt;
&lt;li&gt;&lt;code&gt;===&lt;/code&gt; 구분자 → &lt;code&gt;&amp;lt;hr&amp;gt;&lt;/code&gt; 엘리먼트&lt;/li&gt;
&lt;li&gt;&lt;code&gt;- 이미지 N:&lt;/code&gt; 줄 → 배지 + 설명 형식의 리스트 아이템&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hex_colors&lt;/code&gt; 배열 → 컬러 원형 + 모노 hex 코드 pill 배지&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;복사 기능은 기존 &lt;code&gt;fullPrompt&lt;/code&gt; raw 텍스트를 그대로 사용하므로 영향 없다.&lt;/p&gt;
&lt;h2 id="노-텍스트-디렉티브--컬러-팔레트-제거"&gt;노-텍스트 디렉티브 &amp;amp; 컬러 팔레트 제거
&lt;/h2&gt;&lt;p&gt;주입된 레퍼런스 이미지의 프롬프트에 &amp;ldquo;이미지의 텍스트나 워터마크를 생성에 반영하지 말 것&amp;quot;을 명시하는 no-text 디렉티브를 추가했다. 또한 이미지 카드 오버레이와 상세 모달에서 컬러 팔레트 점 표시(dot 시각화)를 제거했다. 구조화된 프롬프트 섹션의 hex 스와치가 그 역할을 충분히 대체하기 때문이다.&lt;/p&gt;
&lt;h2 id="커밋-로그"&gt;커밋 로그
&lt;/h2&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;fix: list tone/angle category images from S3 instead of local filesystem&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;injection.py&lt;/code&gt;, &lt;code&gt;storage.py&lt;/code&gt;, &lt;code&gt;generation.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: pin torch to CPU-only index to prevent broken CUDA deps on EC2&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: fix the injection prompt&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;prompt.py&lt;/code&gt;, &lt;code&gt;injection.py&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;docs: update README to reflect recent changes&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: extract dominant hex colors from tone reference images&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;injection.py&lt;/code&gt;, &lt;code&gt;schemas.py&lt;/code&gt;, &lt;code&gt;api.ts&lt;/code&gt;, DB migration&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: structured prompt display with hex color swatches in image detail&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;GeneratedImageDetail.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat: add no-text directive for injected refs and remove color palettes&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;prompt.py&lt;/code&gt;, &lt;code&gt;App.tsx&lt;/code&gt;, &lt;code&gt;GeneratedImageDetail.tsx&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;get rid of the test folder&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;test/&lt;/code&gt; 삭제&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;로컬 개발 환경과 프로덕션 환경의 차이를 코드로 명시하라.&lt;/strong&gt; S3 마이그레이션 이후에도 파일 목록 조회 코드가 로컬 경로를 참조하고 있었다. 이런 류의 버그는 개발 환경에서는 조용히 통과되다가 배포 직후에야 드러난다. 추상화 경계(여기서는 &lt;code&gt;S3Storage&lt;/code&gt;)를 일관되게 사용하는 것이 방어책이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CUDA 의존성은 명시적으로 핀해야 한다.&lt;/strong&gt; &lt;code&gt;torch&lt;/code&gt;는 환경에 따라 CPU/CUDA 빌드가 혼재될 수 있다. EC2처럼 GPU가 없는 인스턴스에서 CUDA 빌드가 설치되면 임포트 시점에 실패한다. &lt;code&gt;pyproject.toml&lt;/code&gt;의 &lt;code&gt;[[tool.uv.index]]&lt;/code&gt;로 CPU-only를 명시적으로 강제하면 이 클래스의 문제 전체를 예방할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;UI 렌더링과 데이터 직렬화는 분리해야 한다.&lt;/strong&gt; 복사 기능이 필요한 raw 텍스트와, 화면에 보여줄 구조화된 렌더링을 동일한 데이터 소스에서 각각 파생시키는 패턴이 깔끔하다. &lt;code&gt;getFullPrompt()&lt;/code&gt;는 그대로 유지하고 &lt;code&gt;renderStructuredPrompt()&lt;/code&gt;를 별도로 추가한 방식이 이 원칙을 잘 따른다.&lt;/p&gt;</description></item></channel></rss>