From 0ed0c7a54e9534a32cfeb1c7f700c74cbef89402 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:42:17 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discord/event/DiscordReportCreatedEvent.java | 7 +++++++ .../discord/event/DiscordReviewCompletedEvent.java | 12 ++++++++++++ .../discord/event/DiscordReviewRequestedEvent.java | 11 +++++++++++ 3 files changed, 30 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/common/discord/event/DiscordReportCreatedEvent.java create mode 100644 src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewCompletedEvent.java create mode 100644 src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewRequestedEvent.java diff --git a/src/main/java/com/example/solidconnection/common/discord/event/DiscordReportCreatedEvent.java b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReportCreatedEvent.java new file mode 100644 index 000000000..5abd4e81b --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReportCreatedEvent.java @@ -0,0 +1,7 @@ +package com.example.solidconnection.common.discord.event; + +public record DiscordReportCreatedEvent( + long reporterId +) { + +} diff --git a/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewCompletedEvent.java b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewCompletedEvent.java new file mode 100644 index 000000000..35045e211 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewCompletedEvent.java @@ -0,0 +1,12 @@ +package com.example.solidconnection.common.discord.event; + +import com.example.solidconnection.common.discord.DiscordNotificationType; + +public record DiscordReviewCompletedEvent( + DiscordNotificationType type, + long reviewId, + String applicantInfo, + String marker +) { + +} diff --git a/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewRequestedEvent.java b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewRequestedEvent.java new file mode 100644 index 000000000..4274a0402 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/discord/event/DiscordReviewRequestedEvent.java @@ -0,0 +1,11 @@ +package com.example.solidconnection.common.discord.event; + +import com.example.solidconnection.common.discord.DiscordNotificationType; + +public record DiscordReviewRequestedEvent( + DiscordNotificationType type, + long reviewId, + String applicantInfo +) { + +} From aa95b971e2931c05d451e4593cc3b463518deaf3 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:42:47 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20=EC=BB=A4=EB=B0=8B=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=EB=94=94=EC=8A=A4=EC=BD=94=EB=93=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=EC=9D=84=20=EC=A0=84=EC=86=A1=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=EB=84=88=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscordNotificationEventListener.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java diff --git a/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java b/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java new file mode 100644 index 000000000..7ae0b6040 --- /dev/null +++ b/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java @@ -0,0 +1,52 @@ +package com.example.solidconnection.common.discord; + +import com.example.solidconnection.common.discord.event.DiscordReportCreatedEvent; +import com.example.solidconnection.common.discord.event.DiscordReviewCompletedEvent; +import com.example.solidconnection.common.discord.event.DiscordReviewRequestedEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionalEventListener; + +/* + * - 커밋 이후 별도 스레드에서 전송하므로 디스코드 장애가 업무 트랜잭션에 영향을 주지 않는다. + * - 예외 메시지에 webhook url 이 포함될 수 있으므로 예외 클래스명만 남긴다. + * */ +@Component +@EnableAsync +@RequiredArgsConstructor +@Slf4j +public class DiscordNotificationEventListener { + + private final DiscordNotifier discordNotifier; + + @Async("asyncExecutor") + @TransactionalEventListener + public void handleReviewRequested(DiscordReviewRequestedEvent event) { + try { + discordNotifier.notify(event.type(), event.reviewId(), event.applicantInfo()); + } catch (Exception e) { + log.error("검수 요청 알림을 전송하지 못했습니다. reviewType={}, reviewId={}, exception={}", + event.type(), event.reviewId(), e.getClass().getSimpleName()); + } + } + + @Async("asyncExecutor") + @TransactionalEventListener + public void handleReviewCompleted(DiscordReviewCompletedEvent event) { + try { + discordNotifier.markReviewResult(event.type(), event.reviewId(), event.applicantInfo(), event.marker()); + } catch (Exception e) { + log.error("검수 결과를 표시하지 못했습니다. reviewType={}, reviewId={}, exception={}", + event.type(), event.reviewId(), e.getClass().getSimpleName()); + } + } + + @Async("asyncExecutor") + @TransactionalEventListener + public void handleReportCreated(DiscordReportCreatedEvent event) { + discordNotifier.notify(DiscordNotificationType.REPORT, "신고자 ID: " + event.reporterId()); + } +} From 2d3d19a6abd66927be01651560349003d1ddba4d Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:00 +0900 Subject: [PATCH 03/13] =?UTF-8?q?refactor:=20=EC=84=B1=EC=A0=81=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=A1=9C=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../score/service/ScoreService.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/score/service/ScoreService.java b/src/main/java/com/example/solidconnection/score/service/ScoreService.java index d84aeee7f..a2abc2831 100644 --- a/src/main/java/com/example/solidconnection/score/service/ScoreService.java +++ b/src/main/java/com/example/solidconnection/score/service/ScoreService.java @@ -5,7 +5,7 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; +import com.example.solidconnection.common.discord.event.DiscordReviewRequestedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.s3.domain.UploadPath; import com.example.solidconnection.s3.dto.UploadedFileUrlResponse; @@ -26,6 +26,7 @@ import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -39,7 +40,7 @@ public class ScoreService { private final LanguageTestScoreRepository languageTestScoreRepository; private final SiteUserRepository siteUserRepository; private final HomeUniversityQueryService homeUniversityQueryService; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, MultipartFile file) { @@ -49,7 +50,11 @@ public Long submitGpaScore(long siteUserId, GpaScoreRequest gpaScoreRequest, Mul Gpa gpa = new Gpa(gpaScoreRequest.gpa(), gpaScoreRequest.gpaCriteria(), uploadedFile.fileUrl()); GpaScore newGpaScore = new GpaScore(gpa, siteUser); GpaScore savedNewGpaScore = gpaScoreRepository.save(newGpaScore); - discordNotifier.notify(DiscordNotificationType.GPA_SCORE, savedNewGpaScore.getId(), siteUser.getNickname()); + eventPublisher.publishEvent(new DiscordReviewRequestedEvent( + DiscordNotificationType.GPA_SCORE, + savedNewGpaScore.getId(), + siteUser.getNickname() + )); return savedNewGpaScore.getId(); } @@ -62,7 +67,11 @@ public Long submitLanguageTestScore(long siteUserId, LanguageTestScoreRequest la languageTestScoreRequest.languageTestScore(), uploadedFile.fileUrl()); LanguageTestScore newScore = new LanguageTestScore(languageTest, siteUser); LanguageTestScore savedNewScore = languageTestScoreRepository.save(newScore); - discordNotifier.notify(DiscordNotificationType.LANGUAGE_TEST_SCORE, savedNewScore.getId(), siteUser.getNickname()); + eventPublisher.publishEvent(new DiscordReviewRequestedEvent( + DiscordNotificationType.LANGUAGE_TEST_SCORE, + savedNewScore.getId(), + siteUser.getNickname() + )); return savedNewScore.getId(); } From 5f493b5eeb5ae4d66859e788928469ed5c4c9559 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:12 +0900 Subject: [PATCH 04/13] =?UTF-8?q?refactor:=20=EB=A9=98=ED=86=A0=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=EB=A1=9C=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mentor/service/MentorApplicationService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java index 48679daf1..8cff2cbdc 100644 --- a/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/mentor/service/MentorApplicationService.java @@ -6,7 +6,7 @@ import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; +import com.example.solidconnection.common.discord.event.DiscordReviewRequestedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.MentorApplication; import com.example.solidconnection.mentor.domain.MentorApplicationStatus; @@ -22,6 +22,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -37,7 +38,7 @@ public class MentorApplicationService { private final SiteUserRepository siteUserRepository; private final S3Service s3Service; private final TermRepository termRepository; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional public void submitMentorApplication( @@ -63,11 +64,11 @@ public void submitMentorApplication( mentorApplicationRequest.exchangeStatus() ); MentorApplication savedMentorApplication = mentorApplicationRepository.save(mentorApplication); - discordNotifier.notify( + eventPublisher.publishEvent(new DiscordReviewRequestedEvent( DiscordNotificationType.MENTOR_APPLICATION, savedMentorApplication.getId(), siteUser.getNickname() - ); + )); } private void ensureNoPendingOrApprovedMentorApplication(long siteUserId) { From 1c56bc1bf69879fc8a1e34b424430f5265ec7b7a Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:30 +0900 Subject: [PATCH 05/13] =?UTF-8?q?refactor:=20=ED=95=99=EC=A0=90=20?= =?UTF-8?q?=EA=B2=80=EC=88=98=20=EA=B2=B0=EA=B3=BC=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A1=9C=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminGpaScoreService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java index 064e07f89..90fea25e5 100644 --- a/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java +++ b/src/main/java/com/example/solidconnection/admin/service/AdminGpaScoreService.java @@ -10,14 +10,15 @@ import com.example.solidconnection.application.domain.Gpa; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; import com.example.solidconnection.common.discord.DiscordReviewMarker; +import com.example.solidconnection.common.discord.event.DiscordReviewCompletedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -29,7 +30,7 @@ public class AdminGpaScoreService { private final GpaScoreRepository gpaScoreRepository; private final SiteUserRepository siteUserRepository; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional(readOnly = true) public Page searchGpaScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable) { @@ -64,11 +65,11 @@ private void publishReviewResult(GpaScore gpaScore, VerifyStatus verifyStatus) { } SiteUser siteUser = siteUserRepository.findById(gpaScore.getSiteUserId()) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - discordNotifier.markReviewResult( + eventPublisher.publishEvent(new DiscordReviewCompletedEvent( DiscordNotificationType.GPA_SCORE, gpaScore.getId(), siteUser.getNickname(), marker - ); + )); } } From 5b6bcb60d8ab1e74a6e6d750e9a06292e6d80362 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:34 +0900 Subject: [PATCH 06/13] =?UTF-8?q?refactor:=20=EC=96=B4=ED=95=99=20?= =?UTF-8?q?=EA=B2=80=EC=88=98=20=EA=B2=B0=EA=B3=BC=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A1=9C=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminLanguageTestScoreService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java index d52966f30..09ac4ca34 100644 --- a/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java +++ b/src/main/java/com/example/solidconnection/admin/service/AdminLanguageTestScoreService.java @@ -10,14 +10,15 @@ import com.example.solidconnection.application.domain.LanguageTest; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; import com.example.solidconnection.common.discord.DiscordReviewMarker; +import com.example.solidconnection.common.discord.event.DiscordReviewCompletedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.score.domain.LanguageTestScore; import com.example.solidconnection.score.repository.LanguageTestScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.repository.SiteUserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -29,7 +30,7 @@ public class AdminLanguageTestScoreService { private final LanguageTestScoreRepository languageTestScoreRepository; private final SiteUserRepository siteUserRepository; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional(readOnly = true) public Page searchLanguageTestScores(ScoreSearchCondition scoreSearchCondition, Pageable pageable) { @@ -64,11 +65,11 @@ private void publishReviewResult(LanguageTestScore languageTestScore, VerifyStat } SiteUser siteUser = siteUserRepository.findById(languageTestScore.getSiteUserId()) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - discordNotifier.markReviewResult( + eventPublisher.publishEvent(new DiscordReviewCompletedEvent( DiscordNotificationType.LANGUAGE_TEST_SCORE, languageTestScore.getId(), siteUser.getNickname(), marker - ); + )); } } From b604f51310e8d242db9ba8d2e79951a59070d8e2 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:47 +0900 Subject: [PATCH 07/13] =?UTF-8?q?refactor:=20=EB=A9=98=ED=86=A0=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EA=B2=80=EC=88=98=20=EA=B2=B0=EA=B3=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=A5=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminMentorApplicationService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java b/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java index a2cbed69a..d0741e404 100644 --- a/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java +++ b/src/main/java/com/example/solidconnection/admin/service/AdminMentorApplicationService.java @@ -10,8 +10,8 @@ import com.example.solidconnection.admin.dto.MentorApplicationSearchCondition; import com.example.solidconnection.admin.dto.MentorApplicationSearchResponse; import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; import com.example.solidconnection.common.discord.DiscordReviewMarker; +import com.example.solidconnection.common.discord.event.DiscordReviewCompletedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.mentor.domain.Mentor; import com.example.solidconnection.mentor.domain.MentorApplication; @@ -25,6 +25,7 @@ import java.util.List; import java.util.stream.IntStream; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -38,7 +39,7 @@ public class AdminMentorApplicationService { private final HostUniversityRepository hostUniversityRepository; private final SiteUserRepository siteUserRepository; private final MentorRepository mentorRepository; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional(readOnly = true) public Page searchMentorApplications( @@ -91,12 +92,12 @@ public void rejectMentorApplication( } private void publishReviewResult(long mentorApplicationId, String applicantInfo, String marker) { - discordNotifier.markReviewResult( + eventPublisher.publishEvent(new DiscordReviewCompletedEvent( DiscordNotificationType.MENTOR_APPLICATION, mentorApplicationId, applicantInfo, marker - ); + )); } @Transactional(readOnly = true) From fcfca3235ba85aa5f6e50c98354b3d3097519660 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:43:56 +0900 Subject: [PATCH 08/13] =?UTF-8?q?refactor:=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EC=9D=B4=EB=B2=A4=ED=8A=B8?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=9C=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/report/service/ReportService.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/report/service/ReportService.java b/src/main/java/com/example/solidconnection/report/service/ReportService.java index 06015568e..2c8c7fbbd 100644 --- a/src/main/java/com/example/solidconnection/report/service/ReportService.java +++ b/src/main/java/com/example/solidconnection/report/service/ReportService.java @@ -4,8 +4,7 @@ import com.example.solidconnection.chat.domain.ChatParticipant; import com.example.solidconnection.chat.repository.ChatMessageRepository; import com.example.solidconnection.chat.repository.ChatParticipantRepository; -import com.example.solidconnection.common.discord.DiscordNotificationType; -import com.example.solidconnection.common.discord.DiscordNotifier; +import com.example.solidconnection.common.discord.event.DiscordReportCreatedEvent; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.community.post.domain.Post; @@ -19,6 +18,7 @@ import com.example.solidconnection.siteuser.repository.SiteUserRepository; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; @Service @@ -30,7 +30,7 @@ public class ReportService { private final PostRepository postRepository; private final ChatMessageRepository chatMessageRepository; private final ChatParticipantRepository chatParticipantRepository; - private final DiscordNotifier discordNotifier; + private final ApplicationEventPublisher eventPublisher; @Transactional public void createReport(long reporterId, ReportRequest request) { @@ -42,7 +42,7 @@ public void createReport(long reporterId, ReportRequest request) { Report report = new Report(reporterId, reportedId, request.reportType(), request.targetType(), request.targetId()); reportRepository.save(report); - discordNotifier.notify(DiscordNotificationType.REPORT, "신고자 ID: " + reporterId); + eventPublisher.publishEvent(new DiscordReportCreatedEvent(reporterId)); } private void validateReporterAndReportedExists(long reporterId, long reportedId) { From f0f82a51996a87c7d707ddf5e66ebd453dd2e136 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:44:18 +0900 Subject: [PATCH 09/13] =?UTF-8?q?refactor:=20DiscordNotifier=20=EC=9D=98?= =?UTF-8?q?=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=84=A4=EC=A0=95=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/common/discord/DiscordNotifier.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java b/src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java index 6c15e24a4..6d5bf8b91 100644 --- a/src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java +++ b/src/main/java/com/example/solidconnection/common/discord/DiscordNotifier.java @@ -4,13 +4,10 @@ import com.example.solidconnection.common.discord.service.DiscordNotificationService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Async; -import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.stereotype.Component; import org.springframework.web.client.HttpClientErrorException; @Component -@EnableAsync @Slf4j public class DiscordNotifier { @@ -33,7 +30,6 @@ public DiscordNotifier( this.environment = environment; } - @Async public void notify(DiscordNotificationType type, String applicantInfo) { if (isNotifyDisabled()) { return; From e3deae6e1e2be2ea757c87dbccd73676acee5b9f Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:45:14 +0900 Subject: [PATCH 10/13] =?UTF-8?q?test:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=EB=84=88=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiscordNotificationEventListenerTest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/common/discord/DiscordNotificationEventListenerTest.java diff --git a/src/test/java/com/example/solidconnection/common/discord/DiscordNotificationEventListenerTest.java b/src/test/java/com/example/solidconnection/common/discord/DiscordNotificationEventListenerTest.java new file mode 100644 index 000000000..ffa8d3db8 --- /dev/null +++ b/src/test/java/com/example/solidconnection/common/discord/DiscordNotificationEventListenerTest.java @@ -0,0 +1,56 @@ +package com.example.solidconnection.common.discord; + +import static org.awaitility.Awaitility.await; +import static org.mockito.BDDMockito.then; + +import com.example.solidconnection.common.discord.event.DiscordReviewRequestedEvent; +import com.example.solidconnection.support.TestContainerSpringBootTest; +import java.time.Duration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.transaction.support.TransactionTemplate; + +@TestContainerSpringBootTest +@DisplayName("디스코드 알림 이벤트 리스너 테스트") +class DiscordNotificationEventListenerTest { + + private static final DiscordReviewRequestedEvent EVENT = + new DiscordReviewRequestedEvent(DiscordNotificationType.GPA_SCORE, 1L, "홍길동"); + + @Autowired + private ApplicationEventPublisher eventPublisher; + + @Autowired + private TransactionTemplate transactionTemplate; + + @MockitoBean + private DiscordNotifier discordNotifier; + + @Test + void 트랜잭션이_커밋되면_알림을_전송한다() { + // when + transactionTemplate.executeWithoutResult(status -> eventPublisher.publishEvent(EVENT)); + + // then + await().atMost(Duration.ofSeconds(3)) + .untilAsserted(() -> then(discordNotifier).should() + .notify(EVENT.type(), EVENT.reviewId(), EVENT.applicantInfo())); + } + + @Test + void 트랜잭션이_롤백되면_알림을_전송하지_않는다() { + // when + transactionTemplate.executeWithoutResult(status -> { + eventPublisher.publishEvent(EVENT); + status.setRollbackOnly(); + }); + + // then + await().during(Duration.ofMillis(300)) + .atMost(Duration.ofSeconds(1)) + .untilAsserted(() -> then(discordNotifier).shouldHaveNoInteractions()); + } +} From 90446c380010012acbd4fa44b5c3b1a227e4e0e1 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:46:02 +0900 Subject: [PATCH 11/13] =?UTF-8?q?test:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C?= =?UTF-8?q?=20=EC=84=B1=EC=A0=81=20=EB=93=B1=EB=A1=9D=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../score/service/ScoreServiceTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java index 89cc8535e..285c6d195 100644 --- a/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/score/service/ScoreServiceTest.java @@ -2,7 +2,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willThrow; import com.example.solidconnection.common.VerifyStatus; import com.example.solidconnection.common.discord.DiscordNotifier; @@ -33,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mock.web.MockMultipartFile; import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.web.client.RestClientException; @TestContainerSpringBootTest @DisplayName("점수 서비스 테스트") @@ -158,6 +163,22 @@ void setUp() { assertThat(savedScore.getId()).isEqualTo(scoreId); } + @Test + void 디스코드_알림에_실패해도_GPA_점수를_등록한다() { + // given + GpaScoreRequest request = createGpaScoreRequest(); + MockMultipartFile file = createFile(); + given(s3Service.uploadFile(file, UploadPath.GPA)).willReturn(new UploadedFileUrlResponse("/gpa-report.pdf")); + willThrow(new RestClientException("discord unavailable")) + .given(discordNotifier).notify(any(), anyLong(), anyString()); + + // when + long scoreId = scoreService.submitGpaScore(user.getId(), request, file); + + // then + assertThat(gpaScoreRepository.findById(scoreId)).isPresent(); + } + @Test void 어학_시험_점수를_등록한다() { // given From 1f285996564ef6c590d2efe835667c6b53721086 Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 19:46:47 +0900 Subject: [PATCH 12/13] =?UTF-8?q?test:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EA=B2=80=EC=88=98=20=EA=B2=B0=EA=B3=BC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=88=98=EC=A0=95=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/AdminGpaScoreServiceTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java b/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java index e8f25dbba..8e760e423 100644 --- a/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java +++ b/src/test/java/com/example/solidconnection/admin/service/AdminGpaScoreServiceTest.java @@ -6,6 +6,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.willThrow; import com.example.solidconnection.admin.dto.GpaScoreResponse; import com.example.solidconnection.admin.dto.GpaScoreSearchResponse; @@ -15,6 +19,7 @@ import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.score.domain.GpaScore; import com.example.solidconnection.score.fixture.GpaScoreFixture; +import com.example.solidconnection.score.repository.GpaScoreRepository; import com.example.solidconnection.siteuser.domain.SiteUser; import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; @@ -28,6 +33,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.web.client.RestClientException; @TestContainerSpringBootTest @DisplayName("학점 검증 관리자 서비스 테스트") @@ -36,6 +42,9 @@ class AdminGpaScoreServiceTest { @Autowired private AdminGpaScoreService adminGpaScoreService; + @Autowired + private GpaScoreRepository gpaScoreRepository; + /* * - 테스트 설정의 discord.webhook-url 이 비어 있지 않아 실제 요청이 나갈 수 있으므로 목으로 대체한다. * */ @@ -185,5 +194,25 @@ class GPA_점수_검증_및_수정 { .isInstanceOf(CustomException.class) .hasMessage(GPA_SCORE_NOT_FOUND.getMessage()); } + + @Test + void 디스코드_검수_결과_표시에_실패해도_검증상태를_수정한다() { + // given + GpaScoreUpdateRequest request = new GpaScoreUpdateRequest( + 3.8, + 4.3, + VerifyStatus.APPROVED, + null + ); + willThrow(new RestClientException("discord unavailable")) + .given(discordNotifier).markReviewResult(any(), anyLong(), anyString(), anyString()); + + // when + adminGpaScoreService.updateGpaScore(gpaScore1.getId(), request); + + // then + GpaScore updatedGpaScore = gpaScoreRepository.findById(gpaScore1.getId()).orElseThrow(); + assertThat(updatedGpaScore.getVerifyStatus()).isEqualTo(VerifyStatus.APPROVED); + } } } From 4437d421dcefe4d5f2216e9e41ad262c57fb7f9e Mon Sep 17 00:00:00 2001 From: whqtker Date: Sun, 25 Oct 2026 20:26:01 +0900 Subject: [PATCH 13/13] =?UTF-8?q?refactor:=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=EB=A6=AC=EC=8A=A4=EB=84=88=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/discord/DiscordNotificationEventListener.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java b/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java index 7ae0b6040..4642ab326 100644 --- a/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java +++ b/src/main/java/com/example/solidconnection/common/discord/DiscordNotificationEventListener.java @@ -10,10 +10,6 @@ import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionalEventListener; -/* - * - 커밋 이후 별도 스레드에서 전송하므로 디스코드 장애가 업무 트랜잭션에 영향을 주지 않는다. - * - 예외 메시지에 webhook url 이 포함될 수 있으므로 예외 클래스명만 남긴다. - * */ @Component @EnableAsync @RequiredArgsConstructor