Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
706a6e4
feat: 커스텀 예외 처리를 위한 인터페이스 정의
move-hoon Apr 18, 2025
5f826e0
feat: Slack 요청 본문 파싱 실패 처리를 위한 Unchecked 예외 추가
move-hoon Apr 18, 2025
46b3cec
feat: Slack 메시지 생성 및 전송 실패에 대한 Checked 예외 클래스 구현
move-hoon Apr 18, 2025
b9ef824
feat: Slack 관련 예외 메시지 ErrorMessage enum으로 정의
move-hoon Apr 18, 2025
b952eb3
feat: EnvUtil 클래스 생성 및 웹훅 URL 조회 기능 구현
move-hoon Apr 18, 2025
40c40bf
feat: EnvUtil 클래스 생성 및 웹훅 URL 조회 기능 구현
move-hoon Apr 18, 2025
61ce339
feat: ErrorMessage enum에 예외 코드 추가
move-hoon Apr 18, 2025
ca366fb
feat: HttpClientUtil 유틸 클래스 추가
move-hoon Apr 18, 2025
080ed5c
feat: Slack 메시지 생성을 위한 상수 클래스 및 로그 레벨 기반 색상 매핑 기능 추가
move-hoon Apr 18, 2025
d008ab1
feat: Slack 알림 서비스 구현 및 팩토리 클래스 추가
move-hoon Apr 18, 2025
26ccd38
feat: ObjectMapperConfig 클래스 추가
move-hoon Apr 18, 2025
82a5456
feat: SentryEventDetail DTO 추가
move-hoon Apr 18, 2025
44d709d
delete: Main 클래스 삭제
move-hoon Apr 18, 2025
a413566
feat: Slack API 연동을 위한 VO 객체 추가
move-hoon Apr 19, 2025
6406548
refactor: SlackNotificationService Map 기반 구조를 VO 기반으로 변경
move-hoon Apr 19, 2025
c686519
refactor: 사용하지 않는 상수 삭제
move-hoon Apr 19, 2025
2743173
feat: Sentry 웹훅 처리용 Lambda 핸들러 구현
move-hoon Apr 19, 2025
c0bad08
refactor: APIGatewayProxyRequestEvent 파싱을 위한 WebhookRequest DTO 도입 및 …
move-hoon Apr 19, 2025
02b3e93
chore: 에러메시지 세분화
move-hoon Apr 19, 2025
e56513d
refactor: 가독성을 위한 handler 리팩토링 및 상수 추가
move-hoon Apr 19, 2025
d0f926a
chore: 매직 문자열 상수로 변경
move-hoon Apr 19, 2025
a977ac7
refactor: Slack API 응답 본문 검증 누락에 대한 코드리뷰 반영
move-hoon Apr 19, 2025
9918a13
refactor: Z와 같은 UTC 오프셋을 처리할 수 있도록 OffsetDateTime로 변경
move-hoon Apr 19, 2025
c46ddac
refactor: 경로 파라미터에 대한 NPE 방지 코드리뷰 반영
move-hoon Apr 19, 2025
b6b9c00
fix: Slack 공식 스펙에 맞게 구조화 수정
move-hoon Apr 19, 2025
b09bcaf
refactor: 패키지 이동
move-hoon Apr 19, 2025
50f54cb
refactor: 예외 처리 패턴 개선 및 커스텀 예외 추가
move-hoon Apr 19, 2025
75f6d24
docs: README 업데이트
move-hoon Apr 19, 2025
474bf2d
feat: Release Drafter 설정 추가 및 버전 태그 자동화
move-hoon Apr 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions src/main/java/org/sopt/makers/Main.java

This file was deleted.

25 changes: 25 additions & 0 deletions src/main/java/org/sopt/makers/dto/SentryEventDetail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.sopt.makers.dto;

import com.fasterxml.jackson.databind.JsonNode;

import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record SentryEventDetail(
String issueId,
String webUrl,
String message,
String datetime,
String level
) {
public static SentryEventDetail from(JsonNode eventNode) {
return SentryEventDetail.builder()
.issueId(eventNode.path("issue_id").asText())
.webUrl(eventNode.path("web_url").asText())
.message(eventNode.path("message").asText())
.datetime(eventNode.path("datetime").asText())
.level(eventNode.path("level").asText())
.build();
}
}
35 changes: 35 additions & 0 deletions src/main/java/org/sopt/makers/dto/WebhookRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package org.sopt.makers.dto;

import java.util.Map;

import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;

import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record WebhookRequest(
String stage,
String team,
String type,
String serviceType
) {
public static WebhookRequest from(APIGatewayProxyRequestEvent input) {
String stage = input.getRequestContext().getStage();
Map<String, String> pathParameters = input.getPathParameters();
if (pathParameters == null) {
pathParameters = Map.of();
}

String team = pathParameters.get("team");
String type = pathParameters.get("type");
String serviceType = pathParameters.getOrDefault("service", "slack");

Comment thread
move-hoon marked this conversation as resolved.
return WebhookRequest.builder()
.stage(stage)
.team(team)
.type(type)
.serviceType(serviceType)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sopt.makers.global.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ObjectMapperConfig {

public static ObjectMapper getInstance() {
return ObjectMapperHolder.INSTANCE;
}

private static class ObjectMapperHolder {
private static final ObjectMapper INSTANCE = createObjectMapper();

private static ObjectMapper createObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return objectMapper;
}
}
}
34 changes: 34 additions & 0 deletions src/main/java/org/sopt/makers/global/constant/Color.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.sopt.makers.global.constant;

import java.util.Arrays;
import java.util.Set;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Color {
RED("#FF0000", Set.of("fatal", "critical")),
ORANGE("#E36209", Set.of("error")),
YELLOW("#FFCC00", Set.of("warning")),
GREEN("#36A64F", Set.of("info")),
BLUE("#87CEFA", Set.of("debug")),
GRAY("#AAAAAA", Set.of());

private final String value;
private final Set<String> levels;

public static String getColorByLevel(String level) {
if (level == null || level.trim().isEmpty()) {
return GRAY.value;
}

String normalized = level.trim().toLowerCase();
return Arrays.stream(Color.values())
.filter(color -> color.levels.contains(normalized))
.map(Color::getValue)
.findFirst()
.orElse(GRAY.value);
}
}
49 changes: 49 additions & 0 deletions src/main/java/org/sopt/makers/global/constant/SlackConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.sopt.makers.global.constant;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SlackConstant {
// 날짜 포맷 형식
public static final String DATE_FORMAT_PATTERN = "yyyy-MM-dd HH:mm:ss";

// 버튼 텍스트
public static final String SENTRY_BUTTON_TEXT = "상세 보기";

// HTTP 관련 상수
public static final String HEADER_CONTENT_TYPE = "Content-Type";
public static final String CONTENT_TYPE_JSON = "application/json";

// Slack 블록 타입 상수
public static final String BLOCK_TYPE_HEADER = "header";
public static final String BLOCK_TYPE_SECTION = "section";
public static final String BLOCK_TYPE_ACTIONS = "actions";

// 텍스트 타입 상수
public static final String TEXT_TYPE_PLAIN = "plain_text";
public static final String TEXT_TYPE_MARKDOWN = "mrkdwn";

// 요소 타입 상수 (버튼)
public static final String ELEMENT_TYPE_BUTTON = "button";

// 스타일 상수
public static final String STYLE_PRIMARY = "primary";

// 시간대 관련 상수
public static final String TIMEZONE_SEOUL = "Asia/Seoul";

// 줄바꿈 상수
public static final String NEW_LINE = "\n";

// JSON 키 상수
public static final String MESSAGE = "message";
public static final String ERROR = "error";
public static final String CODE = "code";

// 성공 메시지
public static final String SUCCESS_MESSAGE = "알림이 성공적으로 전송되었습니다";

// 폴백 메시지 (기본 오류 메시지)
public static final String FALLBACK_MESSAGE = "알림이 전송되었습니다";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.sopt.makers.global.exception.base;

public interface BaseErrorCode {
int getStatus();
String getCode();
String getMessage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package org.sopt.makers.global.exception.base;

public interface SentryException {
BaseErrorCode getBaseErrorCode();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.makers.global.exception.checked;

import org.sopt.makers.global.exception.base.BaseErrorCode;
import org.sopt.makers.global.exception.base.SentryException;

import lombok.Getter;

@Getter
public class SentryCheckedException extends Exception implements SentryException {
private final BaseErrorCode baseErrorCode;

protected SentryCheckedException(BaseErrorCode baseErrorCode) {
super(baseErrorCode.getMessage());
this.baseErrorCode = baseErrorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.checked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class SlackMessageBuildException extends SentryCheckedException {
public SlackMessageBuildException(BaseErrorCode errorCode) {
super(errorCode);
}

public static SlackMessageBuildException from(BaseErrorCode errorCode) {
return new SlackMessageBuildException(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.checked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class SlackSendException extends SentryCheckedException {
public SlackSendException(BaseErrorCode errorCode) {
super(errorCode);
}

public static SlackSendException from(BaseErrorCode errorCode) {
return new SlackSendException(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sopt.makers.global.exception.message;

import org.sopt.makers.global.exception.base.BaseErrorCode;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ErrorMessage implements BaseErrorCode {
// ===== Webhook 관련 오류 =====
WEBHOOK_URL_NOT_FOUND(500, "Webhook URL을 찾을 수 없습니다.", "W5001"),

// ===== Slack 및 일반 서비스 오류 =====
INVALID_SLACK_PAYLOAD(400, "Slack 페이로드 형식이 잘못되었습니다.", "S4001"),
UNSUPPORTED_SERVICE_TYPE(400, "지원하지 않는 서비스 유형입니다.", "S4002"),
SLACK_MESSAGE_BUILD_FAILED(500, "Slack 메시지 생성에 실패했습니다.", "S5001"),
SLACK_SEND_INTERRUPTED(500, "Slack 알림 전송이 중단되었습니다.", "S5002"),
SLACK_SERIALIZATION_FAILED(500, "Slack 메시지를 JSON으로 변환하는 중 오류가 발생했습니다.", "S5003"),
SLACK_SEND_FAILED(502, "Slack 전송 요청에 실패했습니다.", "S5021"),
SLACK_NETWORK_ERROR(503, "Slack 서버 연결에 실패했습니다.", "S5031"),

// ===== 공통 오류 =====
UNEXPECTED_SERVER_ERROR(500, "내부 서버 오류가 발생했습니다.", "C5001");

private final int status;
private final String message;
private final String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.unchecked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class HttpRequestException extends SentryUncheckedException {
public HttpRequestException(BaseErrorCode errorCode) {
super(errorCode);
}

public static HttpRequestException from(BaseErrorCode errorCode) {
return new HttpRequestException(errorCode);
}
Comment thread
move-hoon marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.unchecked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class InvalidSlackPayloadException extends SentryUncheckedException {
public InvalidSlackPayloadException(BaseErrorCode errorCode) {
super(errorCode);
}

public static InvalidSlackPayloadException from(BaseErrorCode errorCode) {
return new InvalidSlackPayloadException(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.sopt.makers.global.exception.unchecked;

import org.sopt.makers.global.exception.base.BaseErrorCode;
import org.sopt.makers.global.exception.base.SentryException;

import lombok.Getter;

@Getter
public class SentryUncheckedException extends RuntimeException implements SentryException {
private final BaseErrorCode baseErrorCode;

protected SentryUncheckedException(BaseErrorCode baseErrorCode) {
super(baseErrorCode.getMessage());
this.baseErrorCode = baseErrorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.unchecked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class UnsupportedServiceTypeException extends SentryUncheckedException {
public UnsupportedServiceTypeException(BaseErrorCode errorCode) {
super(errorCode);
}

public static UnsupportedServiceTypeException from(BaseErrorCode errorCode) {
return new UnsupportedServiceTypeException(errorCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.sopt.makers.global.exception.unchecked;

import org.sopt.makers.global.exception.base.BaseErrorCode;

public class WebhookUrlNotFoundException extends SentryUncheckedException {
public WebhookUrlNotFoundException(BaseErrorCode errorCode) {
super(errorCode);
}

public static WebhookUrlNotFoundException from(BaseErrorCode errorCode) {
return new WebhookUrlNotFoundException(errorCode);
}
}
Loading