Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
73a16e3
[test] 500 error 발생시키는 테스트 api 추가
seongjunnoh Sep 30, 2025
2377a42
[feat] 안읽음 알림 여부 조회 api controller 구현 (#317)
seongjunnoh Oct 1, 2025
d28b95e
[feat] 안읽음 알림 여부 조회 api use case 구현 (#317)
seongjunnoh Oct 1, 2025
2bc5f6e
[feat] 안읽음 알림 여부 조회 api 영속성 코드 구현 (#317)
seongjunnoh Oct 1, 2025
c2dee7b
[feat] 안읽음 알림 여부 조회 api querydsl 코드 구현 (#317)
seongjunnoh Oct 1, 2025
c6b5e7b
[test] 안읽음 알림 여부 조회 api 통합 테스트 코드 작성 (#317)
seongjunnoh Oct 1, 2025
0fa3c7d
[feat] Webflux 의존성 추가 (#319)
hd0rable Oct 2, 2025
42bca35
[refactor] 외부api(네이버,알라딘) InternalServerException로 감싸기 (#319)
hd0rable Oct 2, 2025
c99d269
[refactor] 500에러 ExternalApiException로 감싸기 (#319)
hd0rable Oct 2, 2025
3460092
[refactor] FirebaseException이 RuntimeException 상속하도록 수정 (#319)
hd0rable Oct 2, 2025
1b7a147
[feat] 500에러 ExternalApiException 작성 (#319)
hd0rable Oct 2, 2025
591306a
[feat] 서버 내부 오류 예외 (500) 처리 핸들러 통합 및 디스코드 웹훅 트리거 (#319)
hd0rable Oct 2, 2025
c6cd0a5
[feat] 디스코드 웹훅 클라이언트 작성 (#319)
hd0rable Oct 2, 2025
e8a5dfc
[refactor] FirebaseException이 RuntimeException 상속하도록 수정 (#319)
hd0rable Oct 2, 2025
20f3483
[refactor] 관련 에러 코드 수정 (#319)
hd0rable Oct 2, 2025
af7709e
Merge pull request #318 from THIP-TextHip/feat/#317-notification-chec…
seongjunnoh Oct 2, 2025
47f2874
Merge remote-tracking branch 'origin/develop' into feat/#319-discord-…
hd0rable Oct 2, 2025
b89820d
[feat] 디스코드 웹훅 전송 실패시 로깅 (#319)
hd0rable Oct 2, 2025
648f8db
[refactor] 오타 수정 (#319)
hd0rable Oct 2, 2025
a5b8f7b
[chore] logstash 인코더 의존성 주입 (#319)
buzz0331 Oct 3, 2025
6bd3994
[chore] logback 파일 정의 (#319)
buzz0331 Oct 3, 2025
a42a813
[chore] Mdc 로깅 필터 (#319)
buzz0331 Oct 3, 2025
d3cb409
[chore] 응답에서 requestId 반환하도록 수정 (#319)
buzz0331 Oct 3, 2025
4d07e52
[chore] 디스코드 에러 전송시 Mdc에서 추출후 id 전송 (#319)
buzz0331 Oct 3, 2025
c263dec
[refactor] 토큰에서 userId 꺼내서 MDC에 넣도록 수정 (#319)
buzz0331 Oct 3, 2025
ce14b9b
Merge remote-tracking branch 'origin/feat/#319-discord-error' into fe…
buzz0331 Oct 3, 2025
3ee0084
[refactor] 패키지명 오타 수정 (#319)
hd0rable Oct 3, 2025
573d268
[refactor] static 임포트문 추가 (#319)
hd0rable Oct 4, 2025
b1995db
[refactor] static import (#319)
buzz0331 Oct 4, 2025
9cab189
[refactor] 토큰 유효성 검증 순서 변경 (#319)
buzz0331 Oct 4, 2025
fc8ff1a
Merge pull request #320 from THIP-TextHip/feat/#319-discord-error
hd0rable Oct 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ dependencies {

// Firebase
implementation 'com.google.firebase:firebase-admin:9.3.0'

// Webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'

// LogStash
implementation 'net.logstash.logback:logstash-logback-encoder:7.4'
}

def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@
@RestController
@RequiredArgsConstructor
@ConditionalOnProperty(name = "thip.test-api.enabled", havingValue = "true")
public class TestTokenController {
public class TestController {

private final JwtUtil jwtUtil;

@GetMapping("/api/test/token/access")
public String generateAccessToken(@RequestParam Long userId) {
return jwtUtil.createAccessToken(userId);
}

@GetMapping("/api/test/error")
public String throwError() {
throw new RuntimeException("테스트 500 에러");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import konkuk.thip.common.exception.BusinessException;
import konkuk.thip.common.exception.ExternalApiException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -54,14 +54,15 @@ public Integer getPageCount(String isbn) {
// TODO : 알라딘으로부터 page 정보가 없으면 ??
// 보상 시나리오 : 유저에게 "page 정보를 찾을 수 없는 책입니다. 직접 page 정보를 입력하세요" 라고 안내
// 일단 지금은 exception throw 만 진행
throw new BusinessException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
}

JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());

return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
} catch (IOException e) {
Comment on lines 60 to 63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

페이지 수 미존재(0) 반환은 모호합니다 — 명시적으로 예외 처리 권장

item/subInfo가 존재하더라도 pageCount 필드가 없으면 asInt()가 0을 반환해 성공처럼 보입니다. 사용자 안내 시나리오와 일관되게 0인 경우도 ExternalApiException으로 처리하는 편이 안전합니다.

적용 예:

-            return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
+            int pageCount = subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
+            if (pageCount <= 0) {
+                throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
+            }
+            return pageCount;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());
return subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
} catch (IOException e) {
JsonNode subInfo = items.get(0).path(SUB_INFO_PARSING_KEY.getValue());
int pageCount = subInfo.path(PAGE_COUNT_PARSING_KEY.getValue()).asInt();
if (pageCount <= 0) {
throw new ExternalApiException(BOOK_ALADIN_API_ISBN_NOT_FOUND);
}
return pageCount;
} catch (IOException e) {

throw new BusinessException(BOOK_ALADIN_API_PARSING_ERROR);
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
}
Comment on lines +64 to 65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

파싱 예외의 원인(cause) 보존

스택트레이스를 유지하려면 cause를 포함해 던지세요.

-        } catch (IOException e) {
-            throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
+        } catch (IOException e) {
+            throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR, e);
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR);
}
} catch (IOException e) {
throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR, e);
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java
around lines 64-65, the code throws ExternalApiException without preserving the
original parsing exception; modify the throw to include the caught exception as
the cause (pass the caught Throwable/Exception into ExternalApiException via its
constructor or a factory method) so the stack trace and root cause are preserved
when rethrowing.

}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package konkuk.thip.book.adapter.out.api.naver;

import konkuk.thip.common.exception.BusinessException;
import konkuk.thip.common.exception.ExternalApiException;
import konkuk.thip.common.exception.InternalServerException;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
Expand Down Expand Up @@ -63,7 +64,7 @@ private String keywordToEncoding(String keyword) {
try {
text = URLEncoder.encode(keyword, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new BusinessException(BOOK_KEYWORD_ENCODING_FAILED);
throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED);
}
Comment on lines +67 to 68
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

원인 예외(cause) 보존하여 진단력 향상

재래핑 시 원인 예외를 포함하지 않아 스택트레이스 손실이 있습니다. 두 번째 생성자 사용으로 cause를 보존해 주세요.

-            throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED);
+            throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED, e);
@@
-            throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR);
+            throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR, e);
@@
-            throw new InternalServerException(BOOK_NAVER_API_URL_ERROR);
+            throw new InternalServerException(BOOK_NAVER_API_URL_ERROR, e);
@@
-            throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED);
+            throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED, e);
@@
-            throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR);
+            throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR, e);

ExternalApiException에 (ErrorCode, Throwable) 생성자가 없다면 동일 시그니처를 추가하는 것도 고려해 주세요.

Based on learnings

Also applies to: 88-89, 100-103, 120-121


🏁 Script executed:

#!/bin/bash
# InternalServerException 생성자 시그니처 확인
rg -nP "class InternalServerException\b" -g "src/main/java/**/*.java" -C5
rg -nP "InternalServerException\s*\(" -g "src/main/java/**/*.java" -C3

# ExternalApiException 생성자 시그니처 확인
rg -nP "class ExternalApiException\b" -g "src/main/java/**/*.java" -C5
rg -nP "ExternalApiException\s*\(" -g "src/main/java/**/*.java" -C3

Length of output: 19126


예외 래핑 시 원인 예외 보존
src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java의 catch 블록(67-68, 88-89, 100-103, 120-121)에서 throw 시 두 번째 인자로 e를 전달해 스택 트레이스를 보존하세요.

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java around
lines 67-68, 88-89, 100-103 and 120-121, the catch blocks throw
InternalServerException without preserving the original exception; update each
throw to pass the caught exception (e) as the cause/second argument when
constructing InternalServerException so the original stack trace is retained
(e.g., throw new InternalServerException(BOOK_KEYWORD_ENCODING_FAILED, e));
ensure all four catch sites are changed similarly.

return text;
}
Expand All @@ -84,7 +85,7 @@ String get(String apiUrl, Map<String, String> requestHeaders){
return readBody(con.getErrorStream());
}
} catch (IOException e) {
throw new BusinessException(BOOK_NAVER_API_REQUEST_ERROR);
throw new ExternalApiException(BOOK_NAVER_API_REQUEST_ERROR);
} finally {
con.disconnect();
}
Expand All @@ -96,9 +97,9 @@ private HttpURLConnection connect(String apiUrl){
URL url = new URL(apiUrl);
return (HttpURLConnection)url.openConnection();
} catch (MalformedURLException e) {
throw new BusinessException(BOOK_NAVER_API_URL_ERROR);
throw new InternalServerException(BOOK_NAVER_API_URL_ERROR);
} catch (IOException e) {
throw new BusinessException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED);
throw new InternalServerException(BOOK_NAVER_API_URL_HTTP_CONNECT_FAILED);
}
}

Expand All @@ -116,7 +117,7 @@ private String readBody(InputStream body){

return responseBody.toString();
} catch (IOException e) {
throw new BusinessException(BOOK_NAVER_API_RESPONSE_ERROR);
throw new ExternalApiException(BOOK_NAVER_API_RESPONSE_ERROR);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import konkuk.thip.book.application.port.in.dto.BookMostSearchResult;
import konkuk.thip.book.application.port.out.BookRedisCommandPort;
import konkuk.thip.book.application.port.out.BookRedisQueryPort;
import konkuk.thip.common.exception.ExternalApiException;
import konkuk.thip.common.exception.InternalServerException;
import konkuk.thip.common.exception.code.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
Expand Down Expand Up @@ -80,7 +80,7 @@ public List<BookMostSearchResult.BookRankInfo> getYesterdayBookRankInfos(LocalDa
new TypeReference<List<BookMostSearchResult.BookRankInfo>>() {}
);
} catch (JsonProcessingException e) {
throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR);
throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR);
}
Comment on lines +83 to 84
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

JSON 처리 예외의 원인(cause) 보존

스택 추적을 유지하세요.

-            throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR);
+            throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR, e);
-            throw new InternalServerException(JSON_PROCESSING_ERROR);
+            throw new InternalServerException(JSON_PROCESSING_ERROR, e);

Also applies to: 109-110

🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java
around lines 83-84 and 109-110, the catch blocks that throw new
InternalServerException(ErrorCode.JSON_PROCESSING_ERROR) discard the original
exception and its stack trace; update those throws to preserve the cause by
passing the caught exception as the cause (e.g., throw new
InternalServerException(ErrorCode.JSON_PROCESSING_ERROR, e)) or otherwise attach
the original throwable so the stack trace and root cause are retained.

}

Expand All @@ -106,7 +106,7 @@ public void saveBookSearchRankDetail(List<BookMostSearchResult.BookRankInfo> boo
try {
detailJson = objectMapper.writeValueAsString(bookRankDetails);
} catch (JsonProcessingException e) {
throw new ExternalApiException(JSON_PROCESSING_ERROR);
throw new InternalServerException(JSON_PROCESSING_ERROR);
}
redisTemplate.opsForValue().set(redisKey, detailJson);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import jakarta.persistence.EntityManager;
import konkuk.thip.common.entity.StatusType;
import konkuk.thip.common.exception.InvalidStateException;
import konkuk.thip.common.exception.InternalServerException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
Expand Down Expand Up @@ -30,7 +30,7 @@ public class StatusFilterAspect {
*/
private Session currentTxSession() {
if (!TransactionSynchronizationManager.isActualTransactionActive()) {
throw new InvalidStateException(PERSISTENCE_TRANSACTION_REQUIRED);
throw new InternalServerException(PERSISTENCE_TRANSACTION_REQUIRED);
}
return session();
}
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/konkuk/thip/common/discord/DiscordClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package konkuk.thip.common.discord;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class DiscordClient {

@Value("${discord.env}")
private String env;

@Value("${discord.webhook-url}")
private String webhookUrl;

public void sendErrorMessage(String message, String stackTrace, String requestId, String userId) {
if("test".equals(env)) return;

WebClient webClient = WebClient.create();

Map<String, Object> embedData = new HashMap<>();
embedData.put("title", "THIP 서버 500 에러 발생");

Map<String, String> field1 = new HashMap<>();
field1.put("name", "발생시각");
field1.put("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

Map<String, String> field2 = new HashMap<>();
field2.put("name", "에러 명");
field2.put("value", message);

Map<String, String> field3 = new HashMap<>();
field3.put("name", "스택 트레이스");
field3.put("value", stackTrace);

Map<String, String> field4 = new HashMap<>();
field4.put("name", "Request ID");
field4.put("value", requestId != null ? requestId : "N/A");

Map<String, String> field5 = new HashMap<>();
field5.put("name", "User ID");
field5.put("value", userId != null ? userId : "N/A");

embedData.put("fields", List.of(field1, field2, field3, field4, field5));

Map<String, Object> payload = new HashMap<>();
payload.put("embeds", new Object[]{embedData});

webClient.post()
.uri(webhookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload)
.retrieve()
.bodyToMono(Void.class)
.block();
}
Comment on lines +56 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

요청 스레드에서 block() 호출 및 타임아웃 부재 → 신뢰성 저하

예외 처리 중 Discord 호출이 무기한 블록될 수 있습니다. 타임아웃 + 비동기 fire-and-forget로 전환을 권장합니다.

-        webClient.post()
-                .uri(webhookUrl)
-                .contentType(MediaType.APPLICATION_JSON)
-                .bodyValue(payload)
-                .retrieve()
-                .bodyToMono(Void.class)
-                .block();
+        webClient.post()
+                .uri(webhookUrl)
+                .contentType(MediaType.APPLICATION_JSON)
+                .bodyValue(payload)
+                .retrieve()
+                .toBodilessEntity()
+                .timeout(java.time.Duration.ofSeconds(3))
+                .onErrorResume(ex -> reactor.core.publisher.Mono.empty())
+                .subscribe();

추가로, WebClient는 매 호출 생성 대신 빈으로 주입해 재사용하면 연결 풀/성능 측면에서 유리합니다.

Based on learnings

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
webClient.post()
.uri(webhookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload)
.retrieve()
.bodyToMono(Void.class)
.block();
}
webClient.post()
.uri(webhookUrl)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(payload)
.retrieve()
.toBodilessEntity()
.timeout(java.time.Duration.ofSeconds(3))
.onErrorResume(ex -> reactor.core.publisher.Mono.empty())
.subscribe();
}
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/discord/DiscordClient.java around lines
56-63, the current code blocks the calling thread with .block() and has no
timeout; change to use a reusable injected WebClient bean (do not create
per-call), send the request asynchronously and fire-and-forget by using the
reactive chain without .block(), add a timeout (e.g., Duration.ofSeconds(5)) on
the Mono, and subscribe with error handling to log failures (or
.doOnError(...).subscribe()). Ensure WebClient is injected as a @Bean and reused
to benefit from connection pooling.

}
14 changes: 11 additions & 3 deletions src/main/java/konkuk/thip/common/dto/BaseResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;

import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID;

@Slf4j
@Getter
@JsonPropertyOrder({"success", "code", "message", "data"})
@JsonPropertyOrder({"success", "code", "message", "requestId", "data"})
public class BaseResponse<T> {

@JsonProperty("isSuccess")
Expand All @@ -15,17 +20,20 @@ public class BaseResponse<T> {

private final String message;

private final String requestId;

private final T data;

private BaseResponse(boolean success, int code, String message, T data) {
private BaseResponse(boolean success, int code, String message, String requestId, T data) {
this.success = success;
this.code = code;
this.message = message;
this.requestId = requestId;
this.data = data;
}

private BaseResponse(ResponseCode response, T data) {
this(response.isSuccess(), response.getCode(), response.getMessage(), data);
this(response.isSuccess(), response.getCode(), response.getMessage(), MDC.get(REQUEST_ID.getValue()), data);
}

public static <T> BaseResponse<T> ok(T data) {
Expand Down
14 changes: 10 additions & 4 deletions src/main/java/konkuk/thip/common/dto/ErrorResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import org.slf4j.MDC;

import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID;

@Getter
@JsonPropertyOrder({"success", "code", "message"})
@JsonPropertyOrder({"success", "code", "message", "requestId"})
public class ErrorResponse {

@JsonProperty("isSuccess")
Expand All @@ -15,14 +18,17 @@ public class ErrorResponse {

private final String message;

private ErrorResponse(boolean success, int code, String message) {
private final String requestId;

private ErrorResponse(boolean success, int code, String message, String requestId) {
this.success = success;
this.code = code;
this.message = message;
this.requestId = requestId;
}

private ErrorResponse(ResponseCode response) {
this(response.isSuccess(), response.getCode(), response.getMessage());
this(response.isSuccess(), response.getCode(), response.getMessage(), MDC.get(REQUEST_ID.getValue()));
}

public static ErrorResponse of(ResponseCode response) {
Expand All @@ -32,6 +38,6 @@ public static ErrorResponse of(ResponseCode response) {
public static ErrorResponse of(ResponseCode response, String message) {
StringBuilder sb = new StringBuilder();
sb.append(response.getMessage()).append(" ").append(message);
return new ErrorResponse(response.isSuccess(), response.getCode(), sb.toString());
return new ErrorResponse(response.isSuccess(), response.getCode(), sb.toString(), MDC.get(REQUEST_ID.getValue()));
}
}
20 changes: 8 additions & 12 deletions src/main/java/konkuk/thip/common/exception/FirebaseException.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@

import konkuk.thip.common.exception.code.ErrorCode;

public class FirebaseException extends BusinessException {
public FirebaseException(ErrorCode errorCode) {
super(errorCode);
}
public class FirebaseException extends RuntimeException {

public FirebaseException(ErrorCode errorCode, Exception e) {
super(errorCode, e);
}
private final ErrorCode errorCode;

public FirebaseException(Exception e) {
super(ErrorCode.FIREBASE_SEND_ERROR, e);
public FirebaseException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}

public FirebaseException() {
super(ErrorCode.FIREBASE_SEND_ERROR);
public FirebaseException(ErrorCode errorCode, Exception e) {
super(errorCode.getMessage(), e);
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package konkuk.thip.common.exception;

import konkuk.thip.common.exception.code.ErrorCode;

public class InternalServerException extends RuntimeException {

private final ErrorCode errorCode;

public InternalServerException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public InternalServerException(ErrorCode errorCode, Exception e) {
super(errorCode.getMessage(), e);
this.errorCode = errorCode;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,10 @@ public enum ErrorCode implements ResponseCode {
BOOK_NAVER_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80009, "네이버 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
BOOK_NOT_FOUND(HttpStatus.NOT_FOUND, 80010, "존재하지 않는 BOOK 입니다."),
BOOK_ALREADY_SAVED(HttpStatus.BAD_REQUEST, 80011, "사용자가 이미 저장한 책입니다."),
DUPLICATED_BOOKS_IN_COLLECTION(HttpStatus.INTERNAL_SERVER_ERROR, 80012, "중복된 책이 존재합니다."),
BOOK_NOT_SAVED_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80013, "사용자가 저장하지 않은 책은 저장삭제 할 수 없습니다."),
BOOK_NOT_SAVED_DB_CANNOT_DELETE(HttpStatus.BAD_REQUEST, 80014, "DB에 존재하지 않은 책은 저장삭제 할 수 없습니다."),
BOOK_ALADIN_API_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 80015, "알라딘 API 응답 파싱에 실패하였습니다."),
BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),

Comment on lines +84 to 85
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

외부 API ‘ISBN 미존재’의 500 매핑 재검토

알라딘 ISBN 미존재를 500으로 올린 것은 사용자/클라이언트 오류 성격과 맞지 않습니다. 네이버(80009)는 BAD_REQUEST입니다. 일관성 및 UX를 위해 400(또는 404)로 되돌리는 것을 권장합니다.

-    BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
+    BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),

도메인 의도상 404가 더 적절하다면 404도 검토해 주세요.

Based on learnings


🏁 Script executed:

#!/bin/bash
rg -nP --type=java 'NAVER.*ISBN_NOT_FOUND' -C3 src/main/java/konkuk/thip/common/exception/code/ErrorCode.java

Length of output: 625


외부 API ISBN 미존재의 HTTP 상태코드 500 → 400(또는 404)로 변경
알라딘 API에서 ISBN 미존재는 클라이언트 입력 오류이므로 HttpStatus.INTERNAL_SERVER_ERROR 대신 HttpStatus.BAD_REQUEST(또는 도메인 의도에 따라 HttpStatus.NOT_FOUND)로 수정하세요.

-    BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
+    BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.INTERNAL_SERVER_ERROR, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
BOOK_ALADIN_API_ISBN_NOT_FOUND(HttpStatus.BAD_REQUEST, 80016, "알라딘 API 에서 ISBN으로 검색한 결과가 존재하지 않습니다."),
🤖 Prompt for AI Agents
In src/main/java/konkuk/thip/common/exception/code/ErrorCode.java around lines
84-85, the BOOK_ALADIN_API_ISBN_NOT_FOUND entry uses
HttpStatus.INTERNAL_SERVER_ERROR but should represent a client-side error;
change the HttpStatus to HttpStatus.BAD_REQUEST (or HttpStatus.NOT_FOUND if you
prefer domain semantics) while keeping the error code and message intact so the
enum reflects a 4xx status for ISBN-not-found cases.

/**
* 90000 : recentSearch error
Expand Down Expand Up @@ -225,6 +224,7 @@ public enum ErrorCode implements ResponseCode {
FCM_TOKEN_ENABLED_STATE_ALREADY(HttpStatus.BAD_REQUEST, 200001, "요청한 상태로 이미 푸쉬 알림 여부가 설정되어 있습니다."),
FCM_TOKEN_ACCESS_FORBIDDEN(HttpStatus.FORBIDDEN, 200002, "토큰을 소유하고 있는 계정이 아닙니다."),
FIREBASE_SEND_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 200003, "FCM 푸쉬 알림 전송에 실패했습니다."),
FCM_TOKEN_DEVICE_ARRAY_MISMATCH(HttpStatus.INTERNAL_SERVER_ERROR, 200004, "메시지, FCM 토큰, 디바이스 ID 리스트의 크기는 같아야 합니다."),


/**
Expand Down
Loading