Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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;
}
}
10 changes: 9 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,12 @@ springdoc:
groups-order: desc
tags_sorter: alpha
operations_sorter: method # delete - get - patch - post - put 정렬
use-fqn: true
use-fqn: true

external-api:
discord:
feedback-url: ${DISCORD_FEEDBACK_WEBHOOK_URL}
notion:
secret: ${NOTION_SECRET}
database-id:
feedback: ${NOTION_FEEDBACK_DATABASE_ID}