데모(목업) 후보 — 프론트 연동 가이드
모집 에이전트가 발굴한 비가입자를 리스트 관리·광고주 대시보드에 노출하는 dummyItems 연동
데모(목업) 후보 — 프론트 연동 가이드
모집 에이전트가 인스타그램에서 발굴한 비가입자 크리에이터를 캠페인 리스트에 목업으로 올리는 기능입니다. 어드민에서는 데모임을 표기하고, 광고주 화면에서는 표기 없이 일반 신청자와 똑같이 보여야 합니다.
광고주 화면에 데모 표기 금지. 광고주 대시보드에서 dummyItems는 items와 구분 없이 자연스럽게
렌더해야 합니다. 뱃지·툴팁·회색 처리 등 데모임을 드러내는 표기를 넣지 마세요. 어드민에서만 표기합니다.
왜 별도 배열인가
데모 후보는 실제 신청(TB_CAMPAIGN_APPLICATION)이 아니라 별도 테이블(TB_CAMPAIGN_DEMO_APPLICANT)에
있습니다. 우리 회원이 아니라 influence_id(NOT NULL FK)를 채울 수 없기 때문입니다.
그 결과 신청 테이블에는 이 후보의 행이 아예 없습니다. 계약·정산은 모두 신청 행을 기준으로 동작하므로, 이 후보들을 구조적으로 집을 수 없습니다 — 막는 코드가 따로 있는 게 아니라, 집을 대상이 존재하지 않습니다.
선정/예비/제외
데모도 선정·예비·제외 상태를 가질 수 있습니다. 실제 신청자와 같은 API 를 씁니다 — 프런트가 데모와
신청을 나눠 호출하지 않습니다. itemIds 에 실제 신청 ID 와 데모 ID(1억 오프셋)를 섞어 보내도 됩니다
— 서버가 오프셋을 보고 갈라 각각 처리합니다.
| 하는 일 | 엔드포인트 |
|---|---|
| 선정 | POST /ai/progress-table/items/bulk/proposal-with-charge |
| 예비·제외·대기 | PUT /ai/progress-table/items/bulk/user/matching-status (reserved/eliminated/waiting) |
| 단건 변경 | PUT /ai/progress-table/items/{id}/user/matching-status (waiting/matched/reject 만) |
POST /ai/progress-table/items/bulk/proposal-with-charge
{ "collabId": 3126, "itemIds": [18539, 100000012] }선정은 크레딧이 움직이는 경로입니다. proposal-with-charge 는 예산 부족 시 자동 충전
(forceCharge 기본 true)까지 합니다. 데모는 이 계산에서 완전히 빠집니다 — 선정 표시만 하고
크레딧은 1원도 건드리지 않습니다. 데모만 보내면 "데모 후보 N건 선정 처리했습니다. (크레딧 변동 없음)"
으로 응답합니다.
선정 상태는 표시용입니다. 데모도 선정/예비/제외 상태를 가질 수 있지만(selectionStatus),
실제 신청과 달리 부수효과가 없습니다. 실제 신청은 SELECTED 가 되면 매칭·배송 테이블이 생기고
ELIMINATED 면 예산이 UNLOCK 되는데, 데모는 그 어느 것도 일으키지 않습니다 — 목업이 계약·배송·예산을
건드리면 안 되기 때문입니다. 즉 "화면에 그렇게 보인다"는 뜻일 뿐입니다.
응답의 dummyItems[].applicationId는 렌더 편의를 위해 만들어 넣은 값이지 실제 신청 ID가 아닙니다.
아래 ID 규칙을 참고하세요.
ID 규칙 — 신청 ID와 겹치지 않는다
dummyItems는 items와 동일한 구조라 같은 컴포넌트로 그대로 렌더할 수 있습니다.
ID는 1억(100,000,000) 오프셋을 더해 내려가므로 실제 신청 ID와 겹치지 않습니다.
한 목록에 이어 붙여도 key 충돌이 없습니다. (신청 ID는 현재 1만대)
어드민 dummyItems[].applicationId | 광고주 dummyItems[].id | |
|---|---|---|
| 값 | 100000000 + 데모 PK | 동일 |
| 판별 | id >= 100000000 이면 데모 | 동일 |
데모 ID를 신청 상세 조회 API에 쓰지 마세요. 신청 ID가 아니라 데모 테이블 PK에 오프셋을 더한
값이라, 개별 신청을 조회하는 API(GET .../applications/{id} 등)에 넣으면 404가 납니다.
단, 데모를 지원하도록 만든 API에는 받은 값을 그대로 넘기면 됩니다 (서버가 오프셋을 해석):
삭제·노출, 벌크 수정, 그리고 아래 선정/예비/제외(진행테이블 matching-status·proposal-with-charge)까지.
계약·검수는 데모에 존재하지 않습니다.
1. 어드민 — 리스트 관리 응답
GET /ai/admin/dashboard/{campaignNo}/list 응답에 dummyItems가 추가됩니다.
items와 동일한 ListItemDto 구조라 기존 행 컴포넌트를 그대로 재사용할 수 있습니다.
{
"campaign": { "...": "..." },
"items": [ { "applicationId": 18539, "influenceName": "홍길동", "...": "..." } ],
"dummyItems": [
{
"applicationId": 100000012,
"appliedAt": "2026-07-15T18:20:11",
"adminVisible": false,
"sourceType": "AI_RECOMMENDED",
"influenceNo": null,
"influenceName": "creator_jp_1",
"influenceProfileImage": null,
"influenceTel": null,
"influenceEmail": null,
"influenceRankScore": "300000",
"followerCount": "12400",
"averageViewCount": "8300",
"adjustedEngagementRate": 3.4,
"autoCategories": ["뷰티", "라이프스타일"],
"recommendReason": "뷰티 카테고리 릴스 비중이 높고 ...",
"flags": [],
"globalFlags": [],
"campaignFlags": [],
"currentPrice": 250000,
"quotePrice": 300000,
"matchScore": null,
"latestNegotiationId": null,
"contractWritten": null,
"defaultUnitPrice": null,
"previouslyCollaborated": false,
"previouslyExposed": true,
"previouslyListed": true,
"recentPosts": []
}
],
"remainBusinessCredit": 1200000,
"veteranPercentage": 40.0
}데모 행에서 비는 값 — 비가입자라 우리 DB에 없는 정보입니다.
| 필드 | 값 | 이유 |
|---|---|---|
applicationId | 100000000 + 데모 PK | 실제 신청이 아님. 삭제·노출 API에만 사용 |
influenceNo | null | 회원이 아님 |
influenceName | 인스타 username | 실명을 모름 |
snsUrl | 인스타 프로필 링크 | 데모 행에 저장된 account_link. 진행 API 병합에 의존하지 않는다 |
influenceTel, influenceEmail | null | 연락처를 모름 |
influenceRankScore | 예상가(원) 문자열 | 에이전트 산정값. 불가 시 null |
currentPrice, quotePrice, defaultUnitPrice, latestProposedPrice | 어드민이 입력한 값 | 초기엔 null. 가격 벌크로 4종 모두 수정 가능 |
recommendReason | 에이전트 LLM 초안 | 어드민이 벌크 수정으로 다듬는다 |
matchScore | null | 매칭 점수는 신청에만 있음 |
latestNegotiationStatus 등 협상 상태·계약 필드 | null | 데모는 협상·계약이 존재할 수 없다. 제안가(latestProposedPrice)는 협상 이력 없이 값만 보관한다 |
recentPosts | 최근 게시물 최대 3개 | 크롤 DB(PG public.post)를 account_id로 조회해 채운다. 실제 신청자와 동일 |
flags | 비고 칩 합본 | 아래 두 배열을 저장 순서대로 합친 값. 초기엔 빈 배열 |
globalFlags, campaignFlags | 범위별 비고 칩 | 리스트 벌크의 globalFlags/campaignFlags 로 편집. 실제 신청자와 같은 모양 |
sourceType | "AI_RECOMMENDED" | 에이전트 발굴 |
previouslyCollaborated, previouslyExposed, previouslyListed | 기협업/기노출/기리스트 여부 | 회원번호가 없어 SNS 링크(snsUrl)로 판별. 아래 Callout 참고 |
신청이 하나도 없는 캠페인(items: [])에서도 dummyItems는 정상 반환됩니다.
데모 행의 기협업/기노출/기리스트 판별
- 실제 신청자와 같은 scope(에이전시 광고주면 에이전시 전 계정)의 다른 캠페인을 대상으로 합니다.
- 데모는
influenceNo가 없어서snsUrl을 정규 프로필 URL로 맞춰 비교합니다(뒤 슬래시·?igsh=쿼리·대소문자 차이 무시). - 기협업: 다른 캠페인의 실제 신청이
SELECTED였던 적 있음. 데모 행의SELECTED는 화면 표시용이라 세지 않습니다. - 기노출: 실제 신청 또는 이전 데모 행이
adminVisible=true였던 적 있음 - 기리스트: 실제 신청 또는 이전 데모 행으로 올라온 적 있음(상태 무관)
snsUrl이 없는 데모 행은 세 값 모두false입니다.- 칩 상세 API(
previous-campaigns)는influenceNo기준이라 데모 행에서는 쓸 수 없습니다.
2. 기업(광고주) — 캠페인 대시보드 응답
GET /ai/progress-table/item/{id} 응답에 dummyItems가 추가됩니다.
items와 완전히 동일한 shape이라 같은 컴포넌트로 그대로 렌더할 수 있습니다.
{
"step": "MATCHING",
"items": [ { "id": 18539, "creator": "홍길동", "matchingStatus": "waiting", "...": "..." } ],
"dummyItems": [
{
"id": 100000012,
"creator": "creator_jp_1",
"profileImg": null,
"creatorEmail": null,
"influenceId": null,
"creatorLink": "https://www.instagram.com/creator_jp_1/",
"matchingStatus": "waiting",
"flags": [],
"followerCount": 12400,
"avgViewCount": 8300,
"adjustedEngagementRate": 3.4,
"recommendReason": "뷰티 카테고리 릴스 비중이 높고 ...",
"rankPrice": 300000,
"quotePrice": 300000,
"currentPrice": 250000,
"recentPosts": [],
"trackingNumber": null,
"deliveryStatus": null,
"aiCheck": null,
"personCheck": null,
"uploadStatus": null
}
]
}렌더 규칙
items와 이어 붙여 하나의 목록으로 보여줍니다. 순서는 프런트 판단(뒤에 붙이는 것을 권장).- 데모임을 드러내는 표기를 넣지 않습니다.
- 진행 상태 필드(
deliveryStatus,aiCheck,personCheck,uploadStatus, 배송 관련)는 모두null입니다. 실제 신청이 아니므로 계약·배송·검수가 없습니다. 기존 항목의 "아직 진행 전" 표시와 동일하게 처리하세요. matchingStatus는 어드민이 정한 선정 상태를 실제 신청과 같은 규칙으로 매핑한 값입니다(SELECTED→matched등). 상태를 안 정했으면"waiting".creatorEmail,influenceId는null입니다. 이 값에 의존하는 로직이 있다면 방어가 필요합니다.followerCount,avgViewCount,adjustedEngagementRate,recommendReason,rankPrice,quotePrice,currentPrice,recentPosts는 실제 신청과 동일하게 채워집니다. 데모라고 비어 있지 않습니다.adjustedEngagementRate(보정 참여율)는 어드민이 수기 등록에서 직접 넣은 값이 있으면 그 값을, 없으면 크롤 DB 프로필에서 조회한 값을 씁니다. 수기 후보는 크롤 DB에 대응 계정이 없어 직접 넣지 않으면 비어 있습니다.recentPosts는 크롤 DB에서 가져오므로 크롤 이력이 없는 계정은 빈 배열일 수 있습니다. 수기 등록 후보는 크롤 대상이 아니라 등록 시 직접 넣은 값이 쓰입니다(수기 입력분이 크롤 조회보다 우선).profileImg는 어드민이 넣지 않으면null입니다 — 비회원이라 자동으로 가져올 원천이 없습니다 (실제 신청자는 회원 프로필에서 옵니다). 넣으려면 업로드 URL 발급으로 올린 뒤 수기 등록의profileImage에 담거나, 등록 후 리스트 벌크의profileImage로 수정하세요. 비어 있으면 기본 아바타로 처리합니다.
adminVisible = true인 데모만 내려갑니다. 결제미완(PAYMENT_PENDING) 캠페인과
신청자 숨김 모드에서는 items와 마찬가지로 dummyItems도 빈 배열입니다.
레거시(구사스) 캠페인(isLegacy: true)도 동일하게 내려갑니다. 캠페인 종류에 따라 응답 형태가
달라지지 않으므로 프런트는 isLegacy를 따질 필요가 없습니다. dummyItems는 항상 존재하고,
담긴 데모가 없으면 빈 배열입니다.
3. 데모 후보 관리 API
엔드포인트 상세는 각 문서를 참고하세요.
| 하는 일 | 문서 |
|---|---|
| 후보 모달에서 고른 비가입자를 데모로 담기 | POST .../demo-applicants |
| 인스타·틱톡 링크로 벌크 등록 + 자동 채움 | POST .../demo-applicants/bulk-links |
| 광고주 노출 on/off | PATCH .../demo-applicants/{demoId}/visible |
| 데모 후보 삭제 | DELETE .../demo-applicants/{demoId} |
삭제·노출 API 의 {demoId} 에는 dummyItems[].applicationId 를 그대로 넣습니다 — 오프셋은 서버가
해석하므로 프런트에서 뺄셈하지 마세요.
4. 데모 후보 수정 — 기존 벌크 API 를 그대로 씁니다
신규 엔드포인트가 없습니다. 프런트가 신청과 데모를 나눠 호출하지 않도록, 지금 쓰는 벌크 API 에 데모 ID 를 그대로 넣으면 서버가 오프셋을 보고 알아서 데모 테이블로 보냅니다.
| 기존 API | 데모에 적용되는 항목 |
|---|---|
PATCH .../applications/prices/bulk | 가격 4종 — 희망가 defaultUnitPrice, 제안가 proposedPrice, 기준가 quotePrice, 노출가 currentPrice |
PATCH .../{campaignNo}/list | 수기 등록으로 넣은 값 전부 — 이름 username, 링크 accountLink, 프로필이미지 profileImage, 팔로워 followerCount, 조회수 reelsAvgViews, 참여율 adjustedEngagementRate, 적합도 matchScore, fitScore, 예상가 estimatedPrice, 노출가 currentPrice, 카테고리 categoryTags, 추천사 recommendReason, 비고칩 globalFlags·campaignFlags, 최근게시물 recentPosts, 노출 adminVisible |
{
"items": [
{ "applicationId": 18539, "currentPrice": 200000 },
{ "applicationId": 100000012, "defaultUnitPrice": 350000, "proposedPrice": 320000, "quotePrice": 300000, "currentPrice": 250000 }
]
}위 요청에서 18539 는 실제 신청으로, 100000012 는 데모로 갑니다. 프런트는 구분 없이 한 번에 보내면 됩니다.
데모에 없는 항목(계약서·지불방식·유저풀 추가·선정 상태)은 조용히 무시됩니다. 데모는 계약·정산이 존재하지 않기 때문입니다. 가격·노출·추천사만 반영됩니다.
수기 등록으로 넣을 수 있는 값은 전부 리스트 벌크로 고칠 수 있습니다. 오타 하나 때문에 지우고 다시 만들 필요가 없습니다. 단 가격 4종 중 희망가·제안가·기준가는 가격 벌크 쪽입니다(위 표).
categoryTags·recentPosts 는 배열 전체 교체입니다 — 개별 항목에 id 가 없습니다.
비고 칩은 아래 범위별 규약을 따릅니다.
추천사(recommendReason)는 리스트 관리 벌크에만 있습니다. 가격 벌크(prices/bulk)에는 추천사
필드가 없으므로, 추천사를 고치려면 PATCH .../{campaignNo}/list 를 쓰세요.
값이 없던 후보에 새로 넣는 것도 같은 방식입니다.
비고 칩 — 인플루언서(전역)/캠페인 범위
데모 후보도 실제 신청자와 같은 두 칸(인플루언서 비고 / 캠페인 비고)으로 비고를 관리합니다.
응답은 flags(합본)·globalFlags·campaignFlags 세 배열이 모두 나가고, 항목 모양은 실제 신청자와 같은
InfluenceFlagDto 라 같은 컴포넌트로 렌더할 수 있습니다.
편집은 리스트 관리 벌크로만 합니다. 비고 전용 API(/ai/admin/creator-flags,
/ai/admin/campaign-flags)는 influenceNo 로 동작하는데 데모 후보는 그 값이 null 입니다.
{
"items": [
{
"applicationId": 100000012,
"campaignFlags": [{ "text": "3월 촬영 가능", "category": "NEUTRAL" }]
}
]
}- 보낸 범위만 교체하고, 생략(
null)한 범위는 기존 값을 그대로 둡니다. 위 예시처럼 캠페인 비고만 저장해도 전역 비고는 남습니다. - 빈 배열
[]은 그 범위만 전부 삭제합니다. - 한 범위 안에서는 여전히 배열 전체 교체입니다 — 데모 칩은 데모 행의 JSON 이라 개별
id가 없어, 원하는 최종 상태의 배열을 보내야 합니다. - 칩에
scope를 넣지 않아도 어느 필드로 보냈는지에 따라 서버가 범위를 기록합니다. - 응답 칩은 항상
source: "ADMIN",editable: true입니다. 데모에는 자동 라벨링·블랙리스트가 없습니다.
추천사(recommendReason)와 비고 칩은 서로 다른 것이니 혼동하지 마세요.
데모의 전역 비고는 그 데모 행에만 저장됩니다. 실제 크리에이터의 전역 비고는 캠페인을 넘어
따라다니지만, 데모는 비회원이라 TB_INFLUENCE_FLAG 행을 만들 수 없어 데모 행의 JSON 에 담깁니다.
같은 계정을 두 캠페인에 데모로 담으면 비고는 각각 관리됩니다 — 화면 구성만 같고 저장 위치가 다릅니다.
구 방식(flags 배열 통째 교체)도 계속 동작합니다. 범위 구분이 생기기 전에 저장된 칩과
flags 로 보낸 칩은 캠페인 비고로 취급됩니다. 새 화면은 globalFlags/campaignFlags 를 쓰세요.
알려진 제약
일본 캠페인에 한국 크리에이터가 섞일 수 있습니다. 모집 에이전트 파이프라인에 nation 필터가 미구현이라 수집 단계에서 국가가 걸러지지 않습니다. 백엔드에서 막을 수 없으므로, 어드민이 데모로 담을 때 눈으로 확인해야 합니다.