제출물 지표 수집
최종 제출물(인스타 릴스·피드, 틱톡) 링크의 조회수·좋아요 등을 주기적으로 수집하는 흐름, 파이썬 API 계약, 캠페인 인사이트 API
제출물 지표 수집
크리에이터가 최종 제출한 게시물 링크의 지표(조회수·좋아요·댓글·공유·저장)를 캠페인 종료 후 일정 기간 동안 시계열로 쌓습니다.
역할 분담
| 담당 | 내용 | |
|---|---|---|
| 언제 · 누구를 볼지 | 스프링 (SubmissionMetricCollectService) | 큐 테이블 TB_SUBMISSION_METRIC_COLLECT + 5분 폴러 |
| 실제 수집 | 파이썬 (POST /api/submission-metrics/collect) | 무상태. 호출 1회 = RapidAPI 1콜 = post_metrics 1행 |
| 조회 | 파이썬 GET 3종 | 최신값 · 시계열 · 캠페인 단위 (단건 확인·디버깅용) |
| 화면(캠페인 인사이트) | 스프링 (CampaignInsightService) | 크롤 PG 를 직접 읽어 전체 성과 + 크리에이터별 지표 |
자동 검수(FinalSubmissionAutoCheckService)와 같은 모양입니다. 큐 + 폴러인 이유도 같습니다 — 대상 전부를 한 루프로 돌면 단일 스케줄러 스레드가 오래 막히고 블루그린 배포가 루프를 중간에 죽입니다.
등록 시점
FinalSubmissionSubmittedEvent(최초 제출·재제출) 를 커밋 후 비동기로 받아 큐 행을 만듭니다. 관리자 검수 통과(approveReview)는 소명 흐름에서만 세팅되므로 트리거로 쓰지 않습니다.
등록 조건 (하나라도 아니면 생략):
- yml
submission-metrics.collect.enabledAND Flagsmithsubmission_metrics_collect_enabled contentLink있음Collab.snsContentFormat이 아래 표에 있음
SnsContentFormat | platform | post_type |
|---|---|---|
INSTAGRAM_REELS | INSTAGRAM | REEL |
INSTAGRAM_FEED | INSTAGRAM | FEED |
TIKTOK_SHORTS | TIKTOK | VIDEO |
| 그 외 / null | 등록 안 함 |
application_id 당 한 행. 재제출이면 링크·포맷을 갱신하고 next_collect_at = now, consecutive_failures = 0, STOPPED 였어도 ACTIVE 로 되살립니다.
수집 주기
기준점(anchor) = TB_COLLAB.campaign_completed_at(관리자가 캠페인 완료 처리한 시각) 이 있으면 그것, 없으면 TB_FINAL_SUBMISSION.submitted_at.
개월(months) = TB_FINAL_SUBMISSION.secondary_usage_months(제출 시 스냅샷, -1 영구), 없으면 TB_COLLAB.secondary_usage_months.
수집이 한 번 끝날 때마다 그 시점 기준으로 다음 간격을 정합니다 (매번 다시 읽으므로 캠페인 완료 처리가 나중에 돼도 반영됩니다).
| 조건 (위에서부터 첫 일치) | 다음 간격 |
|---|---|
now < anchor + 1개월 | 6시간 |
months == null | 중단 |
months == -1 | 24시간 (무기한) |
now < anchor + max(months, 1)개월 | 24시간 |
| 그 외 | 중단 |
- 연속 실패 5회부터 간격을 24시간으로 늦춥니다. 중단은 안 합니다 (비공개였다 다시 열리는 경우).
- 기준점·제출일이 둘 다 없으면 판단 근거가 없어 중단합니다.
- 중단 =
status = STOPPED,next_collect_at = NULL. 재제출 이벤트가 오면 다시 ACTIVE.
폴러
@Scheduled(cron = "0 */5 * * * *"), 한 번에 30건(submission-metrics.collect.batch-size),next_collect_at오름차순.- 건별로 트랜잭션 없이 파이썬을 부르고
save()— 파이썬 호출(최대 30초) 동안 커넥션을 물지 않습니다. - 배포 중 죽어도
next_collect_at이 안 바뀐 행은 다음 5분에 다시 집힙니다.
결과 반영
| 파이썬 응답 | last_status | 실패 카운트 | 다음 |
|---|---|---|---|
200, status = OK / PRIVATE | 그대로 | 0 | 주기표 |
200, status = REMOVED / ERROR | 그대로 | +1 | 주기표 (5회↑ 24h) |
| 400 / 422 | REJECTED | +1 | STOPPED — 링크가 게시물 URL 이 아님, 재제출이 와야 풀림 |
| 그 외 4xx · 5xx · 타임아웃 | HTTP_ERROR | +1 | 주기표 — 아무것도 기록 안 됐으니 재시도 |
파이썬 API 계약
POST /api/submission-metrics/collect
{
"application_id": 123,
"collab_no": 456,
"platform": "INSTAGRAM",
"post_type": "REEL",
"post_url": "https://www.instagram.com/reel/XXXX/"
}응답 200 (게시물 상태는 결과이지 요청 오류가 아닙니다):
{
"application_id": 123,
"post_id": "XXXX",
"status": "OK",
"error": null,
"crawled_at": "2026-10-01T03:00:00+00:00",
"metrics": { "views": 1200, "likes": 80, "comments": 5, "shares": null, "saves": null }
}status:OK/REMOVED/PRIVATE/ERRORmetrics와crawled_at은 지표를 저장했을 때만 값이 있습니다. REMOVED·ERROR 는 항상 null, PRIVATE 도 지표가 없으면 null.status가 아니라metrics != null로 분기하세요.- 인스타는
shares/saves없음(null), 좋아요 숨김이면likesnull. - 틱톡
REMOVED의error두 가지:post/detail 응답 없음 (삭제 또는 API 오류)— 삭제와 API 오류를 구분 못 함 /post/detail status=REMOVED (takeDown)— 틱톡이 명시적으로 내린 영상. - 400: URL 이 플랫폼 게시물 링크가 아님 /
post_type불일치 / 미지원 플랫폼. 단축 링크(vm.tiktok.com/…,tiktok.com/t/…,instagram.com/share/…)는 게시물 id 를 못 뽑아 400. - 422: 바디 형식 오류. 400 과 같이 다룹니다.
- 5xx / 타임아웃: 파이썬 DB 장애 등. 아무것도 기록되지 않았습니다.
- 동기, 보통 1~3초. 스프링 클라이언트는 read timeout 30초 (
submissionMetricsRestTemplate).
조회
GET /api/submission-metrics/{application_id}→ 매핑 +latest(최신 지표 1건 또는 null). 404 = 수집된 적 없음.GET /api/submission-metrics/{application_id}/history?from=&to=→items[]시계열 오름차순.from/to는 오프셋 포함 ISO8601.GET /api/submission-metrics/collab/{collab_no}→ 캠페인의 제출물별 최신값 목록.
GET 응답 필드: application_id, collab_no, platform, post_type, post_url, post_id, last_collected_at, last_status, last_error, latest — latest 는 {crawled_at, views, likes, comments, shares, saves} 또는 null.
화면(기업·어드민 캠페인 인사이트)이 쓰는 것은 아래 스프링 API 다. 파이썬 GET 은 단건 확인·디버깅용이다.
캠페인 인사이트 API (스프링)
기업·어드민 캠페인 화면의 "캠페인 인사이트" 섹션 — 전체 성과 + 크리에이터별 지표.
| 경로 | 권한 | |
|---|---|---|
| 기업 | GET /ai/collab/{campaignNo}/insight | BUSINESS · AGENCY_CLIENT · ADMIN + 캠페인 소유권 검증(남의 캠페인이면 403 AUTH_002) |
| 어드민 | GET /ai/admin/campaigns/{campaignNo}/insight | ADMIN |
파이썬을 거치지 않는다. 스프링이 크롤 PG 의 submission_post + post_metrics 최신 1행을 직접 읽고(postgresJdbcTemplate), 이름·프로필은 MariaDB 에서 붙인다.
응답
{
"campaignNo": 456,
"summary": { "views": 9999999, "likes": 9999999, "comments": 9999999, "engagementRate": 5.2 },
"creators": [
{
"applicationId": 123,
"influenceNo": 11,
"name": "제이제이이지",
"profileImage": "https://.../profile.jpg",
"platform": "INSTAGRAM",
"postType": "REEL",
"postUrl": "https://www.instagram.com/reel/XXXX/",
"views": 9999999,
"likes": 9999999,
"comments": 9999999,
"engagementRate": 3.5,
"lastStatus": "OK",
"collectedAt": "2026-10-01T03:00:00"
}
]
}참여율
| 식 | |
|---|---|
| 크리에이터 | (좋아요 + 댓글) / 조회수 × 100, 소수 1자리 |
| 전체 | Σ(좋아요+댓글) / Σ조회수 × 100 — 조회수 가중 |
- 조회수가 없거나 0 이면 그 크리에이터의
engagementRate는 null, 전체 참여율 계산에서도 빠진다(피드 이미지, 좋아요·조회수 숨김 등). 다만 그 크리에이터의 좋아요·댓글은 전체 합계에는 들어간다. - 조회수 있는 크리에이터가 하나도 없으면
summary.engagementRate는 null. - 대시보드의 보정 참여율(
influencer_profile.adjusted_engagement_rate)과 정의의 핵심은 같지만, 플랫폼 prior 보정은 넣지 않는다 — 그 보정은 "게시물 몇 개 안 본 계정을 과대평가하지 말자"는 용도라 제출물 1건짜리 캠페인 성과에는 맞지 않는다.
그 밖의 동작
- 정렬: 조회수 내림차순, 없는 크리에이터는 뒤.
- 아직 수집 전인 크리에이터도 목록에 포함되고 지표·
lastStatus·collectedAt이 전부 null 이다(수집 대기 중임을 보이기 위해). lastStatus는 마지막 수집 결과(OK/REMOVED/PRIVATE/ERROR/HTTP_ERROR/REJECTED).REMOVED는 게시물이 내려간 것이고, 그 경우 지표는 마지막으로 저장된 값이다.- 없는 캠페인이면 404 가 아니라 빈 인사이트(
summary0/null,creators: []) — 인사이트 섹션이 상세 화면을 깨면 안 된다. collectedAt은 KST 기준LocalDateTime.
운영
- DDL 은 수동: Flyway 가 꺼져 있어
V202609221200__add_submission_metric_collect.sql을 test/prod DB 에 직접 실행합니다. 플래그를 켜기 전에 테이블 존재를 확인하세요. - 끄는 법: Flagsmith
submission_metrics_collect_enabledOFF → 등록·폴링 즉시 중단(배포 없음). yml 킬스위치SUBMISSION_METRICS_COLLECT_ENABLED=false도 있습니다(배포 필요).scheduler.enabled가 true 여야 폴러가 돕니다. - 소급: 배포 후
docs/superpowers/ops/2026-09-22-submission-metric-collect-backfill.sql한 번 (기준점 13개월 이내만). - 상태 확인:
SELECT last_status, COUNT(*) FROM TB_SUBMISSION_METRIC_COLLECT GROUP BY 1.REJECTED는 링크가 단축 URL 인 건 — 운영이 정규 URL 로 재제출을 유도합니다. - 캠페인 삭제:
TB_SUBMISSION_METRIC_COLLECT는TB_CAMPAIGN_APPLICATIONFK(NO ACTION) 라CampaignDeletionDao가 명시적으로 지웁니다. - 예상 콜: 활성 500건 × 4회/일 ≈ 2,000 RapidAPI 콜/일.
- 스케일 승격: 수천 건·RapidAPI 429 가 보이면 폴러의 직접 호출을 Cloud Tasks 등록으로 바꿉니다. 파이썬은 안 바뀝니다.
관련 설계: docs/superpowers/specs/2026-09-22-submission-metrics-collect-design.md, glowb(python) docs/superpowers/specs/2026-09-21-submission-metrics-design.md.