<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Creative Agent Studio on ICE-ICE-BEAR-BLOG</title><link>https://ice-ice-bear.github.io/ko/tags/creative-agent-studio/</link><description>Recent content in Creative Agent Studio on ICE-ICE-BEAR-BLOG</description><generator>Hugo -- gohugo.io</generator><language>ko</language><lastBuildDate>Thu, 28 May 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://ice-ice-bear.github.io/ko/tags/creative-agent-studio/index.xml" rel="self" type="application/rss+xml"/><item><title>Creative Agent Studio 개발일지 #5 — 폴리시 주간: LLM 토큰 텔레메트리, 멀티세션 격리, 인라인 되감기, 그리고 수정요청 어포던스</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-28-creative-agent-studio-dev5/</link><pubDate>Thu, 28 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-28-creative-agent-studio-dev5/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio 개발일지 #5 — 폴리시 주간: LLM 토큰 텔레메트리, 멀티세션 격리, 인라인 되감기, 그리고 수정요청 어포던스" /&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-22-creative-agent-studio-dev4/" &gt;이전 글: #4 — 5개 게이트, 4개 캔버스, 수정 모드 시스템&lt;/a&gt;이 빌드아웃 푸시를 끝냈다. 6일 뒤, &lt;strong&gt;4 작업일에 23커밋&lt;/strong&gt;이 폴리시 주간을 마무리했다 — 기능 완성된 시스템을 실제 사용자의 손 아래 production에서 버티는 무언가로 바꾸는 종류의 작업.&lt;/p&gt;
&lt;p&gt;주중에 네 갈래가 병렬로 흘렀다. 첫째, &lt;strong&gt;LLM 호출 관측성&lt;/strong&gt; — 모든 모델 호출이 이제 토큰 사용량과 전파된 logCtx를 갖춘 구조화 로그를 발신하고, 런타임은 &lt;code&gt;LOG_LEVEL&lt;/code&gt; 임계 제어를 받았다. 둘째, &lt;strong&gt;새로고침 아래 프로젝트별 상태 격리&lt;/strong&gt; — 2026-05-26의 여덟 커밋이 멀티세션 작업이 떨어진 뒤 표면화된 hooks-order 버그, 끊긴 부트스트랩, crypto.randomUUID 격차를 사냥했다. 셋째, &lt;strong&gt;인라인 과거-게이트 되감기&lt;/strong&gt; — 단일 feat 커밋이 사용자가 활성 콘티 세션 안에서 &amp;ldquo;GATE 2의 세 번째 카피로 바꿔줘&amp;quot;라고 말하고 세션을 떠나지 않을 수 있는 어포던스를 출시했다. 넷째, &lt;strong&gt;수정요청 어포던스 패스&lt;/strong&gt; — 다섯 커밋이 모든 캔버스 탭의 마킹 카드 비주얼을 통일하고, 되돌리기 다이얼로그 카피를 다듬고, 게이트 셀렉터가 사용자가 방금 클릭한 카드를 잊는 상태 버그를 수정했다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#4 — 빌드아웃 (5cb4106)"] --&gt; Thread1["갈래 1 &amp;lt;br/&amp;gt; LLM 관측성 &amp;lt;br/&amp;gt; 토큰 사용량 + logCtx"]
 Prev --&gt; Thread2["갈래 2 &amp;lt;br/&amp;gt; 프로젝트별 격리 &amp;lt;br/&amp;gt; hooks order + 부트스트랩 수정"]
 Prev --&gt; Thread3["갈래 3 &amp;lt;br/&amp;gt; 인라인 과거-게이트 되감기 &amp;lt;br/&amp;gt; 채팅 주도 파이프라인 편집"]
 Prev --&gt; Thread4["갈래 4 &amp;lt;br/&amp;gt; 수정요청 UX &amp;lt;br/&amp;gt; 통일된 마킹 카드 어포던스"]
 Thread1 --&gt; End["#5 종료 (67b820e) &amp;lt;br/&amp;gt; 2026-05-28"]
 Thread2 --&gt; End
 Thread3 --&gt; End
 Thread4 --&gt; End&lt;/pre&gt;&lt;p&gt;관통하는 한 주제 — &lt;strong&gt;기능은 끝났다. 남은 건 새로고침, 부하, 그리고 혼란스러운 사용자 아래에서 정직하게 만드는 일이었다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-1--llm-호출-관측성"&gt;갈래 1 — LLM 호출 관측성
&lt;/h2&gt;&lt;p&gt;#2에서 떨어진 구조화 로거가 토대였다 — 이번 주는 그것을 모든 LLM 호출 사이트에 꿰어서 production 텔레메트리가 실제로 의미를 갖게 했다.&lt;/p&gt;
&lt;p&gt;두 커밋이 무거운 짐을 졌다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(logger): add LOG_LEVEL threshold, silence node:sqlite warning&lt;/code&gt; — &lt;code&gt;LOG_LEVEL&lt;/code&gt; env var로 production이 &lt;code&gt;info&lt;/code&gt; 잡담을 떨구고 &lt;code&gt;warn&lt;/code&gt;/&lt;code&gt;error&lt;/code&gt;만 유지할 수 있다. 동반 변경은 모든 워커 fork에서 출력되던 &lt;code&gt;node:sqlite&lt;/code&gt; 실험적 경고를 억제했다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(observability): emit structured logs across worker + lifecycle paths&lt;/code&gt; — &lt;code&gt;runtime/workers/worker-loop.js&lt;/code&gt;와 &lt;code&gt;runtime/orchestration/run-lifecycle.js&lt;/code&gt;가 이제 각 의미 있는 전이(잡 클레임, 실행 시작, 게이트 발신, 단계 전진, 실행 완료/오류)에서 구조화 이벤트를 로깅.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(observability): instrument LLM calls with token usage + logCtx&lt;/code&gt; — 이게 하중 지지 조각이다. 모든 모델 호출이 이제 다음을 로깅.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/agents/_llm-instrument.js (의역)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;callModelWithLogging&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logCtx&lt;/span&gt;&lt;span class="p"&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;llm_call&amp;#34;&lt;/span&gt;&lt;span class="p"&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;logCtx&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;model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&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;prompt_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&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;output_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_tokens&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;duration_ms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&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;cached_tokens&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cached_input_tokens&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;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="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="nx"&gt;result&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&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="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;llm_call_failed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;logCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;duration_ms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&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;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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="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;logCtx&lt;/code&gt;는 &lt;code&gt;worker-loop.js&lt;/code&gt;에서 전파되며 &lt;code&gt;{ runId, projectId, stage, role }&lt;/code&gt;를 들고 다닌다. 그래서 단일 LLM 호출의 로그 줄이 어느 프로젝트, 어느 세션, 어느 게이트, 어느 에이전트, 어느 모델, 토큰이 얼마나 들었는지를 말해준다. #2에서 셋업했던 Grafana 대시보드에 이제 실제 데이터가 흐른다.&lt;/p&gt;
&lt;p&gt;인프라 측 동반은 &lt;code&gt;feat(terraform): widen EC2 start cron to every day&lt;/code&gt; + &lt;code&gt;fix(terraform): update daily stop schedule to 03:00 KST&lt;/code&gt;. EC2 인스턴스는 프로토타입 시절부터 평일 전용 스케줄로 돌고 있었다 — cron이 이제 매일 박스를 시작하고 03:00 KST에 멈춘다. 운영, 기능 작업 아님.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-2--새로고침-아래-프로젝트별-상태-격리"&gt;갈래 2 — 새로고침 아래 프로젝트별 상태 격리
&lt;/h2&gt;&lt;p&gt;#4의 멀티세션 작업이 새로고침 시점 버그의 전체 부류를 도입했다. 사용자가 workspace 안에서 하드 새로고침하면, React 트리는 어떤 데이터도 로드되기 전에 마운트된다 — 그리고 멀티세션 로직은 사용자가 launcher에서 workspace로 &lt;em&gt;들어왔을&lt;/em&gt; 때만 성립하는 가정(부트스트랩이 이미 일어났다)을 만들고 있었다.&lt;/p&gt;
&lt;p&gt;2026-05-26의 여덟 커밋이 엣지 케이스를 사냥했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): bootstrap projects on workspace refresh, fix hooks order&lt;/code&gt;&lt;/strong&gt; — workspace 페이지가 &lt;code&gt;projects&lt;/code&gt;가 이미 로드됐다고 가정했다. 새로고침 시에는 아니었다. 수정은 workspace effect 안에 부트스트랩 호출을 추가했다 — 하지만 그게 React hooks-order 위반을 트리거했다, 부트스트랩 호출이 &lt;code&gt;useEffect&lt;/code&gt; 안에서 조건부였기 때문. 둘 다 한 커밋으로 수정됐다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): isolate per-project state on switch, show loader during session hydration&lt;/code&gt;&lt;/strong&gt; — 프로젝트 A에서 B로 전환할 때 A의 세션 목록이 B가 로딩되는 동안 보이고 있었다. 수정 — &lt;code&gt;projectId&lt;/code&gt;가 바뀌면 즉시 프로젝트별 슬라이스를 비우고 로더를 보여준 다음 B의 데이터를 hydrate. 사용자는 잠깐의 로더를 보지, A의 세션을 B에 잘못 귀속시키지 않는다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): don't strand /sessions bootstrap behind a stale ref guard&lt;/code&gt;&lt;/strong&gt; — ref로 구현된 &amp;ldquo;이미 부트스트랩 중이면 재부트스트랩하지 마라&amp;rdquo; 가드가 있었다. ref가 &lt;code&gt;true&lt;/code&gt;로 세팅된 뒤 특정 에러 경로에서 클리어되지 않아, 후속 네비게이션이 stranded됐다. 수정 — 부트스트랩 상태를 ref가 아니라 슬라이스에서 추적하고, 성공 &lt;em&gt;과&lt;/em&gt; 실패 모두에서 클리어.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): make Composer first-brief-only, flip ApproveBar toggle label&lt;/code&gt;&lt;/strong&gt; — Composer(채팅 입력)가 &lt;em&gt;모든&lt;/em&gt; 사용자 턴의 진입점이었다. 하지만 세션이 첫 브리프 너머로 가면 게이트 기반 흐름이 인계한다 — Composer의 역할은 숨겨져야 했다. 수정은 첫 브리프가 제출되기 전에만 Composer를 보이게 만들었다 — 그 뒤로는 ApproveBar가 사용자의 표면.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): make bootstrap dep stable so /sessions can't be cancelled forever&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;useChatStream&lt;/code&gt;이 의존성 배열이 불안정한 AbortController를 사용하고 있어서 매 재렌더마다 abort됐다. 수정은 dep를 안정화해서 컨트롤러가 실제 사용자 취소 또는 언마운트에서만 abort되게 했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix(web): polyfill crypto.randomUUID so HTTP prod can create projects/sessions&lt;/code&gt;&lt;/strong&gt; — production EC2가 일부 클라이언트에 평문 HTTP로 서빙하고 있었고(중간 프록시가 HTTPS를 떼어냈다), &lt;code&gt;crypto.randomUUID&lt;/code&gt;는 secure context에서만 사용 가능하다. 프로젝트와 세션이 생성될 수 없었다. 폴리필이 복원.&lt;/p&gt;
&lt;p&gt;마지막이 이 주에서 가장 &amp;ldquo;production이 dev 환경이 숨겼던 걸 드러낸다&amp;rdquo; 케이스다. 로컬호스트는 secure context. 배포 타깃은 항상 그렇지는 않다. 한 줄 폴리필이 EC2 빌드를 사용 가능하게 유지했다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;fix: preserve 분석 보고서 across key-concept revisions&lt;/code&gt;&lt;/strong&gt; — 런타임 수정 동반. 사용자가 GATE 1에서 키 컨셉 선택을 수정할 때, 분석 보고서가 함께 재계산되고 있었다 — 불필요한 작업이고 매번 약간 다른 보고서를 만들었다. 수정은 키 컨셉 수정에 걸쳐 원래 분석을 고정.&lt;/p&gt;
&lt;p&gt;그리고 &lt;code&gt;docs(claude): register Diffs Runtime harness pointer in CLAUDE.md&lt;/code&gt;가 Claude Code 세션이 런타임의 하네스 규약을 자동으로 찾을 수 있도록 포인터를 추가했다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-3--채팅에서-인라인-과거-게이트-되감기"&gt;갈래 3 — 채팅에서 인라인 과거-게이트 되감기
&lt;/h2&gt;&lt;p&gt;단일 커밋이 큰 UX 이동을 출시했다 — &lt;code&gt;feat(web): add inline past-gate rewind for chat-driven pipeline edits&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;설정 — 폴리시 주간이 되자 사용자는 현재 게이트에 대한 수정 모드를 가진 ApproveBar를 갖췄고, 워크플로는 한 번에 한 단계씩 전진했다. 그런데 사용자가 &lt;em&gt;과거&lt;/em&gt; 결정을 바꾸고 싶으면? &amp;ldquo;사실 두 번째 키 컨셉으로 가자&amp;rdquo; — 콘티 단계에 앉아서?&lt;/p&gt;
&lt;p&gt;이전엔 캔버스를 떠나, 수동으로 GATE 1으로 돌아가고, 재선택하고, 후속 모든 게이트를 다시 걸어야 했다. 고통스럽고, 채팅 우선 원칙 위반.&lt;/p&gt;
&lt;p&gt;인라인 되감기는 채팅에서 과거-게이트 의도를 감지한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// (의역 — 결합된 chat-stream + dispatch-sse 경로)
&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="c1"&gt;// &amp;#34;두 번째 키 컨셉으로 다시 가자&amp;#34; → rewind_intent: { gate: &amp;#34;GATE_1&amp;#34;, selection: 2 }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// 프론트엔드가 rewind_proposal SSE 이벤트 수신:
&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="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;rewind_proposal&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="nx"&gt;fromGate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_5&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="nx"&gt;toGate&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_1&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="nx"&gt;affectedDownstreamGates&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;GATE_2&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_3&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_4&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;GATE_5&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="c1"&gt;// UI가 인라인 ConfirmRewindDialog 표시 (갈래 4에서 다듬은 것)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;다이얼로그는 폐기될 것을 명시적으로 나열한다 — GATE 2의 카피 승인, GATE 4의 시나리오 승인 등 — 그래서 사용자가 확인 전 비용을 안다. 확인 시 런타임이 프로젝트를 타깃 게이트로 되돌리고, 하류 모든 것을 재생성하고, 사용자는 새 선택을 할 준비가 된 타깃 게이트의 승인 표면에 도착한다.&lt;/p&gt;
&lt;p&gt;이건 게이트 기반 자동 실행 원칙(&lt;code&gt;interaction-model.md&lt;/code&gt;의 결정 3)의 자연스러운 확장이다 — 단 &lt;em&gt;역방향&lt;/em&gt;으로. 원칙은 — 각 단계는 사용자 승인에서 다음으로 전진. 되감기는 — 각 단계는 &lt;em&gt;되돌아갈&lt;/em&gt; 수도 있고, 런타임은 무엇을 무효화할지 안다.&lt;/p&gt;
&lt;p&gt;동반 &lt;code&gt;docs(claude): add triage-prod-bug skill trigger to enforce browser-first debugging&lt;/code&gt;는 하네스 규칙이다 — 버그가 보고되면, 코드를 읽기 전에 브라우저를 먼저 본다(devtools, network, console). 채팅 우선 제품 원칙에는 디버깅 유사물이 있다 — 먼저 보고, 그 다음 코드.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="갈래-4--수정요청-어포던스-패스"&gt;갈래 4 — 수정요청 어포던스 패스
&lt;/h2&gt;&lt;p&gt;마지막 다섯 커밋이 모든 캔버스 탭의 수정요청 UI를 통일했다. 이 작업 전까지 모든 탭은 자기 마킹 카드 스타일을 구현하고 있었다 — Copy에는 노란 배경, KeyConcept에는 빨간 점선 테두리, Storyboard에는 파란 왼쪽 엣지 바(이거 하나만), Scene에는 비주얼 처리 전혀 없음 — 그리고 사용자가 계속 물었다 — &lt;em&gt;&amp;ldquo;왜 콘티에만 파란 마크가 뜨나요?&amp;rdquo;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="되돌리기-다이얼로그-카피-완화--폐기-리스트에서-최종-게이트-숨기기"&gt;되돌리기 다이얼로그 카피 완화 + 폐기 리스트에서 최종 게이트 숨기기
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;e27316a&lt;/code&gt;. 첫 공격은 가장 거슬리던 카피였다. 되돌리기 다이얼로그가 이렇게 읽혔다.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&amp;ldquo;이 결정을 되돌릴까요?&amp;rdquo;
&amp;ldquo;카피 검토 단계부터 다시 진행합니다.&amp;rdquo;
&amp;ldquo;아래 후속 결정이 새 버전으로 대체됩니다:&amp;rdquo;
– 컨셉 확정 결정
– 시나리오 검토 결정
– &lt;strong&gt;최종 승인 결정&lt;/strong&gt; ← 이 리스트에 있으면 안 됨&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;두 문제 — 폐기 리스트에 &lt;em&gt;최종&lt;/em&gt; 승인 게이트가 들어가 있었지만(그건 끝에 다시 확정하는 단계지 사라지는 단계가 아님), &amp;ldquo;결정&amp;quot;이라는 단어가 너무 기업스러웠다. 실제로는 초안에 대한 크리에이티브 판단이지 이사회 안건이 아니다.&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;// web/src/components/approve/ConfirmRewindDialog.tsx
&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;ul&lt;/span&gt; &lt;span class="na"&gt;data-testid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;confirm-rewind-discard-list&amp;#34;&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;gates&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;final-approval&amp;#39;&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;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&amp;gt;{&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;softTitle&lt;/span&gt;&lt;span class="p"&gt;}&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&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;ul&lt;/span&gt;&lt;span class="p"&gt;&amp;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;final-approval&lt;/code&gt; 게이트를 빼고, &lt;code&gt;softTitle&lt;/code&gt;(예: &amp;ldquo;콘티 검토 결정&amp;rdquo; 대신 &amp;ldquo;콘티 검토&amp;rdquo;)을 쓴다. 같은 &lt;code&gt;Gate&lt;/code&gt; 인터페이스에서 가져오지만 파괴적 컨텍스트와 정보성 컨텍스트에서 다르게 렌더링한다.&lt;/p&gt;
&lt;h3 id="되돌리기-다이얼로그의-게이트-타이틀"&gt;되돌리기 다이얼로그의 게이트 타이틀
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;4ddff68&lt;/code&gt;. 다이얼로그가 기계 생성 게이트 id(&lt;code&gt;g-2&lt;/code&gt;, &lt;code&gt;g-3&lt;/code&gt;)를 타이틀로 쓰고 있었다. 작은 라벨 맵이 이해도를 고쳤다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GATE_LABELS&lt;/span&gt;: &lt;span class="kt"&gt;Record&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;GateKind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;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="s1"&gt;&amp;#39;concept-confirm&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;컨셉 검토&amp;#39;&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="s1"&gt;&amp;#39;scenario-review&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;시나리오 검토&amp;#39;&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="s1"&gt;&amp;#39;storyboard-approve&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;콘티 검토&amp;#39;&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="s1"&gt;&amp;#39;cut-finalize&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;컷 검토&amp;#39;&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="s1"&gt;&amp;#39;final-approval&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;최종 승인&amp;#39;&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;이제 다이얼로그가 &amp;ldquo;콘티 검토 단계부터 다시 진행합니다&amp;quot;라고 읽힌다 — 캔버스 탭 라벨과 정확히 일치하는 표현이라, 사용자가 되감기 후 어느 탭에 도착할지 미리 알 수 있다.&lt;/p&gt;
&lt;h3 id="수정요청-진입-시-approvebar-게이트-미리-선택"&gt;수정요청 진입 시 ApproveBar 게이트 미리 선택
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;c55891b&lt;/code&gt;. 카드의 수정요청을 누르면 ApproveBar가 게이트 셀렉터와 함께 슬라이드 업했는데, 셀렉터가 비어 있어서 사용자가 방금 마킹한 게이트를 다시 클릭해야 했다. 두 줄짜리 &lt;code&gt;useEffect&lt;/code&gt;로 암묵적 컨텍스트를 명시적 선택으로 동기화했다.&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;// web/src/components/approve/ApproveBar.tsx
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;useEffect&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="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;수정요청&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;activeCard&lt;/span&gt;&lt;span class="p"&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="nx"&gt;setGateSelection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeCard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gateId&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;idle&amp;#39;&lt;/span&gt;&lt;span class="p"&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="nx"&gt;setGateSelection&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 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="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="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;activeCard&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;이 변경의 보안 리뷰 패스가 한 가지 노트를 표면화했다 — 게이트 id가 사용자 제어 DOM 이벤트에서 서버 사이드 뮤테이션으로 흘러가는데, 서버에서 어차피 현재 워크플로의 게이트 집합과 대조 검증하니 추가 클라이언트 검증은 불필요. 다만 그 불변식이 리뷰를 통해 명시적으로 기록됐다.&lt;/p&gt;
&lt;h3 id="다섯-탭에-걸친-마킹-카드-비주얼-통일"&gt;다섯 탭에 걸친 마킹 카드 비주얼 통일
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;2804420&lt;/code&gt;. 비주얼 통일 패스. 통일된 처리는 디자인 토큰 하나에 모았다.&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;// design-system / marked-card.css
&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;marked&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;card&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;position&lt;/span&gt;: &lt;span class="kt"&gt;relative&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;outline&lt;/span&gt;: &lt;span class="kt"&gt;2px&lt;/span&gt; &lt;span class="nx"&gt;solid&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;revision&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;outline&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;px&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="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nx"&gt;before&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;content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&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;position&lt;/span&gt;: &lt;span class="kt"&gt;absolute&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;top&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;bottom&lt;/span&gt;: &lt;span class="kt"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt;: &lt;span class="kt"&gt;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="nx"&gt;width&lt;/span&gt;: &lt;span class="kt"&gt;3px&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;background&lt;/span&gt;: &lt;span class="kt"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;revision&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;그리고 모든 탭 컴포넌트는 이제 카드를 이렇게 감싼다.&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;card&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;marked-card&amp;#39;&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;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;marked&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;RevisionLabel&lt;/span&gt; &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;}&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="cm"&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="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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;RevisionLabel&lt;/code&gt;도 추출했다 — 예전엔 각 탭이 자기 라벨을 인라인으로 만들면서 카피가 제각각이었다 (&amp;ldquo;수정 요청됨&amp;rdquo;, &amp;ldquo;리비전&amp;rdquo;, &amp;ldquo;Edit Pending&amp;rdquo;). 이제 컴포넌트 하나, 문자열 하나다.&lt;/p&gt;
&lt;h3 id="모든-게이트-마커를-한-번씩만-표시하고-라이브-게이트-상태-반영"&gt;모든 게이트 마커를 한 번씩만 표시하고 라이브 게이트 상태 반영
&lt;/h3&gt;&lt;p&gt;이 날의 마지막 커밋(&lt;code&gt;67b820e&lt;/code&gt;). workspace 상단의 StageStepper가 어떤 상태에서 중복 게이트 마커를 표시하고 있었다(재발신된 게이트 이벤트가 두 번째 점을 추가) 그리고 실제 gate_state 전이보다 뒤처져 있었다. 한 수정에 두 버그.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;게이트 id로 중복 제거해서 각 게이트가 정확히 한 번 렌더&lt;/li&gt;
&lt;li&gt;파이프라인 슬라이스의 라이브 &lt;code&gt;gate_state&lt;/code&gt;(#4의 12상태 필드)를 와이어드해서 stepper가 방금 일어난 어떤 전이든 반영&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;12상태 gate_state 필드는 일주일 전부터 있었지만, stepper는 여전히 오래된 &amp;ldquo;마지막 완료 게이트&amp;rdquo; 추론을 쓰고 있었다. 이 커밋이 그 격차를 닫았다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="커밋-로그-총-23개"&gt;커밋 로그 (총 23개)
&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;2026-05-25&lt;/td&gt;
 &lt;td&gt;fix stale approve gates during storyboard runs&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(terraform): widen EC2 start cron to every day&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;refactor(canvas): drop submit prop drilling, polish selected-copy card&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(logger): add LOG_LEVEL threshold, silence node:sqlite warning&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(observability): emit structured logs across worker + lifecycle paths&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;feat(observability): instrument LLM calls with token usage + logCtx&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-25&lt;/td&gt;
 &lt;td&gt;chore: refresh lockfile peer-dep flags&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): bootstrap projects on workspace refresh, fix hooks order&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;docs(claude): register Diffs Runtime harness pointer in CLAUDE.md&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix: preserve 분석 보고서 across key-concept revisions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): isolate per-project state on switch, show loader during session hydration&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): don&amp;rsquo;t strand /sessions bootstrap behind a stale ref guard&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): make Composer first-brief-only, flip ApproveBar toggle label&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): make bootstrap dep stable so /sessions can&amp;rsquo;t be cancelled forever&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-26&lt;/td&gt;
 &lt;td&gt;fix(web): polyfill crypto.randomUUID so HTTP prod can create projects/sessions&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;docs(claude): add triage-prod-bug skill trigger to enforce browser-first debugging&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;feat(web): add inline past-gate rewind for chat-driven pipeline edits&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;fix(terraform): update daily stop schedule to 03:00 KST in variables&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-27&lt;/td&gt;
 &lt;td&gt;fix(gitignore): add harnesskit session-logs to .gitignore&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): soften rewind dialog copy and hide final gate from discard list&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): update gate titles in ConfirmRewindDialog for clarity&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): preselect gateSelection on 수정요청 enter, clear it on 닫기&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): unify 수정요청 marked-card visual and label across all tabs&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-05-28&lt;/td&gt;
 &lt;td&gt;fix(web): show every gate marker once and reflect live gate state&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;관측성과 격리 모두 조용히 가정되던 상태를 드러내는 일이었다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM 토큰 텔레메트리가 &lt;em&gt;실제로 돈을 쓰던 것&lt;/em&gt;을 드러냈다 — 이전 대시보드는 큐 깊이와 잡 지속시간을 보여줬는데 둘 다 중요하지만, 느린 에이전트가 쌀 수도 있고 빠른 에이전트가 비쌀 수도 있다, 토큰 사용량에 따라. &lt;code&gt;logCtx&lt;/code&gt;를 모든 호출에 꿰어 넣자 비용 분석이 필요로 하는 프로젝트별, 단계별, 에이전트별 분해가 표면화됐다.&lt;/p&gt;
&lt;p&gt;멀티세션 새로고침 수정들이 &lt;em&gt;workspace가 자기 부트스트랩에 대해 가정하던 것&lt;/em&gt;을 드러냈다. 사용자가 launcher에서 들어왔을 때 기능이 작동했던 이유는 launcher의 부트스트랩이 우연히 만족된 전제조건이었기 때문. 새로고침이 그 전제조건을 깨자 버그가 표면화됐다 — 하지만 내내 거기 있었다, 잠복해.&lt;/p&gt;
&lt;p&gt;인라인 과거-게이트 되감기는 같은 아이디어를 사용자의 멘탈 모델에 적용한 것이다 — 사용자는 *&amp;ldquo;GATE 1로 돌아가서 뭔가 바꾸고 싶다&amp;rdquo;*고 생각한다. 되감기 기능은 그 의도를 시스템에 노출시켜 사용자가 네비게이션 단계로 번역하도록 강제하지 않는다. 채팅 우선 원칙은 단지 입력 메커니즘이 아니다 — 시스템이 의도를 이해할 것이라는 약속이다.&lt;/p&gt;
&lt;p&gt;수정요청 어포던스 패스는 같은 빙산의 작은 보이는 끝이다. 세 겹의 명료함 — 비주얼 일관성, 정직한 카피, 상태 보존 — 이 파괴적 워크플로 동작에 적용되어, 확인을 요청하는 다이얼로그가 잃게 될 것에 대해 진실을 말한다.&lt;/p&gt;
&lt;p&gt;다음 — 여기서부터 시스템은 기능적으로 완성됐으니, 미래 개발일지는 기능 추가보다 production 교훈으로 기울 것이다 — 에이전트 프롬프트 튜닝, 비용 추세, 실제 사용자 세션에서 떠오르는 다음 라운드의 UX 패턴. 5포스트 백필이 깔끔한 호를 닫는다 — 목업, production-readying, 메가푸시, 게이트 워크플로, 폴리시. 제품이 이제 진짜다.&lt;/p&gt;</description></item><item><title>Creative Agent Studio 개발일지 #4 — 5개의 게이트, 4개의 캔버스, 그리고 수정 모드 시스템</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-22-creative-agent-studio-dev4/</link><pubDate>Fri, 22 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-22-creative-agent-studio-dev4/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio 개발일지 #4 — 5개의 게이트, 4개의 캔버스, 그리고 수정 모드 시스템" /&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-19-creative-agent-studio-dev3/" &gt;이전 글: #3 — 134커밋 메가푸시&lt;/a&gt;가 PR4(ApproveSheet + 세 게이트 변형)가 머지되고 프로젝트 상태가 리로드를 살아남는 상태로 끝났다. 3일 뒤 &lt;strong&gt;153 커밋&lt;/strong&gt;이 더 떨어져 4단계 에이전트 워크플로의 남은 모든 조각을 완성했다 — 단계 출력을 실체화한 4변형 캔버스 패널(PR5), 새 GATE 1의 키 컨셉 플래너로 3게이트에서 &lt;strong&gt;5&lt;/strong&gt;게이트로의 워크플로 통합, 하단 드로어 ApproveSheet를 슬림 ApproveBar + 출력 탭으로 대체한 UI 재설계, 그리고 사용자가 전체 단계를 다시 하는 대신 단일 키 컨셉, 카피 드래프트, 컷, 또는 콘티 패널을 외과적으로 재실행할 수 있게 하는 &lt;strong&gt;수정 모드 다중 선택 시스템&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;아래에서는 런타임이 Planner-Generator-Evaluator 루프, 사용자 피드백으로부터 저작되는 연속성 앵커, 프롬프트에 주입되는 진화 노트, 기획 보고서와 콘티를 위한 HTML 템플릿 세트, 그리고 한 프로젝트가 여러 병렬 시도를 들고 갈 수 있도록 하는 세션 격리를 받았다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#3 — 메가푸시 (9a2f851)"] --&gt; Theme1["흐름 1 &amp;lt;br/&amp;gt; PR5 — 4단계 캔버스 &amp;lt;br/&amp;gt; + 리사이즈 + 접기"]
 Prev --&gt; Theme2["흐름 2 &amp;lt;br/&amp;gt; 5게이트 워크플로 &amp;lt;br/&amp;gt; GATE 1에 키 컨셉 플래너"]
 Prev --&gt; Theme3["흐름 3 &amp;lt;br/&amp;gt; ApproveBar + 출력 탭 &amp;lt;br/&amp;gt; 드로어 교체"]
 Prev --&gt; Theme4["흐름 4 &amp;lt;br/&amp;gt; 수정 모드 시스템 &amp;lt;br/&amp;gt; 부분 vs 일괄 수정"]
 Theme1 --&gt; End["#4 종료 (5cb4106) &amp;lt;br/&amp;gt; 2026-05-22"]
 Theme2 --&gt; End
 Theme3 --&gt; End
 Theme4 --&gt; End&lt;/pre&gt;&lt;p&gt;네 흐름, 반복되는 한 모양 — &lt;strong&gt;워크플로가 만지는 모든 레이어에서 &amp;ldquo;전부 아니면 전무&amp;quot;가 되기를 멈췄다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-1--pr5-4단계-변형의-캔버스-패널"&gt;흐름 1 — PR5: 4단계 변형의 캔버스 패널
&lt;/h2&gt;&lt;p&gt;PR2가 &lt;code&gt;CanvasPanel&lt;/code&gt; 플레이스홀더를 예약했다. PR5가 그것을 채웠다. 캔버스는 workspace의 오른쪽 컬럼 — 현재 단계의 &lt;em&gt;현재 artifact&lt;/em&gt;를 단계별 컴포넌트로 렌더링한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/components/canvas/stage-canvas.tsx (의역)
&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;StageCanvas&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stage&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 class="nx"&gt;stage&lt;/span&gt;: &lt;span class="kt"&gt;PresentationStage&lt;/span&gt; &lt;span class="p"&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;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&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;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;research&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ResearchCanvas&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="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;copy&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;CopyCanvas&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="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;scenario&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;ScenarioCanvas&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="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;storyboard&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;StoryboardCanvas&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;각 변형은 &lt;code&gt;pipeline&lt;/code&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;th&gt;보여주는 것&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;research&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ResearchCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;pipelineContext&lt;/code&gt;의 &lt;code&gt;AdBriefRecap&lt;/code&gt; + &lt;code&gt;ResearchSummaryCards&lt;/code&gt; + 활성 태스크 목록&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;copy&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;CopyCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;copyHistory&lt;/code&gt;의 &lt;code&gt;CopyOptions&lt;/code&gt;로 만든 &lt;code&gt;ConceptGrid&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;scenario&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;ScenarioCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Act별 &lt;code&gt;SceneStrip&lt;/code&gt; + &lt;code&gt;scenarioHistory&lt;/code&gt;의 &lt;code&gt;CutChip&lt;/code&gt; 스트립&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;storyboard&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;StoryboardCanvas&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;storyboardImageUrls&lt;/code&gt;에서 scene당 &lt;code&gt;StoryboardPage&lt;/code&gt; 1개&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;콘티 이미지 처리는 작은 아키텍처 선택이 필요했다. base64 PNG 페이로드가 &lt;code&gt;storyboard_image&lt;/code&gt; SSE 이벤트로 도착한다. 파이프라인 슬라이스가 각각을 blob URL(&lt;code&gt;URL.createObjectURL&lt;/code&gt;)로 변환하고, 재생성이 이전 이미지를 누수하지 않도록 replace-on-revoke 단계를 둔다. &lt;code&gt;addStoryboardImage&lt;/code&gt; 리듀서가 revoke 로직을 갖는다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;addStoryboardImage&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scene&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revokeObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;old&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base64ToBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;image/png&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;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storyboardImageUrls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;createObjectURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="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;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add useCanvasResizer hook (pointer capture + clamped delta -&amp;gt; ui.canvasWidth)&lt;/code&gt; — &lt;code&gt;ui.canvasWidth&lt;/code&gt;는 ui 슬라이스에서 360..800px로 클램프&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add CanvasResizer component (vertical drag handle + warm hover tint)&lt;/code&gt; — 비주얼 핸들, 호버 틴트는 Creative Warmth를 따른다(미묘한 따뜻한 그라데이션, 순백 하이라이트 없음)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): WorkspacePage grid column reads ui.canvasWidth + respects canvasCollapsed&lt;/code&gt; — 그리드가 실제로 너비에 반응&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;CanvasHeader&lt;/code&gt;는 접기 토글을 얻어서 파워 유저가 채팅에 집중하고 싶을 때 캔버스를 완전히 숨길 수 있다.&lt;/p&gt;
&lt;h3 id="pr6--폴리시--백엔드-스왑--목업-삭제"&gt;PR6 — 폴리시 + 백엔드 스왑 + 목업 삭제
&lt;/h3&gt;&lt;p&gt;PR6은 &amp;ldquo;React로 모든 게 동작한다&amp;quot;와 &amp;ldquo;실제로 사용자에게 React를 서빙한다&amp;rdquo; 사이의 다리였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): ui slice gains fatalError + setFatalError&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ErrorScreen (vercel_unsupported + unexpected kinds, ko/en)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AppErrorBoundary class component (catches descendants -&amp;gt; ErrorScreen)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): useChatStream detects Vercel 503 -&amp;gt; ui.fatalError = vercel_unsupported&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(server): serve web/dist instead of mockup/ (MOCKUP_DIR -&amp;gt; WEB_DIST_DIR)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(deploy): vercel buildCommand + outputDirectory point to web/dist&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chore: remove legacy mockup/ vanilla-JS SPA&lt;/code&gt; — 목업이, 탄생 6주 만에, 한 커밋으로 삭제됐다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;vercel_unsupported&lt;/code&gt; 에러는 특정 종류다 — Vercel의 서버리스 함수는 장기 SSE 연결을 호스트할 수 없으니, 거기 백엔드를 배포하면 채팅 라우트에서 503이 나온다. 프론트엔드가 이걸 명시적으로 감지하고 일반 네트워크 에러 대신 EC2 전용 제약을 설명하는 화면을 보여준다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-2--워크플로가-5게이트로-통합됨"&gt;흐름 2 — 워크플로가 5게이트로 통합됨
&lt;/h2&gt;&lt;p&gt;PR4까지 워크플로는 승인 지점이 세 개였다 — 카피 뒤 하나, 시나리오 뒤 하나, 콘티 끝에 하나. 5월 21일이 **GATE 1(키 컨셉 선택)**을 도입하고 게이트 수를 다섯으로 만들었다.&lt;/p&gt;
&lt;h3 id="gate-1이-존재해야-했던-이유"&gt;GATE 1이 존재해야 했던 이유
&lt;/h3&gt;&lt;p&gt;기존 흐름은 &lt;code&gt;research → copy로 직접&lt;/code&gt; 실행했다. 사용자가 브리프를 제출하면 리서치가 일어나고, 그 다음 카피 단계가 네 가지 평행 드래프트(각각 감성/직설/유머/하이브리드 각도)를 발신했다. 문제 — 카피 드래프트는 사용자에게 &lt;em&gt;어느 방향&lt;/em&gt;을 원하는지 묻지 않고 리서치 에이전트의 브리프 해석을 상속받았다. 프로젝트당 이틀의 수정이 사용자가 요청하지 않은 다른 각도로 카피 단계를 밀어내는 데 들어가고 있었다.&lt;/p&gt;
&lt;p&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;research → [key_concept_planner가 10개 후보 컨셉 생성: 3-3-2-2 분배]
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → GATE 1 (사용자가 하나 선택)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 카피 단계가 선택된 컨셉에 앵커링되어 실행
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;3-3-2-2 분배(상업적 3, 감성적 3, 내러티브 2, 컨셉추얼 2)는 &lt;code&gt;CATEGORY_PLAN&lt;/code&gt;에서 왔다. 뒤이은 리팩토링(&lt;code&gt;refactor(agents): derive key_concept distribution from CATEGORY_PLAN&lt;/code&gt;)이 분배를 하드코딩에서 계획으로부터 계산되게 바꿔서, 한 곳의 믹스 조정이 전파된다.&lt;/p&gt;
&lt;h3 id="프론트엔드--approvegatekeyconcept"&gt;프론트엔드 — ApproveGateKeyConcept
&lt;/h3&gt;&lt;p&gt;10카드 선택 UI가 &lt;code&gt;ApproveGateKeyConcept&lt;/code&gt;로 출시됐다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add KeyConcept gate types&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): ApproveGateKeyConcept 10-card selection drawer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): wire GATE 1 key-concept gate into ApproveGate&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;백엔드 조각.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): emit GATE 1 key-concept gate after research stage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix(runtime): only block key_concept_planner re-enqueue while job is in flight&lt;/code&gt; — 느린 재렌더 dispatch가 필터아웃되던 레이스 방지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): route selectedKeyConcept to copy and feedback re-entry&lt;/code&gt; — 카피 단계가 선택된 컨셉을 받는다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(agents): copywriter_agent anchors copy to the selected key concept&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): persist key_concept_set artifacts&lt;/code&gt; + &lt;code&gt;record GATE 1 key-concept selections in diff_history&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="gate-3컨셉-승인와-gate-5최종-승인"&gt;GATE 3(컨셉 승인)와 GATE 5(최종 승인)
&lt;/h3&gt;&lt;p&gt;두 게이트가 더 워크플로를 채웠다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): add GATE 3 컨셉 승인 — two-phase scenario stage&lt;/code&gt; — 시나리오가 컨셉 승인 → 전체 시나리오로 쪼개졌다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add GATE 5 (최종 승인) to the storyboard stage&lt;/code&gt; — 프로젝트가 완료된 것으로 간주되기 전 최종 승인&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GATE 2(카피)와 GATE 4(전체 시나리오)와 결합되면, 이게 이 시점부터 모든 UI 라벨이 참조할 정전 5게이트 흐름을 만들었다.&lt;/p&gt;
&lt;h3 id="gate_state--12상태-라이브-전이"&gt;gate_state — 12상태 라이브 전이
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat(runtime): wire gate_state 12-state live transitions&lt;/code&gt;가 게이트 라이프사이클을 잡 라이프사이클에서 분리했다. 12상태가 모든 의미 있는 전이를 인코딩(pending → emitted → awaiting_user → user_approved → user_revising → rerunning → reapproved → &amp;hellip;), 각각 프론트엔드로 표면화되어 UI가 추론된 상태를 폴링하지 않고 의미 있는 &amp;ldquo;이 게이트에 무슨 일이 일어나고 있는가&amp;quot;를 렌더할 수 있다.&lt;/p&gt;
&lt;h3 id="gate-1의-기획-보고서"&gt;GATE 1의 기획 보고서
&lt;/h3&gt;&lt;p&gt;&lt;code&gt;feat: add report_writer agent + 리서치 분석 보고서 at GATE 1&lt;/code&gt;이 key_concept_planner와 나란히 도는 두 번째 스페셜리스트를 추가했다 — 10개 키 컨셉 뒤의 &lt;em&gt;컨텍스트&lt;/em&gt;를 사용자에게 주는 구조화된 리서치 분석 보고서를 생성한다. 이게 없으면 사용자는 컨셉을 차갑게 선택한다 — 있으면 기저 추론을 본다.&lt;/p&gt;
&lt;p&gt;그 다음 후속 — &lt;code&gt;feat: add report_writer planning mode for 최종 기획 보고서&lt;/code&gt; — 가 같은 에이전트에게 워크플로 끝에 다른 &lt;em&gt;종류&lt;/em&gt;의 보고서(전체 프로젝트를 요약하는 기획 보고서)를 생성하도록 가르쳤다.&lt;/p&gt;
&lt;h3 id="html-템플릿-세트"&gt;HTML 템플릿 세트
&lt;/h3&gt;&lt;p&gt;보고서는 채팅 버블이 아니라 &lt;em&gt;문서처럼&lt;/em&gt; 보여야 했다. 연속된 커밋들에서 HTML 템플릿 세트 전체가 도착했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: add base.css slide canvas and primitives&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add storyboard cover and continuity-grid templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add deck.css and four simple deck templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add photo-caption and fullbleed-caption deck templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add annotated-photo, product-lineup, info-card, creative-board templates&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add report_writer HTML conversion design spec&lt;/code&gt; + &lt;code&gt;implementation plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add template gallery index&lt;/code&gt; + &lt;code&gt;LLM-facing template catalog&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;템플릿 카탈로그가 LLM 면 조각이다 — 다중 페이지 문서를 조립할 때 선택할 템플릿의 구조화된 리스트를 에이전트가 받는다. 그래야 페이지별 레이아웃을 핸드코딩하지 않고 콘티 커버, 연속성 그리드, 트리트먼트 그리드, 콘티 시퀀스를 한 프롬프트로 만들 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-3--approvebar가-드로어를-교체하고-출력-탭이-인라인-렌더를-교체"&gt;흐름 3 — ApproveBar가 드로어를 교체하고, 출력 탭이 인라인 렌더를 교체
&lt;/h2&gt;&lt;p&gt;2026-05-21까지 하단 드로어 패턴(&lt;code&gt;ApproveSheet&lt;/code&gt;)은 UX 문제가 됐다 — 열면 캔버스를 덮었고, 캔버스가 &lt;em&gt;결과&lt;/em&gt;가 사는 곳이었다. 사용자는 승인 중인 콘티를 보기 위해 계속 드로어를 접어야 했다.&lt;/p&gt;
&lt;p&gt;세 커밋이 드로어를 죽였다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add slim ApproveBar, replace ApproveGate in ChatPanel&lt;/code&gt; — 채팅 패널 하단의 가로 바&lt;/li&gt;
&lt;li&gt;&lt;code&gt;refactor(web): remove ApproveSheet/StageCanvas drawer stack&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): show output panel on active gate + scenario gate advances&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ApproveBar는 두 버튼(승인 / 수정요청) + 게이트 컨텍스트뿐이다 — 미니멀, 드래그할 드로어 없음. 검사할 실제 artifact는 캔버스(오른쪽 컬럼)에 산다, 늘 그랬듯이.&lt;/p&gt;
&lt;h3 id="출력-탭--캔버스가-현재-단계-표시에서-모든-단계의-탭-히스토리로-피벗"&gt;출력 탭 — 캔버스가 &amp;ldquo;현재 단계 표시&amp;quot;에서 &amp;ldquo;모든 단계의 탭 히스토리&amp;quot;로 피벗
&lt;/h3&gt;&lt;p&gt;ApproveBar가 떨어진 직후, 캔버스 자체가 재설계됐다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add output-tabs definitions and visibility logic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add activeOutputTab to ui slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add output fields to pipeline slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): hydrate output slots from artifacts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): route gate/planning outputs into pipeline slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add analysis/keyConcept/planning/concept tab bodies&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add selectable copy tab body&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add OutputTabs container&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): swap CanvasPanel body to OutputTabs&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이전엔 캔버스가 &lt;em&gt;현재&lt;/em&gt; 단계의 artifact만 보여줬다. 이제 위쪽에 탭이 있다 — 분석, 키 컨셉, 컨셉, 카피, 시나리오, 콘티 — 그리고 사용자는 현재 단계에서 작업하면서도 이전 artifact를 되돌아볼 수 있다. 그게 UX를 완전히 바꾼다 — 캔버스가 단지 현재 상태 디스플레이가 아니라 프로젝트 작업면이 됐다.&lt;/p&gt;
&lt;h3 id="planner-generator-evaluator-루프"&gt;Planner-Generator-Evaluator 루프
&lt;/h3&gt;&lt;p&gt;이 UX 이동의 런타임 등가물이 &lt;strong&gt;Planner-Generator-Evaluator 루프&lt;/strong&gt;다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: Planner-Generator-Evaluator loop + BDI structure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): register copy_evaluator in agent-copy map&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): extend the evaluator loop to the scenario stage (§3.4)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(runtime): extend the evaluator loop to the storyboard stage (§3.4)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fix(runtime): re-assess regenerations in the evaluator loop&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;패턴 — 모든 생성 단계가 이제 Planner → Generator → Evaluator를 돈다. Evaluator는 출력이 품질 바를 만족하는지 결정한다 — 아니면 루프가 피드백과 함께 Generator를 재실행한다. 사용자는 루프가 수렴한(또는 최대 반복 상한을 친) 뒤 &lt;em&gt;최종&lt;/em&gt; 출력을 본다. 이게 출력 탭이 가치 있어진 이유이기도 하다 — 각 단계의 기획 artifact와 평가 노트는 사용자가 파보고 싶을 때 보인다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-4--수정-모드-시스템"&gt;흐름 4 — 수정 모드 시스템
&lt;/h2&gt;&lt;p&gt;이게 이 윈도우에서 가장 큰 UX 약속이다. 이 작업 전에는 &amp;ldquo;카드 하나만 다시 하고 싶어요&amp;quot;는 &amp;ldquo;전체 단계를 다시 해주세요&amp;quot;를 의미했다 — 사용자가 재생성하고 싶은 조각 외 모든 걸 외과적으로 고정할 방법이 없었다.&lt;/p&gt;
&lt;h3 id="의도-분류"&gt;의도 분류
&lt;/h3&gt;&lt;p&gt;첫 조각(&lt;code&gt;feat: add revision-intent — classify chat revision as bulk/partial&lt;/code&gt;)이 채팅 입력의 작은 분류기다. 사용자가 게이트 컨텍스트에서 타이핑하면, 시스템이 묻는다 — 이게 &lt;em&gt;일괄&lt;/em&gt; 요청(&amp;ldquo;카피 전체를 더 감성적으로&amp;rdquo;)인가 &lt;em&gt;부분&lt;/em&gt; 요청(&amp;ldquo;세 번째만 바꿔줘&amp;rdquo;)인가? 출력이 다른 런타임 경로를 결정한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;일괄&lt;/strong&gt; → 피드백을 컨텍스트로 두고 전체 단계 재실행&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;부분&lt;/strong&gt; → UI에서 &amp;ldquo;수정 모드&amp;quot;에 진입하고 사용자가 재생성할 항목을 고름&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/orchestration/revision-intent.js (의역)
&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;classifyRevisionIntent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gateContext&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasPartialMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;partial&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;high&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasBulkMarkers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bulk&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;high&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="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;ambiguous&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt; &lt;span class="c1"&gt;// UI가 사용자에게 명확화를 nudge
&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;h3 id="수정-모드-ui"&gt;수정 모드 UI
&lt;/h3&gt;&lt;p&gt;프론트엔드가 ApproveBar에 수정 모드 토글과 모든 선택 가능한 탭에 다중 선택 체크박스를 얻었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: pipeline store — reviseMode + reviseSelection state&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: add RevisionContext type + ChatContext.revision field&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: ApproveBar revise-mode toggle with selection count&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: KeyConceptTab multi-select checkboxes in revise mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: CopyTab multi-select checkboxes in revise mode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: ScenarioCanvas cut-level revise checkboxes&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: StoryboardCanvas panel-level revise checkboxes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;reviseSelection&lt;/code&gt;은 artifact 종류별 &lt;code&gt;Set&amp;lt;id&amp;gt;&lt;/code&gt;다. 사용자는 개별 카드를 토글한다 — 카운트가 ApproveBar에 라이브로 나타나 몇 개 항목이 재생성될지 항상 안다.&lt;/p&gt;
&lt;h3 id="revision-merge--인덱스-splice"&gt;Revision-merge — 인덱스 splice
&lt;/h3&gt;&lt;p&gt;런타임 측은 재생성된 부분집합을 기존 집합으로 &lt;em&gt;실제로 머지&lt;/em&gt;할 방법이 필요했다. 그게 &lt;code&gt;revision-merge&lt;/code&gt; 모듈이다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/orchestration/revision-merge.js (의역)
&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;mergeRevision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalSet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;regeneratedSubset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;selectedIndices&lt;/span&gt;&lt;span class="p"&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;originalSet&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;regeneratedSubset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;newItem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&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="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;selectedIndices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newItem&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;return&lt;/span&gt; &lt;span class="nx"&gt;merged&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;순수 함수 — 선택된 인덱스에서 splice, 나머지는 고정. 끝까지 테스트됨 — &lt;code&gt;test: end-to-end partial revision keeps frozen items + 3-3-2-2&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="각-에이전트가-부분-재생성을-배웠다"&gt;각 에이전트가 부분 재생성을 배웠다
&lt;/h3&gt;&lt;p&gt;다섯 스페셜리스트 에이전트가 선택된 슬롯만 재생성하는 법을 배웠다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: key_concept_planner regenerates selected slots (category-preserving)&lt;/code&gt; — 3-3-2-2 분배를 보존&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: copywriter_agent regenerates selected copy drafts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: concept_anchor applies revision feedback (GATE 3, whole-doc)&lt;/code&gt; — GATE 3은 컨셉 앵커 자체가 단일 artifact이기 때문에 whole-doc만&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: scene_designer cut-level revision (structure + cut count preserved)&lt;/code&gt; — 총 컷 수 보존&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: storyboard_generator regenerates selected panels + images&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;각 에이전트는 자기의 고정 항목 제약이 무엇인지 알아야 했다. 콘티는 컷 수를 재계산하지 않고 패널 3만 재생성할 수 있었지만, scene_designer는 시나리오의 구조적 계약이 컷 수이기 때문에 총 컷 수를 보존해야 했다.&lt;/p&gt;
&lt;h3 id="라우팅-레이어"&gt;라우팅 레이어
&lt;/h3&gt;&lt;p&gt;두 라우팅 커밋이 함께 묶었다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat: root-planner routes context.revision to single target agent&lt;/code&gt; — 백엔드가 필요한 단일 스페셜리스트만 enqueue&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: route chat revision intent — bulk runs, partial nudges revise mode&lt;/code&gt; — 프론트엔드가 옳은 경로로 dispatch&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat: dispatch revise_mode_hint — open revise mode on partial chat intent&lt;/code&gt; — 백엔드가 힌트 이벤트로 프론트엔드에 수정 모드를 &lt;em&gt;제안&lt;/em&gt;할 수 있어서 부분 의도가 UI를 자동으로 연다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;힌트 이벤트는 신중한 경계였다 — 백엔드는 결코 프론트엔드를 강제하지 않는다, 제안만 한다. 프론트엔드는 자기 로컬 상태가 이유가 있으면 오버라이드할 수 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="흐름-5--세션-격리-프로젝트당-멀티-시도"&gt;흐름 5 — 세션 격리 (프로젝트당 멀티 시도)
&lt;/h2&gt;&lt;p&gt;2026-05-22에 workspace가 프로젝트당 단일 세션에서 프로젝트당 멀티 세션으로 갔다. 프로젝트가 이제 병렬 시도를 들고 갈 수 있다 — 단일 크리에이티브 브리프를 세 가지 다른 방식으로 시도할 수 있고, 각각 자기 세션이고 자기 파이프라인 상태를 갖는다.&lt;/p&gt;
&lt;p&gt;핵심 커밋.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Isolate workspace sessions and restore state per session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;server: project sessions list endpoint&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: session restore and run recovery&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Start a fresh pipeline for a new session's first brief&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launcher: show per-session states on project cards&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;launcher/workspace: deletable sessions + per-session gate&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: scope workspace canvas to the active session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web: honor session routes and preserve revision scopes&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Launcher 카드가 이제 세션별 게이트를 보여준다 — 세 세션을 가진 프로젝트, 둘은 콘티에, 하나는 카피에 있으면, 세 상태 모두 렌더한다. 세션은 개별로 삭제 가능. workspace 캔버스, 수정 모드, 게이트 상태가 모두 활성 세션으로 스코프된다.&lt;/p&gt;
&lt;p&gt;가장 큰 보이지 않는 커밋 — &lt;code&gt;Lock the session after final storyboard approval&lt;/code&gt;. GATE 5가 승인되면 세션은 읽기 전용이 된다. 봉인된 산출물에 우발적 편집을 막고, 사용자가 더 반복하고 싶으면 의식적으로 새 세션을 시작하도록 강제한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;3일에 네 흐름 — 하지만 반복되는 모양은 같다 — &lt;strong&gt;&amp;ldquo;전부 아니면 전무&amp;quot;의 무언가 granular한 것으로의 붕괴.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;드로어-또는-캔버스 이진이 출력 탭 연속체가 됐다 (모든 artifact를 한 번에 볼 수 있다)&lt;/li&gt;
&lt;li&gt;세 게이트가 다섯이 됐다 (새 게이트가 가장 비싼 하류 비용을 가드한다 — 카피 + 시나리오 + 콘티가 선택된 방향으로 실행)&lt;/li&gt;
&lt;li&gt;전체 단계 재실행이 부분 수정이 됐다 (정확히 원하는 것만 재생성)&lt;/li&gt;
&lt;li&gt;프로젝트당 단일 세션이 프로젝트당 멀티 세션이 됐다 (프로젝트가 병렬 아이디어를 들고 갈 수 있다)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 이동들 각각은 이진 버전보다 더 많은 코드 비용이 들었다 — 부분 수정 하나만 해도 의도 분류, 다섯 에이전트의 고정 항목 보존 로직, 스토어의 Set-of-IDs, 인덱스 splice 머지 함수가 필요했다 — 하지만 각각이 사용자가 조용히 견디고 있던 한 부류의 좌절을 제거했다. 153커밋 카운트는 영웅적으로 보인다 — 관통하는 줄은 더 겸손하다 — 시스템이 &amp;ldquo;전부 아니면 전무&amp;quot;라고 말할 때마다, &amp;ldquo;당신이 의미한 것만 정확히&amp;quot;로 교체.&lt;/p&gt;
&lt;p&gt;다음 — 폴리시 주간 — 관측성 계측, 새로고침 아래 프로젝트별 상태 격리, 채팅에서 인라인 과거 게이트 되감기, 그리고 모든 탭에서 비주얼 처리를 마침내 통일한 수정요청 어포던스 패스.&lt;/p&gt;</description></item><item><title>Creative Agent Studio 개발일지 #3 — 134커밋 메가푸시: PR1 Launcher, PR2 Workspace, PR3 SSE, PR4 ApproveGate</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-19-creative-agent-studio-dev3/</link><pubDate>Tue, 19 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-19-creative-agent-studio-dev3/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio 개발일지 #3 — 134커밋 메가푸시: PR1 Launcher, PR2 Workspace, PR3 SSE, PR4 ApproveGate" /&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-18-creative-agent-studio-dev2/" &gt;이전 글: #2 — 비트버킷 마이그레이션, production-readying, React 재작성 시작&lt;/a&gt;이 React 인프라가 부트스트랩된 상태로 끝났다 — 빈 Zustand 스토어, Vite + Tailwind + TS, 세 프리미티브, &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; i18n 컴포넌트, 그리고 PR1 계획. 24시간 뒤 &lt;strong&gt;134개 비머지 커밋&lt;/strong&gt;이 네 PR을 순차적으로 떨어뜨려, 진짜 데이터베이스 영속화와 라이브 SSE 스트리밍을 갖춘 동작하는 React 프론트엔드를 만들어냈다.&lt;/p&gt;
&lt;p&gt;작업은 네 PR과 그 아래 한 스택으로 떨어졌다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PR1 — Launcher&lt;/strong&gt; (projects CRUD, ProjectCard, NewProjectCard, CreateProjectModal, ProjectGrid)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR2 — Workspace 셸&lt;/strong&gt; (StageStepper, ProjectSubbar, SessionsRail, Composer, ChatPanel, CanvasPanel 플레이스홀더)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR3 — SSE + 채팅 흐름&lt;/strong&gt; (chat-stream 파서, dispatch-sse 매퍼, useChatStream 훅, AgentAvatar, 7가지 FeedItem 변형 전체, ChatFeed)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PR4 — ApproveSheet + Gates&lt;/strong&gt; (3-모드 드로어, ApproveGateCopy / Scenario / ResearchInput 변형)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;아래&lt;/strong&gt; — &lt;code&gt;projects&lt;/code&gt;, &lt;code&gt;artifacts&lt;/code&gt;, &lt;code&gt;diff_history&lt;/code&gt; 테이블, CRUD 작업, 전체 REST API, 소프트 삭제 시맨틱&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#2 — 부트스트랩 (752095f)"] --&gt; DB["DB 레이어 &amp;lt;br/&amp;gt; 마이그레이션 + CRUD + REST"]
 DB --&gt; PR1["PR1 — Launcher &amp;lt;br/&amp;gt; 프로젝트가 리로드를 살아남음"]
 PR1 --&gt; PR2["PR2 — Workspace 셸 &amp;lt;br/&amp;gt; StageStepper + SessionsRail + Composer"]
 PR2 --&gt; PR3["PR3 — SSE 채팅 흐름 &amp;lt;br/&amp;gt; chat-stream + dispatch-sse + 7 피드 변형"]
 PR3 --&gt; PR4["PR4 — ApproveSheet &amp;lt;br/&amp;gt; 3-모드 드로어 + 3 게이트 변형"]
 PR4 --&gt; End["#3 종료 (9a2f851) &amp;lt;br/&amp;gt; artifacts로부터 프로젝트 게이트 복원"]&lt;/pre&gt;&lt;p&gt;관통하는 한 가지 주제 — &lt;strong&gt;표면을 아래에서 위로 짓고, 어떤 커밋도 자기가 건너서는 안 되는 레이어를 건너지 않게 하라.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="데이터베이스-레이어--ui와-같은-날"&gt;데이터베이스 레이어 — UI와 같은 날
&lt;/h2&gt;&lt;p&gt;세 마이그레이션과 작은 CRUD 모듈들이 &lt;code&gt;projects&lt;/code&gt;, &lt;code&gt;artifacts&lt;/code&gt;, &lt;code&gt;diff_history&lt;/code&gt;를 일급으로 만들었다. 첫 커밋(&lt;code&gt;feat(db): add projects, artifacts, diff_history tables via migrations&lt;/code&gt;)이 모양을 잡았다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;brand&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;deleted_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&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="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&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;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;artifacts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- JSON
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;evidence_uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- §8.2 — provenance
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&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;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diff_history&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;before&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- JSON (첫 선택 시 null 가능)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;after&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;-- JSON
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&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="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;세 커밋 뒤 CRUD 모듈이 떨어졌다 — &lt;code&gt;createProject&lt;/code&gt;, &lt;code&gt;listProjects&lt;/code&gt;(active만, updated_at desc), &lt;code&gt;getProject&lt;/code&gt;(감사용으로 deleted 행도 반환), &lt;code&gt;renameProject&lt;/code&gt;(부분 패치 시맨틱), &lt;code&gt;softDeleteProject&lt;/code&gt;(idempotent — &lt;code&gt;deleted_at&lt;/code&gt;을 두 번 플립하지 않음).&lt;/p&gt;
&lt;p&gt;idempotency 규칙은 &lt;code&gt;fix(db): softDeleteProject preserves deleted_at on idempotent re-call&lt;/code&gt;에서 명시적으로 테스트됐다. 원래 구현은 &lt;code&gt;UPDATE ... SET deleted_at = ?&lt;/code&gt;를 호출해서 두 번 호출되면 이전 삭제 타임스탬프를 덮어썼다. 수정 — &lt;code&gt;UPDATE ... SET deleted_at = ? WHERE deleted_at IS NULL&lt;/code&gt;. 작은 버그, 큰 원칙 — 소프트 삭제는 묘비지 플래그가 아니다.&lt;/p&gt;
&lt;p&gt;그 다음 REST API가 다섯 커밋으로 위에 올라왔다.&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;POST /api/projects → createProject
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /api/projects → listProjects
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;GET /api/projects/:id → getProject (deleted면 404 — fix(api))
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;PATCH /api/projects/:id → renameProject
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DELETE /api/projects/:id → softDeleteProject
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;별개의 수정이 GET과 PATCH에서 소프트 삭제된 프로젝트를 404로 처리하도록 했다(하위 CRUD 경로에서는 감사로 보이지만 REST 표면에서는 보이지 않음). diff_history 모듈은 &lt;code&gt;appendDiff&lt;/code&gt;를 추가했고, &lt;code&gt;after&lt;/code&gt;가 &lt;code&gt;null&lt;/code&gt;일 때 리터럴 &lt;code&gt;'null'&lt;/code&gt; 문자열을 저장하지 않도록 가드를 넣었다 — 누군가의 프로젝트 히스토리에서 UI에 &lt;code&gt;&amp;quot;null&amp;quot;&lt;/code&gt; 문자열이 렌더되는 걸 발견할 때만 명백한 종류의 버그.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pr1--launcher"&gt;PR1 — Launcher
&lt;/h2&gt;&lt;p&gt;사용자가 보는 첫 화면. 프로젝트 목록, 새로 만들기 어포던스, 검색.&lt;/p&gt;
&lt;p&gt;페이지 아키텍처를 셋업한 구조적 커밋 몇 개.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): real projects slice (CRUD)&lt;/code&gt; — sort-by-updated를 갖춘 &lt;code&gt;useProjects&lt;/code&gt; 훅&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add userName + setUserName to ui slice&lt;/code&gt; — 이중언어 인사말에 보이는 영속 사용자 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AppTopbar with logo + lang toggle&lt;/code&gt; — 사이트 크롬&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add LauncherTopbar + LauncherHero with bilingual greeting&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add SessionsRail hidden-mode stub (PR2 fills workspace mode)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 다음 실제 목록과 생성 흐름.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ProjectsSearch with bilingual placeholder and count kicker&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ProjectCard with progress bar and brand/status&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add NewProjectCard with dashed border and accent hover&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add CreateProjectModal with title + optional brand + validation&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): wire ProjectGrid — search filter, modal, navigation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;카드 진행 막대는 프로젝트가 진행한 같은 &lt;code&gt;gate&lt;/code&gt; 필드를 읽는다 — 그래서 launcher 카드는 각 프로젝트가 어디까지 갔는지의 라이브 스냅샷이다. 클릭 → &lt;code&gt;/projects/:projectId&lt;/code&gt;로 이동. 끝.&lt;/p&gt;
&lt;p&gt;이 PR이 깔끔한 이유는 엄격한 레이어링이다 — 모든 인터랙티브 조각은 스토어에서 읽고, 스토어는 REST API에서 읽고, REST API는 CRUD 모듈에서 읽고, CRUD 모듈은 SQLite에서 읽는다. 지름길 없음. 컴포넌트 자체는 슬라이스만 안다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pr2--workspace-셸"&gt;PR2 — Workspace 셸
&lt;/h2&gt;&lt;p&gt;사용자가 프로젝트 카드를 클릭한 뒤 도착하는 화면. 세 컬럼 — 왼쪽 sessions rail, 가운데 채팅, 오른쪽 canvas 플레이스홀더. 실제 단계별 워크플로는 이 셸 안에 산다.&lt;/p&gt;
&lt;p&gt;11개 구조적 커밋.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add stage-labels lib (gate ↔ stage + ko/en short/long labels)&lt;/code&gt; — 단계의 이중언어 라벨로의 정전 매핑, stepper와 canvas 양쪽에서 사용&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add Session type&lt;/code&gt; + &lt;code&gt;real workspace slice (sessions + composer)&lt;/code&gt; + &lt;code&gt;store-init test&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add sessionsPanelOpen + toggleSessionsPanel to ui slice&lt;/code&gt; — 레일이 접힌다&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add StageStepper (6 dots + bilingual long-form label)&lt;/code&gt; — 단계 진행 표시기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ProjectSubbar wrapping StageStepper&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): fill SessionsRail workspace mode (project header + sessions list + new + back)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add Composer (pill input + send button, console-log submit only)&lt;/code&gt; — 채팅 입력, 아직 제출 와이어링 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ChatPanel skeleton (header + welcome bubble + Composer)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add CanvasPanel placeholder (PR5 fills variants)&lt;/code&gt; — 명시적 연기&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): assemble WorkspacePage (.shell grid) + swap workspace route&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;플레이스홀더 패턴을 짚을 만하다. &lt;code&gt;CanvasPanel&lt;/code&gt;이 커밋 메시지에 명시적으로 &amp;ldquo;PR5 fills variants&amp;quot;라고 적힌 플레이스홀더로 추가됐다. 그것은 레이아웃에 구멍을 남기지 않고 큰 작업을 연기했다 — 레이아웃은 끝났고, 슬롯은 와이어드됐고, 실제 콘텐츠는 사용자에게 아직 출시되지 않을 스텁이었다. PR5가 깔끔하게 떨어질 수 있었던 이유는 PR2가 그 주소를 예약해놨기 때문이다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pr3--sse-배관-채팅-흐름-그리고-7가지-피드-변형"&gt;PR3 — SSE 배관, 채팅 흐름, 그리고 7가지 피드 변형
&lt;/h2&gt;&lt;p&gt;이 PR이 정적 셸을 라이브 앱으로 바꿨다. 세 레이어에 걸친 20+ 커밋 — 스토어, 배관, 컴포넌트.&lt;/p&gt;
&lt;h3 id="스토어-레이어"&gt;스토어 레이어
&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/store/slices/feed.ts (의역)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FeedItem&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="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;user&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;assistant&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;streaming&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;agent_progress&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;stage_complete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stage&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;system_error&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;: &lt;span class="kt"&gt;string&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 class="nx"&gt;kind&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;task_update_note&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&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="kr"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FeedSlice&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="nx"&gt;feed&lt;/span&gt;: &lt;span class="kt"&gt;FeedItem&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;appendChunk&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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="nx"&gt;splitParagraphs&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="k"&gt;void&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="nx"&gt;pushAgentProgress&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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;pushStageComplete&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&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="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;일곱 가지 피드 아이템 타입에 대한 식별된 유니온, 슬라이스의 kind별 리듀서. 그리고 게이트/단계/컨텍스트/lastSubmit 상태를 위한 &lt;code&gt;pipeline&lt;/code&gt; 슬라이스, &lt;code&gt;tasks&lt;/code&gt; + &lt;code&gt;activeSubAgents&lt;/code&gt;(agent_activity 봉투의 싱크)를 얻은 &lt;code&gt;workspace&lt;/code&gt; 슬라이스.&lt;/p&gt;
&lt;h3 id="배관-레이어"&gt;배관 레이어
&lt;/h3&gt;&lt;p&gt;세 순수 모듈과 한 훅.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add chat-stream lib (POST /api/chat + SSE parse + Zod-validated envelopes)&lt;/code&gt; — 와이어 레벨 파서&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add dispatch-sse mapper (SSEEnvelope → store actions)&lt;/code&gt; — 순수 매퍼, DOM 없음&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add useChatStream hook (submit/cancel + AbortController lifecycle)&lt;/code&gt; — 라이프사이클 소유자&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/hooks/use-chat-stream.ts (의역)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;useChatStream() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controllerRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;AbortController&lt;/span&gt; &lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="na"&gt;null&lt;/span&gt;&lt;span class="p"&gt;&amp;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&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;: &lt;span class="kt"&gt;ChatContext&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="nx"&gt;controllerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abort&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;controller&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;AbortController&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;controllerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;controller&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="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envelope&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;streamChat&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt;: &lt;span class="kt"&gt;controller.signal&lt;/span&gt; &lt;span class="p"&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="nx"&gt;dispatchSse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;envelope&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="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="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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;controllerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;abort&lt;/span&gt;&lt;span class="p"&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cancel&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;분할은 — chat-stream은 파싱, dispatch-sse는 매핑, useChatStream은 라이프사이클 소유. 셋, 모듈 셋 — 어떤 모듈도 하나 이상의 일을 하지 않았다. 각각의 테스트(&lt;code&gt;tighten test fetch types&lt;/code&gt; 등)가 같은 날 출시됐다.&lt;/p&gt;
&lt;h3 id="컴포넌트-레이어"&gt;컴포넌트 레이어
&lt;/h3&gt;&lt;p&gt;봉투가 스토어에 떨어질 수 있게 되자 컴포넌트가 렌더했다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AgentAvatar + fix shared/events runtime exports&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add User/Assistant/Streaming bubble components&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AgentProgress + StageComplete + SystemError + TaskUpdateNote&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add FeedItem discriminated dispatcher (7 variants)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add AgentStrip ('Running now' active sub-agents)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ChatFeed (auto-scroll + FeedItem map + welcome empty state)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): wire ChatPanel to AgentStrip + ChatFeed (replaces static welcome bubble)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;FeedItem&lt;/code&gt; 디스패처는 식별된 유니온 패턴이 보답하는 모습이다 — 모든 변형을 자기 컴포넌트로 매핑하는 한 곳, TS가 강제하는 망라성.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AgentStrip&lt;/code&gt;은 &lt;code&gt;interaction-model.md&lt;/code&gt;의 결정 2 — &amp;ldquo;한 줄 상태, 전체 대시보드 없음&amp;rdquo; — 의 실체화였다. 채팅 상단을 가로지르는 가로 스트립으로 현재 실행 중인 서브 에이전트만 표시한다. 사이드 패널 아니고, 대시보드 아니고, 그냥 작업이 일어날 때 나타났다 끝나면 사라지는 인라인 스트립.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="pr4--approvesheet와-세-게이트-변형"&gt;PR4 — ApproveSheet와 세 게이트 변형
&lt;/h2&gt;&lt;p&gt;채팅 전용 흐름을 채팅 + 승인 흐름으로 바꾼 조각.&lt;/p&gt;
&lt;p&gt;ApproveSheet는 세 모드를 갖춘 하단 드로어다 — collapsed(게이트 프롬프트만), half-open(게이트 + 요약), full(게이트 + 전부). 포인터 이벤트로 드래그 리사이즈. Esc로 접힘. 세 게이트 변형이 같은 셸에 떨어진다 — 각 게이트 단계는 자기 변형 컴포넌트를 받는다.&lt;/p&gt;
&lt;p&gt;12개의 구조적 커밋.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ApproveGate payload types&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add approveSheetMode + setApproveSheetMode to ui slice&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): pipeline slice gains copy/scenario history + idx setters&lt;/code&gt; — 과거 드래프트 사이를 전환하는 백포인터 상태&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): projects slice gains bumpProjectGate + assert PR4 fields in store-init&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): dispatch-sse pushes copy/scenario history + bumps project gate on stage_complete&lt;/code&gt; — 게이트 진행을 위한 SSE → 스토어 glue&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그 다음 드로어 메커닉.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add useApproveSheetDrag hook + PointerEvent jsdom polyfill&lt;/code&gt; — 드래그 핸들 훅. PointerEvent는 jsdom에 기본으로 없으니 폴리필이 함께 출시.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ApproveSheet base (3-mode drawer + collapse toggle + drag handle)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그리고 세 게이트 변형.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ApproveGateCopy (grid + select + approve / request-edit)&lt;/code&gt; — 사용자가 N개 카피 드래프트 중 하나를 선택&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ApproveGateScenario (acts list + approve / request-edit)&lt;/code&gt; — 사용자가 다막 시나리오를 승인하거나 수정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feat(web): add ApproveGateResearchInput (question + answer + send)&lt;/code&gt; — 다른 모양 — 에이전트가 사용자에게 리서치 질문을 하고 타이핑한 답을 기다림&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;ApproveGate&lt;/code&gt; 디스패처(변형 선택 + 제출 와이어링)로 함께 묶이고, &lt;code&gt;ChatPanel&lt;/code&gt; 안에 마운트되고, &lt;code&gt;WorkspacePage&lt;/code&gt;에서 submit이 forward된다. 사용자 흐름은 이제 — 채팅에서 묻고 → 에이전트가 돌고 → artifact와 함께 드로어가 올라오고 → 승인 또는 수정. &lt;code&gt;interaction-model.md&lt;/code&gt;의 결정 3(단계 사이 게이트 기반 자동 실행)이 살아 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="하루-끝-상태"&gt;하루 끝 상태
&lt;/h2&gt;&lt;p&gt;오늘의 마지막 커밋(&lt;code&gt;9a2f851 feat(web): restore project gate from artifacts on workspace mount (Slice K)&lt;/code&gt;)이 중요한 루프를 닫았다 — 사용자가 workspace 페이지를 리로드하면, 프로젝트의 게이트 상태가 영속된 artifacts에서 복원된다. 오늘의 작업이 새로고침을 살아남는다.&lt;/p&gt;
&lt;p&gt;아래 스택과 PR1-PR4 사이에서, React 프론트엔드는 하루 끝에 다음을 갖췄다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;동작하는 launcher (진짜 프로젝트, 리로드를 가로질러 영속)&lt;/li&gt;
&lt;li&gt;3컬럼 레이아웃의 workspace 셸&lt;/li&gt;
&lt;li&gt;SSE 스트리밍 라이브 채팅&lt;/li&gt;
&lt;li&gt;모든 7가지 피드 변형이 렌더링&lt;/li&gt;
&lt;li&gt;3가지 게이트 변형을 가진 ApproveSheet 드로어&lt;/li&gt;
&lt;li&gt;리로드를 살아남는 프로젝트 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;목업은 아직 삭제되지 않았지만, 목업이 시연하려던 모든 것이 이제 React에서 돈다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="인사이트"&gt;인사이트
&lt;/h2&gt;&lt;p&gt;이 날이 작동한 이유는 모든 커밋이 자기 레이어를 존중했기 때문이다. 스토어 슬라이스는 PR4가 필요로 할 모양으로 PR2에서 선언됐다. PR3의 SSE 배관은 세 모듈 — 파서, 매퍼, 라이프사이클 — 로 각각 독립적으로 테스트될 수 있었다. PR4의 ApproveGate 변형들은 PR3의 FeedItem 디스패처와 매칭되는 디스패처 패턴에 꽂혔다.&lt;/p&gt;
&lt;p&gt;일어나지 &lt;em&gt;않은&lt;/em&gt; 일 — 그리고 134커밋이 카오스가 아니었던 이유 — 는 &lt;em&gt;레이어 교차 커밋&lt;/em&gt;이다. 어떤 커밋도 &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;과 SQL 컬럼을 같이 추가하지 않았다. 어떤 커밋도 CRUD 모듈에서 슬라이스로 손을 뻗지 않았다. 커밋이 레이어를 가로질러야 할 때는 한 레이어 아래의 기반 커밋이 선행하거나 한 레이어 위의 채택자 커밋이 뒤따랐다. 이 규율이 패치워크 스프린트가 됐을 것을 깔끔한 추가 스택으로 바꿨다.&lt;/p&gt;
&lt;p&gt;새겨둘 멘탈 모델 — &lt;strong&gt;빠르다는 건 지름길이 아니다.&lt;/strong&gt; 이 날이 네 PR을 출시한 이유는 각 PR이 존재하게 한 규율 때문이지, 그 규율을 무시했기 때문이 아니다.&lt;/p&gt;
&lt;p&gt;다음 — canvas 패널, 5게이트 워크플로, key-concept planner, 그리고 사용자가 전체 단계가 아닌 단일 카드만 외과적으로 재실행할 수 있게 하는 revise-mode 다중 선택 패턴.&lt;/p&gt;</description></item><item><title>Creative Agent Studio 개발일지 #2 — 비트버킷 마이그레이션, production-readying, 그리고 React 재작성의 시작</title><link>https://ice-ice-bear.github.io/ko/posts/2026-05-18-creative-agent-studio-dev2/</link><pubDate>Mon, 18 May 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-05-18-creative-agent-studio-dev2/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio 개발일지 #2 — 비트버킷 마이그레이션, production-readying, 그리고 React 재작성의 시작" /&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-07-creative-agent-studio-dev1/" &gt;이전 글: #1 — 4월 목업 시절과 Creative Warmth 테마&lt;/a&gt;를 6주 전에 올렸다. 긴 잠복기를 지나, 2026-05-18이 &lt;strong&gt;하루에 27커밋&lt;/strong&gt;으로 폭발했다 — 그리고 깔끔하게 세 스택으로 떨어졌다. 첫째, 레포 이전을 표시하는 비트버킷 마이그레이션 스냅샷. 둘째, &lt;strong&gt;Node 런타임의 production-readying 패스&lt;/strong&gt; — 구조화 JSON 로거, &lt;code&gt;AbortSignal&lt;/code&gt; 기반 graceful 워커 셧다운, &lt;code&gt;/api/health&lt;/code&gt; + &lt;code&gt;/api/metrics&lt;/code&gt;, 싱글 EC2 배포 자산(systemd, nginx, litestream), 그리고 모놀리식 큐를 쪼개는 세 리팩토링. 셋째, &lt;strong&gt;72시간 안에 목업을 완전히 대체할 React+Vite+TypeScript 부트스트랩&lt;/strong&gt; — 4월에서 그대로 가져온 Creative Warmth 토큰의 Tailwind, 5개 빈 슬라이스의 Zustand 스토어, &lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; i18n 컴포넌트, 그리고 첫 디자인 프리미티브.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Prev["#1 — 4월 목업 &amp;lt;br/&amp;gt; (21f0ebc)"] --&gt; Stack1["스택 1 &amp;lt;br/&amp;gt; 비트버킷 마이그레이션 스냅샷"]
 Prev --&gt; Stack2["스택 2 &amp;lt;br/&amp;gt; 런타임 강화 &amp;lt;br/&amp;gt; 로거 / 셧다운 / health / 배포"]
 Prev --&gt; Stack3["스택 3 &amp;lt;br/&amp;gt; web/ 부트스트랩 &amp;lt;br/&amp;gt; React + Vite + Tailwind + Zustand"]
 Stack1 --&gt; End["#2 종료 &amp;lt;br/&amp;gt; (752095f)"]
 Stack2 --&gt; End
 Stack3 --&gt; End&lt;/pre&gt;&lt;p&gt;이 날의 관통 주제 — &lt;strong&gt;더 이상 프로토타입처럼 다루지 마라.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="스택-1--비트버킷-마이그레이션-스냅샷"&gt;스택 1 — 비트버킷 마이그레이션 스냅샷
&lt;/h2&gt;&lt;p&gt;오늘의 첫 커밋(&lt;code&gt;9d414f2 chore: snapshot project state for bitbucket migration&lt;/code&gt;)은 코드 변경이 아니라 마커였다. 레포는 엔지니어링 외의 이유로 옮겨가고, 스냅샷 커밋은 컷오버를 가독성 있게 만든다 — 이게 옛 집에서 가져온 상태고, 이후 모든 것은 새 집이다.&lt;/p&gt;
&lt;p&gt;미래의 &amp;ldquo;뭐가 바뀐 거지?&amp;rdquo; 조사가 &amp;ldquo;이동에서 살아남은 코드&amp;rdquo; vs &amp;ldquo;이동 이후 추가된 코드&amp;quot;를 찾을 때 이 커밋에서 멈출 수 있다는 점에서 기록할 가치가 있다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="스택-2--런타임-production-readying"&gt;스택 2 — 런타임 production-readying
&lt;/h2&gt;&lt;h3 id="구조화-json-로거--채택"&gt;구조화 JSON 로거 + 채택
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;ead6854&lt;/code&gt;와 &lt;code&gt;d0b9ea4&lt;/code&gt;가 구조화 JSON 로거를 도입하고 채팅 라우트 + 워커 라이프사이클 경로에 채택했다. 기존 패턴은 &lt;code&gt;console.log&lt;/code&gt; 문자열이었는데, 로그 애그리게이터에 실제 배포되면 살아남지 못한다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/observability/logger.js (의역)
&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;createLogger&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="p"&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;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="nx"&gt;info&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;info&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&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="nx"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;warn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&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="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;level&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 class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&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="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;계약은 일부러 최소화 — 심각도 하나, 메시지 문자열 하나, 컨텍스트 객체 하나. 채팅 라우트가 첫 채택자였던 이유 — SSE 프레임이 발신되는 곳이고, 거기서 비구조화된 &lt;code&gt;console.log&lt;/code&gt;는 stdout에서 프레임 헤더와 인터리브되어 로그를 망친다.&lt;/p&gt;
&lt;h3 id="abortsignal-기반-graceful-워커-셧다운"&gt;AbortSignal 기반 graceful 워커 셧다운
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;d9bf640&lt;/code&gt;. 기존 워커 루프는 무한 &lt;code&gt;while (true)&lt;/code&gt; 클레임 앤 런이었다. 워커 프로세스에 &lt;code&gt;kill -TERM&lt;/code&gt;을 보내면 잡 중간에 죽고, SQLite &lt;code&gt;jobs&lt;/code&gt; 행은 &lt;code&gt;running&lt;/code&gt; 상태에 stale &lt;code&gt;worker_lock&lt;/code&gt;을 들고 남았다 — 결국 stale-timeout 메커니즘으로 회수되지만 몇 분의 지연이 있었다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AbortSignal&lt;/code&gt;을 루프에 꿰어 넣은 수정:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-js" data-lang="js"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// runtime/workers/worker-loop.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;startWorkerLoop&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&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;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&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="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;claimNextJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&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="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&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="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;POLL_INTERVAL_MS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&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;continue&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;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="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;runJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;signal&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="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&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;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aborted&lt;/span&gt;&lt;span class="p"&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="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;releaseJobForReclaim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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="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;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&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="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;SIGTERM&lt;/code&gt; 핸들러가 &lt;code&gt;controller.abort()&lt;/code&gt;를 호출하고 워커가 안정될 때까지 기다리는 &lt;code&gt;server/index.js&lt;/code&gt; 변경과 결합되면, 5분 회수 윈도우가 즉시 깔끔한 셧다운으로 바뀐다 — 롤링 배포에 중요하다.&lt;/p&gt;
&lt;h3 id="일일-유지보수--apihealth--apimetrics"&gt;일일 유지보수 + /api/health + /api/metrics
&lt;/h3&gt;&lt;p&gt;세 커밋이 운영 격차를 닫았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;3ac2dfa&lt;/code&gt; — 주기적 이벤트 로그 보존. SSE 프레임이 영속화되면서 &lt;code&gt;events&lt;/code&gt; 테이블이 단조 증가한다. 보존 없이는 장기 실행 배포가 디스크를 채운다. cron 같은 잡이 매일 돌면서 N일보다 오래된 이벤트를 삭제한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;89c7b05&lt;/code&gt; — &lt;code&gt;/api/health&lt;/code&gt;(부트 프로브)와 &lt;code&gt;/api/metrics&lt;/code&gt;(Prometheus 스타일 카운터). 이제 로드 밸런서가 자기 일을 할 수 있고, Grafana 대시보드가 마침내 존재할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;d0b9ea4&lt;/code&gt; — 세 가지(셧다운, 유지보수, 로거)를 &lt;code&gt;server/index.js&lt;/code&gt; 부트에 모두 꿰어 넣음.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Grafana 셋업 자체는 자기 문서를 받았다(&lt;code&gt;4236122 docs: add Grafana Cloud setup guide for EC2 deployment&lt;/code&gt;) — 메트릭 셋이 한 번 배포하고 잊어버릴 만큼 작았기 때문이다. 런타임의 텔레메트리 관심사는 큐 깊이, 잡 지속시간, 게이트 승인까지의 시간뿐이었다.&lt;/p&gt;
&lt;h3 id="싱글-ec2-배포-자산"&gt;싱글 EC2 배포 자산
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;9ac967f&lt;/code&gt;가 싱글 EC2 배포 스택 전체를 추가했다 — Node 서버 + 워커용 systemd 유닛 파일, TLS를 종료하고 &lt;code&gt;:7878&lt;/code&gt;로 프록시하는 nginx 설정, 그리고 &lt;code&gt;data/runtime.sqlite&lt;/code&gt;를 S3로 지속적으로 복제하는 Litestream 설정. Litestream이 핵심이다 — 애플리케이션 코드를 바꾸지 않고 지속적 시점 백업을 제공해서 소규모 팀 앱에 SQLite를 production 선택지로 옹호할 수 있게 한다.&lt;/p&gt;
&lt;h3 id="런타임-리팩토링"&gt;런타임 리팩토링
&lt;/h3&gt;&lt;p&gt;다가올 PR0-PR4 폭주에 코드베이스를 준비시킨 세 동반 리팩토링.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;1cf3bce refactor: split runtime/queue/jobs.js into runs/jobs/events modules&lt;/code&gt; — &lt;code&gt;jobs.js&lt;/code&gt;가 실행 라이프사이클, 잡 클레임, 이벤트 영속화를 뒤섞은 god-module이 됐었다. 단일 책임 세 모듈로 쪼갬.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;4ee661e refactor: extract stage state machine + route events through persistEvent&lt;/code&gt; — &amp;ldquo;실행이 어느 단계에 있고, 거기서 어디로 전이할 수 있는가&amp;rdquo; 로직이 워커 곳곳에 흩뿌려져 있었다. 단일 상태 머신 모듈로 추출. 모든 이벤트 쓰기는 &lt;code&gt;persistEvent&lt;/code&gt;를 거치니 빠진 이벤트가 silent 버그가 될 수 없다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c7291af refactor: consolidate Google AI SDK on @google/genai&lt;/code&gt; — 코드베이스가 두 Google SDK 패키지를 동시에 사용하고 있었다. &lt;code&gt;@google/genai&lt;/code&gt;로 통합.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;런타임 SQLite는 &lt;code&gt;data/&lt;/code&gt;로 이전됐다(&lt;code&gt;2e3ac8a&lt;/code&gt;) — &lt;code&gt;.gitignore&lt;/code&gt;-by-intent 파일이 WAL/SHM 저널 옆 전용 서브디렉토리에 살 수 있도록.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="스택-3--react-재작성-부트스트랩"&gt;스택 3 — React 재작성 부트스트랩
&lt;/h2&gt;&lt;p&gt;목업의 날들은 카운트다운 중이었다. 디자인 스펙(&lt;code&gt;253f83d docs: add design spec for mockup → web/ (React + Vite + TS) rebuild&lt;/code&gt;)과 구현 계획(&lt;code&gt;c81b248 docs: add PR0 implementation plan for web/ infra bootstrap&lt;/code&gt;)이 타깃을 선언했다. 20개 커밋이 그것을 실체화했다.&lt;/p&gt;
&lt;h3 id="패키지--툴링"&gt;패키지 + 툴링
&lt;/h3&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;web/package.json React 18 + Vite + TypeScript
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;web/tsconfig.{json,app.json,node.json} Vite 표준 3-파일 분할
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;web/vite.config.ts /api 프록시 → :7878 (Express 포트와 매칭)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;web/tailwind.config.ts Creative Warmth 토큰
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;web/vitest.config.ts jsdom + RTL 셋업
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Tailwind 설정(&lt;code&gt;17aecd9&lt;/code&gt;)은 짚을 가치가 있다 — 4월 목업 CSS의 모든 Creative Warmth 토큰이 손으로 Tailwind 확장으로 번역됐다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/tailwind.config.ts (의역)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&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;theme&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="nx"&gt;extend&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="nx"&gt;colors&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="s1"&gt;&amp;#39;warm-white&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;#FAF7F2&amp;#39;&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="s1"&gt;&amp;#39;warm-paper&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;#F5F1EA&amp;#39;&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="s1"&gt;&amp;#39;text-primary&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;#2A2622&amp;#39;&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="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;fontFamily&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="nx"&gt;display&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#34;DM Serif Display&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;serif&amp;#39;&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;hand&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Caveat&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;cursive&amp;#39;&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="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="p"&gt;};&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;디자인 토큰은 하나도 재유도되지 않았다 — 그대로 끌어올렸다. 목업은 일회용 코드였지만, 디자인 시스템은 하중을 지탱하고 있었다.&lt;/p&gt;
&lt;h3 id="스토어-스캐폴드"&gt;스토어 스캐폴드
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;0b469b8 feat(web): scaffold Zustand store with 5 empty slices&lt;/code&gt;는 어떤 기능 코드보다 먼저 상태 아키텍처를 셋업했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// web/src/store/index.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;UiSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;ProjectsSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;WorkspaceSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;FeedSlice&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;PipelineSlice&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="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()((&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;createUiSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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;createProjectsSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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;createWorkspaceSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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;createFeedSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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;createPipelineSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;set&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;5개 슬라이스 — &lt;code&gt;ui&lt;/code&gt;, &lt;code&gt;projects&lt;/code&gt;, &lt;code&gt;workspace&lt;/code&gt;, &lt;code&gt;feed&lt;/code&gt;, &lt;code&gt;pipeline&lt;/code&gt; — 가 1일차에 &lt;em&gt;빈&lt;/em&gt; 상태로 선언됐고, 모양을 잠그는 테스트(&lt;code&gt;9543922 test(web): assert store init exposes all five slices&lt;/code&gt;)가 따라붙었다. 그래야 후속 커밋들이 슬라이스를 잊지 않고 필드를 추가할 수 있다. 그 결정이 다음 3일 동안 100+ 커밋이 그 슬라이스들에 병렬로 필드를 추가할 때 보상을 줬다.&lt;/p&gt;
&lt;h3 id="t-i18n-컴포넌트"&gt;&lt;code&gt;&amp;lt;T&amp;gt;&lt;/code&gt; i18n 컴포넌트
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;24ff23e feat(web): add &amp;lt;T&amp;gt; i18n component with KO/EN toggle + tests&lt;/code&gt;. 앱의 주 언어는 한국어지만, 영어 지원을 시작부터 띄운다는 건 어떤 컴포넌트가 쓰이기 전에 이중언어 prop 패턴이 확립됐다는 뜻이다.&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="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;T&lt;/span&gt; &lt;span class="na"&gt;ko&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;키 컨셉을 선택하세요&amp;#34;&lt;/span&gt; &lt;span class="na"&gt;en&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;Select a key concept&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;작은 선택 — 두 props vs i18n 키 조회 — 이지만 소스 오브 트루스를 소비자와 같이 두는 효과가 있다. 단일 개발자 크리에이티브 도구에서는 어떤 프레임워크 레벨 i18n 추상화보다 나았다.&lt;/p&gt;
&lt;h3 id="디자인-프리미티브"&gt;디자인 프리미티브
&lt;/h3&gt;&lt;p&gt;뒤에 오는 모든 것을 정착시킬 세 프리미티브가 1일차에 출시됐다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;bf3ebaa feat(web): add cn() helper + minimal Button primitive&lt;/code&gt; — 조건부 classname을 위한 &lt;code&gt;cn()&lt;/code&gt; 유틸 + 이미 Creative Warmth를 입은 Button.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;a0cc67e feat(web): add Radix-backed Dialog primitive with warm scrim&lt;/code&gt; — 접근성을 위한 Radix, 디자인 언어에 맞춘 따뜻한 scrim(&lt;code&gt;rgba(42, 38, 34, 0.4)&lt;/code&gt; — &lt;code&gt;text-primary&lt;/code&gt;의 반투명, 순흑 아님).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;c2ee60f feat(web): add Input primitive with Creative Warmth styling&lt;/code&gt; — soft-edged warm-paper 배경의 입력 필드.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="공유-이벤트-스키마"&gt;공유 이벤트 스키마
&lt;/h3&gt;&lt;p&gt;커밋 &lt;code&gt;0c37093 feat(shared): add SSE event Zod schemas + type re-exports&lt;/code&gt; — SSE 이벤트 타입을 &lt;code&gt;shared/events/index.mjs&lt;/code&gt;로 옮겨서 백엔드(Express)와 프론트엔드(React)가 같은 Zod 스키마를 import할 수 있게 했다. 양쪽 모두 타입 안전 SSE, 파서 레이어에서 검증.&lt;/p&gt;
&lt;h3 id="pr1-전주곡"&gt;PR1 전주곡
&lt;/h3&gt;&lt;p&gt;하루는 PR1 계획 문서(&lt;code&gt;558e7ec docs: add PR1 (Launcher) implementation plan&lt;/code&gt;)와 첫 launcher 빌딩 블록 — &lt;code&gt;Project&lt;/code&gt; 타입, 필터/정렬/진행 헬퍼(&lt;code&gt;752095f&lt;/code&gt;) — 로 끝났다. Launcher 페이지 자체 — 사용자가 처음 보는 화면 — 의 구현은 내일이었다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="커밋-로그-선별-총-27개"&gt;커밋 로그 (선별, 총 27개)
&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;비트버킷 마이그레이션&lt;/td&gt;
 &lt;td&gt;snapshot project state&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;런타임 강화&lt;/td&gt;
 &lt;td&gt;structured JSON logger; AbortSignal 셧다운; event-log retention; /api/health + /api/metrics; boot에 셋 다 꿰기&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;배포 스택&lt;/td&gt;
 &lt;td&gt;single-EC2 systemd/nginx/litestream; Node ≥ 22.5 engines 필드; SQLite를 data/로; @google/genai 통합&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;리팩토링&lt;/td&gt;
 &lt;td&gt;queue/jobs를 runs/jobs/events로 분할; stage state machine 추출; agents snake_case 리네임; 4월 plan 아카이브&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;문서&lt;/td&gt;
 &lt;td&gt;Grafana Cloud 셋업; web/ 재구축 디자인 스펙; PR0 구현 계획; PR1 launcher 계획&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;web/ 부트스트랩&lt;/td&gt;
 &lt;td&gt;package.json + Vite + tsconfig; Creative Warmth 토큰의 Tailwind; Vitest + jsdom + RTL; Zustand 5-슬라이스 스토어; T i18n 컴포넌트; cn() + Button + Dialog + Input 프리미티브; shared/의 SSE Zod 스키마; React Router 셸&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;하루 27커밋이 커밋 메시지에서는 무관해 보일 세 가지를 했지만 결국 한 결정이었다 — &lt;strong&gt;production 타깃에 commit하라.&lt;/strong&gt; 런타임은 배포에서 살아남는 데 필요한 관측성과 셧다운 시맨틱을 받았다. 배포 스택 자체가 위키 페이지가 아닌 코드로 그려졌다. 그리고 React 재작성은 production 버전이 필요한 같은 디자인 토큰, 타입 검증 SSE, 아키텍처 솔기로 0일차에 시작됐다.&lt;/p&gt;
&lt;p&gt;가르치는 건 이 날 &lt;em&gt;일어나지 않은&lt;/em&gt; 일들이다. 기능 코드 없음. 새 에이전트 없음. 프롬프트 튜닝 없음. 원칙은 — 바닥을 먼저 강화하고, 강화된 바닥 위에 기능을 다음에 짓는다. 다음 날 134커밋 메가푸시는 이게 없었으면 안전하지 않았을 것이다 — god-module 큐, &lt;code&gt;console.log&lt;/code&gt; 코드베이스, 깔끔하게 셧다운하지 않는 워커 위에서 하루에 PR1 → PR4를 돌리는 건 진짜 버그를 운영 노이즈 아래에 묻었을 것이다.&lt;/p&gt;
&lt;p&gt;다음 — 하루 134커밋. PR1 Launcher, PR2 Workspace 셸, PR3 SSE + 채팅 흐름, PR4 ApproveGate, 그리고 이 모든 것 아래의 데이터베이스 레이어.&lt;/p&gt;</description></item><item><title>Creative Agent Studio 개발일지 #1 — 4월 목업 시절, 그리고 Creative Warmth 테마를 입은 이유</title><link>https://ice-ice-bear.github.io/ko/posts/2026-04-07-creative-agent-studio-dev1/</link><pubDate>Tue, 07 Apr 2026 00:00:00 +0900</pubDate><guid>https://ice-ice-bear.github.io/ko/posts/2026-04-07-creative-agent-studio-dev1/</guid><description>&lt;img src="https://ice-ice-bear.github.io/" alt="Featured image of post Creative Agent Studio 개발일지 #1 — 4월 목업 시절, 그리고 Creative Warmth 테마를 입은 이유" /&gt;&lt;h2 id="개요"&gt;개요
&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Creative Agent Studio&lt;/strong&gt;의 첫 개발일지다 — 한국어 기반 광고 크리에이티브 멀티 에이전트 시스템. 사용자의 단일 메시지가 채팅 우선 파이프라인에 진입해 네 가지 프레젠테이션 단계 — &lt;strong&gt;리서치 → 카피 → 시나리오 → 콘티&lt;/strong&gt; — 를 거치며, 단계 사이에 명시적 사람 승인 게이트를 둔다.&lt;/p&gt;
&lt;p&gt;4월은 목업의 시대였다. 이틀 동안 12개의 커밋이 캔버스의 정적 HTML/JS 프로토타입, 런타임 흐름 스펙, 결국 React 재작성에서도 살아남는 &amp;ldquo;Creative Warmth&amp;rdquo; 디자인 테마, 그리고 Vercel에 올릴 배포 모양을 만들어냈다. 나중에 버려지는 코드는 핵심이 아니었다 — 핵심은 &lt;code&gt;interaction-model.md&lt;/code&gt;에 적어둔 결정들과 목업에 박힌 디자인 토큰이었다.&lt;/p&gt;
&lt;pre class="mermaid" style="visibility:hidden"&gt;graph TD
 Idea["사용자 브리프 (채팅)"] --&gt; Research["리서치 단계 &amp;lt;br/&amp;gt; 리서치 에이전트"]
 Research --&gt; Gate1["GATE 1 &amp;lt;br/&amp;gt; 키 컨셉 선택"]
 Gate1 --&gt; Copy["카피 단계 &amp;lt;br/&amp;gt; 4개 드래프트 워커"]
 Copy --&gt; Gate2["GATE 2 &amp;lt;br/&amp;gt; 카피 승인"]
 Gate2 --&gt; Scenario["시나리오 단계 &amp;lt;br/&amp;gt; 3 스페셜리스트"]
 Scenario --&gt; Gate3["GATE 3-4 &amp;lt;br/&amp;gt; 시나리오 승인"]
 Gate3 --&gt; Storyboard["콘티 단계 &amp;lt;br/&amp;gt; 이미지 생성"]
 Storyboard --&gt; Gate5["GATE 5 &amp;lt;br/&amp;gt; 최종 승인"]&lt;/pre&gt;&lt;p&gt;12개 커밋, 한 가지 관통하는 주제 — &lt;strong&gt;구현이 녹슬기 전에 원칙을 못 박는다.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="목업보다-오래-살아남은-세-가지-결정"&gt;목업보다 오래 살아남은 세 가지 결정
&lt;/h2&gt;&lt;p&gt;4월에 추가된 가장 중요한 파일은 코드가 아니라 &lt;code&gt;interaction-model.md&lt;/code&gt;였다 — 세 가지 제품 결정을 단일 결정문서로 결정화한 문서:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;결정 1. 인터랙션 모델 — 채팅 우선.&lt;/strong&gt; 사용자의 주 입력 수단은 자유형 채팅이다. 단계를 직접 클릭하거나 버튼으로 다음으로 넘어가는 방식은 사용하지 않는다. 오케스트레이터가 사용자의 발화를 해석해 적절한 에이전트를 호출한다.&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;결정 2. 에이전트 투명성 — 한 줄 상태 표시.&lt;/strong&gt; 에이전트 실행 중에는 피드에 한 줄짜리 상태 메시지만 표시한다. 전체 에이전트 대시보드나 실시간 로그는 노출하지 않는다.&lt;/p&gt;

 &lt;/blockquote&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;결정 3. 게이트 기반 자동 실행.&lt;/strong&gt; 각 단계가 완료되면 파이프라인은 멈추고, 다음 단계를 자동으로 실행하기 전에 명시적 사람 승인을 기다린다.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;뒤이은 모든 UI 커밋은 — 4월 내내, 그리고 5월 재작성을 가로질러 — 이 세 규칙에 비춰 검증됐다. 목업의 첫 커밋(&lt;code&gt;b67eb98 Add Diffs creative agent studio mockup&lt;/code&gt;)부터 이미 DOM 구조로 인코딩돼 있었다 — &amp;ldquo;다음 단계&amp;rdquo; 버튼 없음, 에이전트는 대시보드가 아니라 글머리 기호 목록으로, 컴포저가 유일한 진입점.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="creative-warmth--디자인-테마"&gt;&amp;ldquo;Creative Warmth&amp;rdquo; — 디자인 테마
&lt;/h2&gt;&lt;p&gt;세 번째 커밋(&lt;code&gt;72e0fdc&lt;/code&gt;)이 모든 걸 견뎌낸 비주얼 베이스라인이었다 — &lt;strong&gt;&amp;ldquo;Redesign: Apply Creative Warmth theme (warm white, DM Serif Display, Caveat).&amp;rdquo;&lt;/strong&gt;&lt;/p&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;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;Warm white (#FAF7F2)&lt;/td&gt;
 &lt;td&gt;순백은 소프트웨어처럼 읽힌다. 따뜻한 톤은 작업실처럼 읽힌다.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;본문 서체&lt;/td&gt;
 &lt;td&gt;DM Serif Display&lt;/td&gt;
 &lt;td&gt;크리에이티브 도구는 생산성 코드가 아니라 문학적인 느낌이어야 한다.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;손글씨 라벨&lt;/td&gt;
 &lt;td&gt;Caveat&lt;/td&gt;
 &lt;td&gt;스페셜리스트 에이전트 이름에 손글씨 배지를 — 밀도 높은 작업면에 인간미를 한 줄 입히는 작은 장치.&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;규칙은 부정형이었다 — &lt;strong&gt;순흑도, 순백도, 어디에도 쓰지 않는다.&lt;/strong&gt; 후속 커밋(&lt;code&gt;640c755&lt;/code&gt;)이 로고 가시성을 고쳐야 했는데 — 원본 SVG가 어두운 색이라 따뜻한 배경 위에서 보이지 않았다 — 수정 방법은 에셋을 재출력하는 대신 &lt;code&gt;filter: brightness(0)&lt;/code&gt; CSS 핵으로 반전시키는 것이었다. 실용적이긴 했지만, 기저 제약은 그대로였다 — 디자인 시스템이 시스템과 싸우는 에셋을 위해 굽혀지지는 않는다.&lt;/p&gt;
&lt;p&gt;이 토큰들은 6주 뒤 React 측 첫 feat(web) 커밋에서 &lt;code&gt;web/tailwind.config.ts&lt;/code&gt;로 그대로 이주한다 — 디자인 결정이 옳은 하중 지지 레이어였다는 증거.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="목업을-정직하게-만든-세-가지-ui-정리"&gt;목업을 정직하게 만든 세 가지 UI 정리
&lt;/h2&gt;&lt;p&gt;연속된 세 커밋(&lt;code&gt;3c225c9&lt;/code&gt;, &lt;code&gt;1be5253&lt;/code&gt;, &lt;code&gt;d37b4c9&lt;/code&gt;)이 세 결정을 위반하던 것들을 공격했다.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;lt;p class=&amp;#34;eyebrow&amp;#34;&amp;gt;Project Management&amp;lt;/p&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;lt;ul class=&amp;#34;agents&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;lt;li&amp;gt;&amp;lt;span class=&amp;#34;dot dot-1&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;Agent 1&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;lt;li&amp;gt;&amp;lt;span class=&amp;#34;dot dot-2&amp;#34;&amp;gt;&amp;lt;/span&amp;gt;Agent 2&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- &amp;lt;/ul&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;lt;ul class=&amp;#34;agents&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;lt;li&amp;gt;리서치 에이전트&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;lt;li&amp;gt;Copy Draft Workers&amp;lt;/li&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ &amp;lt;/ul&amp;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;3c225c9&lt;/code&gt; — 전체를 PM 도구로 프레임하던 &amp;ldquo;Project Management&amp;rdquo; eyebrow 제거. 색상 코드 칩 대신 글머리 기호로 에이전트 이름을 표시 — 칩은 대시보드를 암시했기 때문.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;1be5253&lt;/code&gt; — 상태 레일에서 &amp;ldquo;Background Agents&amp;rdquo; 섹션을 완전히 제거. 결정 2(한 줄 상태, 전체 대시보드 없음)를 위반했다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;d37b4c9&lt;/code&gt; — &lt;strong&gt;컴포저 개편.&lt;/strong&gt; 모델 셀렉터를 입력 바 안으로 옮기고, 파일 첨부 아이콘을 추가하고, 모델 목록을 업데이트했다. 왜 중요한가 — 컴포저는 결정 1의 전체 표면적이다. &amp;ldquo;더 도전적으로&amp;rdquo; 또는 &amp;ldquo;이 두 개 합쳐줘&amp;quot;를 타이핑할 적절한 장소처럼 느껴지지 않는다면, 채팅 우선 원칙은 사고로 실패한다.&lt;/p&gt;
&lt;p&gt;이것들은 기능이 아니었다 — 스펙을 강제하는 &lt;em&gt;삭제&lt;/em&gt;였다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="런타임-흐름-다듬기--목업-아래-뼈대"&gt;런타임 흐름 다듬기 — 목업 아래 뼈대
&lt;/h2&gt;&lt;p&gt;2026-04-07의 세 문서와 한 feat 커밋이 런타임 토대를 깔았다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ecac2e5 docs: add production poc state and routing spec&lt;/code&gt; — 런타임이 네 단계에 걸쳐 어떤 상태를 들고 다닐지 정의.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;7e26c12 docs: add runtime state cleanup spec&lt;/code&gt; — 실행 간 상태가 어떻게 정리될지 정의 (나중에 React 앱이 세션별 상태를 격리해야 할 때 의미를 가짐).&lt;/li&gt;
&lt;li&gt;&lt;code&gt;97df876 feat: refine workspace runtime flow&lt;/code&gt; — 실제 흐름 코드를 다듬음.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;21f0ebc chore: add deployment and design reference files&lt;/code&gt; — 배포 모양과 디자인 참조 폴더를 커밋.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ldquo;배포 참조&amp;quot;는 목업 디렉토리를 가리키는 Vercel buildCommand였다. 그 배포 타깃은 재작성에서도 살아남았다 — 5월에 React 프론트엔드가 출시됐을 때 &lt;code&gt;vercel.json&lt;/code&gt;의 유일한 변경은 &lt;code&gt;outputDirectory: &amp;quot;web/dist&amp;quot;&lt;/code&gt;였다.&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;2026-04-06&lt;/td&gt;
 &lt;td&gt;Add Diffs creative agent studio mockup&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;Fix logo path for Vercel deployment&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;Redesign: Apply Creative Warmth theme (warm white, DM Serif Display, Caveat)&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;Fix logo visibility: apply brightness(0) filter for dark logo on light bg&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;UI: Remove &amp;lsquo;Project Management&amp;rsquo; eyebrow, show agent names as bullets, add LLM dropdown&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;Remove Background Agents section from status rail&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-06&lt;/td&gt;
 &lt;td&gt;Composer: move model selector into input bar, add file attach icon, update models&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-07&lt;/td&gt;
 &lt;td&gt;docs: add production poc state and routing spec&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-07&lt;/td&gt;
 &lt;td&gt;docs: add runtime state cleanup spec&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-07&lt;/td&gt;
 &lt;td&gt;feat: refine workspace runtime flow&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-07&lt;/td&gt;
 &lt;td&gt;chore: ignore local workspace artifacts&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2026-04-07&lt;/td&gt;
 &lt;td&gt;chore: add deployment and design reference files&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;code&gt;interaction-model.md&lt;/code&gt;, 라우팅 스펙, 런타임 클린업 스펙)을 만들었다. 목업은 6주 뒤 삭제된다 — &lt;code&gt;chore: remove legacy mockup/ vanilla-JS SPA&lt;/code&gt; 커밋이 메가푸시에 떨어진다 — 하지만 글의 모든 단락은 여전히 하중을 지탱한다.&lt;/p&gt;
&lt;p&gt;새겨둘 만한 교훈 — &lt;strong&gt;제품 원칙은 구현보다 오래 산다.&lt;/strong&gt; &amp;ldquo;채팅 우선, 한 줄 상태, 게이트 기반 자동 실행&amp;quot;을 — 기능으로도, UI 요구사항으로도 아니라 — &lt;code&gt;결정&lt;/code&gt; 헤딩과 &lt;code&gt;이유&lt;/code&gt; 헤딩이 붙은 결정으로 적어두니, 뒤이은 모든 UI 커밋이 통과해야 할 재판소가 생겼다. 5월에 React 재작성이 시작됐을 때, 그 세 결정은 새 &lt;code&gt;web/&lt;/code&gt; 트리의 원칙으로 그대로 복사됐다. 목업은 일회용이었다 — 원칙은 아니었다.&lt;/p&gt;
&lt;p&gt;다음 — 비트버킷 마이그레이션, 런타임 production-readying, 그리고 한 주말 동안 목업 전체를 대체하게 될 React+Vite+TypeScript 재작성의 시작.&lt;/p&gt;</description></item></channel></rss>