-
Notifications
You must be signed in to change notification settings - Fork 2
feat: 스터디 공지 디스코드 알림 기능 #1026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 스터디 공지 디스코드 알림 기능 #1026
Conversation
📝 WalkthroughWalkthrough이번 변경 사항은 스터디 공지를 디스코드 채널로 전송하는 기능을 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Entity as StudyAnnouncementV2
participant Event as Event System
participant Handler as StudyEventHandlerV2
participant Service as CommonDiscordService
participant Repo as StudyAnnouncementV2Repository
participant Discord as DiscordUtil
participant Channel as Discord Channel
Entity->>+Event: onPostPersist()로 StudyAnnouncementCreatedEvent 발행
Event->>+Handler: StudyAnnouncementCreatedEvent 전달
Handler->>+Service: handleStudyAnnouncementCreatedEvent(event) 호출
Service->>+Repo: studyAnnouncementId로 공지 조회
Repo-->>-Service: StudyAnnouncementV2 반환 (또는 예외 발생)
Service->>+Discord: sendStudyAnnouncementToChannel(채널ID, discordRoleId, studyName, 제목, 링크, 생성일시) 호출
Discord->>+Channel: TextChannel 검색 및 Embed 메시지 전송
Channel-->>-Discord: 전송 확인
Discord-->>-Service: 전송 완료 응답
Service-->>-Handler: 로그 기록 후 종료
Assessment against linked issues
Possibly related PRs
Suggested labels
Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)
73-87: Discord 채널로 스터디 공지사항을 보내는 메서드 구현이 잘 되었습니다.메서드의 구현이 전반적으로 깔끔하며 목적에 맞게 잘 작성되었습니다. 몇 가지 개선 사항을 제안합니다:
- JDA의 메시지 전송 실패 시 예외 처리가 없습니다.
.queue(success -> {}, failure -> {})형태로 콜백을 추가하여 실패 케이스를 처리하는 것을 고려해보세요.- 메시지 형식이 하드코딩되어 있습니다. 나중에 다국어 지원을 고려한다면 메시지 템플릿을 외부화하는 것이 좋습니다.
- 메시지 전송 결과를 로깅하는 코드를 추가하면 디버깅에 도움이 될 것입니다.
public void sendStudyAnnouncementToChannel( String channelId, String studyName, String title, String link, LocalDateTime createdAt) { TextChannel channel = Optional.ofNullable(jda.getTextChannelById(channelId)) .orElseThrow(() -> new CustomException(DISCORD_CHANNEL_NOT_FOUND)); MessageEmbed embed = new EmbedBuilder() .setTitle("📣 " + title, link) .setDescription(studyName + "에 새로운 공지사항이 올라왔어요!") .setTimestamp(createdAt) .build(); - channel.sendMessageEmbeds(embed).queue(); + channel.sendMessageEmbeds(embed).queue( + success -> log.info("스터디 공지사항이 성공적으로 전송되었습니다: {}", title), + error -> log.error("스터디 공지사항 전송 실패: {}, 에러: {}", title, error.getMessage()) + ); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java(3 hunks)src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java(3 hunks)src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java(2 hunks)src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementCreatedEvent.java(1 hunks)src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java(1 hunks)src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java(2 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java (1)
Learnt from: kckc0608
PR: GDSC-Hongik/gdsc-server#1012
File: src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java:0-0
Timestamp: 2025-03-28T07:52:58.277Z
Learning: 디스코드 역할 부여/제거는 핵심 로직이 아닌 부가적인 작업으로 간주하며, 데이터베이스 작업(출석, 과제 등의 기록 관리) 이후에 비동기적으로 처리하는 것이 좋다.
🔇 Additional comments (9)
src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementCreatedEvent.java (1)
1-3: 깔끔한 이벤트 클래스 구현!이벤트 클래스를 Java 레코드로 간결하게 구현한 점이 좋습니다. 이는 불변성을 보장하고 보일러플레이트 코드를 줄이는 좋은 패턴입니다.
src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java (1)
68-68: 디스코드 관련 예외 처리 확장 구현 적절함새로운 디스코드 채널 관련 오류 코드를 기존 디스코드 관련 오류 코드 섹션에 추가한 것이 적절합니다. 메시지도 명확하게 작성되었습니다.
src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java (3)
3-4: 스터디 공지 생성 이벤트 임포트 추가StudyAnnouncementCreatedEvent 클래스를 위한 import가 적절히 추가되었습니다.
13-13: JPA 라이프사이클 어노테이션 임포트 추가PostPersist 어노테이션 임포트가 적절히 추가되었습니다.
45-48: 엔티티 영속화 후 이벤트 발행 로직 추가공지사항 엔티티가 데이터베이스에 저장된 후 자동으로 이벤트를 발행하는 패턴이 적절히 구현되었습니다. 이는 이벤트 기반 아키텍처에 잘 부합합니다.
src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java (3)
3-4: 스터디 공지 생성 이벤트 임포트 추가StudyAnnouncementCreatedEvent 클래스에 대한 import가 적절히 추가되었습니다.
9-9: ApplicationModuleListener 어노테이션 임포트 추가Spring Modulith 이벤트 리스너 어노테이션 임포트가 적절히 추가되었습니다.
41-46:Details
✅ Verification successful
스터디 공지 생성 이벤트 핸들러 구현
스터디 공지사항이 생성되었을 때 디스코드로 알림을 전송하는 이벤트 핸들러가 잘 구현되었습니다. 로깅도 적절히 포함되었습니다.
다만, 기존 핸들러 메서드들은
@TransactionalEventListener를 사용하는 반면, 새 메서드는@ApplicationModuleListener를 사용하고 있습니다. 이 차이가 의도적인지 확인해보세요. 디스코드 알림 전송이 트랜잭션과 무관하게 비동기적으로 실행되어야 한다면 현재 구현이 적절할 수 있습니다.
🌐 Web query:
Spring Modulith ApplicationModuleListener vs TransactionalEventListener 차이점💡 Result:
Spring Modulith의
@ApplicationModuleListener와 Spring의@TransactionalEventListener는 모두 트랜잭션 이벤트 처리에 사용되지만 설계 목적과 동작 방식에서 차이가 있습니다. 모듈 간 통신과 트랜잭션 격리를 위해 특화된 기능을 제공합니다.
핵심 차이점 요약
기능 @ApplicationModuleListener@TransactionalEventListener트랜잭션 전파 REQUIRES_NEW로 새로운 트랜잭션 생성 [1][4]기존 트랜잭션에 종속적 (기본 동작) [5][8] 비동기 처리 @Async로 기본 비동기 실행 [7][9]명시적 @Async추가 필요모듈 간 통신 최적화 Spring Modulith 이벤트 레지스트리와 통합 [3][9] 표준 Spring 이벤트 메커니즘 사용 구성 단순화 복합 애노테이션(Syntactic Sugar) [1][4][7] 별도 트랜잭션/비동기 설정 필요 트랜잭션 롤백 영향도 원본 트랜잭션과 독립적 [1][4] 동일 트랜잭션에서 실행 시 영향 받음 [5][6]
상세 비교 분석
1. 트랜잭션 관리 방식
@ApplicationModuleListener
Propagation.REQUIRES_NEW로 새 트랜잭션 실행 [1][4]- 원본 비즈니스 트랜잭션과 완전히 분리되어 실패 시 서로 영향 없음 [7][9]
readOnlyTransaction옵션으로 읽기 전용 트랜잭션 설정 가능 [1][4]
@TransactionalEventListener
- 기본적으로 현재 트랜잭션의 단계(phase)에 종속적 [5][8]
AFTER_COMMIT설정 시 트랜잭션 커밋 후 실행되지만 동일 스레드에서 처리될 수 있음 [5][6]- 명시적
@Transactional(propagation=REQUIRES_NEW)추가 필요 [2][3]// ApplicationModuleListener 사용 예시 @ApplicationModuleListener public void handleEvent(OrderEvent event) { // 별도 트랜잭션에서 실행 } // 동등한 TransactionalEventListener 설정 @Async @Transactional(propagation = Propagation.REQUIRES_NEW) @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleEvent(OrderEvent event) { // 수동 구성 필요 }2. 모듈 간 통신 신뢰성
@ApplicationModuleListener
- 이벤트 발행 실패 시 재시도 가능한 이벤트 레지스트리와 통합 [9]
EventPublicationRegistry가 이벤트 전달 상태 추적 [3][9]
@TransactionalEventListener
- 이벤트 전달 실패 시 복구 메커니즘 없음 (개발자 구현 필요) [9]
- 트랜잭션 커밋 후 시스템 장애 발생 시 이벤트 유실 가능성 [2][5]
3. 사용 시나리오
@ApplicationModuleListener가 권장되는 경우
- 모노리틱 애플리케이션의 모듈 간 통신
- 이벤트 처리 실패 시 자동 재시도가 필요한 시스템
- 트랜잭션 경계를 엄격히 분리해야 하는 금융 거래 처리
@TransactionalEventListener적합한 경우
- 단일 모듈 내부의 트랜잭션 이벤트 처리
- 간단한 이벤트-리스너 패턴 구현
- 외부 시스템 통합이 필요 없는 내부 이벤트
4. 버전 호환성 주의사항
- Spring Modulith 1.1+에서는
@ApplicationModuleListener가 Deprecated 되었으나, 1.3.x에서 재도입된 것으로 보임 [1][4]- Spring Framework 6.1+에서는 리액티브 트랜잭션 지원 강화 [8]
결론
@ApplicationModuleListener는 Spring Modulith 아키텍처에서 모듈 간 통신을 위해 특화된 고급 추상화로, 복잡한 트랜잭션 설정을 자동화하고 이벤트 처리 신뢰성을 높입니다. 반면@TransactionalEventListener는 기본 Spring 환경에서 유연한 트랜잭션 이벤트 처리를 위해 사용됩니다. 모듈식 설계가 주요 요구사항일 때 전자를, 단순 트랜잭션 이벤트 핸들링에는 후자를 선택하는 것이 적절합니다.Citations:
- 1: https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/ApplicationModuleListener.html
- 2: https://www.adesso.de/en/news/blog/consistent-delivery-of-integration-events-thanks-to-the-use-of-an-event-store-and-spring-application-events-2.jsp
- 3: https://blog.igooo.org/127
- 4: https://docs.spring.io/spring-modulith/docs/current/api/org/springframework/modulith/events/ApplicationModuleListener.html
- 5: https://softice.dev/posts/spring_puzzler_transactional_event_listener/
- 6: https://inma.tistory.com/162
- 7: https://jaehoney.tistory.com/435
- 8: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/event/TransactionalEventListener.html
- 9: https://docs.spring.io/spring-modulith/reference/events.html
- 10: https://bell-sw.com/blog/how-to-build-a-modular-application-with-spring-modulith/
이벤트 처리 방식 차이에 대한 확인 필요
- 스터디 공지 생성 이벤트 핸들러는 로그 기록 및 디스코드 알림 전송 측면에서 잘 구현되었습니다.
- 다만, 기존 이벤트 핸들러들이
@TransactionalEventListener를 사용하던 반면, 이번 핸들러는@ApplicationModuleListener를 사용하고 있습니다.- 최근 문서를 참고하면,
@ApplicationModuleListener는 내부적으로REQUIRES_NEW트랜잭션을 생성하고 기본적으로 비동기 실행을 지원하여 트랜잭션 경계를 분리하는 반면,@TransactionalEventListener는 현재 트랜잭션 내에서 동작하므로 상이한 동작 방식임을 알 수 있습니다.- 디스코드 알림 전송이 트랜잭션과 독립적으로, 비동기적으로 실행되어야 한다면 해당 구현은 의도된 것으로 보이며, 이 차이가 설계상 명확하게 문서화되어 있거나 팀 내에서 공유되어 있는지 추가 확인 부탁드립니다.
src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java (1)
9-10: 의존성 주입 패턴이 일관적으로 적용되었습니다.StudyAnnouncementV2Repository를 필드로 추가하고 생성자 주입 방식을 사용한 것은 좋은 패턴입니다. 코드의 구조가 명확하고 테스트하기 쉬워집니다.
Also applies to: 25-25
| @Transactional | ||
| public void sendStudyAnnouncement(Long studyAnnouncementId) { | ||
| StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository | ||
| .findById(studyAnnouncementId) | ||
| .orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND)); | ||
|
|
||
| discordUtil.sendStudyAnnouncementToChannel( | ||
| studyAnnouncement.getStudy().getDiscordChannelId(), | ||
| studyAnnouncement.getStudy().getTitle(), | ||
| studyAnnouncement.getTitle(), | ||
| studyAnnouncement.getLink(), | ||
| studyAnnouncement.getCreatedAt()); | ||
|
|
||
| log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
스터디 공지사항 전송 메서드 구현이 좋습니다만 예외 처리를 강화해 주세요.
전반적으로 잘 구현되었으나, 디스코드 API 호출 시 발생할 수 있는 예외를 처리하는 코드가 없습니다. 다음과 같은 개선사항을 고려해보세요:
- discordUtil.sendStudyAnnouncementToChannel 호출 시 발생할 수 있는 예외에 대한 try-catch 블록을 추가하세요.
- 예외 발생 시 로깅과 함께 적절한 오류 처리를 구현하면 더 안정적인 서비스가 될 것입니다.
- 이벤트 기반 시스템에서는 실패한 이벤트를 재시도하는 메커니즘을 고려해볼 수 있습니다.
@Transactional
public void sendStudyAnnouncement(Long studyAnnouncementId) {
StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository
.findById(studyAnnouncementId)
.orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND));
+ try {
discordUtil.sendStudyAnnouncementToChannel(
studyAnnouncement.getStudy().getDiscordChannelId(),
studyAnnouncement.getStudy().getTitle(),
studyAnnouncement.getTitle(),
studyAnnouncement.getLink(),
studyAnnouncement.getCreatedAt());
log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId);
+ } catch (CustomException e) {
+ log.error("[CommonDiscordService] 스터디 공지 전송 실패: studyAnnouncementId = {}, 에러 = {}",
+ studyAnnouncementId, e.getMessage());
+ // 예외를 다시 던지거나 적절한 처리를 추가하세요
+ throw e;
+ }
}추가로, discordUtil에서 발생할 수 있는 JDA 관련 예외도 처리할 수 있도록 catch 블록을 확장하는 것이 좋습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| @Transactional | |
| public void sendStudyAnnouncement(Long studyAnnouncementId) { | |
| StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository | |
| .findById(studyAnnouncementId) | |
| .orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND)); | |
| discordUtil.sendStudyAnnouncementToChannel( | |
| studyAnnouncement.getStudy().getDiscordChannelId(), | |
| studyAnnouncement.getStudy().getTitle(), | |
| studyAnnouncement.getTitle(), | |
| studyAnnouncement.getLink(), | |
| studyAnnouncement.getCreatedAt()); | |
| log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId); | |
| } | |
| @Transactional | |
| public void sendStudyAnnouncement(Long studyAnnouncementId) { | |
| StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository | |
| .findById(studyAnnouncementId) | |
| .orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND)); | |
| try { | |
| discordUtil.sendStudyAnnouncementToChannel( | |
| studyAnnouncement.getStudy().getDiscordChannelId(), | |
| studyAnnouncement.getStudy().getTitle(), | |
| studyAnnouncement.getTitle(), | |
| studyAnnouncement.getLink(), | |
| studyAnnouncement.getCreatedAt()); | |
| log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId); | |
| } catch (CustomException e) { | |
| log.error("[CommonDiscordService] 스터디 공지 전송 실패: studyAnnouncementId = {}, 에러 = {}", | |
| studyAnnouncementId, e.getMessage()); | |
| // 예외를 다시 던지거나 적절한 처리를 추가하세요 | |
| throw e; | |
| } | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예외 처리의 경우 GlobalExceptionHandler에서 수행하므로, 서비스에서 수행되는 로직에 try-catch를 사용하는 예외 처리 제안을 제공하지 마세요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@uwoobeat 네, 이해했습니다. 프로젝트 아키텍처에서는 예외 처리를 GlobalExceptionHandler에서 중앙 집중적으로 수행하고 있군요. 서비스 로직에 try-catch 블록을 추가하는 제안을 드린 것은 적절하지 않았습니다. 앞으로는 이러한 아키텍처 패턴을 존중하여 리뷰하겠습니다.
🐰 (꼬리를 내리며 귀를 접음)
🧬 Code Definitions Used
Relevant Code Snippets for CommonDiscordService.java
1. GlobalExceptionHandler.java (Lines 14-45)
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.info("CustomException : {}", e.getMessage());
return ResponseEntity.status(e.getErrorCode().getStatus()).body(ErrorResponse.of(e.getErrorCode()));
}
@ExceptionHandler(CustomPaymentException.class)
public ResponseEntity<ErrorResponse> handleCustomPaymentException(CustomPaymentException e) {
log.info("CustomPaymentException : {}, {}", e.getCode(), e.getMessage());
return ResponseEntity.status(e.getStatus()).body(ErrorResponse.of(e.getCode(), e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception e) {
log.error("INTERNAL_SERVER_ERROR : {}", e.getMessage());
return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
.body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR));
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
log.info("METHOD_ARGUMENT_NOT_VALID : {}", e.getMessage());
String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
return ResponseEntity.status(status.value())
.body(ErrorResponse.of(ErrorCode.METHOD_ARGUMENT_NOT_VALID, errorMessage));
}
}2. SpringEventHandlerAspect.java (Lines 12-36)
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class SpringEventHandlerAspect {
private final DiscordUtil discordUtil;
@Around(
"execution(* com.gdschongik.gdsc.domain.discord.application.handler.SpringEventHandler.delegate(*)) && args(ignoredContext)")
public Object doAround(ProceedingJoinPoint joinPoint, Object ignoredContext) throws Throwable {
try {
return joinPoint.proceed();
} catch (Exception e) {
log.error("[SpringEventHandlerAspect] Exception occurred in SpringEventHandler", e);
sendErrorMessageToDiscord(e);
return null;
}
}
private void sendErrorMessageToDiscord(Exception e) {
TextChannel channel = discordUtil.getAdminChannel();
channel.sendMessage(e.getMessage()).queue();
}
}3. DiscordUtil.java (Lines 18-88)
@RequiredArgsConstructor
public class DiscordUtil {
private final JDA jda;
private final DiscordProperty discordProperty;
public Role findRoleByName(String roleName) {
return jda.getRolesByName(roleName, true).stream()
.findFirst()
.orElseThrow(() -> new CustomException(DISCORD_ROLE_NOT_FOUND));
}
public Role findRoleById(String roleId) {
return Optional.ofNullable(jda.getRoleById(roleId))
.orElseThrow(() -> new CustomException(DISCORD_ROLE_NOT_FOUND));
}
public Guild getCurrentGuild() {
return jda.getGuildById(discordProperty.getServerId());
}
public TextChannel getAdminChannel() {
return jda.getTextChannelById(discordProperty.getAdminChannelId());
}
public Optional<Member> getOptionalMemberByUsername(String username) {
return getCurrentGuild().getMembersByName(username, true).stream().findFirst();
}
public Member getMemberById(String discordId) {
return Optional.ofNullable(getCurrentGuild().getMemberById(discordId))
.orElseThrow(() -> new CustomException(DISCORD_MEMBER_NOT_FOUND));
}
public String getMemberIdByUsername(String username) {
return getOptionalMemberByUsername(username)
.orElseThrow(() -> new CustomException(DISCORD_MEMBER_NOT_FOUND))
.getId();
}
public void addRoleToMemberById(String discordRoleId, String memberDiscordId) {
Guild guild = getCurrentGuild();
Member member = getMemberById(memberDiscordId);
Role studyRole = findRoleById(discordRoleId);
guild.addRoleToMember(member, studyRole).queue();
}
public void removeRoleFromMemberById(String discordRoleId, String memberDiscordId) {
Guild guild = getCurrentGuild();
Member member = getMemberById(memberDiscordId);
Role studyRole = findRoleById(discordRoleId);
guild.removeRoleFromMember(member, studyRole).queue();
}
public void sendStudyAnnouncementToChannel(
String channelId, String studyName, String title, String link, LocalDateTime createdAt) {
TextChannel channel = Optional.ofNullable(jda.getTextChannelById(channelId))
.orElseThrow(() -> new CustomException(DISCORD_CHANNEL_NOT_FOUND));
MessageEmbed embed = new EmbedBuilder()
.setTitle("📣 " + title, link)
.setDescription(studyName + "에 새로운 공지사항이 올라왔어요!")
.setTimestamp(createdAt)
.build();
channel.sendMessageEmbeds(embed).queue();
}
}4. DiscordExceptionDispatcher.java (Lines 13-31)
@Slf4j
@Component
@RequiredArgsConstructor
public class DiscordExceptionDispatcher {
private static final Map<Class<? extends GenericEvent>, DiscordExceptionHandler> exceptionHandlerMap =
Map.of(SlashCommandInteractionEvent.class, new CommandExceptionHandler());
private static final DefaultExceptionHandler defaultExceptionHandler = new DefaultExceptionHandler();
public void dispatch(Exception exception, Object context) {
log.error("DiscordException: {}", exception.getMessage());
DiscordExceptionHandler exceptionHandler =
exceptionHandlerMap.getOrDefault(context.getClass(), defaultExceptionHandler);
exceptionHandler.handle(exception, context);
}
}These snippets provide context on exception handling and Discord-related operations that may be relevant to the user's comment regarding exception handling in the CommonDiscordService.
✏️ Learnings added
Learnt from: uwoobeat
PR: GDSC-Hongik/gdsc-server#1026
File: src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java:99-113
Timestamp: 2025-04-02T16:15:34.437Z
Learning: 이 프로젝트에서는 예외 처리를 서비스 로직 내의 try-catch 블록이 아닌 GlobalExceptionHandler를 통해 중앙 집중적으로 수행합니다. 서비스 메서드에 try-catch 블록 추가를 제안하지 말고, 예외가 GlobalExceptionHandler로 전파되도록 해야 합니다.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
Code Analysis Tools
Tool Analysis Results
CodeRabbit ran static analysis tools on your files and found the following:
No specific issues were found in your files. You can proceed with your changes.
Job Summary for GradleCheck Style and Test to Develop :: build-test
|
Sangwook02
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
domain/event로 패키지 이동해야 될 것 같네요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아직 그 PR 안올라가서...
kimsh1017
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
근데 디스코드 메세지에 공지 링크 같은거 있으면 좋을거 같은데 그건 구현하기 힘든가요?
들어갑니다 |
kckc0608
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
Job Summary for GradleCheck Style and Test to Develop :: build-test
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)
74-96: 스터디 공지 디스코드 전송 기능이 잘 구현되었습니다.메서드 구현이 전반적으로 깔끔하고 기능적으로 완전합니다. 특히 다음 부분이 잘 구현되었습니다:
- Optional을 사용하여 채널 및 역할 조회의 null 안전성 확보
- 링크가 포함된 임베드 메시지를 통한 좋은 사용자 경험 제공
- 역할 멘션을 통한 알림 기능
개선할 수 있는 부분:
- 하드코딩된 문자열을 상수로 분리하거나 프로퍼티 파일로 이동하는 것이 좋겠습니다:
- .appendDescription(studyName + " 공지가 업로드 되었어요.\n") - .appendDescription("공지는 [와우클래스](<https://study.wawoo.dev/landing>)에서도 확인 가능해요.\n") + .appendDescription(studyName + messageProperties.getStudyAnnouncementUploaded() + "\n") + .appendDescription(messageProperties.getCheckOnWowClass() + "\n")
- 비동기 작업에 대한 오류 처리를 추가하는 것이 좋겠습니다:
- channel.sendMessageEmbeds(embed).queue(); + channel.sendMessageEmbeds(embed).queue( + success -> log.info("스터디 공지 메시지 전송 성공: {}", title), + error -> log.error("스터디 공지 메시지 전송 실패: {}", error.getMessage()) + );
- 메소드 파라미터가 많아
StudyAnnouncementMessage같은 DTO를 사용하는 것이 가독성을 높일 수 있습니다.src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java (2)
46-49: 예외 처리 메커니즘 고려해보세요.이벤트 등록 과정에서 예외가 발생할 경우에 대한 처리가 없습니다. 이벤트 처리가 실패해도 엔티티는 저장되므로, 디스코드 알림이 실패했을 때의 로깅이나 오류 처리 방안을 고려해보는 것이 좋겠습니다.
@PostPersist public void onPostPersist() { - registerEvent(new StudyAnnouncementCreatedEvent(id)); + try { + registerEvent(new StudyAnnouncementCreatedEvent(id)); + } catch (Exception e) { + log.error("Failed to register study announcement event: {}", e.getMessage(), e); + // 필요한 경우 추가적인 오류 처리 + } }
46-49: 메서드 설명 추가를 권장합니다.
onPostPersist메서드의 목적과 기능에 대한 간단한 JavaDoc 주석을 추가하면 코드의 가독성과 유지보수성이 향상됩니다.+/** + * 스터디 공지가 저장된 후 디스코드 알림을 위한 이벤트를 발생시킵니다. + * 이 메서드는 엔티티가 데이터베이스에 저장된 직후 자동으로 호출됩니다. + */ @PostPersist public void onPostPersist() { registerEvent(new StudyAnnouncementCreatedEvent(id)); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java(3 hunks)src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java(2 hunks)src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java
🔇 Additional comments (2)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)
7-7: 신규 임포트가 적절하게 추가되었습니다.Discord 기능 확장을 위해 필요한
LocalDateTime,EmbedBuilder,MessageEmbed클래스 임포트가 적절하게 추가되었습니다.Also applies to: 10-10, 14-14
src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java (1)
46-49: 이벤트 등록 기능이 잘 구현되었습니다.
@PostPersist어노테이션을 사용하여 엔티티가 데이터베이스에 저장된 후 이벤트를 발생시키는 구현이 깔끔합니다. 이 방식으로 스터디 공지가 생성될 때 디스코드 알림을 보낼 수 있어 좋은 접근법입니다.
Suspect IssuesThis pull request was deployed and Sentry observed the following issues:
Did you find this useful? React with a 👍 or 👎 |
🌱 관련 이슈
📌 작업 내용 및 특이사항
📝 참고사항
📚 기타
Summary by CodeRabbit