POST /ai/business/contents/review/{reviewId}/feedbacks/bulk
벌크 피드백 추가 / 검수 완료
벌크 피드백 추가 / 검수 완료
검수 라운드의 모든 아이템에 대해 피드백 추가 또는 검수 완료를 일괄 처리합니다.
기존 개별 API의 문제점 해결
기존 개별 API(POST /{itemId}/feedbacks)는 아이템마다 검수 횟수(count)가 증가하여, 여러 아이템을 순차적으로 처리할 때 "검수 횟수 초과" 오류가 발생할 수 있습니다.
벌크 API는 모든 아이템을 한 번에 처리하고, 검수 횟수는 라운드 단위로 1번만 증가합니다.
처리 규칙
- 각 아이템별로
feedback이 null이고 파일이 없으면 -> 승인 (APPROVED) feedback내용이나 파일이 있으면 -> 재제출 요청 (REJECTED)approveWithFeedback: true시 -> 피드백을 생성하되 즉시 해결(resolved) 처리하고 승인 (APPROVED)isDraft: true시 -> 피드백 + 파일만 저장, 상태 변경/검수 횟수 증가/슬랙 알림 없음 (임시저장)- 검수 횟수(count)는 전체 처리 후 1번만 증가
- 피드백 마감일은 가장 마지막 피드백 기준으로 4일 후 설정 (
approveWithFeedback시에는 설정하지 않음)
임시저장 (isDraft: true) 동작 방식
- 피드백 내용과 첨부 파일(S3 업로드)만 저장하고, 제출물 상태 변경/검수 횟수 증가/슬랙 알림을 수행하지 않습니다.
- 같은 검수 라운드에 대해 임시저장을 다시 호출하면 기존 임시저장 피드백이 삭제되고 새로 저장됩니다.
- 정식 제출(
isDraft: false/null)하면 기존 임시저장 피드백이 자동 삭제된 후 정식 처리됩니다. guidelineFeedbackItems도 함께 임시저장됩니다.- 조회 시
isDraft: true인 피드백은 프론트에서 "임시저장된 피드백"으로 구분 표시할 수 있습니다.
HTTP 요청
POST /ai/business/contents/review/{reviewId}/feedbacks/bulk
Authorization: Bearer {access_token}
Content-Type: multipart/form-dataPath Parameters
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
reviewId | long | 예 | 검수 라운드 ID |
Request Parts
| 파트 | 타입 | 필수 | 설명 |
|---|---|---|---|
request | BulkFeedbackDto.Request | 예 | 벌크 피드백 요청 JSON |
files | file[] | 아니오 | 첨부 파일 (모든 아이템 파일을 배열로 전달) |
Request 필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
items | ItemFeedback[] | 예 | 아이템별 피드백 목록 |
approveWithFeedback | boolean | 아니오 | true: 피드백을 생성하되 즉시 해결 처리하고 승인. false/null: 기존 동작 (피드백 있으면 반려) |
isDraft | boolean | 아니오 | true: 임시저장 (피드백만 저장, 상태 변경/검수 횟수 증가/슬랙 알림 없음). false/null: 기존 동작 |
guidelineFeedbackItems | GuidelineFeedbackItem[] | 아니오 | 가이드라인 피드백 목록 (아래 상세 참조) |
GuidelineFeedbackItem 필드 설명
가이드라인 피드백은 3가지 종류(feedbackType)로 구분됩니다:
| feedbackType | 설명 | category 예시 | item 예시 |
|---|---|---|---|
GUIDELINE_UNREFLECTED | 가이드라인에 있는 항목인데 반영 안 됨 | "필수 촬영 항목", "필수 소구 포인트" | "제형 표현 샷", "발색 샷" |
GUIDELINE_EXTRA | 가이드라인 외 추가 피드백 항목 | "자막", "녹음", "편집효과" | null (보통 사용 안 함) |
자유 피드백(FREE)은 기존 items[]로 전달
guidelineFeedbackItems에는 GUIDELINE_UNREFLECTED 또는 GUIDELINE_EXTRA만 넣으세요.
자유 피드백은 기존처럼 items[]의 feedback 필드를 사용합니다.
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
feedbackType | string | 예 | 피드백 종류: GUIDELINE_UNREFLECTED 또는 GUIDELINE_EXTRA |
category | string | 아니오 | 가이드라인 섹션명 또는 추가항목명. 프론트에서 가이드라인 데이터를 보고 동적으로 설정 |
item | string | 아니오 | 가이드라인 항목명 (GUIDELINE_UNREFLECTED일 때 사용) |
comment | string | 아니오 | 피드백 텍스트 설명 |
가이드라인 피드백 저장 방식
- 가이드라인 피드백은 기존
ContentFeedback과 동일한 테이블에 저장됩니다. - VIDEO 타입 제출물에 자동 바인딩됩니다 (VIDEO가 없으면 첫 번째 제출물).
category와item값은 프론트에서 가이드라인 데이터를 보고 동적으로 설정하면 됩니다. 백엔드는 String 그대로 저장만 합니다.- 가이드라인 버전이 바뀌어도 백엔드 수정이 필요 없습니다.
request JSON 구조
기본 사용 (기존 동작)
{
"items": [
{
"itemId": 244,
"feedback": null,
"highlightedText": null,
"selection": null,
"fileIndices": [],
"mediaList": []
},
{
"itemId": 245,
"feedback": null,
"highlightedText": null,
"selection": null,
"fileIndices": [],
"mediaList": []
},
{
"itemId": 246,
"feedback": "피드백 내용입니다. 이 부분을 수정해주세요.",
"highlightedText": "수정 필요한 텍스트",
"selection": "{\"anchor\":{},\"focus\":{}}",
"fileIndices": [0, 1],
"mediaList": [
{
"mediaType": "IMAGE",
"startTime": null,
"endTime": null,
"comment": "이 부분 참고"
},
{
"mediaType": "VIDEO",
"startTime": 10.5,
"endTime": 15.0,
"comment": "이 구간 재촬영 필요"
}
]
}
]
}가이드라인 피드백 포함
기존 아이템별 피드백(items)과 함께, 가이드라인 관련 피드백(guidelineFeedbackItems)을 같이 보낼 수 있습니다.
가이드라인 피드백은 VIDEO 제출물에 자동으로 바인딩됩니다.
{
"items": [
{
"itemId": 244,
"feedback": null,
"fileIndices": [],
"mediaList": []
}
],
"guidelineFeedbackItems": [
{
"feedbackType": "GUIDELINE_UNREFLECTED",
"category": "필수 촬영 항목",
"item": "제형 표현 샷",
"comment": "제형 표현이 충분히 보이지 않습니다."
},
{
"feedbackType": "GUIDELINE_UNREFLECTED",
"category": "필수 소구 포인트",
"item": "수분 공급 효과",
"comment": null
},
{
"feedbackType": "GUIDELINE_EXTRA",
"category": "자막",
"item": null,
"comment": "자막 크기가 너무 작습니다."
}
]
}피드백 포함 승인 (approveWithFeedback: true)
피드백을 기록으로 남기되, 반려하지 않고 승인 처리합니다. 생성된 피드백은 즉시 resolved=true 상태로 저장됩니다.
{
"approveWithFeedback": true,
"items": [
{
"itemId": 244,
"feedback": null,
"highlightedText": null,
"selection": null,
"fileIndices": [],
"mediaList": []
},
{
"itemId": 245,
"feedback": "경미한 수정사항 있으나 승인 처리합니다.",
"highlightedText": null,
"selection": null,
"fileIndices": [],
"mediaList": []
}
]
}임시저장 (isDraft: true)
피드백을 저장만 하고 상태 변경 없이 나중에 이어서 작업할 수 있습니다. 동일 검수 라운드에 다시 임시저장하면 이전 임시저장이 교체됩니다.
{
"isDraft": true,
"items": [
{
"itemId": 244,
"feedback": null,
"fileIndices": [],
"mediaList": []
},
{
"itemId": 245,
"feedback": "작성 중인 피드백...",
"highlightedText": "수정 필요한 부분",
"selection": "{\"anchor\":{},\"focus\":{}}",
"fileIndices": [0],
"mediaList": [
{
"mediaType": "IMAGE",
"comment": "참고 이미지"
}
]
}
],
"guidelineFeedbackItems": [
{
"feedbackType": "GUIDELINE_UNREFLECTED",
"category": "필수 촬영 항목",
"item": "제형 표현 샷",
"comment": "작성 중..."
}
]
}ItemFeedback 필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
itemId | long | 예 | 제출물 아이템 ID |
feedback | string | 아니오 | 피드백 내용 (null이면 승인 처리) |
highlightedText | string | 아니오 | 하이라이트된 텍스트 |
selection | string | 아니오 | 선택 영역 정보 (JSON) |
fileIndices | int[] | 아니오 | files 배열에서의 인덱스 목록 |
mediaList | MediaMeta[] | 아니오 | 각 파일에 대한 메타데이터 |
MediaMeta 필드 설명
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
mediaType | string | 아니오 | 미디어 타입 (IMAGE, VIDEO) |
startTime | double | 아니오 | 영상 시작 시간 (초) |
endTime | double | 아니오 | 영상 끝 시간 (초) |
comment | string | 아니오 | 미디어에 대한 코멘트 |
응답
성공 응답 (200 OK)
{
"status": 200,
"code": null,
"message": "3건의 제출물이 승인되었습니다.",
"data": {
"success": true,
"approvedCount": 3,
"rejectedCount": 0,
"processedItemIds": [244, 245, 246],
"message": "3건의 제출물이 승인되었습니다."
}
}피드백 포함 응답 (기존 동작 - 반려)
{
"status": 200,
"code": null,
"message": "2건 승인, 1건 피드백 추가가 완료되었습니다.",
"data": {
"success": true,
"approvedCount": 2,
"rejectedCount": 1,
"processedItemIds": [244, 245, 246],
"message": "2건 승인, 1건 피드백 추가가 완료되었습니다."
}
}피드백 포함 승인 응답 (approveWithFeedback: true)
피드백이 있는 아이템도 승인으로 처리되므로 rejectedCount가 0이 됩니다.
{
"status": 200,
"code": null,
"message": "2건의 제출물이 승인되었습니다.",
"data": {
"success": true,
"approvedCount": 2,
"rejectedCount": 0,
"processedItemIds": [244, 245],
"message": "2건의 제출물이 승인되었습니다."
}
}임시저장 응답 (isDraft: true)
임시저장 시에는 approvedCount, rejectedCount가 모두 0입니다.
{
"status": 200,
"code": null,
"message": "2건 임시저장 완료",
"data": {
"success": true,
"approvedCount": 0,
"rejectedCount": 0,
"processedItemIds": [244, 245],
"message": "2건 임시저장 완료"
}
}에러 응답
에러 응답 (404) - 검수 횟수 초과
{
"status": 404,
"code": "INVALID_DATA",
"message": "검수 횟수를 초과했습니다. (현재: 1/1) 검수 추가 요청이 필요합니다.",
"data": null
}