# task-1002.1: 2팀 카드뉴스 내용 부실 원인 분석 보고서

**작성일**: 2026-03-25
**작성자**: 헤르메스 (dev1 팀장)
**팀원**: 불칸(코드경로분석), 이리스(이미지비교), 아르고스(검증)

---

## SCQA

**S**: task-1000.1(1팀)과 task-1001.1(2팀)에서 각각 카드뉴스 생성+업로드 E2E 테스트를 수행했으며, 양 팀 모두 업로드 자체는 성공했다.

**C**: 제이회장님 피드백에서 2팀 결과물(CrossPublisher 배치)의 내용이 부실하다는 지적이 있었다. 디자인(테마/레이아웃)은 정상이나, 본문 슬라이드의 텍스트가 "요점 1~4" 수준의 플레이스홀더에 머물러 있어 카드뉴스로서의 정보 가치가 없다.

**Q**: 1팀과 2팀의 콘텐츠 품질 차이의 정확한 원인은 무엇이며, 재발 방지를 위해 어떤 조치가 필요한가?

**A**: 2팀이 CrossPublisher E2E 테스트 시 ContentGeneratorV2(Claude AI) 파이프라인을 우회하여 최소한의 items 데이터를 직접 구성했고, 이로 인해 `render_all` 레거시 경로를 탔기 때문이다. 1팀은 `cli.py pipeline` 표준 경로를 사용하여 Claude가 생성한 풍부한 slides JSON(슬라이드당 2~3개 카드, 130~230자)을 `render_from_slides`로 렌더링했지만, 2팀은 수동 구성 items(슬라이드당 1개 카드, 32~38자)를 `render_all`로 렌더링했다.

---

## Phase 1: 콘텐츠 생성 경로 비교

### 1팀 표준 경로

```
cli.py:730 (pipeline -t cardnews --source news --upload)
  → orchestrator.run_cardnews(upload=True)
    → topic_selector.select_single_topic()           # 토픽 무작위 선택
    → ContentGeneratorV2().generate(topic, context)   # Claude CLI subprocess → slides JSON
    → CardNewsRenderer.render_from_slides(content["slides"], theme)  # V2 렌더링
    → ThreadsPublisher.publish_cardnews(content=content)  # slides 키 보존 → V2 분기
```

**핵심**: ContentGeneratorV2가 Claude CLI subprocess를 호출하여 5~7장의 구조화된 slides JSON을 생성. 각 슬라이드 타입(cover/card_list/detail/body/cta)에 맞는 풍부한 텍스트 포함.

### 2팀 경로 (Phase별)

**Phase 2 (Instagram 단독)** — 동일 파이프라인 사용, 정상 품질:
```
(표준 파이프라인으로 카드뉴스 생성 → 103633_* 배치)
  → ContentGeneratorV2 사용 → render_from_slides → 풍부한 콘텐츠
  → 인라인 스크립트로 Instagram 업로드
```

**Phase 3 (CrossPublisher)** — **문제 발생 경로**:
```
인라인 스크립트 → CrossPublisher.publish_cardnews(
    title="보험 상품군 완전 정리 가이드",
    items=[{"title":"요점 1", "description":"..."}, ...],  # 수동 구성, 최소 데이터
    content={}  # slides 키 없음
)
  → ThreadsPublisher.publish_cardnews(content={})
    → "slides" in content → False  # threads_publisher.py:156
    → CardNewsRenderer.render_all(items=items)  # 레거시 분기 진입
  → InstagramPublisher도 동일하게 render_all 분기
```

### 같은 파이프라인을 탔는가?

**아니오.** 1팀은 표준 파이프라인(`cli.py pipeline`)을 사용했고, 2팀 Phase 3은 인라인 스크립트로 CrossPublisher를 직접 호출하면서 ContentGeneratorV2를 거치지 않았다. 2팀 Phase 2는 표준 파이프라인을 사용하여 정상 품질이었다.

---

## Phase 2: 콘텐츠 품질 비교

### 1팀 카드뉴스 (103620 배치, Threads 업로드)

- **토픽**: "교육이 부실해서 스스로 공부해야 하는 상황이에요"
- **테마**: NavyGold
- **본문 슬라이드 구성**: 슬라이드당 2~3개 카드 박스 + TIP 배너
- **텍스트 분량**:
  - 슬라이드 01: ~130자 (카드 2개 - "입사 후 '알아서 하세요'", "공부가 내 숙제가 됩니다")
  - 슬라이드 02: ~120자 (카드 2개 - "성장 속도가 느려집니다", "번아웃이 빨리 옵니다")
  - 슬라이드 03: ~180자 (카드 3개 + TIP - "실전 중심 프리미엄 교육", "1:1 밀착 코칭", "24시간 세일즈캠퍼스")
  - 슬라이드 04: ~185자 (카드 3개 + TIP - "Consulting Logic", "IT Work-Flow", "AI Automation")
- **파일 크기 평균**: 94KB (본문 84~120KB)

### 2팀 카드뉴스 세트A (103633 배치, Instagram 단독) — 정상

- **토픽**: 동일 ("교육도 없이 혼자 버텼나요")
- **테마**: PurplePink (BlackRed)
- **콘텐츠 품질**: 1팀과 동일 수준 — 슬라이드당 2~3개 카드, 130~230자
- **파일 크기 평균**: 93KB

### 2팀 카드뉴스 세트B (104909 배치, CrossPublisher) — **부실**

- **토픽**: "보험 상품군 완전 정리 가이드"
- **테마**: NavyGold
- **본문 슬라이드 구성**: 슬라이드당 1개 카드 박스, 상단 60% 빈 공간
- **텍스트 분량**:
  - 슬라이드 01: ~35자 (카드 1개 - "요점 1: 복잡한 보험 상품군을 카테고리별로 정리")
  - 슬라이드 02: ~38자 (카드 1개 - "요점 2: 보험 가입 전 꼭 확인해야 할 핵심 포인트")
  - 슬라이드 03: ~32자 (카드 1개 - "요점 3: 주요 보험사별 보장 범위와 차이점")
  - 슬라이드 04: ~34자 (카드 1개 - "요점 4: 같은 보장, 더 저렴한 보험료를 위한 전략")
- **파일 크기 평균**: 50KB (본문 32~39KB)

### 정량 비교

- 1팀 본문 텍스트: 평균 **154자/슬라이드**, 카드 2~3개/슬라이드
- 2팀(부실) 본문 텍스트: 평균 **35자/슬라이드**, 카드 1개/슬라이드
- 텍스트 분량 비율: 2팀은 1팀의 **22.7%** 수준
- 파일 크기 비율: 2팀 본문 평균 34KB vs 1팀 평균 97KB (35%)

---

## Phase 3: 원인 특정

### 원인 1 (직접 원인): ContentGeneratorV2 미호출

2팀 Phase 3에서 CrossPublisher를 직접 호출할 때 ContentGeneratorV2(Claude AI)를 통한 콘텐츠 생성을 수행하지 않았다. 인라인 테스트 스크립트에서 `items` 파라미터를 수동으로 최소한으로 구성하여 전달했다.

- **증거**: 본문 카드 제목이 "요점 1", "요점 2" 등 플레이스홀더 수준
- **증거**: 카드 설명이 1줄(20자 내외)로, Claude가 생성하는 2~3문장(50~80자)과 대비

### 원인 2 (분기 원인): render_all 레거시 경로 진입

`threads_publisher.py:156`의 분기 로직:
```python
if content and "slides" in content and isinstance(content["slides"], list):
    # V2 경로: render_from_slides → card_list/detail 타입 지원
else:
    # 레거시 경로: render_all → render_body 단일 타입만
```

2팀이 `content`에서 `slides` 키를 제거(1차 시도에서 빈 리스트 → 0개 이미지 실패 후 우회)하여 레거시 분기에 진입.

### 원인 3 (구조 원인): render_all vs render_from_slides 레이아웃 차이

- `render_all`: 각 item을 `render_body()` 단일 타입으로 렌더링 → 슬라이드당 1개 카드
- `render_from_slides`: slide type에 따라 `render_card_list()`, `render_detail()` 등 선택 → 슬라이드당 2~3개 카드 + TIP 배너
- `render_all`은 cover의 category를 `"고민공감"`으로 하드코딩(`cardnews.py:2535`), keywords 미전달

### five_stage_pipeline 사용 여부

- 양 팀 모두 `five_stage_pipeline.py`를 직접 사용하지 않음. `orchestrator.run_cardnews()`는 `ContentGeneratorV2`를 직접 호출(`orchestrator.py:470`)
- `FiveStagePipeline`은 `orchestrator.py`에서 import되지 않음 — 별도 고급 경로로 존재

### 토픽 선택 경로

- 1팀: `topic_selector.select_single_topic()` 자동 선택 → evergreen_topics.json 기반
- 2팀 Phase 3: 인라인 스크립트에서 직접 지정 ("보험 상품군 완전 정리 가이드")

### 원인 요약 다이어그램

```
1팀 경로 (OK):
  cli.py pipeline
    → orchestrator.run_cardnews()
      → ContentGeneratorV2.generate()  ← Claude AI가 풍부한 slides JSON 생성
        → slides: [{type:"cover",...}, {type:"card_list", items:[3개],...}, ...]
      → render_from_slides(slides)
        → render_card_list() → 슬라이드당 2~3 카드 + TIP
      → ThreadsPublisher(content={slides:[...]})  ← slides 키 보존
        → "slides" in content → True → render_from_slides  ← V2 분기

2팀 Phase 3 경로 (부실):
  인라인 스크립트
    → ContentGeneratorV2 미호출  ← ★ 핵심 원인
      → items: [{"title":"요점1","description":"..."}, ...]  ← 수동 최소 데이터
    → CrossPublisher(items=items, content={})  ← slides 키 없음
      → ThreadsPublisher(content={})
        → "slides" in content → False → render_all  ← 레거시 분기
          → render_body() × 4 → 슬라이드당 1 카드
```

---

## Phase 4: 재현 가능성 및 해결방안

### 재현 조건

다음 조건이 모두 충족되면 동일 현상 재현:
1. CrossPublisher 또는 개별 Publisher를 직접 호출
2. `content` 파라미터에 `slides` 키 미포함 (또는 `content=None`)
3. `items` 파라미터에 최소 데이터 전달

재현 명령 (실행하지 않음 — API 크레딧 절약):
```python
CrossPublisher().publish_cardnews(
    title="테스트 제목",
    items=[{"title": f"요점 {i}", "description": f"간단한 설명 {i}"} for i in range(1,5)],
    content={},  # slides 없음 → render_all 분기
)
```

### 해결방안

#### 방안 1 (권장): Publisher에 slides 필수 가드레일 추가

**수정 위치**: `threads_publisher.py:156`, `instagram_publisher.py:106` (동일 분기)

```python
# Before (현재)
if content and "slides" in content and isinstance(content["slides"], list):
    # V2 경로
else:
    # 레거시 경로 → 무조건 진입

# After (제안)
if content and "slides" in content and isinstance(content["slides"], list):
    if len(content["slides"]) == 0:
        raise ValueError("content['slides']가 빈 리스트입니다. ContentGeneratorV2로 콘텐츠를 먼저 생성하세요.")
    # V2 경로
else:
    logger.warning("slides 키 없음 — render_all 레거시 경로 사용. 콘텐츠 품질이 저하될 수 있습니다.")
    # 레거시 경로 (후방호환 유지)
```

#### 방안 2: CrossPublisher에 자동 콘텐츠 생성 옵션 추가

**수정 위치**: `cross_publisher.py:30-38`

`content` 미제공 시 CrossPublisher가 내부에서 ContentGeneratorV2를 호출하여 slides를 자동 생성하는 옵션 추가. 이렇게 하면 인라인 스크립트에서도 항상 V2 품질 보장.

#### 방안 3 (최소): 문서화

`CrossPublisher.publish_cardnews()` docstring에 "content 파라미터에 slides 키가 포함된 dict를 전달해야 V2 품질의 카드뉴스가 생성됩니다. slides 미포함 시 레거시 render_all 경로가 사용되어 콘텐츠 품질이 저하됩니다." 명시.

---

## 발견 이슈 및 해결

### 자체 해결 (0건)

분석 전용 작업으로 코드 수정 없음.

### 범위 외 미해결 (3건)

1. **render_all에 빈 slides 방어 로직 부재** — 범위 외 사유: 코드 수정은 이 분석 작업의 범위 밖. 해결방안 1로 별도 작업 필요.
2. **CrossPublisher에 콘텐츠 자동 생성 기능 부재** — 범위 외 사유: 아키텍처 결정 사항. 해결방안 2로 별도 작업 필요.
3. **render_all의 category 하드코딩 (`"고민공감"`, cardnews.py:2535)** — 범위 외 사유: 레거시 경로의 기존 제약. 장기적으로 render_all 경로의 호출자가 category를 전달할 수 있도록 파라미터 추가 권장.

---

## 테스트 결과 증거

### 이미지 파일 크기 비교 (정량적 증거)

- 1팀 본문 (103620_01~04): 84KB, 84KB, 120KB, 118KB → 평균 **101.5KB**
- 2팀 부실 본문 (104909_01~04): 34KB, 39KB, 34KB, 32KB → 평균 **34.8KB**
- 2팀 정상 본문 (103633_01~04): 84KB, 84KB, 118KB, 117KB → 평균 **100.8KB**
- **2팀 정상 배치는 1팀과 동일 수준(100.8KB vs 101.5KB), 부실 배치만 1/3(34.8KB)**

### 이미지 텍스트 내용 비교 (정량적 증거)

- 1팀 본문 텍스트: 슬라이드당 120~185자, 카드 2~3개
- 2팀 부실 본문 텍스트: 슬라이드당 32~38자, 카드 1개, "요점 1~4" 플레이스홀더 제목
- 텍스트 분량 비율: 22.7%

### 코드 경로 비교 (정량적 증거)

- `threads_publisher.py:156`: `"slides" in content` 분기 조건 확인
- `cardnews.py:2535`: render_all category `"고민공감"` 하드코딩 확인
- `cardnews.py:2544-2561`: render_all의 item당 1개 render_body 렌더링 확인

---

## 셀프 QC 체크리스트

- [x] 1. 다른 파일에 영향: 없음 (분석 전용, 코드 수정 없음)
- [x] 2. 엣지 케이스: N/A (분석 작업)
- [x] 3. 작업 지시와 정확히 일치: Phase 1~4 전체 수행 완료
- [x] 4. 에러 처리/보안: API 키/토큰 값 미노출 확인
- [x] 5. 테스트 커버리지: N/A (분석 작업, 코드 변경 없음)
- [x] 6. 발견 이슈 처리: 3건 범위 외 사유 명시, 각 해결방안 제시

---

## 생성/수정 파일 목록

- 생성: `/home/jay/workspace/memory/reports/task-1002.1.md` (본 보고서)
