POST /ai/admin/campaigns/{campaignNo}/recruitment/reopen
캠페인 모집 마감 취소 (관리자 전용)
모집 마감 취소
마감된 모집을 다시 열고 새 모집 종료일을 지정합니다.
본 API는 관리자 전용입니다. 모집 이후 단계(선정·계약·제작) 일정은 재계산하지 않습니다. 마감을 되돌리는 것과 전체 일정을 미루는 것은 별개 결정이기 때문입니다.
HTTP 요청
POST /ai/admin/campaigns/{campaignNo}/recruitment/reopen
Authorization: Bearer {access_token}
Content-Type: application/json{
"recruitmentEndDate": "2026-08-25T23:59:59"
}Path Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
campaignNo | Integer | 예 | 캠페인 번호 |
Request Body
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
recruitmentEndDate | LocalDateTime | 예 | 새 모집 종료일. 현재 시각 이후여야 합니다. |
새 종료일을 서버가 정하지 않는 이유는, 언제까지 다시 열지가 운영 판단이기 때문입니다. 모집 종료 API가 시각을 받지 않는 것과 대비됩니다.
처리 흐름
- 입력 검증 —
recruitmentEndDate가 없거나 과거이면 캠페인을 조회하기도 전에 거부 - 캠페인 조회 — 없으면
CAMPAIGN_NOT_FOUND - 모집 종료일 갱신 —
TB_COLLAB.recruitment_end_date = 지정한 시각 - 모집 단계 일정 갱신 —
TB_CAMPAIGN_SCHEDULE의phase = RECRUITMENT행만end_date = 지정한 시각
original_end_date는 변경하지 않습니다. 지연 일수 계산의 기준값이기 때문입니다. 모집 종료 API와 동일한 규칙입니다.
아직 모집 중인 캠페인에 호출하면 종료일 이동(연장/단축)으로 동작하고 wasClosed가 false가 됩니다.
응답
성공 응답 (200 OK)
{
"status": 200,
"code": null,
"message": "모집 마감이 취소되었습니다.",
"data": {
"campaignNo": 2485,
"recruitmentEndDate": "2026-08-25T23:59:59",
"previousRecruitmentEndDate": "2026-08-18T14:32:10",
"wasClosed": true
}
}wasClosed가 false면 메시지는 "모집 종료일이 변경되었습니다."로 내려갑니다.
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
campaignNo | Integer | 캠페인 번호 |
recruitmentEndDate | LocalDateTime | 새로 적용된 모집 종료일 |
previousRecruitmentEndDate | LocalDateTime | 변경 전 모집 종료일. 값이 없던 캠페인이면 null |
wasClosed | Boolean | 호출 시점에 마감 상태였는지 여부 |
에러 응답
| 상태 코드 | code | 사례 |
|---|---|---|
400 | RECRUITMENT_END_DATE_INVALID | 새 종료일 누락 또는 과거 시각 |
404 | CAMPAIGN_NOT_FOUND | 캠페인 없음 |
401 | — | 인증 실패 |
전원 매칭 시 자동 모집 종료
모집은 어드민이 닫지 않아도 선정 인원 전원이 계약 서명을 마치면 자동으로 마감됩니다.
판정 기준
기존 CampaignCompletionNotifier.isAllSelectedSigned를 그대로 사용합니다.
- 서명 대기 중인 제안(
PROPOSAL) 인원이 0명 - 선정(
SELECTED) 인원이 1명 이상 - 선정 인원 전원의 최신 계약 상태가
SIGNED
모집인원(TB_COLLAB.person)은 자유텍스트(VARCHAR)라 정원 기반 판정을 쓸 수 없습니다. ReserveCascadeService도 같은 이유로 예비 승계에서 정원 기준을 배제했습니다. 이 판정은 정원을 보지 않고 "제안한 사람이 남김없이 서명했는가"만 보므로 파싱 함정이 없습니다.
트리거 지점
ReserveReleaseListener가 ReserveReleaseCheckEvent를 AFTER_COMMIT에 받는 지점입니다. 이 이벤트는 전원 서명이 완성될 수 있는 네 경로 모두가 발행합니다.
| 발행 지점 | 경로 |
|---|---|
ContractService | 마지막 크리에이터의 계약 서명 |
ProgressTableService | 광고주의 선정 인원 제외 |
AdminDashboardService | 어드민의 선정 인원 제외 |
CampaignApplicationService | 크리에이터 자진 이탈 |
같은 자리에서 남은 예비(RESERVED)를 대기(WAITING)로 되돌리는 처리가 이미 일어납니다. 예비를 푸는 것과 모집을 닫는 것은 같은 판정을 공유하는 짝입니다 — 예비가 풀리고 나면 이후 제외가 발생해도 자동 충원이 없으므로, 그 시점부터 모집을 열어둘 이유가 없습니다.
자동 종료가 일어나지 않는 경우
- 크레딧 부족으로 예비 승계가 남아 있는 경우(
creditBlocked) — 입금하면 승계가 재개되므로 모집을 닫지 않습니다. - 이미 마감된 캠페인 — 종료일을 다시 밀지 않습니다.
- 전원 서명 전 — 서명 대기 인원이 하나라도 있으면 닫지 않습니다.
자동 종료 이후 계약 후 제외가 발생하면 예비가 이미 해제된 상태라 자동 충원 수단이 없습니다. 이때 인원을 더 받으려면 본 마감 취소 API로 모집을 다시 열어야 합니다.
자동 종료 실패는 로그만 남기고 본 흐름(서명·제외)으로 예외를 전파하지 않습니다.