From 0001c1d7104aca644a35532675c8780807111a14 Mon Sep 17 00:00:00 2001 From: erika0915 Date: Tue, 6 Jan 2026 13:38:16 +0900 Subject: [PATCH 1/5] =?UTF-8?q?[SCRUM-433]=20chore:=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/kkinikong/be/feedback/service/FeedbackService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java b/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java index f9f8296..f999e1c 100644 --- a/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java +++ b/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java @@ -10,14 +10,12 @@ import com.kkinikong.be.feedback.domain.Feedback; import com.kkinikong.be.feedback.dto.request.FeedbackRequest; 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; @Transactional public void addFeedback(FeedbackRequest request) { From 93b65c076f9d65a9c018f908c3ba5c41109a2bfa Mon Sep 17 00:00:00 2001 From: erika0915 Date: Tue, 6 Jan 2026 14:27:41 +0900 Subject: [PATCH 2/5] =?UTF-8?q?[SCRUM-433]=20Feat:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20=EA=B4=80=EB=A0=A8=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feedback/event/FeedbackEventListener.java | 19 +++++++++++++++++++ .../event/payload/FeedbackCreatedEvent.java | 10 ++++++++++ .../be/feedback/service/FeedbackService.java | 5 +++++ .../be/global/config/AsyncConfig.java | 11 +++++++++++ 4 files changed, 45 insertions(+) create mode 100644 src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java create mode 100644 src/main/java/com/kkinikong/be/feedback/event/payload/FeedbackCreatedEvent.java diff --git a/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java new file mode 100644 index 0000000..bd79ff8 --- /dev/null +++ b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java @@ -0,0 +1,19 @@ +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; + +@Component +@RequiredArgsConstructor +public class FeedbackEventListener { + + @Async("externalApiExecutor") + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void handleFeedbackCreatedEvent(FeedbackCreatedEvent feedbackCreatedEvent) {} +} diff --git a/src/main/java/com/kkinikong/be/feedback/event/payload/FeedbackCreatedEvent.java b/src/main/java/com/kkinikong/be/feedback/event/payload/FeedbackCreatedEvent.java new file mode 100644 index 0000000..cb3fdd4 --- /dev/null +++ b/src/main/java/com/kkinikong/be/feedback/event/payload/FeedbackCreatedEvent.java @@ -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()); + } +} diff --git a/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java b/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java index f999e1c..44b5de4 100644 --- a/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java +++ b/src/main/java/com/kkinikong/be/feedback/service/FeedbackService.java @@ -2,6 +2,7 @@ import java.util.stream.Collectors; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -9,6 +10,7 @@ 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; @Service @@ -16,6 +18,7 @@ public class FeedbackService { private final FeedbackRepository feedbackRepository; + private final ApplicationEventPublisher eventPublisher; @Transactional public void addFeedback(FeedbackRequest request) { @@ -25,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)); } } diff --git a/src/main/java/com/kkinikong/be/global/config/AsyncConfig.java b/src/main/java/com/kkinikong/be/global/config/AsyncConfig.java index 7224268..ea8da3c 100644 --- a/src/main/java/com/kkinikong/be/global/config/AsyncConfig.java +++ b/src/main/java/com/kkinikong/be/global/config/AsyncConfig.java @@ -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; + } } From 686a4e05155fad9ffe7b118ee9d7707a5493eddf Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 8 Jan 2026 00:15:49 +0900 Subject: [PATCH 3/5] =?UTF-8?q?[SCRUM-433]=20Feat:=20=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feedback/event/FeedbackEventListener.java | 7 +- .../feedback/util/discord/DiscordClient.java | 71 +++++++++++++++++++ src/main/resources/application.yml | 6 +- 3 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/kkinikong/be/feedback/util/discord/DiscordClient.java diff --git a/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java index bd79ff8..2c4c6c4 100644 --- a/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java +++ b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java @@ -8,12 +8,17 @@ import lombok.RequiredArgsConstructor; import com.kkinikong.be.feedback.event.payload.FeedbackCreatedEvent; +import com.kkinikong.be.feedback.util.discord.DiscordClient; @Component @RequiredArgsConstructor public class FeedbackEventListener { + private final DiscordClient discordClient; + @Async("externalApiExecutor") @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) - public void handleFeedbackCreatedEvent(FeedbackCreatedEvent feedbackCreatedEvent) {} + public void handleFeedbackCreatedEvent(FeedbackCreatedEvent feedbackCreatedEvent) { + discordClient.sendFeedbackAlert(feedbackCreatedEvent); + } } diff --git a/src/main/java/com/kkinikong/be/feedback/util/discord/DiscordClient.java b/src/main/java/com/kkinikong/be/feedback/util/discord/DiscordClient.java new file mode 100644 index 0000000..56c41fd --- /dev/null +++ b/src/main/java/com/kkinikong/be/feedback/util/discord/DiscordClient.java @@ -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 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(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 3d07585..74a6fe5 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -46,4 +46,8 @@ springdoc: groups-order: desc tags_sorter: alpha operations_sorter: method # delete - get - patch - post - put 정렬 - use-fqn: true \ No newline at end of file + use-fqn: true + +external-api: + discord: + feedback-url: ${DISCORD_FEEDBACK_WEBHOOK_URL} \ No newline at end of file From b750dd5c8a21c62c9b2d70dc18a03674eca85a8b Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 8 Jan 2026 12:44:49 +0900 Subject: [PATCH 4/5] =?UTF-8?q?[SCRUM-433]=20Feat:=20=EB=85=B8=EC=85=98=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=B2=A0=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feedback/event/FeedbackEventListener.java | 3 + .../be/feedback/util/notion/NotionClient.java | 62 +++++++++++++++++++ src/main/resources/application.yml | 6 +- 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java diff --git a/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java index 2c4c6c4..a4f0cdc 100644 --- a/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java +++ b/src/main/java/com/kkinikong/be/feedback/event/FeedbackEventListener.java @@ -9,16 +9,19 @@ 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); } } diff --git a/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java b/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java new file mode 100644 index 0000000..ec11710 --- /dev/null +++ b/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java @@ -0,0 +1,62 @@ +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.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(); + + Map 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())))), + "별점", Map.of("number", feedbackCreatedEvent.rating()), + "유형", Map.of("select", Map.of("name", feedbackCreatedEvent.type())), + "피드백 날짜", 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(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 74a6fe5..b437169 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -50,4 +50,8 @@ springdoc: external-api: discord: - feedback-url: ${DISCORD_FEEDBACK_WEBHOOK_URL} \ No newline at end of file + feedback-url: ${DISCORD_FEEDBACK_WEBHOOK_URL} + notion: + secret: ${NOTION_SECRET} + database-id: + feedback: ${NOTION_FEEDBACK_DATABASE_ID} \ No newline at end of file From 33771bfb3e91126126e4803c6f1be3213e029a1c Mon Sep 17 00:00:00 2001 From: erika0915 Date: Thu, 8 Jan 2026 13:19:50 +0900 Subject: [PATCH 5/5] =?UTF-8?q?[SCRUM-433]=20Feat:=20=ED=94=BC=EB=93=9C?= =?UTF-8?q?=EB=B0=B1=20api=EC=97=90=20=EB=85=B8=EC=85=98=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=20=EB=A7=9E=EC=B6=94=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../be/feedback/util/notion/NotionClient.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java b/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java index ec11710..3182a16 100644 --- a/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java +++ b/src/main/java/com/kkinikong/be/feedback/util/notion/NotionClient.java @@ -12,6 +12,7 @@ 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 @@ -32,6 +33,15 @@ public void sendFeedback(FeedbackCreatedEvent feedbackCreatedEvent) { // 현재 날짜 String today = LocalDate.now().toString(); + // 유형 + List> 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 body = Map.of( "parent", Map.of("database_id", feedbackId), @@ -41,11 +51,16 @@ public void sendFeedback(FeedbackCreatedEvent feedbackCreatedEvent) { Map.of( "title", List.of( - Map.of("text", Map.of("content", feedbackCreatedEvent.content())))), + Map.of( + "text", + Map.of( + "content", + feedbackCreatedEvent.content() != null + ? feedbackCreatedEvent.content() + : "")))), "별점", Map.of("number", feedbackCreatedEvent.rating()), - "유형", Map.of("select", Map.of("name", feedbackCreatedEvent.type())), + "유형", Map.of("multi_select", typeList), "피드백 날짜", Map.of("date", Map.of("start", today)))); - webClient .post() .uri("https://api.notion.com/v1/pages")