From 73a16e3841bfa9d2d7f2485a42f960ac1149bd95 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Tue, 30 Sep 2025 18:37:10 +0900 Subject: [PATCH 01/27] =?UTF-8?q?[test]=20500=20error=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=EC=8B=9C=ED=82=A4=EB=8A=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20api?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/{TestTokenController.java => TestController.java} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename src/main/java/konkuk/thip/{TestTokenController.java => TestController.java} (80%) diff --git a/src/main/java/konkuk/thip/TestTokenController.java b/src/main/java/konkuk/thip/TestController.java similarity index 80% rename from src/main/java/konkuk/thip/TestTokenController.java rename to src/main/java/konkuk/thip/TestController.java index 3d4399b23..b91df45ec 100644 --- a/src/main/java/konkuk/thip/TestTokenController.java +++ b/src/main/java/konkuk/thip/TestController.java @@ -10,7 +10,7 @@ @RestController @RequiredArgsConstructor @ConditionalOnProperty(name = "thip.test-api.enabled", havingValue = "true") -public class TestTokenController { +public class TestController { private final JwtUtil jwtUtil; @@ -18,4 +18,9 @@ public class TestTokenController { public String generateAccessToken(@RequestParam Long userId) { return jwtUtil.createAccessToken(userId); } + + @GetMapping("/api/test/error") + public String throwError() { + throw new RuntimeException("테스트 500 에러"); + } } From 2377a424589fcf3d073513f1df1aef019023f12f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:28:57 +0900 Subject: [PATCH 02/27] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20controller=20=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/NotificationQueryController.java | 13 +++++++++++++ .../NotificationUncheckedExistsResponse.java | 9 +++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java index a8ace6dab..dec6a9e67 100644 --- a/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/NotificationQueryController.java @@ -8,6 +8,8 @@ import konkuk.thip.common.swagger.annotation.ExceptionDescription; import konkuk.thip.notification.adapter.in.web.response.NotificationShowEnableStateResponse; import konkuk.thip.notification.adapter.in.web.response.NotificationShowResponse; +import konkuk.thip.notification.adapter.in.web.response.NotificationUncheckedExistsResponse; +import konkuk.thip.notification.application.port.in.NotificationExistsUncheckedUseCase; import konkuk.thip.notification.application.port.in.NotificationShowEnableStateUseCase; import konkuk.thip.notification.application.port.in.NotificationShowUseCase; import konkuk.thip.notification.application.port.in.dto.NotificationType; @@ -26,6 +28,7 @@ public class NotificationQueryController { private final NotificationShowEnableStateUseCase notificationShowEnableStateUseCase; private final NotificationShowUseCase notificationShowUseCase; + private final NotificationExistsUncheckedUseCase notificationExistsUncheckedUseCase; @Operation( summary = "사용자 푸시알림 수신여부 조회 (마이페이지 -> 알림설정)", @@ -56,4 +59,14 @@ public BaseResponse showNotifications( ) { return BaseResponse.ok(notificationShowUseCase.showNotifications(userId, cursor, NotificationType.from(type))); } + + @Operation( + summary = "유저의 안읽은 알림 존재 여부 확인", + description = "유저가 읽지 않은 알림이 존재하는지 여부를 확인합니다." + ) + @GetMapping("/notifications/exists-unchecked") + public BaseResponse existsUnchecked(@Parameter(hidden = true) @UserId final Long userId) { + return BaseResponse.ok(NotificationUncheckedExistsResponse.of( + notificationExistsUncheckedUseCase.existsUnchecked(userId))); + } } diff --git a/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java new file mode 100644 index 000000000..f48dd55f5 --- /dev/null +++ b/src/main/java/konkuk/thip/notification/adapter/in/web/response/NotificationUncheckedExistsResponse.java @@ -0,0 +1,9 @@ +package konkuk.thip.notification.adapter.in.web.response; + +public record NotificationUncheckedExistsResponse( + boolean exists +) { + public static NotificationUncheckedExistsResponse of(boolean exists) { + return new NotificationUncheckedExistsResponse(exists); + } +} From d28b95e18f22898b882428e61eace1699fe6e3d7 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:10 +0900 Subject: [PATCH 03/27] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20use=20case=20=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationExistsUncheckedUseCase.java | 6 ++++++ .../NotificationExistsUncheckedService.java | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java create mode 100644 src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java diff --git a/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java b/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java new file mode 100644 index 000000000..3b34473df --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/port/in/NotificationExistsUncheckedUseCase.java @@ -0,0 +1,6 @@ +package konkuk.thip.notification.application.port.in; + +public interface NotificationExistsUncheckedUseCase { + + boolean existsUnchecked(Long userId); +} diff --git a/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java b/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java new file mode 100644 index 000000000..66783b6ea --- /dev/null +++ b/src/main/java/konkuk/thip/notification/application/service/NotificationExistsUncheckedService.java @@ -0,0 +1,20 @@ +package konkuk.thip.notification.application.service; + +import konkuk.thip.notification.application.port.in.NotificationExistsUncheckedUseCase; +import konkuk.thip.notification.application.port.out.NotificationQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class NotificationExistsUncheckedService implements NotificationExistsUncheckedUseCase { + + private final NotificationQueryPort notificationQueryPort; + + @Override + @Transactional(readOnly = true) + public boolean existsUnchecked(Long userId) { + return notificationQueryPort.existsUnchecked(userId); + } +} From 2bc5f6e8fc48ca9bca4ffd6614159cffec1df8b4 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:39 +0900 Subject: [PATCH 04/27] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/NotificationQueryPersistenceAdapter.java | 5 +++++ .../application/port/out/NotificationQueryPort.java | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java index 289428136..ab90570a3 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/NotificationQueryPersistenceAdapter.java @@ -40,6 +40,11 @@ public CursorBasedList findFeedAndRoomNotificationsByUserI )); } + @Override + public boolean existsUnchecked(Long userId) { + return notificationJpaRepository.existsByUserIdAndIsCheckedFalse(userId); + } + private CursorBasedList findNotificationsByPrimaryKeyCursor(Cursor cursor, PrimaryKeyNotificationQueryFunction queryFunction) { Long lastNotificationId = cursor.isFirstRequest() ? null : cursor.getLong(0); int pageSize = cursor.getPageSize(); diff --git a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java index e1af77781..c718817c6 100644 --- a/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java +++ b/src/main/java/konkuk/thip/notification/application/port/out/NotificationQueryPort.java @@ -11,4 +11,6 @@ public interface NotificationQueryPort { CursorBasedList findRoomNotificationsByUserId(Long userId, Cursor cursor); CursorBasedList findFeedAndRoomNotificationsByUserId(Long userId, Cursor cursor); + + boolean existsUnchecked(Long userId); } From c2dee7bd887cb86e82734c720e35c75ef5ab687d Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:29:51 +0900 Subject: [PATCH 05/27] =?UTF-8?q?[feat]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20querydsl=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84=20(#31?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/NotificationQueryRepository.java | 2 ++ .../repository/NotificationQueryRepositoryImpl.java | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java index ccf540952..836899999 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepository.java @@ -11,4 +11,6 @@ public interface NotificationQueryRepository { List findRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); List findFeedAndRoomNotificationsOrderByCreatedAtDesc(Long userId, Long lastNotificationId, int pageSize); + + boolean existsByUserIdAndIsCheckedFalse(Long userId); } diff --git a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java index c0fd7b310..5cf78fa3a 100644 --- a/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java +++ b/src/main/java/konkuk/thip/notification/adapter/out/persistence/repository/NotificationQueryRepositoryImpl.java @@ -47,6 +47,17 @@ public List findFeedAndRoomNotificationsOrderByCreatedAtDe return getNotificationQueryDtos(pageSize, notification, where); } + @Override + public boolean existsByUserIdAndIsCheckedFalse(Long userId) { + Integer result = queryFactory.selectOne() + .from(notification) + .where(notification.userJpaEntity.userId.eq(userId) + .and(notification.isChecked.eq(false))) + .fetchFirst(); + + return result != null; + } + private static BooleanExpression applyCursor(Long lastNotificationId, BooleanExpression where, QNotificationJpaEntity notification) { if (lastNotificationId != null) { where = where.and(notification.notificationId.lt(lastNotificationId)); From c6b5e7b861e8ca2b053c3a952164c9db6fe917b2 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Wed, 1 Oct 2025 17:30:05 +0900 Subject: [PATCH 06/27] =?UTF-8?q?[test]=20=EC=95=88=EC=9D=BD=EC=9D=8C=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=97=AC=EB=B6=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?api=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#317)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationExistsUncheckedApiTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java diff --git a/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java new file mode 100644 index 000000000..ad2ce70e1 --- /dev/null +++ b/src/test/java/konkuk/thip/notification/adapter/in/web/NotificationExistsUncheckedApiTest.java @@ -0,0 +1,72 @@ +package konkuk.thip.notification.adapter.in.web; + +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.notification.adapter.out.jpa.NotificationJpaEntity; +import konkuk.thip.notification.adapter.out.persistence.repository.NotificationJpaRepository; +import konkuk.thip.notification.domain.value.NotificationCategory; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.transaction.annotation.Transactional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@Transactional +@AutoConfigureMockMvc(addFilters = false) +@DisplayName("[통합] 안읽은 알림 존재 여부 확인 api 통합 테스트") +class NotificationExistsUncheckedApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private NotificationJpaRepository notificationJpaRepository; + @Autowired private JdbcTemplate jdbcTemplate; + + @Test + @DisplayName("유저가 읽지 않은 알림이 있을 경우, true 를 반환한다.") + void notification_exists_unchecked_true() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + NotificationJpaEntity n1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림1", NotificationCategory.FEED)); + + //when + ResultActions result = mockMvc.perform(get("/notifications/exists-unchecked") + .requestAttr("userId", user.getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.exists").value(true)); + } + + @Test + @DisplayName("유저가 읽지 않은 알림이 없을 경우, false 를 반환한다.") + void notification_exists_unchecked_false() throws Exception { + //given + UserJpaEntity user = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + NotificationJpaEntity n1 = notificationJpaRepository.save(TestEntityFactory.createNotification(user, "알림1", NotificationCategory.FEED)); + jdbcTemplate.update( + "UPDATE notifications SET is_checked = TRUE WHERE notification_id = ?", + n1.getNotificationId() + ); + + //when + ResultActions result = mockMvc.perform(get("/notifications/exists-unchecked") + .requestAttr("userId", user.getUserId())); + + //then + result.andExpect(status().isOk()) + .andExpect(jsonPath("$.data.exists").value(false)); + } +} From 0fa3c7db66bee2498b6afe549cfa01b84dfed119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 22:58:54 +0900 Subject: [PATCH 07/27] =?UTF-8?q?[feat]=20Webflux=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 4f43d1200..97fd5306f 100644 --- a/build.gradle +++ b/build.gradle @@ -92,6 +92,9 @@ dependencies { // Firebase implementation 'com.google.firebase:firebase-admin:9.3.0' + + // Webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' } def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile From 42bca35882e4108e96cc0ec0cad445ef2a093099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:02:09 +0900 Subject: [PATCH 08/27] =?UTF-8?q?[refactor]=20=EC=99=B8=EB=B6=80api(?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EB=B2=84,=EC=95=8C=EB=9D=BC=EB=94=98)=20Inte?= =?UTF-8?q?rnalServerException=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EA=B8=B0=20(#?= =?UTF-8?q?319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../book/adapter/out/api/aladin/AladinApiUtil.java | 7 ++++--- .../book/adapter/out/api/naver/NaverApiUtil.java | 13 +++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java index 7a63ac018..45d936d64 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/aladin/AladinApiUtil.java @@ -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; @@ -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) { - throw new BusinessException(BOOK_ALADIN_API_PARSING_ERROR); + throw new ExternalApiException(BOOK_ALADIN_API_PARSING_ERROR); } } } + diff --git a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java index b89786c48..685aa0568 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java +++ b/src/main/java/konkuk/thip/book/adapter/out/api/naver/NaverApiUtil.java @@ -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; @@ -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); } return text; } @@ -84,7 +85,7 @@ String get(String apiUrl, Map 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(); } @@ -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); } } @@ -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); } } From c99d2693b24d1724cc4f44b4f39e4e2b93bed3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:02:40 +0900 Subject: [PATCH 09/27] =?UTF-8?q?[refactor]=20500=EC=97=90=EB=9F=AC=20Exte?= =?UTF-8?q?rnalApiException=EB=A1=9C=20=EA=B0=90=EC=8B=B8=EA=B8=B0=20(#319?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/book/adapter/out/persistence/BookRedisAdapter.java | 6 +++--- .../java/konkuk/thip/common/aop/StatusFilterAspect.java | 4 ++-- .../thip/common/security/oauth2/CustomSuccessHandler.java | 4 ++-- .../oauth2/tokenstorage/RedisLoginTokenStorage.java | 4 ++-- .../java/konkuk/thip/config/AwsS3ImageUrlInitializer.java | 4 ++-- .../domain/value/NotificationRedirectSpecConverter.java | 6 +++--- .../out/persistence/UserTokenBlacklistRedisAdapter.java | 4 ++-- src/main/java/konkuk/thip/user/domain/User.java | 3 ++- 8 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java index 66f762426..5185f8b07 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookRedisAdapter.java @@ -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; @@ -80,7 +80,7 @@ public List getYesterdayBookRankInfos(LocalDa new TypeReference>() {} ); } catch (JsonProcessingException e) { - throw new ExternalApiException(ErrorCode.JSON_PROCESSING_ERROR); + throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR); } } @@ -106,7 +106,7 @@ public void saveBookSearchRankDetail(List 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); } diff --git a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java index 916165723..0c00fab6e 100644 --- a/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java +++ b/src/main/java/konkuk/thip/common/aop/StatusFilterAspect.java @@ -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; @@ -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(); } diff --git a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java index b0008a60e..49daa6368 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/CustomSuccessHandler.java @@ -3,7 +3,7 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.InternalServerException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.oauth2.tokenstorage.LoginTokenStorage; import konkuk.thip.common.security.util.JwtUtil; @@ -51,7 +51,7 @@ public void onAuthenticationSuccess( if (!webDomainProperties.isAllowed(Objects.toString(webRedirectDomain, ""))) { List origins = webDomainProperties.getWebDomainUrls(); if (origins == null || origins.isEmpty()) { - throw new AuthException(ErrorCode.WEB_DOMAIN_ORIGIN_EMPTY); + throw new InternalServerException(ErrorCode.WEB_DOMAIN_ORIGIN_EMPTY); } webRedirectDomain = origins.get(0); } diff --git a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java index 689dd2133..6fd15371f 100644 --- a/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java +++ b/src/main/java/konkuk/thip/common/security/oauth2/tokenstorage/RedisLoginTokenStorage.java @@ -1,6 +1,6 @@ package konkuk.thip.common.security.oauth2.tokenstorage; -import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.exception.InternalServerException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.common.security.oauth2.TokenType; import lombok.RequiredArgsConstructor; @@ -39,7 +39,7 @@ public Entry consume(String key) { return entry; } - throw new AuthException(ErrorCode.JSON_PROCESSING_ERROR); + throw new InternalServerException(ErrorCode.JSON_PROCESSING_ERROR); } private String toRedisKey(String key) { diff --git a/src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java b/src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java index 3b31abaa0..377472ec4 100644 --- a/src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java +++ b/src/main/java/konkuk/thip/config/AwsS3ImageUrlInitializer.java @@ -1,7 +1,7 @@ package konkuk.thip.config; import jakarta.annotation.PostConstruct; -import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.InternalServerException; import konkuk.thip.config.properties.AwsS3Properties; import konkuk.thip.room.domain.value.Category; import konkuk.thip.user.domain.value.Alias; @@ -20,7 +20,7 @@ public class AwsS3ImageUrlInitializer { void bindCloudFrontBaseUrl() { String baseUrl = awsS3Properties.cloudFrontBaseUrl(); if (baseUrl == null || baseUrl.isEmpty()) { - throw new BusinessException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED); + throw new InternalServerException(AWS_BUCKET_BASE_URL_NOT_CONFIGURED); } Alias.registerBaseUrlSupplier(awsS3Properties::cloudFrontBaseUrl); diff --git a/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java index 59f203628..1c2564208 100644 --- a/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java +++ b/src/main/java/konkuk/thip/notification/domain/value/NotificationRedirectSpecConverter.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; -import konkuk.thip.common.exception.InvalidStateException; +import konkuk.thip.common.exception.InternalServerException; import java.io.IOException; @@ -22,7 +22,7 @@ public String convertToDatabaseColumn(NotificationRedirectSpec attribute) { try { return objectMapper.writeValueAsString(attribute); } catch (JsonProcessingException e) { - throw new InvalidStateException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED); + throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_SERIALIZE_FAILED); } } @@ -32,7 +32,7 @@ public NotificationRedirectSpec convertToEntityAttribute(String dbData) { try { return objectMapper.readValue(dbData, NotificationRedirectSpec.class); } catch (IOException e) { - throw new InvalidStateException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED); + throw new InternalServerException(NOTIFICATION_REDIRECT_DATA_DESERIALIZE_FAILED); } } } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java index 0c2a72949..b39b0f4e1 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import konkuk.thip.common.exception.ExternalApiException; +import konkuk.thip.common.exception.InternalServerException; import konkuk.thip.common.security.oauth2.LoginUser; import konkuk.thip.common.security.util.JwtUtil; import konkuk.thip.user.application.port.UserTokenBlacklistCommandPort; @@ -52,7 +52,7 @@ public void addTokenToBlacklist(String token) { mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); valueJson = mapper.writeValueAsString(valueMap); } catch (JsonProcessingException e) { - throw new ExternalApiException(JSON_PROCESSING_ERROR); + throw new InternalServerException(JSON_PROCESSING_ERROR); } redisTemplate.opsForValue().set(key, valueJson); log.info("블랙리스트에 탈퇴한 회원 토큰 및 관련 정보 추가 - userId: {}, withdrawalTime: {}, expiration: {}", diff --git a/src/main/java/konkuk/thip/user/domain/User.java b/src/main/java/konkuk/thip/user/domain/User.java index ca2b08f28..e9115062a 100644 --- a/src/main/java/konkuk/thip/user/domain/User.java +++ b/src/main/java/konkuk/thip/user/domain/User.java @@ -1,6 +1,7 @@ package konkuk.thip.user.domain; import konkuk.thip.common.entity.BaseDomainEntity; +import konkuk.thip.common.exception.InternalServerException; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.common.exception.code.ErrorCode; import konkuk.thip.user.domain.value.Alias; @@ -80,7 +81,7 @@ private void validateCanUpdateNickname(String nickname) { public void markAsDeleted() { if (this.oauth2Id == null) { - throw new InvalidStateException(USER_OAUTH2ID_CANNOT_BE_NULL); + throw new InternalServerException(USER_OAUTH2ID_CANNOT_BE_NULL); } if (this.oauth2Id.startsWith("deleted:")) { throw new InvalidStateException(USER_ALREADY_DELETED); From 3460092a55a0e75a3de36c6b463b9cd4403f4c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:03:04 +0900 Subject: [PATCH 10/27] =?UTF-8?q?[refactor]=20FirebaseException=EC=9D=B4?= =?UTF-8?q?=20RuntimeException=20=EC=83=81=EC=86=8D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/FirebaseException.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/FirebaseException.java b/src/main/java/konkuk/thip/common/exception/FirebaseException.java index a4d97a046..027083741 100644 --- a/src/main/java/konkuk/thip/common/exception/FirebaseException.java +++ b/src/main/java/konkuk/thip/common/exception/FirebaseException.java @@ -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; } } From 1b7a14703e01f7cfbc7551aac5b067d981063363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:03:28 +0900 Subject: [PATCH 11/27] =?UTF-8?q?[feat]=20500=EC=97=90=EB=9F=AC=20External?= =?UTF-8?q?ApiException=20=EC=9E=91=EC=84=B1=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/InternalServerException.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/exception/InternalServerException.java diff --git a/src/main/java/konkuk/thip/common/exception/InternalServerException.java b/src/main/java/konkuk/thip/common/exception/InternalServerException.java new file mode 100644 index 000000000..35062bbb6 --- /dev/null +++ b/src/main/java/konkuk/thip/common/exception/InternalServerException.java @@ -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; + } +} From 591306a113ce1b514212f63612310dc06a9cc9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:04:05 +0900 Subject: [PATCH 12/27] =?UTF-8?q?[feat]=20=EC=84=9C=EB=B2=84=20=EB=82=B4?= =?UTF-8?q?=EB=B6=80=20=EC=98=A4=EB=A5=98=20=EC=98=88=EC=99=B8=20(500)=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=20=ED=95=B8=EB=93=A4=EB=9F=AC=20=ED=86=B5?= =?UTF-8?q?=ED=95=A9=20=EB=B0=8F=20=EB=94=94=EC=8A=A4=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9B=B9=ED=9B=85=20=ED=8A=B8=EB=A6=AC=EA=B1=B0=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/GlobalExceptionHandler.java | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index 6fdf69a56..dee1f5199 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -2,9 +2,9 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; +import konkuk.thip.common.Discord.DiscordClient; import konkuk.thip.common.dto.ErrorResponse; -import konkuk.thip.common.exception.AuthException; -import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.common.exception.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -25,6 +25,8 @@ @RequiredArgsConstructor public class GlobalExceptionHandler { + private final DiscordClient discordClient; + // 요청한 API가 없는 경우 @ExceptionHandler(NoHandlerFoundException.class) public ResponseEntity noHandlerExceptionHandler(NoHandlerFoundException e) { @@ -102,19 +104,36 @@ public ResponseEntity businessExceptionHandler(BusinessException .body(ErrorResponse.of(e.getErrorCode(), detail)); } - // 서버 내부 오류 예외 처리 - @ExceptionHandler(RuntimeException.class) - public ResponseEntity runtimeExceptionHandler(RuntimeException e) { - log.error("[RuntimeExceptionHandler] {}", e.getMessage(), e); // 메시지와 스택트레이스 출력 - return ResponseEntity - .status(API_SERVER_ERROR.getHttpStatus()) - .body(ErrorResponse.of(API_SERVER_ERROR)); - } + // 서버 내부 오류 예외 (500) 처리 + @ExceptionHandler({RuntimeException.class, IllegalStateException.class, + FirebaseException.class, InternalServerException.class, ExternalApiException.class}) + public ResponseEntity handleServerErrors(Exception e) { + log.error("[ServerErrorHandler] {}", e.getMessage(), e); + + String exceptionClassName = e.getClass().getSimpleName(); // 예외 클래스명 + String combinedMessage = "[" + exceptionClassName + "] " + e.getMessage(); // 메시지에 예외 클래스명 포함 + String stackTrace = org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(e); + + // 스택트레이스 요약: 두,세번째 줄 + 마지막줄 + String[] lines = stackTrace.split("\n"); + String stackSummary; + if (lines.length <= 3) { + stackSummary = stackTrace; // 짧으면 다 보여줌 + } else { + stackSummary = lines[1] + "\n" + lines[2] + "\n" + lines[lines.length - 1]; + } + +// // MDC에서 requestId, userId 추출 +// String requestId = MDC.get("requestId"); +// String userId = MDC.get("userId"); + + // MDC에서 requestId, userId 추출 + String requestId = "dummyRequestId"; + String userId = "dummyUserId"; + + // Discord 웹훅 전송 + discordClient.sendErrorMessage(combinedMessage, stackSummary, requestId, userId); - // IllegalStateException 예외 처리 - @ExceptionHandler(IllegalStateException.class) - public ResponseEntity illegalStateExceptionHandler(IllegalStateException e) { - log.error("[IllegalStateExceptionHandler] {}", e.getMessage()); return ResponseEntity .status(API_SERVER_ERROR.getHttpStatus()) .body(ErrorResponse.of(API_SERVER_ERROR)); From c6cd0a5b0b45232c9684bc99ef060201d739c419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:04:17 +0900 Subject: [PATCH 13/27] =?UTF-8?q?[feat]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9B=B9=ED=9B=85=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=9E=91=EC=84=B1=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/Discord/DiscordClient.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/Discord/DiscordClient.java diff --git a/src/main/java/konkuk/thip/common/Discord/DiscordClient.java b/src/main/java/konkuk/thip/common/Discord/DiscordClient.java new file mode 100644 index 000000000..2c59eb0ed --- /dev/null +++ b/src/main/java/konkuk/thip/common/Discord/DiscordClient.java @@ -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(env.equals("test")) return; + + WebClient webClient = WebClient.create(); + + Map embedData = new HashMap<>(); + embedData.put("title", "THIP 서버 500 에러 발생"); + + Map field1 = new HashMap<>(); + field1.put("name", "발생시각"); + field1.put("value", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + + Map field2 = new HashMap<>(); + field2.put("name", "에러 명"); + field2.put("value", message); + + Map field3 = new HashMap<>(); + field3.put("name", "스택 트레이스"); + field3.put("value", stackTrace); + + Map field4 = new HashMap<>(); + field4.put("name", "Request ID"); + field4.put("value", requestId != null ? requestId : "N/A"); + + Map 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 payload = new HashMap<>(); + payload.put("embeds", new Object[]{embedData}); + + webClient.post() + .uri(webhookUrl) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(payload) + .retrieve() + .bodyToMono(Void.class) + .block(); + } +} \ No newline at end of file From e8a5dfc95724931e3846f4c6081672122bd9b95e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:04:26 +0900 Subject: [PATCH 14/27] =?UTF-8?q?[refactor]=20FirebaseException=EC=9D=B4?= =?UTF-8?q?=20RuntimeException=20=EC=83=81=EC=86=8D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../message/adapter/out/firebase/FirebaseAdapter.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java index f907ceb6a..c1a0ae2dc 100644 --- a/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java +++ b/src/main/java/konkuk/thip/message/adapter/out/firebase/FirebaseAdapter.java @@ -11,6 +11,9 @@ import java.util.List; +import static konkuk.thip.common.exception.code.ErrorCode.FCM_TOKEN_DEVICE_ARRAY_MISMATCH; +import static konkuk.thip.common.exception.code.ErrorCode.FIREBASE_SEND_ERROR; + @Slf4j @Component @Profile("!test & !local") @@ -29,14 +32,14 @@ public void send(Message message, String fcmToken, String deviceId) { log.debug("[FCM:SEND] ok id={} token={} device={}", messageId, maskDependingProfile(fcmToken), maskDependingProfile(deviceId)); } catch (FirebaseMessagingException e) { log.warn("[FCM:SEND] fail token={} device={} code={} msg={}", maskDependingProfile(fcmToken), maskDependingProfile(deviceId), e.getMessagingErrorCode(), e.getMessage()); - throw new FirebaseException(e); + throw new FirebaseException(FIREBASE_SEND_ERROR); } } @Override public void sendBatch(List messages, List fcmTokens, List deviceIds) { if (messages.size() != fcmTokens.size() || messages.size() != deviceIds.size()) { - throw new FirebaseException(new IllegalArgumentException("메시지, FCM 토큰, 디바이스 ID 리스트의 크기는 같아야 합니다.")); + throw new FirebaseException(FCM_TOKEN_DEVICE_ARRAY_MISMATCH); } try { @@ -62,11 +65,11 @@ public void sendBatch(List messages, List fcmTokens, List 0) { log.warn("[FCM:BATCH] 일부 메시지 전송 실패: {}/{}", batchResponse.getFailureCount(), messages.size()); - throw new FirebaseException(); + throw new FirebaseException(FIREBASE_SEND_ERROR); } } catch (FirebaseMessagingException e) { log.warn("[FCM:BATCH] 메시지 전송 실패: code={} msg={}", e.getMessagingErrorCode(), e.getMessage()); - throw new FirebaseException(e); + throw new FirebaseException(FIREBASE_SEND_ERROR); } } From 20f348354bbb55241387b608f705ca504a337e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:04:41 +0900 Subject: [PATCH 15/27] =?UTF-8?q?[refactor]=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 0d7f37207..3bd6ee7b7 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -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으로 검색한 결과가 존재하지 않습니다."), /** * 90000 : recentSearch error @@ -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 리스트의 크기는 같아야 합니다."), /** From b89820dc90b0373edbc99aead3076c34dcf22696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:28:48 +0900 Subject: [PATCH 16/27] =?UTF-8?q?[feat]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9B=B9=ED=9B=85=20=EC=A0=84=EC=86=A1=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=EC=8B=9C=20=EB=A1=9C=EA=B9=85=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/exception/handler/GlobalExceptionHandler.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index dee1f5199..67495289c 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -132,7 +132,11 @@ public ResponseEntity handleServerErrors(Exception e) { String userId = "dummyUserId"; // Discord 웹훅 전송 - discordClient.sendErrorMessage(combinedMessage, stackSummary, requestId, userId); + try { + discordClient.sendErrorMessage(combinedMessage, stackSummary, requestId, userId); + } catch (Exception sendException) { + log.error("[ServerErrorHandler] -> [Discord] 전송 실패", sendException); + } return ResponseEntity .status(API_SERVER_ERROR.getHttpStatus()) From 648f8dbd286046c33d2bd97c6124c9e3aea37c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Thu, 2 Oct 2025 23:41:12 +0900 Subject: [PATCH 17/27] =?UTF-8?q?[refactor]=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/Discord/DiscordClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/common/Discord/DiscordClient.java b/src/main/java/konkuk/thip/common/Discord/DiscordClient.java index 2c59eb0ed..a2e503629 100644 --- a/src/main/java/konkuk/thip/common/Discord/DiscordClient.java +++ b/src/main/java/konkuk/thip/common/Discord/DiscordClient.java @@ -14,14 +14,14 @@ @Component public class DiscordClient { - @Value("$(discord.env") + @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(env.equals("test")) return; + if("test".equals(env)) return; WebClient webClient = WebClient.create(); From a5b8f7bee91ee7c8befb0067817a9ddd399a0f60 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:34:21 +0900 Subject: [PATCH 18/27] =?UTF-8?q?[chore]=20logstash=20=EC=9D=B8=EC=BD=94?= =?UTF-8?q?=EB=8D=94=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=A3=BC=EC=9E=85=20?= =?UTF-8?q?(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 97fd5306f..451c26b64 100644 --- a/build.gradle +++ b/build.gradle @@ -95,6 +95,9 @@ dependencies { // 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 From 6bd39945cd5ddc69e96f9393ec34dd87a7154dde Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:34:47 +0900 Subject: [PATCH 19/27] =?UTF-8?q?[chore]=20logback=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/logback-spring.xml | 85 +++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 src/main/resources/logback-spring.xml diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 000000000..f03dda54d --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + ${console.format} + + + + + + ${LOG_PATH}/info/info.log + + ${file.format} + + + INFO + ACCEPT + DENY + + + ${LOG_PATH}/info/info.%d{yyyy-MM-dd}.%i.log + 10MB + 10 + + + + + + ${LOG_PATH}/warn/warn.log + + ${file.format} + + + WARN + ACCEPT + DENY + + + ${LOG_PATH}/warn/warn.%d{yyyy-MM-dd}.%i.log + 10MB + 10 + + + + + + ${LOG_PATH}/error/error.log + + ${file.format} + + + ERROR + ACCEPT + DENY + + + ${LOG_PATH}/error/error.%d{yyyy-MM-dd}.%i.log + 10MB + 10 + + + + + + + + + + \ No newline at end of file From a42a813a91f1b55627f5b969b5e57cc643ea15ca Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:35:08 +0900 Subject: [PATCH 20/27] =?UTF-8?q?[chore]=20Mdc=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/logging/LoggingConstant.java | 16 +++++++++ .../thip/common/logging/MdcLoggingFilter.java | 34 +++++++++++++++++++ .../konkuk/thip/config/SecurityConfig.java | 3 ++ 3 files changed, 53 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/logging/LoggingConstant.java create mode 100644 src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java diff --git a/src/main/java/konkuk/thip/common/logging/LoggingConstant.java b/src/main/java/konkuk/thip/common/logging/LoggingConstant.java new file mode 100644 index 000000000..155485e00 --- /dev/null +++ b/src/main/java/konkuk/thip/common/logging/LoggingConstant.java @@ -0,0 +1,16 @@ +package konkuk.thip.common.logging; + +public enum LoggingConstant { + REQUEST_ID("request_id"), + USER_ID("user_id"); + + private final String value; + + LoggingConstant(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java b/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java new file mode 100644 index 000000000..2286e4532 --- /dev/null +++ b/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java @@ -0,0 +1,34 @@ +package konkuk.thip.common.logging; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.MDC; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.UUID; + +import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID; + +@Component +public class MdcLoggingFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, + jakarta.servlet.http.HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + try { + String rawRequestId = request.getHeader("X-Request-ID"); + String requestId = (rawRequestId == null || rawRequestId.trim().isEmpty()) + ? UUID.randomUUID().toString() + : rawRequestId; + + MDC.put(REQUEST_ID.getValue(), requestId); + filterChain.doFilter(request, response); + } finally { + MDC.clear(); + } + } +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/config/SecurityConfig.java b/src/main/java/konkuk/thip/config/SecurityConfig.java index 4bfac31bc..daec669c8 100644 --- a/src/main/java/konkuk/thip/config/SecurityConfig.java +++ b/src/main/java/konkuk/thip/config/SecurityConfig.java @@ -1,5 +1,6 @@ package konkuk.thip.config; +import konkuk.thip.common.logging.MdcLoggingFilter; import konkuk.thip.common.security.constant.SecurityWhitelist; import konkuk.thip.common.security.filter.JwtAuthenticationEntryPoint; import konkuk.thip.common.security.filter.JwtAuthenticationFilter; @@ -46,6 +47,7 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomOAuth2UserService customOAuth2UserService; private final CustomSuccessHandler customSuccessHandler; + private final MdcLoggingFilter mdcLoggingFilter; private final ClientRegistrationRepository clientRegistrationRepository; private final WebDomainProperties webDomainProperties; @@ -69,6 +71,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterBefore(mdcLoggingFilter, JwtAuthenticationFilter.class) .oauth2Login((oauth2) -> oauth2 .authorizationEndpoint(authorizationEndpointConfig -> authorizationEndpointConfig .authorizationRequestResolver(resolver) From d3cb4097000f14a6000c97ddfe7baa673079163f Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:35:37 +0900 Subject: [PATCH 21/27] =?UTF-8?q?[chore]=20=EC=9D=91=EB=8B=B5=EC=97=90?= =?UTF-8?q?=EC=84=9C=20requestId=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/dto/BaseResponse.java | 14 +++++++++++--- .../java/konkuk/thip/common/dto/ErrorResponse.java | 14 ++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/main/java/konkuk/thip/common/dto/BaseResponse.java b/src/main/java/konkuk/thip/common/dto/BaseResponse.java index 1d4718d61..ba20eff2d 100644 --- a/src/main/java/konkuk/thip/common/dto/BaseResponse.java +++ b/src/main/java/konkuk/thip/common/dto/BaseResponse.java @@ -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 { @JsonProperty("isSuccess") @@ -15,17 +20,20 @@ public class BaseResponse { 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 BaseResponse ok(T data) { diff --git a/src/main/java/konkuk/thip/common/dto/ErrorResponse.java b/src/main/java/konkuk/thip/common/dto/ErrorResponse.java index 6b983a124..c0b1b17e2 100644 --- a/src/main/java/konkuk/thip/common/dto/ErrorResponse.java +++ b/src/main/java/konkuk/thip/common/dto/ErrorResponse.java @@ -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") @@ -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) { @@ -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())); } } From 4d07e52edbf24e50da82f7a4a701b581b7dddc17 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:36:01 +0900 Subject: [PATCH 22/27] =?UTF-8?q?[chore]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=97=90=EB=9F=AC=20=EC=A0=84=EC=86=A1=EC=8B=9C=20?= =?UTF-8?q?Mdc=EC=97=90=EC=84=9C=20=EC=B6=94=EC=B6=9C=ED=9B=84=20id=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/handler/GlobalExceptionHandler.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index dee1f5199..cc023f24b 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -7,6 +7,7 @@ import konkuk.thip.common.exception.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; import org.springframework.http.ResponseEntity; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -19,6 +20,8 @@ import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID; +import static konkuk.thip.common.logging.LoggingConstant.USER_ID; @Slf4j @RestControllerAdvice @@ -123,13 +126,9 @@ public ResponseEntity handleServerErrors(Exception e) { stackSummary = lines[1] + "\n" + lines[2] + "\n" + lines[lines.length - 1]; } -// // MDC에서 requestId, userId 추출 -// String requestId = MDC.get("requestId"); -// String userId = MDC.get("userId"); - // MDC에서 requestId, userId 추출 - String requestId = "dummyRequestId"; - String userId = "dummyUserId"; + String requestId = MDC.get(REQUEST_ID.getValue()); + String userId = MDC.get(USER_ID.getValue()); // Discord 웹훅 전송 discordClient.sendErrorMessage(combinedMessage, stackSummary, requestId, userId); From c263dec77cc6272185e155d3ef41de6599caf193 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Fri, 3 Oct 2025 16:36:24 +0900 Subject: [PATCH 23/27] =?UTF-8?q?[refactor]=20=ED=86=A0=ED=81=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20userId=20=EA=BA=BC=EB=82=B4=EC=84=9C=20MDC=EC=97=90?= =?UTF-8?q?=20=EB=84=A3=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../filter/JwtAuthenticationFilter.java | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index 51070cf47..8baf595e8 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -13,6 +13,7 @@ import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; import org.springframework.http.server.PathContainer; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -26,6 +27,7 @@ import java.util.List; import static konkuk.thip.common.exception.code.ErrorCode.*; +import static konkuk.thip.common.logging.LoggingConstant.USER_ID; import static konkuk.thip.common.security.constant.AuthParameters.*; @Slf4j @@ -67,24 +69,12 @@ protected void doFilterInternal(HttpServletRequest request, FilterChain filterChain) throws ServletException, IOException { try { String token = extractToken(request); - if (token == null) { - throw new AuthException(AUTH_TOKEN_NOT_FOUND); - } - - if (userTokenBlacklistQueryPort.isTokenBlacklisted(token)) { - throw new AuthException(AUTH_BLACKLIST_TOKEN); - } - - if (!jwtUtil.validateToken(token)) { - throw new AuthException(AUTH_INVALID_TOKEN); - } - if (jwtUtil.isExpired(token)) { - throw new AuthException(AUTH_EXPIRED_TOKEN); - } + validateToken(token); request.setAttribute(JWT_TOKEN_ATTRIBUTE.getValue(), token); LoginUser loginUser = jwtUtil.getLoginUser(token); + MDC.put(USER_ID.getValue(), String.valueOf(loginUser.userId())); if (loginUser.userId() != null) { request.setAttribute(JWT_ACCESS_TOKEN_KEY.getValue(), loginUser.userId()); @@ -105,6 +95,24 @@ protected void doFilterInternal(HttpServletRequest request, } } + private void validateToken(String token) { + if (token == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + + if (userTokenBlacklistQueryPort.isTokenBlacklisted(token)) { + throw new AuthException(AUTH_BLACKLIST_TOKEN); + } + + if (!jwtUtil.validateToken(token)) { + throw new AuthException(AUTH_INVALID_TOKEN); + } + + if (jwtUtil.isExpired(token)) { + throw new AuthException(AUTH_EXPIRED_TOKEN); + } + } + private String extractToken(HttpServletRequest request) { String authorization = request.getHeader(JWT_HEADER_KEY.getValue()); if (authorization != null && authorization.startsWith(JWT_PREFIX.getValue())) { From 3ee008479675b0b6d2b58b5a469d4fd899466543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 3 Oct 2025 16:41:29 +0900 Subject: [PATCH 24/27] =?UTF-8?q?[refactor]=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=EB=AA=85=20=EC=98=A4=ED=83=80=20=EC=88=98=EC=A0=95=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/common/{Discord => discord}/DiscordClient.java | 2 +- .../thip/common/exception/handler/GlobalExceptionHandler.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/main/java/konkuk/thip/common/{Discord => discord}/DiscordClient.java (98%) diff --git a/src/main/java/konkuk/thip/common/Discord/DiscordClient.java b/src/main/java/konkuk/thip/common/discord/DiscordClient.java similarity index 98% rename from src/main/java/konkuk/thip/common/Discord/DiscordClient.java rename to src/main/java/konkuk/thip/common/discord/DiscordClient.java index a2e503629..302037859 100644 --- a/src/main/java/konkuk/thip/common/Discord/DiscordClient.java +++ b/src/main/java/konkuk/thip/common/discord/DiscordClient.java @@ -1,4 +1,4 @@ -package konkuk.thip.common.Discord; +package konkuk.thip.common.discord; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index f93c2ca82..478200baf 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -2,7 +2,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; -import konkuk.thip.common.Discord.DiscordClient; +import konkuk.thip.common.discord.DiscordClient; import konkuk.thip.common.dto.ErrorResponse; import konkuk.thip.common.exception.*; import lombok.RequiredArgsConstructor; From 573d268782e04cd0636ab5f8d91467f3d69c72fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Sat, 4 Oct 2025 20:33:37 +0900 Subject: [PATCH 25/27] =?UTF-8?q?[refactor]=20static=20=EC=9E=84=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=EB=AC=B8=20=EC=B6=94=EA=B0=80=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/exception/handler/GlobalExceptionHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java index 478200baf..2fba5aed8 100644 --- a/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java +++ b/src/main/java/konkuk/thip/common/exception/handler/GlobalExceptionHandler.java @@ -22,6 +22,7 @@ import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.common.logging.LoggingConstant.REQUEST_ID; import static konkuk.thip.common.logging.LoggingConstant.USER_ID; +import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace; @Slf4j @RestControllerAdvice @@ -115,7 +116,7 @@ public ResponseEntity handleServerErrors(Exception e) { String exceptionClassName = e.getClass().getSimpleName(); // 예외 클래스명 String combinedMessage = "[" + exceptionClassName + "] " + e.getMessage(); // 메시지에 예외 클래스명 포함 - String stackTrace = org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace(e); + String stackTrace = getStackTrace(e); // 스택트레이스 요약: 두,세번째 줄 + 마지막줄 String[] lines = stackTrace.split("\n"); From b1995db61a14954f6d43bc710379d579e68e5640 Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 5 Oct 2025 01:59:58 +0900 Subject: [PATCH 26/27] [refactor] static import (#319) --- src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java b/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java index 2286e4532..ef15bdd05 100644 --- a/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java +++ b/src/main/java/konkuk/thip/common/logging/MdcLoggingFilter.java @@ -3,6 +3,7 @@ import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.MDC; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; @@ -17,7 +18,7 @@ public class MdcLoggingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, - jakarta.servlet.http.HttpServletResponse response, + HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String rawRequestId = request.getHeader("X-Request-ID"); From 9cab189a73808c2540c09f083ed433a9afc6d9cb Mon Sep 17 00:00:00 2001 From: janghyunjun Date: Sun, 5 Oct 2025 02:00:09 +0900 Subject: [PATCH 27/27] =?UTF-8?q?[refactor]=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=A6=9D=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=20=EB=B3=80=EA=B2=BD=20(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/security/filter/JwtAuthenticationFilter.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index 8baf595e8..3c07d2bee 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -104,13 +104,13 @@ private void validateToken(String token) { throw new AuthException(AUTH_BLACKLIST_TOKEN); } - if (!jwtUtil.validateToken(token)) { - throw new AuthException(AUTH_INVALID_TOKEN); - } - if (jwtUtil.isExpired(token)) { throw new AuthException(AUTH_EXPIRED_TOKEN); } + + if (!jwtUtil.validateToken(token)) { + throw new AuthException(AUTH_INVALID_TOKEN); + } } private String extractToken(HttpServletRequest request) {