이 문서는 Pingdom Backend가 2026-08-10 기준으로 실제 반환하거나 기록하는 오류 코드와 재시도 동작을 구분해 설명한다. 클라이언트, 운영자, 외부 연동 handler가 오류를 같은 의미로 해석하고, 구현보다 강한 전달 보장이나 재시도 약속을 문서로 선언하지 않는 것이 목적이다.
관리자 Outbox 운영 API는 최종 실패 event의 조회·재처리만 제공한다. 비내구성 Spring 이벤트의 일반 재생이나 외부 부수효과의 exactly-once 처리를 보장하지 않는다.
관련 문서:
- v1 API 경로 정책
- 운영 관측성
- DB migration 운영 Runbook
- Pingdom 2.0 목표 아키텍처와 도메인 이벤트
- Pingdom 2.0 출시 전환·적용·복구 Runbook
| 구분 | 책임 | 식별자 예시 | 재시도 의미 |
|---|---|---|---|
| HTTP API 오류 코드 | Controller, Security, Controller Advice의 클라이언트 응답 분류 | INVALID_TOKEN, RATE_LIMIT_EXCEEDED |
클라이언트가 요청을 다시 보낼지 판단하는 근거다. 서버의 자동 재시도를 뜻하지 않는다. |
| Outbox 상태 | 외부 부수효과 작업의 worker 처리 상태 | PENDING, RETRY, FAILED |
worker가 동일 Outbox event를 다시 처리할지 나타낸다. |
| Notification delivery 오류 코드 | 이메일·FCM 발송 결과의 내부 기록 분류 | EMAIL_SEND_FAILED, FCM_INVALID_TOKEN |
발송 결과 조회·운영 분석용이며 Outbox 재시도를 직접 제어하지 않는다. |
| Provider 오류 코드 | Firebase·Postmark 등이 반환한 원본 코드 | provider별 문자열 | 내부 코드와 별도로 기록하며 API 계약 코드로 노출하지 않는다. |
HTTP API 오류와 비동기 delivery 결과는 같은 요청에서 함께 관찰될 수 있어도 서로 다른 계약이다. 좋아요 요청이 성공한 뒤 FCM 발송이 실패하면 HTTP 성공 여부와 notification delivery 실패 기록을 분리해 판단한다.
| 발생 경로 | 현재 응답 본문 | 비고 |
|---|---|---|
| 도메인 예외, rate-limit 예외, JWT 인증·인가 실패 | {"message":"...","code":"..."} |
code는 클라이언트 분기용 문자열이다. |
| Bean Validation 실패 | {"message":"입력값을 확인해주세요.","errors":{"field":"..."}} |
공통 code는 없다. |
| ConstraintViolation, 일반 DataIntegrityViolation, ResponseStatusException | {"message":"..."} |
일부 제약 조건은 409 코드로 변환되지만, 그 외에는 code가 없다. |
따라서 모든 실패 응답에 code가 있다고 가정해서는 안 된다. OpenAPI의
ErrorResponse schema도 일부 Controller 문서에만 사용되므로, 현재 전체
API의 단일 오류 schema를 보장하지 않는다.
아래 상태 코드는 각 enum 또는 Security handler가 현재 설정한 값이다. 메시지는 사용자
표시 문구이므로 제어 흐름은 메시지가 아닌 상태 코드와 code으로 판단한다.
| 영역·정의 위치 | HTTP 상태 | 코드 |
|---|---|---|
인증·계정 AuthErrorCode |
401 | INVALID_CREDENTIALS, INVALID_TOKEN, EXPIRED_TOKEN, OAUTH_LINK_TOKEN_INVALID |
인증·계정 AuthErrorCode |
403 | ADMIN_ACCESS_REQUIRED, USER_BANNED, USER_WITHDRAWN |
인증·계정 AuthErrorCode |
400 | INVALID_PASSWORD_RESET_TOKEN, EXPIRED_PASSWORD_RESET_TOKEN, PASSWORD_MISMATCH, INVALID_EMAIL_VERIFICATION_CODE, EXPIRED_EMAIL_VERIFICATION_CODE |
인증·계정 AuthErrorCode |
409 | EMAIL_ALREADY_VERIFIED, DUPLICATE_USERNAME, DUPLICATE_EMAIL, OAUTH_EMAIL_CONFLICT, OAUTH_EMAIL_MISMATCH, OAUTH_ACCOUNT_ALREADY_LINKED, OAUTH_PASSWORD_CONFIRMATION_REQUIRED, OAUTH_LOCAL_PASSWORD_REQUIRED |
인증·계정 AuthErrorCode |
404 | OAUTH_ACCOUNT_NOT_LINKED, USER_NOT_FOUND |
| JWT Security handler | 401 | INVALID_TOKEN, EXPIRED_TOKEN |
| JWT Security handler | 403 | ACCESS_DENIED |
사용자 UsersErrorCode |
400 | PASSWORD_MISMATCH, INVALID_TRAVEL_SCHEDULE_PERIOD |
사용자 UsersErrorCode |
404 | USER_NOT_FOUND, TRAVEL_SCHEDULE_NOT_FOUND |
사용자 UsersErrorCode |
409 | USERNAME_ALREADY_EXISTS, TRAVEL_SCHEDULE_NOT_EDITABLE, TRAVEL_SCHEDULE_CONCURRENT_MODIFICATION |
Merchant Owner MerchantOwnerErrorCode |
400 | ADMIN_ACCOUNT_NOT_ALLOWED, INVALID_ONBOARDING_METRIC, INVALID_OPERATIONAL_QUALITY_METRIC, INVALID_PLACE_INFORMATION |
Merchant Owner MerchantOwnerErrorCode |
403 | USER_ACCOUNT_NOT_ELIGIBLE, ACTIVE_OWNER_REQUIRED, PLACE_CLAIMANT_NOT_ELIGIBLE |
Merchant Owner MerchantOwnerErrorCode |
404 | PROFILE_NOT_FOUND, PLACE_NOT_FOUND, OWNER_PLACE_NOT_FOUND, VERIFICATION_NOT_FOUND, PLACE_CLAIM_NOT_FOUND, PLACE_INFORMATION_NOT_FOUND |
Merchant Owner MerchantOwnerErrorCode |
409 | PROFILE_ALREADY_EXISTS, INVALID_PROFILE_STATE, PLACE_ALREADY_ASSIGNED, PLACE_ALREADY_ASSIGNED_TO_REQUESTER, PLACE_OWNERSHIP_CHANGED, VERIFICATION_ALREADY_EXISTS, INVALID_VERIFICATION_STATE, VERIFICATION_REQUIRED, PLACE_CLAIM_ALREADY_PENDING, INVALID_PLACE_CLAIM_STATE |
지도·게시글 MapErrorCode |
400 | PLACE_ID_REQUIRED, PLACE_SEARCH_CONDITION_INVALID, UNSUPPORTED_PLACE_SEARCH_SORT, IMAGE_FILE_EMPTY, IMAGE_FILE_TOO_LARGE, UNSUPPORTED_IMAGE_TYPE, INVALID_IMAGE_FILE, IMAGE_RESOLUTION_TOO_LARGE, ALREADY_LIKED, NOT_LIKED, ALREADY_POSTED |
지도·게시글 MapErrorCode |
403 | REPORTER_RESTRICTED, REPORT_APPEAL_NOT_ALLOWED, OTHERS_NOT_DELETED, OTHERS_NOT_UPDATE, OTHERS_PLACE_NOT_DELETED, OTHERS_PLACE_MEDIA_NOT_MANAGED, PLACE_INFORMATION_REPORT_FORBIDDEN, PLACE_INFORMATION_DISPUTE_FORBIDDEN |
지도·게시글 MapErrorCode |
404 | IMAGE_NOT_FOUND, REPORT_NOT_FOUND, PLACE_NOT_FOUND, PLACE_MEDIA_NOT_FOUND, PLACE_EVENT_NOT_FOUND, PLACE_INFORMATION_REPORT_NOT_FOUND, PLACE_INFORMATION_DISPUTE_NOT_FOUND, RECOMMENDATION_EXPLANATION_NOT_FOUND, BOOKMARK_NOT_FOUND |
지도·게시글 MapErrorCode |
409 | ALREADY_REPORTED_IMAGE, REPORT_APPEAL_ALREADY_EXISTS, PLACE_ALREADY_EXISTS, PLACE_INFORMATION_REPORT_ALREADY_SUBMITTED, FAVORITE_ALREADY_EXISTS, BOOKMARK_ALREADY_EXISTS |
지도·게시글 MapErrorCode |
500 | DELETE_ERROR, S3_NOT_CONFIGURED, S3_CONNECTION_ERROR, UPLOAD_ERROR |
알림 NotificationsErrorCode |
400 | CANNOT_SEND_NOTIFICATION_TO_SELF, INVALID_FCM_TOKEN, INVALID_NOTIFICATION_TIMEZONE, INVALID_QUIET_HOURS |
알림 NotificationsErrorCode |
404 | FCM_TOKEN_NOT_FOUND, NOTIFICATION_NOT_FOUND |
알림 NotificationsErrorCode |
500 | NOTIFICATION_SEND_FAILED |
Offer OfferErrorCode |
400 | INVALID_OFFER_PERIOD, INVALID_OFFER_INPUT |
Offer OfferErrorCode |
403 | PLACE_NOT_OWNED, TOURIST_ELIGIBILITY_REQUIRED |
Offer OfferErrorCode |
404 | OFFER_NOT_FOUND, COUPON_NOT_FOUND |
Offer OfferErrorCode |
409 | INVALID_OFFER_STATE, OFFER_NOT_AVAILABLE, OFFER_SOLD_OUT, COUPON_ALREADY_ISSUED, COUPON_NOT_REDEEMABLE |
예약 가능 시간 AvailabilityErrorCode |
400 | INVALID_AVAILABILITY_INPUT |
예약 가능 시간 AvailabilityErrorCode |
403 | PLACE_NOT_OWNED |
예약 가능 시간 AvailabilityErrorCode |
404 | AVAILABILITY_NOT_FOUND |
예약 가능 시간 AvailabilityErrorCode |
409 | AVAILABILITY_ALREADY_EXISTS, INVALID_AVAILABILITY_STATE, AVAILABILITY_CAPACITY_EXCEEDED |
예약 ReservationErrorCode |
400 | INVALID_RESERVATION_INPUT |
예약 ReservationErrorCode |
403 | RESERVATION_FORBIDDEN, TOURIST_ACCOUNT_REQUIRED |
예약 ReservationErrorCode |
404 | RESERVATION_NOT_FOUND |
예약 ReservationErrorCode |
409 | INVALID_RESERVATION_STATE, IDEMPOTENCY_KEY_REUSED |
방문자 검증 VisitorVerificationErrorCode |
400 | LOCATION_OBSERVATION_EXPIRED, LOCATION_TOO_INACCURATE, VISIT_EVIDENCE_FILE_EMPTY, VISIT_EVIDENCE_FILE_INVALID, INVALID_REPORT_DETAILS, INVALID_REVIEW, INVALID_CORRECTION_DETAILS, INVALID_CORRECTION_REVIEW, INVALID_SCOUT_PROFILE_DETAILS, INVALID_SCOUT_ACTIVITY_ELIGIBILITY_PERIOD |
방문자 검증 VisitorVerificationErrorCode |
403 | REPORT_FORBIDDEN, ADMIN_ACCOUNT_REQUIRED, TOURIST_ACCOUNT_REQUIRED, CORRECTION_FORBIDDEN, SCOUT_PROFILE_ACCOUNT_REQUIRED, SCOUT_PROFILE_FORBIDDEN, SCOUT_ACTIVITY_NOT_ELIGIBLE |
방문자 검증 VisitorVerificationErrorCode |
404 | REPORT_NOT_FOUND, PLACE_NOT_FOUND, CHECK_IN_NOT_FOUND, VISIT_EVIDENCE_NOT_FOUND, CORRECTION_NOT_FOUND, SCOUT_PROFILE_NOT_FOUND, SCOUT_ACTIVITY_ELIGIBILITY_NOT_FOUND |
방문자 검증 VisitorVerificationErrorCode |
409 | DAILY_CHECK_IN_ALREADY_EXISTS, VISIT_EVIDENCE_ALREADY_EXISTS, ACTIVE_REPORT_ALREADY_EXISTS, INVALID_REPORT_STATE, CORRECTION_NOT_ALLOWED, ACTIVE_CORRECTION_ALREADY_EXISTS, SCOUT_PROFILE_ALREADY_EXISTS, INVALID_SCOUT_PROFILE_STATE, SCOUT_ACTIVITY_PROFILE_REQUIRED, INVALID_SCOUT_ACTIVITY_ELIGIBILITY_STATE |
방문자 검증 VisitorVerificationErrorCode |
413 | VISIT_EVIDENCE_FILE_TOO_LARGE |
방문자 검증 VisitorVerificationErrorCode |
422 | OUTSIDE_CHECK_IN_RADIUS |
방문자 검증 VisitorVerificationErrorCode |
503 | VISIT_EVIDENCE_STORAGE_UNAVAILABLE |
관리자 AdminErrorCode |
400 | ADMIN_ROLE_ASSIGNMENT_INVALID, PLACE_MERGE_INVALID_REQUEST, PLACE_OPERATING_STATUS_INVALID_REQUEST, PLACE_DISCOVERY_STATUS_INVALID_REQUEST, PLACE_INFORMATION_VERIFICATION_INVALID_REQUEST, PLACE_INFORMATION_REPORT_INVALID_REQUEST, PLACE_INFORMATION_DISPUTE_INVALID_REQUEST, PLACE_OPERATING_SCHEDULE_INVALID_REQUEST, PLACE_EVENT_INVALID_PERIOD, RECOMMENDATION_TRAFFIC_POLICY_INVALID_REQUEST, RECOMMENDATION_TRAFFIC_POLICY_TOTAL_INVALID, RECOMMENDATION_METRIC_QUERY_TOO_LARGE, TRUST_SCORE_INTERVENTION_RULE_INVALID_REQUEST, AD_INVALID_PERIOD, UNSUPPORTED_PLACE_SORT_PARAM, INVALID_SANCTION_PERIOD, INVALID_SANCTION_FILTER_PERIOD, INVALID_AUDIT_LOG_FILTER_PERIOD, INVALID_NOTIFICATION_DELIVERY_FILTER_PERIOD |
관리자 AdminErrorCode |
403 | ADMIN_PERMISSION_REQUIRED |
관리자 AdminErrorCode |
404 | ADMIN_ROLE_ASSIGNMENT_NOT_FOUND, ADMIN_TARGET_USER_NOT_FOUND, POST_NOT_FOUND, PLACE_NOT_FOUND, PLACE_DUPLICATE_NOT_FOUND, PLACE_INFORMATION_EVIDENCE_NOT_FOUND, PLACE_INFORMATION_REPORT_NOT_FOUND, PLACE_INFORMATION_DISPUTE_NOT_FOUND, PLACE_EVENT_NOT_FOUND, PLACE_MERGE_HISTORY_NOT_FOUND, RECOMMENDATION_EXPLANATION_NOT_FOUND, RECOMMENDATION_TRAFFIC_POLICY_VERSION_NOT_FOUND, TRUST_SCORE_ANOMALY_NOT_FOUND, TRUST_SCORE_REPORTER_POLICY_NOT_FOUND, TRUST_SCORE_INTERVENTION_RULE_NOT_FOUND, AD_NOT_FOUND, REPORT_NOT_FOUND, APPEAL_NOT_FOUND |
관리자 AdminErrorCode |
409 | ADMIN_ROLE_ASSIGNMENT_CONFLICT, PLACE_INFORMATION_REPORT_ALREADY_SUBMITTED, PLACE_EVENT_UPDATE_NOT_ALLOWED, PLACE_EVENT_PUBLISH_NOT_ALLOWED, PLACE_EVENT_CANCEL_NOT_ALLOWED, PLACE_EVENT_CONNECTED, PLACE_CHECK_IN_CONNECTED, PLACE_MERGE_NOT_ALLOWED, PLACE_MERGE_ALREADY_RESTORED, PLACE_MERGE_RESTORE_NOT_ALLOWED, PLACE_KAKAO_PLACE_ID_CONFLICT, TRUST_SCORE_INTERVENTION_RULE_DUPLICATED, REPORT_ALREADY_PROCESSED, APPEAL_ALREADY_PROCESSED, USER_NOT_BANNED, PENDING_REPORT_NOT_FOUND |
관리자 AdminErrorCode |
500 | AUDIT_LOG_WRITE_FAILED, RECOMMENDATION_POLICY_HISTORY_WRITE_FAILED, POST_DELETE_FAILED, S3_NOT_CONFIGURED, S3_CONNECTION_ERROR, S3_REPORT_FAILED |
| 요청 제한 | 429 | RATE_LIMIT_EXCEEDED |
| 요청 제한 저장소 장애 | 503 | RATE_LIMIT_UNAVAILABLE |
위 enum 기반 예외는 GlobalExceptionHandler의 전용 handler를 통해
{"message":"...","code":"..."} 형태로 반환된다. 새 도메인 예외를 추가할 때는
enum, exception, Controller Advice handler, OpenAPI 오류 문서가 함께 갱신됐는지 확인한다.
전체 오류 응답 표준화는 Controller Advice와 OpenAPI baseline에 영향을 주므로 별도 구현
이슈에서 결정한다.
DataIntegrityViolation 중 사용자명 중복, OAuth 계정 연결 중복, 지도 북마크 중복은 409와
대응 코드로 변환된다. 장소별 게시글 중복은 ALREADY_POSTED의 선언 상태인
400과 대응 코드로 변환된다. 그 외의 무결성 오류는 500과 메시지만 반환한다.
서버는 일반 HTTP 요청의 재시도 횟수, backoff, 멱등성 키를 제공하지 않는다. 아래 표는 현재 응답을 받은 클라이언트의 안전한 기본 판단이며, 새로운 자동 재시도 계약을 추가하는 정책은 아니다.
| 응답 | 기본 처리 | 재시도 판단 |
|---|---|---|
| 400, 404, 409, 413, 422 또는 validation 오류 | 요청 값·현재 리소스 상태를 수정한다. | 자동 재시도하지 않는다. |
401 INVALID_TOKEN, EXPIRED_TOKEN |
인증 정보를 갱신하거나 로그인 흐름을 수행한다. | 인증 복구 후에도 원래 요청이 조회 또는 멱등한 요청일 때만 다시 보낸다. |
| 403 | 권한·사용자 상태를 확인한다. | 자동 재시도하지 않는다. |
429 RATE_LIMIT_EXCEEDED |
요청 빈도를 낮춘다. | 현재 Retry-After 헤더가 없으므로 서버가 대기 시간을 약속하지 않는다. 조회 또는 멱등한 요청에 한해 제품별 제한된 backoff를 적용할 수 있다. |
503 RATE_LIMIT_UNAVAILABLE 또는 저장소 일시 장애 |
제한 저장소·외부 저장소·의존성 장애로 본다. | 조회 또는 멱등한 요청에 한해 횟수가 제한된 backoff 재시도를 검토한다. 상태 변경 요청은 중복 효과를 먼저 검토한다. |
| 500 | 서버·외부 의존성 오류로 분류하고 요청 ID와 오류 코드를 보존한다. | 일반적인 자동 재시도 계약이 아니다. 같은 상태 변경 요청을 즉시 반복하지 않는다. |
Outbox worker는 5초 주기로 준비된 event를 선점한다. 설정은
outbox.max-attempts=5, base-backoff=PT10S,
max-backoff=PT10M, processing-timeout=PT5M이다.
PENDING 또는 RETRY
│ worker가 선점
▼
PROCESSING ── 성공 ──► SUCCEEDED
│
├─ 실패 1~4회: 10초, 20초, 40초, 80초 지연 후 RETRY
└─ 실패 5회: FAILED
- 5분을 넘긴
PROCESSINGevent는 stale recovery 과정에서 같은 실패·backoff 규칙을 적용한다. FAILEDevent는OUTBOX_RECOVERY권한을 가진 관리자가POST /admin/outbox-events/{eventId}/retry로 시도 횟수를 초기화해RETRY로 전환할 수 있다. 장애 원인과 중복 외부 효과 안전성을 먼저 확인하고 사유를 입력해야 하며, 동일 event의 반복 요청은 409로 거절된다.deduplication_key는 중복 event 저장을 막을 뿐 외부 공급자 호출의 exactly-once 처리를 보장하지 않는다.
| 처리 | 실패 시 동작 | delivery 기록과 Outbox의 관계 |
|---|---|---|
| 이메일 인증·비밀번호 재설정 | handler가 예외를 기록한 뒤 다시 던진다. worker가 Outbox를 RETRY 또는 FAILED로 전이한다. |
RETRY_SCHEDULED delivery 기록은 최대 시도 횟수에서 FINAL_FAILED가 될 수 있다. |
| 좋아요 FCM의 무효 토큰 | 토큰을 삭제하고 실패를 기록한다. | handler 예외를 전파하지 않으므로 해당 Outbox event는 성공 처리될 수 있다. |
| 좋아요 FCM의 일시 실패 | retryable=true으로 실패를 기록한다. |
현재 FCM service가 예외를 잡아 전파하지 않으므로 retryable=true은 Outbox 재시도 예약을 뜻하지 않는다. |
| payload 역직렬화 실패 | handler가 예외를 던진다. | event는 일반 Outbox 실패 정책을 따른다. |
notification delivery의 retryable, attempt_count,
RETRY_SCHEDULED, FINAL_FAILED는 발송 결과 관찰용 모델이다.
이 값은 Outbox worker의 상태 전이를 직접 제어하지 않는다. FCM 일시 실패도 실제 Outbox
재시도로 처리해야 한다면 중복 전송, 트랜잭션 경계, notification 생성 중복을 검토하는
별도 구현 이슈가 필요하다.
| 대상 | 현재 기준 | #809 반영 내용 |
|---|---|---|
| HTTP API·OpenAPI | 오류 본문과 Security 응답은 기존 구현을 따른다. | GET /admin/outbox-events 조회와 POST /admin/outbox-events/{eventId}/retry 명령을 OpenAPI baseline에 추가한다. |
| Flyway | V5__create_outbox_event.sql이 Outbox 상태·시도 정보를 저장한다. |
V90에서 상태·시도 횟수 제약과 관리자 조회 인덱스를 추가하며 기존 row 값은 변경하지 않는다. |
| 운영 | Outbox metric, handler 로그, 관리자 감사 이력이 실패 분석의 근거다. | 수동 재처리 결과를 pingdom.outbox.manual_retry로 기록하고 payload는 API·감사 로그에서 제외한다. |
| 점검 항목 | 대조 위치 | 결과 |
|---|---|---|
| enum 기반 도메인 예외 응답 | GlobalExceptionHandler, 각 *ErrorCode enum |
전용 handler가 있는 도메인은 message, code를 반환한다. |
| Validation, ConstraintViolation, ResponseStatusException | GlobalExceptionHandler |
일부 실패 응답에는 code가 없으므로 클라이언트는 상태 코드와 본문 형태를 함께 처리해야 한다. |
| 요청 제한 오류 | RateLimitException, RateLimitUnavailableException |
429와 503은 코드가 있지만 현재 Retry-After 헤더 계약은 없다. |
| Outbox 재시도 설정 | application.yaml, OutboxEvent, OutboxEventWorker |
5초 선점 주기, 최대 5회, 10초 기반 backoff, 10분 최대 backoff, 5분 stale recovery 기준을 따른다. |
| Outbox 운영 복구 | AdminOutboxEventController, AdminOutboxEventRecoveryService |
FAILED만 행 잠금 후 RETRY로 전환하며 권한·사유·감사 이력을 요구한다. |
| notification delivery 기록 | NotificationDeliveryRecorder, NotificationDeliveryRecordWriter, AdminNotificationDeliveryController |
delivery의 retryable과 상태는 운영 관찰용이며 Outbox 상태 전이를 직접 제어하지 않는다. |
| DB migration 영향 | V5__create_outbox_event.sql, V90__add_outbox_recovery_operations.sql |
기존 Outbox row를 보존하면서 허용 상태·시도 횟수 제약과 운영 조회 인덱스를 적용한다. |
- HTTP 오류는 상태 코드,
code이 있으면 해당 코드,X-Request-Id를 함께 보존한다. validation 오류는errors필드를 함께 기록한다. - Outbox 실패는 event ID, event type, aggregate, attempt count, last error를 보존한다.
pingdom.outbox.max_attempts_exceeded증가 또는pingdom.outbox.events{status="FAILED"}가 0보다 큰 경우 즉시 원인을 확인한다.- 이메일·FCM 문제는 provider 오류 코드, 내부 delivery 오류 코드, delivery 상태를 Outbox 상태와 혼동하지 않고 대조한다.
- 원인이 해결돼도 동일 event의 중복 외부 효과가 안전한지 확인하기 전에는 재처리하거나 같은 상태 변경 요청을 다시 보내지 않는다.
| 일자 | 이슈 | 내용 | 상태 |
|---|---|---|---|
| 2026-07-10 | #839, #840, #841 | HTTP 오류 코드, Outbox·delivery 재시도 책임, API·Flyway·운영 연결 기준을 문서화 | 완료 |
| 2026-07-21 | #841 | 구현 대조 결과, 누락된 도메인 오류 코드, 관련 운영 문서 링크 기준을 갱신 | 완료 |
| 2026-08-10 | #809, #810, #811 | 권한·감사·관측성을 갖춘 Outbox 조회 및 수동 재처리 운영 계약 추가 | 완료 |