Discord Webhook 알림 연동 (#161)#164
Conversation
- infrastructure/discord/ 패키지 신설 (DiscordNotifier 인터페이스 + prod 구현체) - 스케줄러 배치 결과 Discord 알림 (성공/실패 구분) - 회원가입, 핸들등록 이벤트 알림 - BatchResult record 도입으로 서비스 반환 타입 변경
|
Note
|
| Cohort / File(s) | Summary |
|---|---|
Discord 인프라 추가 src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java, src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java, src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordProperties.java, src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java, src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java |
Discord 메시지 포맷 레코드(DiscordMessage), 알림 추상화 인터페이스(DiscordNotifier), 설정 바인딩 레코드(DiscordProperties), 프로덕션 웹훅 전송 구현(DiscordWebhookNotifier) 및 프로파일별 페이크 구현(FakeDiscordNotifier) 추가. |
회원 관련 통합 src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java, src/main/java/com/ryu/studyhelper/member/MemberService.java |
신규 회원 생성 및 핸들 검증 시 Discord 이벤트 전송 로직 추가(이메일 마스킹 포함). DiscordNotifier 의존성 주입. |
스케줄러·배치 알림 통합 src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java, src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java |
스케줄러 실행 결과를 BatchResult로 캡처하고 성공/실패에 따라 Discord에 알림 전송하도록 흐름을 재구성. 공통 notifyDiscord 헬퍼 추가. |
배치 결과 타입 변경 src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java, src/main/java/com/ryu/studyhelper/recommendation/service/RecommendationEmailService.java, src/main/java/com/ryu/studyhelper/recommendation/service/ScheduledRecommendationService.java |
BatchResult 레코드 추가 및 sendAll() / prepareDailyRecommendations() 등 반환 타입을 void → BatchResult로 변경하여 결과 집계 반환. |
설정 추가 src/main/resources/application.yml |
discord.webhooks 설정 항목(scheduler, event)을 환경변수 바인딩으로 추가. |
Sequence Diagram
sequenceDiagram
actor User
participant GoogleOAuth2UserService
participant DiscordNotifier as DiscordNotifier
participant DiscordWebhookNotifier
participant DiscordAPI as Discord API
User->>GoogleOAuth2UserService: OAuth2 로그인 (신규 사용자)
activate GoogleOAuth2UserService
GoogleOAuth2UserService->>GoogleOAuth2UserService: 새 멤버 생성 및 이메일 마스킹
GoogleOAuth2UserService->>DiscordNotifier: sendEvent(DiscordMessage)
deactivate GoogleOAuth2UserService
activate DiscordWebhookNotifier
DiscordWebhookNotifier->>DiscordWebhookNotifier: webhook URL 검증
DiscordWebhookNotifier->>DiscordAPI: POST /webhooks (application/json)
DiscordAPI-->>DiscordWebhookNotifier: 2xx/204 응답
deactivate DiscordWebhookNotifier
sequenceDiagram
participant Scheduler
participant ScheduledRecommendationService
participant RecommendationEmailService
participant DiscordNotifier as DiscordNotifier
participant DiscordWebhookNotifier
participant DiscordAPI as Discord API
Scheduler->>ScheduledRecommendationService: prepareDailyRecommendations()
activate ScheduledRecommendationService
ScheduledRecommendationService->>RecommendationEmailService: sendAll()
activate RecommendationEmailService
RecommendationEmailService-->>ScheduledRecommendationService: BatchResult
deactivate RecommendationEmailService
ScheduledRecommendationService->>ScheduledRecommendationService: 처리 결과 집계 (BatchResult / Exception)
ScheduledRecommendationService-->>DiscordNotifier: sendScheduler(DiscordMessage) (성공 또는 에러 기반)
deactivate ScheduledRecommendationService
activate DiscordWebhookNotifier
DiscordWebhookNotifier->>DiscordAPI: POST /webhooks (scheduler)
DiscordAPI-->>DiscordWebhookNotifier: 2xx/204 응답
deactivate DiscordWebhookNotifier
Estimated code review effort
🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related PRs
- recommendation: split God Class into 5 services + remove legacy files #153: 추천 스케줄러 및 배치 처리 흐름(EmailSendScheduler, ProblemRecommendationScheduler, BatchResult 관련) 변경과 코드 레벨로 연관됨.
- 142 refactor solvedac client/service 역할 분리 #143:
MemberService관련 의존성 및 회원 흐름 변경과 직접적인 코드 연관성 있음.
Poem
🐰 웅성웅성 디스코드 별빛 아래
새 멤버 오면 소식 가볍게 전하고
배치가 끝나면 당근 춤추며 알리네
로그 대신 종소리로 운영을 깨우고
토끼가 기쁘게 메시지 전해요 ✨
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
✅ Passed checks (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | PR 제목 'Discord Webhook 알림 연동 (#161)'은 변경 사항의 핵심인 Discord Webhook 알림 통합을 명확하게 요약합니다. |
| Linked Issues check | ✅ Passed | PR 변경 사항이 이슈 #161의 주요 요구사항을 충족합니다: Discord Webhook 인터페이스 구현, 배치 결과 알림(성공/실패 색상 구분), 비즈니스 이벤트 알림, 프로필별 환경 분리, 채널 분리(scheduler/event). |
| Out of Scope Changes check | ✅ Passed | 모든 변경 사항이 이슈 #161 범위 내입니다. 추가된 파일들은 모두 Discord 알림 기능 구현과 관련된 것이며, BatchResult 반환 타입 변경도 배치 결과 추적을 위한 필수 변경입니다. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
161-feature-discord-webhook-알림-연동
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands and usage tips.
🧪 테스트 결과145 tests 143 ✅ 2s ⏱️ Results for commit 1294024. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java (1)
70-76:maskEmail에서@가 없는 입력에 대한 방어 처리 부재.
email.indexOf('@')가-1을 반환하면email.substring(-1)에서StringIndexOutOfBoundsException이 발생합니다. Google OAuth에서 유효한 이메일이 오겠지만, 방어적 코딩을 권장합니다.🛡️ 방어 코드 제안
private String maskEmail(String email) { + if (email == null || !email.contains("@")) { + return "***"; + } int atIndex = email.indexOf('@'); if (atIndex <= 3) { return email.charAt(0) + "***" + email.substring(atIndex); } return email.substring(0, 3) + "***" + email.substring(atIndex); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java` around lines 70 - 76, maskEmail currently assumes an '@' exists and calls substring(atIndex) which throws StringIndexOutOfBoundsException when indexOf('@') returns -1; update maskEmail to defensively handle null/empty inputs and the case where atIndex == -1 by first validating email (null/empty) and checking if atIndex < 0, then return a safe fallback (e.g., the original email or a partially masked version like first 3 chars + "***") instead of calling substring(-1); keep the existing behavior for normal emails (use email.charAt(0)/substring(0,3) and substring(atIndex) when atIndex >= 0).src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java (1)
1-6: 깔끔한 record 정의입니다.배치 결과를 캡슐화하는 간결한 구현입니다.
참고:
ScheduledRecommendationService에서 이미 추천이 존재하는 팀은continue로 스킵되므로totalCount != successCount + failCount인 경우가 발생할 수 있습니다. 이는 의도된 동작이라면 문제없지만,skippedCount필드를 추가하면 알림 메시지에서 더 정확한 정보를 전달할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java` around lines 1 - 6, BatchResult should include a skippedCount to represent teams that were intentionally skipped so totals reconcile; update the record definition (BatchResult) to add an int skippedCount field and ensure all places that construct it (notably ScheduledRecommendationService where you currently continue for already-existing recommendations) pass skippedCount (or compute skippedCount = totalCount - successCount - failCount when appropriate) and update any notification/summary logic that reads total/success/fail to also include skippedCount in messages.src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java (1)
5-5: 사용되지 않는@Primaryimport가 있습니다.🧹 미사용 import 제거
-import org.springframework.context.annotation.Primary;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java` at line 5, Remove the unused import org.springframework.context.annotation.Primary from the DiscordWebhookNotifier class (import line shown in the diff) — delete that import and run import-organize/formatting so there are no unused imports remaining in src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java.src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java (1)
34-40:ProblemRecommendationScheduler와 동일하게 소요시간 계산 불일치 문제가 있습니다.Line 34, 37의 로그 소요시간과 Line 40의
notifyDiscord전달 소요시간이 서로 다른System.currentTimeMillis()호출로 계산됩니다.ProblemRecommendationScheduler에 제안한 것과 동일하게 소요시간을 한 번만 계산하여 일관되게 사용하세요.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java` around lines 34 - 40, Compute the duration once after the try/catch using the existing startTime (e.g., long duration = System.currentTimeMillis() - startTime) and reuse that single duration variable in the log calls and in the notifyDiscord(result, failure, duration) call so all messages use the same elapsed time; update EmailSendScheduler to replace the multiple System.currentTimeMillis() calls with this single duration variable.src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java (2)
34-40: 소요시간이 두 번 계산되어 Discord 알림에 전달되는 값이 로그 값과 다릅니다.Line 34, 37에서 로그에 기록하는 소요시간과 Line 40에서
notifyDiscord에 전달하는 소요시간이 별도의System.currentTimeMillis()호출로 계산됩니다. Discord 알림에는 로그보다 약간 더 큰 값이 전달됩니다.소요시간을 한 번만 계산하여 일관되게 사용하세요.
♻️ 소요시간 일관성 개선
try { result = scheduledRecommendationService.prepareDailyRecommendations(); - log.info("=== 문제 추천 배치 작업 완료 === (소요시간: {}ms)", System.currentTimeMillis() - startTime); } catch (Exception e) { failure = e; - log.error("=== 문제 추천 배치 작업 실패 === (소요시간: {}ms)", System.currentTimeMillis() - startTime, e); } - notifyDiscord(result, failure, System.currentTimeMillis() - startTime); + long elapsed = System.currentTimeMillis() - startTime; + + if (failure != null) { + log.error("=== 문제 추천 배치 작업 실패 === (소요시간: {}ms)", elapsed, failure); + } else { + log.info("=== 문제 추천 배치 작업 완료 === (소요시간: {}ms)", elapsed); + } + + notifyDiscord(result, failure, elapsed);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java` around lines 34 - 40, Compute the elapsed time once and reuse it instead of calling System.currentTimeMillis() twice: after the try/catch calculate long elapsed = System.currentTimeMillis() - startTime and replace the two inline System.currentTimeMillis() - startTime usages in log.info/log.error and the notifyDiscord(result, failure, ...) call with that elapsed variable so the logged message (in the log.info/log.error blocks) and the notifyDiscord(...) invocation use the identical duration; update references around notifyDiscord and the existing startTime variable accordingly.
43-55:notifyDiscord메서드가EmailSendScheduler와 완전히 동일하게 중복됩니다.두 스케줄러의
notifyDiscord로직이 제목 문자열만 다르고 구조가 동일합니다. 공통 유틸리티나 헬퍼로 추출하면 유지보수성이 향상됩니다. 비동기 이벤트 처리로 분리 예정이라면 그때 함께 정리해도 무방합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java` around lines 43 - 55, notifyDiscord in ProblemRecommendationScheduler duplicates EmailSendScheduler; extract the shared logic into a single helper (e.g., SchedulerNotifier.notifyDiscord or NotificationHelper.notifyDiscord) that accepts the Discord notifier (discordNotifier), BatchResult result, Exception failure, long elapsed and two title strings (successTitle and failureTitle) or a single failure/success title decision inside the helper; update ProblemRecommendationScheduler.notifyDiscord and EmailSendScheduler.notifyDiscord to call that helper, preserving use of DiscordMessage.error and DiscordMessage.batchResult and the try/catch with log.warn for notification exceptions.src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java (1)
54-60:event()메서드에서keyValues길이가 홀수일 경우 마지막 값이 무시됩니다.
keyValues가 이름-값 쌍으로 전달되어야 하지만, 호출자가 실수로 홀수 개를 전달하면 마지막 항목이 조용히 무시됩니다. 방어적으로 검증을 추가하면 디버깅이 쉬워집니다.🛡️ 홀수 길이 방어 코드 제안
public static DiscordMessage event(String title, String... keyValues) { + if (keyValues.length % 2 != 0) { + throw new IllegalArgumentException("keyValues는 이름-값 쌍이어야 합니다 (짝수 개)"); + } List<Field> fields = new ArrayList<>(); for (int i = 0; i + 1 < keyValues.length; i += 2) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java` around lines 54 - 60, The event(String title, String... keyValues) method silently drops a trailing value when keyValues has odd length; add defensive validation at the start of event(...) to check keyValues != null and that keyValues.length is even, and if not throw an IllegalArgumentException (or return a clear error) indicating that keyValues must be provided as name/value pairs; keep the rest of the method (building List<Field> and calling create(title, COLOR_INFO, fields)) unchanged so callers like Field and create are used as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java`:
- Around line 3-6: Javadoc for DiscordNotifier incorrectly mentions an "에러"
channel that doesn't exist; update the class/interface comment in
DiscordNotifier to remove "에러" (or replace with correct channels) so it only
lists the actual methods provided (e.g., 스케줄러, 이벤트), and ensure the description
clearly states that error reporting is handled by Sentry rather than a sendError
method.
In
`@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java`:
- Around line 23-26: The RestClient in the DiscordWebhookNotifier constructor is
created without timeouts (restClient = RestClient.create()), which can block the
scheduler; replace this with RestClient.builder() and supply a configured
requestFactory that sets short connection and read timeouts (eg. connect ~2s,
read ~5s) before calling build(); update the DiscordWebhookNotifier constructor
to use the builder approach so all outgoing Discord calls use the
timeout-configured RestClient.
In `@src/main/java/com/ryu/studyhelper/member/MemberService.java`:
- Around line 86-89: verifySolvedAcHandle is a `@Transactional` method and calling
discordNotifier.sendEvent(...) can throw and roll back
member.changeHandle(handle); to fix, prevent notifier exceptions from escaping
the transaction by wrapping the discordNotifier.sendEvent call in a try-catch
that logs failures (without rethrowing) or modify the notifier implementation to
swallow/handle its own exceptions; locate discordNotifier.sendEvent(...) in
MemberService (and the call site where member.changeHandle(handle) is performed)
and ensure sendEvent failures are caught and handled so the transaction commits
even if the Discord notification fails.
---
Nitpick comments:
In `@src/main/java/com/ryu/studyhelper/auth/GoogleOAuth2UserService.java`:
- Around line 70-76: maskEmail currently assumes an '@' exists and calls
substring(atIndex) which throws StringIndexOutOfBoundsException when
indexOf('@') returns -1; update maskEmail to defensively handle null/empty
inputs and the case where atIndex == -1 by first validating email (null/empty)
and checking if atIndex < 0, then return a safe fallback (e.g., the original
email or a partially masked version like first 3 chars + "***") instead of
calling substring(-1); keep the existing behavior for normal emails (use
email.charAt(0)/substring(0,3) and substring(atIndex) when atIndex >= 0).
In
`@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordMessage.java`:
- Around line 54-60: The event(String title, String... keyValues) method
silently drops a trailing value when keyValues has odd length; add defensive
validation at the start of event(...) to check keyValues != null and that
keyValues.length is even, and if not throw an IllegalArgumentException (or
return a clear error) indicating that keyValues must be provided as name/value
pairs; keep the rest of the method (building List<Field> and calling
create(title, COLOR_INFO, fields)) unchanged so callers like Field and create
are used as before.
In
`@src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java`:
- Line 5: Remove the unused import
org.springframework.context.annotation.Primary from the DiscordWebhookNotifier
class (import line shown in the diff) — delete that import and run
import-organize/formatting so there are no unused imports remaining in
src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java.
In
`@src/main/java/com/ryu/studyhelper/recommendation/dto/internal/BatchResult.java`:
- Around line 1-6: BatchResult should include a skippedCount to represent teams
that were intentionally skipped so totals reconcile; update the record
definition (BatchResult) to add an int skippedCount field and ensure all places
that construct it (notably ScheduledRecommendationService where you currently
continue for already-existing recommendations) pass skippedCount (or compute
skippedCount = totalCount - successCount - failCount when appropriate) and
update any notification/summary logic that reads total/success/fail to also
include skippedCount in messages.
In
`@src/main/java/com/ryu/studyhelper/recommendation/scheduler/EmailSendScheduler.java`:
- Around line 34-40: Compute the duration once after the try/catch using the
existing startTime (e.g., long duration = System.currentTimeMillis() -
startTime) and reuse that single duration variable in the log calls and in the
notifyDiscord(result, failure, duration) call so all messages use the same
elapsed time; update EmailSendScheduler to replace the multiple
System.currentTimeMillis() calls with this single duration variable.
In
`@src/main/java/com/ryu/studyhelper/recommendation/scheduler/ProblemRecommendationScheduler.java`:
- Around line 34-40: Compute the elapsed time once and reuse it instead of
calling System.currentTimeMillis() twice: after the try/catch calculate long
elapsed = System.currentTimeMillis() - startTime and replace the two inline
System.currentTimeMillis() - startTime usages in log.info/log.error and the
notifyDiscord(result, failure, ...) call with that elapsed variable so the
logged message (in the log.info/log.error blocks) and the notifyDiscord(...)
invocation use the identical duration; update references around notifyDiscord
and the existing startTime variable accordingly.
- Around line 43-55: notifyDiscord in ProblemRecommendationScheduler duplicates
EmailSendScheduler; extract the shared logic into a single helper (e.g.,
SchedulerNotifier.notifyDiscord or NotificationHelper.notifyDiscord) that
accepts the Discord notifier (discordNotifier), BatchResult result, Exception
failure, long elapsed and two title strings (successTitle and failureTitle) or a
single failure/success title decision inside the helper; update
ProblemRecommendationScheduler.notifyDiscord and
EmailSendScheduler.notifyDiscord to call that helper, preserving use of
DiscordMessage.error and DiscordMessage.batchResult and the try/catch with
log.warn for notification exceptions.
src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordNotifier.java
Show resolved
Hide resolved
src/main/java/com/ryu/studyhelper/infrastructure/discord/DiscordWebhookNotifier.java
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java (1)
26-31:title()이null일 경우 로그에 "null"이 출력될 수 있습니다.
embeds리스트의 존재 여부는 확인하고 있지만, 첫 번째 embed의title()이null인 경우는 처리되지 않습니다. 로그 전용이라 실질적 영향은 없지만, 일관성을 위해 방어 처리를 고려해볼 수 있습니다.🔧 제안: null-safe title 추출
private String extractTitle(DiscordMessage message) { if (message.embeds() == null || message.embeds().isEmpty()) { return "(제목 없음)"; } - return message.embeds().get(0).title(); + String title = message.embeds().get(0).title(); + return title != null ? title : "(제목 없음)"; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java` around lines 26 - 31, The extractTitle method currently checks for missing embeds but not for a null first-embed title; update FakeDiscordNotifier.extractTitle to null-safely handle the first embed's title by checking message.embeds().get(0).title() for null and returning a fallback like "(제목 없음)" (or an empty string) when title() is null, ensuring logs never print "null" while keeping existing embed-existence checks intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In
`@src/main/java/com/ryu/studyhelper/infrastructure/discord/FakeDiscordNotifier.java`:
- Around line 26-31: The extractTitle method currently checks for missing embeds
but not for a null first-embed title; update FakeDiscordNotifier.extractTitle to
null-safely handle the first embed's title by checking
message.embeds().get(0).title() for null and returning a fallback like "(제목 없음)"
(or an empty string) when title() is null, ensuring logs never print "null"
while keeping existing embed-existence checks intact.
관련 이슈
변경 내용
변경 유형
테스트
스크린샷 (UI 변경 시)
참고사항
Summary by CodeRabbit
New Features
Chores