From 20b4cb4f1a3814db0c24721561b6b266f4a0b992 Mon Sep 17 00:00:00 2001 From: wjh Date: Fri, 7 Nov 2025 12:22:06 +0900 Subject: [PATCH 1/4] initial commit --- build.gradle | 26 ++++++ .../review/controller/ReviewController.java | 34 ++++++++ .../review/converter/ReviewConverter.java | 21 +++++ .../review/dto/ReviewMyReviewResponse.java | 13 +++ .../review/enums/ReviewRatingGroup.java | 41 ++++++++++ .../repository/ReviewQueryRepository.java | 17 ++++ .../repository/ReviewQueryRepositoryImpl.java | 80 +++++++++++++++++++ .../review/repository/ReviewRepository.java | 2 +- .../result/ReviewSummaryProjection.java | 15 ++++ .../domain/review/service/ReviewService.java | 39 +++++++++ .../umc9th/global/config/QuerydslConfig.java | 22 +++++ 11 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java create mode 100644 src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java create mode 100644 src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java create mode 100644 src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java create mode 100644 src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java create mode 100644 src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java create mode 100644 src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java create mode 100644 src/main/java/com/example/umc9th/domain/review/service/ReviewService.java create mode 100644 src/main/java/com/example/umc9th/global/config/QuerydslConfig.java diff --git a/build.gradle b/build.gradle index ee064e0..4eb072b 100644 --- a/build.gradle +++ b/build.gradle @@ -32,8 +32,34 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // QueryDSL : OpenFeign + implementation "io.github.openfeign.querydsl:querydsl-jpa:7.0" + implementation "io.github.openfeign.querydsl:querydsl-core:7.0" + annotationProcessor "io.github.openfeign.querydsl:querydsl-apt:7.0:jpa" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "jakarta.annotation:jakarta.annotation-api" } tasks.named('test') { useJUnitPlatform() } + +// QueryDSL 관련 설정 +// generated/querydsl 폴더 생성 & 삽입 +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile + +// 소스 세트에 생성 경로 추가 (구체적인 경로 지정) +sourceSets { + main.java.srcDirs += [ querydslDir ] +} + +// 컴파일 시 생성 경로 지정 +tasks.withType(JavaCompile).configureEach { + options.generatedSourceOutputDirectory.set(querydslDir) +} + +// clean 태스크에 생성 폴더 삭제 로직 추가 +clean.doLast { + file(querydslDir).deleteDir() +} diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java new file mode 100644 index 0000000..ea05b91 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -0,0 +1,34 @@ +package com.example.umc9th.domain.review.controller; + +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.service.ReviewService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1") +public class ReviewController { + + private final ReviewService reviewService; + + @GetMapping("/users/{userId}/reviews") + public ResponseEntity> getMyReviews(@PathVariable Long userId, + @RequestParam(required = false) String restaurantName, + @RequestParam(required = false) Integer ratingFloor, + @PageableDefault(size = 10) Pageable pageable) { + Page response = reviewService.getMyReviews(userId, restaurantName, ratingFloor, pageable); + return ResponseEntity.ok(response); + } +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java new file mode 100644 index 0000000..8ef617b --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/converter/ReviewConverter.java @@ -0,0 +1,21 @@ +package com.example.umc9th.domain.review.converter; + +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; + +public final class ReviewConverter { + + private ReviewConverter() { + } + + public static ReviewMyReviewResponse toMyReviewResponse(ReviewSummaryProjection projection) { + return new ReviewMyReviewResponse( + projection.reviewId(), + projection.restaurantName(), + projection.reviewStar(), + projection.body(), + projection.createdAt() + ); + } +} + diff --git a/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java b/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java new file mode 100644 index 0000000..a9ccb52 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/dto/ReviewMyReviewResponse.java @@ -0,0 +1,13 @@ +package com.example.umc9th.domain.review.dto; + +import java.time.LocalDateTime; + +public record ReviewMyReviewResponse( + Long reviewId, + String restaurantName, + Integer reviewStar, + String body, + LocalDateTime createdAt +) { +} + diff --git a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java new file mode 100644 index 0000000..715d221 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java @@ -0,0 +1,41 @@ +package com.example.umc9th.domain.review.enums; + +import java.util.Arrays; + +public enum ReviewRatingGroup { + + FIVE(5, 5), + FOUR(4, 4), + THREE(3, 3), + TWO(2, 2), + ONE(1, 1); + + private final int minInclusive; + private final int maxInclusive; + + ReviewRatingGroup(int minInclusive, int maxInclusive) { + this.minInclusive = minInclusive; + this.maxInclusive = maxInclusive; + } + + public int getMinInclusive() { + return minInclusive; + } + + public int getMaxInclusive() { + return maxInclusive; + } + + public static ReviewRatingGroup fromValue(Integer value) { + if (value == null) { + return null; + } + return Arrays.stream(values()) + .filter(group -> group.minInclusive == value) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unsupported rating group value: " + value)); + } +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java new file mode 100644 index 0000000..7f9b99e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java @@ -0,0 +1,17 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ReviewQueryRepository { + + Page findMyReviews(Long userId, + String restaurantName, + ReviewRatingGroup ratingGroup, + Pageable pageable); +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java new file mode 100644 index 0000000..740c450 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepositoryImpl.java @@ -0,0 +1,80 @@ +package com.example.umc9th.domain.review.repository; + +import com.example.umc9th.domain.restaurant.entity.QRestaurant; +import com.example.umc9th.domain.review.entity.QReview; +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Projections; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class ReviewQueryRepositoryImpl implements ReviewQueryRepository { + + private final JPAQueryFactory queryFactory; + + private static final QReview review = QReview.review; + private static final QRestaurant restaurant = QRestaurant.restaurant; + + @Override + public Page findMyReviews(Long userId, + String restaurantName, + ReviewRatingGroup ratingGroup, + Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder() + .and(review.user.id.eq(userId)); + + if (StringUtils.hasText(restaurantName)) { + builder.and(restaurant.name.eq(restaurantName)); + } + + BooleanExpression ratingCondition = ratingGroupCondition(ratingGroup); + if (ratingCondition != null) { + builder.and(ratingCondition); + } + + List content = queryFactory + .select(Projections.constructor(ReviewSummaryProjection.class, + review.id, + restaurant.name, + review.reviewStar, + review.body, + review.createdAt + )) + .from(review) + .join(review.restaurant, restaurant) + .where(builder) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(review.createdAt.desc()) + .fetch(); + + Long total = queryFactory + .select(review.count()) + .from(review) + .join(review.restaurant, restaurant) + .where(builder) + .fetchOne(); + + long totalElements = total != null ? total : 0L; + return new PageImpl<>(content, pageable, totalElements); + } + + private BooleanExpression ratingGroupCondition(ReviewRatingGroup ratingGroup) { + if (ratingGroup == null) { + return null; + } + return review.reviewStar.between(ratingGroup.getMinInclusive(), ratingGroup.getMaxInclusive()); + } +} + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java index 7a7a4ac..c2d2221 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewRepository.java @@ -3,6 +3,6 @@ import com.example.umc9th.domain.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; -public interface ReviewRepository extends JpaRepository { +public interface ReviewRepository extends JpaRepository, ReviewQueryRepository { } diff --git a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java new file mode 100644 index 0000000..505f89e --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java @@ -0,0 +1,15 @@ +package com.example.umc9th.domain.review.repository.result; + +import java.time.LocalDateTime; + +public record ReviewSummaryProjection( + Long reviewId, + String restaurantName, + Integer reviewStar, + String body, + LocalDateTime createdAt +) { +} + + + diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java new file mode 100644 index 0000000..335939d --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -0,0 +1,39 @@ +package com.example.umc9th.domain.review.service; + +import com.example.umc9th.domain.review.converter.ReviewConverter; +import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; +import com.example.umc9th.domain.review.enums.ReviewRatingGroup; +import com.example.umc9th.domain.review.repository.ReviewRepository; +import com.example.umc9th.domain.review.repository.result.ReviewSummaryProjection; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ReviewService { + + private final ReviewRepository reviewRepository; + + public Page getMyReviews(Long userId, + String restaurantName, + Integer ratingFloor, + Pageable pageable) { + ReviewRatingGroup ratingGroup = ReviewRatingGroup.fromValue(ratingFloor); + + Page projectionPage = reviewRepository.findMyReviews( + userId, + restaurantName, + ratingGroup, + pageable + ); + + return projectionPage.map(ReviewConverter::toMyReviewResponse); + } +} + + + diff --git a/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java new file mode 100644 index 0000000..3041f90 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java @@ -0,0 +1,22 @@ +package com.example.umc9th.global.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @PersistenceContext + private EntityManager entityManager; + + @Bean + public JPAQueryFactory jpaQueryFactory() { + return new JPAQueryFactory(entityManager); + } +} + + + From 9ba4fef047efe665c0cd25b881fcc4d04806252b Mon Sep 17 00:00:00 2001 From: wjh Date: Thu, 13 Nov 2025 23:42:11 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 2 ++ .../review/enums/ReviewRatingGroup.java | 2 ++ .../repository/ReviewQueryRepository.java | 1 + .../result/ReviewSummaryProjection.java | 2 ++ .../domain/review/service/ReviewService.java | 2 ++ .../test/controller/TestController.java | 26 ++++++++++++++ .../domain/test/converter/TestConverter.java | 12 +++++++ .../domain/test/dto/res/TestResDTO.java | 12 +++++++ .../umc9th/global/apiPayload/ApiResponse.java | 35 +++++++++++++++++++ .../global/apiPayload/code/BaseErrorCode.java | 11 ++++++ .../apiPayload/code/BaseSuccessCode.java | 5 +++ .../apiPayload/code/GeneralErrorCode.java | 28 +++++++++++++++ .../apiPayload/code/GeneralSuccessCode.java | 19 ++++++++++ .../umc9th/global/config/QuerydslConfig.java | 2 ++ 14 files changed, 159 insertions(+) create mode 100644 src/main/java/com/example/umc9th/domain/test/controller/TestController.java create mode 100644 src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java create mode 100644 src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index ea05b91..7dc3ed8 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -32,3 +32,5 @@ public ResponseEntity> getMyReviews(@PathVariable L + + diff --git a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java index 715d221..187c053 100644 --- a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java +++ b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java @@ -39,3 +39,5 @@ public static ReviewRatingGroup fromValue(Integer value) { + + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java index 7f9b99e..f819f16 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/ReviewQueryRepository.java @@ -15,3 +15,4 @@ Page findMyReviews(Long userId, + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java index 505f89e..249e10f 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java @@ -13,3 +13,5 @@ public record ReviewSummaryProjection( + + diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java index 335939d..8487136 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -37,3 +37,5 @@ public Page getMyReviews(Long userId, + + diff --git a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java new file mode 100644 index 0000000..11f6f84 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java @@ -0,0 +1,26 @@ +package com.example.umc9th.domain.test.controller; + +import com.example.umc9th.domain.test.converter.TestConverter; +import com.example.umc9th.domain.test.dto.res.TestResDTO; +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/temp") +public class TestController { + + @GetMapping("/test") + public ApiResponse test() throws Exception { + // 응답 코드 정의 + GeneralSuccessCode code = GeneralSuccessCode.SUCCESS; + return ApiResponse.onSuccess( + code, + TestConverter.toTestingDTO("This is Test!") + ); + } +} diff --git a/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java b/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java new file mode 100644 index 0000000..3f9c8b4 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java @@ -0,0 +1,12 @@ +package com.example.umc9th.domain.test.converter; + +import com.example.umc9th.domain.test.dto.res.TestResDTO; + +public class TestConverter { + + public static TestResDTO.Testing toTestingDTO(String testing) { + return TestResDTO.Testing.builder() + .testing(testing) + .build(); + } +} diff --git a/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java b/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java new file mode 100644 index 0000000..9eb8ce7 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java @@ -0,0 +1,12 @@ +package com.example.umc9th.domain.test.dto.res; + +import lombok.Getter; +import lombok.Builder; +public class TestResDTO { + + @Builder + @Getter + public static class Testing { + private String testing; + } +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java b/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java new file mode 100644 index 0000000..83f3cd3 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/ApiResponse.java @@ -0,0 +1,35 @@ +package com.example.umc9th.global.apiPayload; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.code.BaseSuccessCode; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +@JsonPropertyOrder({"isSuccess", "code", "message", "result"}) +public class ApiResponse { + + @JsonProperty("isSuccess") + private final Boolean isSuccess; + + @JsonProperty("code") + private final String code; + + @JsonProperty("message") + private final String message; + + @JsonProperty("result") + private T result; + + // 성공한 경우 (result 포함) + public static ApiResponse onSuccess(BaseSuccessCode code, T result) { + return new ApiResponse<>(true, code.getCode(), code.getMessage(), result); + } + // 실패한 경우 (result 포함) + public static ApiResponse onFailure(BaseErrorCode code, T result) { + return new ApiResponse<>(false, code.getCode(), code.getMessage(), result); + } +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java new file mode 100644 index 0000000..979b46f --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseErrorCode.java @@ -0,0 +1,11 @@ +package com.example.umc9th.global.apiPayload.code; + +import org.springframework.http.HttpStatus; + +public interface BaseErrorCode { + + HttpStatus getStatus(); + String getCode(); + String getMessage(); +} + \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java new file mode 100644 index 0000000..1806292 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java @@ -0,0 +1,5 @@ +package com.example.umc9th.global.apiPayload.code; + +public interface BaseSuccessCode { + +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java new file mode 100644 index 0000000..f59946b --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java @@ -0,0 +1,28 @@ +package com.example.umc9th.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralErrorCode implements BaseErrorCode{ + + BAD_REQUEST(HttpStatus.BAD_REQUEST, + "COMMON400_1", + "잘못된 요청입니다."), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, + "AUTH401_1", + "인증이 필요합니다."), + FORBIDDEN(HttpStatus.FORBIDDEN, + "AUTH403_1", + "요청이 거부되었습니다."), + NOT_FOUND(HttpStatus.NOT_FOUND, + "COMMON404_1", + "요청한 리소스를 찾을 수 없습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java new file mode 100644 index 0000000..967615c --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralSuccessCode.java @@ -0,0 +1,19 @@ +package com.example.umc9th.global.apiPayload.code; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum GeneralSuccessCode implements BaseSuccessCode{ + + SUCCESS(HttpStatus.OK, + "SUCCESS", + "Success"), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} diff --git a/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java index 3041f90..2128605 100644 --- a/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java +++ b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java @@ -20,3 +20,5 @@ public JPAQueryFactory jpaQueryFactory() { + + From b68c7b5da8c3082f2e7bef52dafe9b8428b33b93 Mon Sep 17 00:00:00 2001 From: wjh Date: Fri, 14 Nov 2025 11:24:06 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC=20=EB=B0=8F=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 1 + .../review/enums/ReviewRatingGroup.java | 1 + .../result/ReviewSummaryProjection.java | 1 + .../domain/review/service/ReviewService.java | 1 + .../test/controller/TestController.java | 20 ++++++++- .../domain/test/converter/TestConverter.java | 16 ++++++- .../domain/test/dto/res/TestResDTO.java | 11 ++++- .../domain/test/exception/TestException.java | 10 +++++ .../test/exception/code/TestErrorCode.java | 19 +++++++++ .../service/command/TestCommandService.java | 5 +++ .../command/TestCommandServiceImpl.java | 5 +++ .../test/service/query/TestQueryService.java | 5 +++ .../service/query/TestQueryServiceImpl.java | 18 ++++++++ .../apiPayload/code/GeneralErrorCode.java | 3 ++ .../exception/GeneralException.java | 12 ++++++ .../handler/GeneralExceptionAdvice.java | 42 +++++++++++++++++++ .../umc9th/global/config/QuerydslConfig.java | 1 + 17 files changed, 166 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/umc9th/domain/test/exception/TestException.java create mode 100644 src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java create mode 100644 src/main/java/com/example/umc9th/domain/test/service/command/TestCommandService.java create mode 100644 src/main/java/com/example/umc9th/domain/test/service/command/TestCommandServiceImpl.java create mode 100644 src/main/java/com/example/umc9th/domain/test/service/query/TestQueryService.java create mode 100644 src/main/java/com/example/umc9th/domain/test/service/query/TestQueryServiceImpl.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java create mode 100644 src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index 7dc3ed8..3111654 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -34,3 +34,4 @@ public ResponseEntity> getMyReviews(@PathVariable L + diff --git a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java index 187c053..a373c05 100644 --- a/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java +++ b/src/main/java/com/example/umc9th/domain/review/enums/ReviewRatingGroup.java @@ -41,3 +41,4 @@ public static ReviewRatingGroup fromValue(Integer value) { + diff --git a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java index 249e10f..2c6799d 100644 --- a/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java +++ b/src/main/java/com/example/umc9th/domain/review/repository/result/ReviewSummaryProjection.java @@ -15,3 +15,4 @@ public record ReviewSummaryProjection( + diff --git a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java index 8487136..cceface 100644 --- a/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc9th/domain/review/service/ReviewService.java @@ -39,3 +39,4 @@ public Page getMyReviews(Long userId, + diff --git a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java index 11f6f84..981f7a4 100644 --- a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java +++ b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java @@ -2,11 +2,13 @@ import com.example.umc9th.domain.test.converter.TestConverter; import com.example.umc9th.domain.test.dto.res.TestResDTO; +import com.example.umc9th.domain.test.service.query.TestQueryService; import com.example.umc9th.global.apiPayload.ApiResponse; import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -14,13 +16,29 @@ @RequestMapping("/temp") public class TestController { + private final TestQueryService testQueryService; + @GetMapping("/test") - public ApiResponse test() throws Exception { + public ApiResponse test() { // 응답 코드 정의 GeneralSuccessCode code = GeneralSuccessCode.SUCCESS; + return ApiResponse.onSuccess( code, TestConverter.toTestingDTO("This is Test!") ); } + + // 예외 상황 + @GetMapping("/exception") + public ApiResponse exception( + @RequestParam Long flag + ) { + + testQueryService.checkFlag(flag); + + // 응답 코드 정의 + GeneralSuccessCode code = GeneralSuccessCode.SUCCESS; + return ApiResponse.onSuccess(code, TestConverter.toExceptionDTO("This is Test!")); + } } diff --git a/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java b/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java index 3f9c8b4..af81183 100644 --- a/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java +++ b/src/main/java/com/example/umc9th/domain/test/converter/TestConverter.java @@ -4,9 +4,21 @@ public class TestConverter { - public static TestResDTO.Testing toTestingDTO(String testing) { + // 객체 -> DTO + public static TestResDTO.Testing toTestingDTO( + String testing + ) { return TestResDTO.Testing.builder() - .testing(testing) + .testString(testing) + .build(); + } + + // 객체 -> DTO + public static TestResDTO.Exception toExceptionDTO( + String testing + ){ + return TestResDTO.Exception.builder() + .testString(testing) .build(); } } diff --git a/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java b/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java index 9eb8ce7..9f13f52 100644 --- a/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java +++ b/src/main/java/com/example/umc9th/domain/test/dto/res/TestResDTO.java @@ -1,12 +1,19 @@ package com.example.umc9th.domain.test.dto.res; -import lombok.Getter; import lombok.Builder; +import lombok.Getter; + public class TestResDTO { @Builder @Getter public static class Testing { - private String testing; + private String testString; + } + + @Builder + @Getter + public static class Exception { + private String testString; } } diff --git a/src/main/java/com/example/umc9th/domain/test/exception/TestException.java b/src/main/java/com/example/umc9th/domain/test/exception/TestException.java new file mode 100644 index 0000000..1faf200 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/exception/TestException.java @@ -0,0 +1,10 @@ +package com.example.umc9th.domain.test.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; + +public class TestException extends GeneralException { + public TestException(BaseErrorCode code) { + super(code); + } +} diff --git a/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java b/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java new file mode 100644 index 0000000..7264f21 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/exception/code/TestErrorCode.java @@ -0,0 +1,19 @@ +package com.example.umc9th.domain.test.exception.code; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum TestErrorCode implements BaseErrorCode { + + // For test + TEST_EXCEPTION(HttpStatus.BAD_REQUEST, "TEST400_1", "이거는 테스트"), + ; + + private final HttpStatus status; + private final String code; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandService.java b/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandService.java new file mode 100644 index 0000000..9b92691 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandService.java @@ -0,0 +1,5 @@ +package com.example.umc9th.domain.test.service.command; + +public interface TestCommandService { + +} diff --git a/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandServiceImpl.java b/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandServiceImpl.java new file mode 100644 index 0000000..93d6607 --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/service/command/TestCommandServiceImpl.java @@ -0,0 +1,5 @@ +package com.example.umc9th.domain.test.service.command; + +public class TestCommandServiceImpl { + +} diff --git a/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryService.java b/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryService.java new file mode 100644 index 0000000..a6419ab --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryService.java @@ -0,0 +1,5 @@ +package com.example.umc9th.domain.test.service.query; + +public interface TestQueryService { + void checkFlag(Long flag); +} \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryServiceImpl.java b/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryServiceImpl.java new file mode 100644 index 0000000..a6190fa --- /dev/null +++ b/src/main/java/com/example/umc9th/domain/test/service/query/TestQueryServiceImpl.java @@ -0,0 +1,18 @@ +package com.example.umc9th.domain.test.service.query; + +import com.example.umc9th.domain.test.exception.TestException; +import com.example.umc9th.domain.test.exception.code.TestErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class TestQueryServiceImpl implements TestQueryService { + + @Override + public void checkFlag(Long flag){ + if (flag == 1){ + throw new TestException(TestErrorCode.TEST_EXCEPTION); + } + } +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java index f59946b..8c0faa8 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/GeneralErrorCode.java @@ -20,6 +20,9 @@ public enum GeneralErrorCode implements BaseErrorCode{ NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404_1", "요청한 리소스를 찾을 수 없습니다."), + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, + "COMMON500_1", + "예기치 않은 서버 에러가 발생했습니다."), ; private final HttpStatus status; diff --git a/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java b/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java new file mode 100644 index 0000000..bd7517f --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/exception/GeneralException.java @@ -0,0 +1,12 @@ +package com.example.umc9th.global.apiPayload.exception; + +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class GeneralException extends RuntimeException { + + private final BaseErrorCode code; +} diff --git a/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java new file mode 100644 index 0000000..195e36c --- /dev/null +++ b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -0,0 +1,42 @@ +package com.example.umc9th.global.apiPayload.handler; + +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.BaseErrorCode; +import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; +import com.example.umc9th.global.apiPayload.exception.GeneralException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GeneralExceptionAdvice { + + // 애플리케이션에서 발생하는 커스텀 예외를 처리 + @ExceptionHandler(GeneralException.class) + public ResponseEntity> handleException( + GeneralException ex + ) { + + return ResponseEntity.status(ex.getCode().getStatus()) + .body(ApiResponse.onFailure( + ex.getCode(), + null + ) + ); + } + + // 그 외의 정의되지 않은 모든 예외 처리 + @ExceptionHandler(Exception.class) + public ResponseEntity> handleException( + Exception ex + ) { + + BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; + return ResponseEntity.status(code.getStatus()) + .body(ApiResponse.onFailure( + code, + ex.getMessage() + ) + ); + } +} diff --git a/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java index 2128605..3a83a32 100644 --- a/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java +++ b/src/main/java/com/example/umc9th/global/config/QuerydslConfig.java @@ -22,3 +22,4 @@ public JPAQueryFactory jpaQueryFactory() { + From 6b635ad04f197d805b6f8d9d3ab169fb900cf236 Mon Sep 17 00:00:00 2001 From: wjh Date: Fri, 14 Nov 2025 12:03:11 +0900 Subject: [PATCH 4/4] =?UTF-8?q?feat:=20discord=20=EC=9B=B9=ED=9B=85=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../review/controller/ReviewController.java | 7 +-- .../test/controller/TestController.java | 5 ++ .../apiPayload/code/BaseSuccessCode.java | 6 +++ .../handler/GeneralExceptionAdvice.java | 46 ++++++++++++++++++- .../global/config/RestTemplateConfig.java | 14 ++++++ .../notification/dto/DiscordMessage.java | 22 +++++++++ .../service/DiscordNotificationService.java | 7 +++ .../DiscordNotificationServiceImpl.java | 29 ++++++++++++ src/main/resources/application.yml | 6 ++- 9 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/umc9th/global/config/RestTemplateConfig.java create mode 100644 src/main/java/com/example/umc9th/global/notification/dto/DiscordMessage.java create mode 100644 src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationService.java create mode 100644 src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationServiceImpl.java diff --git a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java index 3111654..31a08e5 100644 --- a/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java +++ b/src/main/java/com/example/umc9th/domain/review/controller/ReviewController.java @@ -2,11 +2,12 @@ import com.example.umc9th.domain.review.dto.ReviewMyReviewResponse; import com.example.umc9th.domain.review.service.ReviewService; +import com.example.umc9th.global.apiPayload.ApiResponse; +import com.example.umc9th.global.apiPayload.code.GeneralSuccessCode; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; -import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -21,12 +22,12 @@ public class ReviewController { private final ReviewService reviewService; @GetMapping("/users/{userId}/reviews") - public ResponseEntity> getMyReviews(@PathVariable Long userId, + public ApiResponse> getMyReviews(@PathVariable Long userId, @RequestParam(required = false) String restaurantName, @RequestParam(required = false) Integer ratingFloor, @PageableDefault(size = 10) Pageable pageable) { Page response = reviewService.getMyReviews(userId, restaurantName, ratingFloor, pageable); - return ResponseEntity.ok(response); + return ApiResponse.onSuccess(GeneralSuccessCode.SUCCESS, response); } } diff --git a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java index 981f7a4..8b9b8a3 100644 --- a/src/main/java/com/example/umc9th/domain/test/controller/TestController.java +++ b/src/main/java/com/example/umc9th/domain/test/controller/TestController.java @@ -41,4 +41,9 @@ public ApiResponse exception( GeneralSuccessCode code = GeneralSuccessCode.SUCCESS; return ApiResponse.onSuccess(code, TestConverter.toExceptionDTO("This is Test!")); } + + @GetMapping("/error") + public ApiResponse error() { + throw new RuntimeException("500 Error Test!"); + } } diff --git a/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java index 1806292..57bf5bc 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/code/BaseSuccessCode.java @@ -1,5 +1,11 @@ package com.example.umc9th.global.apiPayload.code; +import org.springframework.http.HttpStatus; + public interface BaseSuccessCode { + String getCode(); + String getMessage(); + HttpStatus getStatus(); } + \ No newline at end of file diff --git a/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java index 195e36c..a0849cd 100644 --- a/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java +++ b/src/main/java/com/example/umc9th/global/apiPayload/handler/GeneralExceptionAdvice.java @@ -4,13 +4,23 @@ import com.example.umc9th.global.apiPayload.code.BaseErrorCode; import com.example.umc9th.global.apiPayload.code.GeneralErrorCode; import com.example.umc9th.global.apiPayload.exception.GeneralException; +import com.example.umc9th.global.notification.dto.DiscordMessage; +import com.example.umc9th.global.notification.service.DiscordNotificationService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + @RestControllerAdvice +@RequiredArgsConstructor public class GeneralExceptionAdvice { + private final DiscordNotificationService discordNotificationService; + // 애플리케이션에서 발생하는 커스텀 예외를 처리 @ExceptionHandler(GeneralException.class) public ResponseEntity> handleException( @@ -28,9 +38,12 @@ public ResponseEntity> handleException( // 그 외의 정의되지 않은 모든 예외 처리 @ExceptionHandler(Exception.class) public ResponseEntity> handleException( - Exception ex + Exception ex, + HttpServletRequest request ) { + sendDiscordAlert(ex, request); + BaseErrorCode code = GeneralErrorCode.INTERNAL_SERVER_ERROR; return ResponseEntity.status(code.getStatus()) .body(ApiResponse.onFailure( @@ -39,4 +52,35 @@ public ResponseEntity> handleException( ) ); } + + private void sendDiscordAlert(Exception ex, HttpServletRequest request) { + String alertTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String exceptionName = ex.getClass().getSimpleName(); + String exceptionMessage = ex.getMessage(); + String requestUri = request.getRequestURI(); + String requestMethod = request.getMethod(); + + String description = String.format( + "## 🚨 500 Internal Server Error 🚨\n\n" + + "**- 발생 시각**: %s\n" + + "**- 요청 URI**: %s\n" + + "**- HTTP 메서드**: %s\n" + + "**- 예외 클래스**: %s\n" + + "**- 예외 메시지**: %s\n", + alertTime, requestUri, requestMethod, exceptionName, exceptionMessage + ); + + DiscordMessage.Embed embed = DiscordMessage.Embed.builder() + .title("🔥 서버 에러 발생 🔥") + .description(description) + .color(15158332) // Red color + .build(); + + DiscordMessage discordMessage = DiscordMessage.builder() + .content("서버 에러가 발생했습니다.") + .embeds(new DiscordMessage.Embed[]{embed}) + .build(); + + discordNotificationService.sendMessage(discordMessage); + } } diff --git a/src/main/java/com/example/umc9th/global/config/RestTemplateConfig.java b/src/main/java/com/example/umc9th/global/config/RestTemplateConfig.java new file mode 100644 index 0000000..3c6b9d2 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/config/RestTemplateConfig.java @@ -0,0 +1,14 @@ +package com.example.umc9th.global.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} diff --git a/src/main/java/com/example/umc9th/global/notification/dto/DiscordMessage.java b/src/main/java/com/example/umc9th/global/notification/dto/DiscordMessage.java new file mode 100644 index 0000000..4aed1fb --- /dev/null +++ b/src/main/java/com/example/umc9th/global/notification/dto/DiscordMessage.java @@ -0,0 +1,22 @@ +package com.example.umc9th.global.notification.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class DiscordMessage { + private String content; + + @JsonProperty("embeds") + private Embed[] embeds; + + @Getter + @Builder + public static class Embed { + private String title; + private String description; + private int color; + } +} diff --git a/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationService.java b/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationService.java new file mode 100644 index 0000000..6bde6a9 --- /dev/null +++ b/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationService.java @@ -0,0 +1,7 @@ +package com.example.umc9th.global.notification.service; + +import com.example.umc9th.global.notification.dto.DiscordMessage; + +public interface DiscordNotificationService { + void sendMessage(DiscordMessage message); +} diff --git a/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationServiceImpl.java b/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationServiceImpl.java new file mode 100644 index 0000000..74c476d --- /dev/null +++ b/src/main/java/com/example/umc9th/global/notification/service/DiscordNotificationServiceImpl.java @@ -0,0 +1,29 @@ +package com.example.umc9th.global.notification.service; + +import com.example.umc9th.global.notification.dto.DiscordMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class DiscordNotificationServiceImpl implements DiscordNotificationService { + + @Value("${discord.webhook.url}") + private String discordWebhookUrl; + + private final RestTemplate restTemplate; + + @Override + public void sendMessage(DiscordMessage message) { + try { + log.info("Sending Discord notification."); + restTemplate.postForObject(discordWebhookUrl, message, String.class); + } catch (Exception e) { + log.error("Failed to send Discord notification.", e); + } + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 29ca312..0063717 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,8 @@ spring: ddl-auto: update # 애플리케이션 실행 시 데이터베이스 스키마의 상태를 설정 properties: hibernate: - format_sql: true # 출력되는 SQL 쿼리를 보기 좋게 포맷팅 \ No newline at end of file + format_sql: true # 출력되는 SQL 쿼리를 보기 좋게 포맷팅 + +discord: + webhook: + url: "https://discord.com/api/webhooks/1438720476204498987/7NE_rXydx9r2fy0HKRtGMmYiNoiDrGcy_9aQbh24XMOG3x3kWYPbqbJ7s4OHfBOv2YnM" \ No newline at end of file