Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.kkinikong.be.feedback.event;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import lombok.RequiredArgsConstructor;

import com.kkinikong.be.feedback.event.payload.FeedbackCreatedEvent;
import com.kkinikong.be.feedback.util.discord.DiscordClient;
import com.kkinikong.be.feedback.util.notion.NotionClient;

@Component
@RequiredArgsConstructor
public class FeedbackEventListener {

private final DiscordClient discordClient;
private final NotionClient notionClient;

@Async("externalApiExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleFeedbackCreatedEvent(FeedbackCreatedEvent feedbackCreatedEvent) {
discordClient.sendFeedbackAlert(feedbackCreatedEvent);
notionClient.sendFeedback(feedbackCreatedEvent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kkinikong.be.feedback.event.payload;

import com.kkinikong.be.feedback.domain.Feedback;

public record FeedbackCreatedEvent(int rating, String content, String type) {
public static FeedbackCreatedEvent from(Feedback feedback) {
return new FeedbackCreatedEvent(
feedback.getRating(), feedback.getContent(), feedback.getType());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@

import java.util.stream.Collectors;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;

import com.kkinikong.be.feedback.domain.Feedback;
import com.kkinikong.be.feedback.dto.request.FeedbackRequest;
import com.kkinikong.be.feedback.event.payload.FeedbackCreatedEvent;
import com.kkinikong.be.feedback.repository.FeedbackRepository;
import com.kkinikong.be.user.repository.UserRepository;

@Service
@RequiredArgsConstructor
public class FeedbackService {

private final FeedbackRepository feedbackRepository;
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public void addFeedback(FeedbackRequest request) {
Expand All @@ -27,5 +28,7 @@ public void addFeedback(FeedbackRequest request) {
Feedback.builder().rating(request.rating()).content(request.content()).type(type).build();

feedbackRepository.save(feedback);

eventPublisher.publishEvent(FeedbackCreatedEvent.from(feedback));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.kkinikong.be.feedback.util.discord;

import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import com.kkinikong.be.feedback.event.payload.FeedbackCreatedEvent;

@Slf4j
@Component
@RequiredArgsConstructor
public class DiscordClient {

private final WebClient webClient;

@Value("${external-api.discord.feedback-url}")
private String feedbackUrl;

public void sendFeedbackAlert(FeedbackCreatedEvent feedbackCreatedEvent) {
// 디스코드 규격에 맞는 JSON 바디 생성
Map<String, Object> body =
Map.of(
"embeds",
List.of(
Map.of(
"title",
"\uD83D\uDCCC 유저의 피드백 도착",
"color",
5814783,
"fields",
List.of(
Map.of(
"name",
"⭐ 별점",
"value",
feedbackCreatedEvent.rating() + "점",
"inline",
false),
Map.of(
"name",
"🏷️ 유형",
"value",
feedbackCreatedEvent.type(),
"inline",
false),
Map.of(
"name",
"📝 내용",
"value",
feedbackCreatedEvent.content(),
"inline",
false)))));

// WebClient를 이용한 비동기 전송
webClient
.post()
.uri(feedbackUrl)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.doOnSuccess(response -> log.info("디스코드 알림 전송 성공"))
.doOnError(error -> log.error("디스코드 알림 전송 실패: {}", error.getMessage()))
.subscribe();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.kkinikong.be.feedback.util.notion;

import java.time.LocalDate;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import com.kkinikong.be.feedback.domain.type.FeedbackType;
import com.kkinikong.be.feedback.event.payload.FeedbackCreatedEvent;

@Slf4j
@Component
@RequiredArgsConstructor
public class NotionClient {

private final WebClient webClient;

@Value("${external-api.notion.secret}")
private String secret;

@Value("${external-api.notion.database-id.feedback}")
private String feedbackId;

public void sendFeedback(FeedbackCreatedEvent feedbackCreatedEvent) {

// 현재 날짜
String today = LocalDate.now().toString();

// 유형
List<Map<String, String>> typeList = new java.util.ArrayList<>();
if (feedbackCreatedEvent.type() != null && !feedbackCreatedEvent.type().isBlank()) {
for (String t : feedbackCreatedEvent.type().split(",")) {
String trimmed = t.trim();
typeList.add(Map.of("name", FeedbackType.valueOf(trimmed).getLabel()));
}
}

Map<String, Object> body =
Map.of(
"parent", Map.of("database_id", feedbackId),
"properties",
Map.of(
"내용",
Map.of(
"title",
List.of(
Map.of(
"text",
Map.of(
"content",
feedbackCreatedEvent.content() != null
? feedbackCreatedEvent.content()
: "")))),
"별점", Map.of("number", feedbackCreatedEvent.rating()),
"유형", Map.of("multi_select", typeList),
"피드백 날짜", Map.of("date", Map.of("start", today))));
webClient
.post()
.uri("https://api.notion.com/v1/pages")
.header("Authorization", "Bearer " + secret)
.header("Notion-Version", "2022-06-28")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.doOnSuccess(res -> log.info("노션 피드백 기록 성공"))
.doOnError(err -> log.error("노션 기록 실패: {}", err.getMessage()))
.subscribe();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/kkinikong/be/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,15 @@ public Executor asyncExecutor() {
executor.initialize();
return executor;
}

@Bean(name = "externalApiExecutor")
public Executor externalApiExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(15);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("ExternalApiExecutor-");
executor.initialize();
return executor;
}
}
15 changes: 11 additions & 4 deletions src/main/java/com/kkinikong/be/report/domain/type/ReportType.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package com.kkinikong.be.report.domain.type;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum ReportType {
STORE,
REVIEW,
COMMUNITY_POST,
COMMUNITY_COMMENT
STORE("가맹점"),
REVIEW("리뷰"),
COMMUNITY_POST("커뮤니티 게시글"),
COMMUNITY_COMMENT("커뮤니티 댓글");

private final String label;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.kkinikong.be.report.event;

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import lombok.RequiredArgsConstructor;

import com.kkinikong.be.report.event.payload.ReportCreatedEvent;
import com.kkinikong.be.report.util.discord.DiscordClient;
import com.kkinikong.be.report.util.notion.NotionClient;

@Component
@RequiredArgsConstructor
public class ReportEventListener {

private final NotionClient notionClient;
private final DiscordClient discordClient;

@Async("externalApiExecutor")
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleReportCreatedEvent(ReportCreatedEvent reportCreatedEvent) {
discordClient.sendReportAlert(reportCreatedEvent);
notionClient.sendReport(reportCreatedEvent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.kkinikong.be.report.event.payload;

import com.kkinikong.be.report.domain.Report;
import com.kkinikong.be.report.domain.type.ReportType;

public record ReportCreatedEvent(
ReportType reportType,
Long targetId,
Long userId,
String userNickname,
String reason,
String description) {
public static ReportCreatedEvent from(Report report) {
return new ReportCreatedEvent(
report.getReportType(),
report.getTargetId(),
report.getUser().getId(),
report.getUser().getNickname(),
report.getReason(),
report.getDescription());
}
}
21 changes: 16 additions & 5 deletions src/main/java/com/kkinikong/be/report/service/ReportService.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.time.LocalDateTime;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -19,6 +20,7 @@
import com.kkinikong.be.report.domain.type.ReportType;
import com.kkinikong.be.report.domain.type.StoreReportReason;
import com.kkinikong.be.report.dto.request.ReportRequest;
import com.kkinikong.be.report.event.payload.ReportCreatedEvent;
import com.kkinikong.be.report.exception.ReportException;
import com.kkinikong.be.report.exception.errorcode.ReportErrorCode;
import com.kkinikong.be.report.respository.ReportRepository;
Expand All @@ -42,6 +44,7 @@ public class ReportService {
private final ReviewRepository reviewRepository;
private final CommunityPostRepository communityPostRepository;
private final CommentRepository commentRepository;
private final ApplicationEventPublisher applicationEventPublisher;

@Transactional
public void reportStore(
Expand All @@ -66,6 +69,7 @@ public void reportStore(
.build();

reportRepository.save(report);
applicationEventPublisher.publishEvent(ReportCreatedEvent.from(report));
}

@Transactional
Expand All @@ -80,7 +84,9 @@ public void reportReview(
checkSelfReport(review.getUser().getId(), userId);
checkTargetExist(ReportType.REVIEW, reviewId, userId);

saveReport(reviewId, ReportType.REVIEW, CommonReportReason, reportRequest, user);
Report report =
saveReport(reviewId, ReportType.REVIEW, CommonReportReason, reportRequest, user);
applicationEventPublisher.publishEvent(ReportCreatedEvent.from(report));
}

@Transactional
Expand All @@ -97,7 +103,9 @@ public void reportCommunityPost(

communityPost.incrementReportCount();

saveReport(postId, ReportType.COMMUNITY_POST, CommonReportReason, reportRequest, user);
Report report =
saveReport(postId, ReportType.COMMUNITY_POST, CommonReportReason, reportRequest, user);
applicationEventPublisher.publishEvent(ReportCreatedEvent.from(report));
}

@Transactional
Expand All @@ -115,10 +123,13 @@ public void reportComment(

comment.incrementReportCount();

saveReport(commentId, ReportType.COMMUNITY_COMMENT, CommonReportReason, reportRequest, user);
Report report =
saveReport(
commentId, ReportType.COMMUNITY_COMMENT, CommonReportReason, reportRequest, user);
applicationEventPublisher.publishEvent(ReportCreatedEvent.from(report));
}

private void saveReport(
private Report saveReport(
Long targetId,
ReportType reportType,
CommonReportReason commonReportReason,
Expand All @@ -136,7 +147,7 @@ private void saveReport(
.user(user)
.build();

reportRepository.save(report);
return reportRepository.save(report);
}

private User findUserOrThrow(Long userId) {
Expand Down
Loading