Skip to content

Latest commit

ย 

History

History
489 lines (380 loc) ยท 16.5 KB

api-conventions.md

File metadata and controls

489 lines (380 loc) ยท 16.5 KB

REST API ์„ค๊ณ„ ๊ทœ์•ฝ

๋ชจ๋“  Core Server REST API๋Š” ๋ณธ ๋ฌธ์„œ์˜ ๊ทœ์•ฝ์„ ๋”ฐ๋ฅธ๋‹ค. OpenAPI ์ŠคํŽ™์€ springdoc-openapi๊ฐ€ ์ž๋™ ์ƒ์„ฑํ•˜๊ณ , ํ”„๋ก ํŠธ๋Š” openapi-typescript๋กœ ํƒ€์ž…์„ ์ƒ์„ฑํ•œ๋‹ค.


1. URL ๊ตฌ์กฐ

/api/{resource}                  # ์ปฌ๋ ‰์…˜
/api/{resource}/{id}             # ๋‹จ์ผ ๋ฆฌ์†Œ์Šค
/api/{resource}/{id}/{sub}       # ์„œ๋ธŒ ๋ฆฌ์†Œ์Šค
/api/{resource}/{id}:{action}    # ์•ก์…˜ (PATCH ๋Œ€์‹  ๋ช…์‹œ์  ๋™์‚ฌ)
  • ๋ฆฌ์†Œ์Šค๋ช…์€ ๋ณต์ˆ˜ํ˜•, kebab-case ๊ธˆ์ง€, snake_case ๊ธˆ์ง€ โ†’ repositories, interview-sessions ๋Œ€์‹  sessions (์งง๊ณ  ๋ช…ํ™•ํ•˜๊ฒŒ)
  • ์•ก์…˜์ด RESTful ๋™์‚ฌ๋กœ ํ‘œํ˜„ ์•ˆ ๋  ๋•Œ๋งŒ :start, :end ํ˜•์‹ ์‚ฌ์šฉ

ํ‘œ์ค€ ๋งคํ•‘

๋™์ž‘ ๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ ์˜ˆ์‹œ
์ปฌ๋ ‰์…˜ ์กฐํšŒ GET /api/sessions
๋‹จ๊ฑด ์กฐํšŒ GET /api/sessions/{id}
์ƒ์„ฑ POST /api/sessions
๋ถ€๋ถ„ ์ˆ˜์ • PATCH /api/sessions/{id}
์ „์ฒด ๊ต์ฒด PUT (๊ฑฐ์˜ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
์‚ญ์ œ (soft) DELETE /api/sessions/{id}
์•ก์…˜ ํŠธ๋ฆฌ๊ฑฐ PATCH /api/sessions/{id}/start, /api/sessions/{id}/end

2. ์ฃผ์š” API ๊ทธ๋ฃน

2.1 ์ธ์ฆ

POST   /api/auth/github            GitHub OAuth ์‹œ์ž‘ (URL ๋ฐœ๊ธ‰)
GET    /api/auth/github/callback   OAuth ์ฝœ๋ฐฑ ์ฒ˜๋ฆฌ
POST   /api/auth/refresh           ํ† ํฐ ๊ฐฑ์‹ 
DELETE /api/auth/logout            ๋กœ๊ทธ์•„์›ƒ (refresh token revoke)

2.2 ์‚ฌ์šฉ์ž

GET    /api/users/me               ๋‚ด ์ •๋ณด
PATCH  /api/users/me               ๋‚ด ์ •๋ณด ์ˆ˜์ • (์ œํ•œ์ )
DELETE /api/users/me               ํšŒ์› ํƒˆํ‡ด (soft)
GET    /api/users/me/stats         ๋‚ด ํ†ต๊ณ„ (์ ์ˆ˜ ์ถ”์ด)
GET    /api/users/me/consents      ๋™์˜ ์ด๋ ฅ
POST   /api/users/me/consents      ๋™์˜ ์ œ์ถœ

2.3 ์ž๋ฃŒ

GET    /api/repositories           ๋‚ด๊ฐ€ ๋“ฑ๋กํ•œ ๋ ˆํฌ ๋ชฉ๋ก
GET    /api/repositories/github    GitHub์˜ ๋‚ด ์ „์ฒด ๋ ˆํฌ (๋“ฑ๋ก ๊ฐ€๋Šฅ ํ›„๋ณด)
POST   /api/repositories           ๋“ฑ๋ก
GET    /api/repositories/{id}      ์ƒ์„ธ (๋ถ„์„ ์ƒํƒœ ํฌํ•จ)
DELETE /api/repositories/{id}      ์†Œํ”„ํŠธ ์‚ญ์ œ
POST   /api/repositories/{id}/reanalyze   ์žฌ๋ถ„์„ ์š”์ฒญ

POST   /api/resumes                ์—…๋กœ๋“œ
GET    /api/resumes                ๋ชฉ๋ก
GET    /api/resumes/{id}           ์ƒ์„ธ
DELETE /api/resumes/{id}           ์†Œํ”„ํŠธ ์‚ญ์ œ
POST   /api/resumes/{id}/reanalyze ์žฌ๋ถ„์„

GET    /api/documents              ๋ถ„์„ ๋ฌธ์„œ ๋ชฉ๋ก
GET    /api/documents/{id}         ๋ถ„์„ ๋ฌธ์„œ ์ƒ์„ธ (S3 URL ํฌํ•จ)
GET    /api/documents/{id}/content ๋ถ„์„ ์›๋ฌธ(๋งˆํฌ๋‹ค์šด) ํ”„๋ก์‹œ โ€” presigned ๋Š” ๋‚ด๋ถ€ ํ˜ธ์ŠคํŠธ๋ผ Core ์ค‘๊ณ„

2.4 ๋ฉด์ ‘ ์„ธ์…˜

POST   /api/sessions               ์ƒ์„ฑ
GET    /api/sessions               ํžˆ์Šคํ† ๋ฆฌ ๋ชฉ๋ก
GET    /api/sessions/{id}          ์ƒ์„ธ (๋ฉ”์‹œ์ง€/ํ”ผ๋“œ๋ฐฑ ํฌํ•จ)
PATCH  /api/sessions/{id}          ๋ฉ”๋ชจ/์ œ๋ชฉ ์ˆ˜์ •
PATCH  /api/sessions/{id}/start    ์‹œ์ž‘ (READY โ†’ IN_PROGRESS)
PATCH  /api/sessions/{id}/end      ์ข…๋ฃŒ
DELETE /api/sessions/{id}          ์†Œํ”„ํŠธ ์‚ญ์ œ

GET    /api/sessions/{id}/messages ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก
POST   /api/sessions/{id}/messages ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ (RealTime โ†’ Core ์ „์šฉ)

GET    /api/sessions/{id}/feedback ํ”ผ๋“œ๋ฐฑ ์กฐํšŒ
GET    /api/sessions/{id}/feedback/report  AI ํ•™์Šต ๋ฆฌํฌํŠธ(๋งˆํฌ๋‹ค์šด) ํ”„๋ก์‹œ โ€” ์†Œ์œ ์ž ์ „์šฉ, /documents/{id}/content ์™€ ๋™์ผ ํŒจํ„ด

2.5 ์‹œ์Šคํ…œ

GET    /api/system/health          ํ—ฌ์Šค์ฒดํฌ (PG/MQ/S3/AI ์ƒํƒœ)
GET    /api/system/version         ๋ฒ„์ „ ์ •๋ณด

2.6 ๋‚ด๋ถ€ API (์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ ์ „์šฉ)

/api/internal/* prefix. ์™ธ๋ถ€์—์„œ ํ˜ธ์ถœ ๊ธˆ์ง€ โ€” Spring Security์—์„œ ์™ธ๋ถ€ ํŒจํ‚ท์„ ์ฐจ๋‹จํ•˜๊ณ  X-Internal-API-Key ํ—ค๋”๋กœ ๊ฒ€์ฆ. AI Server์™€ RealTime Server๋งŒ ํ˜ธ์ถœ์ž.

GET  /api/internal/users/{userId}/github-token         AI โ†’ Core
PUT  /api/internal/documents/{documentId}/embeddings   AI โ†’ Core
POST /api/internal/embeddings/search                   AI โ†’ Core
POST /api/internal/ai-logs                             AI โ†’ Core

์ž์„ธํ•œ ์š”์ฒญยท์‘๋‹ต ์Šคํ‚ค๋งˆ์™€ ์ธ์ฆ ๊ทœ์•ฝ์€ ยง10 ๋‚ด๋ถ€ API ๋ถ€๋ก ์ฐธ์กฐ.


3. ์š”์ฒญ ๊ทœ์•ฝ

3.1 ํ—ค๋”

ํ—ค๋” ํ•„์ˆ˜ ์„ค๋ช…
Authorization: Bearer {token} ์ธ์ฆ ํ•„์š” ์‹œ JWT access token
X-Trace-Id ๊ถŒ์žฅ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ถ€์—ฌํ•˜์ง€ ์•Š์œผ๋ฉด Gateway๊ฐ€ ๋ถ€์—ฌ
Content-Type: application/json POST/PATCH multipart/form-data๋Š” ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ๋งŒ

3.2 ํŽ˜์ด์ง€๋„ค์ด์…˜

GET /api/sessions?page=0&size=20&sort=createdAt,desc
  • 0-based page
  • size ์ตœ๋Œ€ 100 (์ดˆ๊ณผ ์‹œ 100์œผ๋กœ clamp)
  • ์ •๋ ฌ: {field},{asc|desc} ํ˜•์‹, ๋‹ค์ค‘ ์ •๋ ฌ์€ &sort= ๋ฐ˜๋ณต

3.3 ํ•„ํ„ฐ๋ง

GET /api/sessions?status=COMPLETED&jobCategory=BACKEND&from=2026-01-01&to=2026-04-30
  • ๋‚ ์งœ๋Š” ISO 8601 (YYYY-MM-DD ๋˜๋Š” RFC 3339)
  • ๋‹ค์ค‘ ๊ฐ’์€ ์ฝค๋งˆ: ?status=COMPLETED,CANCELLED

4. ์‘๋‹ต ๊ทœ์•ฝ

4.1 ์„ฑ๊ณต (2xx)

๋‹จ๊ฑด:

{
  "id": 42,
  "title": "๋ฐฑ์—”๋“œ ๋ชจ์˜๋ฉด์ ‘ #3",
  "status": "COMPLETED",
  "createdAt": "2026-04-25T14:30:00Z"
}

์ปฌ๋ ‰์…˜ (ํŽ˜์ด์ง€):

{
  "content": [ {...}, {...} ],
  "page": 0,
  "size": 20,
  "totalElements": 137,
  "totalPages": 7,
  "first": true,
  "last": false
}

4.2 ์—๋Ÿฌ (4xx, 5xx)

ํ‘œ์ค€ ํ˜•์‹:

{
  "code": "RESUME_INVALID_FILE_TYPE",
  "message": "PDF ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.",
  "traceId": "9f4e5b...",
  "timestamp": "2026-04-27T15:20:00Z",
  "details": {
    "uploadedType": "image/jpeg"
  }
}
ํ•„๋“œ ์„ค๋ช…
code ๋„๋ฉ”์ธ ์—๋Ÿฌ ์ฝ”๋“œ (๋Œ€๋ฌธ์ž SNAKE_CASE) โ€” ํด๋ผ์ด์–ธํŠธ ๋ถ„๊ธฐ ๊ธฐ์ค€
message ์‚ฌ์šฉ์ž์—๊ฒŒ ๋…ธ์ถœ ๊ฐ€๋Šฅํ•œ ํ•œ๊ตญ์–ด ๋ฉ”์‹œ์ง€
traceId ๋กœ๊ทธ ์ถ”์ ์šฉ, ์‚ฌ์šฉ์ž ๋ฌธ์˜ ์‹œ ํ™œ์šฉ
details ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ (validation ์‹คํŒจ ํ•„๋“œ ๋“ฑ)

4.3 ํ‘œ์ค€ HTTP ์ƒํƒœ ์ฝ”๋“œ

์ƒํ™ฉ ์ฝ”๋“œ
์„ฑ๊ณต (์กฐํšŒ/์ˆ˜์ •) 200
์ƒ์„ฑ ์„ฑ๊ณต 201 (Location ํ—ค๋” ํฌํ•จ)
๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ์‹œ์ž‘ 202
๋ณธ๋ฌธ ์—†์Œ (DELETE ๋“ฑ) 204
์ž˜๋ชป๋œ ์š”์ฒญ (validation ์‹คํŒจ) 400
์ธ์ฆ ๋ˆ„๋ฝ/๋งŒ๋ฃŒ 401
๊ถŒํ•œ ๋ถ€์กฑ 403
๋ฆฌ์†Œ์Šค ์—†์Œ 404
์ถฉ๋Œ (์ค‘๋ณต ๋“ฑ๋ก ๋“ฑ) 409
์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ ์˜๋ฏธ์  ์˜ค๋ฅ˜ 422
์„œ๋ฒ„ ์˜ค๋ฅ˜ 500
์™ธ๋ถ€ ์˜์กด์„ฑ ์žฅ์•  (LLM API ๋“ฑ) 502 / 503

์ž˜๋ชป๋œ ์š”์ฒญ์˜ ์ƒํƒœ ์ฝ”๋“œ

ํด๋ผ์ด์–ธํŠธ ์ž˜๋ชป์€ ๋ฐ˜๋“œ์‹œ 4xx ๋กœ ๋๋‚ธ๋‹ค. 5xx ๋กœ ์ƒˆ์–ด ๋‚˜๊ฐ€๋ฉด ๋‘ ๊ฐ€์ง€๊ฐ€ ๋™์‹œ์— ๋ง๊ฐ€์ง„๋‹ค โ€” ํด๋ผ์ด์–ธํŠธ ์žฌ์‹œ๋„ ๋กœ์ง์ด ์˜์›ํžˆ ์„ฑ๊ณตํ•˜์ง€ ๋ชปํ•  ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•˜๊ณ , ์šด์˜ ๋กœ๊ทธ์—์„œ ์ง„์งœ ์žฅ์• ๊ฐ€ ์˜คํƒ€ URLยท๋ด‡ ์Šค์บ”์— ๋ฌปํžŒ๋‹ค. ์‹ค์ œ๋กœ 2026-09-27 ์ ๊ฒ€ ์‹œ์  ์šด์˜ ๋กœ๊ทธ์˜ SYS_INTERNAL_ERROR 11๊ฑด์ด ์ „๋ถ€ ์•„๋ž˜ ๋„ค ์ข…๋ฅ˜์˜€๊ณ  ์„œ๋ฒ„ ๋ฒ„๊ทธ๋Š” 0๊ฑด์ด์—ˆ๋‹ค.

์ƒํ™ฉ ์˜ˆ์™ธ ์‘๋‹ต
๋ณธ๋ฌธ ๋ˆ„๋ฝยท๊นจ์ง„ JSONยทํ•„๋“œ ํƒ€์ž… ๋ถˆ์ผ์น˜ HttpMessageNotReadableException 400 MALFORMED_REQUEST
๊ฒฝ๋กœ๋ณ€์ˆ˜ ๋ณ€ํ™˜ ์‹คํŒจ (/api/sessions/abc) MethodArgumentTypeMismatchException 400 MALFORMED_REQUEST
@Valid ์œ„๋ฐ˜ MethodArgumentNotValidException 400 VALIDATION_ERROR
์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ๋กœ NoResourceFoundException 404 ENDPOINT_NOT_FOUND
์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฉ”์„œ๋“œ HttpRequestMethodNotSupportedException 405 METHOD_NOT_ALLOWED

ํŒŒ์‹ฑ ์‹คํŒจ ์‘๋‹ต์— ์›๋ฌธ ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€๋ฅผ ์‹ฃ์ง€ ์•Š๋Š”๋‹ค โ€” ๊นจ์ง„ ๋ณธ๋ฌธ ์กฐ๊ฐ(= ์‚ฌ์šฉ์ž ์ž…๋ ฅ)์ด ๊ทธ๋Œ€๋กœ ๋ฐ˜์‚ฌ๋œ๋‹ค. ๊ณ ์ • ๋ฌธ๊ตฌ๋งŒ ๋‚ด๋ณด๋‚ด๊ณ  ์›์ธ์€ ์„œ๋ฒ„ ๋กœ๊ทธ์— ์˜ˆ์™ธ ํƒ€์ž…์œผ๋กœ๋งŒ ๋‚จ๊ธด๋‹ค.

์ƒˆ @RequestBody ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•  ๋•Œ ์ด ํ‘œ๋ฅผ ๋‹ค์‹œ ๋ณผ ํ•„์š”๋Š” ์—†๋‹ค โ€” GlobalExceptionHandler ๊ฐ€ ์ „์—ญ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๊ณ  ํšŒ๊ท€๋Š” GlobalExceptionHandlerClientErrorTest ๊ฐ€ ๊ณ ์ •ํ•œ๋‹ค.


5. ์—๋Ÿฌ ์ฝ”๋“œ ์นดํƒˆ๋กœ๊ทธ

์‹ ๊ทœ ์ถ”๊ฐ€ ์‹œ ๋ณธ ์นดํƒˆ๋กœ๊ทธ์— ๋ฐ˜๋“œ์‹œ ๋“ฑ๋ก. ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ๋‘ ๋„๋ฉ”์ธ์—์„œ ์‚ฌ์šฉ ๊ธˆ์ง€.

# ์ธ์ฆ (AUTH_*)
AUTH_INVALID_TOKEN          401  ํ† ํฐ ํ˜•์‹ ์˜ค๋ฅ˜
AUTH_EXPIRED_TOKEN          401  ๋งŒ๋ฃŒ
AUTH_REVOKED_TOKEN          401  revoke๋จ
AUTH_GITHUB_OAUTH_FAILED    401  GitHub OAuth ์‹คํŒจ
AUTH_CONSENT_REQUIRED       403  ํ•„์ˆ˜ ๋™์˜ ๋ฏธ์™„๋ฃŒ

# ์‚ฌ์šฉ์ž (USER_*)
USER_NOT_FOUND              404
USER_ALREADY_DELETED        410

# ์ด๋ ฅ์„œ (RESUME_*)
RESUME_INVALID_FILE_TYPE    400  PDF ์™ธ ์—…๋กœ๋“œ
RESUME_FILE_TOO_LARGE       400  ํฌ๊ธฐ ์ดˆ๊ณผ
RESUME_EMPTY_FILE           400
RESUME_NOT_FOUND            404
RESUME_IN_USE               409  ํ™œ์„ฑ ์„ธ์…˜์—์„œ ์‚ฌ์šฉ ์ค‘

# ๋ ˆํฌ (REPO_*)
REPO_NOT_FOUND              404
REPO_ALREADY_REGISTERED     409
REPO_GITHUB_API_FAILED      502
REPO_PRIVATE_NO_ACCESS      403

# ๋ถ„์„ (DOC_*)
DOC_NOT_ANALYZED            422  ANALYZED ์ƒํƒœ ์•„๋‹˜
DOC_ANALYSIS_FAILED         500
DOC_NOT_FOUND               404

# ์„ธ์…˜ (SESSION_*)
SESSION_INVALID_STATE       422  ์ƒํƒœ ์ „์ด ๋ถˆ๊ฐ€
SESSION_MAX_REACHED         422  ์ตœ๋Œ€ ์งˆ๋ฌธ/์‹œ๊ฐ„ ๋„๋‹ฌ
SESSION_NOT_FOUND           404
SESSION_FORBIDDEN           403  ํƒ€์ธ ์„ธ์…˜ ์ ‘๊ทผ

# ํ”ผ๋“œ๋ฐฑ (FEEDBACK_*)
FEEDBACK_NOT_READY          404  ์•„์ง ์ƒ์„ฑ ์ค‘ (ํด๋ง/SSE ๋Œ€๊ธฐ ์ง€์†)
FEEDBACK_GENERATION_FAILED  404  ์ƒ์„ฑ ์‹คํŒจ ๋งˆ์ปค ์กด์žฌ โ€” ๋Œ€๊ธฐ ์ค‘๋‹จ, ์žฌ์ƒ์„ฑ ์œ ๋„. details.retriable(boolean) ๋™๋ด‰
FEEDBACK_NOT_FOUND          404  ๊ณต์œ  ํ† ํฐ ๋ฌดํšจ
FEEDBACK_ALREADY_EXISTS     409  ์žฌ์ƒ์„ฑ ์š”์ฒญ ์‹œ ์ด๋ฏธ ์กด์žฌ (์žฌ์กฐํšŒ ์‹ ํ˜ธ)
FEEDBACK_REPORT_NOT_AVAILABLE 422  ๋ฆฌํฌํŠธ ํŒŒ์ผ ์—†์Œ โ€” AI ์ €์žฅ ์‹คํŒจ ํด๋ฐฑ(reportS3Key=null)ยท๊ตฌ๋ฒ„์ „ ํ”ผ๋“œ๋ฐฑ

# ์‹œ์Šคํ…œ (SYS_*)
SYS_RATE_LIMITED            429
SYS_DEPENDENCY_DOWN         503  RabbitMQ/AI/LLM ๋‹ค์šด
SYS_INTERNAL_ERROR          500

6. ๋ฉฑ๋“ฑ์„ฑ (Idempotency)

POST ์š”์ฒญ ์ค‘ ๊ฒฐ์ œยท๋ฉ”์‹œ์ง€ ๋ฐœํ–‰ ๋“ฑ ์ค‘๋ณต ์ฒ˜๋ฆฌ ์œ„ํ—˜์ด ์žˆ๋Š” ๊ฒฝ์šฐ:

POST /api/sessions
Idempotency-Key: 8b3c5e2a-...

์„œ๋ฒ„๋Š” 24์‹œ๊ฐ„ ๋™์•ˆ ๊ฐ™์€ ํ‚ค๋กœ ๋“ค์–ด์˜จ ์š”์ฒญ์— ๋Œ€ํ•ด ๋™์ผ ์‘๋‹ต์„ ๋ฐ˜ํ™˜.

Phase 1์—์„œ๋Š” ๊ฒฐ์ œ ์—†์œผ๋ฏ€๋กœ ํ•„์ˆ˜๋Š” ์•„๋‹ˆ๋‚˜, AI ์ž‘์—… ๋ฐœํ–‰ API(reanalyze ๋“ฑ)์— ์ ์šฉ ๊ถŒ์žฅ.


7. ๋ฒ„์ „ ๊ด€๋ฆฌ

  • ๋ณธ ๋ฌธ์„œ ๊ธฐ์ค€ v1. ๋ชจ๋“  ๊ฒฝ๋กœ์— /api/v1/... ์ ‘๋‘๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ (Breaking Change ๋ฐœ์ƒ ์‹œ์ ์— ๋„์ž…).
  • Breaking Change ์ •์ฑ…: ์‹ ๊ทœ ๊ฒฝ๋กœ ์ถ”๊ฐ€ + ๊ตฌ ๊ฒฝ๋กœ๋Š” ์ตœ์†Œ 1๊ฐœ phase ์œ ์ง€ ํ›„ ์ œ๊ฑฐ.

8. OpenAPI ์ž๋™ ์ƒ์„ฑ

Backend

  • springdoc-openapi application.properties:
    springdoc.api-docs.path=/api/v3/api-docs
    springdoc.swagger-ui.path=/api/swagger-ui.html
    
  • @Operation, @Schema ์• ๋…ธํ…Œ์ด์…˜์œผ๋กœ ์‘๋‹ต ์˜ˆ์‹œ ์ž‘์„ฑ

Frontend

  • openapi-typescript ์œผ๋กœ frontend/src/shared/api/generated.ts ์ƒ์„ฑ
  • ๋นŒ๋“œ ์Šคํฌ๋ฆฝํŠธ:
    npx openapi-typescript http://localhost:8080/api/v3/api-docs -o src/shared/api/generated.ts
  • ๋ณ€๊ฒฝ ์‹œ PR์— ํฌํ•จ

9. ๋น„๋™๊ธฐ ์ž‘์—… ์‘๋‹ต ํŒจํ„ด

POST/PATCH๊ฐ€ ์ฆ‰์‹œ ์™„๋ฃŒ๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ (AI ๋ถ„์„ ํŠธ๋ฆฌ๊ฑฐ ๋“ฑ):

HTTP/1.1 202 Accepted
{
  "taskId": "doc-analyze-42",
  "status": "QUEUED",
  "statusUrl": "/api/documents/42",
  "streamUrl": "/api/stream/documents/42"
}

ํด๋ผ์ด์–ธํŠธ๋Š”:

  1. SSE (streamUrl) ์šฐ์„  ๊ตฌ๋… โ†’ ์ƒํƒœ ํ‘ธ์‹œ ์ˆ˜์‹ 
  2. SSE ๋Š๊ธฐ๋ฉด statusUrl ํด๋ง fallback (5์ดˆ ๊ฐ„๊ฒฉ)

์ƒ์„ธ: event-stream.md


10. ๋‚ด๋ถ€ API ๋ถ€๋ก (์„œ๋น„์Šค ๊ฐ„ ํ˜ธ์ถœ)

/api/internal/* endpoint๋Š” ๋‹ค์Œ ๊ทœ์•ฝ์„ ๋”ฐ๋ฅธ๋‹ค.

10.1 ์ธ์ฆ

  • ๋ชจ๋“  ์š”์ฒญ์— X-Internal-API-Key: {key} ํ—ค๋” ํ•„์ˆ˜
  • ํ‚ค๋Š” ํ™˜๊ฒฝ๋ณ€์ˆ˜ CORE_INTERNAL_API_KEY (Core/AI/RealTime ๋™์ผ ๊ฐ’)
  • JWT ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ถˆํ•„์š”. ๋‹จ ํ˜ธ์ถœ์ž๊ฐ€ ์‹ ๋ขฐ ์˜์—ญ ์•ˆ์— ์žˆ์–ด์•ผ ํ•จ (Docker ๋‚ด๋ถ€๋ง, K8s ClusterIP ๋“ฑ)
  • ํ‚ค ๋ˆ„๋ฝ/๋ถˆ์ผ์น˜ โ†’ 401 Unauthorized, body: { "code": "INTERNAL_AUTH_FAILED" }

10.2 GET /api/internal/users/{userId}/github-token

๋ถ„์„ ์‹œ์ ์— ์‚ฌ์šฉ์ž GitHub access token์„ ์งง๊ฒŒ ์œ„์ž„. AES-256์œผ๋กœ ์ €์žฅ๋œ ํ‰๋ฌธ token์„ ๋ณตํ˜ธํ™”ํ•ด ๋ฐ˜ํ™˜ (๋ฉ”๋ชจ๋ฆฌ/๋กœ๊ทธ/์‘๋‹ต body์—๋งŒ ๋…ธ์ถœ, DBยท์ด๋ฒคํŠธ์—”๋ฒจ๋กœํ”„์—๋Š” ์ ˆ๋Œ€ ๋™๋ด‰ํ•˜์ง€ ์•Š์Œ).

Path parameter

์ด๋ฆ„ ํƒ€์ž… ์„ค๋ช…
userId long users.id

Response 200

{ "accessToken": "ghp_xxx..." }

Response 404 โ€” ์‚ฌ์šฉ์ž ์—†์Œ ๋˜๋Š” token ๋ˆ„๋ฝ (USER_NOT_FOUND) Response 401/403 โ€” ์ธ์ฆ ์‹คํŒจ (INTERNAL_AUTH_FAILED) Response 5xx โ€” Core ์ผ์‹œ ์žฅ์•  (ํ˜ธ์ถœ์ž๋Š” retriable๋กœ ๊ฐ„์ฃผ)

10.3 PUT /api/internal/documents/{documentId}/embeddings

analyzed_documents ํ•œ ๊ฑด์— ๋Œ€ํ•œ chunk + embedding์„ document_embeddings ํ…Œ์ด๋ธ”์— idempotent upsert. AI๊ฐ€ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ callback ๋ฐœํ–‰ ์ง์ „์— ํ˜ธ์ถœํ•œ๋‹ค.

Path parameter

์ด๋ฆ„ ํƒ€์ž… ์„ค๋ช…
documentId long analyzed_documents.id (Core๊ฐ€ publish ์ง์ „ ๋ฏธ๋ฆฌ ์ƒ์„ฑ)

Request body

{
  "model": "gemini-embedding-001",
  "dim": 1536,
  "chunks": [
    { "chunkIndex": 0, "chunkText": "...", "embedding": [0.012, -0.003, ...] },
    { "chunkIndex": 1, "chunkText": "...", "embedding": [...] }
  ]
}
ํ•„๋“œ ํƒ€์ž… ๋น„๊ณ 
model string ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ID (๊ฐ์‚ฌ์šฉ)
dim int ์ž„๋ฒ ๋”ฉ ์ฐจ์›. DB ์ปฌ๋Ÿผ ์ฐจ์›๊ณผ ์ผ์น˜ํ•ด์•ผ ํ•จ (๋ถˆ์ผ์น˜ โ†’ 400)
chunks[].chunkIndex int 0-base, ๊ฐ™์€ document ๋‚ด unique
chunks[].chunkText string ์›๋ฌธ ์ฒญํฌ (โ‰ค N KB, ์ปฌ๋Ÿผ ๊ธธ์ด ์ œํ•œ)
chunks[].embedding float[] ๊ธธ์ด == dim

Response 200

{ "upserted": 18 }

Response 400 โ€” dim ๋ถˆ์ผ์น˜, payload ๊ฒ€์ฆ ์‹คํŒจ (EMBEDDING_BAD_REQUEST) Response 404 โ€” analyzed_documents.id ์—†์Œ (DOCUMENT_NOT_FOUND) Response 401/403 โ€” INTERNAL_AUTH_FAILED Response 5xx โ€” Core ์ผ์‹œ ์žฅ์•  (retriable)

Idempotency

  • ๊ฐ™์€ (documentId, chunkIndex) ์กฐํ•ฉ ์žฌํ˜ธ์ถœ ์‹œ row๋ฅผ ๋ฎ์–ด์”€ (INSERT ... ON CONFLICT UPDATE)
  • ๋ถ€๋ถ„ ์žฌ์‹œ๋„ ์‹œ ๋ˆ„๋ฝ ์ฒญํฌ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก AI๋Š” ์ „์ฒด ์ฒญํฌ๋ฅผ ํ•œ ๋ฒˆ์— ๋ณด๋‚ด๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ

10.4 POST /api/internal/embeddings/search

RAG ๊ฒ€์ƒ‰. AI๊ฐ€ ์งˆ๋ฌธ ํ’€ยท๊ผฌ๋ฆฌ์งˆ๋ฌธยทํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ ์‹œ ์ปจํ…์ŠคํŠธ ์ฒญํฌ๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค. document_embeddings์—์„œ pgvector cosine topK โ€” queryText๊ฐ€ ์ฃผ์–ด์ง€๋ฉด ๋ฒกํ„ฐ + full-text(BM25) RRF ํ•˜์ด๋ธŒ๋ฆฌ๋“œ๋กœ ๊ฒ€์ƒ‰ํ•œ๋‹ค.

full-text ์ชฝ tsquery ๋Š” OR ๊ฒฐํ•ฉ์ด๋‹ค(2026-09-29). plainto_tsquery ๊ธฐ๋ณธ๊ฐ’์ธ AND ๋กœ๋Š” ์ด API ์˜ ์‹ค์ œ ์งˆ์˜(๊ผฌ๋ฆฌ์งˆ๋ฌธ = ์ง์ „์งˆ๋ฌธ+๋‹ต๋ณ€, ์งˆ๋ฌธ ํ’€ = ๋ชจ๋“œ+์ง๊ตฐ+์ž๊ธฐ์†Œ๊ฐœ 600์ž, ํ† ํฐ ์ˆ˜์‹ญ ๊ฐœ)๋ฅผ ์ „๋ถ€ ๋‹ด์€ ์ฒญํฌ๊ฐ€ ์—†์–ด full-text ๋ธŒ๋žœ์น˜๊ฐ€ ํ•ญ์ƒ 0๊ฑด์ด์—ˆ๊ณ  RRF ๊ฐ€ ๋ฒกํ„ฐ ๋‹จ๋…์œผ๋กœ ํ‡ดํ™”ํ–ˆ๋‹ค โ€” ์šด์˜ ์‹ค์ธก 47/47ยท30/30 ์ด 0๊ฑด. OR ๋กœ ๋ฐ”๊พผ ๋’ค ์šด์˜๊ณผ ๊ฐ™์€ ํ—ค๋”ฉ ์ฒญํ‚น ์ธ๋ฑ์Šค์—์„œ MRR 0.791โ†’0.885, Top-1 0.681โ†’0.830, Recall 0.936โ†’0.979.

Request body

{
  "queryEmbedding": [0.012, -0.003, ...],
  "queryText": "๋‚™๊ด€์  ๋ฝ ๋™์‹œ์„ฑ ์ œ์–ด",
  "documentIds": [12, 13],
  "topK": 5
}
ํ•„๋“œ ํƒ€์ž… ๋น„๊ณ 
queryEmbedding float[] ํ•„์ˆ˜. ์ฟผ๋ฆฌ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ
queryText string ์„ ํƒ. ์ฃผ์–ด์ง€๋ฉด ํ•˜์ด๋ธŒ๋ฆฌ๋“œ(RRF) ๊ฒ€์ƒ‰
documentIds long[] ๊ฒ€์ƒ‰ ๋ฒ”์œ„ ์ œํ•œ. ๋น„์–ด ์žˆ์œผ๋ฉด ์ „์ฒด
topK int ์–‘์ˆ˜. ๋ฏธ์ง€์ • ์‹œ 5

Response 200

{
  "hits": [
    { "documentId": 12, "chunkIndex": 3, "chunkText": "...", "distance": 0.18 }
  ]
}

Response 400 โ€” queryEmbedding ๋ˆ„๋ฝ Response 401/403 โ€” INTERNAL_AUTH_FAILED

RAG ๋ณด๊ฐ•์šฉ์ด๋ผ fatal ์ด ์•„๋‹ˆ๋‹ค โ€” AI(core/client.py: search_embeddings)๋Š” ํ˜ธ์ถœ ์‹คํŒจยทnon-2xx ๋ฅผ ๋นˆ ๊ฒฐ๊ณผ๋กœ ํด๋ฐฑํ•˜๊ณ  ๊ฒ€์ƒ‰ ์—†์ด ์ƒ์„ฑ์„ ๊ณ„์†ํ•œ๋‹ค.

10.5 POST /api/internal/ai-logs

AI์˜ LLM ํ˜ธ์ถœ๋ณ„ ํ† ํฐยท์ง€์—ฐ์‹œ๊ฐ„์„ ai_request_logs์— ๊ธฐ๋ก (US-30 ๊ด€์ธก์„ฑ). LangChain ์ฝœ๋ฐฑ(observability/llm_logging_callback.py)์ด ํ˜ธ์ถœ ์™„๋ฃŒยท์‹คํŒจ ์‹œ๋งˆ๋‹ค ๋ฐœ์‚ฌํ•œ๋‹ค.

Request body

{
  "userId": 123,
  "sessionId": 99,
  "requestType": "generate.followup",
  "modelName": "gemini-3.5-flash-lite",
  "inputTokens": 1820,
  "outputTokens": 210,
  "latencyMs": 1830,
  "status": "SUCCEEDED",
  "errorMessage": null
}
ํ•„๋“œ ํƒ€์ž… ๋น„๊ณ 
requestType string ํ•„์ˆ˜ (NotBlank)
status string ํ•„์ˆ˜
๋‚˜๋จธ์ง€ โ€” ์ „๋ถ€ nullable

Response 202 โ€” ๊ธฐ๋ก ํ์ž‰ (๋ณธ๋ฌธ ์—†์Œ) Response 401/403 โ€” INTERNAL_AUTH_FAILED

Fire-and-forget โ€” AI๋Š” ์‹คํŒจํ•ด๋„ raise ํ•˜์ง€ ์•Š๊ณ  ๊ฒฝ๊ณ  ๋กœ๊ทธ๋งŒ ๋‚จ๊ธด๋‹ค(๊ด€์ธก ์‹คํŒจ๊ฐ€ ๋ณธ ์ž‘์—…์„ ๋ง‰์ง€ ์•Š๊ฒŒ).