테마
Story: E-12-S-01 키워드 목록 API 개발
메타
| 항목 | 값 |
|---|---|
| Story ID | E-12-S-01 |
| Epic | E-12 Keyword Analysis |
| 상태 | ready-for-dev |
| 우선순위 | P0 |
| 규모 | M (2pt) |
| 담당 | BE (하록) |
사용자 스토리
As a 광고를 운영하는 셀러, I want 캠페인 내 키워드별 성과 데이터를 조회, So that 어떤 키워드가 순이익을 내는지/잃는지 분석할 수 있다.
수락 기준 (Acceptance Criteria)
AC-01: 키워드 목록 조회
| 항목 | 내용 |
|---|---|
| Given | 셀러가 캠페인 상세 페이지에 있고, 해당 캠페인에 키워드 데이터가 있음 |
| When | 키워드 목록 API를 호출 (campaignId, startDt, endDt) |
| Then | 키워드별 집계 데이터 반환 (키워드명, 노출수, 클릭수, 광고비, 주문수, 전환매출, 판매량) |
AC-02: 순이익 계산 포함
| 항목 | 내용 |
|---|---|
| Given | 셀러의 원가(마진율) 데이터가 있음 |
| When | 키워드 목록 API 응답 생성 |
| Then | 각 키워드별 순이익 계산하여 반환 (순이익 = 전환매출 - 원가 - 광고비×1.1) |
AC-03: 정렬 및 페이지네이션 지원
| 항목 | 내용 |
|---|---|
| Given | 키워드 수가 많은 캠페인 |
| When | sortBy, sortOrder, page, size 파라미터와 함께 호출 |
| Then | 정렬된 결과를 페이지 단위로 반환 (totalElements, totalPages 포함) |
AC-04: 원가 미입력 시 처리
| 항목 | 내용 |
|---|---|
| Given | 셀러가 원가를 입력하지 않음 (purchaseYn = 'N') |
| When | 키워드 목록 API 호출 |
| Then | 순이익 필드는 null로 반환, ROAS 등 다른 지표는 정상 반환 |
AC-05: 데이터 없음 처리
| 항목 | 내용 |
|---|---|
| Given | 해당 캠페인이 비검색 광고만 운영 (키워드 없음) 또는 기간 내 데이터 없음 |
| When | 키워드 목록 API 호출 |
| Then | 빈 배열 반환 (dataList: [], totalElements: 0) |
태스크 분해
Task 1: API 엔드포인트 설계 AC-01
- [ ] 1.1: POST
/ads/campaign/{campaignId}/keywords엔드포인트 생성 - [ ] 1.2: Request DTO 정의 (startDt, endDt, sortBy, sortOrder, page, size)
- [ ] 1.3: Response DTO 정의 (아래 스펙 참조)
Task 2: 데이터 조회 로직 AC-01, AC-03
- [ ] 2.1: CoupangAdKeywordDaily 테이블에서 키워드별 집계 쿼리
- [ ] 2.2: 정렬 로직 구현 (순이익, 광고비, ROAS 등)
- [ ] 2.3: 페이지네이션 처리
Task 3: 순이익 계산 로직 AC-02, AC-04
- [ ] 3.1: 원가 데이터 조회 (SettlementMargin 또는 Product)
- [ ] 3.2: 키워드별 순이익 계산 (전환매출 - 원가 - 광고비×1.1)
- [ ] 3.3: purchaseYn = 'N'인 경우 순이익 null 처리
Task 4: 예외 처리 AC-05
- [ ] 4.1: 키워드 데이터 없음 시 빈 배열 반환
- [ ] 4.2: 캠페인 ID 유효성 검증
- [ ] 4.3: API 에러 응답 표준화
Task 5: 테스트 작성
- [ ] 5.1: 단위 테스트 (계산 로직)
- [ ] 5.2: 통합 테스트 (API 엔드포인트)
Dev Notes
API 스펙
Request
typescript
POST /ads/campaign/{campaignId}/keywords
{
startDt: "YYYYMMDD",
endDt: "YYYYMMDD",
sortBy: "netProfit" | "adCost" | "roas" | "impressions" | "clicks",
sortOrder: "asc" | "desc",
page: number, // 1부터 시작
size: number // 10, 20, 50, 100
}Response
typescript
{
totalElements: number,
totalPages: number,
pageNo: number,
pageSize: number,
summary: {
totalAdCost: number,
totalNetProfit: number | null,
purchaseYn: "Y" | "N",
excludedCount: number // 제외키워드 등록 수
},
dataList: [{
keyword: string,
impressions: number,
clicks: number,
adCost: number,
adCostRatio: number, // 광고비 비중 (%)
cpc: number | null, // 클릭 0이면 null
orders: number,
cvr: number | null, // 클릭 0이면 null
revenue: number,
roas: number | null, // 광고비 0이면 null
netProfit: number | null, // 원가 미입력 시 null
netProfitRatio: number | null,
quantity: number,
isExcluded: boolean // 제외키워드 등록 여부
}]
}데이터 소스
sql
-- CoupangAdKeywordDaily 테이블
SELECT
keyword,
SUM(exposureCount) as impressions,
SUM(clickCount) as clicks,
SUM(adCost) as adCost,
SUM(orders) as orders,
SUM(quantity) as quantity,
SUM(conversionAmount) as revenue
FROM CoupangAdKeywordDaily
WHERE vendorId = :vendorId
AND campaignId = :campaignId
AND adDate BETWEEN :startDt AND :endDt
GROUP BY keyword
ORDER BY :sortBy :sortOrder
LIMIT :size OFFSET :offset;참고 구현
| 항목 | 경로 |
|---|---|
| 기존 캠페인 목록 API | backend/src/ads/campaign/list |
| 순이익 계산 로직 | backend/src/ads/profit-calculator |
충돌 감지
| 항목 | 상태 | 설명 |
|---|---|---|
| 기존 코드 충돌 | ✅ 없음 | 신규 엔드포인트 |
| DB 스키마 변경 | ✅ 없음 | 기존 테이블 사용 |
References
| 출처 | 경로/링크 | 참조 섹션 |
|---|---|---|
| Epic Spec | epic-specs/E-12-keyword-analysis.md | 로직 2: 데이터 소스 |
| 광고분석 화면 구조 | .context/global/ads-analysis-screen.md | API 구조 |
핸드오프 전 체크리스트
문맥 & 요구사항
- [x] 사용자 스토리가 명확한가?
- [x] 수락 기준이 Given-When-Then 형식인가?
- [x] 모든 엣지 케이스가 정의되었는가?
재해 예방
- [x] 기존 코드에 유사 기능 확인했는가?
- [x] 충돌 감지 항목에 ⚠️가 없는가?
AI Agent 최적화
- [x] Dev Notes가 충분히 상세한가?
- [x] References가 정확한가?
- [x] 태스크 분해가 구체적인가?
최종 점검
- [x] 상태가
ready-for-dev로 설정되었는가?
검증 결과: ✅ PASS 검증일: 2026-01-28
생성일: 2026-01-28생성자: 📋 Penny
