diff --git a/build.gradle b/build.gradle
index d641c37ad..1ee05e906 100644
--- a/build.gradle
+++ b/build.gradle
@@ -117,7 +117,7 @@ tasks.named('test') {
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
sourceSets {
- main.java.srcDirs += [ querydslDir ]
+ main.java.srcDirs += [querydslDir]
}
tasks.withType(JavaCompile).configureEach {
diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
index d83793ba0..a7ca3a616 100644
--- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
+++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberBanService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.BanMemberUseCase;
@@ -11,8 +12,8 @@
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -22,7 +23,7 @@ public class MemberBanService implements BanMemberUseCase {
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 멤버를 영구적으로 차단합니다.
@@ -30,7 +31,7 @@ public class MemberBanService implements BanMemberUseCase {
*
해당 멤버의 계정 잠금 정보를 조회하고, 없으면 새로 생성합니다.
* Redis에 저장된 해당 멤버의 인증 토큰을 삭제하며, Slack에 밴 알림을 전송합니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param memberId 차단할 멤버의 ID
* @return 저장된 계정 잠금 정보의 ID
*/
@@ -58,6 +59,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {
private void sendSlackBanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_BANNED, "ID: " + memberId + ", Name: " + memberName);
+ String memberBannedMessage = "ID: " + memberId + ", Name: " + memberName;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_BANNED, request, memberBannedMessage));
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
index 391628d8e..1aaf67e4d 100644
--- a/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
+++ b/src/main/java/page/clab/api/domain/auth/accountLockInfo/application/service/MemberUnbanService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.in.UnbanMemberUseCase;
@@ -10,8 +11,8 @@
import page.clab.api.domain.auth.accountLockInfo.domain.AccountLockInfo;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -20,7 +21,7 @@ public class MemberUnbanService implements UnbanMemberUseCase {
private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort;
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 차단된 멤버를 해제합니다.
@@ -28,7 +29,7 @@ public class MemberUnbanService implements UnbanMemberUseCase {
* 해당 멤버의 계정 잠금 정보를 조회하고 해제합니다.
* 해제된 정보는 저장되며, Slack에 해제 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param memberId 해제할 멤버의 ID
* @return 업데이트된 계정 잠금 정보의 ID
*/
@@ -55,6 +56,8 @@ private AccountLockInfo createAccountLockInfo(String memberId) {
private void sendSlackUnbanNotification(HttpServletRequest request, String memberId) {
String memberName = externalRetrieveMemberUseCase.getMemberBasicInfoById(memberId).getMemberName();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.MEMBER_UNBANNED, "ID: " + memberId + ", Name: " + memberName);
+ String memberUnbannedMessage = "ID: " + memberId + ", Name: " + memberName;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_UNBANNED, request, memberUnbannedMessage));
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
index 6694afdec..42bc014d8 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRegisterService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.dto.mapper.BlacklistIpDtoMapper;
@@ -10,8 +11,8 @@
import page.clab.api.domain.auth.blacklistIp.application.port.out.RegisterBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,17 +20,16 @@ public class BlacklistIpRegisterService implements RegisterBlacklistIpUseCase {
private final RegisterBlacklistIpPort registerBlacklistIpPort;
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final BlacklistIpDtoMapper mapper;
/**
* 지정된 IP 주소를 블랙리스트에 등록합니다.
*
* 해당 IP 주소가 이미 블랙리스트에 존재하는지 확인하고,
- * 존재하지 않을 경우 새롭게 등록합니다.
- * 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.
+ * 존재하지 않을 경우 새롭게 등록합니다. 새로운 IP가 등록되면 Slack을 통해 보안 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param requestDto 블랙리스트에 추가할 IP 주소 정보를 담은 DTO
* @return 기존에 존재하거나 새로 추가된 블랙리스트 IP 주소
*/
@@ -42,7 +42,12 @@ public String registerBlacklistIp(HttpServletRequest request, BlacklistIpRequest
.orElseGet(() -> {
BlacklistIp blacklistIp = mapper.fromDto(requestDto);
registerBlacklistIpPort.save(blacklistIp);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, "Added IP: " + ipAddress);
+
+ String blacklistAddedMessage = "Added IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request,
+ blacklistAddedMessage));
+
return ipAddress;
});
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
index 5de09b640..26b558adc 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpRemoveService.java
@@ -2,14 +2,15 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.RemoveBlacklistIpUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -17,7 +18,7 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 지정된 IP 주소를 블랙리스트에서 제거합니다.
@@ -25,7 +26,7 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
* 블랙리스트에 등록된 IP 주소 정보를 조회하고 해당 정보를 삭제합니다.
* 삭제가 완료되면 Slack을 통해 보안 알림이 전송됩니다.
*
- * @param request 현재 요청 객체
+ * @param request 현재 요청 객체
* @param ipAddress 제거할 블랙리스트 IP 주소
* @return 삭제된 블랙리스트 IP 주소
*/
@@ -34,7 +35,12 @@ public class BlacklistIpRemoveService implements RemoveBlacklistIpUseCase {
public String removeBlacklistIp(HttpServletRequest request, String ipAddress) {
BlacklistIp blacklistIp = retrieveBlacklistIpPort.getByIpAddress(ipAddress);
removeBlacklistIpPort.delete(blacklistIp);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: " + ipAddress);
+
+ String blacklistRemovedMessage = "Deleted IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
+ blacklistRemovedMessage));
+
return blacklistIp.getIpAddress();
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
index 342a2561f..cdcf60021 100644
--- a/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
+++ b/src/main/java/page/clab/api/domain/auth/blacklistIp/application/service/BlacklistIpResetService.java
@@ -1,17 +1,17 @@
package page.clab.api.domain.auth.blacklistIp.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.blacklistIp.application.port.in.ResetBlacklistIpsUseCase;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RemoveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.application.port.out.RetrieveBlacklistIpPort;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
-
-import java.util.List;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,7 +19,7 @@ public class BlacklistIpResetService implements ResetBlacklistIpsUseCase {
private final RetrieveBlacklistIpPort retrieveBlacklistIpPort;
private final RemoveBlacklistIpPort removeBlacklistIpPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 블랙리스트에 등록된 모든 IP 주소를 초기화합니다.
@@ -38,7 +38,12 @@ public List resetBlacklistIps(HttpServletRequest request) {
.map(BlacklistIp::getIpAddress)
.toList();
removeBlacklistIpPort.deleteAll();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_REMOVED, "Deleted IP: ALL");
+
+ String blacklistRemovedMessage = "Deleted IP: ALL";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_REMOVED, request,
+ blacklistRemovedMessage));
+
return blacklistedIps;
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
index 655a51a06..402cf4ee7 100644
--- a/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
+++ b/src/main/java/page/clab/api/domain/auth/login/application/service/TwoFactorAuthenticationService.java
@@ -1,8 +1,10 @@
package page.clab.api.domain.auth.login.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountAccessLog.domain.AccountAccessResult;
@@ -21,11 +23,10 @@
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.global.auth.jwt.JwtTokenProvider;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType;
import page.clab.api.global.util.HttpReqResUtil;
-import java.util.List;
-
@Service
@RequiredArgsConstructor
@Qualifier("twoFactorAuthenticationService")
@@ -36,12 +37,14 @@ public class TwoFactorAuthenticationService implements ManageLoginUseCase {
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalRegisterAccountAccessLogUseCase externalRegisterAccountAccessLogUseCase;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final JwtTokenProvider jwtTokenProvider;
@Transactional
@Override
- public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto) throws LoginFailedException, MemberLockedException {
+ public LoginResult authenticate(HttpServletRequest request,
+ TwoFactorAuthenticationRequestDto twoFactorAuthenticationRequestDto)
+ throws LoginFailedException, MemberLockedException {
String memberId = twoFactorAuthenticationRequestDto.getMemberId();
MemberLoginInfoDto loginMember = externalRetrieveMemberUseCase.getMemberLoginInfoById(memberId);
String totp = twoFactorAuthenticationRequestDto.getTotp();
@@ -55,9 +58,11 @@ public LoginResult authenticate(HttpServletRequest request, TwoFactorAuthenticat
return LoginResult.create(header, true);
}
- private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request) throws MemberLockedException, LoginFailedException {
+ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpServletRequest request)
+ throws MemberLockedException, LoginFailedException {
if (!manageAuthenticatorUseCase.isAuthenticatorValid(memberId, totp)) {
- externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId, AccountAccessResult.FAILURE);
+ externalRegisterAccountAccessLogUseCase.registerAccountAccessLog(request, memberId,
+ AccountAccessResult.FAILURE);
externalManageAccountLockUseCase.handleLoginFailure(request, memberId);
throw new LoginFailedException("잘못된 인증번호입니다.");
}
@@ -67,18 +72,21 @@ private void verifyTwoFactorAuthentication(String memberId, String totp, HttpSer
private TokenInfo generateAndSaveToken(MemberLoginInfoDto memberInfo) {
TokenInfo tokenInfo = jwtTokenProvider.generateToken(memberInfo.getMemberId(), memberInfo.getRole());
String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist();
- externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo, clientIpAddress);
+ externalManageRedisTokenUseCase.saveToken(memberInfo.getMemberId(), memberInfo.getRole(), tokenInfo,
+ clientIpAddress);
return tokenInfo;
}
private void sendAdminLoginNotification(HttpServletRequest request, MemberLoginInfoDto loginMember) {
if (loginMember.isSuperAdminRole()) {
- slackService.sendAdminLoginNotification(request, loginMember);
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, GeneralAlertType.ADMIN_LOGIN, request, loginMember));
}
}
@Override
- public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto) throws LoginFailedException, MemberLockedException {
+ public LoginResult login(HttpServletRequest request, LoginRequestDto requestDto)
+ throws LoginFailedException, MemberLockedException {
throw new UnsupportedOperationException("Method not implemented");
}
diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
index 7789853cb..a31e7ee8a 100644
--- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
+++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpRemoveService.java
@@ -2,25 +2,29 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.RemoveAbnormalAccessIpUseCase;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RemoveIpAccessMonitorPort;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
public class AbnormalAccessIpRemoveService implements RemoveAbnormalAccessIpUseCase {
private final RemoveIpAccessMonitorPort removeIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public String removeAbnormalAccessIp(HttpServletRequest request, String ipAddress) {
removeIpAccessMonitorPort.deleteById(ipAddress);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: " + ipAddress);
+ String abnormalAccessIpDeletedMessage = "Deleted IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request,
+ abnormalAccessIpDeletedMessage));
return ipAddress;
}
}
diff --git a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
index 7a5c7dbcb..f347f2644 100644
--- a/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
+++ b/src/main/java/page/clab/api/domain/auth/redisIpAccessMonitor/application/service/AbnormalAccessIpsClearService.java
@@ -1,17 +1,17 @@
package page.clab.api.domain.auth.redisIpAccessMonitor.application.service;
import jakarta.servlet.http.HttpServletRequest;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.in.ClearAbnormalAccessIpsUseCase;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.ClearIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
-
-import java.util.List;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,14 +19,19 @@ public class AbnormalAccessIpsClearService implements ClearAbnormalAccessIpsUseC
private final ClearIpAccessMonitorPort clearIpAccessMonitorPort;
private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Override
@Transactional
public List clearAbnormalAccessIps(HttpServletRequest request) {
List ipAccessMonitors = retrieveIpAccessMonitorPort.findAll();
clearIpAccessMonitorPort.deleteAll();
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, "Deleted IP: ALL");
+
+ String abnormalAccessIpClearedMessage = "Deleted IP: ALL";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_DELETED, request,
+ abnormalAccessIpClearedMessage));
+
return ipAccessMonitors;
}
}
diff --git a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
index f2e487749..cdcd2dee8 100644
--- a/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
+++ b/src/main/java/page/clab/api/domain/community/board/application/service/BoardRegisterService.java
@@ -1,6 +1,8 @@
package page.clab.api.domain.community.board.application.service;
+import java.util.List;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.community.board.application.dto.mapper.BoardDtoMapper;
@@ -8,17 +10,16 @@
import page.clab.api.domain.community.board.application.port.in.RegisterBoardUseCase;
import page.clab.api.domain.community.board.application.port.out.RegisterBoardPort;
import page.clab.api.domain.community.board.domain.Board;
-import page.clab.api.global.common.slack.domain.SlackBoardInfo;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
import page.clab.api.global.common.file.application.UploadedFileService;
import page.clab.api.global.common.file.domain.UploadedFile;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
import page.clab.api.global.exception.PermissionDeniedException;
-import java.util.List;
-
@Service
@RequiredArgsConstructor
public class BoardRegisterService implements RegisterBoardUseCase {
@@ -27,7 +28,7 @@ public class BoardRegisterService implements RegisterBoardUseCase {
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
private final UploadedFileService uploadedFileService;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final BoardDtoMapper mapper;
/**
@@ -48,10 +49,14 @@ public String registerBoard(BoardRequestDto requestDto) throws PermissionDeniedE
Board board = mapper.fromDto(requestDto, currentMemberInfo.getMemberId(), uploadedFiles);
board.validateAccessPermissionForCreation(currentMemberInfo);
if (board.shouldNotifyForNewBoard(currentMemberInfo)) {
- externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(), "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다.");
+ externalSendNotificationUseCase.sendNotificationToMember(currentMemberInfo.getMemberId(),
+ "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다.");
}
- SlackBoardInfo boardInfo = SlackBoardInfo.create(board, currentMemberInfo);
- slackService.sendNewBoardNotification(boardInfo);
+
+ BoardNotificationInfo boardInfo = BoardNotificationInfo.create(board, currentMemberInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOARD, null,
+ boardInfo));
+
return registerBoardPort.save(board).getCategory().getKey();
}
}
diff --git a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
index 11c69305c..357a59cde 100644
--- a/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
+++ b/src/main/java/page/clab/api/domain/hiring/application/application/service/ApplicationApplyService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.hiring.application.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.hiring.application.application.dto.mapper.ApplicationDtoMapper;
@@ -10,7 +11,8 @@
import page.clab.api.domain.hiring.application.domain.Application;
import page.clab.api.external.hiring.application.application.port.ExternalRetrieveRecruitmentUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
@Service
@RequiredArgsConstructor
@@ -19,9 +21,9 @@ public class ApplicationApplyService implements ApplyForApplicationUseCase {
private final RegisterApplicationPort registerApplicationPort;
private final ExternalRetrieveRecruitmentUseCase externalRetrieveRecruitmentUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final ApplicationDtoMapper mapper;
-
+
@Transactional
@Override
public String applyForClub(ApplicationRequestDto requestDto) {
@@ -30,7 +32,10 @@ public String applyForClub(ApplicationRequestDto requestDto) {
String applicationType = application.getApplicationTypeForNotificationPrefix();
externalSendNotificationUseCase.sendNotificationToAdmins(applicationType + requestDto.getStudentId() + " " +
requestDto.getName() + "님이 지원하였습니다.");
- slackService.sendNewApplicationNotification(requestDto);
+
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_APPLICATION, null,
+ requestDto));
+
return registerApplicationPort.save(application).getStudentId();
}
}
diff --git a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
index 1e759332e..9f32d5adb 100644
--- a/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
+++ b/src/main/java/page/clab/api/domain/library/bookLoanRecord/application/service/BookLoanRequestService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.library.bookLoanRecord.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -17,8 +18,9 @@
import page.clab.api.external.library.book.application.port.ExternalRetrieveBookUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SlackBookLoanRecordInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
import page.clab.api.global.exception.CustomOptimisticLockingFailureException;
@Service
@@ -30,21 +32,19 @@ public class BookLoanRequestService implements RequestBookLoanUseCase {
private final ExternalRetrieveBookUseCase externalRetrieveBookUseCase;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
/**
* 도서 대출 신청을 처리합니다.
*
* 현재 로그인한 멤버의 대출 상태와 한도를 검증한 후,
- * 도서의 대출 신청이 이미 존재하는지 확인합니다.
- * 대출 신청이 성공적으로 완료되면 멤버와 Slack에 알림을 전송하고,
- * 대출 기록을 저장한 후 그 ID를 반환합니다.
+ * 도서의 대출 신청이 이미 존재하는지 확인합니다. 대출 신청이 성공적으로 완료되면 멤버와 Slack에 알림을 전송하고, 대출 기록을 저장한 후 그 ID를 반환합니다.
*
* @param requestDto 도서 대출 신청 요청 정보 DTO
* @return 저장된 대출 기록의 ID
* @throws CustomOptimisticLockingFailureException 동시에 다른 사용자가 대출을 신청하여 충돌이 발생한 경우 예외 발생
- * @throws MaxBorrowLimitExceededException 대출 한도를 초과한 경우 예외 발생
- * @throws BookAlreadyAppliedForLoanException 이미 신청된 도서일 경우 예외 발생
+ * @throws MaxBorrowLimitExceededException 대출 한도를 초과한 경우 예외 발생
+ * @throws BookAlreadyAppliedForLoanException 이미 신청된 도서일 경우 예외 발생
*/
@Transactional
@Override
@@ -60,10 +60,13 @@ public Long requestBookLoan(BookLoanRecordRequestDto requestDto) throws CustomOp
BookLoanRecord bookLoanRecord = BookLoanRecord.create(book.getId(), borrowerInfo);
- externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(), "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다.");
+ externalSendNotificationUseCase.sendNotificationToMember(borrowerInfo.getMemberId(),
+ "[" + book.getTitle() + "] 도서 대출 신청이 완료되었습니다.");
- SlackBookLoanRecordInfo bookLoanRecordInfo = SlackBookLoanRecordInfo.create(book, borrowerInfo);
- slackService.sendNewBookLoanRequestNotification(bookLoanRecordInfo);
+ BookLoanRecordNotificationInfo bookLoanRecordInfo = BookLoanRecordNotificationInfo.create(book,
+ borrowerInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_BOOK_LOAN_REQUEST, null,
+ bookLoanRecordInfo));
return registerBookLoanRecordPort.save(bookLoanRecord).getId();
} catch (ObjectOptimisticLockingFailureException e) {
diff --git a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
index db2ced3c0..ceed5fb73 100644
--- a/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
+++ b/src/main/java/page/clab/api/domain/memberManagement/member/application/service/MemberRoleManagementService.java
@@ -2,6 +2,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest;
@@ -11,8 +12,8 @@
import page.clab.api.domain.memberManagement.member.application.port.out.UpdateMemberPort;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.domain.memberManagement.member.domain.Role;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -20,11 +21,12 @@ public class MemberRoleManagementService implements ManageMemberRoleUseCase {
private final RetrieveMemberPort retrieveMemberPort;
private final UpdateMemberPort updateMemberPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Transactional
@Override
- public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, ChangeMemberRoleRequest request) {
+ public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId,
+ ChangeMemberRoleRequest request) {
Member member = retrieveMemberPort.getById(memberId);
Role oldRole = member.getRole();
@@ -34,9 +36,13 @@ public String changeMemberRole(HttpServletRequest httpServletRequest, String mem
member.changeRole(newRole);
updateMemberPort.update(member);
- slackService.sendSecurityAlertNotification(httpServletRequest, SecurityAlertType.MEMBER_ROLE_CHANGED,
- String.format("[%s] %s: %s -> %s",
- member.getId(), member.getName(), oldRole, newRole));
+
+ String memberRoleChangedMessage = String.format("[%s] %s: %s -> %s", member.getId(), member.getName(), oldRole,
+ newRole);
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.MEMBER_ROLE_CHANGED, httpServletRequest,
+ memberRoleChangedMessage));
+
return memberId;
}
diff --git a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
index 3d50a19e3..533b7d879 100644
--- a/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
+++ b/src/main/java/page/clab/api/domain/members/membershipFee/application/service/MembershipFeeRegisterService.java
@@ -1,6 +1,7 @@
package page.clab.api.domain.members.membershipFee.application.service;
import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberBasicInfoDto;
@@ -11,8 +12,9 @@
import page.clab.api.domain.members.membershipFee.domain.MembershipFee;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
import page.clab.api.external.memberManagement.notification.application.port.ExternalSendNotificationUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SlackMembershipFeeInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
@Service
@RequiredArgsConstructor
@@ -21,7 +23,7 @@ public class MembershipFeeRegisterService implements RegisterMembershipFeeUseCas
private final RegisterMembershipFeePort registerMembershipFeePort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
private final ExternalSendNotificationUseCase externalSendNotificationUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
private final MembershipFeeDtoMapper mapper;
@Transactional
@@ -30,8 +32,10 @@ public Long registerMembershipFee(MembershipFeeRequestDto requestDto) {
MemberBasicInfoDto memberInfo = externalRetrieveMemberUseCase.getCurrentMemberBasicInfo();
MembershipFee membershipFee = mapper.fromDto(requestDto, memberInfo.getMemberId());
externalSendNotificationUseCase.sendNotificationToAdmins("새로운 회비 내역이 등록되었습니다.");
- SlackMembershipFeeInfo membershipFeeInfo = SlackMembershipFeeInfo.create(membershipFee, memberInfo);
- slackService.sendNewMembershipFeeNotification(membershipFeeInfo);
+ MembershipFeeNotificationInfo membershipFeeInfo = MembershipFeeNotificationInfo.create(membershipFee,
+ memberInfo);
+ eventPublisher.publishEvent(new NotificationEvent(this, ExecutivesAlertType.NEW_MEMBERSHIP_FEE, null,
+ membershipFeeInfo));
return registerMembershipFeePort.save(membershipFee).getId();
}
}
diff --git a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
index 4a9eb6dde..436c68313 100644
--- a/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
+++ b/src/main/java/page/clab/api/external/auth/accountLockInfo/port/ExternalAccountLockManagementService.java
@@ -3,6 +3,7 @@
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.accountLockInfo.application.port.out.RegisterAccountLockInfoPort;
@@ -13,8 +14,8 @@
import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberDetailedInfoDto;
import page.clab.api.external.auth.accountLockInfo.application.ExternalManageAccountLockUseCase;
import page.clab.api.external.memberManagement.member.application.port.ExternalRetrieveMemberUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -23,7 +24,7 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou
private final RetrieveAccountLockInfoPort retrieveAccountLockInfoPort;
private final RegisterAccountLockInfoPort registerAccountLockInfoPort;
private final ExternalRetrieveMemberUseCase externalRetrieveMemberUseCase;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Value("${security.login-attempt.max-failures}")
private int maxLoginFailures;
@@ -39,7 +40,7 @@ public class ExternalAccountLockManagementService implements ExternalManageAccou
*
* @param memberId 잠금 해제하려는 멤버의 ID
* @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생
- * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
+ * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
*/
@Transactional
@Override
@@ -55,17 +56,17 @@ public void handleAccountLockInfo(String memberId) throws MemberLockedException,
* 로그인 실패를 처리하고 계정 잠금을 관리합니다.
*
* 로그인 실패 시 멤버의 존재 여부와 계정 잠금 상태를 확인합니다.
- * 로그인 실패 횟수를 증가시키며, 설정된 최대 실패 횟수에 도달하면 계정을 잠그고
- * Slack에 보안 알림을 전송합니다.
+ * 로그인 실패 횟수를 증가시키며, 설정된 최대 실패 횟수에 도달하면 계정을 잠그고 Slack에 보안 알림을 전송합니다.
*
- * @param request 현재 HTTP 요청 객체
+ * @param request 현재 HTTP 요청 객체
* @param memberId 로그인 실패를 기록할 멤버의 ID
* @throws MemberLockedException 계정이 현재 잠겨 있을 경우 예외 발생
- * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
+ * @throws LoginFailedException 멤버가 존재하지 않을 경우 예외 발생
*/
@Transactional
@Override
- public void handleLoginFailure(HttpServletRequest request, String memberId) throws MemberLockedException, LoginFailedException {
+ public void handleLoginFailure(HttpServletRequest request, String memberId)
+ throws MemberLockedException, LoginFailedException {
ensureMemberExists(memberId);
AccountLockInfo accountLockInfo = ensureAccountLockInfo(memberId);
validateAccountLockStatus(accountLockInfo);
@@ -99,7 +100,10 @@ private void sendSlackLoginFailureNotification(HttpServletRequest request, Strin
String memberName = memberInfo.getMemberName();
if (memberInfo.isAdminRole()) {
request.setAttribute("member", memberId + " " + memberName);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.REPEATED_LOGIN_FAILURES, "로그인 실패 횟수 초과로 계정이 잠겼습니다.");
+ String repeatedLoginFailuresMessage = "로그인 실패 횟수 초과로 계정이 잠겼습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.REPEATED_LOGIN_FAILURES, request,
+ repeatedLoginFailuresMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
index a600c8b3d..61ac9a980 100644
--- a/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
+++ b/src/main/java/page/clab/api/external/auth/redisIpAccessMonitor/application/service/ExternalIpAccessMonitorRegisterService.java
@@ -4,14 +4,15 @@
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RegisterIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.application.port.out.RetrieveIpAccessMonitorPort;
import page.clab.api.domain.auth.redisIpAccessMonitor.domain.RedisIpAccessMonitor;
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalRegisterIpAccessMonitorUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
@Service
@RequiredArgsConstructor
@@ -19,7 +20,7 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI
private final RegisterIpAccessMonitorPort registerIpAccessMonitorPort;
private final RetrieveIpAccessMonitorPort retrieveIpAccessMonitorPort;
- private final SlackService slackService;
+ private final ApplicationEventPublisher eventPublisher;
@Value("${security.ip-attempt.max-attempts}")
private int maxAttempts;
@@ -29,7 +30,10 @@ public class ExternalIpAccessMonitorRegisterService implements ExternalRegisterI
public void registerIpAccessMonitor(HttpServletRequest request, String ipAddress) {
RedisIpAccessMonitor redisIpAccessMonitor = getOrCreateRedisIpAccessMonitor(ipAddress);
if (redisIpAccessMonitor.isBlocked()) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, "Blocked IP: " + ipAddress);
+ String abnormalAccessIpBlockedMessage = "Blocked IP: " + ipAddress;
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS_IP_BLOCKED, request,
+ abnormalAccessIpBlockedMessage));
}
registerIpAccessMonitorPort.save(redisIpAccessMonitor);
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
index 954d80f94..ed2287c8e 100644
--- a/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/CustomBasicAuthenticationFilter.java
@@ -4,8 +4,11 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Base64;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -15,21 +18,17 @@
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase;
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase;
import page.clab.api.global.auth.util.IpWhitelistValidator;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.WhitelistPathMatcher;
-import java.io.IOException;
-import java.util.Base64;
-
/**
* {@code CustomBasicAuthenticationFilter}는 기본 인증 필터를 확장하여 추가적인 보안 기능을 제공합니다.
*
* IP 주소 기반 접근 제한, 화이트리스트 경로 검증, 사용자 인증 정보를 바탕으로
- * Slack 보안 알림을 전송하는 기능을 포함합니다. 또한 Swagger 또는 Actuator에 대한
- * 접근이 성공하거나 실패할 경우 이를 Slack에 알립니다.
+ * Slack 보안 알림을 전송하는 기능을 포함합니다. 또한 Swagger 또는 Actuator에 대한 접근이 성공하거나 실패할 경우 이를 Slack에 알립니다.
*
* 이 필터는 다음과 같은 추가 검증을 수행합니다:
*
@@ -47,22 +46,29 @@
public class CustomBasicAuthenticationFilter extends BasicAuthenticationFilter {
private final IpWhitelistValidator ipWhitelistValidator;
- private final SlackService slackService;
private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
+ private final ApplicationEventPublisher eventPublisher;
public CustomBasicAuthenticationFilter(
AuthenticationManager authenticationManager,
IpWhitelistValidator ipWhitelistValidator,
- SlackService slackService,
ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase,
- ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase
+ ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase,
+ ApplicationEventPublisher eventPublisher
) {
super(authenticationManager);
this.externalCheckIpBlockedUseCase = externalCheckIpBlockedUseCase;
this.externalRetrieveBlacklistIpUseCase = externalRetrieveBlacklistIpUseCase;
this.ipWhitelistValidator = ipWhitelistValidator;
- this.slackService = slackService;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @NotNull
+ private static String[] decodeCredentials(String authorizationHeader) {
+ String base64Credentials = authorizationHeader.substring("Basic ".length());
+ String credentials = new String(Base64.getDecoder().decode(base64Credentials));
+ return credentials.split(":", 2);
}
@Override
@@ -82,7 +88,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
super.doFilterInternal(request, response, chain);
}
- private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException {
+ private boolean authenticateUserCredentials(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
String authorizationHeader = request.getHeader("Authorization");
if (authorizationHeader == null || !authorizationHeader.startsWith("Basic ")) {
response.setHeader("WWW-Authenticate", "Basic realm=\"Please enter your username and password\"");
@@ -122,28 +129,29 @@ private boolean verifyIpAddressAccess(HttpServletResponse response) throws IOExc
return true;
}
- @NotNull
- private static String[] decodeCredentials(String authorizationHeader) {
- String base64Credentials = authorizationHeader.substring("Basic ".length());
- String credentials = new String(Base64.getDecoder().decode(base64Credentials));
- return credentials.split(":", 2);
- }
-
private void sendAuthenticationSuccessAlertSlackMessage(HttpServletRequest request) {
String path = request.getRequestURI();
if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 허가되었습니다.");
+ String apiDocsAccessMessage = "API 문서에 대한 접근이 허가되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage));
} else if (WhitelistPathMatcher.isActuatorRequest(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 허가되었습니다.");
+ String actuatorAccessMessage = "Actuator에 대한 접근이 허가되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage));
}
}
private void sendAuthenticationFailureAlertSlackMessage(HttpServletRequest request) {
String path = request.getRequestURI();
if (WhitelistPathMatcher.isSwaggerIndexEndpoint(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.API_DOCS_ACCESS,"API 문서에 대한 접근이 거부되었습니다.");
+ String apiDocsAccessMessage = "API 문서에 대한 접근이 거부되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.API_DOCS_ACCESS, request, apiDocsAccessMessage));
} else if (WhitelistPathMatcher.isActuatorRequest(path)) {
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ACTUATOR_ACCESS,"Actuator에 대한 접근이 거부되었습니다.");
+ String actuatorAccessMessage = "Actuator에 대한 접근이 거부되었습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.ACTUATOR_ACCESS, request, actuatorAccessMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
index b8314f9bf..5f7548fd1 100644
--- a/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/InvalidEndpointAccessFilter.java
@@ -6,25 +6,24 @@
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import page.clab.api.domain.auth.blacklistIp.domain.BlacklistIp;
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRegisterBlacklistIpUseCase;
import page.clab.api.external.auth.blacklistIp.application.port.ExternalRetrieveBlacklistIpUseCase;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.SecurityPatternChecker;
-import java.io.IOException;
-
/**
- * {@code InvalidEndpointAccessFilter}는 서버 내부 파일 및 디렉토리에 대한 비정상적인 접근을 차단하고
- * 보안 경고를 전송하는 필터입니다.
+ * {@code InvalidEndpointAccessFilter}는 서버 내부 파일 및 디렉토리에 대한 비정상적인 접근을 차단하고 보안 경고를 전송하는 필터입니다.
*
* 특정 패턴을 통해 비정상적인 접근 시도를 탐지하며, 비정상적인 경로로 접근을 시도한 IP를
* 블랙리스트에 등록하고, Slack을 통해 보안 경고 메시지를 전송합니다.
@@ -44,13 +43,14 @@
@Slf4j
public class InvalidEndpointAccessFilter extends GenericFilterBean {
- private final SlackService slackService;
private final String fileURL;
private final ExternalRegisterBlacklistIpUseCase externalRegisterBlacklistIpUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
+ private final ApplicationEventPublisher eventPublisher;
@Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String path = httpRequest.getRequestURI();
boolean isUploadedFileAccess = path.startsWith(fileURL);
@@ -75,7 +75,8 @@ private void handleSuspiciousAccess(HttpServletRequest request, HttpServletRespo
private void logSuspiciousAccess(HttpServletRequest request, String clientIpAddress) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
- String id = (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName();
+ String id =
+ (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName();
String requestUrl = request.getRequestURI();
String httpMethod = request.getMethod();
int statusCode = HttpServletResponse.SC_FORBIDDEN;
@@ -97,7 +98,9 @@ private void sendSecurityAlerts(HttpServletRequest request, String clientIpAddre
String abnormalAccessMessage = "서버 내부 파일 및 디렉토리에 대한 접근이 감지되었습니다.";
String blacklistAddedMessage = "Added IP: " + clientIpAddress;
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.ABNORMAL_ACCESS, abnormalAccessMessage);
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.BLACKLISTED_IP_ADDED, blacklistAddedMessage);
+ eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.ABNORMAL_ACCESS, request,
+ abnormalAccessMessage));
+ eventPublisher.publishEvent(new NotificationEvent(this, SecurityAlertType.BLACKLISTED_IP_ADDED, request,
+ blacklistAddedMessage));
}
}
diff --git a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
index 12bda3af8..fba999b53 100644
--- a/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
+++ b/src/main/java/page/clab/api/global/auth/filter/JwtAuthenticationFilter.java
@@ -6,8 +6,10 @@
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.IOException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
@@ -16,14 +18,12 @@
import page.clab.api.external.auth.redisIpAccessMonitor.application.port.ExternalCheckIpBlockedUseCase;
import page.clab.api.external.auth.redisToken.application.port.ExternalManageRedisTokenUseCase;
import page.clab.api.global.auth.jwt.JwtTokenProvider;
-import page.clab.api.global.common.slack.application.SlackService;
-import page.clab.api.global.common.slack.domain.SecurityAlertType;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
import page.clab.api.global.util.HttpReqResUtil;
import page.clab.api.global.util.ResponseUtil;
import page.clab.api.global.util.WhitelistPathMatcher;
-import java.io.IOException;
-
/**
* {@code JwtAuthenticationFilter}는 JWT 토큰을 검증하고, IP 주소 기반 접근 제한을 수행하는 필터입니다.
*
@@ -45,14 +45,15 @@
@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {
- private final SlackService slackService;
private final JwtTokenProvider jwtTokenProvider;
+ private final ApplicationEventPublisher eventPublisher;
private final ExternalManageRedisTokenUseCase externalManageRedisTokenUseCase;
private final ExternalCheckIpBlockedUseCase externalCheckIpBlockedUseCase;
private final ExternalRetrieveBlacklistIpUseCase externalRetrieveBlacklistIpUseCase;
@Override
- public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+ throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
String path = httpServletRequest.getRequestURI();
@@ -71,7 +72,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
private boolean verifyIpAddressAccess(HttpServletResponse response, String clientIpAddress) throws IOException {
- if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress) || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) {
+ if (externalRetrieveBlacklistIpUseCase.existsByIpAddress(clientIpAddress)
+ || externalCheckIpBlockedUseCase.isIpBlocked(clientIpAddress)) {
log.info("[{}] : 서비스 이용이 제한된 IP입니다.", clientIpAddress);
ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED);
return false;
@@ -79,12 +81,15 @@ private boolean verifyIpAddressAccess(HttpServletResponse response, String clien
return true;
}
- private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress) throws IOException {
+ private boolean authenticateToken(HttpServletRequest request, HttpServletResponse response, String clientIpAddress)
+ throws IOException {
String token = jwtTokenProvider.resolveToken(request);
// 토큰이 존재하고 유효한 경우
if (token != null && jwtTokenProvider.validateToken(token)) {
- RedisToken redisToken = jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token) : externalManageRedisTokenUseCase.findByAccessToken(token);
+ RedisToken redisToken =
+ jwtTokenProvider.isRefreshToken(token) ? externalManageRedisTokenUseCase.findByRefreshToken(token)
+ : externalManageRedisTokenUseCase.findByAccessToken(token);
if (redisToken == null) {
log.warn("존재하지 않는 토큰입니다.");
ResponseUtil.sendErrorResponse(response, HttpServletResponse.SC_UNAUTHORIZED);
@@ -108,7 +113,9 @@ private boolean authenticateToken(HttpServletRequest request, HttpServletRespons
private void sendSecurityAlertSlackMessage(HttpServletRequest request, RedisToken redisToken) {
if (redisToken.isAdminToken()) {
request.setAttribute("member", redisToken.getId());
- slackService.sendSecurityAlertNotification(request, SecurityAlertType.DUPLICATE_LOGIN, "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.");
+ String duplicateLoginMessage = "토큰 발급 IP와 다른 IP에서 접속하여 토큰을 삭제하였습니다.";
+ eventPublisher.publishEvent(
+ new NotificationEvent(this, SecurityAlertType.DUPLICATE_LOGIN, request, duplicateLoginMessage));
}
}
}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java
new file mode 100644
index 000000000..00117991b
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingRetrieveController.java
@@ -0,0 +1,30 @@
+package page.clab.api.global.common.notificationSetting.adapter.in.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import page.clab.api.global.common.dto.ApiResponse;
+import page.clab.api.global.common.notificationSetting.application.dto.response.NotificationSettingResponseDto;
+import page.clab.api.global.common.notificationSetting.application.port.in.RetrieveNotificationSettingUseCase;
+
+@RestController
+@RequestMapping("/api/v1/notification-settings")
+@RequiredArgsConstructor
+@Tag(name = "Notification Setting", description = "웹훅 알림 설정")
+public class NotificationSettingRetrieveController {
+
+ private final RetrieveNotificationSettingUseCase retrieveNotificationSettingUseCase;
+
+ @Operation(summary = "[S] 웹훅 알림 조회", description = "ROLE_SUPER 이상의 권한이 필요함")
+ @PreAuthorize("hasRole('SUPER')")
+ @GetMapping("")
+ public ApiResponse> getNotificationSettings() {
+ List notificationSettings = retrieveNotificationSettingUseCase.retrieveNotificationSettings();
+ return ApiResponse.success(notificationSettings);
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java
new file mode 100644
index 000000000..74023a6bc
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/in/web/NotificationSettingToggleController.java
@@ -0,0 +1,33 @@
+package page.clab.api.global.common.notificationSetting.adapter.in.web;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import page.clab.api.global.common.dto.ApiResponse;
+import page.clab.api.global.common.notificationSetting.application.dto.request.NotificationSettingToggleRequestDto;
+import page.clab.api.global.common.notificationSetting.application.port.in.ManageNotificationSettingUseCase;
+
+@RestController
+@RequestMapping("/api/v1/notification-settings")
+@RequiredArgsConstructor
+@Tag(name = "Notification Setting", description = "웹훅 알림 설정")
+public class NotificationSettingToggleController {
+
+ private final ManageNotificationSettingUseCase manageNotificationSettingUseCase;
+
+ @Operation(summary = "[S] 웹훅 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함")
+ @PreAuthorize("hasRole('SUPER')")
+ @PutMapping("")
+ public ApiResponse toggleNotificationSetting(
+ @Valid @RequestBody NotificationSettingToggleRequestDto requestDto
+ ) {
+ manageNotificationSettingUseCase.toggleNotificationSetting(requestDto.getAlertType(), requestDto.isEnabled());
+ return ApiResponse.success();
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java
new file mode 100644
index 000000000..4bae839c3
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingPersistenceAdapter.java
@@ -0,0 +1,34 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.persistence;
+
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import page.clab.api.global.common.notificationSetting.application.port.out.RetrieveNotificationSettingPort;
+import page.clab.api.global.common.notificationSetting.application.port.out.UpdateNotificationSettingPort;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.NotificationSetting;
+
+@Component
+@RequiredArgsConstructor
+public class NotificationSettingPersistenceAdapter implements
+ RetrieveNotificationSettingPort,
+ UpdateNotificationSettingPort {
+
+ private final NotificationSettingRepository repository;
+
+ @Override
+ public List findAll() {
+ return repository.findAll();
+ }
+
+ @Override
+ public Optional findByAlertType(AlertType alertType) {
+ return repository.findByAlertType(alertType);
+ }
+
+ @Override
+ public NotificationSetting save(NotificationSetting setting) {
+ return repository.save(setting);
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
similarity index 52%
rename from src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java
rename to src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
index e4cab3c38..4be9efb7b 100644
--- a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/persistence/NotificationSettingRepository.java
@@ -1,10 +1,9 @@
-package page.clab.api.global.common.slack.dao;
-
-import org.springframework.data.jpa.repository.JpaRepository;
-import page.clab.api.global.common.slack.domain.AlertType;
-import page.clab.api.global.common.slack.domain.NotificationSetting;
+package page.clab.api.global.common.notificationSetting.adapter.out.persistence;
import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.NotificationSetting;
public interface NotificationSettingRepository extends JpaRepository {
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java
new file mode 100644
index 000000000..d67f0a17f
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/AbstractWebhookClient.java
@@ -0,0 +1,17 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import jakarta.servlet.http.HttpServletRequest;
+import java.util.concurrent.CompletableFuture;
+import page.clab.api.global.common.notificationSetting.application.port.out.WebhookClient;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+
+/**
+ * {@code AbstractWebhookClient}는 Discord 및 Slack Webhook 클라이언트의 공통 인터페이스를 정의하는 추상 클래스입니다.
+ */
+public abstract class AbstractWebhookClient implements WebhookClient {
+
+ @Override
+ public abstract CompletableFuture sendMessage(String webhookUrl, AlertType alertType,
+ HttpServletRequest request,
+ Object additionalData);
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java
new file mode 100644
index 000000000..b6bfedf51
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordNotificationSender.java
@@ -0,0 +1,25 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import page.clab.api.global.common.notificationSetting.application.event.NotificationEvent;
+import page.clab.api.global.common.notificationSetting.application.port.out.NotificationSender;
+import page.clab.api.global.common.notificationSetting.domain.PlatformType;
+
+@Component
+@RequiredArgsConstructor
+public class DiscordNotificationSender implements NotificationSender {
+
+ private final DiscordWebhookClient discordWebhookClient;
+
+ @Override
+ public String getPlatformName() {
+ return PlatformType.DISCORD.getName();
+ }
+
+ @Override
+ public void sendNotification(NotificationEvent event, String webhookUrl) {
+ discordWebhookClient.sendMessage(webhookUrl, event.getAlertType(), event.getRequest(),
+ event.getAdditionalData());
+ }
+}
diff --git a/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java
new file mode 100644
index 000000000..fb1c443d3
--- /dev/null
+++ b/src/main/java/page/clab/api/global/common/notificationSetting/adapter/out/webhook/DiscordWebhookClient.java
@@ -0,0 +1,362 @@
+package page.clab.api.global.common.notificationSetting.adapter.out.webhook;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.env.Environment;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import page.clab.api.domain.hiring.application.application.dto.request.ApplicationRequestDto;
+import page.clab.api.domain.memberManagement.member.application.dto.shared.MemberLoginInfoDto;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BoardNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.BookLoanRecordNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.dto.notification.MembershipFeeNotificationInfo;
+import page.clab.api.global.common.notificationSetting.application.service.WebhookCommonService;
+import page.clab.api.global.common.notificationSetting.config.NotificationConfigProperties;
+import page.clab.api.global.common.notificationSetting.domain.AlertType;
+import page.clab.api.global.common.notificationSetting.domain.ExecutivesAlertType;
+import page.clab.api.global.common.notificationSetting.domain.GeneralAlertType;
+import page.clab.api.global.common.notificationSetting.domain.SecurityAlertType;
+import page.clab.api.global.util.HttpReqResUtil;
+
+/**
+ * {@code DiscordWebhookClient}는 다양한 알림 유형에 따라 Discord 메시지를 구성하고 전송하는 클래스입니다.
+ *
+ * 주요 기능:
+ *
+ * - {@link #sendMessage(String, AlertType, HttpServletRequest, Object)}: Discord에 알림 메시지를 비동기적으로 전송
+ * - {@link #createEmbeds(AlertType, HttpServletRequest, Object)}: 알림 유형에 따라 Discord 메시지 임베드 생성
+ * - 다양한 알림 유형에 맞는 메시지 형식을 생성하는 전용 메서드
+ *
+ *
+ * Discord Webhook API를 사용하여 웹훅 URL을 통해 메시지를 전송하며, 메시지 전송 실패 시 로그에 오류를 기록합니다.
+ *
+ * AlertType을 기반으로 여러 도메인에서 발생하는 이벤트를 Discord를 통해 모니터링할 수 있도록 지원하며,
+ * Discord 알림은 주로 서버 이벤트, 보안 경고, 신규 신청, 관리자 로그인 등의 이벤트를 다룹니다.
+ *
+ * @see HttpClient
+ * @see HttpRequest
+ * @see HttpResponse
+ */
+@Component
+@Slf4j
+public class DiscordWebhookClient extends AbstractWebhookClient {
+
+ private final HttpClient httpClient;
+ private final ObjectMapper objectMapper;
+ private final NotificationConfigProperties.CommonProperties commonProperties;
+ private final Environment environment;
+ private final WebhookCommonService webhookCommonService;
+
+ public DiscordWebhookClient(
+ NotificationConfigProperties notificationConfigProperties,
+ ObjectMapper objectMapper,
+ Environment environment,
+ WebhookCommonService webhookCommonService
+ ) {
+ this.httpClient = HttpClient.newHttpClient();
+ this.objectMapper = objectMapper;
+ this.commonProperties = notificationConfigProperties.getCommon();
+ this.environment = environment;
+ this.webhookCommonService = webhookCommonService;
+ }
+
+ /**
+ * Discord에 알림 메시지를 비동기적으로 전송합니다.
+ *
+ * @param webhookUrl 메시지를 보낼 Discord 웹훅 URL
+ * @param alertType 알림 유형을 나타내는 {@link AlertType}
+ * @param request HttpServletRequest 객체, 클라이언트 요청 정보
+ * @param additionalData 추가 데이터
+ * @return 메시지 전송 성공 여부를 나타내는 CompletableFuture
+ */
+ public CompletableFuture sendMessage(String webhookUrl, AlertType alertType,
+ HttpServletRequest request, Object additionalData) {
+ Map payload = createPayload(alertType, request, additionalData);
+
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ String jsonPayload = objectMapper.writeValueAsString(payload);
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .uri(URI.create(webhookUrl))
+ .header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
+ .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
+ .build();
+
+ HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+
+ if (response.statusCode() == HttpStatus.NO_CONTENT.value()) {
+ return true;
+ } else {
+ log.error("Discord notification failed: {}", response.body());
+ return false;
+ }
+ } catch (IOException | InterruptedException e) {
+ log.error("Failed to send Discord message: {}", e.getMessage(), e);
+ return false;
+ }
+ });
+ }
+
+ /**
+ * 알림 유형과 요청 정보, 추가 데이터를 사용하여 Discord 메시지 페이로드를 생성합니다.
+ *
+ * @param alertType 알림 유형
+ * @param request 클라이언트 요청 정보
+ * @param additionalData 추가 데이터
+ * @return 생성된 페이로드 맵
+ */
+ public Map createPayload(AlertType alertType, HttpServletRequest request, Object additionalData) {
+ List