Skip to content

feat(editorial): Stage 3 chat + assets 격리 + publish snapshot + thumbnail + commercial#446

Merged
cocoyoon merged 14 commits into
devfrom
feature/editorial-magazine-chat
May 6, 2026
Merged

feat(editorial): Stage 3 chat + assets 격리 + publish snapshot + thumbnail + commercial#446
cocoyoon merged 14 commits into
devfrom
feature/editorial-magazine-chat

Conversation

@cocoyoon
Copy link
Copy Markdown
Member

@cocoyoon cocoyoon commented May 4, 2026

이슈 #429 의 Stage 3 + 운영 model 정리 + 산출물 품질 묶음 PR.

작업 묶음

1. Stage 3 — 매거진 대화형 편집 agent

  • DB: editorial_article_chat_sessions / editorial_article_chat_messages 테이블
  • ai-server: editorial_article_chat/ 모듈 — google-genai function calling agent
  • web: ChatPanel.tsx + useEditorialChat.ts (TanStack Query)
  • drafts/[id] 페이지 우측 sidebar 통합. 매니저가 자연어로 섹션 수정 → layout_json 즉시 갱신.

2. Assets 격리 + Operation publish snapshot (architectural)

  • staging 테이블 (recommendations / draft articles / events / chat / discovery_settings) 모두 assets DB 로 이동 — operation 위생 보호 (검증 안 된 자산 격리). raw_posts 패턴 정합성.
  • operation 에 신규 editorial_articles (publish 전용 snapshot, status 컬럼 없음)
  • Publish 클릭 시 assets → operation snapshot 복사 + assets status='published' UPDATE.
  • Public read endpoints: GET /api/v1/editorial-articles?page / /{id_or_slug} (operation, RLS 공개 SELECT)

3. UX 개선

  • 즉시 가시성: Approve → Rust 트랜잭션 안에서 article INSERT (status='generating') → Drafts 탭 즉시 표시 (이전엔 1분 silence)
  • cycle_seconds → cycle_minutes: UI 직관성 (21600초360분)
  • Hard delete 버튼: recommendations / articles 둘 다 — 테스트 garbage 정리
  • Discovery cycle 최소 30 → 1분: pipeline 과 일치

4. 산출물 품질

  • Hero 제거: 매거진 본문에 banner image 없음 — title/subtitle 헤더 + 섹션만. compose_layout prompt 에서 hero 섹션 만들지 말 것 + MagazineRenderer 에서 hero section 안 그림.
  • gpt-image-2 thumbnail: 4:5 nano-banana → 2:3 gpt-image-2 로 전환. 한글 title 정확 렌더 + DECODED MAG 워터마크 (lime #eafd67 — --mag-accent) 가 같은 호출에서 처리.

5. Commercial features

  • 상품 링크: solutions.original_url / affiliate_url 을 layout 에 노출 → SolutionCard 의 검은 Shop 버튼 (ShoppingBag icon) → 새 탭으로 상품 페이지.
  • Try-on 통합: 기존 vton 인프라 (VtonModal / vtonStore / useVtonTryOn) 재사용. SolutionCard 에 Shirt icon 버튼 — 클릭 시 VtonModal 자동 열림.
  • 단일-post 매거진에도 적용: MagazineItemsSection.tsx 의 Compact + Full view 두 곳 모두 try-on 버튼 추가. 기존 1 post → 1 magazine 매거진들도 try-on 가능.

DB Migrations

  • supabase-assets/migrations/20260505020000_editorial_staging.sql (5 staging tables + RLS)
  • supabase/migrations/20260505000000_editorial_article_chat.sql (chat sessions/messages — assets 이동 전 작성, 이후 operation drop 마이그에 포함)
  • supabase/migrations/20260505010000_editorial_cycle_minutes.sql (cycle 컬럼 rename)
  • supabase/migrations/20260505020001_editorial_articles_published.sql (operation publish snapshot)
  • supabase/migrations/20260505020002_drop_editorial_staging.sql (operation staging cleanup)

Prod 적용 단계 (PR 머지 후 별도 작업)

  1. cd supabase-assets && supabase db push — assets 신규 5 테이블
  2. supabase db push (operation) — publish snapshot + staging drop. prod staging 테이블 데이터 백업 필수 (별도 dump). 로컬 dev 무관.
  3. vercel env add OPENAI_API_KEY (이미 있으면 skip) — gpt-image-2 thumbnail
  4. vercel env add AI_SERVER_HTTP_URL — chat router 접근 (web → ai-server FastAPI)

Out of scope (후속 이슈로 분리)

  • 매거진 생성 고도화 (tools 확장: solution swap / post add-remove / regenerate full)
  • 매거진 레이아웃 다각화 (sections type 추가, themed templates)
  • affiliate_url ingestion 파이프라인
  • price_amount 정규화 (현재는 metadata.price 만)
  • public web 페이지 /magazine/[slug] (backend endpoint 만 있음)
  • R2 prod / staging 버킷 분리

검증 (로컬 풀 사이클)

  • ✅ Approve → assets 즉시 INSERT → 1분 내 enqueue → graph (~2분) → status='draft', thumbnail 한글+워터마크
  • ✅ Publish → operation snapshot 복사 + assets='published' + 공개 endpoint 200
  • ✅ Chat agent: 'remove_section' / 'update_title' tool 호출 후 layout 즉시 변경
  • ✅ SolutionCard: Shop / Try-on 버튼 둘 다 (좁은 카드 포함)
  • ✅ 단일 post 매거진: try-on 버튼 추가 동일 동작

#429

cocoyoon added 2 commits May 5, 2026 00:17
editorial_article 의 대화형 편집을 위한 2 테이블.

- editorial_article_chat_sessions: 한 article 에 여러 세션 가능
  (article_id FK CASCADE + created_by + last_message_at).
- editorial_article_chat_messages: role (user/assistant/tool/system) +
  content + tool_calls (JSONB) + tool_call_id/name + tool_result (JSONB).
  agent 의 tool 실행 audit 보존.

RLS: admin-only ALL (채팅 내용은 운영 audit). #429
컨텐츠 매니저가 채팅으로 매거진 layout 을 다듬는 agent.
LangGraph 대신 google-genai function calling + 단순 loop.
Vercel AI SDK / Claude 도입 안 함 — 기존 Gemini stack 일관성.

ai-server (editorial_article_chat 모듈):
- tools.py: 7 tools (update_title/subtitle, update_section_body/title,
  remove_section, regenerate_section_body, get_current_layout). 각 tool
  은 in-memory layout 을 mutate, 마지막에 한 번 DB persist.
  regenerate_section_body 는 별도 Gemini 호출로 카피 생성.
- agent.py: run_turn — 기존 messages 로드 → contents 구성 → Gemini Pro
  호출 → function_call 있으면 ToolExecutor → 결과 model 에 다시 → 반복
  (max 6 step). 모든 messages DB 영속화. layout 변경 있으면 트랜잭션.
- repository.py: chat_sessions / chat_messages CRUD + article load.
- api.py: FastAPI router (/api/v1/editorial-article-chat/*) — sessions
  list/create, messages list/send. #416 gRPC-only 컨벤션 예외 (chat 은
  multi-turn / admin UI 호출 패턴이라 HTTP 자연스러움).
- bootstrap.py: chat router include.

web:
- Next.js proxy routes /api/admin/editorial-article-chat/{sessions,messages}
  → ai-server (admin auth + bearer 패턴). AI_SERVER_HTTP_URL env (server-env.ts).
- useEditorialChat hook: useChatSessions / useCreateChatSession /
  useChatMessages / useSendChatMessage. Mutation 성공 시 article query
  invalidate → MagazineRenderer 자동 리렌더.
- ChatPanel.tsx: drafts/[id] 페이지 우측 sidebar 에 통합. 가장 최근
  세션 자동 로드/생성. user/assistant/tool 메시지 differentiated.
  Enter 전송, Shift+Enter 줄바꿈.

검증:
- curl 로 'get_current_layout' tool 호출 → agent 가 4섹션 인식 정확
- 'remove_section' → DB 4→3 섹션, layout_changed=true 확인

#429
@cocoyoon cocoyoon added enhancement New feature or request backend 백엔드/API ai AI/자동화 labels May 4, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
decoded-app Ready Ready Preview, Comment May 6, 2026 1:56am

운영자가 admin UI 에서 '21600초' 같은 값 보면 직관 X. 분 단위가 자연스러움.
editorial_pipeline_settings + editorial_discovery_settings 두 settings 모두
일관성 있게 변경.

DB migration (20260505010000):
- 두 테이블의 cycle_seconds 컬럼 → cycle_minutes (CEIL(/60) 변환)
- pipeline: DEFAULT 1, CHECK >=1 (이전 60초 = 1분)
- discovery: DEFAULT 360, CHECK >=30 (이전 21600초 = 6h, 최소 30분)
- Idempotent: 컬럼이 이미 cycle_minutes 면 skip.

ai-server:
- repository dataclass field rename
- scheduler _interval_elapsed: cycle_minutes * 60 으로 내부 계산
- default fallback values 도 분 단위 (1, 360)

api-server (Rust):
- editorial_pipeline_settings.rs / editorial_discovery_settings.rs 의 JSON
  field + SQL column + validation 모두 cycle_minutes 로 rename
- min validation: pipeline >=1, discovery >=30

web:
- useEditorialPipelineSettings / useDiscoverySettings 의 type rename
- AutoPipelineSettingsPanel / DiscoverySettingsPanel:
  state var (cycleMinutes), label 'Cycle (s)' → 'Cycle (min)',
  min input value 분 단위로 조정

검증: GET /admin/editorial-{pipeline,discovery}/settings → cycle_minutes
1 / 360 정상 응답.

raw_posts 의 cycle_seconds 는 별도 feature 라 이번 PR scope 아님. #429
cocoyoon added 2 commits May 5, 2026 01:06
이전 cycle_seconds >= 1800 (30분) 제약을 그대로 옮기다 보니 cycle_minutes >= 30
이 됐는데, pipeline 의 minimum 은 1분 — 비대칭. 30분 floor 는 임의 보호값
이고 운영자가 직접 정해야 할 값이지 시스템이 강제할 이유 없음.

- migration: editorial_discovery_settings cycle_minutes CHECK >= 1
- Rust: validation if c < 1 (메시지는 이미 'must be >= 1' 였는데 sed 누락)
- Web: DiscoverySettingsPanel min={1}, setCycleMinutes Math.max(1, ...)

검증: PATCH cycle_minutes=1 → 204. #429
테스트 garbage / 잘못 만든 row 정리 위해 hard DELETE 추가. 기존 soft archive
(status='archived') 와는 별개 — UI 에서 X 아이콘 옆 휴지통 아이콘.

api-server (Rust):
- DELETE /api/v1/admin/editorial-recommendations/{id} — 직접 row 삭제
  (article 의 recommendation_id 는 FK SET NULL, article 자체는 보존)
- DELETE /api/v1/admin/editorial-articles/{id} — 직접 row 삭제
  (events / chat_sessions 는 FK CASCADE 같이 삭제, recommendation 의
  article_id 는 SET NULL)

web:
- Next.js proxy DELETE handlers (admin auth + bearer)
- useDeleteRecommendation / useDeleteArticle (TanStack mutation, list invalidate)
- RecommendationTable / ArticleTable: 휴지통 아이콘 + 확인 dialog (browser
  confirm — 단순)

검증: DELETE non-existent id → 404 'not found' 정상 (routing OK). #429
이전: Approve 클릭 → Rust PATCH (recommendation status=approved 만) →
ArticlePickupScheduler 가 1분 polling 으로 article INSERT + ARQ enqueue →
사용자가 1분 내내 silence 봄.

현재: Rust PATCH 가 같은 트랜잭션 안에서:
  1. recommendation status='approved' + approved_by/_at
  2. editorial_articles INSERT (status='generating', source_post_ids 복사,
     title 임시 = angle_title)
  3. recommendation.article_id 연결
  4. event INSERT ('created' / 'admin approved')
→ 클릭 즉시 Drafts 탭에 'generating' 표시.

ArticlePickupScheduler 단순화 — enqueue 만 담당:
  editorial_articles.status='generating' AND no event note='enqueued' →
  ARQ enqueue → event INSERT note='enqueued'. (이전 INSERT 로직 제거.)

검증: PATCH approve → 204 → DB 즉시 article 'generating' 확인 → 22초 후
scheduler 가 enqueued event 추가. #429
cocoyoon added 2 commits May 5, 2026 13:52
매거진 staging (검증 안 된 자산) 을 operation 에서 assets 로 이동. publish
시점에만 operation 의 신규 editorial_articles 테이블로 snapshot 복사. raw_posts
패턴 정합성. #429

supabase-assets/migrations/20260505020000_editorial_staging.sql:
- editorial_recommendations / editorial_articles (status draft 등 + thumbnail_url)
  / editorial_article_events / editorial_discovery_settings (cycle_minutes
  default 360, min 1) / editorial_article_chat_sessions / chat_messages
- assets RLS pattern (enable + no policies, service_role bypass — raw_posts 동일)
- 모든 FK 는 assets 안에서만 (cross-DB FK X). source_post_ids 는 logical uuid[].

supabase/migrations/20260505020001_editorial_articles_published.sql:
- operation editorial_articles (publish 전용 snapshot). status 컬럼 없음.
- id = assets article id 그대로. source_article_id (audit, FK 없음).
- title / subtitle / hero_image_url / thumbnail_url / layout_json /
  source_post_ids / slug (UNIQUE, NULL 가능) / published_at / published_by.
- RLS: 공개 SELECT (운영 매거진 reader), admin-only mutation (is_admin).

supabase/migrations/20260505020002_drop_editorial_staging.sql:
- operation 의 staging 5 테이블 DROP CASCADE.
- 기존 staging editorial_articles 잔재 처리 — 'status' 컬럼 있고
  'source_article_id' 없으면 staging 잔재로 판단해 DROP + 신 publish 스키마 재생성.
- 마이그레이션 순서: 20260505020001 (publish 생성) → 20260505020002 (staging drop).

로컬: supabase-assets/ 의 마이그레이션은 dev 에서 별도 'assets' DB (
postgres:5432/assets) 에 적용. .env.backend.dev 의 ASSETS_DATABASE_URL 을
localhost 로 override (gitignored).
#429 architectural refactor + 산출물 품질 개선.

═══ assets 격리 ═══
매거진 staging 데이터 (recommendations / draft articles / events / chat /
discovery_settings) 를 operation 에서 assets DB 로 이동. operation 위생
보호 — 검증 안 된 자산이 prod 데이터와 섞이지 않게.

ai-server:
- _container.py: EditorialDiscoveryContainer 가 assets+operation 두 풀 받음.
  ArticlePickupScheduler 는 assets 만. nano_banana_client 를 RawPostsContainer
  → InfrastructureContainer 로 승격 (raw_posts + editorial 공용).
- editorial_discovery/repository.py: settings/recommendations 는 assets, posts/
  spots/solutions 는 operation. used_posts CTE 분리 — 두 풀 분리 호출.
- editorial_article/nodes/fetch_sources.py: recommendations 는 assets, posts
  는 operation 분리 호출.
- editorial_article/nodes/publish.py: assets 의 editorial_articles UPDATE +
  recommendation status='drafted' + event INSERT.
- editorial_article_service.py: ctx 에서 assets+operation 두 풀 + nano_banana
  + r2 받아 graph config 주입.
- bootstrap.py: chat router 에 assets pool 주입.
- worker.py: ctx 에 nano_banana_client + r2_client 추가 (#429 공용).

api-server:
- 모든 admin editorial handler (recommendations / articles / discovery_settings)
  의 state.db → state.assets_db.
- approve_in_tx: assets 트랜잭션으로 동작 (이미 변경됨).

═══ Publish 복사 (assets → operation snapshot) ═══

api-server:
- editorial_articles.rs PATCH 의 'published' 분기 → publish_to_operation():
  1. assets 에서 article 로드
  2. operation 의 신규 editorial_articles 테이블에 INSERT (id 동일,
     source_article_id = id, layout_json + hero/thumb URL + source_post_ids
     + slug NULL 가능 + published_at + published_by). ON CONFLICT (id) DO UPDATE.
  3. assets 의 status='published' UPDATE + event INSERT.
  두 DB 라 분산 트랜잭션 X — best-effort 순서.
- editorial_articles_published/{handlers,mod}.rs: 신규 공개 read endpoints.
  GET /api/v1/editorial-articles?page&per_page (목록), /{id_or_slug} (상세).
  state.db (operation) 사용. RLS public_can_select_editorial_articles 에 의존.
- domains/mod.rs + router.rs: 신규 도메인 등록.

═══ Nano-banana hero (16:9) + thumbnail (4:5) ═══

ai-server:
- editorial_article/nodes/generate_hero_thumbnail.py (신규):
  source 이미지 1장 다운로드 → nano-banana reframe 16:9 + 4:5 병렬 호출
  → R2 업로드 (editorial-magazines/{article_id}/{hero,thumbnail}.png) →
  layout.{hero_image_url, thumbnail_url} 갱신. graceful — 실패해도 publish 진행.
- editorial_article/graph.py: compose_layout → generate_hero_thumbnail → publish.
- editorial_article/models.py: MagazineLayout.thumbnail_url 추가.
- nodes/publish.py SQL: thumbnail_url 컬럼도 함께 UPDATE.

web:
- useEditorialArticles.ts: ArticleListItem.thumbnail_url + MagazineLayout.thumbnail_url 타입.
- ArticleTable.tsx: 4:5 썸네일 컬럼 추가 (thumbnail_url 우선, fallback hero_image_url, placeholder).

═══ 검증 (로컬 풀 사이클) ═══

1. assets 마이그레이션 적용 (별도 supabase-assets/migrations/20260505020000)
2. operation publish snapshot + staging drop 마이그레이션
3. .env.backend.dev / packages/api-server/.env.dev 에서 ASSETS_DATABASE_URL
   을 localhost:54322/assets 로 override (gitignored, 로컬 dev 만)
4. Sample recommendation INSERT (assets) → Approve via Rust PATCH → 즉시
   assets editorial_articles INSERT (status='generating')
5. ArticlePickupScheduler 1분 내 ARQ enqueue → 그래프 실행
   (fetch_sources from assets+operation → compose → generate_hero_thumbnail
    → publish to assets) — 약 2분 후 status='draft', hero+thumbnail R2 업로드 확인
6. PATCH publish → operation editorial_articles INSERT, assets status='published'
7. GET /api/v1/editorial-articles → 200, 운영 매거진 반환

#429
이전 perl 단일 라인 sed 가 'state\n.db' (멀티라인) 패턴 놓침 — query_one /
query_all / execute 의 chained call 4-5곳 미변환. operation pool 가리키니
'relation editorial_recommendations does not exist' 500.

추가: ArticleListItem / ArticleDetailResponse 에 thumbnail_url 필드 +
list/detail SQL 에 컬럼 포함 — web 의 ArticleTable 썸네일 표시용.

검증: GET /api/v1/admin/editorial-recommendations 200, GET .../editorial-articles
200 (thumbnail_url 응답 확인). #429
매거진 viewer 에서 hero image 안 보여줌 (사용자 결정 — 운영자가 매거진 본문에
hero banner 불필요라 판단). thumbnail 만 list / OG 카드용으로 생성.

이전 generate_hero_thumbnail (16:9 + 4:5 두 reframe 호출) → generate_thumbnail
로 단순화. 4:5 한 번만 호출.

prompt 강화 — 단순 reframe / re-crop 결과 ('input 그대로') 가 아니라 적극적
editorial transformation 을 명시:
  - 'INSPIRATION, not literal source' — input 을 영감으로 사용, 재해석
  - composition / lighting / color grading 디렉티브 명시
  - 'should look DIFFERENT from the source — magazine art director's interpretation'
  - 'add visual sophistication: grain, color shifts, contrast, lighting drama'
  - 'avoid generic stock-photo / social-media filter look'

frontend (MagazineRenderer): header 의 hero block 제거. title + subtitle 만.

DB schema 의 hero_image_url 컬럼은 유지 (compose_layout 이 source URL 로 채울
수 있고, OG meta 등 다른 용도 가능). 안 쓰는 필드 schema migration 제거는 향후
별도 PR.

#429
매거진 thumbnail 생성을 nano-banana → gpt-image-2 로 전환. nano-banana 의
한글 text rendering 약해서 typo 잦음 — gpt-image-2 가 한글 + DECODED MAG
워터마크 모두 정확하게 렌더링.

ai-server (신규):
- managers/llm/adapters/openai_image.py: OpenAIImageClient (edit / generate),
  gpt-image-2 default, timeout 300s.
  crop_to_4_5 helper 도 같이 (현재는 미사용 — 2:3 portrait 직접 사용).
- _container.py: openai_image_client Singleton (InfrastructureContainer).
- worker.py: ctx 에 openai_image_client 추가.
- editorial_article_service.py: graph config 에 openai_image_client 주입.

generate_thumbnail.py 변경:
- nano-banana → openai_image_client 사용
- 1024x1536 (2:3 portrait) 직접 — 4:5 center crop 제거 (crop 이 watermark
  + title 양쪽 잘라냈음)
- prompt:
  - 한글 title 을 bottom-left 에 magazine 스타일로 overlay
  - 'DECODED MAG' wordmark top-right 에 lime green (#eafd67 — brand --mag-accent)
  - editorial color grading
  - 053 magazine / W Korea / Vogue Korea Instagram covers reference

prompts.py (compose_layout): hero 섹션 만들지 말 것 + hero_image_url null
유지 명시 (이미 적용됐으나 schema/order 정리).

MagazineRenderer.tsx: header 의 hero block 제거 + SectionView 의 case 'hero'
도 null (기존 article 의 hero section 안 보여줌).

비용: gpt-image-2 ~$0.04/image (vs nano-banana ~$0.01-0.04). 한글 quality
trade-off. #429
cocoyoon added 2 commits May 6, 2026 10:38
매거진의 각 solution 카드에 두 가지 commercial CTA 추가:

1. 상품 링크 (Shop)
   - solutions.original_url (현재 65% 채워짐) / affiliate_url (향후) 을 layout 에 포함
   - ai-server: SourceSolution + SectionSolution pydantic 에 original_url /
     affiliate_url 필드 추가
   - fetch_sources_node: SQL 에 original_url, affiliate_url SELECT 추가
   - prompts.py: solutions 페이로드 에 두 url 포함 + LLM 에게 그대로 복사하도록 명시
     (변형 X). 출력 schema 에도 추가.
   - web: MagazineSolution 타입 + SolutionCard 에 검은 Shop 버튼 (ShoppingBag
     icon) — 새 탭으로 상품 페이지 열기. affiliate_url 우선, 없으면 original_url.

2. Try-on 버튼
   - 기존 vton 인프라 (VtonModal / vtonStore / useVtonTryOn) 재사용
   - vtonStore.openWithItems(post_id, items) 호출 — VtonModal 이 글로벌 mount
     돼있어 즉시 표시. API 가 thumbnail_url 만 사용해서 solution.image_url
     그대로 전달.
   - SolutionCard 에 Shirt icon 버튼 추가 (image_url 있을 때만 노출)

검증 (article a39116b8-...):
- layout_json sections[].solutions[].original_url 정상 채움 (Celine, Carousell 등)
- 풀 사이클 (approve → graph → publish snapshot) 정상

#429
이전: flex + flex-1 Shop → try-on 이 카드 너비 초과 시 잘림.
변경: grid grid-cols-[1fr_auto] — Shop 가변 (남은 공간 + truncate),
try-on icon shrink-0 으로 항상 자기 너비. 카드 너비 무관 둘 다 보임. #429
기존 1 post → 1 magazine (post_magazines) 의 MagazineItemsSection 에도
multi-source 매거진과 동일하게 Try-on 버튼 추가:

- Compact view: 'Shop' 텍스트 링크 옆에 'Try-on' inline 버튼 (셔츠 icon)
- Full view: 'Shop Now' rounded 버튼 옆에 같은 스타일 'Try-on' rounded 버튼

vtonStore.openWithItems(spot_id, [{id, title, thumbnail_url, ...}]) 호출 패턴
재사용. image_url 있을 때만 노출. accentColor 적용 일관. #429
@cocoyoon cocoyoon added the bump:minor Backward-compatible additions label May 6, 2026
@cocoyoon cocoyoon changed the title feat(editorial): Stage 3 — 매거진 article 대화형 편집 (chat agent) feat(editorial): Stage 3 chat + assets 격리 + publish snapshot + thumbnail + commercial May 6, 2026
@cocoyoon cocoyoon merged commit aed5449 into dev May 6, 2026
6 of 7 checks passed
@cocoyoon cocoyoon deleted the feature/editorial-magazine-chat branch May 6, 2026 02:32
@github-project-automation github-project-automation Bot moved this from Todo to Done in decoded-monorepo May 6, 2026
cocoyoon added a commit that referenced this pull request May 6, 2026
…#450)

* fix(db): editorial_articles publish migration drop staging first

#446 의 020001 (publish editorial_articles 생성) 이 020002 (drop staging) 보다
먼저 돌면서 staging 의 editorial_articles 가 살아있는 채로 publish 의 slug 컬럼
인덱스 만들려다 SQLSTATE 42703 (column does not exist) 로 실패. operation cloud
배포 시 발견.

DROP TABLE IF EXISTS public.editorial_articles CASCADE 를 020001 시작 부분에
추가해서 idempotent 하게. 020002 의 동등 로직과 안전 중복.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(editorial-chat): web→api-server only, ai-server gRPC compute (#446 fixup)

PR #446 의 Stage 3 chat 이 web → ai-server (HTTP) 직접 호출로 구현돼서 모노레포
컨벤션 위반. ai-server 는 internal compute, web 은 항상 api-server 만 호출.
Prod 배포 전에 같은 스코프 (#446) 안에서 fixup.

책임 분리:
  - api-server: chat session/message CRUD (assets DB), layout persist,
    admin 인증 게이팅 — 새 도메인 `domains/admin/editorial_article_chat`.
  - ai-server: 한 턴 LLM 실행 (Gemini + tool loop) — pure compute,
    gRPC `inbound.Queue/RunChatTurn`. DB 안 만짐.
  - web: 기존 admin proxy 패턴 (`API_BASE_URL` + Bearer token) 으로 통일.

ai-server 정리:
  - editorial_article_chat/api.py 삭제 (HTTP router)
  - editorial_article_chat/repository.py 삭제 (DB layer)
  - agent.run_turn 시그니처 변경: (article_title, layout, history, user_text)
    → RunTurnResult (events list + final_layout + tool_calls_made + ...)
  - ToolExecutor 생성자에서 db 제거, persist_layout 삭제
  - bootstrap.py 의 chat router include 제거
  - MetadataServicer.RunChatTurn 신규: layout/history JSON 파싱 → run_turn
    → events_json + final_layout_json 직렬화

api-server 신규:
  - entities/assets_editorial_article_chat_{sessions,messages}.rs
  - domains/admin/editorial_article_chat.rs (4 handlers)
  - services/decoded_ai_grpc/client.rs::run_chat_turn (deadline 120s)
  - send_message 오케스트레이션: user msg INSERT → load article+history
    → gRPC RunChatTurn → events 순서대로 INSERT → layout UPDATE if changed
    → session.last_message_at touch

web:
  - 2 chat route 가 AI_SERVER_HTTP_URL → API_BASE_URL + Bearer 로 교체
  - server-env.ts 에서 AI_SERVER_HTTP_URL export 삭제

Streaming 은 out of scope (이번 PR 은 unary, send 클릭 후 ~30s 대기. 후속
streaming 은 #448 의 "대화형 강화"). prod ai.decoded.style 외부 노출 차단은
별도 SRE 작업 (이 PR 변경 후 web 이 더 이상 호출하지 않음).

Local 검증:
  - cargo build OK
  - tsc --noEmit OK (chat 파일 clean)
  - gRPC RunChatTurn E2E: layout 'Test 매거진' → 'K-pop 가을 룩' 변경 확인,
    events_json sequence (assistant tool_call → tool result → assistant text)
    + layout_changed=True + final_layout 갱신.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai AI/자동화 backend 백엔드/API bump:minor Backward-compatible additions enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant