From 844d433a2f18b290beeeb833150fe57f44c5eebf Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 02:15:03 +0900 Subject: [PATCH 1/3] =?UTF-8?q?discord:=20Discord=20Webhook=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=97=B0=EB=8F=99=20(#161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - infrastructure/discord/ 패키지 신설 (DiscordNotifier 인터페이스 + prod 구현체) - 스케줄러 배치 결과 Discord 알림 (성공/실패 구분) - 회원가입, 핸들등록 이벤트 알림 - BatchResult record 도입으로 서비스 반환 타입 변경 --- .../auth/GoogleOAuth2UserService.java | 17 +++++ .../discord/DiscordMessage.java | 67 +++++++++++++++++++ .../discord/DiscordNotifier.java | 12 ++++ .../discord/DiscordProperties.java | 13 ++++ .../discord/DiscordWebhookNotifier.java | 54 +++++++++++++++ .../ryu/studyhelper/member/MemberService.java | 8 +++ .../dto/internal/BatchResult.java | 6 ++ .../scheduler/EmailSendScheduler.java | 33 ++++++--- .../ProblemRecommendationScheduler.java | 33 ++++++--- .../service/RecommendationEmailService.java | 4 +- .../ScheduledRecommendationService.java | 4 +- src/main/resources/application.yml | 6 ++ 12 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordProperties.java create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java create mode 100644 src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java diff --git a/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java b/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java index 800a247..c454bb0 100644 --- a/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java +++ b/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java @@ -2,6 +2,8 @@ import com.ryu.studyhelper.auth.dto.OAuthInfo; import com.ryu.studyhelper.config.security.PrincipalDetails; +import com.ryu.studyhelper.infrastructure.discord.DiscordMessage; +import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier; import com.ryu.studyhelper.member.repository.MemberRepository; import com.ryu.studyhelper.member.domain.Member; import jakarta.transaction.Transactional; @@ -22,6 +24,7 @@ public class GoogleOAuth2UserService extends DefaultOAuth2UserService { private final MemberRepository memberRepository; + private final DiscordNotifier discordNotifier; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { @@ -54,7 +57,21 @@ private Member saveOrUpdateMember(OAuthInfo oAuthInfo) { Member newMember = Member.create("google", oAuthInfo.getProviderId(), oAuthInfo.getEmail()); Member savedMember = memberRepository.save(newMember); log.info("신규 사용자 생성 완료: {}", savedMember.getEmail()); + + discordNotifier.sendEvent(DiscordMessage.event("신규 회원 가입", + "회원 ID", String.valueOf(savedMember.getId()), + "이메일", maskEmail(savedMember.getEmail()) + )); + return savedMember; } } + + private String maskEmail(String email) { + int atIndex = email.indexOf('@'); + if (atIndex <= 3) { + return email.charAt(0) + "***" + email.substring(atIndex); + } + return email.substring(0, 3) + "***" + email.substring(atIndex); + } } \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java new file mode 100644 index 0000000..5eda509 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java @@ -0,0 +1,67 @@ +package com.ryu.studyhelper.infrastructure.discord; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +/** + * Discord Embed 메시지 구조 + * + * 사용법: 용도에 맞는 팩토리 메서드를 사용 + * - batchResult() : 배치 작업 결과 (스케줄러) + * - error() : 예외 발생 (스케줄러 실패) + * - event() : 비즈니스 이벤트 (가입, 핸들등록 등) + */ +public record DiscordMessage(List embeds) { + + record Embed(String title, String description, int color, List fields, String timestamp) {} + + record Field(String name, String value, boolean inline) {} + + private static final int COLOR_SUCCESS = 0x2ECC71; + private static final int COLOR_ERROR = 0xE74C3C; + private static final int COLOR_INFO = 0x3498DB; + + /** + * 배치 작업 결과 + * 실패가 있으면 빨간색, 없으면 초록색 + */ + public static DiscordMessage batchResult(String title, int totalCount, int successCount, int failCount, long elapsedMs) { + int color = failCount > 0 ? COLOR_ERROR : COLOR_SUCCESS; + return create(title, color, List.of( + new Field("대상", totalCount + "건", true), + new Field("성공", successCount + "건", true), + new Field("실패", failCount + "건", true), + new Field("소요시간", elapsedMs + "ms", true) + )); + } + + /** + * 예외 발생 (빨간색) + */ + public static DiscordMessage error(String title, Exception e, long elapsedMs) { + return create(title, COLOR_ERROR, List.of( + new Field("예외", e.getClass().getSimpleName(), true), + new Field("메시지", e.getMessage() != null ? e.getMessage() : "(없음)", false), + new Field("소요시간", elapsedMs + "ms", true) + )); + } + + /** + * 비즈니스 이벤트 (파란색) + * keyValues는 이름-값 쌍: "회원 ID", "1", "이메일", "test@..." + */ + public static DiscordMessage event(String title, String... keyValues) { + List fields = new ArrayList<>(); + for (int i = 0; i + 1 < keyValues.length; i += 2) { + fields.add(new Field(keyValues[i], keyValues[i + 1], true)); + } + return create(title, COLOR_INFO, fields); + } + + private static DiscordMessage create(String title, int color, List fields) { + return new DiscordMessage(List.of( + new Embed(title, null, color, fields, Instant.now().toString()) + )); + } +} \ No newline at end of file diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java new file mode 100644 index 0000000..7d6c011 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java @@ -0,0 +1,12 @@ +package com.ryu.studyhelper.infrastructure.discord; + +/** + * Discord Webhook 알림 인터페이스 + * 채널별로 분리된 메서드 제공 (스케줄러, 이벤트, 에러) + */ +public interface DiscordNotifier { + + void sendScheduler(DiscordMessage message); + + void sendEvent(DiscordMessage message); +} diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordProperties.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordProperties.java new file mode 100644 index 0000000..db5ffff --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordProperties.java @@ -0,0 +1,13 @@ +package com.ryu.studyhelper.infrastructure.discord; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Discord Webhook URL 설정 + * 채널별 Webhook URL 바인딩 + */ +@ConfigurationProperties(prefix = "discord.webhooks") +public record DiscordProperties( + String scheduler, + String event +) {} diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java new file mode 100644 index 0000000..2f21d6f --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java @@ -0,0 +1,54 @@ +package com.ryu.studyhelper.infrastructure.discord; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +/** + * Discord Webhook 알림 구현체 (prod 전용) + * RestClient로 Webhook POST 요청 + */ +@Component +@Profile("prod") +@EnableConfigurationProperties(DiscordProperties.class) +@Slf4j +public class DiscordWebhookNotifier implements DiscordNotifier { + + private final DiscordProperties properties; + private final RestClient restClient; + + public DiscordWebhookNotifier(DiscordProperties properties) { + this.properties = properties; + this.restClient = RestClient.create(); + } + + @Override + public void sendScheduler(DiscordMessage message) { + send(properties.scheduler(), message); + } + + @Override + public void sendEvent(DiscordMessage message) { + send(properties.event(), message); + } + + private void send(String webhookUrl, DiscordMessage message) { + if (webhookUrl == null || webhookUrl.isBlank()) { + log.debug("Discord webhook URL이 설정되지 않아 알림을 건너뜁니다"); + return; + } + try { + restClient.post() + .uri(webhookUrl) + .header("Content-Type", "application/json") + .body(message) + .retrieve() + .toBodilessEntity(); + } catch (Exception e) { + log.warn("Discord 알림 전송 실패: {}", e.getMessage()); + } + } +} diff --git a/src/main/java/com/ryu/studyhelper/member/MemberService.java b/src/main/java/com/ryu/studyhelper/member/MemberService.java index 24627a8..c6d474d 100644 --- a/src/main/java/com/ryu/studyhelper/member/MemberService.java +++ b/src/main/java/com/ryu/studyhelper/member/MemberService.java @@ -3,6 +3,8 @@ import com.ryu.studyhelper.common.enums.CustomResponseStatus; import com.ryu.studyhelper.common.exception.CustomException; import com.ryu.studyhelper.config.security.jwt.JwtUtil; +import com.ryu.studyhelper.infrastructure.discord.DiscordMessage; +import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier; import com.ryu.studyhelper.infrastructure.solvedac.SolvedAcClient; import com.ryu.studyhelper.infrastructure.mail.sender.MailSender; import com.ryu.studyhelper.member.mail.EmailChangeMailBuilder; @@ -35,6 +37,7 @@ public class MemberService { private final JwtUtil jwtUtil; private final MailSender mailSender; private final EmailChangeMailBuilder emailChangeMailBuilder; + private final DiscordNotifier discordNotifier; private final Clock clock; @Value("${FRONTEND_URL:http://localhost:5173}") @@ -80,6 +83,11 @@ public Member verifySolvedAcHandle(Long memberId, String handle) { Member member = getById(memberId); member.changeHandle(handle); + discordNotifier.sendEvent(DiscordMessage.event("핸들 등록", + "회원 ID", String.valueOf(memberId), + "핸들", handle + )); + return member; } diff --git a/src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java b/src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java new file mode 100644 index 0000000..d79adbb --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java @@ -0,0 +1,6 @@ +package com.ryu.studyhelper.recommendation.dto.internal; + +/** + * 배치 작업 결과 + */ +public record BatchResult(int totalCount, int successCount, int failCount) {} diff --git a/src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java b/src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java index 2342dbc..414b9f6 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java @@ -1,5 +1,8 @@ package com.ryu.studyhelper.recommendation.scheduler; +import com.ryu.studyhelper.infrastructure.discord.DiscordMessage; +import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier; +import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; import com.ryu.studyhelper.recommendation.service.RecommendationEmailService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,26 +19,38 @@ public class EmailSendScheduler { private final RecommendationEmailService recommendationEmailService; + private final DiscordNotifier discordNotifier; - /** - * 매일 오전 9시에 이메일 발송 - * Cron 표현식: "0 0 9 * * *" = 매일 9시 0분 0초 - */ @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Seoul") public void sendPendingEmails() { log.info("=== 이메일 발송 배치 작업 시작 ==="); long startTime = System.currentTimeMillis(); + BatchResult result = null; + Exception failure = null; try { - recommendationEmailService.sendAll(); + result = recommendationEmailService.sendAll(); + log.info("=== 이메일 발송 배치 작업 완료 === (소요시간: {}ms)", System.currentTimeMillis() - startTime); + } catch (Exception e) { + failure = e; + log.error("=== 이메일 발송 배치 작업 실패 === (소요시간: {}ms)", System.currentTimeMillis() - startTime, e); + } - long endTime = System.currentTimeMillis(); - log.info("=== 이메일 발송 배치 작업 완료 === (소요시간: {}ms)", endTime - startTime); + notifyDiscord(result, failure, System.currentTimeMillis() - startTime); + } + private void notifyDiscord(BatchResult result, Exception failure, long elapsed) { + try { + if (failure != null) { + discordNotifier.sendScheduler(DiscordMessage.error("이메일 발송 배치 실패", failure, elapsed)); + } else { + String title = result.failCount() > 0 ? "이메일 발송 배치 실패" : "이메일 발송 배치 완료"; + discordNotifier.sendScheduler(DiscordMessage.batchResult( + title, result.totalCount(), result.successCount(), result.failCount(), elapsed)); + } } catch (Exception e) { - long endTime = System.currentTimeMillis(); - log.error("=== 이메일 발송 배치 작업 실패 === (소요시간: {}ms)", endTime - startTime, e); + log.warn("Discord 알림 전송 실패", e); } } } diff --git a/src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java b/src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java index b2d70bb..51b3ef9 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java @@ -1,5 +1,8 @@ package com.ryu.studyhelper.recommendation.scheduler; +import com.ryu.studyhelper.infrastructure.discord.DiscordMessage; +import com.ryu.studyhelper.infrastructure.discord.DiscordNotifier; +import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; import com.ryu.studyhelper.recommendation.service.ScheduledRecommendationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -16,26 +19,38 @@ public class ProblemRecommendationScheduler { private final ScheduledRecommendationService scheduledRecommendationService; + private final DiscordNotifier discordNotifier; - /** - * 매일 오전 6시에 문제 추천 준비 - * Cron 표현식: "0 0 6 * * *" = 매일 6시 0분 0초 - */ @Scheduled(cron = "0 0 6 * * *", zone = "Asia/Seoul") public void prepareDailyRecommendations() { log.info("=== 문제 추천 배치 작업 시작 ==="); long startTime = System.currentTimeMillis(); + BatchResult result = null; + Exception failure = null; try { - scheduledRecommendationService.prepareDailyRecommendations(); + result = scheduledRecommendationService.prepareDailyRecommendations(); + log.info("=== 문제 추천 배치 작업 완료 === (소요시간: {}ms)", System.currentTimeMillis() - startTime); + } catch (Exception e) { + failure = e; + log.error("=== 문제 추천 배치 작업 실패 === (소요시간: {}ms)", System.currentTimeMillis() - startTime, e); + } - long endTime = System.currentTimeMillis(); - log.info("=== 문제 추천 배치 작업 완료 === (소요시간: {}ms)", endTime - startTime); + notifyDiscord(result, failure, System.currentTimeMillis() - startTime); + } + private void notifyDiscord(BatchResult result, Exception failure, long elapsed) { + try { + if (failure != null) { + discordNotifier.sendScheduler(DiscordMessage.error("문제 추천 배치 실패", failure, elapsed)); + } else { + String title = result.failCount() > 0 ? "문제 추천 배치 실패" : "문제 추천 배치 완료"; + discordNotifier.sendScheduler(DiscordMessage.batchResult( + title, result.totalCount(), result.successCount(), result.failCount(), elapsed)); + } } catch (Exception e) { - long endTime = System.currentTimeMillis(); - log.error("=== 문제 추천 배치 작업 실패 === (소요시간: {}ms)", endTime - startTime, e); + log.warn("Discord 알림 전송 실패", e); } } } diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java index 601fbab..e5a03d4 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java @@ -1,5 +1,6 @@ package com.ryu.studyhelper.recommendation.service; + import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; import com.ryu.studyhelper.infrastructure.mail.sender.MailSender; import com.ryu.studyhelper.recommendation.domain.member.EmailSendStatus; import com.ryu.studyhelper.recommendation.domain.member.MemberRecommendation; @@ -33,7 +34,7 @@ public class RecommendationEmailService { * 배치: PENDING 상태의 추천들에 대해 이메일 발송 * 미션 사이클 기준(06:00~06:00)으로 조회 */ - public void sendAll() { + public BatchResult sendAll() { LocalDateTime now = LocalDateTime.now(clock); LocalDateTime missionCycleStart = MissionCyclePolicy.getMissionCycleStart(clock); log.info("이메일 발송 배치 시작: {} (미션 사이클: {} 06:00 ~)", now.toLocalDate(), missionCycleStart.toLocalDate()); @@ -54,6 +55,7 @@ public void sendAll() { log.info("이메일 발송 배치 완료 - 대상: {}개, 성공: {}개, 실패: {}개", pendingRecommendations.size(), successCount, failCount); + return new BatchResult(pendingRecommendations.size(), successCount, failCount); } /** diff --git a/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java b/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java index c15c4ec..3eb613a 100644 --- a/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java +++ b/src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java @@ -1,5 +1,6 @@ package com.ryu.studyhelper.recommendation.service; +import com.ryu.studyhelper.recommendation.dto.internal.BatchResult; import com.ryu.studyhelper.recommendation.domain.RecommendationType; import com.ryu.studyhelper.recommendation.repository.RecommendationRepository; import com.ryu.studyhelper.team.domain.Team; @@ -34,7 +35,7 @@ public class ScheduledRecommendationService { * 문제 추천만 수행 (이메일 발송 X) * 미션 사이클 기준(06:00~06:00)으로 중복 체크 */ - public void prepareDailyRecommendations() { + public BatchResult prepareDailyRecommendations() { LocalDateTime now = LocalDateTime.now(clock); LocalDateTime missionCycleStart = MissionCyclePolicy.getMissionCycleStart(clock); log.info("문제 추천 준비 시작: {} (미션 사이클: {} 06:00 ~)", now.toLocalDate(), missionCycleStart.toLocalDate()); @@ -64,6 +65,7 @@ public void prepareDailyRecommendations() { log.info("문제 추천 배치 완료 - 대상: {}개, 성공: {}개, 실패: {}개", activeTeams.size(), successCount, failCount); + return new BatchResult(activeTeams.size(), successCount, failCount); } private List getActiveTeams(LocalDate date) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3e85ea5..69283b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,6 +38,12 @@ aws: from-email: ${AWS_SES_FROM_EMAIL:noreply@example.com} configuration-set: ${AWS_SES_CONFIGURATION_SET:} +# Discord Webhook 설정 +discord: + webhooks: + scheduler: ${DISCORD_WEBHOOK_SCHEDULER:} + event: ${DISCORD_WEBHOOK_EVENT:} + # JWT 공통 설정 jwt: secret: ${JWT_SECRET} From 3f3145ee622a24892122aab83d25432f225669e1 Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 02:23:11 +0900 Subject: [PATCH 2/3] =?UTF-8?q?discord:=20FakeDiscordNotifier=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=E2=80=94=20test/local=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=ED=95=84=20=EB=B9=88=20=EB=93=B1=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../discord/FakeDiscordNotifier.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java new file mode 100644 index 0000000..3f02422 --- /dev/null +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java @@ -0,0 +1,32 @@ +package com.ryu.studyhelper.infrastructure.discord; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * 로컬/테스트용 Discord 알림 구현체 + * 실제 Webhook 호출 대신 로그 출력 + */ +@Component +@Profile("!prod") +@Slf4j +public class FakeDiscordNotifier implements DiscordNotifier { + + @Override + public void sendScheduler(DiscordMessage message) { + log.info("[FAKE DISCORD] 스케줄러 알림: {}", extractTitle(message)); + } + + @Override + public void sendEvent(DiscordMessage message) { + log.info("[FAKE DISCORD] 이벤트 알림: {}", extractTitle(message)); + } + + private String extractTitle(DiscordMessage message) { + if (message.embeds() == null || message.embeds().isEmpty()) { + return "(제목 없음)"; + } + return message.embeds().get(0).title(); + } +} From 12940241b0ed3fc4abf985bd8c33f518af26bf2f Mon Sep 17 00:00:00 2001 From: ryuwldnjs Date: Wed, 18 Feb 2026 02:28:26 +0900 Subject: [PATCH 3/3] =?UTF-8?q?discord:=20Javadoc=20=EC=88=98=EC=A0=95,=20?= =?UTF-8?q?RestClient=20=ED=83=80=EC=9E=84=EC=95=84=EC=9B=83=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95,=20=EC=95=8C=EB=A6=BC=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=ED=8A=B8=EB=9E=9C=EC=9E=AD=EC=85=98=20=EB=A1=A4=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../studyhelper/auth/GoogleOAuth2UserService.java | 12 ++++++++---- .../infrastructure/discord/DiscordNotifier.java | 2 +- .../discord/DiscordWebhookNotifier.java | 11 +++++++++-- .../com/ryu/studyhelper/member/MemberService.java | 12 ++++++++---- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java b/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java index c454bb0..6d37887 100644 --- a/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java +++ b/src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java @@ -58,10 +58,14 @@ private Member saveOrUpdateMember(OAuthInfo oAuthInfo) { Member savedMember = memberRepository.save(newMember); log.info("신규 사용자 생성 완료: {}", savedMember.getEmail()); - discordNotifier.sendEvent(DiscordMessage.event("신규 회원 가입", - "회원 ID", String.valueOf(savedMember.getId()), - "이메일", maskEmail(savedMember.getEmail()) - )); + try { + discordNotifier.sendEvent(DiscordMessage.event("신규 회원 가입", + "회원 ID", String.valueOf(savedMember.getId()), + "이메일", maskEmail(savedMember.getEmail()) + )); + } catch (Exception e) { + log.warn("Discord 알림 전송 실패", e); + } return savedMember; } diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java index 7d6c011..2208771 100644 --- a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java @@ -2,7 +2,7 @@ /** * Discord Webhook 알림 인터페이스 - * 채널별로 분리된 메서드 제공 (스케줄러, 이벤트, 에러) + * 채널별로 분리된 메서드 제공 (스케줄러, 이벤트) */ public interface DiscordNotifier { diff --git a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java index 2f21d6f..ff105a5 100644 --- a/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java +++ b/src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java @@ -2,11 +2,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; +import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestClient; +import java.time.Duration; + /** * Discord Webhook 알림 구현체 (prod 전용) * RestClient로 Webhook POST 요청 @@ -22,7 +24,12 @@ public class DiscordWebhookNotifier implements DiscordNotifier { public DiscordWebhookNotifier(DiscordProperties properties) { this.properties = properties; - this.restClient = RestClient.create(); + SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); + factory.setConnectTimeout(Duration.ofSeconds(3)); + factory.setReadTimeout(Duration.ofSeconds(5)); + this.restClient = RestClient.builder() + .requestFactory(factory) + .build(); } @Override diff --git a/src/main/java/com/ryu/studyhelper/member/MemberService.java b/src/main/java/com/ryu/studyhelper/member/MemberService.java index c6d474d..3979e7c 100644 --- a/src/main/java/com/ryu/studyhelper/member/MemberService.java +++ b/src/main/java/com/ryu/studyhelper/member/MemberService.java @@ -83,10 +83,14 @@ public Member verifySolvedAcHandle(Long memberId, String handle) { Member member = getById(memberId); member.changeHandle(handle); - discordNotifier.sendEvent(DiscordMessage.event("핸들 등록", - "회원 ID", String.valueOf(memberId), - "핸들", handle - )); + try { + discordNotifier.sendEvent(DiscordMessage.event("핸들 등록", + "회원 ID", String.valueOf(memberId), + "핸들", handle + )); + } catch (Exception e) { + log.warn("Discord 알림 전송 실패", e); + } return member; }