Skip to content
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] 디스코드 알림 로직 추가 #126

Merged
merged 22 commits into from
Mar 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
1861f1a
chore: discord bot 의존성 추가
coli-geonwoo Mar 21, 2025
d27aeb1
feat: discord properties 구현
coli-geonwoo Mar 21, 2025
1da6066
feat: discord notifier 구현
coli-geonwoo Mar 21, 2025
a9c2954
feat: discord notification aop 작성
coli-geonwoo Mar 21, 2025
e66a393
fix: discord 에러 픽스
coli-geonwoo Mar 21, 2025
1035ea2
feat: 디스코드 알림 기능 구현
coli-geonwoo Mar 21, 2025
e84d4d3
style: 개행 추가
coli-geonwoo Mar 21, 2025
1cedd98
chore: 테스트용 yml 파일 수정
coli-geonwoo Mar 23, 2025
06da828
refactor: channelNotifier 인터페이스화
coli-geonwoo Mar 23, 2025
073962d
test: 테스트에 controller mocking
coli-geonwoo Mar 23, 2025
97e577e
test: channelNotifier 모킹
coli-geonwoo Mar 23, 2025
713f971
chore: ChannelNotifier 리네임
coli-geonwoo Mar 28, 2025
0815fa6
chore: discord 설정을 prod,dev yml파일에 반영
coli-geonwoo Mar 28, 2025
c2ec37b
chore: client 패키지 구조화
coli-geonwoo Mar 28, 2025
2e39b21
refactor: config 파일로 프로파이별 의존성 관리
coli-geonwoo Mar 28, 2025
620144a
chore : test base에서 notifier 모킹 제거
coli-geonwoo Mar 28, 2025
16a8c06
refactor: discordnotifier 수동빈 등록에 따른 컴포넌트화 제거
coli-geonwoo Mar 28, 2025
f2eba9a
refactor: norifierConfig에서 properties 임포트
coli-geonwoo Mar 28, 2025
01e2eac
refactor: ConsoleNotifier에 에러메시지 출력기능도 추가
coli-geonwoo Mar 28, 2025
8898de9
refactor : DiscordProperties 가 특정 프로파일에만 등록되도록 변경
leegwichan Mar 28, 2025
c7ce165
refactor: ConsoleNotifier에 log로 출력
coli-geonwoo Mar 28, 2025
5b24092
refactor: 디스코드 기준 개행문자로 통일
coli-geonwoo Mar 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ dependencies {
// Logging
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml"

//Discord
implementation 'net.dv8tion:JDA:5.0.0-beta.24'
}

bootJar {
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/com/debatetimer/client/notifier/ConsoleNotifier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.debatetimer.client.notifier;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConsoleNotifier implements ErrorNotifier {

private static final String ERROR_SEND_MESSAGE = "에러 정보가 채널로 발송되었습니다.";

@Override
public void sendErrorMessage(Throwable throwable) {
log.error(ERROR_SEND_MESSAGE);
log.error(throwable.getMessage());
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/debatetimer/client/notifier/DiscordNotifier.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.debatetimer.client.notifier;

import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import java.util.Arrays;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;

@Slf4j
public class DiscordNotifier implements ErrorNotifier {

private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n```\n";
private static final String DISCORD_LINE_SEPARATOR = "/n";
private static final int STACK_TRACE_LENGTH = 10;

private final DiscordProperties properties;
private final JDA jda;

public DiscordNotifier(DiscordProperties discordProperties) {
this.properties = discordProperties;
this.jda = initializeJda(properties.getToken());
}

private JDA initializeJda(String token) {
try {
return JDABuilder.createDefault(token).build().awaitReady();
} catch (InterruptedException e) {
throw new DTInitializationException(InitializationErrorCode.JDA_INITIALIZATION_FAIL);
}
}

public void sendErrorMessage(Throwable throwable) {
TextChannel channel = jda.getTextChannelById(properties.getChannelId());
String errorMessage = throwable.getMessage();
String stackTrace = getStackTraceAsString(throwable);

String errorNotification = NOTIFICATION_PREFIX
+ errorMessage
+ DISCORD_LINE_SEPARATOR
+ stackTrace;
channel.sendMessage(errorNotification).queue();
}

private String getStackTraceAsString(Throwable throwable) {
return Arrays.stream(throwable.getStackTrace())
.map(StackTraceElement::toString)
.limit(STACK_TRACE_LENGTH)
.collect(Collectors.joining(System.lineSeparator()));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.debatetimer.client.notifier;

import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@ConfigurationProperties(prefix = "discord")
public class DiscordProperties {

private final String token;
private final String channelId;

public DiscordProperties(String token, String channelId) {
validate(token);
validate(channelId);
this.token = token;
this.channelId = channelId;
}

private void validate(String element) {
if (element == null || element.isBlank()) {
throw new DTInitializationException(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.debatetimer.client.notifier;

public interface ErrorNotifier {

void sendErrorMessage(Throwable throwable);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.debatetimer.client;
package com.debatetimer.client.oauth;

import com.debatetimer.aop.logging.LoggingClient;
import com.debatetimer.dto.member.MemberCreateRequest;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.debatetimer.client;
package com.debatetimer.client.oauth;

import com.debatetimer.dto.member.MemberCreateRequest;
import com.debatetimer.exception.custom.DTInitializationException;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.debatetimer.config;

import com.debatetimer.client.OAuthProperties;
import com.debatetimer.client.oauth.OAuthProperties;
import com.debatetimer.controller.tool.jwt.JwtTokenProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/com/debatetimer/config/NotifierConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.debatetimer.config;


import com.debatetimer.client.notifier.ConsoleNotifier;
import com.debatetimer.client.notifier.DiscordNotifier;
import com.debatetimer.client.notifier.DiscordProperties;
import com.debatetimer.client.notifier.ErrorNotifier;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

public class NotifierConfig {

@Profile({"dev", "prod"})
@Configuration
@RequiredArgsConstructor
@EnableConfigurationProperties(DiscordProperties.class)
public static class DiscordNotifierConfig {

private final DiscordProperties discordProperties;

@Bean
public ErrorNotifier discordNotifier() {
return new DiscordNotifier(discordProperties);
}
}

@Profile({"test", "local"})
@Configuration
public static class ConsoleNotifierConfig {

@Bean
public ErrorNotifier consoleNotifier() {
return new ConsoleNotifier();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
public enum InitializationErrorCode {

OAUTH_PROPERTIES_EMPTY("OAuth 구성 요소들이 입력되지 않았습니다"),
DISCORD_PROPERTIES_EMPTY("디스코드 봇 구성 요소들이 입력되지 않았습니다"),
JDA_INITIALIZATION_FAIL("디스코드 client 구성에 실패하였습니다"),

CORS_ORIGIN_EMPTY("CORS Origin 은 적어도 한 개 있어야 합니다"),
CORS_ORIGIN_STRING_BLANK("CORS Origin 에 빈 값이 들어올 수 없습니다"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.debatetimer.exception.handler;

import com.debatetimer.client.notifier.ErrorNotifier;
import com.debatetimer.exception.ErrorResponse;
import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.custom.DTServerErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import com.debatetimer.exception.errorcode.ResponseErrorCode;
import com.debatetimer.exception.errorcode.ServerErrorCode;
import jakarta.validation.ConstraintViolationException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.ClientAbortException;
import org.springframework.http.HttpStatus;
Expand All @@ -21,8 +23,11 @@

@Slf4j
@RestControllerAdvice
@RequiredArgsConstructor
public class GlobalExceptionHandler {

private final ErrorNotifier errorNotifier;

@ExceptionHandler(BindException.class)
public ResponseEntity<ErrorResponse> handleBindingException(BindException exception) {
log.warn("message: {}", exception.getMessage());
Expand Down Expand Up @@ -78,12 +83,14 @@ public ResponseEntity<ErrorResponse> handleClientException(DTClientErrorExceptio
@ExceptionHandler(DTServerErrorException.class)
public ResponseEntity<ErrorResponse> handleServerException(DTServerErrorException exception) {
log.error("message: {}", exception.getMessage());
errorNotifier.sendErrorMessage(exception);
return toResponse(exception.getHttpStatus(), exception.getMessage());
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception exception) {
log.error("exception: {}", exception.getMessage());
errorNotifier.sendErrorMessage(exception);
return toResponse(ServerErrorCode.INTERNAL_SERVER_ERROR);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.debatetimer.service.auth;

import com.debatetimer.client.OAuthClient;
import com.debatetimer.client.oauth.OAuthClient;
import com.debatetimer.domain.member.Member;
import com.debatetimer.dto.member.MemberCreateRequest;
import com.debatetimer.dto.member.MemberInfo;
Expand Down
4 changes: 4 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@ jwt:
access_token_expiration: ${secret.jwt.access_token_expiration}
refresh_token_expiration: ${secret.jwt.refresh_token_expiration}

discord:
token: ${secret.discord.token}
channelId: ${secret.discord.channelId}

#logging:
# config: classpath:logging/log4j2-dev.yml
4 changes: 4 additions & 0 deletions src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,9 @@ jwt:
access_token_expiration: ${secret.jwt.access_token_expiration}
refresh_token_expiration: ${secret.jwt.refresh_token_expiration}

discord:
token: ${secret.discord.token}
channelId: ${secret.discord.channelId}

#logging:
# config: classpath:logging/log4j2-prod.yml
5 changes: 3 additions & 2 deletions src/test/java/com/debatetimer/DatabaseSchemaManagerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.context.ActiveProfiles;

@SpringBootTest
@ActiveProfiles("flyway")
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@ActiveProfiles({"test", "flyway"})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

c.f. 프로파일이 여러 개 일 경우 앞에서부터 값을 세팅하고, 뒤의 프로파일에 값이 겹치는게 있다면 덮어씌워버립니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 이거 처음 알았네요

class DatabaseSchemaManagerTest {

@Autowired
Expand Down
5 changes: 5 additions & 0 deletions src/test/java/com/debatetimer/DebateTimerApplicationTest.java
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package com.debatetimer;

import com.debatetimer.client.notifier.ErrorNotifier;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

@SpringBootTest
class DebateTimerApplicationTest {

@MockitoBean
private ErrorNotifier errorNotifier;

@Test
void contextLoads() {
}
Expand Down
36 changes: 36 additions & 0 deletions src/test/java/com/debatetimer/client/DiscordPropertiesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.debatetimer.client;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.debatetimer.client.notifier.DiscordProperties;
import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.NullAndEmptySource;
import org.junit.jupiter.params.provider.ValueSource;

class DiscordPropertiesTest {

@Nested
class Validate {

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"\n", "\t "})
void 디스코드봇_토큰이_비어있을_경우_예외를_발생시킨다(String empty) {
assertThatThrownBy(() -> new DiscordProperties(empty, "channelId"))
.isInstanceOf(DTInitializationException.class)
.hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage());
}

@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {"\n", "\t "})
void 디스코드_채널_아이디가_비어있을_경우_예외를_발생시킨다(String empty) {
assertThatThrownBy(() -> new DiscordProperties("token", empty))
.isInstanceOf(DTInitializationException.class)
.hasMessage(InitializationErrorCode.DISCORD_PROPERTIES_EMPTY.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import com.debatetimer.client.oauth.OAuthProperties;
import com.debatetimer.exception.custom.DTInitializationException;
import com.debatetimer.exception.errorcode.InitializationErrorCode;
import org.junit.jupiter.api.Nested;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.debatetimer.controller;

import com.debatetimer.DataBaseCleaner;
import com.debatetimer.client.OAuthClient;
import com.debatetimer.client.oauth.OAuthClient;
import com.debatetimer.fixture.CustomizeTableGenerator;
import com.debatetimer.fixture.CustomizeTimeBoxGenerator;
import com.debatetimer.fixture.HeaderGenerator;
Expand Down