실패 유형 레퍼런스
Aset의 각 엔티티에 걸친 모든 실패 상태, 오류 유형, 복구 조치를 모았다. 각 실패 유형에는 apps/infra/db/seed.sql에 대응하는 목 데이터가 있다.
예시일 뿐, 코드가 실제로 쓰는 값이 아니다
이 페이지는 시드·목 데이터 레퍼런스로 작성됐다. 그래서 아래 표의 failure_type 값과 오류 문자열 대부분은 예시이지 백엔드가 실제로 기록하는 값이 아니다. 프론트엔드에서 이 값들로 failure_type 분기를 만들지 말 것.
코드가 실제로 쓰는 값(V1):
- 입금:
failure_type을 쓰는 코드가 없다. 실패한 입금은status = FAILED만 설정된다(유형화된 사유가 없다). - 상환:
INVESTOR_CANCELLED(투자자 자가 취소.status = REJECTED로 저장된다)와 거부 시 어드민이 입력한 사유(없으면REJECTED). 지급 실패 전용failure_type은 없다. - 수익:
ON_CHAIN_TX_FAILED(온체인 distribute 트랜잭션 실패.yield-distributions.post.create.ts,lib/shared/yield/run-distribution.ts). - 알림: deliver 워커(
notifications.worker.deliver.ts)의INVALID_RECIPIENT와SERVER_ERROR, 그리고 실제 SES 이벤트 경로(notifications.webhook.ses.ts)의BOUNCED와SPAM_FILTERED. 쓰이지 않는 것은TIMEOUT과RATE_LIMITED뿐이다.
아래 표는 런타임 분류 체계가 아니라 시드·목 행에 대한 표시 문구 가이드로 볼 것.
개요
| 분류 | 개수 |
|---|---|
| 입금 상태 | 5 |
| 상환 실패 | 5 |
| 수익 실패 | 4 |
| KYC/SBT 상태 | 3 |
| 알림 실패 | 6 |
입금 실패 deposits.status + failure_type
입금 처리, 에스크로 관리, LP 토큰 민팅과 그 복구 절차를 다룬다.
상태 흐름: PENDING → PROCESSING → COMPLETED 또는 FAILED
| 상태 | 실패 유형 | 오류 예시 | 어드민 표시 | 투자자 표시 | 복구 |
|---|---|---|---|---|---|
| FAILED | LP_MINT_FAILED | ERC20: transfer amount exceeds balance | 빨간색 "LP Mint Failed" + 오류 | "입금을 처리하지 못했습니다" | 어드민이 재시도 |
| FAILED | FM_NOTIFICATION_FAILED | SMTP delivery failed: mailbox unavailable | 노란색 "FM Not Notified" + 재시도 | 표시 안 함(내부용) | 어드민이 FM 통지 재시도 |
| PROCESSING | — | — | 파란색 "Processing" + 스피너 | "입금을 처리하고 있습니다" | 1시간 넘게 멈춰 있으면 자동 에스컬레이션 |
상환 실패 redemption_requests.status + failure_type
락업 검증과 reserve·펀딩 확인을 포함한 워크플로다. FM 사전 확인 단계는 없다(FM_ACCEPTED는 v3-34에서 제거됐다).
상태 흐름(즉시): REQUESTED → PROCESSING → COMPLETED(파트너 자금을 기다리는 동안에는 PENDING_RESERVE) 상태 흐름(회차, v3-26): REQUESTED → QUEUED → PARTIALLY_FILLED* → COMPLETED (*부분 체결분은 청구될 때까지 이월된다)
| 상태 | 실패 유형 | 오류 예시 | 어드민 표시 | 투자자 표시 | 복구 |
|---|---|---|---|---|---|
| FAILED | PAYOUT_TX_FAILED | Insufficient reserve balance in escrow | 빨간색 "Payout Failed" + 에스크로 정보 | "상환을 완료하지 못했습니다" | 어드민이 에스크로를 보충하고 재시도 |
| FAILED | FM_NOTIFICATION_FAILED | SMTP connection refused on port 587 | 노란색 "FM Not Notified" | 투자자에게 표시 안 함 | FM 통지 재시도 |
| FAILED | CONTRACT_EXECUTION_FAILED | execution reverted: InsufficientReserve | 빨간색 "Contract Error" + 부족액 | "처리 오류입니다. 지원팀에 문의해 주세요" | 어드민이 reserve 부족을 해결 |
| FAILED | FUND_TRANSFER_FAILED | ERC20: transfer exceeds allowance — approval expired | 빨간색 "Transfer Failed" + 기한 | "펀드 이체 대기 중" | FM이 지갑 승인을 갱신 |
| REJECTED | LOCKUP_NOT_MET | Lockup period (180 days) has not elapsed | 회색 "Rejected" + 사유 | "상환 불가: 락업 진행 중" | 락업이 끝날 때까지 대기 |
목 데이터: seed.sql의 상환 ID 7~11.
수익 분배 실패 yield_distributions.status + failure_type
수익 자동 발생, 일괄 청구 처리, 트랜잭션 실패 처리를 다룬다.
상태 흐름: PROCESSING → DISTRIBUTED 또는 FAILED
PENDING은 없어졌다. 이 고아 상태는 생산자를 지워서 해결했다(v3-104, 마이그레이션 0113)
PENDING은 원래 레거시 서버키 생성 경로의 insert와 settleYield 사이의 1초 미만 간극에만 존재했다(FM 경로는 PROCESSING으로 insert하고 POST /{id}/distribute를 기다리며 멈추고, 인덱서는 DISTRIBUTED만 쓴다). 거기 머무른 행은 크래시가 남긴 고아였고 아무것도 그것을 고칠 수 없었다. 인덱서는 tx_hash로 정합을 맞추는데 그런 행에는 아직 tx가 없었고, yield.scheduler.reconcile은 claimable_yield만 미러링한다. 그래서 그 기간은 영원히 미분배로 읽혔고, 홀더가 지급받았는지를 DB로 알 방법이 없었다.
원래 계획한 해법은 sweeper였는데 대체됐다. 그 레거시 경로는 제품에서 어차피 도달할 수 없었기 때문이다(어드민 UI가 fund_wallet이 있는 풀에 대해 서버키를 거부하고, v3-26에 따라 생성 시 fund_wallet이 필수이며, 표시 전용 풀은 분배할 pool_address가 없다). 그래서 sweeper 대신 그 경로를 삭제했다. POST /yield-distributions는 deposit_tx_hash 없이는 400을 반환한다. 그리고 마이그레이션 0113이 이 상태를 표현 불가능하게 만들었다(기본값 PROCESSING + CHECK (status <> 'PENDING')). tx가 없는 기존 PENDING 행은 FAILED / **ORPHANED_LEGACY_PATH**로 백필했다. tx가 있는 행이 있었다면 마이그레이션을 중단시켜 온체인 수동 정합으로 넘겼을 것이다. 이미 정산됐을 수도 있는 기간을 FAILED로 표시하면 지급된 기간을 미지급으로 기록하게 되기 때문이다.
⚠️ yield_status enum에는 PENDING이 남아 있다. yield_distribution_investors.status가 정당하게 쓰기 때문이다(아직 청구되지 않은 배분분). 금지는 yield_distributions에 대한 CHECK뿐이다.
24시간이 넘은 PROCESSING 행은 사람이 봐야 하는 상태다
실패 유형이 아니라 정체다. FM의 depositYield는 온체인에 올라갔고(행에 deposit_tx_hash가 있다) 수익은 풀에 있는데, POST /{id}/distribute가 실행되지 않아 홀더가 지급받지 못했다. 복구 경로는 있다. 그 엔드포인트, 그리고 어드민 Yield 표의 Distribute 액션이다(검증할 입금 tx가 있을 때 정확히 그때만 제공되고, 없으면 사유와 함께 비활성화된다. deposit_tx_hash가 없는 행은 제거된 서버 경로에서 온 것이고 인덱서가 정합을 맞추므로, 재시도하면 두 번 지급할 위험이 있다).
dashboard_alert_counts.stalled_yield로 노출돼 대시보드의 "Stalled Distributions" 알림이 되고, Yield 화면의 Failed 탭에서 **Stalled**로 보인다. 대응하는 ADMIN 알림은 문구가 없어 만들지 못했다(v3-104).
| 상태 | 실패 유형 | 오류 예시 | 어드민 표시 | 투자자 표시 | 복구 |
|---|---|---|---|---|---|
| FAILED | ORPHANED_LEGACY_PATH | Recorded by the removed legacy server-key create path and never settled (no deposit or settle tx) | 빨간색 "Orphaned Legacy Path" | 표시 안 함 | 기간을 다시 기록(체인에 도달한 것이 없다) |
| FAILED | TX_FAILED | Gas price exceeded maximum threshold | 빨간색 "TX Failed" + 가스 정보 | "수익 분배가 지연되고 있습니다" | 어드민이 가스를 올려 재시도 |
| FAILED | FM_NOTIFICATION_FAILED | SendGrid API error 429: rate limit exceeded | 노란색 "FM Not Notified" | 표시 안 함 | 레이트 리밋이 풀린 뒤 재시도 |
| FAILED | DISTRIBUTION_TX_FAILED | ERC20 transfer failed — pool escrow insufficient | 빨간색 "Distribution Failed" + tx 해시 | "수익 분배가 지연되고 있습니다" | 어드민이 에스크로를 보충 |
| FAILED | CLAIM_PROCESSING_FAILED | Batch claim tx gas estimation failed: out of gas for 4 recipients | 빨간색 "Claim Failed" + 수령자 수 | "수익 청구 대기 중" | 어드민이 배치를 쪼개 재시도 |
참고
가스 한도를 위한 배치 최적화가 있다. 지수 백오프로 최대 3회 자동 재시도한다. SendGrid 레이트 리밋은 30분에서 2시간 뒤 재시도한다. 목 데이터는 seed.sql의 수익 ID 8~11.
KYC / SBT 실패 users.kyc_status + sbt_status
신원 검증과 Soul Bound Token 민팅(비동기 민팅 포함)을 다룬다.
KYC 흐름: PENDING → APPROVED / REJECTED
SBT 흐름: NOT_MINTED → MINTED / FAILED
| 엔티티 | 상태 | 내용 | 어드민 표시 | 투자자 표시 | 복구 |
|---|---|---|---|---|---|
| KYC | REJECTED | 신원 검증 실패 | 빨간색 "KYC Rejected" + 사유 | "인증에 실패했습니다. 다시 제출해 주세요" | 유저가 KYC 재제출 |
| KYC | PENDING | 심사 대기 | 노란색 "Pending Review" | "심사 중" | 어드민이 심사 |
| SBT | FAILED | 트랜잭션 타임아웃 또는 체인 오류 | 빨간색 "SBT Mint Failed" + tx 해시 | "자격증명 발급에 실패했습니다" + 재시도 | 어드민이 재민팅 실행 |
알림 실패 notification_deliveries.status + .failure_type
⚠️ notification_logs는 더 이상 존재하지 않는다. 마이그레이션 0110이 이를 세 테이블로 나눴다(v3-103). 발송 결과는 이제 **notification_deliveries**에 있고, status(delivery_status enum)와 선택값 failure_type으로 표현된다. 11-db-schema → 알림과 13-operations → 데이터 모델 참조.
상태 흐름: PENDING → SENT → (SES 이벤트) BOUNCED / COMPLAINED, 또는 발송 오류 시 FAILED, 또는 의도적으로 보내지 않았을 때 SKIPPED.
SKIPPED는 실패가 아니고 실패처럼 스타일링해서도 안 된다. "이 사람에게 이메일을 보내지 않기로 했고 그 이유가 무엇인지"를 기록한 것이다(OPTED_OUT / NO_ADDRESS / SUPPRESSED_ADDRESS). 실패 알림 KPI에 들어가는 것은 FAILED뿐이고, INVALID_RECIPIENT도 거기서 제외된다(0107).
| 상태 | 실사용 | 기록 주체 | 뜻 | 자동 재시도 | 복구 |
|---|---|---|---|---|---|
SENT | 예 | notifications.worker.deliver.ts | SES가 접수함 | — | — |
FAILED | 예 | notifications.worker.deliver.ts | 발송이 예외를 던짐(SERVER_ERROR, INVALID_RECIPIENT) | SQS 1/5/15/60/240분 → DLQ | 설정이나 수신자 주소 수정 |
BOUNCED | 예 | notifications.webhook.ses.ts | SES 반송. 하드 바운스면 주소도 차단된다(HARD_BOUNCE) | 아니오 | 주소를 갱신한 뒤 차단 해제 |
COMPLAINED | 예 | notifications.webhook.ses.ts | 수신자가 스팸으로 신고함. 주소를 차단한다(COMPLAINT) | 아니오 | 동의 없이 다시 추가하지 말 것 |
SKIPPED | 예 | preferences/decide.ts | 의도적 미발송이며 사유가 기록됨 | — | 조치 불필요 |
PENDING | 예 | 팬아웃 시 | 큐에 들어갔고 아직 시도 전 | — | — |
아직 쓰이지 않는 failure_type 값. TIMEOUT과 RATE_LIMITED는 notification_failure_type에 있지만 아무것도 기록하지 않는다. SPAM_FILTERED는 이제 쓰인다. SES complaint 경로가 여기에 매핑된다. 원래 여섯 개 중 넷이 실사용이고 둘이 예약 상태다.
목 데이터: seed.sql의 SBT 민팅 실패, LP 민팅 실패, FM 반송.