희망자풀 목록 조회
캠페인 신청 희망자를 크롤 지표와 함께 조회합니다. 같은 사람의 중복 등록은 한 건으로 묶입니다.
희망자풀 목록 조회
캠페인 신청 희망자(collab_applicants)를 PostgreSQL 크롤 지표(팔로워, 평균 조회수·좋아요·댓글, 릴스 지표, 보정 참여율, 랭킹가)와 함께 조회합니다.
이 테이블은 랜딩 폼이 받은 값을 그대로 쌓기 때문에 같은 사람이 여러 번 들어와 있습니다. 이 API는 조회 시점에 중복을 묶어 최신 등록 건만 내려줍니다.
HTTP 요청
GET /ai/admin/collab-applicants
Authorization: Bearer {access_token}Query Parameters
| 파라미터 | 타입 | 필수 | 기본값 | 설명 |
|---|---|---|---|---|
snsType | String | 아니오 | (전체) | INSTAGRAM · TIKTOK · YOUTUBE · NAVER · FACEBOOK. 모르는 값은 필터를 걸지 않음. 하위 호환용 — 새 연동은 snsTypes 사용 |
snsTypes | String[] | 아니오 | (전체) | 플랫폼 복수 선택. 하나라도 맞으면 통과. snsTypes=INSTAGRAM,TIKTOK 또는 키 반복 |
category | String | 아니오 | (전체) | 협업 카테고리 단일 토큰. CSV 토큰 단위 일치 (BEAUTY가 BEAUTY_TOOL에 걸리지 않음). 하위 호환용 — 새 연동은 categories 사용 |
categories | String[] | 아니오 | (전체) | 카테고리 복수 선택. 등록 카테고리와 하나라도 겹치면 통과 (토큰 단위, 대소문자 무시) |
countries | String[] | 아니오 | (전체) | 국가 코드 복수 선택 (KR · JP …). 크롤 프로필의 country 기준 |
createdFrom · createdTo | LocalDate | 아니오 | (없음) | 등록일 범위 yyyy-MM-dd, 양 끝 포함 |
keyword | String | 아니오 | (전체) | 이름 · 이메일 · 전화번호 · SNS 계정 부분 일치 |
crawled | Boolean | 아니오 | (전체) | true면 크롤 지표가 있는 사람만, false면 없는 사람만 |
minFollowers · maxFollowers | Integer | 아니오 | (없음) | 팔로워 범위 |
minAvgViews · maxAvgViews | Long | 아니오 | (없음) | 평균 조회수 범위. 플랫폼에 맞는 컬럼 기준 |
minRankingPrice · maxRankingPrice | Integer | 아니오 | (없음) | 랭킹가 범위. 지정하면 페이지 밖 인원까지 랭킹가를 조회해 거름 |
sort | String | 아니오 | CREATED_AT | CREATED_AT · FOLLOWERS · AVG_VIEWS · ENGAGEMENT_RATE · RANKING_PRICE |
direction | String | 아니오 | desc | asc 또는 desc |
page | Integer | 아니오 | 0 | 0-based |
size | Integer | 아니오 | 20 | 최대 200 |
지표 범위를 지정하면 크롤 데이터가 없는 사람은 제외됩니다. 값이 없는 것을 0으로 쳐서 남기면 "팔로워 1만 이상"에 지표 없는 사람이 섞여 들어오기 때문입니다. 범위를 안 주면 그대로 보입니다.
countries 와 minRankingPrice · maxRankingPrice 도 같습니다. 국가는 크롤 프로필에만 있어 미수집자는 국가 필터에서 빠지고, 랭킹가는 틱톡·네이버가 항상 null 이라 랭킹가 범위를 걸면 그 둘도 빠집니다.
단일 · 복수 파라미터
snsType · category 는 기존 연동을 위해 남겨둔 단일 값입니다. 복수 값(snsTypes · categories)과 함께 오면 합쳐서 봅니다 (snsType=TIKTOK&snsTypes=INSTAGRAM → 틱톡 또는 인스타).
등록일 범위
createdFrom · createdTo 는 snsType 필터처럼 그룹이 아니라 행에 걸립니다. 같은 사람이 3월과 9월에 등록했고 createdTo=2026-03-31 로 조회하면, 범위 안의 3월 등록 건이 대표로 나옵니다.
GET /ai/admin/collab-applicants?snsTypes=INSTAGRAM,TIKTOK&categories=FOOD,TRAVEL&countries=KR&createdFrom=2026-09-01&createdTo=2026-09-30&minRankingPrice=100000정렬
CREATED_AT 을 뺀 나머지는 크롤 지표라 값이 없는 사람이 섞여 있습니다. 오름차순이든 내림차순이든 값 없는 사람은 항상 뒤로 보냅니다 — 지표순 정렬의 목적이 상위를 추리는 것이라, 빈 값이 앞에 오면 정렬이 무의미해집니다. 동점은 최신 등록순입니다.
AVG_VIEWS 는 플랫폼에 맞는 컬럼을 기준으로 하므로, 인스타는 릴스 평균끼리 · 틱톡은 전체 평균끼리 비교됩니다. RANKING_PRICE 는 틱톡·네이버가 항상 null 이라 그 둘이 전부 뒤로 밀립니다.
모르는 sort 값은 400 대신 CREATED_AT 으로 떨어집니다. avgViews 같은 camelCase 표기도 받습니다.
중복 제거 규칙
정규화된 SNS 핸들 · 이메일 · 전화번호(숫자만) 중 하나라도 겹치면 같은 사람으로 묶습니다. 연결은 전이됩니다 — A와 B가 이메일로 이어지고 B와 C가 전화로 이어지면 A와 C도 한 사람입니다.
- 대표 행은 가장 최근 등록 건이고, 나머지는
duplicateIds로만 남습니다 - 전화번호는 숫자 9자리 미만이면 판정에 쓰지 않습니다 (
없음같은 값이 남남을 묶는 것을 막기 위함) snsType필터는 그룹이 아니라 행에 걸립니다. 이메일로 묶인 인스타·틱톡 등록 건 중 필터에 맞는 행이 대표가 됩니다
운영 실측(2026-08-26) 기준 3,261행이 1,994명으로 묶입니다. 한 사람이 최대 13행까지 묶인 사례가 있습니다.
크롤 지표 매칭
sns_account_id는 폼 입력 원본이라 전체 URL · @핸들 · 맨 핸들 · 스킴 없는 호스트가 섞여 있습니다. 이 API는 저장값에서 핸들을 뽑아 표준 프로필 URL로 맞춘 뒤 PostgreSQL influencer_profile.account_link를 조회합니다.
| 매칭 방식 | 인스타 기준 매칭률 |
|---|---|
| 저장값 그대로 | 444 / 1,910 (23%) |
| 정규화 후 표준 URL 2변형 | 1,644 / 1,910 (86%) |
크롤 데이터가 없으면 crawled가 false이고 지표 필드는 전부 null입니다.
틱톡은 매칭률이 낮습니다. 크롤 수집 트리거가 뒤늦게 붙어서 그 이전 등록 건에는 PostgreSQL 데이터 자체가 없습니다.
평균 지표의 기준은 플랫폼마다 다릅니다
avgViews · avgLikes · avgComments 는 플랫폼에 맞는 컬럼에서 골라 채웁니다. 어느 쪽에서 왔는지는 metricBasis 로 알 수 있습니다.
| 플랫폼 | metricBasis | 읽는 컬럼 |
|---|---|---|
| 인스타그램 · 유튜브 | REELS | reels_avg_views · reels_avg_likes · reels_avg_comments |
| 틱톡 · 네이버 | FLAT | avg_views · avg_likes · avg_comments |
틱톡과 네이버는 "릴스"에 대응하는 콘텐츠 구분이 없어 크롤러가 reels_avg_* 를 채우지 않습니다. 운영 실측(2026-08-27) 기준 reels_avg_views 가 채워진 행은 인스타 107,663건인 반면 틱톡·네이버는 0건입니다. 그래서 프론트에서 플랫폼 구분 없이 릴스 필드를 읽으면 틱톡·네이버 항목이 통째로 빈칸이 됩니다.
rankingPrice 는 릴스 조회수로 산출되므로 틱톡·네이버는 항상 null 입니다. 값이 없는 것이지 0원이 아니며, 화면에서 0 이나 최저가로 표시하면 안 됩니다.
응답
성공 응답 (200 OK)
{
"status": 200,
"code": null,
"message": "희망자풀 조회 성공",
"data": {
"items": [
{
"id": 3260,
"applicantName": "김글로브",
"contactEmail": "creator@example.com",
"notificationPhone": "010-1234-5678",
"snsType": "INSTAGRAM",
"snsAccountId": "https://www.instagram.com/berryzzin?igsh=abc",
"snsAccountLink": "https://www.instagram.com/berryzzin",
"categories": "FASHION,BEAUTY",
"defaultUnitPrice": "300000",
"createdAt": "2026-08-26T12:14:16",
"crawled": true,
"followers": 12345,
"avgViews": 15200.0,
"avgLikes": 610.0,
"avgComments": 44.0,
"metricBasis": "REELS",
"adjustedEngagementRate": 3.2,
"rankingPrice": 220000,
"duplicateCount": 3,
"duplicateIds": [2841, 1502]
}
],
"page": 0,
"size": 20,
"totalElements": 1994,
"totalPages": 100,
"totalRowsBeforeDedup": 3264,
"sort": "CREATED_AT",
"direction": "desc",
"summary": {
"totalPeople": 1994,
"totalRows": 3264,
"bySnsType": {
"FACEBOOK": 0,
"INSTAGRAM": 1740,
"YOUTUBE": 82,
"TIKTOK": 55,
"NAVER": 117
},
"crawled": 1663,
"uncrawled": 331
}
}
}항목 필드
| 필드 | 타입 | 설명 |
|---|---|---|
id | Long | 대표 행 ID (중복 그룹 중 가장 최근 등록 건) |
applicantName | String | 신청자명 |
contactEmail | String | 협업 연락용 이메일 |
notificationPhone | String | 협업 알림용 전화번호 |
snsType | String | SNS 타입 |
snsAccountId | String | 저장된 원본 입력값 |
snsAccountLink | String | 정규화된 프로필 링크 |
categories | String | 협업 카테고리 (대문자 CSV) |
defaultUnitPrice | String | 평소 진행 단가 (신청자 입력값) |
createdAt | DateTime | 대표 행 등록일시 |
crawled | Boolean | 크롤 데이터 매칭 여부. false면 아래 지표가 전부 null |
followers | Integer | 팔로워 수 |
avgViews · avgLikes · avgComments | Double | 평균 조회수 · 좋아요 · 댓글. 기준은 metricBasis 참고 |
metricBasis | String | REELS(인스타·유튜브) 또는 FLAT(틱톡·네이버) |
adjustedEngagementRate | Double | 보정 참여율 |
rankingPrice | Integer | 랭킹가(원). 틱톡·네이버는 항상 null |
duplicateCount | Integer | 묶인 등록 건수 (본인 포함). 1이면 중복 없음 |
duplicateIds | Long[] | 묶인 나머지 행 ID (최신순) |
페이지 필드
| 필드 | 타입 | 설명 |
|---|---|---|
page · size | Integer | 요청한 페이지 · 크기 |
totalElements | Long | 필터·중복 제거 후 인원 수 |
totalPages | Integer | 총 페이지 수 |
totalRowsBeforeDedup | Long | 중복 제거 전 원본 행 수 (필터 적용 후) |
sort · direction | String | 실제로 적용된 정렬 |
summary — 필터와 무관한 전체 기준
필터 칩에 붙일 숫자라 필터를 걸어도 바뀌지 않습니다. 필터 적용 후 숫자가 필요하면 totalElements 를 쓰세요.
| 필드 | 타입 | 설명 |
|---|---|---|
totalPeople | Long | 중복 제거 후 전체 인원 |
totalRows | Long | 중복 제거 전 전체 행 수 |
bySnsType | Object | SNS 타입별 인원. 0명인 타입도 키가 있습니다 |
crawled · uncrawled | Long | 크롤 지표 보유 여부별 인원 |
에러 응답
| 상태 코드 | 설명 |
|---|---|
401 | 인증 실패 |
403 | ADMIN 권한 없음 |
성능
전량을 메모리로 올려 중복을 묶은 뒤 필터·정렬·페이징합니다. 중복 판정 키가 저장된 sns_account_id가 아니라 거기서 뽑아낸 정규화 핸들이라 SQL로 접을 수 없기 때문입니다.
PostgreSQL 프로필도 전량 조회합니다 — summary의 크롤 인원 집계와 지표 정렬·필터가 모두 페이지 밖 사람들의 값을 필요로 합니다. 운영 실측(2026-08-27) 기준 4,506키에 217ms 이고, 키가 이만큼 많으면 플래너가 인덱스 대신 Seq Scan을 고릅니다. unnest + JOIN 으로 바꿔 인덱스를 강제하면 405ms로 오히려 느려져 그대로 두었습니다.
랭킹가는 sort=RANKING_PRICE 일 때만 전량 조회하고, 그 외에는 현재 페이지만 조회합니다.
현재 3천 행 규모라 문제없지만, 수만 행으로 늘면 정규화 핸들을 컬럼으로 저장하고 SQL 페이징으로 바꿔야 합니다.