API Contracts¶
Moodiary 백엔드가 노출하거나 의존하는 API의 상세 명세. FE 와 공유하는 단일 진실의 원천.
- 목적이 다른 두 문서:
plan.md: 왜 / 언제 / 우선순위- 이 문서: 어떻게 생겼나 (URL / Method / 헤더 / request / response / status / 예시)
- 갱신 규칙: API를 새로 만들거나 응답 포맷을 바꿀 때 함께 갱신. PR description에 "spec 갱신" 한 줄 추가.
- 실시간 진실의 원천 (구현된 API): Swagger UI. 이 문서는 설계 의도 + 헤더 요구사항 + 외부 계약 을 보강.
📑 목차¶
- 엔드포인트 요약 (한눈 표)
- 공통 규약
- 공통 헤더
- Content-Type / 인코딩
- 날짜·시간
- ID
- 에러 응답 — 공통 포맷
- 페이지네이션
- 인증
- CORS
- 구현된 API
- Auth
- Post
- Calendar
- AI Response (🚧 Stub — PR 4-pre)
- 외부 시스템 계약
- AI 추론 서버
엔드포인트 요약 (한눈 표)¶
FE 개발자용 치트시트. 인증 필요 =
Authorization: Bearer <accessToken>헤더 첨부 필수. Body = 요청 본문 존재 시Content-Type: application/json필수.
| 기능 | Method | URL | 인증 헤더 | Body | 성공 응답 |
|---|---|---|---|---|---|
| 회원가입 | POST |
/auth/signup |
❌ 불필요 | ✅ JSON | 201 UUID 문자열 |
| 로그인 | POST |
/auth/login |
❌ 불필요 | ✅ JSON | 200 { accessToken, refreshToken, userId, nickname } |
| 토큰 갱신 | POST |
/auth/refresh |
❌ 불필요 | ✅ JSON | 200 { accessToken, refreshToken } (rotation) |
| 로그아웃 | POST |
/auth/logout |
❌ 불필요 | ✅ JSON | 204 (바디 없음) |
| 게시글 작성 | POST |
/post |
✅ 필수 | ✅ JSON | 201 UUID 문자열 |
| 내 게시글 전체 조회 | GET |
/post |
✅ 필수 | from/to/keyword/sort (전부 선택) |
200 PostResponseDto[] / 400 잘못된 정렬·범위·형식 |
| 게시글 단건 조회 | GET |
/post/{id} |
✅ 필수 | ❌ | 200 PostResponseDto |
| 게시글 수정 | PUT |
/post/{id} |
✅ 필수 | ✅ JSON | 200 PostResponseDto |
| 게시글 삭제 | DELETE |
/post/{id} |
✅ 필수 | ❌ | 204 (바디 없음) |
| 월별 캘린더 조회 | GET |
/calendar?year=YYYY&month=MM |
✅ 필수 | ❌ | 200 CalendarDayResponseDto[] |
| AI 응답 폴링 (🚧 Stub) | GET |
/post/{id}/ai-response |
✅ 필수 | ❌ | 200 상태별 payload |
화이트리스트 (인증 불필요) — Spring Security SecurityConfig.WHITELIST:
- /auth/** — 회원가입/로그인은 인증 자체가 불가능
- /swagger-ui/**, /swagger-ui.html, /v3/api-docs/**, /swagger-resources/** — 문서/스펙
- /error — 스프링 기본 에러 디스패치
그 외 모든 요청은 authenticated() — 토큰 없거나 잘못되면 401 { "message": "인증이 필요합니다." }.
공통 규약¶
공통 헤더¶
| 헤더 | 언제 | 값 |
|---|---|---|
Authorization |
보호된 엔드포인트 (위 표에서 ✅) | Bearer <accessToken> — 로그인 응답의 accessToken |
Content-Type |
request body 가 있을 때 | application/json; charset=UTF-8 |
Accept |
(권장) | application/json |
응답 헤더 — 서버가 항상 Content-Type: application/json; charset=UTF-8 로 응답 (에러 포함). CORS preflight 응답은 Access-Control-Allow-* 헤더가 자동 첨부됨.
Content-Type / 인코딩¶
- 요청 / 응답 모두
application/json; charset=UTF-8 - 이모지 포함 가능 → DB는
utf8mb4필수, 응답 인코딩도 UTF-8 강제
날짜·시간¶
- 저장: UTC (DB
DATETIME(6)) - 응답: ISO-8601 with offset (
2026-05-24T13:14:15.123+09:00) 또는 KST 명시 ISO — 코드에서 일관 유지 - 캘린더 등 "일자" 단위 그룹핑은 KST(
Asia/Seoul) 기준 - 캘린더 응답의
date는yyyy-MM-dd(시간 없음)
ID¶
- 모든 엔티티 PK = UUID v4 (
@UuidGenerator) - URL Path / JSON 모두 표준 36자 표기 (
9c4d401e-63ba-413e-abbe-a6d5cf869f0e)
에러 응답 — 공통 포맷¶
모든 4xx/5xx 응답은 동일한 envelope:
상태 코드별 의미:
| HTTP | 의미 | 핸들러 |
|---|---|---|
400 |
Validation 실패 (@NotBlank, @Email, @Pattern, @Size 등) |
MethodArgumentNotValidException |
400 |
JSON 파싱 실패 / 형식 오류 | HttpMessageNotReadableException |
400 |
필수 쿼리 파라미터 누락 | MissingServletRequestParameterException |
401 |
인증 누락 / 토큰 무효 / 만료 | SecurityConfig authenticationEntryPoint |
401 |
로그인 시 이메일/비밀번호 불일치 | InvalidCredentialsException |
403 |
인증은 했지만 권한 없음 (본인 글이 아님 등) | ForbiddenException 등 도메인 예외 |
404 |
리소스 없음 | PostNotFoundException 등 도메인 예외 |
409 |
충돌 (이메일/닉네임 중복) | DuplicateEmailException / DuplicateNicknameException |
500 |
서버 오류 (예상 외) | Exception fallback |
🛑 breaking change 금지: 이 envelope 모양(
{"message": "..."})을 깨면 프론트가 깨집니다. 필드 추가는 OK, 제거/이름 변경은 X.
페이지네이션¶
- 현재 미적용 (Post 전체 조회는 그냥 List 반환 — 본인 글만)
- 추후 도입 시 Spring Data 표준
Page<T>형태 (content,totalElements,totalPages,number)
인증¶
- 헤더:
Authorization: Bearer <accessToken>— 로그인 응답에서 받은 JWT - 화이트리스트 (위 "엔드포인트 요약 (한눈 표)" 참조)
- 그 외 모든 엔드포인트는 토큰 필수, 미인증 →
401 { "message": "인증이 필요합니다." } - 토큰 검증 흐름:
JwtAuthenticationFilter가 헤더 파싱 →JwtTokenProvider가 서명/만료 검증 →SecurityContext에UUID userId를 principal 로 박음 → 컨트롤러는@AuthenticationPrincipal UUID userId로 받는다 - 상세 (JWT 구조 / 시크릿 관리 / 약점) →
security.md
CORS¶
- 허용 origin 만 호출 가능. 기본값: 로컬 dev (
http://localhost:3000,http://localhost:5173) - 운영은 EC2
.env의APP_CORS_ALLOWED_ORIGINS환경변수로 S3 endpoint URL 추가 - 와일드카드(*) 허용 X —
allowCredentials=true와 충돌하므로 정확한 URL 만 - 허용 메서드:
GET / POST / PUT / DELETE / OPTIONS - 허용 헤더:
Authorization/Content-Type/Accept - preflight (OPTIONS) 캐시: 3600초
- 미허용 origin → 403
구현된 API¶
Source of truth: Swagger UI. 아래는 빠른 참조용 요약 + 의도 설명.
Auth¶
화이트리스트 — 인증 헤더 불필요. 회원가입/로그인은 인증 자체가 불가능하므로 토큰 없이 호출.
POST /auth/signup¶
회원가입. 비밀번호는 BCrypt로 해시되어 저장.
Headers
Request
email(string, required) —@Email형식 검증password(string, required) — 영문+숫자 조합 8자 이상, 특수문자 불가. 정규식^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$nickname(string, required) — 2~20자
Response — 201 Created
⚠️ 응답 바디는 UUID 문자열 한 줄 (객체 아님). FE는
response.text()또는response.json()으로 string 으로 받는다.
에러
- 400 — {"message": "이메일 형식이 올바르지 않습니다."} / {"message": "비밀번호는 영문과 숫자를 포함한 8자 이상이어야 합니다."} / {"message": "닉네임은 2자 이상 20자 이하여야 합니다."} 등
- 409 — {"message": "이미 가입된 이메일입니다."} / {"message": "이미 사용 중인 닉네임입니다."}
POST /auth/login¶
이메일/비밀번호로 로그인. 성공 시 access token (1시간 수명) + refresh token (2주 수명) 같이 발급. 이후 보호된 엔드포인트는 Authorization: Bearer <accessToken> 헤더 첨부.
Headers
Request
Response — 200 OK
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "Xy7-aBcD...",
"userId": "9c4d401e-63ba-413e-abbe-a6d5cf869f0e",
"nickname": "무디"
}
accessToken— JWT (HS256). 1시간 수명. 이후 모든 보호된 요청의Authorization헤더에Bearer <accessToken>형태로 첨부refreshToken— 32-byte secure random (base64). 2주 수명. access 만료 시POST /auth/refresh로 갱신userId— 로그인된 사용자 UUID (FE 가 캐싱해두면 편리)nickname— 로그인된 사용자의 닉네임 (전역 유니크, 최대 20자). FE 헤더/프로필 표시용. LOCAL / OAuth2 로그인 양쪽 동일하게 포함.
📌 토큰 수명 (PR 10): - access token = 1시간 (
JWT_ACCESS_EXPIRATION_MS, 기본 3,600,000ms) - refresh token = 2주 (JWT_REFRESH_EXPIRATION_MS, 기본 1,209,600,000ms) - Rotation 적용: refresh 호출마다 기존 토큰 무효화 + 새 토큰 발급 → 탈취된 refresh 가 한 번만 유효⚠️ FE 보관 정책: 두 토큰 모두 localStorage 저장 (현재 패턴). XSS 노출 시 둘 다 위험 — 향후 cookie 전환 시점은 PR 11/12 후속에서 재검토.
에러
- 400 — 이메일/비밀번호 빈 값 (@NotBlank)
- 401 — {"message": "이메일 또는 비밀번호가 올바르지 않습니다."} (이메일 미존재와 비번 틀림을 구분하지 않음 — 사용자 enumeration 방지)
POST /auth/refresh¶
Refresh token 으로 새 access + 새 refresh 발급. Rotation 적용 — 기존 refresh 는 즉시 무효화.
Headers
Request
Response — 200 OK
- 응답으로 받은 새 refresh 를 즉시 저장 + 기존 refresh 는 폐기. 동일 refresh 로 두 번 호출 시 두 번째는 401.
FE 호출 패턴:
// access 만료 401 받으면 자동 갱신 → 원래 요청 재시도
async function refreshToken() {
const stored = localStorage.getItem('refreshToken');
const res = await fetch('/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: stored })
});
if (!res.ok) {
// refresh 도 만료 → 재로그인 페이지로
return null;
}
const { accessToken, refreshToken } = await res.json();
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', refreshToken);
return accessToken;
}
에러
- 400 — refreshToken 빈 값 (@NotBlank)
- 401 — {"message": "유효하지 않은 refresh token 입니다."} (존재 X / 만료 / 이미 revoke — 정보 노출 최소화 위해 셋 다 같은 메시지)
POST /auth/logout¶
Refresh token 을 무효화. Access token 은 stateless 라 서버에서 즉시 차단 불가 — FE 는 logout 호출과 함께 localStorage 의 토큰들도 즉시 제거해야 함.
Headers
Request
Response — 204 No Content (응답 body 없음)
📌 Idempotent — 이미 무효화된 토큰 / 존재하지 않는 토큰을 보내도 204. 사용자가 logout 을 두 번 눌러도 동일.
FE 호출 패턴:
async function logout() {
const refresh = localStorage.getItem('refreshToken');
if (refresh) {
await fetch('/auth/logout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: refresh })
});
}
// BE 응답 무관하게 로컬도 즉시 클리어
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
// 로그인 페이지로 redirect
}
에러
- 400 — refreshToken 빈 값
Post¶
모든 Post 엔드포인트는 인증 헤더 필수 (
Authorization: Bearer <accessToken>). 소유권 검증 — 본인 글만 조회/수정/삭제 가능. 다른 사람 글 접근 시403.
POST /post¶
게시글 생성. 작성자는 토큰에서 추출한 현재 사용자로 자동 설정.
Headers
Request
title(string, required,@NotBlank, 최대 255자)content(string, required,@NotBlank, 최대 10000자) — DB 컬럼은TEXT. 여러 줄(개행 포함) 본문 정상 저장. 길이 초과 시 400 (T-036).postDate(stringyyyy-MM-dd, optional) — 일기 날짜 (entry date). "지나간 날짜의 일기" 작성 시 사용. 누락 시 서버가 오늘 (LocalDate.now()) 로 폴백.createdAt(작성 시점, 자동) 과 별개.
Response — 201 Created
⚠️ 응답 바디는 UUID 문자열 한 줄 (객체 아님).
에러
- 400 — {"message": "제목은 필수입니다."} / {"message": "내용은 필수입니다."} / {"message": "제목은 255자를 넘을 수 없습니다."} / {"message": "내용은 10000자를 넘을 수 없습니다."} / {"message": "요청 형식이 올바르지 않습니다."}
- 401 — 인증 누락/실패
GET /post¶
내 게시글 전체 조회. 다른 사용자의 글은 절대 포함 안 됨. 정렬(기본 일기날짜 최신순) + 선택적 필터 지원.
Headers
Query Parameters (전부 선택)
| 파라미터 | 타입 | 기본값 | 설명 |
|---|---|---|---|
from |
LocalDate (yyyy-MM-dd) |
없음 | 일기 날짜(postDate) 하한 (inclusive). 한쪽만 줘도 됨. |
to |
LocalDate (yyyy-MM-dd) |
없음 | 일기 날짜(postDate) 상한 (inclusive). |
keyword |
String |
없음 | 제목/내용 부분일치 (대소문자 무시). |
sort |
String |
postDate,desc |
필드,방향 형식. 필드: postDate/createdAt, 방향: asc/desc. |
예: GET /post?from=2026-05-01&to=2026-05-31&keyword=여행&sort=postDate,desc
Response — 200 OK
[
{
"id": "9c4d401e-63ba-413e-abbe-a6d5cf869f0e",
"title": "오늘의 기분",
"content": "오늘은 기분이 좋았다.",
"postDate": "2026-05-29"
}
]
빈 결과면 []. 같은 postDate 가 여러 건이면 createdAt 내림차순 → id 오름차순으로 안정 정렬(tiebreaker).
에러
- 400 — 잘못된 정렬 필드/방향 (sort=content,desc 등), 날짜 범위 역전 (from > to), 또는 날짜 형식 오류 (from=abc)
- 401 — 인증 누락/실패
📌 페이지네이션 없음 (TODO — 도입 시 응답이
Page<>wrapper 로 바뀌는 breaking change라 FE 계약 합의 필요). 📌 인덱스 권장:post(user_id, post_date)복합 인덱스. 기본 정렬이 postDate 라 user_id 필터 + postDate 정렬을 한 인덱스로 처리.ddl-auto: update는 인덱스를 보장하지 않으므로 운영 트래픽 증가 시 수동 DDL 필요.
GET /post/{id}¶
단건 조회. 본인 글만 가능.
Headers
Path
- id — UUID
Response — 200 OK
{
"id": "9c4d401e-63ba-413e-abbe-a6d5cf869f0e",
"title": "오늘의 기분",
"content": "오늘은 기분이 좋았다.",
"postDate": "2026-05-29"
}
에러
- 401 — 인증 누락/실패
- 403 — {"message": "본인 글만 조회할 수 있습니다."} (다른 사람 글)
- 404 — {"message": "게시글을 찾을 수 없습니다. id=<uuid>"}
PUT /post/{id}¶
수정 (전체 대체). 본인 글만 수정 가능.
Headers
Request
- body는POST /post와 동일한 검증 + postDate 도 동일 시맨틱 (선택, 누락 시 오늘 폴백). 날짜 잘못 적었을 때 수정 가능.
Response — 200 OK
{
"id": "9c4d401e-63ba-413e-abbe-a6d5cf869f0e",
"title": "수정된 제목",
"content": "수정된 본문",
"postDate": "2026-05-28"
}
📌 POST 와 달리 객체 반환 (UUID 문자열 아님).
에러
- 400 — validation 실패
- 401 — 인증 누락/실패
- 403 — 본인 글 아님
- 404 — 존재하지 않는 ID
DELETE /post/{id}¶
삭제. 본인 글만 가능.
Headers
Response — 204 No Content 바디 없음.
에러
- 401 — 인증 누락/실패
- 403 — 본인 글 아님
- 404 — 존재하지 않는 ID
Calendar¶
GET /calendar¶
월별 캘린더 데이터 — 그 달 전체 일자를 배열로 반환. 글 없는 날도 postId: null, emoji: null 로 포함하므로 FE 는 인덱스로 격자에 바로 매핑할 수 있다.
🚧 현재 시점:
emoji는 항상null.Post + AiResponse JOIN은 PR 4 (AI 비동기 응답) 머지 후 후속 PR 에서 추가. FE 는emoji == null이면 placeholder(회색 점 등) 로 처리.
Headers
Query
- year (int, required, 범위 2020 ~ 현재+1)
- month (int, required, 1-12)
예시 호출
Response — 200 OK (예: 2026-05, 현재 시점)
[
{ "date": "2026-05-01", "postId": null, "emoji": null },
{ "date": "2026-05-02", "postId": "9c4d401e-63ba-413e-abbe-a6d5cf869f0e", "emoji": null },
{ "date": "2026-05-03", "postId": "ab12cd34-1111-2222-3333-444455556666", "emoji": null },
...
{ "date": "2026-05-31", "postId": null, "emoji": null }
]
Response — 200 OK (PR 4 + 후속 emoji JOIN 머지 후 예상)
[
{ "date": "2026-05-02", "postId": "9c4d401e-...", "emoji": "😊" },
{ "date": "2026-05-03", "postId": "ab12cd34-...", "emoji": "😢" }
]
규칙:
- 응답 배열 길이 = 그 달의 실제 일수 (28/29/30/31)
- 하루에 여러 글이 있으면 마지막 글(created_at MAX)의 postId 만 노출
- 일자 그룹핑은 KST 기준 (Asia/Seoul)
- AI 응답이 PENDING/FAILED이거나 PR 4 미구현 → emoji: null (postId는 채워짐)
에러
- 400 — 잘못된 year/month 값 ({"message": "year는 2020 이상, month는 1-12 사이여야 합니다."})
- 400 — 필수 쿼리 파라미터 누락 ({"message": "필수 파라미터가 누락되었습니다: year"})
- 401 — 인증 누락/실패
AI Response (🚧 Stub — PR 4-pre)¶
🚧 운영 기본값 = Stub 모드 (
ai.client.mode=stub): 실제 AI 서버 호출 없이 고정 응답을 즉시 반환.POST /post직후 거의 즉시DONE으로 전이되어 폴링 한 번이면 결과 도달.HTTP 어댑터 구현됨 (
ai.client.mode=http):HttpAiResponseClient가{AI_SERVER_URL}/chat으로 POST + 타임아웃 + 4xx/5xx/파싱실패 → FAILED. AI 서버 배포됨 (Hugging Face Space) — 외부 계약은 아래 외부 시스템 계약 섹션 참조. 운영 전환은AI_CLIENT_MODE=http+AI_SERVER_URL설정.
GET /post/{id}/ai-response¶
사용자가 일기를 작성하면 즉시 POST /post 가 201 을 반환하고, AI 응답은 비동기로 처리된다. 프론트는 이 엔드포인트를 폴링해서 완료 여부를 확인.
흐름: POST /post (201, 일기 저장 + AiResponse(PENDING) 동시 저장 + 비동기 트리거) → FE 폴링 → status: PENDING 또는 DONE / FAILED.
Headers
Response — 200 OK (상태에 따라)
status: PENDING — Async 워커가 아직 결과를 못 받은 상태. errorMessage 키는 응답에 없음 (@JsonInclude(NON_NULL)).
{
"postId": "9c4d401e-...",
"status": "PENDING",
"content": null,
"emotion": null,
"homeComment": null
}
status: DONE — 추론 성공. errorMessage 키는 응답에 없음.
{
"postId": "9c4d401e-...",
"status": "DONE",
"content": "오늘 기분이 좋으셨군요! 그 순간을 더 자세히 떠올려보세요.",
"emotion": "happy",
"homeComment": "좋은 하루였네요!"
}
status: FAILED — 추론 실패. content / emotion / homeComment 는 null, errorMessage 에 사유.
{
"postId": "9c4d401e-...",
"status": "FAILED",
"content": null,
"emotion": null,
"homeComment": null,
"errorMessage": "AI 서버 응답 시간 초과"
}
필드 명시 정책: errorMessage 는 FAILED 일 때만 응답에 포함된다 (@JsonInclude(NON_NULL)). PENDING / DONE 응답에는 errorMessage 키 자체가 없다. content / emotion / homeComment 는 비대칭 없이 모든 상태에서 키가 있고 null 일 수 있음.
⚠️ FE 계약 변경 (2026-06-12): 기존
emoji(😊 유니코드) 필드가 제거되고emotion(감정 라벨, 예"happy") +homeComment(홈화면 문장) 로 교체됨. AI 서버(/chat) 응답 구조에 맞춤.
에러
- 401 — 인증 누락/실패
- 403 — 본인 글이 아님 (Post 존재 노출 방지를 위해 404 보다 우선 검증)
- 404 — postId 가 존재하지 않음. 메시지: "AI 응답을 찾을 수 없습니다. postId=<uuid>"
📌 FE 폴링 정책 합의 필요: 1초 간격? 지수 백오프? → 합의 후 결정. Stub 단계에선 거의 즉시 DONE 이라 폴링 1~2회로 충분.
외부 시스템 계약¶
우리가 호출하는 서버의 API. Swagger에는 안 나옴. 여기서 합의 → 변경 시 양쪽 동기화.
AI 추론 서버¶
✅ 배포됨 — Hugging Face Space.
HttpAiResponseClient(ai.client.mode=http) 가 호출. 운영 기본값은stub이라AI_CLIENT_MODE=http+AI_SERVER_URL설정 시 활성. - Base URL:https://dlqudwn153-moo-diary-ai-prompt.hf.space- Swagger:/docs- 인증: 없음 (공개 Space)
우리가 보낼 요청¶
POST {AI_SERVER_URL}/chat
Content-Type: application/json
{
"user_text": "오늘은 기분이 좋았다. 친구를 만나서...",
"recent_emotions": "",
"diary_date": "6월 11일"
}
user_text(string, required) — 일기 본문. 빈 문자열 금지 (우리 측@NotBlank로 입구에서 차단).recent_emotions(string) — 최근 감정 요약. 현 단계 미사용 → 항상 빈 문자열 (AI 담당자 합의).diary_date(string) — 일기 날짜를"M월 d일"한국어 포맷으로 (예:"6월 11일"). 우리post.postDate를 변환.
우리가 기대하는 응답 (성공)¶
{
"emotion": "neutral",
"aiText": "AI가 생성한 공감/분석 문장",
"homeComment": "홈화면에 보여줄 짧은 문장",
"diaryDate": "6월 11일"
}
aiText(string, required) — 사용자 일기에 대한 AI 공감/분석 본문. 우리 DBai_response_content로 매핑.emotion(string, required) — 감정 라벨 (예:"neutral","happy"). DBai_response_emotion.homeComment(string) — 홈화면용 짧은 문장. DBai_response_home_comment. 누락 시 빈 문자열로 관용.diaryDate(string) — 요청 날짜 에코백. 우리는 사용 안 함 (무시).
필수 필드 검증:
aiText또는emotion누락 시 파싱 실패 → FAILED.homeComment만 없으면 빈 문자열로 진행.
운영 메모¶
- 응답 지연: HF 무료 Space 는 절전(cold start) 후 첫 호출이 느리고, 내부 SAIFEX + 감정 모델 다단계라 응답이 수~수십 초.
AI_TIMEOUT_MS기본 30초 (env 로 조정). - env:
AI_CLIENT_MODE/AI_SERVER_URL/AI_TIMEOUT_MS→ compose.yaml + EC2.env+ GitHub Secrets 3곳 동기화 (누락 시 stub/localhost 기본값으로 조용히 동작).
우리 측 실패 처리¶
| 시나리오 | 우리 행동 |
|---|---|
| 응답 시간 초과 (기본 30초) | AiResponse.status = FAILED, errorMessage = "AI 서버 호출 실패: ..." |
| HTTP 5xx 응답 | 재시도 없이 FAILED (일시 장애) |
| HTTP 4xx 응답 | 재시도 없이 FAILED (요청 거절) |
응답 파싱 실패 (aiText/emotion 누락) |
FAILED, errorMessage 에 파싱 에러 |
→ 일기 자체는 무조건 저장 성공. AI 응답 실패는 일기 작성 흐름을 막지 않음.
📝 Changelog¶
| 일자 | 변경 |
|---|---|
| 2026-05-24 | 신설. 공통 규약 / 구현된 Post CRUD 5개 / 예정 API (Auth, AI 응답 폴링, Calendar) / 외부 AI 서버 계약 명세 |
| 2026-05-25 | PR 5 Calendar API 진행 중. GET /calendar?year=YYYY&month=MM 구현 — emoji 는 PR 4 머지 전까지 항상 null 임시 처리. 공통 규약에 MissingServletRequestParameterException 400 매핑 명시. |
| 2026-05-26 | FE 공유용 정비. 엔드포인트 요약 표 추가 (URL/Method/인증 헤더/Body/응답 한눈에). Auth (/auth/signup, /auth/login) 와 Calendar (/calendar) 를 "예정" → "구현된 API" 로 이동. Post CRUD 에 실제 구현된 인증 헤더 (Authorization: Bearer) + 소유권 401/403 매핑 반영. 회원가입 비밀번호 정책 (^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$) 명시. 로그인 응답에 userId 포함 명시. 409 (이메일/닉네임 중복) 에러 코드 추가. 공통 헤더 섹션 신설. |
| 2026-05-27 | PR 4-pre 머지. GET /post/{id}/ai-response 를 "예정 API" → "구현된 API (🚧 Stub 모드)" 로 이동. Stub 박스 + errorMessage 분기 정책 (@JsonInclude(NON_NULL), FAILED 만 포함) 명시. 엔드포인트 요약 표에 "🚧 Stub" 표시. 외부 시스템 계약 섹션 헤더 "PR 4" → "PR 4-final" 로 명확화. POST /post 가 일기 + AiResponse(PENDING) 같은 트랜잭션 저장 후 비동기 트리거하는 흐름 추가. |
| 2026-06-09 | AI 서버 HTTP 어댑터 (PR 4-final) 구현 — AiResponseClient 인터페이스화 + StubAiResponseClient/HttpAiResponseClient + ai.client.mode 토글 (OAuth2 패턴). 외부 계약에 userId 추가({userId,postId,title,content} → {message,emoji}), 인증 보류, 잠정 명시. 운영 기본값 stub 유지(AI 서버 미배포). |
| 2026-06-09 | GET /post 정렬/필터 추가. Query Parameters 표 (from/to/keyword/sort, 전부 선택) + 기본 postDate,desc + 안정 tiebreaker(createdAt desc → id asc) 명시. 400 에러 (잘못된 정렬·범위 역전·날짜 형식) 추가. 엔드포인트 요약 표 Body 칸 갱신. 인덱스 권장 (post(user_id, post_date)) 노트. 페이지네이션은 여전히 TODO (도입 시 Page<> breaking change). |
| 2026-06-12 | AI 서버 실연동 계약 확정 (/chat) — AI 서버 배포됨(Hugging Face Space). 외부 계약 /inference {userId,postId,title,content} → {message,emoji} → /chat {user_text, recent_emotions:"", diary_date} → {emotion, aiText, homeComment, diaryDate}. 폴링 응답 DTO 의 emoji → emotion+homeComment 교체 (FE 계약 변경). recent_emotions 는 미사용(빈 문자열), diary_date 는 "M월 d일" 포맷. timeout 기본 10s→30s (HF cold start). 운영 env AI_* 3개를 compose+.env+Secrets 에 추가 필요. |