From c445d06b54f284a785c99e1d228008b4512f1cc3 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 22 Apr 2025 16:49:31 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20Text=20=EC=9D=B8=ED=84=B0=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=EB=A1=9C=20=EC=B6=94=EC=83=81=ED=99=94?= =?UTF-8?q?=ED=95=98=EA=B3=A0=20MarkdownText/PlainText=EB=A1=9C=20?= =?UTF-8?q?=EC=84=B8=EB=B6=84=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MarkdownText는 emoji 필드 없이 동작하도록 수정 --- .../sopt/makers/vo/slack/text/MarkdownText.java | 12 ++++++++++++ .../sopt/makers/vo/slack/text/PlainText.java | 13 +++++++++++++ .../org/sopt/makers/vo/slack/text/Text.java | 17 +++-------------- 3 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/sopt/makers/vo/slack/text/MarkdownText.java create mode 100644 src/main/java/org/sopt/makers/vo/slack/text/PlainText.java diff --git a/src/main/java/org/sopt/makers/vo/slack/text/MarkdownText.java b/src/main/java/org/sopt/makers/vo/slack/text/MarkdownText.java new file mode 100644 index 0000000..fd3b25f --- /dev/null +++ b/src/main/java/org/sopt/makers/vo/slack/text/MarkdownText.java @@ -0,0 +1,12 @@ +package org.sopt.makers.vo.slack.text; + +import static org.sopt.makers.global.constant.SlackConstant.*; + +public record MarkdownText( + String type, + String text +) implements Text { + public static MarkdownText newInstance(String text) { + return new MarkdownText(TEXT_TYPE_MARKDOWN, text); + } +} diff --git a/src/main/java/org/sopt/makers/vo/slack/text/PlainText.java b/src/main/java/org/sopt/makers/vo/slack/text/PlainText.java new file mode 100644 index 0000000..c10f571 --- /dev/null +++ b/src/main/java/org/sopt/makers/vo/slack/text/PlainText.java @@ -0,0 +1,13 @@ +package org.sopt.makers.vo.slack.text; + +import static org.sopt.makers.global.constant.SlackConstant.*; + +public record PlainText( + String type, + String text, + boolean emoji +) implements Text { + public static PlainText newInstance(String text, boolean emoji) { + return new PlainText(TEXT_TYPE_PLAIN, text, emoji); + } +} diff --git a/src/main/java/org/sopt/makers/vo/slack/text/Text.java b/src/main/java/org/sopt/makers/vo/slack/text/Text.java index 9517229..1496b10 100644 --- a/src/main/java/org/sopt/makers/vo/slack/text/Text.java +++ b/src/main/java/org/sopt/makers/vo/slack/text/Text.java @@ -1,17 +1,6 @@ package org.sopt.makers.vo.slack.text; -import static org.sopt.makers.global.constant.SlackConstant.*; - -public record Text( - String type, - String text, - boolean emoji -) { - public static Text newPlainInstance(String text, boolean emoji) { - return new Text(TEXT_TYPE_PLAIN, text, emoji); - } - - public static Text newFieldInstance(String text) { - return new Text(TEXT_TYPE_MARKDOWN, text, false); - } +public sealed interface Text permits PlainText, MarkdownText { + String type(); + String text(); } From 337dff3ad87c4a52ae4ffa921c50b4147881a050 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 22 Apr 2025 16:50:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=84=B0=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/sopt/makers/vo/slack/block/SectionBlock.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sopt/makers/vo/slack/block/SectionBlock.java b/src/main/java/org/sopt/makers/vo/slack/block/SectionBlock.java index 8042a01..c71b1b7 100644 --- a/src/main/java/org/sopt/makers/vo/slack/block/SectionBlock.java +++ b/src/main/java/org/sopt/makers/vo/slack/block/SectionBlock.java @@ -1,10 +1,11 @@ package org.sopt.makers.vo.slack.block; -import org.sopt.makers.vo.slack.text.Text; import static org.sopt.makers.global.constant.SlackConstant.*; import java.util.List; +import org.sopt.makers.vo.slack.text.Text; + public record SectionBlock( String type, List fields, From 1e327516a8287ca51978f31caf446116c57b11ff Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 22 Apr 2025 16:50:45 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20=EA=B5=AC=ED=98=84=EC=B2=B4?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=B4=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/sopt/makers/vo/slack/block/HeaderBlock.java | 7 ++++--- src/main/java/org/sopt/makers/vo/slack/element/Button.java | 6 ++++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/sopt/makers/vo/slack/block/HeaderBlock.java b/src/main/java/org/sopt/makers/vo/slack/block/HeaderBlock.java index 6918336..9249d96 100644 --- a/src/main/java/org/sopt/makers/vo/slack/block/HeaderBlock.java +++ b/src/main/java/org/sopt/makers/vo/slack/block/HeaderBlock.java @@ -1,14 +1,15 @@ package org.sopt.makers.vo.slack.block; -import org.sopt.makers.vo.slack.text.Text; - import static org.sopt.makers.global.constant.SlackConstant.*; +import org.sopt.makers.vo.slack.text.PlainText; +import org.sopt.makers.vo.slack.text.Text; + public record HeaderBlock( String type, Text text ) implements Block { public static HeaderBlock newInstance(String text) { - return new HeaderBlock(BLOCK_TYPE_HEADER, Text.newPlainInstance(text, true)); + return new HeaderBlock(BLOCK_TYPE_HEADER, PlainText.newInstance("🚨 " + text, true)); } } diff --git a/src/main/java/org/sopt/makers/vo/slack/element/Button.java b/src/main/java/org/sopt/makers/vo/slack/element/Button.java index cfb0ea6..f65b272 100644 --- a/src/main/java/org/sopt/makers/vo/slack/element/Button.java +++ b/src/main/java/org/sopt/makers/vo/slack/element/Button.java @@ -1,8 +1,10 @@ package org.sopt.makers.vo.slack.element; -import org.sopt.makers.vo.slack.text.Text; import static org.sopt.makers.global.constant.SlackConstant.*; +import org.sopt.makers.vo.slack.text.PlainText; +import org.sopt.makers.vo.slack.text.Text; + public record Button( String type, Text text, @@ -10,6 +12,6 @@ public record Button( String url ) implements Element { public static Button newInstance(String text, String url) { - return new Button(ELEMENT_TYPE_BUTTON, Text.newPlainInstance(text, true), STYLE_PRIMARY, url); + return new Button(ELEMENT_TYPE_BUTTON, PlainText.newInstance(text, true), STYLE_PRIMARY, url); } } From 3b83e2562756219f875703e0487a6b6e94e9a770 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 22 Apr 2025 16:51:31 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20title=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EB=8F=84=20=EB=B0=9B=EB=8F=84=EB=A1=9D=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/sopt/makers/dto/SentryEventDetail.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/sopt/makers/dto/SentryEventDetail.java b/src/main/java/org/sopt/makers/dto/SentryEventDetail.java index 845f8fc..7ee1f17 100644 --- a/src/main/java/org/sopt/makers/dto/SentryEventDetail.java +++ b/src/main/java/org/sopt/makers/dto/SentryEventDetail.java @@ -11,7 +11,8 @@ public record SentryEventDetail( String webUrl, String message, String datetime, - String level + String level, + String title ) { public static SentryEventDetail from(JsonNode eventNode) { return SentryEventDetail.builder() @@ -20,6 +21,7 @@ public static SentryEventDetail from(JsonNode eventNode) { .message(eventNode.path("message").asText()) .datetime(eventNode.path("datetime").asText()) .level(eventNode.path("level").asText()) + .title(eventNode.path("title").asText()) .build(); } } From e72c049fe5a56ffce38d4cbb02cae3548e74f32a Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Tue, 22 Apr 2025 16:52:19 +0900 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20SlackMessage=20vo=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=9E=AC=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/SlackNotificationService.java | 61 +++++++++++-------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/sopt/makers/service/SlackNotificationService.java b/src/main/java/org/sopt/makers/service/SlackNotificationService.java index 352d3bd..47c0dbf 100644 --- a/src/main/java/org/sopt/makers/service/SlackNotificationService.java +++ b/src/main/java/org/sopt/makers/service/SlackNotificationService.java @@ -21,12 +21,13 @@ import org.sopt.makers.global.exception.message.ErrorMessage; import org.sopt.makers.global.exception.unchecked.HttpRequestException; import org.sopt.makers.global.util.HttpClientUtil; -import org.sopt.makers.vo.slack.message.SlackMessage; import org.sopt.makers.vo.slack.block.ActionsBlock; import org.sopt.makers.vo.slack.block.Block; import org.sopt.makers.vo.slack.block.HeaderBlock; import org.sopt.makers.vo.slack.block.SectionBlock; import org.sopt.makers.vo.slack.element.Button; +import org.sopt.makers.vo.slack.message.SlackMessage; +import org.sopt.makers.vo.slack.text.MarkdownText; import org.sopt.makers.vo.slack.text.Text; import com.fasterxml.jackson.databind.ObjectMapper; @@ -54,8 +55,8 @@ private SlackMessage createSlackMessage(String team, String type, String stage, try { return buildSlackMessage(team, type, stage, sentryEventDetail); } catch (DateTimeException e) { - log.error("Slack 메시지 생성 실패: team={}, type={}, stage={}, id={}, error={}", - team, type, stage, sentryEventDetail.issueId(), e.getMessage(), e); + log.error("Slack 메시지 생성 실패: team={}, type={}, stage={}, id={}, error={}", team, type, stage, + sentryEventDetail.issueId(), e.getMessage(), e); throw SlackMessageBuildException.from(ErrorMessage.SLACK_MESSAGE_BUILD_FAILED); } } @@ -74,17 +75,17 @@ private void sendSlackMessage(SlackMessage slackMessage, String webhookUrl, Stri } } - private void handleSlackResponse(HttpResponse response, String team, String type, - String stage, SentryEventDetail sentryEventDetail) throws SlackSendException { + private void handleSlackResponse(HttpResponse response, String team, String type, String stage, + SentryEventDetail sentryEventDetail) throws SlackSendException { if (response.statusCode() != 200 || !"ok".equalsIgnoreCase(response.body())) { - String errorMsg = String.format("Slack API 응답 오류, status: %d, body: %s", - response.statusCode(), response.body()); + String errorMsg = String.format("Slack API 응답 오류, status: %d, body: %s", response.statusCode(), + response.body()); log.error("{}", errorMsg); throw SlackSendException.from(ErrorMessage.SLACK_SEND_FAILED); } - log.info("[Slack 전송 완료] team={}, type={}, stage={}, id={}, statusCode={}", - team, type, stage, sentryEventDetail.issueId(), response.statusCode()); + log.info("[Slack 전송 완료] team={}, type={}, stage={}, id={}, statusCode={}", team, type, stage, + sentryEventDetail.issueId(), response.statusCode()); } /** @@ -96,9 +97,10 @@ private SlackMessage buildSlackMessage(String team, String type, String stage, String color = Color.getColorByLevel(sentryEventDetail.level()); List blocks = new ArrayList<>(); - blocks.add(buildHeaderBlock(sentryEventDetail.level())); - blocks.add(buildDetailsBlock(team, type, stage, formattedDate)); - blocks.add(buildMessageBlock(sentryEventDetail.message())); + blocks.add(buildHeaderBlock(sentryEventDetail.message())); + blocks.add(buildDetailsBlock(team, type, stage, formattedDate, sentryEventDetail.issueId(), + sentryEventDetail.level())); + blocks.add(buildMessageBlock(sentryEventDetail.title())); blocks.add(buildActionsBlock(sentryEventDetail.webUrl())); return SlackMessage.newInstance(blocks, color); @@ -107,30 +109,36 @@ private SlackMessage buildSlackMessage(String team, String type, String stage, /** * 헤더 블록 구성 */ - private Block buildHeaderBlock(String level) { - return HeaderBlock.newInstance(String.format("[%s] 오류 발생", level.toUpperCase())); + private Block buildHeaderBlock(String message) { + return HeaderBlock.newInstance(message); } /** * 상세 정보 블록 구성 */ - private SectionBlock buildDetailsBlock(String team, String type, String stage, String date) { - List fields = List.of( - Text.newFieldInstance(String.format("*환경:*%s%s", NEW_LINE, stage)), - Text.newFieldInstance(String.format("*팀:*%s%s", NEW_LINE, team)), - Text.newFieldInstance(String.format("*유형:*%s%s", NEW_LINE, type)), - Text.newFieldInstance(String.format("*발생 시간:*%s%s", NEW_LINE, date)) - ); + private Block buildDetailsBlock(String team, String type, String stage, String date, String issueId, String level) { + List fields = List.of(MarkdownText.newInstance(String.format("*Environment:*%s%s", NEW_LINE, stage)), + MarkdownText.newInstance(String.format("*Team:*%s%s", NEW_LINE, team)), + MarkdownText.newInstance(String.format("*Server Type:*%s%s", NEW_LINE, type)), + MarkdownText.newInstance(String.format("*Issue Id:*%s%s", NEW_LINE, issueId)), + MarkdownText.newInstance(String.format("*Happen:*%s%s", NEW_LINE, date)), + MarkdownText.newInstance(String.format("*Level:*%s%s", NEW_LINE, level))); + return SectionBlock.newInstanceWithFields(fields); } /** * 메시지 블록 구성 */ - private Block buildMessageBlock(String message) { - return SectionBlock.newInstanceWithText( - Text.newFieldInstance(String.format("*오류 메시지:*%s%s", NEW_LINE, message)) - ); + private Block buildMessageBlock(String title) { + Text text = MarkdownText.newInstance(""" + *Error Details:* + ``` + %s + ``` + """.formatted(title.trim())); + + return SectionBlock.newInstanceWithText(text); } /** @@ -145,8 +153,7 @@ private Block buildActionsBlock(String webUrl) { */ private String formatDateTime(String isoDatetime) { OffsetDateTime utcTime = OffsetDateTime.parse(isoDatetime, DateTimeFormatter.ISO_DATE_TIME); - LocalDateTime koreaTime = utcTime.atZoneSameInstant(ZoneId.of(TIMEZONE_SEOUL)) - .toLocalDateTime(); + LocalDateTime koreaTime = utcTime.atZoneSameInstant(ZoneId.of(TIMEZONE_SEOUL)).toLocalDateTime(); return koreaTime.format(DateTimeFormatter.ofPattern(DATE_FORMAT_PATTERN)); } }