<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Internal Tools on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/internal-tools/</link><description>Recent content in Internal Tools on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Thu, 07 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/internal-tools/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search 개발일지 #18 — OpenAI gpt-image-2 합류, 모델/제품 라이브러리, 그리고 내부 권한 분리</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-07-hybrid-search-dev18/</link><pubDate>Thu, 07 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-07-hybrid-search-dev18/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search 개발일지 #18 — OpenAI gpt-image-2 합류, 모델/제품 라이브러리, 그리고 내부 권한 분리" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-04-22-hybrid-search-dev17/" &gt;이전 글: #17 — 톤 풀 swap, 모델 인젝션 prompt v2&lt;/a&gt;을 쓴 이래 73개 커밋이 들어갔다. 가장 큰 변화는 &lt;strong&gt;인젝션 모드 자체를 버린 것&lt;/strong&gt; — 한 화면에 여러 톤 모드를 펼쳐놓던 UX를 모델/제품 탭 두 개로 단순화했다. 동시에 비교용 사이드 B를 OpenAI gpt-image-2로 라우팅하기 시작했다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Old["dev #17 까지 &amp;lt;br/&amp;gt; injection-mode pills (5톤)"] --&gt; Refactor["pills 제거 &amp;lt;br/&amp;gt; model/product 탭"]
 Refactor --&gt; A["Side A: Gemini 3.1 Flash &amp;lt;br/&amp;gt; (메인)"]
 Refactor --&gt; B["Side B: OpenAI gpt-image-2 &amp;lt;br/&amp;gt; (비교)"]
 Library["per-user library &amp;lt;br/&amp;gt; model + product"] --&gt; A
 Library --&gt; B
 Internal["internal 권한 게이트 &amp;lt;br/&amp;gt; tone-lock + S3 admin"] --&gt; A&lt;/pre&gt;&lt;p&gt;73개 커밋을 다섯 흐름으로 묶었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="openai-gpt-image-2를-사이드-b로-합류"&gt;OpenAI gpt-image-2를 사이드 B로 합류
&lt;/h2&gt;&lt;p&gt;지금까지 hybrid는 Gemini 단일 백엔드였다. dev #18에서는 비교 평가를 위해 사이드 B를 OpenAI &lt;code&gt;gpt-image-2&lt;/code&gt;로 라우팅하기 시작했다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 UI["프런트엔드 generate"] --&gt; Backend["FastAPI /generate"]
 Backend --&gt; Gather["asyncio.gather()"]
 Gather --&gt; SideA["Side A &amp;lt;br/&amp;gt; Gemini 3.1 Flash"]
 Gather --&gt; SideB["Side B &amp;lt;br/&amp;gt; OpenAI gpt-image-2"]
 SideA --&gt; CompareUI["frontend a/b 키보드 비교"]
 SideB --&gt; CompareUI&lt;/pre&gt;&lt;p&gt;핵심 커밋:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AsyncOpenAI&lt;/code&gt; 클라이언트와 OpenAI image-gen config 와이어링&lt;/strong&gt; (&lt;code&gt;052d42f&lt;/code&gt;) — 환경 변수, 타임아웃, retry 정책을 backend config에 추가.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;공유 image IO helper와 OpenAI image service&lt;/strong&gt; (&lt;code&gt;1fb9b43&lt;/code&gt;) — Gemini와 OpenAI 응답을 공통 포맷으로 변환하는 어댑터.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;5톤 → side A/B 의미론으로 리팩터링&lt;/strong&gt; (&lt;code&gt;d91067e&lt;/code&gt;, &lt;code&gt;ec38fa8&lt;/code&gt;) — &lt;code&gt;tone3&lt;/code&gt;, &lt;code&gt;tone5&lt;/code&gt; 같은 필드명을 &lt;code&gt;side_a&lt;/code&gt;, &lt;code&gt;side_b&lt;/code&gt;로 교체. 더 이상 톤 종류가 아니라 비교용 측면이라는 의미.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gather에서 cancellation 차단 + unsupported quality 파라미터 제거&lt;/strong&gt; (&lt;code&gt;8759a78&lt;/code&gt;) — &lt;code&gt;asyncio.gather&lt;/code&gt;는 한쪽 task가 raise하면 다른 쪽이 cancel될 수 있다. 둘 다 살리려면 &lt;code&gt;return_exceptions=True&lt;/code&gt;로 shield하고 따로 처리.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;코너 케이스 두 개:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;aspect ratio 매핑&lt;/strong&gt; — gpt-image-2는 &lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;1024x1792&lt;/code&gt;, &lt;code&gt;1792x1024&lt;/code&gt;만 지원 (&lt;code&gt;97f7204&lt;/code&gt;). UI에서 입력한 임의 비율을 가장 가까운 지원 사이즈로 매핑.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;B 실패 메시지 surface&lt;/strong&gt; (&lt;code&gt;7d31f62&lt;/code&gt;) — 비교용 사이드라도 실패하면 UI에 알려줘야 한다. 조용히 빠지면 비교 결과가 한쪽만 나와서 혼란.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="인젝션-모드-폐기-모델제품-라이브러리-도입"&gt;인젝션 모드 폐기, 모델/제품 라이브러리 도입
&lt;/h2&gt;&lt;p&gt;dev #17까지는 &amp;ldquo;tone injection mode&amp;quot;라는 추상화가 있었다. 5톤 × 사용자 업로드 모델 × 옵션 매트릭스가 화면에 펼쳐져서 학습 비용이 높았다. dev #18에서 정면으로 갈아치웠다 — &lt;strong&gt;모델 탭과 제품 탭 두 개&lt;/strong&gt;.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Lib["LibraryTab"] --&gt; ModelTab["모델 (인물 사진)"]
 Lib --&gt; ProductTab["제품 (오브젝트 사진)"]
 ModelTab --&gt; ModelUpload["직접 업로드"]
 ModelTab --&gt; ModelGen["Gemini ID-photo로 재생성"]
 ProductTab --&gt; ProductUpload["업로드 + 자동 전처리"]
 ProductUpload --&gt; AutoPick["ready 시 자동 픽"]
 ModelGen --&gt; Generate["generate 호출"]
 ProductUpload --&gt; Generate&lt;/pre&gt;&lt;p&gt;흐름:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;사용자별 자산 라이브러리&lt;/strong&gt; (&lt;code&gt;b933191&lt;/code&gt;) — 업로드한 모델/제품을 사용자 계정에 저장. 다른 톤 만들 때 재사용 가능.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;인젝션 모드 pill을 모델/제품 탭으로 교체&lt;/strong&gt; (&lt;code&gt;1450767&lt;/code&gt;) — UI 단순화. &amp;ldquo;어떤 모드를 쓸지&amp;rdquo; 정하는 단계가 사라졌다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;모델 ID-photo 재생성&lt;/strong&gt; (&lt;code&gt;db64b05&lt;/code&gt;) — 업로드한 인물 사진을 Gemini로 ID 사진 스타일로 정제. 일관성 있는 모델 슬롯을 만들기 위함.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;role-aware prompt directives&lt;/strong&gt; (&lt;code&gt;ffb8ccf&lt;/code&gt;) — 모델/제품 레퍼런스가 프롬프트에 들어갈 때 역할이 명시된다. &amp;ldquo;이 사람을 모델로&amp;rdquo;, &amp;ldquo;이 오브젝트를 제품으로&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;제품 업로드 자동 전처리 + ready 시 자동 픽&lt;/strong&gt; (&lt;code&gt;69db8c2&lt;/code&gt;) — 업로드 → 백그라운드 전처리 → 끝나면 자동으로 활성화. 사용자가 한 단계 덜 누른다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;processing state surfacing + toast&lt;/strong&gt; (&lt;code&gt;f3ff587&lt;/code&gt;) — 전처리 중인 자산은 별도 상태로 표시. 어색한 silent wait 제거.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;중간에 한 번 후퇴가 있었다. &lt;strong&gt;모델 자동 인젝션을 켰다 껐다 다시 켰다&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bdf0aae&lt;/code&gt; — auto model injection 끄고 직접 업로드만 (label wrap 버그도 같이 픽스)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;394f91f&lt;/code&gt; — auto model injection 다시 복원, generated-image drop도 받게&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;직접 업로드 only일 때 사용자가 이미지를 한 장씩 올려야 해서 마찰이 컸다. 결국 자동 인젝션이 default가 되고, 직접 업로드는 옵션으로 남았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="톤-풀-큐레이션-0428--0429--0504"&gt;톤 풀 큐레이션: 0428 → 0429 → 0504
&lt;/h2&gt;&lt;p&gt;생성 품질의 8할은 톤 레퍼런스 풀에서 나온다. 풀이 너무 다양하면 결과가 뒤죽박죽되고, 너무 좁으면 모든 결과가 비슷해진다.&lt;/p&gt;
&lt;p&gt;이번 사이클의 큐레이션 작업:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;0428 모델 셀렉션 풀로 model_image_ref 스왑&lt;/strong&gt; (&lt;code&gt;c1e5d39&lt;/code&gt;) — 0428 셋이 더 일관된 lighting을 보여줘서 메인 모델 풀 교체.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;two-category tones + person-aware model slot&lt;/strong&gt; (&lt;code&gt;cb3a260&lt;/code&gt;) — 톤을 2개 카테고리(natural/film, studio/clean)로 나누고, 인물이 있는 톤일 때만 모델 슬롯 활성화.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;0429 subfolder로 auto-pick 스코프 제한&lt;/strong&gt; (&lt;code&gt;27d335d&lt;/code&gt;) — 자동으로 톤을 고를 때 0429 큐레이션 셋만 후보로 둠. 노이즈 컷.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;slug-named tone refs를 generation_logs에 rewrite&lt;/strong&gt; (&lt;code&gt;76a1a64&lt;/code&gt;) — S3 corpus를 swap하면서 path naming이 바뀌어서 기존 로그를 다시 매핑.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;a(natural,film) 톤 풀 0429 → 0504 reseed&lt;/strong&gt; (&lt;code&gt;c43214e&lt;/code&gt;, 마지막 커밋) — 가장 자주 쓰이는 톤 카테고리를 최신 셋으로 교체.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;scripts/&lt;/code&gt;에 S3 corpus swap 유틸리티들을 기록으로 남겼다 (&lt;code&gt;f169dd4&lt;/code&gt;). 다음 큐레이션 사이클에서 같은 작업을 반복할 때 쓸 수 있게.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;nginx&lt;/code&gt; 한 줄 픽스도 의외로 컸다 (&lt;code&gt;9f252ff&lt;/code&gt;). 백엔드 타임아웃과 nginx의 &lt;code&gt;/api/&lt;/code&gt; 타임아웃이 어긋나서, OpenAI 응답이 느릴 때 nginx가 먼저 502를 던지고 백엔드의 retry까지 trigger하는 이상한 상황이 있었다. 정렬 + upstream retry 비활성화로 해결.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="internal-vs-external-권한-분리"&gt;Internal vs External: 권한 분리
&lt;/h2&gt;&lt;p&gt;이 사이클에서 처음으로 &lt;strong&gt;internal 사용자&lt;/strong&gt;(팀 내부) 개념이 들어왔다. 데모 데이/외부 베타에서는 보여주면 안 되는 기능들이 있었기 때문.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 User["로그인 사용자"] --&gt; Check{"is_internal?"}
 Check -- "yes" --&gt; Internal["Internal 기능 노출"]
 Check -- "no" --&gt; External["External (default)"]
 Internal --&gt; ToneLock["tone refs pin &amp;lt;br/&amp;gt; 다음 generation에 잠금"]
 Internal --&gt; Admin["S3 image manager &amp;lt;br/&amp;gt; 톤/모델/제품 큐레이션"]
 External --&gt; Generate["일반 generate flow"]&lt;/pre&gt;&lt;p&gt;세 PR로 분리해서 머지:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PR #16 — internal-vs-external user tiers + UI gating&lt;/strong&gt; (&lt;code&gt;f33e9d0&lt;/code&gt;) — DB에 &lt;code&gt;is_internal&lt;/code&gt; 컬럼 추가, UI에서 internal-only 컴포넌트는 plain &lt;code&gt;&amp;lt;&amp;gt;&amp;lt;/&amp;gt;&lt;/code&gt;로 단락.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR #17 — internal-only tone-lock&lt;/strong&gt; (&lt;code&gt;199a405&lt;/code&gt;) — 같은 톤 레퍼런스를 여러 generation에 고정해서 비교 evaluation을 깨끗하게.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR #18 — internal-only S3 image manager&lt;/strong&gt; (&lt;code&gt;8096425&lt;/code&gt;) — 웹 UI에서 톤/모델/제품 corpus를 직접 관리. 이전엔 S3 콘솔로 직접 들어가야 했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;feat/admin-s3-manager&lt;/code&gt; 브랜치는 main을 두 번 머지해야 했다 (&lt;code&gt;9d5fa1e&lt;/code&gt;, &lt;code&gt;a35bf53&lt;/code&gt;). 다른 흐름들이 동시에 들어가서 conflict가 누적됐다 — 큰 흐름 머지 직후에 admin 브랜치를 sync해두는 게 좋다는 교훈.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="카메라렌즈-피커-ux-다듬기"&gt;카메라/렌즈 피커 UX 다듬기
&lt;/h2&gt;&lt;p&gt;카메라/렌즈 선택 UX를 한 사이클 동안 한 줄씩 다듬었다.&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;2439c98&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;angle picker dropdown에 thumbnail 표시 (선택 전 미리보기)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;4f615a7&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;레퍼런스 이미지 hover 시 zoom 버튼 노출&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;5be9daa&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&amp;ldquo;카메라 &amp;amp; 렌즈&amp;quot;로 이름 변경, 렌즈 random default, 모델 creator 추가&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;b4aeed3&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;None 옵션 명시 표시 + LensPicker에 None 선택지&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;228ff9f&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;LensPicker가 선택 후 자동 닫힘&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;024253e&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;angle/lens default가 none일 때도 picker 클릭 가능&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;bb13dd3&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;하단 바 정리 — General + Edit 우측 + 활성 상태 강화&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;020c509&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;generation prompt 입력창에 여백 추가 (multiline 친화)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;8208a11&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;library tab + prompt area zoom + transparent overlay&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;349d142&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;preview 모달에서 auto-pick 필터 re-roll, dead label 제거&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;a-z 단축키&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;A/B 비교용 화살표 + &amp;lsquo;a&amp;rsquo;,&amp;lsquo;b&amp;rsquo; 키보드 비교 (&lt;code&gt;fad542e&lt;/code&gt;)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;마지막 키보드 비교 단축키가 의외로 좋았다. 마우스로 두 결과 사이를 왔다갔다 하다가 &amp;lsquo;a&amp;rsquo;/&amp;lsquo;b&amp;rsquo; 키만 누르면 토글 — 비교 평가 속도가 체감 2배.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;dev #17에서 #18로 오면서 &lt;strong&gt;추상화를 줄이는 게 진보였다.&lt;/strong&gt; &amp;ldquo;tone injection mode&amp;quot;라는 5축짜리 추상화는 사용자에게 코드 모델을 강요하는 거였고, 실제 mental model은 &amp;ldquo;사람을 넣을지 / 물건을 넣을지&amp;rdquo; 둘 중 하나다. 모델/제품 두 탭으로 줄인 게 정답.&lt;/p&gt;
&lt;p&gt;OpenAI 사이드 B 라우팅도 같은 결의 결정. 단일 모델로 evaluation을 추측하는 것보다 두 모델 응답을 나란히 보고 키보드로 토글하는 게 빠르다. &lt;code&gt;asyncio.gather&lt;/code&gt; shield 같은 디테일이 신경 쓰이지만, 한쪽이 죽었을 때 어떻게 처리할지 명시적으로 정해두면 같은 패턴을 재사용할 수 있다.&lt;/p&gt;
&lt;p&gt;권한 게이트는 의외로 작은 변화로 큰 효과를 본 케이스. &lt;code&gt;is_internal&lt;/code&gt; 컬럼 하나 + UI에서 conditional rendering, 그러면 internal-only S3 admin이나 tone-lock 같은 기능을 메인 코드베이스에 두면서도 외부 사용자에게는 안 보이게 할 수 있다. 별도 admin 앱으로 빼지 않은 게 비용을 크게 아꼈다.&lt;/p&gt;
&lt;p&gt;다음 dev #19에서 다룰 것: gpt-image-2의 quality A/B 결과 정리, 모델 라이브러리에 group 개념(여러 사람 한 번에) 추가, internal tone-lock을 external로 풀어줄 조건.&lt;/p&gt;</description></item></channel></rss>