From e29f08f7ca9530cf400e6fde583dce2dd71c1a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Thu, 20 Jun 2024 14:18:53 +0900 Subject: [PATCH 1/7] =?UTF-8?q?refactor(Slack):=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=EC=9D=84=20=EB=B0=9C=ED=96=89-=EA=B5=AC?= =?UTF-8?q?=EB=8F=85=20=ED=8C=A8=ED=84=B4=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EB=B0=8F=20=EC=95=8C=EB=A6=BC=EC=9D=84=20On/Off=20?= =?UTF-8?q?=EA=B0=80=EB=8A=A5=ED=95=9C=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/ApplicationService.java | 3 +- .../NotificationSettingService.java | 27 ++ .../slack/application/SlackService.java | 249 +--------------- .../slack/application/SlackServiceHelper.java | 268 ++++++++++++++++++ .../dao/NotificationSettingRepository.java | 13 + .../global/common/slack/domain/AlertType.java | 9 + .../slack/domain/AlertTypeConverter.java | 42 +++ .../common/slack/domain/GeneralAlertType.java | 18 ++ .../slack/domain/NotificationSetting.java | 39 +++ .../slack/domain/SecurityAlertType.java | 3 +- .../common/slack/event/NotificationEvent.java | 24 ++ .../slack/listener/NotificationListener.java | 28 ++ 12 files changed, 483 insertions(+), 240 deletions(-) create mode 100644 src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java create mode 100644 src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java create mode 100644 src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java create mode 100644 src/main/java/page/clab/api/global/common/slack/domain/AlertType.java create mode 100644 src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java create mode 100644 src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java create mode 100644 src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java create mode 100644 src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java create mode 100644 src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java diff --git a/src/main/java/page/clab/api/domain/application/application/ApplicationService.java b/src/main/java/page/clab/api/domain/application/application/ApplicationService.java index 81fa9b55d..9fbabd6e2 100644 --- a/src/main/java/page/clab/api/domain/application/application/ApplicationService.java +++ b/src/main/java/page/clab/api/domain/application/application/ApplicationService.java @@ -1,6 +1,5 @@ package page.clab.api.domain.application.application; -import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -44,7 +43,7 @@ public String createApplication(ApplicationRequestDto requestDto) { notificationService.sendNotificationToAdmins(requestDto.getStudentId() + " " + requestDto.getName() + "님이 동아리에 지원하였습니다."); - slackService.sendApplicationNotification(requestDto); + slackService.sendNewApplicationNotification(requestDto); return applicationRepository.save(application).getStudentId(); } diff --git a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java new file mode 100644 index 000000000..7a1525245 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java @@ -0,0 +1,27 @@ +package page.clab.api.global.common.slack.application; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import page.clab.api.global.common.slack.dao.NotificationSettingRepository; +import page.clab.api.global.common.slack.domain.AlertType; +import page.clab.api.global.common.slack.domain.NotificationSetting; + +@Service +@RequiredArgsConstructor +public class NotificationSettingService { + + private final NotificationSettingRepository settingRepository; + + @Transactional + public NotificationSetting getOrCreateDefaultSetting(AlertType alertType) { + return settingRepository.findByAlertType(alertType) + .orElseGet(() -> createAndSaveDefaultSetting(alertType)); + } + + private NotificationSetting createAndSaveDefaultSetting(AlertType alertType) { + NotificationSetting defaultSetting = NotificationSetting.createDefault(alertType); + return settingRepository.save(defaultSetting); + } + +} diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java index 48c76eeb3..f7d2d949c 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java @@ -1,267 +1,44 @@ package page.clab.api.global.common.slack.application; -import com.slack.api.Slack; -import com.slack.api.model.Attachment; -import static com.slack.api.model.block.Blocks.actions; -import static com.slack.api.model.block.Blocks.section; -import com.slack.api.model.block.LayoutBlock; -import static com.slack.api.model.block.composition.BlockCompositions.markdownText; -import static com.slack.api.model.block.composition.BlockCompositions.plainText; -import static com.slack.api.model.block.element.BlockElements.asElements; -import static com.slack.api.model.block.element.BlockElements.button; -import com.slack.api.webhook.Payload; -import com.slack.api.webhook.WebhookResponse; -import io.ipinfo.api.model.IPResponse; -import io.ipinfo.spring.strategies.attribute.AttributeStrategy; import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; -import org.springframework.core.env.Environment; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import page.clab.api.domain.application.dto.request.ApplicationRequestDto; import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.slack.domain.GeneralAlertType; import page.clab.api.global.common.slack.domain.SecurityAlertType; -import page.clab.api.global.config.SlackConfig; -import page.clab.api.global.util.HttpReqResUtil; +import page.clab.api.global.common.slack.event.NotificationEvent; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryMXBean; -import java.lang.management.MemoryUsage; -import java.lang.management.OperatingSystemMXBean; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.stream.Collectors; - -@Service @Slf4j +@Service +@RequiredArgsConstructor public class SlackService { - private final Slack slack; - - private final String webhookUrl; - - private final String webUrl; - - private final String apiUrl; - - private final String color; - - private final Environment environment; - - private final AttributeStrategy attributeStrategy; - - public SlackService(SlackConfig slackConfig, Environment environment, AttributeStrategy attributeStrategy) { - this.slack = slackConfig.slack(); - this.webhookUrl = slackConfig.getWebhookUrl(); - this.webUrl = slackConfig.getWebUrl(); - this.apiUrl = slackConfig.getApiUrl(); - this.color = slackConfig.getColor(); - this.environment = environment; - this.attributeStrategy = attributeStrategy; - } + private final ApplicationEventPublisher eventPublisher; public void sendServerErrorNotification(HttpServletRequest request, Exception e) { - List blocks = createErrorBlocks(request, e); - sendSlackMessageWithBlocks(blocks); + eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.SERVER_ERROR, request, e)); } public void sendSecurityAlertNotification(HttpServletRequest request, SecurityAlertType alertType, String additionalMessage) { - List blocks = createSecurityAlertBlocks(request, alertType, additionalMessage); - sendSlackMessageWithBlocks(blocks); + eventPublisher.publishEvent(new NotificationEvent(this, alertType, request, additionalMessage)); } public void sendAdminLoginNotification(HttpServletRequest request, Member loginMember) { - List blocks = createAdminLoginBlocks(request, loginMember); - sendSlackMessageWithBlocks(blocks); + eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.ADMIN_LOGIN, request, loginMember)); } - public void sendApplicationNotification(ApplicationRequestDto applicationRequestDto) { - List blocks = createApplicationBlocks(applicationRequestDto); - sendSlackMessageWithBlocks(blocks); + public void sendNewApplicationNotification(ApplicationRequestDto applicationRequestDto) { + eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.APPLICATION_CREATED, null, applicationRequestDto)); } @EventListener(ContextRefreshedEvent.class) public void sendServerStartNotification() { - List blocks = createServerStartBlocks(); - sendSlackMessageWithBlocks(blocks); - } - - private CompletableFuture sendSlackMessageWithBlocks(List blocks) { - return CompletableFuture.supplyAsync(() -> { - Payload payload = Payload.builder() - .blocks(List.of(blocks.getFirst())) - .attachments(Collections.singletonList( - Attachment.builder() - .color(color) - .blocks(blocks.subList(1, blocks.size())) - .build() - )).build(); - try { - WebhookResponse response = slack.send(webhookUrl, payload); - if (response.getCode() == 200) { - return true; - } else { - log.error("Slack notification failed: {}", response.getMessage()); - return false; - } - } catch (IOException e) { - log.error("Failed to send Slack message: {}", e.getMessage(), e); - return false; - } - }); - } - - private List createErrorBlocks(HttpServletRequest request, Exception e) { - String httpMethod = request.getMethod(); - String requestUrl = request.getRequestURI(); - String username = getUsername(); - - String errorMessage = e.getMessage() == null ? "No error message provided" : e.getMessage(); - String detailedMessage = extractMessageAfterException(errorMessage); - log.error("Server Error: {}", detailedMessage); - return Arrays.asList( - section(section -> section.text(markdownText(":firecracker: *Server Error*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + username), - markdownText("*Endpoint:*\n[" + httpMethod + "] " + requestUrl) - ))), - section(section -> section.text(markdownText("*Error Message:*\n" + detailedMessage))), - section(section -> section.text(markdownText("*Stack Trace:*\n```" + getStackTraceSummary(e) + "```"))) - ); - } - - private List createSecurityAlertBlocks(HttpServletRequest request, SecurityAlertType alertType, String additionalMessage) { - String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - String requestUrl = request.getRequestURI(); - String username = getUsername(request); - String location = getLocation(request); - - return Arrays.asList( - section(section -> section.text(markdownText(String.format(":imp: *%s*", alertType.getTitle())))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + username), - markdownText("*IP Address:*\n" + clientIpAddress), - markdownText("*Location:*\n" + location), - markdownText("*Endpoint:*\n" + requestUrl) - ))), - section(section -> section.text(markdownText("*Details:*\n" + alertType.getDefaultMessage() + "\n" + additionalMessage))) - ); - } - - private List createAdminLoginBlocks(HttpServletRequest request, Member loginMember) { - String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); - String location = getLocation(request); - - return Arrays.asList( - section(section -> section.text(markdownText(String.format(":mechanic: *%s Login*", loginMember.getRole().getDescription())))), - section(section -> section.fields(Arrays.asList( - markdownText("*User:*\n" + loginMember.getId() + " " + loginMember.getName()), - markdownText("*IP Address:*\n" + clientIpAddress), - markdownText("*Location:*\n" + location) - ))) - ); - } - - private List createApplicationBlocks(ApplicationRequestDto requestDto) { - List blocks = new ArrayList<>(); - - blocks.add(section(section -> section.text(markdownText(":sparkles: *New Application*")))); - blocks.add(section(section -> section.fields(Arrays.asList( - markdownText("*Type:*\n" + requestDto.getApplicationType().getDescription()), - markdownText("*Student ID:*\n" + requestDto.getStudentId()), - markdownText("*Name:*\n" + requestDto.getName()), - markdownText("*Grade:*\n" + requestDto.getGrade() + "학년"), - markdownText("*Interests:*\n" + requestDto.getInterests()) - )))); - - if (requestDto.getGithubUrl() != null && !requestDto.getGithubUrl().isEmpty()) { - blocks.add(actions(actions -> actions.elements(asElements( - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Github"))) - .url(requestDto.getGithubUrl()) - .actionId("click_github")) - )))); - } - return blocks; - } - - private List createServerStartBlocks() { - String osInfo = System.getProperty("os.name") + " " + System.getProperty("os.version"); - String jdkVersion = System.getProperty("java.version"); - - OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); - int availableProcessors = osBean.getAvailableProcessors(); - double systemLoadAverage = osBean.getSystemLoadAverage(); - double cpuUsage = ((systemLoadAverage / availableProcessors) * 100); - - MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); - MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); - String memoryInfo = formatMemoryUsage(heapMemoryUsage); - - return Arrays.asList( - section(section -> section.text(markdownText("*:battery: Server Started*"))), - section(section -> section.fields(Arrays.asList( - markdownText("*Environment:* \n" + environment.getProperty("spring.profiles.active")), - markdownText("*OS:* \n" + osInfo), - markdownText("*JDK Version:* \n" + jdkVersion), - markdownText("*CPU Usage:* \n" + String.format("%.2f%%", cpuUsage)), - markdownText("*Memory Usage:* \n" + memoryInfo) - ))), - actions(actions -> actions.elements(asElements( - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Web"))) - .url(webUrl) - .value("click_web")), - button(b -> b.text(plainText(pt -> pt.emoji(true).text("Swagger"))) - .url(apiUrl) - .value("click_swagger")) - ))) - ); - } - - private String extractMessageAfterException(String message) { - String exceptionIndicator = "Exception:"; - int exceptionIndex = message.indexOf(exceptionIndicator); - return exceptionIndex == -1 ? message : message.substring(exceptionIndex + exceptionIndicator.length()).trim(); - } - - private String getStackTraceSummary(Exception e) { - return Arrays.stream(e.getStackTrace()) - .limit(10) - .map(StackTraceElement::toString) - .collect(Collectors.joining("\n")); - } - - private String formatMemoryUsage(MemoryUsage memoryUsage) { - long usedMemory = memoryUsage.getUsed() / (1024 * 1024); - long maxMemory = memoryUsage.getMax() / (1024 * 1024); - return String.format("%dMB / %dMB (%.2f%%)", usedMemory, maxMemory, ((double) usedMemory / maxMemory) * 100); - } - - private static String getUsername() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); - } - - private @NotNull String getUsername(HttpServletRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return Optional.ofNullable(request.getAttribute("member")) - .map(Object::toString) - .orElseGet(() -> Optional.ofNullable(authentication) - .map(Authentication::getName) - .orElse("anonymous")); - } - - private @NotNull String getLocation(HttpServletRequest request) { - IPResponse ipResponse = attributeStrategy.getAttribute(request); - return ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.SERVER_START, null, null)); } } diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java new file mode 100644 index 000000000..7a9c7cefe --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java @@ -0,0 +1,268 @@ +package page.clab.api.global.common.slack.application; + +import com.slack.api.Slack; +import com.slack.api.model.Attachment; +import static com.slack.api.model.block.Blocks.actions; +import static com.slack.api.model.block.Blocks.section; +import com.slack.api.model.block.LayoutBlock; +import static com.slack.api.model.block.composition.BlockCompositions.markdownText; +import static com.slack.api.model.block.composition.BlockCompositions.plainText; +import static com.slack.api.model.block.element.BlockElements.asElements; +import static com.slack.api.model.block.element.BlockElements.button; +import com.slack.api.webhook.Payload; +import com.slack.api.webhook.WebhookResponse; +import io.ipinfo.api.model.IPResponse; +import io.ipinfo.spring.strategies.attribute.AttributeStrategy; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.env.Environment; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import page.clab.api.domain.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.member.domain.Member; +import page.clab.api.global.common.slack.domain.AlertType; +import page.clab.api.global.common.slack.domain.GeneralAlertType; +import page.clab.api.global.common.slack.domain.SecurityAlertType; +import page.clab.api.global.config.SlackConfig; +import page.clab.api.global.util.HttpReqResUtil; + +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryMXBean; +import java.lang.management.MemoryUsage; +import java.lang.management.OperatingSystemMXBean; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +@Component +@Slf4j +public class SlackServiceHelper { + + private final Slack slack; + private final String webhookUrl; + private final String webUrl; + private final String apiUrl; + private final String color; + private final Environment environment; + + private final AttributeStrategy attributeStrategy; + + public SlackServiceHelper(SlackConfig slackConfig, Environment environment, AttributeStrategy attributeStrategy) { + this.slack = slackConfig.slack(); + this.webhookUrl = slackConfig.getWebhookUrl(); + this.webUrl = slackConfig.getWebUrl(); + this.apiUrl = slackConfig.getApiUrl(); + this.color = slackConfig.getColor(); + this.environment = environment; + this.attributeStrategy = attributeStrategy; + } + + public CompletableFuture sendSlackMessage(AlertType alertType, HttpServletRequest request, Object additionalData) { + List blocks = createBlocks(alertType, request, additionalData); + + return CompletableFuture.supplyAsync(() -> { + Payload payload = Payload.builder() + .blocks(List.of(blocks.getFirst())) + .attachments(Collections.singletonList( + Attachment.builder() + .color(color) + .blocks(blocks.subList(1, blocks.size())) + .build() + )).build(); + try { + WebhookResponse response = slack.send(webhookUrl, payload); + if (response.getCode() == 200) { + return true; + } else { + log.error("Slack notification failed: {}", response.getMessage()); + return false; + } + } catch (IOException e) { + log.error("Failed to send Slack message: {}", e.getMessage(), e); + return false; + } + }); + } + + public List createBlocks(AlertType alertType, HttpServletRequest request, Object additionalData) { + if (alertType instanceof SecurityAlertType) { + return createSecurityAlertBlocks(request, alertType, additionalData.toString()); + } else if (alertType instanceof GeneralAlertType) { + switch ((GeneralAlertType) alertType) { + case ADMIN_LOGIN: + if (additionalData instanceof Member) { + return createAdminLoginBlocks(request, (Member) additionalData); + } + break; + case APPLICATION_CREATED: + if (additionalData instanceof ApplicationRequestDto) { + return createApplicationBlocks((ApplicationRequestDto) additionalData); + } + break; + case SERVER_START: + return createServerStartBlocks(); + case SERVER_ERROR: + if (additionalData instanceof Exception) { + return createErrorBlocks(request, (Exception) additionalData); + } + break; + default: + log.error("Unknown alert type: {}", alertType); + return List.of(); + } + } + return List.of(); + } + + private List createErrorBlocks(HttpServletRequest request, Exception e) { + String httpMethod = request.getMethod(); + String requestUrl = request.getRequestURI(); + String username = getUsername(); + + String errorMessage = e.getMessage() == null ? "No error message provided" : e.getMessage(); + String detailedMessage = extractMessageAfterException(errorMessage); + log.error("Server Error: {}", detailedMessage); + return Arrays.asList( + section(section -> section.text(markdownText(":firecracker: *Server Error*"))), + section(section -> section.fields(Arrays.asList( + markdownText("*User:*\n" + username), + markdownText("*Endpoint:*\n[" + httpMethod + "] " + requestUrl) + ))), + section(section -> section.text(markdownText("*Error Message:*\n" + detailedMessage))), + section(section -> section.text(markdownText("*Stack Trace:*\n```" + getStackTraceSummary(e) + "```"))) + ); + } + + private List createSecurityAlertBlocks(HttpServletRequest request, AlertType alertType, String additionalMessage) { + String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String requestUrl = request.getRequestURI(); + String username = getUsername(request); + String location = getLocation(request); + + return Arrays.asList( + section(section -> section.text(markdownText(String.format(":imp: *%s*", alertType.getTitle())))), + section(section -> section.fields(Arrays.asList( + markdownText("*User:*\n" + username), + markdownText("*IP Address:*\n" + clientIpAddress), + markdownText("*Location:*\n" + location), + markdownText("*Endpoint:*\n" + requestUrl) + ))), + section(section -> section.text(markdownText("*Details:*\n" + alertType.getDefaultMessage() + "\n" + additionalMessage))) + ); + } + + private List createAdminLoginBlocks(HttpServletRequest request, Member loginMember) { + String clientIpAddress = HttpReqResUtil.getClientIpAddressIfServletRequestExist(); + String location = getLocation(request); + + return Arrays.asList( + section(section -> section.text(markdownText(String.format(":mechanic: *%s Login*", loginMember.getRole().getDescription())))), + section(section -> section.fields(Arrays.asList( + markdownText("*User:*\n" + loginMember.getId() + " " + loginMember.getName()), + markdownText("*IP Address:*\n" + clientIpAddress), + markdownText("*Location:*\n" + location) + ))) + ); + } + + private List createApplicationBlocks(ApplicationRequestDto requestDto) { + List blocks = new ArrayList<>(); + + blocks.add(section(section -> section.text(markdownText(":sparkles: *New Application*")))); + blocks.add(section(section -> section.fields(Arrays.asList( + markdownText("*Type:*\n" + requestDto.getApplicationType().getDescription()), + markdownText("*Student ID:*\n" + requestDto.getStudentId()), + markdownText("*Name:*\n" + requestDto.getName()), + markdownText("*Grade:*\n" + requestDto.getGrade() + "학년"), + markdownText("*Interests:*\n" + requestDto.getInterests()) + )))); + + if (requestDto.getGithubUrl() != null && !requestDto.getGithubUrl().isEmpty()) { + blocks.add(actions(actions -> actions.elements(asElements( + button(b -> b.text(plainText(pt -> pt.emoji(true).text("Github"))) + .url(requestDto.getGithubUrl()) + .actionId("click_github")) + )))); + } + return blocks; + } + + private List createServerStartBlocks() { + String osInfo = System.getProperty("os.name") + " " + System.getProperty("os.version"); + String jdkVersion = System.getProperty("java.version"); + + OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + int availableProcessors = osBean.getAvailableProcessors(); + double systemLoadAverage = osBean.getSystemLoadAverage(); + double cpuUsage = ((systemLoadAverage / availableProcessors) * 100); + + MemoryMXBean memoryMXBean = ManagementFactory.getMemoryMXBean(); + MemoryUsage heapMemoryUsage = memoryMXBean.getHeapMemoryUsage(); + String memoryInfo = formatMemoryUsage(heapMemoryUsage); + + return Arrays.asList( + section(section -> section.text(markdownText("*:battery: Server Started*"))), + section(section -> section.fields(Arrays.asList( + markdownText("*Environment:* \n" + environment.getProperty("spring.profiles.active")), + markdownText("*OS:* \n" + osInfo), + markdownText("*JDK Version:* \n" + jdkVersion), + markdownText("*CPU Usage:* \n" + String.format("%.2f%%", cpuUsage)), + markdownText("*Memory Usage:* \n" + memoryInfo) + ))), + actions(actions -> actions.elements(asElements( + button(b -> b.text(plainText(pt -> pt.emoji(true).text("Web"))) + .url(webUrl) + .value("click_web")), + button(b -> b.text(plainText(pt -> pt.emoji(true).text("Swagger"))) + .url(apiUrl) + .value("click_swagger")) + ))) + ); + } + + private String extractMessageAfterException(String message) { + String exceptionIndicator = "Exception:"; + int exceptionIndex = message.indexOf(exceptionIndicator); + return exceptionIndex == -1 ? message : message.substring(exceptionIndex + exceptionIndicator.length()).trim(); + } + + private String getStackTraceSummary(Exception e) { + return Arrays.stream(e.getStackTrace()) + .limit(10) + .map(StackTraceElement::toString) + .collect(Collectors.joining("\n")); + } + + private String formatMemoryUsage(MemoryUsage memoryUsage) { + long usedMemory = memoryUsage.getUsed() / (1024 * 1024); + long maxMemory = memoryUsage.getMax() / (1024 * 1024); + return String.format("%dMB / %dMB (%.2f%%)", usedMemory, maxMemory, ((double) usedMemory / maxMemory) * 100); + } + + private static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return (authentication == null || authentication.getName() == null) ? "anonymous" : authentication.getName(); + } + + private @NotNull String getUsername(HttpServletRequest request) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return Optional.ofNullable(request.getAttribute("member")) + .map(Object::toString) + .orElseGet(() -> Optional.ofNullable(authentication) + .map(Authentication::getName) + .orElse("anonymous")); + } + + private @NotNull String getLocation(HttpServletRequest request) { + IPResponse ipResponse = attributeStrategy.getAttribute(request); + return ipResponse == null ? "Unknown" : ipResponse.getCountryName() + ", " + ipResponse.getCity(); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java b/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java new file mode 100644 index 000000000..a66b8d5cd --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/dao/NotificationSettingRepository.java @@ -0,0 +1,13 @@ +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; + +import java.util.Optional; + +public interface NotificationSettingRepository extends JpaRepository { + + Optional findByAlertType(AlertType alertType); + +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java new file mode 100644 index 000000000..971d10449 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/domain/AlertType.java @@ -0,0 +1,9 @@ +package page.clab.api.global.common.slack.domain; + +public interface AlertType { + + String getTitle(); + + String getDefaultMessage(); + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java new file mode 100644 index 000000000..b860706c2 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java @@ -0,0 +1,42 @@ +package page.clab.api.global.common.slack.domain; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.HashMap; +import java.util.Map; + +@Converter(autoApply = true) +public class AlertTypeConverter implements AttributeConverter { + + private static final Map CACHE = new HashMap<>(); + + static { + for (GeneralAlertType type : GeneralAlertType.values()) { + CACHE.put(type.getTitle(), type); + } + for (SecurityAlertType type : SecurityAlertType.values()) { + CACHE.put(type.getTitle(), type); + } + } + + @Override + public String convertToDatabaseColumn(AlertType alertType) { + if (alertType == null) { + return null; + } + return alertType.getTitle(); + } + + @Override + public AlertType convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isEmpty()) { + return null; + } + AlertType alertType = CACHE.get(dbData); + if (alertType == null) { + throw new IllegalArgumentException("Unknown alert type: " + dbData); + } + return alertType; + } +} diff --git a/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java new file mode 100644 index 000000000..d653e3eea --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java @@ -0,0 +1,18 @@ +package page.clab.api.global.common.slack.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum GeneralAlertType implements AlertType { + + ADMIN_LOGIN("관리자 로그인", "Admin login."), + APPLICATION_CREATED("새 지원서", "New application has been submitted."), + SERVER_START("서버 시작", "Server has been started."), + SERVER_ERROR("서버 에러", "Server error occurred."); + + private final String title; + private final String defaultMessage; + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java b/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java new file mode 100644 index 000000000..cf691c512 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java @@ -0,0 +1,39 @@ +package page.clab.api.global.common.slack.domain; + +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@Builder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Entity +public class NotificationSetting { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Convert(converter = AlertTypeConverter.class) + private AlertType alertType; + + private boolean enabled; + + public static NotificationSetting createDefault(AlertType alertType) { + return NotificationSetting.builder() + .alertType(alertType) + .enabled(true) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java index 587de828d..022fa1e32 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java @@ -5,7 +5,7 @@ @Getter @AllArgsConstructor -public enum SecurityAlertType { +public enum SecurityAlertType implements AlertType { ABNORMAL_ACCESS("비정상적인 접근", "Unexpected access pattern detected."), REPEATED_LOGIN_FAILURES("지속된 로그인 실패", "Multiple consecutive failed login attempts."), @@ -20,5 +20,4 @@ public enum SecurityAlertType { private final String title; private final String defaultMessage; - } diff --git a/src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java b/src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java new file mode 100644 index 000000000..5ac93ee69 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/event/NotificationEvent.java @@ -0,0 +1,24 @@ +package page.clab.api.global.common.slack.event; + +import jakarta.servlet.http.HttpServletRequest; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; +import page.clab.api.global.common.slack.domain.AlertType; + +@Getter +public class NotificationEvent extends ApplicationEvent { + + private final AlertType alertType; + + private final HttpServletRequest request; + + private final Object additionalData; + + public NotificationEvent(Object source, AlertType alertType, HttpServletRequest request, Object additionalData) { + super(source); + this.alertType = alertType; + this.request = request; + this.additionalData = additionalData; + } + +} diff --git a/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java b/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java new file mode 100644 index 000000000..423030969 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java @@ -0,0 +1,28 @@ +package page.clab.api.global.common.slack.listener; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import page.clab.api.global.common.slack.application.NotificationSettingService; +import page.clab.api.global.common.slack.application.SlackServiceHelper; +import page.clab.api.global.common.slack.domain.AlertType; +import page.clab.api.global.common.slack.domain.NotificationSetting; +import page.clab.api.global.common.slack.event.NotificationEvent; + +@Component +@RequiredArgsConstructor +public class NotificationListener { + + private final NotificationSettingService settingService; + private final SlackServiceHelper slackServiceHelper; + + @EventListener + public void handleNotificationEvent(NotificationEvent event) { + AlertType alertType = event.getAlertType(); + NotificationSetting setting = settingService.getOrCreateDefaultSetting(alertType); + + if (setting.isEnabled()) { + slackServiceHelper.sendSlackMessage(alertType, event.getRequest(), event.getAdditionalData()); + } + } +} \ No newline at end of file From 73fb757956ac3c52bf47fd4a1604241f1f9775a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Thu, 20 Jun 2024 14:37:51 +0900 Subject: [PATCH 2/7] =?UTF-8?q?feat(Slack):=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/NotificationSettingController.java | 34 +++++++++++++++++++ .../NotificationSettingService.java | 11 ++++++ .../slack/domain/AlertTypeResolver.java | 23 +++++++++++++ .../slack/domain/NotificationSetting.java | 4 +++ .../NotificationSettingUpdateRequestDto.java | 20 +++++++++++ .../exception/AlertTypeNotFoundException.java | 9 +++++ src/main/resources/messages.properties | 2 ++ 7 files changed, 103 insertions(+) create mode 100644 src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java create mode 100644 src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java create mode 100644 src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java create mode 100644 src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java diff --git a/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java b/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java new file mode 100644 index 000000000..bb5d267e3 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java @@ -0,0 +1,34 @@ +package page.clab.api.global.common.slack.api; + +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.annotation.Secured; +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.slack.application.NotificationSettingService; +import page.clab.api.global.common.slack.dto.request.NotificationSettingUpdateRequestDto; + +@RestController +@RequestMapping("/api/v1/notification-settings") +@RequiredArgsConstructor +@Tag(name = "Notification Setting", description = "알림 설정") +public class NotificationSettingController { + + private final NotificationSettingService notificationSettingService; + + @Operation(summary = "[S] 슬랙 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + @PutMapping("") + public ApiResponse updateNotificationSetting( + @Valid @RequestBody NotificationSettingUpdateRequestDto requestDto + ) { + notificationSettingService.updateNotificationSetting(requestDto.getAlertType(), requestDto.isEnabled()); + return ApiResponse.success(); + } + +} diff --git a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java index 7a1525245..7a56e3bed 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java @@ -5,12 +5,15 @@ import org.springframework.transaction.annotation.Transactional; import page.clab.api.global.common.slack.dao.NotificationSettingRepository; import page.clab.api.global.common.slack.domain.AlertType; +import page.clab.api.global.common.slack.domain.AlertTypeResolver; import page.clab.api.global.common.slack.domain.NotificationSetting; @Service @RequiredArgsConstructor public class NotificationSettingService { + private final AlertTypeResolver alertTypeResolver; + private final NotificationSettingRepository settingRepository; @Transactional @@ -24,4 +27,12 @@ private NotificationSetting createAndSaveDefaultSetting(AlertType alertType) { return settingRepository.save(defaultSetting); } + @Transactional + public void updateNotificationSetting(String alertTypeName, boolean enabled) { + AlertType alertType = alertTypeResolver.resolve(alertTypeName); + NotificationSetting setting = getOrCreateDefaultSetting(alertType); + setting.updateEnabled(enabled); + settingRepository.save(setting); + } + } diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java new file mode 100644 index 000000000..a0c8d101d --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeResolver.java @@ -0,0 +1,23 @@ +package page.clab.api.global.common.slack.domain; + +import org.springframework.stereotype.Service; +import page.clab.api.global.common.slack.exception.AlertTypeNotFoundException; + +@Service +public class AlertTypeResolver { + + public AlertType resolve(String alertTypeName) { + for (GeneralAlertType type : GeneralAlertType.values()) { + if (type.getTitle().equals(alertTypeName)) { + return type; + } + } + for (SecurityAlertType type : SecurityAlertType.values()) { + if (type.getTitle().equals(alertTypeName)) { + return type; + } + } + throw new AlertTypeNotFoundException(alertTypeName); + } + +} \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java b/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java index cf691c512..a6b8ff8da 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/NotificationSetting.java @@ -36,4 +36,8 @@ public static NotificationSetting createDefault(AlertType alertType) { .build(); } + public void updateEnabled(boolean enabled) { + this.enabled = enabled; + } + } \ No newline at end of file diff --git a/src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java b/src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java new file mode 100644 index 000000000..ed1343b02 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/dto/request/NotificationSettingUpdateRequestDto.java @@ -0,0 +1,20 @@ +package page.clab.api.global.common.slack.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class NotificationSettingUpdateRequestDto { + + @NotNull(message = "{notNull.notificationSetting.alertType}") + @Schema(description = "알림 타입", example = "서버 시작") + private String alertType; + + @NotNull(message = "{notNull.notificationSetting.enabled}") + @Schema(description = "알림 활성화 여부", example = "true") + private boolean enabled; + +} diff --git a/src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java b/src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java new file mode 100644 index 000000000..e7ea55bf6 --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/exception/AlertTypeNotFoundException.java @@ -0,0 +1,9 @@ +package page.clab.api.global.common.slack.exception; + +public class AlertTypeNotFoundException extends RuntimeException { + + public AlertTypeNotFoundException(String alertTypeName) { + super("Unknown alert type: " + alertTypeName); + } + +} \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index d42cc8b92..6970588fd 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -157,6 +157,8 @@ notNull.news.source=출처는 필수 입력 항목입니다. notNull.news.date=날짜는 필수 입력 항목입니다. notNull.notification.memberId=학번은 필수 입력 항목입니다. notNull.notification.content=내용은 필수 입력 항목입니다. +notNull.notificationSetting.alertType=알림 타입은 필수 입력 항목입니다. +notNull.notificationSetting.enabled=알림 활성화 여부는 필수 입력 항목입니다. notNull.product.name=제품명은 필수 입력 항목입니다. notNull.product.description=설명은 필수 입력 항목입니다. notNull.recruitment.startDate=시작 날짜는 필수 입력 항목입니다. From c324c4acc7e5744f886e36a1450beb567e3031d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Thu, 20 Jun 2024 14:44:07 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat(Slack):=20=EC=8A=AC=EB=9E=99=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/NotificationSettingController.java | 12 +++++++++ .../NotificationSettingService.java | 26 +++++++++++++------ .../NotificationSettingResponseDto.java | 22 ++++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java diff --git a/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java b/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java index bb5d267e3..e9757bb3d 100644 --- a/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java +++ b/src/main/java/page/clab/api/global/common/slack/api/NotificationSettingController.java @@ -5,6 +5,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -12,6 +13,9 @@ import page.clab.api.global.common.dto.ApiResponse; import page.clab.api.global.common.slack.application.NotificationSettingService; import page.clab.api.global.common.slack.dto.request.NotificationSettingUpdateRequestDto; +import page.clab.api.global.common.slack.dto.response.NotificationSettingResponseDto; + +import java.util.List; @RestController @RequestMapping("/api/v1/notification-settings") @@ -21,6 +25,14 @@ public class NotificationSettingController { private final NotificationSettingService notificationSettingService; + @Operation(summary = "[S] 슬랙 알림 조회", description = "ROLE_SUPER 이상의 권한이 필요함") + @Secured({"ROLE_SUPER"}) + @GetMapping("") + public ApiResponse getNotificationSettings() { + List notificationSettings = notificationSettingService.getNotificationSettings(); + return ApiResponse.success(notificationSettings); + } + @Operation(summary = "[S] 슬랙 알림 설정 변경", description = "ROLE_SUPER 이상의 권한이 필요함") @Secured({"ROLE_SUPER"}) @PutMapping("") diff --git a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java index 7a56e3bed..16aad7c4d 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/NotificationSettingService.java @@ -7,6 +7,9 @@ import page.clab.api.global.common.slack.domain.AlertType; import page.clab.api.global.common.slack.domain.AlertTypeResolver; import page.clab.api.global.common.slack.domain.NotificationSetting; +import page.clab.api.global.common.slack.dto.response.NotificationSettingResponseDto; + +import java.util.List; @Service @RequiredArgsConstructor @@ -16,6 +19,21 @@ public class NotificationSettingService { private final NotificationSettingRepository settingRepository; + @Transactional(readOnly = true) + public List getNotificationSettings() { + return settingRepository.findAll().stream() + .map(NotificationSettingResponseDto::toDto) + .toList(); + } + + @Transactional + public void updateNotificationSetting(String alertTypeName, boolean enabled) { + AlertType alertType = alertTypeResolver.resolve(alertTypeName); + NotificationSetting setting = getOrCreateDefaultSetting(alertType); + setting.updateEnabled(enabled); + settingRepository.save(setting); + } + @Transactional public NotificationSetting getOrCreateDefaultSetting(AlertType alertType) { return settingRepository.findByAlertType(alertType) @@ -27,12 +45,4 @@ private NotificationSetting createAndSaveDefaultSetting(AlertType alertType) { return settingRepository.save(defaultSetting); } - @Transactional - public void updateNotificationSetting(String alertTypeName, boolean enabled) { - AlertType alertType = alertTypeResolver.resolve(alertTypeName); - NotificationSetting setting = getOrCreateDefaultSetting(alertType); - setting.updateEnabled(enabled); - settingRepository.save(setting); - } - } diff --git a/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java b/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java new file mode 100644 index 000000000..25bb27b3f --- /dev/null +++ b/src/main/java/page/clab/api/global/common/slack/dto/response/NotificationSettingResponseDto.java @@ -0,0 +1,22 @@ +package page.clab.api.global.common.slack.dto.response; + +import lombok.Builder; +import lombok.Getter; +import page.clab.api.global.common.slack.domain.NotificationSetting; + +@Getter +@Builder +public class NotificationSettingResponseDto { + + private String alertType; + + private boolean enabled; + + public static NotificationSettingResponseDto toDto(NotificationSetting setting) { + return NotificationSettingResponseDto.builder() + .alertType(setting.getAlertType().getTitle()) + .enabled(setting.isEnabled()) + .build(); + } + +} From c835f3588c0ac6bb8683994c286f06029bf20ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Thu, 20 Jun 2024 14:46:36 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor(Slack):=20AlertType=20=EC=98=88?= =?UTF-8?q?=EC=99=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/global/common/slack/domain/AlertTypeConverter.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java index b860706c2..c96092da1 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/AlertTypeConverter.java @@ -2,6 +2,7 @@ import jakarta.persistence.AttributeConverter; import jakarta.persistence.Converter; +import page.clab.api.global.common.slack.exception.AlertTypeNotFoundException; import java.util.HashMap; import java.util.Map; @@ -35,7 +36,7 @@ public AlertType convertToEntityAttribute(String dbData) { } AlertType alertType = CACHE.get(dbData); if (alertType == null) { - throw new IllegalArgumentException("Unknown alert type: " + dbData); + throw new AlertTypeNotFoundException(dbData); } return alertType; } From 33cae77b8f4672fef95de39829801804127f21b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Thu, 20 Jun 2024 14:48:45 +0900 Subject: [PATCH 5/7] =?UTF-8?q?refactor(Slack):=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=BB=A8=EB=B2=A4=EC=85=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clab/api/global/common/slack/domain/SecurityAlertType.java | 1 + .../api/global/common/slack/listener/NotificationListener.java | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java index 022fa1e32..a0921ba64 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/SecurityAlertType.java @@ -20,4 +20,5 @@ public enum SecurityAlertType implements AlertType { private final String title; private final String defaultMessage; + } diff --git a/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java b/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java index 423030969..9dac2e8fe 100644 --- a/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java +++ b/src/main/java/page/clab/api/global/common/slack/listener/NotificationListener.java @@ -14,6 +14,7 @@ public class NotificationListener { private final NotificationSettingService settingService; + private final SlackServiceHelper slackServiceHelper; @EventListener @@ -25,4 +26,5 @@ public void handleNotificationEvent(NotificationEvent event) { slackServiceHelper.sendSlackMessage(alertType, event.getRequest(), event.getAdditionalData()); } } + } \ No newline at end of file From 4575174a8a1398898802d6d1390d1ba6869b2dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Fri, 21 Jun 2024 15:33:32 +0900 Subject: [PATCH 6/7] =?UTF-8?q?feat(Slack):=20=EC=83=88=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=95=8C=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/application/BoardService.java | 4 +++ .../clab/api/domain/board/domain/Board.java | 1 + .../slack/application/SlackService.java | 5 ++++ .../slack/application/SlackServiceHelper.java | 25 +++++++++++++++++++ .../common/slack/domain/GeneralAlertType.java | 1 + 5 files changed, 36 insertions(+) diff --git a/src/main/java/page/clab/api/domain/board/application/BoardService.java b/src/main/java/page/clab/api/domain/board/application/BoardService.java index 5462632f4..4c9427fda 100644 --- a/src/main/java/page/clab/api/domain/board/application/BoardService.java +++ b/src/main/java/page/clab/api/domain/board/application/BoardService.java @@ -24,6 +24,7 @@ import page.clab.api.global.common.dto.PagedResponseDto; 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.exception.NotFoundException; import page.clab.api.global.exception.PermissionDeniedException; import page.clab.api.global.validation.ValidationService; @@ -43,6 +44,8 @@ public class BoardService { private final ValidationService validationService; + private final SlackService slackService; + private final BoardRepository boardRepository; private final BoardLikeRepository boardLikeRepository; @@ -59,6 +62,7 @@ public String createBoard(BoardRequestDto requestDto) throws PermissionDeniedExc if (board.shouldNotifyForNewBoard()) { notificationService.sendNotificationToMember(currentMember, "[" + board.getTitle() + "] 새로운 공지사항이 등록되었습니다."); } + slackService.sendNewBoardNotification(board); return boardRepository.save(board).getCategory().getKey(); } diff --git a/src/main/java/page/clab/api/domain/board/domain/Board.java b/src/main/java/page/clab/api/domain/board/domain/Board.java index 99847c588..44db8437b 100644 --- a/src/main/java/page/clab/api/domain/board/domain/Board.java +++ b/src/main/java/page/clab/api/domain/board/domain/Board.java @@ -69,6 +69,7 @@ public class Board extends BaseEntity { private String imageUrl; + @Getter @Column(nullable = false) private boolean wantAnonymous; diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java index f7d2d949c..8963b6f54 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackService.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackService.java @@ -8,6 +8,7 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import page.clab.api.domain.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.member.domain.Member; import page.clab.api.global.common.slack.domain.GeneralAlertType; import page.clab.api.global.common.slack.domain.SecurityAlertType; @@ -36,6 +37,10 @@ public void sendNewApplicationNotification(ApplicationRequestDto applicationRequ eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.APPLICATION_CREATED, null, applicationRequestDto)); } + public void sendNewBoardNotification(Board board) { + eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.BOARD_CREATED, null, board)); + } + @EventListener(ContextRefreshedEvent.class) public void sendServerStartNotification() { eventPublisher.publishEvent(new NotificationEvent(this, GeneralAlertType.SERVER_START, null, null)); diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java index 7a9c7cefe..6dc40941b 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java @@ -21,6 +21,7 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import page.clab.api.domain.application.dto.request.ApplicationRequestDto; +import page.clab.api.domain.board.domain.Board; import page.clab.api.domain.member.domain.Member; import page.clab.api.global.common.slack.domain.AlertType; import page.clab.api.global.common.slack.domain.GeneralAlertType; @@ -46,10 +47,15 @@ public class SlackServiceHelper { private final Slack slack; + private final String webhookUrl; + private final String webUrl; + private final String apiUrl; + private final String color; + private final Environment environment; private final AttributeStrategy attributeStrategy; @@ -106,6 +112,11 @@ public List createBlocks(AlertType alertType, HttpServletRequest re return createApplicationBlocks((ApplicationRequestDto) additionalData); } break; + case BOARD_CREATED: + if (additionalData instanceof Board) { + return createBoardBlocks((Board) additionalData); + } + break; case SERVER_START: return createServerStartBlocks(); case SERVER_ERROR: @@ -194,6 +205,20 @@ private List createApplicationBlocks(ApplicationRequestDto requestD return blocks; } + private List createBoardBlocks(Board board) { + List blocks = new ArrayList<>(); + String username = board.isWantAnonymous() ? + board.getNickname() : board.getMember().getId() + " " + board.getMember().getName(); + + blocks.add(section(section -> section.text(markdownText(":mag: *New Board*")))); + blocks.add(section(section -> section.fields(Arrays.asList( + markdownText("*Title:*\n" + board.getTitle()), + markdownText("*Category:*\n" + board.getCategory().getDescription()), + markdownText("*User:*\n" + username) + )))); + return blocks; + } + private List createServerStartBlocks() { String osInfo = System.getProperty("os.name") + " " + System.getProperty("os.version"); String jdkVersion = System.getProperty("java.version"); diff --git a/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java b/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java index d653e3eea..42444236e 100644 --- a/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java +++ b/src/main/java/page/clab/api/global/common/slack/domain/GeneralAlertType.java @@ -9,6 +9,7 @@ public enum GeneralAlertType implements AlertType { ADMIN_LOGIN("관리자 로그인", "Admin login."), APPLICATION_CREATED("새 지원서", "New application has been submitted."), + BOARD_CREATED("새 게시글", "New board has been created."), SERVER_START("서버 시작", "Server has been started."), SERVER_ERROR("서버 에러", "Server error occurred."); From ef356504e4e5d9972addec79278b0bb387378e42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EA=B4=80=ED=9D=AC?= Date: Fri, 21 Jun 2024 15:42:04 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor(Slack):=20=EC=83=88=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EA=B8=80=20=EC=95=8C=EB=A6=BC=20=EC=9D=B4=EB=AA=A8?= =?UTF-8?q?=EC=A7=80=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/global/common/slack/application/SlackServiceHelper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java index 6dc40941b..3bd9c0e6d 100644 --- a/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java +++ b/src/main/java/page/clab/api/global/common/slack/application/SlackServiceHelper.java @@ -210,7 +210,7 @@ private List createBoardBlocks(Board board) { String username = board.isWantAnonymous() ? board.getNickname() : board.getMember().getId() + " " + board.getMember().getName(); - blocks.add(section(section -> section.text(markdownText(":mag: *New Board*")))); + blocks.add(section(section -> section.text(markdownText(":writing_hand: *New Board*")))); blocks.add(section(section -> section.fields(Arrays.asList( markdownText("*Title:*\n" + board.getTitle()), markdownText("*Category:*\n" + board.getCategory().getDescription()),