<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Typst on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/typst/</link><description>Recent content in Typst on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Mon, 11 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/typst/index.xml" rel="self" type="application/rss+xml"/><item><title>hybrid-image-search 개발일지 #19 — 모델 풀 246개 통합, 어드민 권한, gpt-image-2 해상도, 그리고 에러 핸들링 Phase 1</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-11-hybrid-search-dev19/</link><pubDate>Mon, 11 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-11-hybrid-search-dev19/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post hybrid-image-search 개발일지 #19 — 모델 풀 246개 통합, 어드민 권한, gpt-image-2 해상도, 그리고 에러 핸들링 Phase 1" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://ice-ice-bear.github.io/posts/2026-05-07-hybrid-search-dev18/" &gt;이전 글: #18 — gpt-image-2 합류, 모델/제품 라이브러리, 권한 분리&lt;/a&gt;에서 사이드 B로 OpenAI를 라우팅하기 시작했다면, #19는 그 결정의 부작용을 다듬는 사이클이었다. 21개 커밋, 다섯 PR(#20–#24), 그리고 마지막 날에는 Grafana Cloud Loki 로그로 만든 Typst PDF 에러 리포트가 결국 코드 변경을 이끌었다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Start["dev #18 (c43214e)"] --&gt; Eval["검색 평가 하니스 &amp;lt;br/&amp;gt; offline search-quality 베이스라인"]
 Eval --&gt; Pool["모델 풀 0428/0504 통합 &amp;lt;br/&amp;gt; 142 → 246"]
 Pool --&gt; ModelUX["모델 UX &amp;lt;br/&amp;gt; mode preservation, 16:9 crop, 베이스 force-Edit"]
 ModelUX --&gt; Admin["어드민 &amp;lt;br/&amp;gt; 액티비티 로그 모달, Nano 게이트"]
 Admin --&gt; ResQuality["gpt-image-2 해상도/품질 &amp;lt;br/&amp;gt; 사용자 입력 패스스루"]
 ResQuality --&gt; Phase1["Phase 1 에러 핸들링 &amp;lt;br/&amp;gt; Typst 리포트 → global deadline"]
 Phase1 --&gt; End["dev #19 (e09036d)"]&lt;/pre&gt;&lt;p&gt;이번 사이클의 핵심 질문은 &lt;strong&gt;&amp;ldquo;비교 사이드 B가 production에서 실패하기 시작했을 때, 무엇을 재시도하고 무엇을 빠르게 포기할 것인가.&amp;rdquo;&lt;/strong&gt; 그 결정은 마지막 커밋에 가장 또렷하게 박혔다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="검색-평가-하니스-top_k_fusion64-기각"&gt;검색 평가 하니스: top_k_fusion=64 기각
&lt;/h2&gt;&lt;p&gt;dev #19의 첫 그룹은 검색 사이드의 평가 인프라였다. 그동안은 reranker 변경이나 fusion 파라미터를 production에서 직접 시험했다 — 정성적 인상은 있지만 정량 지표는 없었다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Query["query set &amp;lt;br/&amp;gt; (curated)"] --&gt; Fusion["RRF fusion &amp;lt;br/&amp;gt; (top_k_fusion 후보)"]
 Fusion --&gt; Rerank["bge-reranker &amp;lt;br/&amp;gt; (top_k_rerank)"]
 Rerank --&gt; Eval["offline harness &amp;lt;br/&amp;gt; recall@5, mrr@10"]
 Eval --&gt; Baseline["2026-05-07 baseline JSON"]&lt;/pre&gt;&lt;p&gt;핵심 커밋:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(eval): offline search-quality harness + 2026-05-07 baseline&lt;/code&gt;&lt;/strong&gt; — query set + ground truth + RRF→rerank 파이프라인을 CLI로 묶었다. baseline JSON을 repo에 박아 future 비교의 기준선으로 사용.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;docs(search): top_k_fusion=64 evaluated and rejected — eval harness wins&lt;/code&gt;&lt;/strong&gt; — 직관적으로 fusion 후보를 더 넓게 보면 좋을 거 같아서 64를 시도했지만 harness 결과 +0.2% gain. 비용(reranker GPU 시간 +30%) 대비 무의미. &lt;strong&gt;하니스가 직관을 이긴 첫 사례&lt;/strong&gt;라서 docs에 못 박았다.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(search): request-level OTel span attrs + reranker-doc cleanup&lt;/code&gt;&lt;/strong&gt; — 트레이싱에 query, fusion candidates, rerank scores를 attrs로 attach. 다음 사이클의 분석 인프라가 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="모달-portaling-positionfixed를-viewport에-박아두기"&gt;모달 portaling: &lt;code&gt;position:fixed&lt;/code&gt;를 viewport에 박아두기
&lt;/h2&gt;&lt;p&gt;작은 버그지만 의외로 부수 효과가 컸다. 모달이 부모의 &lt;code&gt;transform: ...&lt;/code&gt; 컨텍스트 안에 있어서 &lt;code&gt;position: fixed&lt;/code&gt;가 부모 기준으로 위치를 잡는 문제. CSS spec상 &lt;code&gt;transform&lt;/code&gt;이 걸린 부모는 fixed의 containing block을 본인 영역으로 만든다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// before — 모달이 ImagePanel 안에서 렌더
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ImagePanel() {&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;translateZ(0)&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}}&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* GPU 레이어 강제 */&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;showModal&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;/&amp;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;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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="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;// after — Portal로 body 직접 마운트
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createPortal&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kr"&gt;from&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;react-dom&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;ImagePanel() {&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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;&amp;lt;&amp;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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;translateZ(0)&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;}}&amp;gt;...&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;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 class="nx"&gt;showModal&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;createPortal&lt;/span&gt;&lt;span class="p"&gt;(&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Modal&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="p"&gt;&amp;lt;/&amp;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="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;fix: portal modals to body so position:fixed pins to viewport&lt;/code&gt;) 그대로다. 한 줄짜리 버그처럼 보였지만 사이드 이펙트가 두 개 — 모달 z-index 재설정 + &lt;code&gt;onClose&lt;/code&gt; 클릭 outside 감지 로직 수정.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="모델-풀-0428과-0504를-한-풀로-합치기-142--246"&gt;모델 풀: 0428과 0504를 한 풀로 합치기 (142 → 246)
&lt;/h2&gt;&lt;p&gt;dev #18 마지막에 0428 풀을 0504로 reseed했다 (folder-hint 라벨 포함). 4월 28일자 카탈로그를 5월 4일자로 갈아치운 셈. 그런데 사용자 피드백이 빠르게 왔다 — &amp;ldquo;이전 풀에서 잘 쓰던 모델들이 사라졌어.&amp;rdquo;&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Pool0428["0428 풀 &amp;lt;br/&amp;gt; 142 모델"] --&gt; Reseed["dev #18: reseed &amp;lt;br/&amp;gt; 0504로 교체"]
 Reseed --&gt; Pool0504["0504 풀 &amp;lt;br/&amp;gt; ~146 모델"]
 Pool0428 -- "merge back" --&gt; Merged["통합 풀 &amp;lt;br/&amp;gt; 246 모델"]
 Pool0504 --&gt; Merged
 Merged --&gt; Picker["model-picker &amp;lt;br/&amp;gt; dedupe + 필터 exhaustion 표시"]&lt;/pre&gt;&lt;p&gt;두 커밋이 이 흐름을 마무리했다:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(models): merge 0428 pool back into 0504 model pool (142 -&amp;gt; 246)&lt;/code&gt;&lt;/strong&gt; — 풀을 합쳐서 사용자가 둘 다 접근 가능하게. 중복 제거 후 246개로 안착.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;feat(model-picker): dedupe re-picks and surface filter exhaustion&lt;/code&gt;&lt;/strong&gt; — 같은 사용자에게 같은 모델을 두 번 추천하지 않도록 dedupe. 필터를 너무 좁히면 후보가 0이 되는데, 그 경우 UI에 &amp;ldquo;더 이상 후보 없음 — 필터를 풀어보세요&amp;rdquo; 메시지를 surface.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id="모델-ux-mode-preservation-169-crop-베이스-force-edit"&gt;모델 UX: mode preservation, 16:9 crop, 베이스 force-Edit
&lt;/h2&gt;&lt;p&gt;PR #20–#22가 이 묶음이었다. 핵심 결정 세 개:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(1) 라이브러리 패널을 닫아도 generation mode 유지.&lt;/strong&gt; dev #18까지는 라이브러리 패널을 닫으면 mode가 &lt;code&gt;auto&lt;/code&gt;로 reset되었다. 사용자는 &amp;ldquo;방금 Edit 모드를 골랐는데 왜 닫으면 풀리지?&amp;ldquo;라고 했다. 명시적 선택은 명시적 변경으로만 풀어야 한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tsx" data-lang="tsx"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// frontend/lib/state.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;closeLibrary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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 class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;setLibraryPanelCollapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;setActiveLibraryTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;resetInjectionMode&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="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 class="o"&gt;+&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;closeLibrary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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 class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;setLibraryPanelCollapsed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&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="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;setActiveLibraryTab&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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="o"&gt;+&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;fix(generation): reset injection mode to auto when closing library panel&lt;/code&gt;)로 정확히 라이브러리 패널 닫는 경우 한정으로 reset이 명시되었다. 두 커밋이 같은 결정을 양 끝에서 묶었다 — 의도하지 않은 reset은 제거하고, 의도된 reset만 명시한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) gpt-image-2의 16:9 crop.&lt;/strong&gt; gpt-image-2는 출력 비율이 &lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;1024x1536&lt;/code&gt;, &lt;code&gt;1536x1024&lt;/code&gt; 셋뿐이다. 사용자가 16:9를 선택해도 백엔드는 1536x1024를 받는다. 그래서 UI에서 prediction box를 16:9로 그리고, 결과를 받으면 center-crop으로 16:9로 잘라서 보여준다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(3) &amp;ldquo;베이스&amp;rdquo; 버튼이 강제로 Edit 모드 진입.&lt;/strong&gt; detail 화면에서 베이스 모델 버튼을 누르면 inheriting source mode가 아니라 Edit 모드로 들어가야 한다. 그 외 경로(자동 인젝션, 모델 픽커)는 auto로 들어간다.&lt;/p&gt;
&lt;p&gt;뒤이은 커밋 &lt;code&gt;fix(generation): tighten model auto-injection + require base for Edit mode&lt;/code&gt;가 이 룰을 백엔드까지 강제 — Edit 모드 요청에 base 이미지가 없으면 422 reject.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="어드민-액티비티-로그-모달-nano-모드-게이트"&gt;어드민: 액티비티 로그 모달, Nano 모드 게이트
&lt;/h2&gt;&lt;p&gt;PR #21과 #24는 내부 운영용 기능이었다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Activity log 모달&lt;/strong&gt; — 어드민이 특정 사용자의 최근 generate 호출을 viewer/다운로드 가능. 디버깅 + 베타 테스터 지원에 필수.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph LR
 Admin["admin 페이지 &amp;lt;br/&amp;gt; 사용자 검색"] --&gt; Modal["activity log 모달"]
 Modal --&gt; View["view 모드 &amp;lt;br/&amp;gt; 최근 N개 호출"]
 Modal --&gt; Download["download 모드 &amp;lt;br/&amp;gt; JSONL export"]
 View --&gt; Anon["PII 마스킹 &amp;lt;br/&amp;gt; image url 미노출"]&lt;/pre&gt;&lt;p&gt;&lt;strong&gt;Nano Only 모드&lt;/strong&gt; — 새로운 admin allowlist 패턴. 특정 어드민 사용자(&lt;code&gt;khk@diffs.studio&lt;/code&gt;)는 &amp;ldquo;Nano Only&amp;rdquo; 모드에서 비용 절감된 작은 모델만 호출 가능. Production 비용 제어 + 시연/데모용 안전 모드.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="gpt-image-2-해상도품질-패스스루"&gt;gpt-image-2 해상도/품질 패스스루
&lt;/h2&gt;&lt;p&gt;오늘(2026-05-11) 첫 커밋은 작지만 production 영향이 컸다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;feat(generation): pass user resolution + quality to gpt-image-2&lt;/code&gt; — 그동안 백엔드는 사용자가 선택한 resolution/quality를 무시하고 default(&lt;code&gt;1024x1024&lt;/code&gt;, &lt;code&gt;quality=auto&lt;/code&gt;)로 호출했다. 사용자가 &amp;ldquo;고해상도 16:9&amp;quot;를 골라도 결과는 1024 정사각형. UI에 setter가 있지만 백엔드 와이어링이 빠져있었다.&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;# backend/openai_service.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aspect&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;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="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1024&amp;#34;&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="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_pick_size&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aspect&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;requested_quality&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;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="o"&gt;+&lt;/span&gt; &lt;span class="c1"&gt;# gpt-image-2 hard-limits output to 1024x1024 / 1024x1536 / 1536x1024&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="c1"&gt;# (max ~3:2). The size mapper picks the closest valid output.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;16:9&amp;#34;&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;21:9&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="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1536x1024&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;aspect&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;9:16&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="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1536&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;1024x1024&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;b5a0ede — fix(generation-feed): keep each card at its A-image's natural aspect&lt;/code&gt;도 같은 맥락. generation feed의 카드 그리드가 사이드 A(Gemini, 더 유연한 비율)의 비율을 따라가도록 수정.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="phase-1-에러-핸들링-grafana-loki--typst--코드-결정"&gt;Phase 1 에러 핸들링: Grafana Loki → Typst → 코드 결정
&lt;/h2&gt;&lt;p&gt;이번 사이클에서 가장 흥미로운 흐름은 마지막 세션(109분, &lt;code&gt;1358feee&lt;/code&gt;)이었다. Grafana Cloud Loki에서 최근 7일치 image generation 에러 로그를 뽑고, Typst로 PDF 리포트를 만든 다음, &lt;strong&gt;그 리포트의 권고대로&lt;/strong&gt; 코드를 고친다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;flowchart TD
 Loki["Grafana Cloud Loki &amp;lt;br/&amp;gt; service_name=hybrid-image-search"] --&gt; Tally["에러 카테고리 집계 &amp;lt;br/&amp;gt; 6 카테고리"]
 Tally --&gt; Report["docs/error-report.typ &amp;lt;br/&amp;gt; 비중, 재시도 가능성, 권고"]
 Report --&gt; Decision["전체 retry? 또는 선택과 집중?"]
 Decision --&gt; Phase1["Phase 1만 적용 &amp;lt;br/&amp;gt; (위험도 낮은 것)"]
 Decision --&gt; Defer["Phase 2-1 보류 &amp;lt;br/&amp;gt; (Gemini/OpenAI 재시도)"]
 Phase1 --&gt; Code["e09036d &amp;lt;br/&amp;gt; global deadline + 명시적 에러 분류"]&lt;/pre&gt;&lt;p&gt;리포트에서 사용자가 했던 정확한 질문이 결정을 만들었다:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;&amp;ldquo;재시도 로직을 단순 적용한다면 해당 시간대의 이미지 생성 호출에도 영향을 줄 수 있지 않아?&amp;rdquo;&lt;/strong&gt;&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;그 통찰은 리포트 v2에 들어갔다. 만약 #1(Gemini 503)과 #2(OpenAI 자체 retry)에 추가로 #3(Gemini → OpenAI fallback)까지 동시에 켜면, 한 사용자가 최악의 경우 두 API의 retry 곱셈을 한 번에 받는다. 그러면 thundering herd처럼 발생 시간대의 처리량이 무너진다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;결정: Phase 1만 적용. 위험도 낮은 것 — 명시적 에러 분류 + global deadline.&lt;/strong&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;# backend/service.py — global deadline 패턴&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_with_deadline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&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="k"&gt;return&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;wait_for&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;_generate_inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deadline_s&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="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;except&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;TimeoutError&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 class="n"&gt;GenerationError&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;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;timeout&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;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# ← user 입장에서는 fresh request로 다시 시도&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Image generation exceeded 60s deadline&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="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;except&lt;/span&gt; &lt;span class="n"&gt;gemini&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServerError&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 class="c1"&gt;# 503 류&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 class="n"&gt;GenerationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;upstream-503&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&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 class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;openai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIError&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;raise&lt;/span&gt; &lt;span class="n"&gt;GenerationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;openai-api&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;의도된 design choice&lt;/strong&gt;: retriable=False로 통일. 백엔드는 재시도하지 않고, 사용자가 명시적으로 새 요청을 보낸다. 이게 phase 1의 안전 boundary다. Phase 2에서 어떤 카테고리에 한해 자동 retry를 부활시킬지는 Loki 데이터를 1-2주 더 모은 뒤에 결정한다.&lt;/p&gt;
&lt;hr&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;chore: reseed model pool from 0428 to 0504 with folder-hint labels&lt;/td&gt;
 &lt;td&gt;data/model_pool/*.json&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix: portal modals to body so position:fixed pins to viewport&lt;/td&gt;
 &lt;td&gt;frontend/components/Modal.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(search): request-level OTel span attrs + reranker-doc cleanup&lt;/td&gt;
 &lt;td&gt;backend/search/*.py, observability&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(eval): offline search-quality harness + 2026-05-07 baseline&lt;/td&gt;
 &lt;td&gt;scripts/eval/, eval/baselines/*.json&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;docs(search): top_k_fusion=64 evaluated and rejected — eval harness wins&lt;/td&gt;
 &lt;td&gt;docs/decisions/&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(models): merge 0428 pool back into 0504 model pool (142 -&amp;gt; 246)&lt;/td&gt;
 &lt;td&gt;data/model_pool/&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(ui): mode preservation, larger model preview, GPT 16:9 crop, model name in detail&lt;/td&gt;
 &lt;td&gt;frontend (PR #20)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(admin): user activity log modal with view/download&lt;/td&gt;
 &lt;td&gt;backend/admin/, frontend/admin/ (PR #21)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(model-picker): dedupe re-picks and surface filter exhaustion&lt;/td&gt;
 &lt;td&gt;frontend/components/ModelPicker.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): reset injection mode to auto when closing library panel&lt;/td&gt;
 &lt;td&gt;frontend/lib/state.ts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(detail): 베이스 button forces Edit mode instead of inheriting source mode&lt;/td&gt;
 &lt;td&gt;frontend (PR #22)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): tighten model auto-injection + require base for Edit mode&lt;/td&gt;
 &lt;td&gt;backend/generation/, frontend (PR #23)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(admin): Nano Only mode + add &lt;a class="link" href="mailto:khk@diffs.studio" &gt;khk@diffs.studio&lt;/a&gt; to admin allowlist&lt;/td&gt;
 &lt;td&gt;backend/auth/, admin (PR #24)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;feat(generation): pass user resolution + quality to gpt-image-2&lt;/td&gt;
 &lt;td&gt;backend/openai_service.py&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation-feed): keep each card at its A-image&amp;rsquo;s natural aspect&lt;/td&gt;
 &lt;td&gt;frontend/components/GenerationFeed.tsx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;fix(generation): harden error handling (Phase 1 + global deadline)&lt;/td&gt;
 &lt;td&gt;backend/service.py, docs/error-report.typ&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;&lt;strong&gt;(1) 평가 하니스는 한 번 baseline을 박으면 직관을 이긴다.&lt;/strong&gt; top_k_fusion=64 기각 사례는 dev #19에서 가장 작은 코드 변경 (docs 한 파일)이지만 가장 큰 process 변경이다. 이제부터 search 사이드 파라미터 변경은 baseline JSON 대비 측정해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(2) 모달 portaling 같은 작은 CSS 결함은 production 데이터로만 잡힌다.&lt;/strong&gt; &lt;code&gt;transform: translateZ(0)&lt;/code&gt;로 GPU 레이어를 강제한 결정 자체는 잘못이 아니었다. 다만 그 결정이 &lt;code&gt;position: fixed&lt;/code&gt;의 containing block을 바꾼다는 사실은 React DevTools로는 안 보이고 실제 브라우저에서 모달이 어긋난 순간에야 드러났다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(3) Grafana Loki → Typst → 코드 결정의 흐름이 의외로 강력했다.&lt;/strong&gt; 평소엔 대시보드를 보고 patch를 푸는데, 이번엔 7일치 로그를 카테고리별로 묶고 PDF 리포트로 정리한 다음 코드를 손댔다. 보고서를 만드는 과정이 곧 design doc이 되었다 — &amp;ldquo;Phase 1만 적용, Phase 2 보류&amp;quot;라는 의사결정이 리포트 본문에 박혀있다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;(4) Production 에러 대응의 첫 룰은 &amp;ldquo;재시도를 함부로 늘리지 않기&amp;rdquo;.&lt;/strong&gt; 한 곳의 retry는 안전해 보이지만, 두 곳이 곱해지면 thundering herd가 된다. 사용자가 직접 던진 질문이 이 결론으로 가는 길을 잡아냈다.&lt;/p&gt;
&lt;p&gt;다음 사이클 #20은 Phase 2 — Loki 1-2주치 데이터로 Gemini 503의 순수 발생 빈도를 측정한 뒤 카테고리별 selective retry 정책을 결정한다.&lt;/p&gt;</description></item></channel></rss>