API 해지 팝업 추가 및 사용자 오류 메시지 처리 개선

- API 해지 신청 팝업 UI 및 로직 구현
- 사용자 친화적 예외 처리 헬퍼(UserErrorMessageResolver) 추가
- Webhook 신청/수정, 글로벌 예외 처리에 사용자 메시지 연동
This commit is contained in:
Rinjae
2026-07-30 16:22:45 +09:00
parent 829630ae5c
commit 6c231c4189
19 changed files with 696 additions and 139 deletions
@@ -0,0 +1,76 @@
package com.eactive.apim.portal.common.exception;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.transaction.UnexpectedRollbackException;
import java.net.SocketTimeoutException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class UserErrorMessageResolverTest {
@Test
@DisplayName("JTA 롤백 원문은 노출하지 않고 롤백 안내 문구로 치환한다")
void jtaRollbackIsReplaced() {
// 실제로 팝업에 노출됐던 원문
Exception ex = new UnexpectedRollbackException(
"JTA transaction unexpectedly rolled back (maybe due to a timeout); nested exception is "
+ "javax.transaction.RollbackException: Transaction set to rollback only");
String message = UserErrorMessageResolver.resolve(ex);
assertTrue(message.contains("변경 내용이 저장되지 않았습니다"), message);
assertTrue(!message.contains("JTA") && !message.contains("Rollback"), message);
// "maybe due to a timeout" 이 붙어 있어도 rollback only 면 타임아웃 문구를 쓰지 않는다
assertTrue(!message.contains("시간이 초과"), message);
}
@Test
@DisplayName("실제 타임아웃으로 롤백된 경우 타임아웃 안내를 준다")
void transactionTimeoutIsReported() {
Exception ex = new TransactionSystemException("Could not commit JTA transaction",
new SocketTimeoutException("Read timed out"));
assertTrue(UserErrorMessageResolver.resolve(ex).contains("시간이 초과"));
}
@Test
@DisplayName("제약 위반은 데이터 충돌 안내로 치환한다 (롤백 예외로 감싸여 있어도)")
void constraintViolationIsReported() {
Exception ex = new UnexpectedRollbackException("JTA transaction unexpectedly rolled back",
new DataIntegrityViolationException("ORA-00001: unique constraint violated"));
assertTrue(UserErrorMessageResolver.resolve(ex).contains("충돌"));
}
@Test
@DisplayName("서비스가 던진 한글 안내문은 그대로 전달한다")
void koreanBusinessMessageIsKept() {
String original = "이미 초대가 진행 중인 휴대폰 번호입니다.";
assertEquals(original, UserErrorMessageResolver.resolve(new IllegalStateException(original)));
}
@Test
@DisplayName("영문/기술 메시지와 메시지 없는 예외는 기본 안내로 대체한다")
void technicalMessageIsMasked() {
assertEquals(UserErrorMessageResolver.DEFAULT_MESSAGE,
UserErrorMessageResolver.resolve(new NullPointerException()));
assertEquals(UserErrorMessageResolver.DEFAULT_MESSAGE,
UserErrorMessageResolver.resolve(new IllegalStateException("Invitation is not in a cancelable state")));
assertEquals(UserErrorMessageResolver.DEFAULT_MESSAGE, UserErrorMessageResolver.resolve(null));
}
@Test
@DisplayName("팝업용 변환은 줄바꿈을 <br> 로 바꾼다")
void htmlConversionUsesBrTag() {
String html = UserErrorMessageResolver.resolveAsHtml(new NullPointerException());
assertTrue(html.contains("<br>"), html);
assertTrue(!html.contains("\n"), html);
}
}