# task-1461.1 보고서 — 대시보드 캠페인뷰 배너 3버전 비교 뷰 추가

**작성**: 마르둑 (개발5팀장)
**작성일**: 2026-04-05
**검증 레벨**: normal

---

## S — Situation

task-1454.1, task-1455.1, task-1456.1에서 각각 9셀 배너 3버전을 제작했으나 같은 output/banners/ 디렉토리를 덮어쓰며 마지막 버전만 남아있다. 제이회장님이 3버전을 대시보드에서 비교하며 셀별로 선택할 수 있는 뷰가 필요하다.

## C — Complication

git 이력에 배너 파일 커밋이 1건만 존재하여 3버전 중 2개(v1454, v1455)를 복원할 수 없다. 현재 디스크에는 마지막으로 기록된 1개 버전(task-1456.1)만 존재한다.

## Q — Question

복원 가능한 1개 버전으로 비교 뷰 인프라를 구축하고, 향후 나머지 2버전이 추가되면 즉시 비교할 수 있는 대시보드를 만들 수 있는가?

## A — Answer

비교 뷰 인프라를 완성하여 v1456(현재 유일 버전) 비교/선택이 동작한다. v1454/v1455는 placeholder 디렉토리로 구성되어, 해당 태스크의 배너를 재생성하여 디렉토리에 넣으면 즉시 비교 가능하다. GET/POST API 2개, 프론트엔드 컴포넌트 1개, 배너비교 탭 추가 완료. curl 테스트 5건 전체 통과.

---

## 작업 상세

### 1. 사전 작업: 버전 디렉토리 구조 생성
- `output/banners/versions/v1456/` — 현재 9셀 배너 전체 복사
- `output/banners/versions/v1454/`, `v1455/` — 빈 placeholder 디렉토리 (재생성 대기)

### 2. 백엔드 API (엔키, sonnet)
- `GET /api/banner-versions` — 버전 목록, 셀별 available 상태 동적 검사, DQ 점수 포함
- `POST /api/banner-versions/select` — 셀별 버전 선택, atomic write, 선택 파일 복사
- `GET /api/banners/versions/{path}` — 기존 banners 정적 서빙 경로 활용 (추가 코드 불필요)

### 3. 프론트엔드 컴포넌트 (이쉬타르, sonnet)
- `BannerCompareView.js` — 9셀 그리드 + 셀별 3버전 비교 뷰
- `App.js` — '배너비교' 탭 등록

### 4. 데이터 구조 불일치 수정 (팀장 직접 개입)
- 프론트엔드는 versions를 배열로 기대, 백엔드는 딕셔너리로 반환 → API 응답 변환 코드 추가

## 발견 이슈 및 해결 (4건)

1. **3버전 복원 불가**: git에 1개 커밋만 존재. v1454/v1455 배너 파일 복원 불가.
   - 해결: v1456만 실제 파일로 구성, v1454/v1455는 placeholder + available=false 처리. 재생성 시 자동 인식.

2. **프론트/백엔드 데이터 구조 불일치**: 프론트엔드가 versions를 배열+cells 서브객체로 기대하나 백엔드는 딕셔너리 반환.
   - 해결: GET /api/banner-versions에서 딕셔너리→배열 변환 + 셀별 available/dq_score 구조 동적 생성.

3. **bg-*.png 중복 복사 방지**: 배경 이미지는 원본 셀 디렉토리에 이미 존재하므로 select 시 복사 대상에서 제외.
   - 해결: copy_targets를 html/png 4개로 한정, bg-*.png 제외.

4. **Pyright shutil 미사용 경고**: import shutil이 line 25에 있고 사용은 line 2241 — Pyright false positive.
   - 해결: 실제 사용 확인, 무시 처리.

## 테스트 결과

| 테스트 | 결과 | 상세 |
|--------|------|------|
| GET /api/banner-versions | PASS | versions 3개, cells 9개, selections 9개 반환 |
| POST select (정상) | PASS | cell-1-incar-fair → v1456 선택 성공, 파일 복사 확인 |
| POST select (null) | PASS | 선택 해제 성공 |
| POST select (잘못된 cell_id) | PASS | 400 에러 반환 |
| POST select (잘못된 version) | PASS | 400 에러 반환 |
| 이미지 서빙 (v1456) | PASS | 200, image/png, 1.08MB |
| 이미지 서빙 (v1454, 미존재) | PASS | 404 반환 |

## 산출물 파일 목록

### 신규 생성
- `/home/jay/workspace/dashboard/components/BannerCompareView.js`
- `/home/jay/workspace/dashboard/data/banner-versions.json`
- `/home/jay/workspace/output/banners/versions/v1456/cell-1-incar-fair/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-1-incar-fair/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-1-incar-fair/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-1-incar-fair/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-2-incar-leader/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-2-incar-leader/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-2-incar-leader/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-2-incar-leader/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-3-incar-support/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-3-incar-support/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-3-incar-support/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-3-incar-support/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-4-ga-fair/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-4-ga-fair/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-4-ga-fair/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-4-ga-fair/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-5-ga-leader/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-5-ga-leader/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-5-ga-leader/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-5-ga-leader/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-6-ga-support/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-6-ga-support/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-6-ga-support/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-6-ga-support/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-7-snu-fair/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-7-snu-fair/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-7-snu-fair/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-7-snu-fair/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-8-snu-leader/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-8-snu-leader/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-8-snu-leader/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-8-snu-leader/google-resp-1200x628.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-9-snu-support/meta-feed-1080x1080.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-9-snu-support/meta-feed-1080x1080.png`
- `/home/jay/workspace/output/banners/versions/v1456/cell-9-snu-support/google-resp-1200x628.html`
- `/home/jay/workspace/output/banners/versions/v1456/cell-9-snu-support/google-resp-1200x628.png`

### 수정
- `/home/jay/workspace/dashboard/server.py` (GET/POST 배너 버전 API 추가)
- `/home/jay/workspace/dashboard/components/App.js` (배너비교 탭 등록)

## 모델 사용 기록

| 팀원 | 역할 | 모델 | 작업 |
|------|------|------|------|
| 엔키 | 백엔드 | sonnet | server.py API 2개 구현 |
| 이쉬타르 | 프론트엔드 | sonnet | BannerCompareView.js + App.js 탭 등록 |
| 마르둑 | 팀장 | opus | 설계/검토/데이터 불일치 수정/통합 테스트 |

## 비고

- v1454, v1455 배너 파일은 git에서 복원 불가. 해당 태스크의 디자인팀이 재생성하여 `output/banners/versions/v1454/`, `v1455/` 하위에 배치하면 대시보드에서 자동으로 비교 가능.
- 기존 캠페인뷰 기능 훼손 없음 (별도 탭으로 분리).
