[AI] 04_모델_API_설계 - 100-hours-a-week/9-team-Devths-WIKI GitHub Wiki

[AI] 04_모델_API_설계

AI Server 내부 API 명세서

프로젝트: Devths AI 취업 도우미 작성일: 2026-01-13 API 버전: v1.6 마지막 업데이트: 2026-01-26


개요

이 문서는 Backend(Spring Boot) → AI Server(FastAPI) 간의 내부 통신 API를 정의합니다.

[Frontend] → [Backend] → [AI Server] → [VectorDB/LLM]
                ↑
          이 구간의 API

Table of Contents

API 엔드포인트 목록

# 기능 Method Endpoint 처리방식
1 텍스트 추출 + 임베딩 POST /ai/text/extract 비동기
2 채팅 (대화/면접 질문/면접 리포트) POST /ai/chat 스트리밍 (SSE)
3 캘린더 일정 파싱 POST /ai/calendar/parse 동기
4 게시판 첨부파일 마스킹 POST /ai/masking/draft 비동기
5 비동기 작업 상태 조회 GET /ai/task/{task_id} 동기

공통 사항

인증

X-API-Key: your-api-key-here

Base URL

http://ai-server:8000

1. 텍스트 추출 + 임베딩

개요

이력서와 채용공고를 함께 받아 OCR 처리 후 임베딩 저장 및 분석 리포트를 생성합니다.

처리 흐름: resume OCR → job_posting OCR → VectorDB 저장 → 분석 리포트 생성 → 응답 응답 활용: Backend는 OCR 텍스트를 RDB에 저장하고, 분석 리포트를 채팅 메시지로 표시

Endpoint

POST /ai/text/extract

처리 방식

비동기 - task_id 반환 → 폴링 필요

Request Headers

Header 필수
X-API-Key your-api-key-here
Content-Type application/json

Request Body

{
    "model": "gemini",
    "user_id": 12,
    "resume": {
        "file_id": 23,
        "s3_key": "https://bucket.s3.amazonaws.com/users/12/resume/abc123.pdf",
        "file_type": "pdf",
        "text": null
    },
    "job_posting": {
        "file_id": null,
        "s3_key": null,
        "file_type": null,
        "text": "카카오 백엔드 개발자 채용\n자격요건: Java, Spring..."
    }
}

사용 예시:

  • 파일 업로드: s3_key + file_type 제공, textnull
  • 텍스트 입력: text 제공, s3_keyfile_typenull
  • 혼합: resume은 파일, job_posting은 텍스트 (또는 그 반대)

Request Fields

필드 타입 필수 설명
model string gemini(기본값), openai, vllm
user_id int 사용자 ID
resume object 이력서/포트폴리오 정보
job_posting object 채용공고 정보

⚠️ resumejob_posting은 필수 입력해야 합니다

Document Object (resume / job_posting)

필드 타입 필수 설명
file_id int | null 파일 ID (파일 업로드 시 사용, 선택사항)
s3_key string | null S3 파일 URL 또는 키 (파일 업로드 시 사용)
file_type string | null pdf, image (파일 업로드 시 사용)
text string | null 직접 입력 텍스트 (텍스트 입력 시 사용)

입력 규칙:

  • ⚠️ 각 문서에서 s3_key + file_type 또는 text 중 하나는 필수입니다
  • ⚠️ s3_keytext를 동시에 사용할 수 없습니다
  • ⚠️ s3_key 사용 시 file_type은 필수입니다

Response (202 Accepted)

{
    "task_id": "task_abc123def456",
    "status": "processing"
}

Error Responses

400 Bad Request

resume과 job_posting 필수 입력 오류:

{
    "detail": {
        "code": "INVALID_REQUEST",
        "message": "resume과 job_posting 는 필수 입력해야합니다"
    }
}

잘못된 file_type:

{
    "detail": {
        "code": "INVALID_FILE_TYPE",
        "message": "file_type은 pdf 또는 image만 가능합니다",
        "field": "resume.file_type"
    }
}

s3_key 또는 text 필수 오류:

{
    "detail": {
        "code": "INVALID_DOCUMENT",
        "message": "s3_key 또는 text 중 하나는 필수입니다",
        "field": "resume"
    }
}

401 Unauthorized

{
    "detail": {
        "code": "UNAUTHORIZED",
        "message": "유효하지 않은 API Key입니다"
    }
}

404 Not Found

{
    "detail": {
        "code": "FILE_NOT_FOUND",
        "message": "파일을 찾을 수 없습니다: users/12/resume/abc123.pdf"
    }
}

422 Unprocessable Entity

{
    "detail": {
        "code": "OCR_FAILED",
        "message": "이미지에서 텍스트를 추출할 수 없습니다"
    }
}

429 Too Many Requests

{
    "detail": {
        "code": "RATE_LIMIT_EXCEEDED",
        "message": "요청 한도 초과. 1분 후 재시도하세요"
    }
}

500 Internal Server Error

{
    "detail": {
        "code": "INTERNAL_ERROR",
        "message": "내부 서버 오류가 발생했습니다"
    }
}

503 Service Unavailable

LLM 서비스 불가:

{
    "detail": {
        "code": "LLM_UNAVAILABLE",
        "message": "AI 서비스에 연결할 수 없습니다"
    }
}

S3 스토리지 불가:

{
    "detail": {
        "code": "S3_UNAVAILABLE",
        "message": "파일 스토리지에 연결할 수 없습니다"
    }
}

Polling 완료 시 (GET /ai/task/{task_id})

분석 완료 응답:

{
    "task_id": "task_abc123def456",
    "room_id": 32,
    "status": "completed",
    "result": {
        "success": true,
        "resume_ocr": "이력서 OCR 텍스트...",
        "job_posting_ocr": "채용공고 OCR 텍스트...",
        "resume_analysis": {
            "strengths": ["React 숙련도", "프로젝트 경험"],
            "weaknesses": ["백엔드 경험 부족"],
            "suggestions": ["Spring 학습 권장"]
        },
        "posting_analysis": {
            "company": "카카오",
            "position": "백엔드 개발자",
            "required_skills": ["Java", "Spring", "MySQL"],
            "preferred_skills": ["Docker", "Kubernetes"]
        }
    }
}

Response Fields

필드 타입 설명
task_id string 작업 ID
room_id int 채팅방 ID
status string processing, completed, failed
result.success boolean 성공 여부
result.resume_ocr string 이력서 OCR 텍스트
result.job_posting_ocr string 채용공고 OCR 텍스트
result.resume_analysis object 이력서 분석 결과
result.posting_analysis object 채용공고 분석 결과

2. 채팅 (대화/면접 질문/면접 리포트)

개요

모든 LLM 응답을 통합 처리합니다. context.mode로 기능을 구분합니다.

Endpoint

POST /ai/chat

처리 방식

스트리밍 (SSE) - Server-Sent Events로 실시간 응답

Request Headers

Header 필수
X-API-Key your-api-key-here
Content-Type application/json

Request Body - 일반 대화 (mode: general)

{
    "model": "gemini",
    "room_id": "room_001",
    "user_id": 12,
    "message": "이력서 작성 팁 알려줘",
    "context": {
        "mode": "general",
        "resume": null,
        "job_posting": null,
        "interview_type": null,
        "session_id": null,
        "question_count": null
    }
}

Request Body - 면접 질문 생성 (mode: interview_question)

{
    "model": "gemini",
    "room_id": "room_001",
    "user_id": 12,
    "message": "기술 면접 질문 생성해줘",
    "context": {
        "mode": "interview_question",
        "resume_ocr": "이력서 OCR 내용...",
        "job_posting": "채용공고 OCR 내용...",
        "interview_type": "technical",
        "session_id": "interview_001",
        "question_count": 0
    }
}

Request Body - 면접 리포트 생성 (면접 종료)

{
    "model": "gemini",
    "room_id": "room_001",
    "user_id": 12,
    "session_id": "interview_001",
    "context": [
        {
            "question": "React의 Virtual DOM이 무엇인가요?",
            "answer": "실제 DOM과 비교해서 변경된 부분만 업데이트하는 거예요"
        },
        {
            "question": "Reconciliation 알고리즘에 대해 설명해주세요.",
            "answer": "변경사항을 비교하는 알고리즘입니다"
        }
    ]
}

Request Fields

필드 타입 필수 설명
model string | null gemini(기본값), vllm
room_id string 채팅방 ID
user_id int 사용자 ID
message string | null 사용자 메시지 (일반 대화 시 필수)
session_id string | null 면접 세션 ID (면접 리포트 시 필수)
context object | array 채팅 컨텍스트 또는 Q&A 배열

Context Object (일반 대화/면접 질문)

필드 타입 필수 설명
mode string general(기본값), interview_question, interview_report
resume_ocr string | null 이력서 OCR 텍스트 (면접 질문 생성 시)
job_posting string | null 채용공고 OCR 텍스트 (면접 질문 생성 시)
interview_type string | null technical, personality (면접 모드 시)
session_id string | null 면접 세션 ID
question_count int | null 현재까지 생성된 질문 수

Context Array (면접 리포트)

면접 리포트 생성 시 context는 Q&A 배열입니다:

[
    { "question": "질문1", "answer": "답변1" },
    { "question": "질문2", "answer": "답변2" }
]

ChatMode 설명

Mode 설명 사용 시점
general 일반 대화 (기본값) 취업 관련 질문, RAG 검색
interview_question 면접 질문 생성 면접 모드 시작 시, 꼬리질문 생성 시
interview_report 면접 리포트 생성 면접 종료 시 평가 및 피드백

SSE Response 형식

일반 대화:

{
    "success": true,
    "mode": "general",
    "response": "이력서 작성 팁을 알려드릴게요..."
}

면접 질문:

{
    "success": true,
    "mode": "interview_question",
    "response": "React와 Vue의 차이점에 대해 설명해주세요.",
    "interview_type": "technical"
}

면접 리포트:

{
    "success": true,
    "mode": "interview_report",
    "report": {
        "evaluations": [
            {
                "question": "React의 Virtual DOM이 무엇인가요?",
                "answer": "실제 DOM과 비교해서 변경된 부분만 업데이트하는 거예요",
                "good_points": ["Virtual DOM의 기본 개념을 잘 이해하고 있음"],
                "improvements": ["Reconciliation 알고리즘 설명 추가하면 좋음"]
            }
        ],
        "strength_patterns": ["기술 개념에 대한 이해도가 높음"],
        "weakness_patterns": ["심화 개념 설명이 부족함"],
        "learning_guide": ["React 심화 개념 학습 (Fiber, Concurrent Mode)"]
    }
}

Error Responses

400 Bad Request

필수 필드 누락:

{
    "detail": {
        "code": "INVALID_REQUEST",
        "message": "room_id는 필수입니다",
        "field": "room_id"
    }
}

잘못된 mode:

{
    "detail": {
        "code": "INVALID_MODE",
        "message": "mode는 general, interview_question, interview_report 중 하나여야 합니다",
        "field": "context.mode"
    }
}

잘못된 면접 타입:

{
    "detail": {
        "code": "INVALID_INTERVIEW_TYPE",
        "message": "interview_type은 technical 또는 personality만 가능합니다",
        "field": "context.interview_type"
    }
}

필수 context 누락:

{
    "detail": {
        "code": "MISSING_CONTEXT",
        "message": "interview_question 모드에서 resume은 필수입니다",
        "field": "context.resume"
    }
}

빈 메시지:

{
    "detail": {
        "code": "EMPTY_MESSAGE",
        "message": "message는 비어있을 수 없습니다",
        "field": "message"
    }
}

history 초과:

{
    "detail": {
        "code": "HISTORY_TOO_LONG",
        "message": "history는 최대 20개까지 가능합니다",
        "field": "history"
    }
}

401 Unauthorized

{
    "detail": {
        "code": "UNAUTHORIZED",
        "message": "유효하지 않은 API Key입니다"
    }
}

404 Not Found

{
    "detail": {
        "code": "FILE_NOT_FOUND",
        "message": "파일을 찾을 수 없습니다"
    }
}

422 Unprocessable Entity

{
    "detail": {
        "code": "SESSION_NOT_FOUND",
        "message": "면접 세션을 찾을 수 없습니다: interview_001"
    }
}

429 Too Many Requests

{
    "detail": {
        "code": "RATE_LIMIT_EXCEEDED",
        "message": "동시 연결 한도 초과"
    }
}

500 Internal Server Error

{
    "detail": {
        "code": "STREAM_ERROR",
        "message": "스트리밍 연결이 중단되었습니다"
    }
}

503 Service Unavailable

LLM 서비스 불가:

{
    "detail": {
        "code": "LLM_UNAVAILABLE",
        "message": "AI 서비스에 연결할 수 없습니다"
    }
}

VectorDB 서비스 불가:

{
    "detail": {
        "code": "VECTORDB_UNAVAILABLE",
        "message": "검색 서비스에 연결할 수 없습니다"
    }
}

3. 캘린더 일정 파싱

개요

채용공고 파일/텍스트를 분석하여 일정 정보를 추출합니다.

Endpoint

POST /ai/calendar/parse

처리 방식

동기 - 즉시 응답 반환 (Gemini Flash API 사용)

Request Headers

Header 필수
X-API-Key your-api-key-here

Request Body

{
    "s3_key": "https://s3.../job_posting.png",
    "text": null
}

또는

{
    "s3_key": null,
    "text": "카카오 백엔드 개발자 채용\n서류마감: 2026-01-15\n코딩테스트: 2026-01-20..."
}

Request Fields

필드 타입 필수 설명
s3_key string | null ⚠️ 채용공고 파일 S3 URL
text string | null ⚠️ 채용공고 텍스트

⚠️ s3_key 또는 text 중 하나는 필수

Response (200 OK)

{
    "success": true,
    "company": "카카오",
    "position": "백엔드 개발자",
    "schedules": [
        { "stage": "서류 마감", "date": "2026-01-15", "time": null },
        { "stage": "코딩테스트", "date": "2026-01-20", "time": "14:00" },
        { "stage": "1차 면접", "date": "2026-01-25", "time": null }
    ],
    "hashtags": ["#카카오", "#백엔드", "#신입"]
}

Response Fields

필드 타입 설명
success boolean 성공 여부
company string 회사명
position string 포지션
schedules array 전형 일정 목록
schedules[].stage string 전형 단계
schedules[].date string 날짜 (YYYY-MM-DD)
schedules[].time string | null 시간 (HH:MM)
hashtags array 추출된 해시태그

Error Responses

400 Bad Request

필수 필드 누락:

{
    "detail": {
        "code": "INVALID_REQUEST",
        "message": "s3_key 또는 text 중 하나는 필수입니다"
    }
}

잘못된 URL:

{
    "detail": {
        "code": "INVALID_URL",
        "message": "유효하지 않은 URL 형식입니다",
        "field": "s3_key"
    }
}

401 Unauthorized

{
    "detail": {
        "code": "UNAUTHORIZED",
        "message": "유효하지 않은 API Key입니다"
    }
}

404 Not Found

{
    "detail": {
        "code": "FILE_NOT_FOUND",
        "message": "파일을 찾을 수 없습니다"
    }
}

422 Unprocessable Entity

파싱 실패:

{
    "detail": {
        "code": "PARSE_FAILED",
        "message": "일정 정보를 추출할 수 없습니다"
    }
}

일정 없음:

{
    "detail": {
        "code": "NO_SCHEDULE_FOUND",
        "message": "채용공고에서 일정을 찾을 수 없습니다"
    }
}

503 Service Unavailable

{
    "detail": {
        "code": "LLM_UNAVAILABLE",
        "message": "AI 서비스에 연결할 수 없습니다"
    }
}

4. 게시판 첨부파일 마스킹

개요

게시판 첨부파일에서 개인정보(이름, 전화번호, 이메일, 얼굴)를 감지하고 마스킹합니다.

Endpoint

POST /ai/masking/draft

처리 방식

비동기 - task_id 반환 → 폴링 필요

Request Headers

Header 필수
X-API-Key your-api-key-here

Request Body

{
    "s3_key": "https://s3.../document.png",
    "file_type": "image",
    "model": "gemini"
}

Request Fields

필드 타입 필수 설명
s3_key string 원본 파일 S3 URL
file_type string image, pdf
model string gemini(기본값), openai, vllm

Response (202 Accepted)

{
    "task_id": "task_masking_001",
    "status": "processing"
}

Error Responses

400 Bad Request

필수 필드 누락:

{
    "detail": {
        "code": "INVALID_REQUEST",
        "message": "s3_key은 필수입니다",
        "field": "s3_key"
    }
}

잘못된 파일 타입:

{
    "detail": {
        "code": "INVALID_FILE_TYPE",
        "message": "file_type은 image 또는 pdf만 가능합니다",
        "field": "file_type"
    }
}

잘못된 URL:

{
    "detail": {
        "code": "INVALID_URL",
        "message": "유효하지 않은 URL 형식입니다",
        "field": "s3_key"
    }
}

401 Unauthorized

{
    "detail": {
        "code": "UNAUTHORIZED",
        "message": "유효하지 않은 API Key입니다"
    }
}

404 Not Found

{
    "detail": {
        "code": "FILE_NOT_FOUND",
        "message": "파일을 찾을 수 없습니다"
    }
}

422 Unprocessable Entity

{
    "detail": {
        "code": "MASKING_FAILED",
        "message": "이미지 마스킹에 실패했습니다"
    }
}

503 Service Unavailable

{
    "detail": {
        "code": "S3_UNAVAILABLE",
        "message": "파일 저장에 실패했습니다"
    }
}

Polling 완료 시 (GET /ai/task/{task_id})

{
    "task_id": "task_masking_001",
    "status": "completed",
    "result": {
        "success": true,
        "original_url": "https://s3.../document.png",
        "masked_url": "https://s3.../document_masked.png",
        "thumbnail_url": "https://s3.../document_masked_thumb.png",
        "detected_pii": [
            { "type": "name", "coordinates": [100, 50, 200, 80], "confidence": 0.95 },
            { "type": "phone", "coordinates": [100, 100, 250, 130], "confidence": 0.92 },
            { "type": "email", "coordinates": [100, 150, 300, 180], "confidence": 0.98 },
            { "type": "face", "coordinates": [400, 50, 500, 150], "confidence": 0.99 }
        ]
    }
}

PII Type

Type 설명
name 이름
phone 전화번호
email 이메일
face 얼굴

5. 비동기 작업 상태 조회

개요

비동기 처리 작업의 상태를 조회하고 결과를 확인합니다.

Endpoint

GET /ai/task/{task_id}

Path Parameters

필드 타입 필수 설명
task_id string 작업 ID

Response - 처리 중

{
    "task_id": "task_abc123",
    "status": "processing",
    "progress": 65,
    "message": "OCR 처리 중..."
}

Response - 완료

{
    "task_id": "task_abc123",
    "status": "completed",
    "result": { ... }
}

참고: result 내용은 작업 유형에 따라 다릅니다:

  • 텍스트 추출 + 분석: resume_ocr, job_posting_ocr, resume_analysis, posting_analysis 포함
  • 마스킹: original_url, masked_url, thumbnail_url, detected_pii 포함

Response - 실패

{
    "task_id": "task_abc123",
    "status": "failed",
    "error": {
        "code": "OCR_ERROR",
        "message": "파일 형식을 인식할 수 없습니다."
    }
}

Status 값

Status 설명
processing 처리 중
completed 완료
failed 실패

Error Responses

400 Bad Request

{
    "detail": {
        "code": "INVALID_TASK_ID",
        "message": "유효하지 않은 task_id 형식입니다"
    }
}

401 Unauthorized

{
    "detail": {
        "code": "UNAUTHORIZED",
        "message": "유효하지 않은 API Key입니다"
    }
}

404 Not Found

{
    "detail": {
        "code": "TASK_NOT_FOUND",
        "message": "작업을 찾을 수 없습니다: task_abc123"
    }
}

410 Gone

{
    "detail": {
        "code": "TASK_EXPIRED",
        "message": "작업이 만료되었습니다"
    }
}

폴링 권장 사항

  • 초기 폴링 간격: 1초
  • 최대 대기 시간: 300초 (5분)
  • 지수 백오프: 실패 시 간격을 점진적으로 증가 (1초 → 2초 → 4초 → 8초)

공통 에러 응답

HTTP Status Codes

Status 설명 사용 시점
400 Bad Request 필수 파라미터 누락, 잘못된 형식
401 Unauthorized X-API-Key 누락/불일치
404 Not Found task_id 없음, 파일 없음
410 Gone 작업 만료
422 Unprocessable Entity 파라미터 형식은 맞지만 처리 불가
429 Too Many Requests Rate Limit 초과
500 Internal Server Error 서버 내부 오류
503 Service Unavailable 외부 서비스(Gemini, S3) 연결 실패

에러 응답 형식

{
    "detail": {
        "code": "ERROR_CODE",
        "message": "사람이 읽을 수 있는 에러 메시지",
        "field": "에러 발생 필드 (optional)"
    }
}

에러 코드 전체 목록

Code 설명 관련 API
INVALID_REQUEST 필수 파라미터 누락 전체
INVALID_FILE_TYPE 지원하지 않는 파일 타입 1, 4
INVALID_DOCUMENT 문서 정보 불완전 1
INVALID_MODE 잘못된 chat mode 2
INVALID_INTERVIEW_TYPE 잘못된 면접 타입 2
INVALID_TASK_ID 잘못된 task_id 형식 5
INVALID_URL 잘못된 URL 형식 3, 4
MISSING_CONTEXT 필수 context 누락 2
EMPTY_MESSAGE 빈 메시지 2
HISTORY_TOO_LONG history 초과 2
UNAUTHORIZED 인증 실패 전체
FILE_NOT_FOUND 파일 없음 1, 2, 3, 4
TASK_NOT_FOUND 작업 없음 5
TASK_EXPIRED 작업 만료 5
SESSION_NOT_FOUND 면접 세션 없음 2
OCR_FAILED OCR 실패 1
PARSE_FAILED 파싱 실패 3
NO_SCHEDULE_FOUND 일정 없음 3
MASKING_FAILED 마스킹 실패 4
STREAM_ERROR 스트리밍 오류 2
RATE_LIMIT_EXCEEDED 요청 한도 초과 전체
INTERNAL_ERROR 내부 서버 오류 전체
LLM_UNAVAILABLE LLM 서비스 불가 1, 2, 3
S3_UNAVAILABLE S3 서비스 불가 1, 4
VECTORDB_UNAVAILABLE VectorDB 서비스 불가 2

Rate Limiting

API 유형 제한
동기 API 100 requests/min
비동기 API 50 requests/min
스트리밍 API 20 connections/min

헤더 응답

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1640995200

백엔드 ↔ AI 서버 매핑 가이드

백엔드 외부 API와 AI 서버 내부 API의 매핑:

백엔드 외부 API AI 서버 내부 API 설명
POST /api/ai-chatrooms/{roomId}/messages POST /ai/chat (mode: general) 일반 채팅
POST /api/ai/chatrooms/{roomId}/analysis POST /ai/text/extract 분석 요청
POST /api/ai/chatrooms/{roomId}/interview POST /ai/chat (mode: interview_question) 면접 시작
POST /api/ai/chatrooms/{roomId}/evaluation POST /ai/chat (mode: interview_report) 면접 평가
POST /api/ai/events/extraction POST /ai/calendar/parse 일정 추출
POST /api/board/upload POST /ai/masking/draft 마스킹 요청

에러 처리 권장 사항

Backend 에러 핸들링

// Spring Boot 예시
@ExceptionHandler
public ResponseEntity<ErrorResponse> handleAIServerError(AIServerException e) {
    switch (e.getCode()) {
        case "TASK_NOT_FOUND":
            return ResponseEntity.status(404).body(e.toResponse());
        case "LLM_UNAVAILABLE":
            // 재시도 로직 또는 사용자에게 안내
            return ResponseEntity.status(503).body(e.toResponse());
        default:
            return ResponseEntity.status(500).body(e.toResponse());
    }
}

재시도 정책

에러 코드 재시도 설명
LLM_UNAVAILABLE 최대 3회, 지수 백오프 (1초 → 2초 → 4초)
S3_UNAVAILABLE 최대 3회, 지수 백오프
VECTORDB_UNAVAILABLE 최대 3회, 지수 백오프
RATE_LIMIT_EXCEEDED X-RateLimit-Reset 헤더 확인 후 재시도
TASK_EXPIRED 새 요청 필요
OCR_FAILED 파일 확인 필요
UNAUTHORIZED API Key 확인 필요

문서 작성: 2026-01-13 작성자: AI Team 버전: v1.6 마지막 업데이트: 2026-01-26

변경 이력

버전 날짜 변경 내용
v1.6 2026-01-26 백엔드 회의 후 API 명세 업데이트: s3_key 필드 통일, 면접 리포트 context 배열 구조, 에러 응답 상세화
v1.5 2026-01-25 API 명세 최신화: file_url 필드 추가, 에러 응답 상세화, Request/Response 예시 보완
v1.4 2026-01-23 API별 상세 에러 케이스 추가
v1.3 2026-01-23 /ai/text/extract를 batch 구조로 변경 (resume + job_posting 동시 처리)
v1.2 2026-01-23 Request Body 필드명 RDB 컬럼명과 동기화
v1.1 2026-01-23 초기 API 명세 작성