From dc8c0c2ce34750e9183a0289a9af3ad478ad5deb Mon Sep 17 00:00:00 2001
From: JaeSeo Yang <96044622+psychology50@users.noreply.github.com>
Date: Fri, 31 Jan 2025 16:12:10 +0900
Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Spending=20History=20Sharin?=
=?UTF-8?q?g=20API=20(#233)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: add spending list read method with day to spending repository
* feat: add spending list read method with day to spending service
* feat: impl daily spending aggregate service
* test: aggregate slice test
* fix: change return type to pair
* rename: fix test name
* feat: spending-chat-share-event
* feat: add spending-chat-share-exchange-properties
* chore: add spending chat share property to infra yml
* feat: impl spending chat share event handler
* fix: spending-on-dates type error in the spending-chat-share-event
* feat: impl spending-chat-share-helper
* feat: add share-to-chat-room to spending usecase
* feat: add invalid share type error code to spending-error-code
* feat: impl spending share enum type
* feat: add spending-chat-share-query dto
* feat: add missing-share-param error code to spending error code
* feat: add share-spending api to controller
* docs: write swagger docs about spending share api
* chore: apply binder & queue & event handler for share to chat room
* fix: invalid validation in the spending chat share event's name field
* feat: add share constant to the message category type
* feat: add default create message method to the send-message-command
* fix: add sender id field to spending-chat-share-event
* fix: add user id to event parameter when publish event
* feat: impl sending-share-event listener
* fix: add headers to send message command
* refactor: convert spending-share-event-listener to kotlin
* fix: add date field to spending-chat-share-event
* fix: add date to chat message's header
---
.../api/apis/ledger/api/SpendingApi.java | 27 ++++++-
.../ledger/controller/SpendingController.java | 26 +++++++
.../api/apis/ledger/dto/SpendingShareReq.java | 20 +++++
.../helper/SpendingChatShareHelper.java | 49 ++++++++++++
.../DailySpendingAggregateService.java | 39 ++++++++++
.../apis/ledger/usecase/SpendingUseCase.java | 7 ++
.../converter/SpendingShareTypeConverter.java | 17 ++++
.../api/common/query/SpendingShareType.java | 11 +++
.../DailySpendingAggregateServiceTest.java | 78 +++++++++++++++++++
.../spending/exception/SpendingErrorCode.java | 2 +
.../repository/SpendingCustomRepository.java | 2 +
.../SpendingCustomRepositoryImpl.java | 14 ++++
.../spending/service/SpendingRdbService.java | 5 ++
.../message/type/MessageCategoryType.java | 3 +-
.../common/event/SpendingChatShareEvent.java | 53 +++++++++++++
.../event/SpendingChatShareEventHandler.java | 36 +++++++++
.../SpendingChatShareExchangeProperties.java | 21 +++++
.../infra/config/MessageBrokerConfig.java | 23 +++++-
.../src/main/resources/application-infra.yml | 3 +
.../socket/command/SendMessageCommand.java | 21 ++++-
.../relay/SpendingShareEventListener.kt | 65 ++++++++++++++++
.../socket/service/ChatMessageSendService.kt | 3 +-
22 files changed, 516 insertions(+), 9 deletions(-)
create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/dto/SpendingShareReq.java
create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/helper/SpendingChatShareHelper.java
create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateService.java
create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/converter/SpendingShareTypeConverter.java
create mode 100644 pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/query/SpendingShareType.java
create mode 100644 pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateServiceTest.java
create mode 100644 pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEvent.java
create mode 100644 pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEventHandler.java
create mode 100644 pennyway-infra/src/main/java/kr/co/pennyway/infra/common/properties/SpendingChatShareExchangeProperties.java
create mode 100644 pennyway-socket/src/main/java/kr/co/pennyway/socket/relay/SpendingShareEventListener.kt
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/api/SpendingApi.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/api/SpendingApi.java
index c0ba48b1e..04a277bd5 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/api/SpendingApi.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/api/SpendingApi.java
@@ -4,16 +4,14 @@
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.ExampleObject;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.media.SchemaProperty;
+import io.swagger.v3.oas.annotations.media.*;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import kr.co.pennyway.api.apis.ledger.dto.SpendingIdsDto;
import kr.co.pennyway.api.apis.ledger.dto.SpendingReq;
import kr.co.pennyway.api.apis.ledger.dto.SpendingSearchRes;
+import kr.co.pennyway.api.apis.ledger.dto.SpendingShareReq;
import kr.co.pennyway.api.common.annotation.ApiExceptionExplanation;
import kr.co.pennyway.api.common.annotation.ApiResponseExplanations;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
@@ -109,4 +107,25 @@ public interface SpendingApi {
)
}))
ResponseEntity> deleteSpendings(@RequestBody SpendingIdsDto spendingIds, @AuthenticationPrincipal SecurityUserDetails user);
+
+ @Operation(summary = "지출 내역 공유", method = "GET", description = """
+ 사용자의 지출 내역을 공유하고 공유된 지출 내역을 반환합니다.
+ <채팅방 공유 시> 전송할 채팅방 아이디를 누락한 경우 예외를 발생시키지만, 가입하지 않은 채팅방 아이디를 전송한 경우는 유효한 방에만 전송하고 별도의 예외를 발생시키지 않습니다.
+ """)
+ @Parameters({
+ @Parameter(name = "type", description = "공유할 목적지(타입)", required = true, in = ParameterIn.QUERY, examples = {
+ @ExampleObject(name = "채팅방 공유", value = "CHAT_ROOM")
+ }),
+ @Parameter(name = "year", description = "년도", example = "2025", required = true, in = ParameterIn.QUERY),
+ @Parameter(name = "month", description = "월", example = "1", required = true, in = ParameterIn.QUERY),
+ @Parameter(name = "day", description = "일", example = "28", required = true, in = ParameterIn.QUERY),
+ @Parameter(name = "chatRoomIds", description = "공유할 채팅방 ID 목록 배열 (채팅방 공유 시, null 혹은 빈 배열 허용하지 않음.)", in = ParameterIn.QUERY, array = @ArraySchema(schema = @Schema(type = "long"))),
+ @Parameter(name = "query", hidden = true)
+ })
+ @ApiResponseExplanations(errors = {
+ @ApiExceptionExplanation(name = "전송 타입 오류", description = "유효하지 않은 목적지로 지출 내용을 공유할 수 없습니다.", value = SpendingErrorCode.class, constant = "INVALID_SHARE_TYPE"),
+ @ApiExceptionExplanation(name = "채팅방 공유 파라미터 누락", description = "지출 내역 공유 시 필수 파라미터가 누락되었습니다.", value = SpendingErrorCode.class, constant = "MISSING_SHARE_PARAM")
+ })
+ @ApiResponse(responseCode = "200")
+ ResponseEntity> shareSpending(@Validated SpendingShareReq.ShareQueryParam query, @AuthenticationPrincipal SecurityUserDetails user);
}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/controller/SpendingController.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/controller/SpendingController.java
index 720619b9f..31e634b45 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/controller/SpendingController.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/controller/SpendingController.java
@@ -3,7 +3,9 @@
import kr.co.pennyway.api.apis.ledger.api.SpendingApi;
import kr.co.pennyway.api.apis.ledger.dto.SpendingIdsDto;
import kr.co.pennyway.api.apis.ledger.dto.SpendingReq;
+import kr.co.pennyway.api.apis.ledger.dto.SpendingShareReq;
import kr.co.pennyway.api.apis.ledger.usecase.SpendingUseCase;
+import kr.co.pennyway.api.common.query.SpendingShareType;
import kr.co.pennyway.api.common.response.SuccessResponse;
import kr.co.pennyway.api.common.security.authentication.SecurityUserDetails;
import kr.co.pennyway.domain.domains.spending.exception.SpendingErrorCode;
@@ -17,6 +19,8 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
+import java.time.LocalDate;
+
@Slf4j
@RestController
@RequiredArgsConstructor
@@ -79,6 +83,28 @@ public ResponseEntity> deleteSpendings(@RequestBody SpendingIdsDto spendingIds
return ResponseEntity.ok(SuccessResponse.noContent());
}
+ @Override
+ @GetMapping("/share")
+ @PreAuthorize("isAuthenticated()")
+ public ResponseEntity> shareSpending(
+ @Validated SpendingShareReq.ShareQueryParam query,
+ @AuthenticationPrincipal SecurityUserDetails user
+ ) {
+ var date = LocalDate.of(query.year(), query.month(), query.day());
+
+ if (query.type().equals(SpendingShareType.CHAT_ROOM)) {
+ if (query.chatRoomIds() == null || query.chatRoomIds().isEmpty()) {
+ throw new SpendingErrorException(SpendingErrorCode.MISSING_SHARE_PARAM);
+ }
+
+ spendingUseCase.shareToChatRoom(user.getUserId(), query.chatRoomIds(), date);
+ } else {
+ throw new SpendingErrorException(SpendingErrorCode.INVALID_SHARE_TYPE);
+ }
+
+ return ResponseEntity.ok(SuccessResponse.noContent());
+ }
+
/**
* categoryId가 -1이면 서비스에서 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 CUSTOM이나 OTHER이 될 수 없고,
* categoryId가 -1이 아니면 사용자가 정의한 카테고리를 사용하므로 저장하려는 지출 내역의 icon은 CUSTOM임을 확인한다.
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/dto/SpendingShareReq.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/dto/SpendingShareReq.java
new file mode 100644
index 000000000..f1d704b30
--- /dev/null
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/dto/SpendingShareReq.java
@@ -0,0 +1,20 @@
+package kr.co.pennyway.api.apis.ledger.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import kr.co.pennyway.api.common.query.SpendingShareType;
+
+import java.util.List;
+
+public class SpendingShareReq {
+ @Schema(description = "지출 공유 요청")
+ public record ShareQueryParam(
+ @Schema(description = "공유 타입 (대/소문자 허용)", example = "chat_room")
+ SpendingShareType type,
+ int year,
+ int month,
+ int day,
+ @Schema(description = "공유할 채팅방 ID 배열. 공유 타입이 chat_room인 경우 필수", example = "1")
+ List chatRoomIds
+ ) {
+ }
+}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/helper/SpendingChatShareHelper.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/helper/SpendingChatShareHelper.java
new file mode 100644
index 000000000..bcd892d3a
--- /dev/null
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/helper/SpendingChatShareHelper.java
@@ -0,0 +1,49 @@
+package kr.co.pennyway.api.apis.ledger.helper;
+
+import kr.co.pennyway.api.apis.ledger.service.DailySpendingAggregateService;
+import kr.co.pennyway.common.annotation.Helper;
+import kr.co.pennyway.domain.context.account.service.UserService;
+import kr.co.pennyway.domain.context.chat.service.ChatMemberService;
+import kr.co.pennyway.domain.domains.user.exception.UserErrorCode;
+import kr.co.pennyway.domain.domains.user.exception.UserErrorException;
+import kr.co.pennyway.infra.common.event.SpendingChatShareEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationEventPublisher;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+@Slf4j
+@Helper
+@RequiredArgsConstructor
+public class SpendingChatShareHelper {
+ private final DailySpendingAggregateService dailySpendingAggregateService;
+
+ private final UserService userService;
+ private final ChatMemberService chatMemberService;
+
+ private final ApplicationEventPublisher eventPublisher;
+
+ public void execute(Long userId, List chatRoomIds, LocalDate date) {
+ var user = userService.readUser(userId)
+ .orElseThrow(() -> new UserErrorException(UserErrorCode.NOT_FOUND));
+ var aggregatedSpendings = dailySpendingAggregateService.execute(userId, date.getYear(), date.getMonthValue(), date.getDayOfMonth());
+ var joinedChatRoomIds = chatMemberService.readChatRoomIdsByUserId(userId);
+
+ var spendingOnDate = new ArrayList();
+ for (var pair : aggregatedSpendings) {
+ var categoryInfo = pair.getFirst();
+ var amount = pair.getSecond();
+
+ spendingOnDate.add(SpendingChatShareEvent.SpendingOnDate.of(categoryInfo.id(), categoryInfo.name(), categoryInfo.icon().name(), amount));
+ }
+
+ chatRoomIds.stream()
+ .filter(joinedChatRoomIds::contains)
+ .forEach(chatRoomId -> {
+ eventPublisher.publishEvent(new SpendingChatShareEvent(chatRoomId, user.getName(), user.getId(), date, spendingOnDate));
+ });
+ }
+}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateService.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateService.java
new file mode 100644
index 000000000..d53edd0f0
--- /dev/null
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateService.java
@@ -0,0 +1,39 @@
+package kr.co.pennyway.api.apis.ledger.service;
+
+import kr.co.pennyway.domain.domains.spending.domain.Spending;
+import kr.co.pennyway.domain.domains.spending.dto.CategoryInfo;
+import kr.co.pennyway.domain.domains.spending.service.SpendingRdbService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.util.Pair;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.summingLong;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DailySpendingAggregateService {
+ private final SpendingRdbService spendingRdbService;
+
+ @Transactional(readOnly = true)
+ public List> execute(Long userId, int year, int month, int day) {
+ var spendings = spendingRdbService.readSpendings(userId, year, month, day);
+
+ return spendings.stream()
+ .collect(
+ groupingBy(
+ Spending::getCategory,
+ summingLong(Spending::getAmount)
+ )
+ )
+ .entrySet().stream()
+ .map(entry -> Pair.of(entry.getKey(), entry.getValue()))
+ .sorted((o1, o2) -> (int) (o2.getSecond() - o1.getSecond()))
+ .toList();
+ }
+}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/usecase/SpendingUseCase.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/usecase/SpendingUseCase.java
index 9a3a1d59e..fbe5e1f53 100644
--- a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/usecase/SpendingUseCase.java
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/apis/ledger/usecase/SpendingUseCase.java
@@ -2,6 +2,7 @@
import kr.co.pennyway.api.apis.ledger.dto.SpendingReq;
import kr.co.pennyway.api.apis.ledger.dto.SpendingSearchRes;
+import kr.co.pennyway.api.apis.ledger.helper.SpendingChatShareHelper;
import kr.co.pennyway.api.apis.ledger.mapper.SpendingMapper;
import kr.co.pennyway.api.apis.ledger.service.SpendingDeleteService;
import kr.co.pennyway.api.apis.ledger.service.SpendingSaveService;
@@ -13,6 +14,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
+import java.time.LocalDate;
import java.util.List;
@Slf4j
@@ -24,6 +26,8 @@ public class SpendingUseCase {
private final SpendingUpdateService spendingUpdateService;
private final SpendingDeleteService spendingDeleteService;
+ private final SpendingChatShareHelper spendingChatShareHelper;
+
@Transactional
public SpendingSearchRes.Individual createSpending(Long userId, SpendingReq request) {
Spending spending = spendingSaveService.createSpending(userId, request);
@@ -62,4 +66,7 @@ public void deleteSpendings(List spendingIds) {
spendingDeleteService.deleteSpendings(spendingIds);
}
+ public void shareToChatRoom(Long userId, List chatRoomIds, LocalDate date) {
+ spendingChatShareHelper.execute(userId, chatRoomIds, date);
+ }
}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/converter/SpendingShareTypeConverter.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/converter/SpendingShareTypeConverter.java
new file mode 100644
index 000000000..bb0c8503b
--- /dev/null
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/converter/SpendingShareTypeConverter.java
@@ -0,0 +1,17 @@
+package kr.co.pennyway.api.common.converter;
+
+import kr.co.pennyway.api.common.query.SpendingShareType;
+import kr.co.pennyway.domain.domains.spending.exception.SpendingErrorCode;
+import kr.co.pennyway.domain.domains.spending.exception.SpendingErrorException;
+import org.springframework.core.convert.converter.Converter;
+
+public class SpendingShareTypeConverter implements Converter {
+ @Override
+ public SpendingShareType convert(String type) {
+ try {
+ return SpendingShareType.valueOf(type.toUpperCase());
+ } catch (IllegalArgumentException e) {
+ throw new SpendingErrorException(SpendingErrorCode.INVALID_SHARE_TYPE);
+ }
+ }
+}
diff --git a/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/query/SpendingShareType.java b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/query/SpendingShareType.java
new file mode 100644
index 000000000..5b57a1f75
--- /dev/null
+++ b/pennyway-app-external-api/src/main/java/kr/co/pennyway/api/common/query/SpendingShareType.java
@@ -0,0 +1,11 @@
+package kr.co.pennyway.api.common.query;
+
+public enum SpendingShareType {
+ CHAT_ROOM("chat_room");
+
+ private final String type;
+
+ SpendingShareType(String type) {
+ this.type = type;
+ }
+}
diff --git a/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateServiceTest.java b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateServiceTest.java
new file mode 100644
index 000000000..3016f8856
--- /dev/null
+++ b/pennyway-app-external-api/src/test/java/kr/co/pennyway/api/apis/ledger/service/DailySpendingAggregateServiceTest.java
@@ -0,0 +1,78 @@
+package kr.co.pennyway.api.apis.ledger.service;
+
+import kr.co.pennyway.api.config.ExternalApiDBTestConfig;
+import kr.co.pennyway.api.config.ExternalApiIntegrationTest;
+import kr.co.pennyway.api.config.fixture.UserFixture;
+import kr.co.pennyway.domain.domains.spending.domain.Spending;
+import kr.co.pennyway.domain.domains.spending.domain.SpendingCustomCategory;
+import kr.co.pennyway.domain.domains.spending.repository.SpendingCustomCategoryRepository;
+import kr.co.pennyway.domain.domains.spending.repository.SpendingRepository;
+import kr.co.pennyway.domain.domains.spending.type.SpendingCategory;
+import kr.co.pennyway.domain.domains.user.domain.User;
+import kr.co.pennyway.domain.domains.user.repository.UserRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.time.LocalDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@Slf4j
+@ExternalApiIntegrationTest
+public class DailySpendingAggregateServiceTest extends ExternalApiDBTestConfig {
+ @Autowired
+ private UserRepository userRepository;
+
+ @Autowired
+ private SpendingRepository spendingRepository;
+
+ @Autowired
+ private SpendingCustomCategoryRepository spendingCustomCategoryRepository;
+
+ @Autowired
+ private DailySpendingAggregateService dailySpendingAggregateService;
+
+ private static Spending createSpending(String accountName, LocalDateTime spendAt, SpendingCategory category, Integer amount, SpendingCustomCategory spendingCustomCategory, User user) {
+ return Spending.builder()
+ .accountName(accountName)
+ .spendAt(spendAt)
+ .category(category)
+ .amount(amount)
+ .spendingCustomCategory(spendingCustomCategory)
+ .user(user)
+ .build();
+ }
+
+ @Test
+ public void shouldReturnDailySpendingDescOrder() {
+ // given
+ var user = userRepository.save(UserFixture.GENERAL_USER.toUser());
+ var spendingCustomCategory1 = spendingCustomCategoryRepository.save(SpendingCustomCategory.of("커스텀1", SpendingCategory.EDUCATION, user));
+ var spendingCustomCategory2 = spendingCustomCategoryRepository.save(SpendingCustomCategory.of("커스텀2", SpendingCategory.FOOD, user));
+
+ var today = LocalDateTime.now();
+
+ var defaultFoodSpending1 = spendingRepository.save(createSpending("시스템 카테고리 지출1", today, SpendingCategory.FOOD, 10000, null, user));
+ var defaultFoodSpending2 = spendingRepository.save(createSpending("시스템 카테고리 지출2", today, SpendingCategory.FOOD, 20000, null, user));
+ var defaultEducationSpending1 = spendingRepository.save(createSpending("시스템 카테고리 지출3", today, SpendingCategory.EDUCATION, 30000, null, user));
+ var defaultEducationSpending2 = spendingRepository.save(createSpending("시스템 카테고리 지출4", today, SpendingCategory.EDUCATION, 40000, null, user));
+ var systemEducationSpending1 = spendingRepository.save(createSpending("커스텀 카테고리 지출1", today, SpendingCategory.CUSTOM, 50000, spendingCustomCategory1, user));
+ var systemEducationSpending2 = spendingRepository.save(createSpending("커스텀 카테고리 지출2", today, SpendingCategory.CUSTOM, 60000, spendingCustomCategory1, user));
+ var systemFoodSpending1 = spendingRepository.save(createSpending("커스텀 카테고리 지출3", today, SpendingCategory.CUSTOM, 70000, spendingCustomCategory2, user));
+ var systemFoodSpending2 = spendingRepository.save(createSpending("커스텀 카테고리 지출4", today, SpendingCategory.CUSTOM, 80000, spendingCustomCategory2, user));
+
+ // when
+ var result = dailySpendingAggregateService.execute(user.getId(), today.getYear(), today.getMonthValue(), today.getDayOfMonth());
+
+ // then
+ assertEquals(result.get(0).getFirst(), systemFoodSpending1.getCategory());
+ assertEquals(result.get(0).getSecond(), systemFoodSpending1.getAmount() + systemFoodSpending2.getAmount());
+ assertEquals(result.get(1).getFirst(), systemEducationSpending1.getCategory());
+ assertEquals(result.get(1).getSecond(), systemEducationSpending1.getAmount() + systemEducationSpending2.getAmount());
+ assertEquals(result.get(2).getFirst(), defaultEducationSpending1.getCategory());
+ assertEquals(result.get(2).getSecond(), defaultEducationSpending1.getAmount() + defaultEducationSpending2.getAmount());
+ assertEquals(result.get(3).getFirst(), defaultFoodSpending1.getCategory());
+ assertEquals(result.get(3).getSecond(), defaultFoodSpending1.getAmount() + defaultFoodSpending2.getAmount());
+ }
+}
diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/exception/SpendingErrorCode.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/exception/SpendingErrorCode.java
index eb38dc5f4..6cfb30c2c 100644
--- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/exception/SpendingErrorCode.java
+++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/exception/SpendingErrorCode.java
@@ -15,6 +15,8 @@ public enum SpendingErrorCode implements BaseErrorCode {
INVALID_ICON_WITH_CATEGORY_ID(StatusCode.BAD_REQUEST, ReasonCode.CLIENT_ERROR, "icon의 정보와 categoryId의 정보가 존재할 수 없는 조합입니다."),
INVALID_TYPE_WITH_CATEGORY_ID(StatusCode.BAD_REQUEST, ReasonCode.CLIENT_ERROR, "type의 정보와 categoryId의 정보가 존재할 수 없는 조합입니다."),
INVALID_CATEGORY_TYPE(StatusCode.BAD_REQUEST, ReasonCode.CLIENT_ERROR, "존재하지 않는 카테고리 타입입니다."),
+ INVALID_SHARE_TYPE(StatusCode.BAD_REQUEST, ReasonCode.MALFORMED_PARAMETER, "부적절한 공유 타입입니다."),
+ MISSING_SHARE_PARAM(StatusCode.BAD_REQUEST, ReasonCode.MISSING_REQUIRED_PARAMETER, "지출 내역 공유 시 필수 파라미터가 누락되었습니다."),
/* 404 Not Found */
NOT_FOUND_SPENDING(StatusCode.NOT_FOUND, ReasonCode.REQUESTED_RESOURCE_NOT_FOUND, "존재하지 않는 지출 내역입니다."),
diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepository.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepository.java
index 3c94a3481..c0c5d7619 100644
--- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepository.java
+++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepository.java
@@ -10,4 +10,6 @@ public interface SpendingCustomRepository {
Optional findTotalSpendingAmountByUserId(Long userId, int year, int month);
List findByYearAndMonth(Long userId, int year, int month);
+
+ List findByYearAndMonthAndDay(Long userId, int year, int month, int day);
}
diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepositoryImpl.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepositoryImpl.java
index 7c780895c..89b05dfbc 100644
--- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepositoryImpl.java
+++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/repository/SpendingCustomRepositoryImpl.java
@@ -61,4 +61,18 @@ public List findByYearAndMonth(Long userId, int year, int month) {
.orderBy(orderSpecifiers.toArray(new OrderSpecifier[0]))
.fetch();
}
+
+ @Override
+ public List findByYearAndMonthAndDay(Long userId, int year, int month, int day) {
+ Sort sort = Sort.by(Sort.Order.desc("spendAt"));
+
+ return queryFactory.selectFrom(spending)
+ .leftJoin(spending.spendingCustomCategory, spendingCustomCategory).fetchJoin()
+ .where(spending.spendAt.year().eq(year)
+ .and(spending.spendAt.month().eq(month))
+ .and(spending.spendAt.dayOfMonth().eq(day))
+ .and(spending.user.id.eq(userId))
+ )
+ .fetch();
+ }
}
diff --git a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/service/SpendingRdbService.java b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/service/SpendingRdbService.java
index a8c2c08a7..5f6eb2df5 100644
--- a/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/service/SpendingRdbService.java
+++ b/pennyway-domain/domain-rdb/src/main/java/kr/co/pennyway/domain/domains/spending/service/SpendingRdbService.java
@@ -50,6 +50,11 @@ public List readSpendings(Long userId, int year, int month) {
return spendingRepository.findByYearAndMonth(userId, year, month);
}
+ @Transactional(readOnly = true)
+ public List readSpendings(Long userId, int year, int month, int day) {
+ return spendingRepository.findByYearAndMonthAndDay(userId, year, month, day);
+ }
+
@Transactional(readOnly = true)
public int readSpendingTotalCountByCategoryId(Long userId, Long categoryId) {
return spendingRepository.countByUser_IdAndSpendingCustomCategory_Id(userId, categoryId);
diff --git a/pennyway-domain/domain-redis/src/main/java/kr/co/pennyway/domain/domains/message/type/MessageCategoryType.java b/pennyway-domain/domain-redis/src/main/java/kr/co/pennyway/domain/domains/message/type/MessageCategoryType.java
index 869f62f23..971e8cd8a 100644
--- a/pennyway-domain/domain-redis/src/main/java/kr/co/pennyway/domain/domains/message/type/MessageCategoryType.java
+++ b/pennyway-domain/domain-redis/src/main/java/kr/co/pennyway/domain/domains/message/type/MessageCategoryType.java
@@ -9,7 +9,8 @@
@RequiredArgsConstructor
public enum MessageCategoryType {
NORMAL("0", "NORMAL"),
- SYSTEM("1", "SYSTEM");
+ SYSTEM("1", "SYSTEM"),
+ SHARE("2", "SHARE");
private static final Map stringToEnum = Stream.of(values()).collect(java.util.stream.Collectors.toMap(Object::toString, e -> e));
private final String code;
diff --git a/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEvent.java b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEvent.java
new file mode 100644
index 000000000..c3f2951a8
--- /dev/null
+++ b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEvent.java
@@ -0,0 +1,53 @@
+package kr.co.pennyway.infra.common.event;
+
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
+
+public record SpendingChatShareEvent(
+ Long chatRoomId,
+ String name,
+ Long senderId,
+ LocalDate date,
+ List spendingOnDates
+) {
+ public SpendingChatShareEvent {
+ Objects.requireNonNull(chatRoomId, "chatRoomId는 null일 수 없습니다.");
+ Objects.requireNonNull(name, "name은 null일 수 없습니다.");
+ Objects.requireNonNull(senderId, "senderId는 null일 수 없습니다.");
+ Objects.requireNonNull(date, "date는 null일 수 없습니다.");
+ Objects.requireNonNull(spendingOnDates, "spendingOnDates는 null일 수 없습니다.");
+ }
+
+ public record SpendingOnDate(
+ boolean isCustom,
+ Long categoryId,
+ String name,
+ String icon,
+ Long amount
+ ) {
+ public SpendingOnDate {
+ Objects.requireNonNull(categoryId, "categoryId는 null일 수 없습니다.");
+ Objects.requireNonNull(icon, "icon은 null일 수 없습니다.");
+ Objects.requireNonNull(amount, "amount는 null일 수 없습니다.");
+
+ if (isCustom && categoryId < 0 || !isCustom && categoryId != -1) {
+ throw new IllegalArgumentException("isCustom이 " + isCustom + "일 때 categoryId는 " + (isCustom ? "0 이상" : "-1") + "이어야 합니다.");
+ }
+
+ if (isCustom && icon.equals("CUSTOM")) {
+ throw new IllegalArgumentException("사용자 정의 카테고리는 OTHER가 될 수 없습니다.");
+ }
+
+ if (!StringUtils.hasText(name)) {
+ throw new IllegalArgumentException("name은 null이거나 빈 문자열일 수 없습니다.");
+ }
+ }
+
+ public static SpendingOnDate of(Long categoryId, String name, String icon, Long amount) {
+ return new SpendingOnDate(!categoryId.equals(-1L), categoryId, name, icon, amount);
+ }
+ }
+}
diff --git a/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEventHandler.java b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEventHandler.java
new file mode 100644
index 000000000..4619e999c
--- /dev/null
+++ b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/event/SpendingChatShareEventHandler.java
@@ -0,0 +1,36 @@
+package kr.co.pennyway.infra.common.event;
+
+import kr.co.pennyway.infra.client.broker.MessageBrokerAdapter;
+import kr.co.pennyway.infra.common.properties.ChatExchangeProperties;
+import kr.co.pennyway.infra.common.properties.SpendingChatShareExchangeProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.event.EventListener;
+import org.springframework.messaging.MessageHeaders;
+import org.springframework.messaging.support.MessageBuilder;
+import org.springframework.scheduling.annotation.Async;
+
+import java.util.Map;
+
+@Slf4j
+@RequiredArgsConstructor
+public class SpendingChatShareEventHandler {
+ private final MessageBrokerAdapter messageBrokerAdapter;
+ private final ChatExchangeProperties chatExchangeProperties;
+ private final SpendingChatShareExchangeProperties spendingChatShareExchangeProperties;
+
+ @Async
+ @EventListener
+ public void handle(SpendingChatShareEvent event) {
+ log.debug("handle: {}", event);
+
+ var headers = new MessageHeaders(Map.of("Content-Type", "application/json"));
+ var message = MessageBuilder.createMessage(event, headers);
+
+ messageBrokerAdapter.send(
+ chatExchangeProperties.getExchange(),
+ spendingChatShareExchangeProperties.getRoutingKey(),
+ message
+ );
+ }
+}
diff --git a/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/properties/SpendingChatShareExchangeProperties.java b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/properties/SpendingChatShareExchangeProperties.java
new file mode 100644
index 000000000..242c42ec0
--- /dev/null
+++ b/pennyway-infra/src/main/java/kr/co/pennyway/infra/common/properties/SpendingChatShareExchangeProperties.java
@@ -0,0 +1,21 @@
+package kr.co.pennyway.infra.common.properties;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+@Getter
+@RequiredArgsConstructor
+@ConfigurationProperties(prefix = "pennyway.rabbitmq.spending-chat-share")
+public class SpendingChatShareExchangeProperties {
+ private final String queue;
+ private final String routingKey;
+
+ @Override
+ public String toString() {
+ return "SpendingChatShareExchangeProperties{" +
+ "queue='" + queue + '\'' +
+ ", routingKey='" + routingKey + '\'' +
+ '}';
+ }
+}
diff --git a/pennyway-infra/src/main/java/kr/co/pennyway/infra/config/MessageBrokerConfig.java b/pennyway-infra/src/main/java/kr/co/pennyway/infra/config/MessageBrokerConfig.java
index b56061b92..69961bb20 100644
--- a/pennyway-infra/src/main/java/kr/co/pennyway/infra/config/MessageBrokerConfig.java
+++ b/pennyway-infra/src/main/java/kr/co/pennyway/infra/config/MessageBrokerConfig.java
@@ -7,10 +7,12 @@
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import kr.co.pennyway.infra.client.broker.MessageBrokerAdapter;
import kr.co.pennyway.infra.common.event.ChatRoomJoinEventHandler;
+import kr.co.pennyway.infra.common.event.SpendingChatShareEventHandler;
import kr.co.pennyway.infra.common.importer.PennywayInfraConfig;
import kr.co.pennyway.infra.common.properties.ChatExchangeProperties;
import kr.co.pennyway.infra.common.properties.ChatJoinEventExchangeProperties;
import kr.co.pennyway.infra.common.properties.RabbitMqProperties;
+import kr.co.pennyway.infra.common.properties.SpendingChatShareExchangeProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
@@ -35,11 +37,12 @@
@Slf4j
@EnableRabbit
@RequiredArgsConstructor
-@EnableConfigurationProperties({ChatExchangeProperties.class, ChatJoinEventExchangeProperties.class, RabbitMqProperties.class})
+@EnableConfigurationProperties({ChatExchangeProperties.class, ChatJoinEventExchangeProperties.class, RabbitMqProperties.class, SpendingChatShareExchangeProperties.class})
public class MessageBrokerConfig implements PennywayInfraConfig {
private final RabbitMqProperties rabbitMqProperties;
private final ChatExchangeProperties chatExchangeProperties;
private final ChatJoinEventExchangeProperties chatJoinEventExchangeProperties;
+ private final SpendingChatShareExchangeProperties spendingChatShareExchangeProperties;
@Bean
public TopicExchange chatExchange() {
@@ -56,6 +59,11 @@ public Queue chatJoinEventQueue(ChatJoinEventExchangeProperties chatJoinEventExc
return new Queue(chatJoinEventExchangeProperties.getQueue(), true);
}
+ @Bean
+ public Queue spendingChatShareQueue(SpendingChatShareExchangeProperties spendingChatShareExchangeProperties) {
+ return new Queue(spendingChatShareExchangeProperties.getQueue(), true);
+ }
+
@Bean
public Binding chatBinding(Queue chatQueue, TopicExchange chatExchange) {
return BindingBuilder
@@ -72,6 +80,14 @@ public Binding chatJoinEventBinding(Queue chatJoinEventQueue, TopicExchange chat
.with(chatJoinEventExchangeProperties.getRoutingKey());
}
+ @Bean
+ public Binding spendingShareEventBinding(Queue spendingChatShareQueue, TopicExchange chatExchange) {
+ return BindingBuilder
+ .bind(spendingChatShareQueue)
+ .to(chatExchange)
+ .with(spendingChatShareExchangeProperties.getRoutingKey());
+ }
+
@Bean
public Module dateTimeModule() {
return new JavaTimeModule();
@@ -151,4 +167,9 @@ public MessageBrokerAdapter messageBrokerAdapter(RabbitMessagingTemplate rabbitM
public ChatRoomJoinEventHandler chatRoomJoinEventHandler(MessageBrokerAdapter messageBrokerAdapter, ChatExchangeProperties chatExchangeProperties, ChatJoinEventExchangeProperties chatJoinEventExchangeProperties) {
return new ChatRoomJoinEventHandler(messageBrokerAdapter, chatExchangeProperties, chatJoinEventExchangeProperties);
}
+
+ @Bean
+ public SpendingChatShareEventHandler spendingChatShareEventHandler(MessageBrokerAdapter messageBrokerAdapter, ChatExchangeProperties chatExchangeProperties, SpendingChatShareExchangeProperties spendingChatShareExchangeProperties) {
+ return new SpendingChatShareEventHandler(messageBrokerAdapter, chatExchangeProperties, spendingChatShareExchangeProperties);
+ }
}
diff --git a/pennyway-infra/src/main/resources/application-infra.yml b/pennyway-infra/src/main/resources/application-infra.yml
index 818f50a44..4b33bc6f4 100644
--- a/pennyway-infra/src/main/resources/application-infra.yml
+++ b/pennyway-infra/src/main/resources/application-infra.yml
@@ -63,6 +63,9 @@ pennyway:
chat-join-event:
queue: ${RABBITMQ_CHAT_JOIN_QUEUE:chat.join.queue}
routing-key: ${RABBITMQ_CHAT_JOIN_ROUTING:chat.join.*}
+ spending-chat-share:
+ queue: ${RABBITMQ_SPENDING_CHAT_QUEUE:spending.chat.queue}
+ routing-key: ${RABBITMQ_SPENDING_CHAT_EXCHANGE:chat.share.spending.*}
oauth2:
client:
diff --git a/pennyway-socket/src/main/java/kr/co/pennyway/socket/command/SendMessageCommand.java b/pennyway-socket/src/main/java/kr/co/pennyway/socket/command/SendMessageCommand.java
index f27a912e5..22c01928b 100644
--- a/pennyway-socket/src/main/java/kr/co/pennyway/socket/command/SendMessageCommand.java
+++ b/pennyway-socket/src/main/java/kr/co/pennyway/socket/command/SendMessageCommand.java
@@ -1,5 +1,6 @@
package kr.co.pennyway.socket.command;
+import jakarta.annotation.Nullable;
import kr.co.pennyway.domain.domains.message.type.MessageCategoryType;
import kr.co.pennyway.domain.domains.message.type.MessageContentType;
import kr.co.pennyway.socket.common.constants.SystemMessageConstants;
@@ -16,7 +17,8 @@ public record SendMessageCommand(
MessageCategoryType categoryType,
long senderId,
String senderName,
- Map messageIdHeader
+ Map messageIdHeader,
+ @Nullable Map headers
) {
public SendMessageCommand {
if (chatRoomId <= 0) {
@@ -51,6 +53,7 @@ public static SendMessageCommand createSystemMessage(long chatRoomId, String con
MessageCategoryType.SYSTEM,
SystemMessageConstants.SYSTEM_SENDER_ID,
null,
+ null,
null
);
}
@@ -74,7 +77,21 @@ public static SendMessageCommand createUserMessage(long chatRoomId, String conte
MessageCategoryType.NORMAL,
senderId,
senderName,
- messageIdHeader
+ messageIdHeader,
+ null
+ );
+ }
+
+ public static SendMessageCommand createMessage(long chatRoomId, String content, MessageContentType contentType, MessageCategoryType categoryType, long senderId, String senderName, Map messageIdHeader, @Nullable Map headers) {
+ return new SendMessageCommand(
+ chatRoomId,
+ content,
+ contentType,
+ categoryType,
+ senderId,
+ senderName,
+ messageIdHeader,
+ headers
);
}
}
\ No newline at end of file
diff --git a/pennyway-socket/src/main/java/kr/co/pennyway/socket/relay/SpendingShareEventListener.kt b/pennyway-socket/src/main/java/kr/co/pennyway/socket/relay/SpendingShareEventListener.kt
new file mode 100644
index 000000000..cde1a95b8
--- /dev/null
+++ b/pennyway-socket/src/main/java/kr/co/pennyway/socket/relay/SpendingShareEventListener.kt
@@ -0,0 +1,65 @@
+package kr.co.pennyway.socket.relay;
+
+import com.fasterxml.jackson.databind.ObjectMapper
+import kr.co.pennyway.domain.domains.message.type.MessageCategoryType
+import kr.co.pennyway.domain.domains.message.type.MessageContentType
+import kr.co.pennyway.infra.common.event.SpendingChatShareEvent
+import kr.co.pennyway.infra.common.properties.ChatExchangeProperties
+import kr.co.pennyway.socket.command.SendMessageCommand
+import kr.co.pennyway.socket.common.util.logger
+import kr.co.pennyway.socket.service.ChatMessageSendService
+import lombok.extern.slf4j.Slf4j
+import org.springframework.amqp.rabbit.annotation.Exchange
+import org.springframework.amqp.rabbit.annotation.Queue
+import org.springframework.amqp.rabbit.annotation.QueueBinding
+import org.springframework.amqp.rabbit.annotation.RabbitListener
+import org.springframework.boot.context.properties.EnableConfigurationProperties
+import org.springframework.stereotype.Component
+
+@Slf4j
+@Component
+@EnableConfigurationProperties(ChatExchangeProperties::class)
+class SpendingShareEventListener(
+ private val chatMessageSendService: ChatMessageSendService,
+ private val objectMapper: ObjectMapper
+) {
+ private companion object {
+ private val log = logger()
+ }
+
+ @RabbitListener(
+ containerFactory = "simpleRabbitListenerContainerFactory",
+ bindings = [QueueBinding(
+ value = Queue("\${pennyway.rabbitmq.spending-chat-share.queue}"),
+ exchange = Exchange(value = "\${pennyway.rabbitmq.chat.exchange}", type = "topic"),
+ key = ["\${pennyway.rabbitmq.spending-chat-share.routing-key}"]
+ )]
+ )
+ fun handle(event: SpendingChatShareEvent) {
+ log.debug("handle: {}", event)
+
+ convertToJson(event.spendingOnDates())
+ .getOrNull()
+ ?.let { payload ->
+ chatMessageSendService.execute(
+ SendMessageCommand.createMessage(
+ event.chatRoomId(),
+ payload,
+ MessageContentType.TEXT,
+ MessageCategoryType.SHARE,
+ event.senderId(),
+ event.name(),
+ null,
+ mapOf("Content-Type" to "application/json", "date" to event.date())
+ )
+ )
+ }
+ }
+
+ private fun convertToJson(spendingOnDates: List): Result =
+ runCatching {
+ objectMapper.writeValueAsString(spendingOnDates)
+ }.onFailure {
+ log.error("Failed to serialize spendingOnDates", it)
+ }
+}
\ No newline at end of file
diff --git a/pennyway-socket/src/main/java/kr/co/pennyway/socket/service/ChatMessageSendService.kt b/pennyway-socket/src/main/java/kr/co/pennyway/socket/service/ChatMessageSendService.kt
index 4f8fb2c61..7ba6a8382 100644
--- a/pennyway-socket/src/main/java/kr/co/pennyway/socket/service/ChatMessageSendService.kt
+++ b/pennyway-socket/src/main/java/kr/co/pennyway/socket/service/ChatMessageSendService.kt
@@ -33,7 +33,8 @@ class ChatMessageSendService(
messageBrokerAdapter.convertAndSend(
exchange,
"chat.room.${command.chatRoomId}",
- ChatMessageDto.Response.from(message)
+ ChatMessageDto.Response.from(message),
+ command.headers
)
}