From 9997ce0b26b764d87969b666d06ae4329961b595 Mon Sep 17 00:00:00 2001 From: 1000hyehyang Date: Thu, 12 Feb 2026 19:51:30 +0900 Subject: [PATCH] =?UTF-8?q?fix(#391);=20=EC=BA=A0=ED=8E=98=EC=9D=B8=20?= =?UTF-8?q?=EC=A0=9C=EC=95=88=20=EC=96=91=EB=B0=A9=ED=96=A5=20=EA=B3=A0?= =?UTF-8?q?=EB=A0=A4=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B0=8F=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EC=84=A4=EA=B3=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/CampaignProposalSentEvent.java | 1 + .../service/CampaignProposalService.java | 1 + .../apply/CampaignApplySentEventListener.java | 4 +- .../CampaignProposalSentEventListener.java | 8 ++-- .../service/room/ChatRoomCommandService.java | 11 +++++- .../room/ChatRoomCommandServiceImpl.java | 39 +++++++++++++------ .../tx/SpringAfterCommitExecutor.java | 11 ++++-- .../rest/controller/ChatController.java | 2 +- 8 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/example/RealMatch/business/application/event/CampaignProposalSentEvent.java b/src/main/java/com/example/RealMatch/business/application/event/CampaignProposalSentEvent.java index 95886b97..daec7625 100644 --- a/src/main/java/com/example/RealMatch/business/application/event/CampaignProposalSentEvent.java +++ b/src/main/java/com/example/RealMatch/business/application/event/CampaignProposalSentEvent.java @@ -8,6 +8,7 @@ */ public record CampaignProposalSentEvent( Long proposalId, + Long actorUserId, Long brandUserId, Long creatorUserId, Long campaignId, diff --git a/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalService.java b/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalService.java index c3362ac3..22d13967 100644 --- a/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalService.java +++ b/src/main/java/com/example/RealMatch/business/application/service/CampaignProposalService.java @@ -336,6 +336,7 @@ private void publishProposalSentEvent(CampaignProposal proposal, boolean isRePro CampaignProposalSentEvent event = new CampaignProposalSentEvent( proposal.getId(), + proposal.getSenderUserId(), brandUserId, creatorUserId, campaignId, diff --git a/src/main/java/com/example/RealMatch/chat/application/event/apply/CampaignApplySentEventListener.java b/src/main/java/com/example/RealMatch/chat/application/event/apply/CampaignApplySentEventListener.java index c5d00b8b..818293d6 100644 --- a/src/main/java/com/example/RealMatch/chat/application/event/apply/CampaignApplySentEventListener.java +++ b/src/main/java/com/example/RealMatch/chat/application/event/apply/CampaignApplySentEventListener.java @@ -50,11 +50,11 @@ public void handleCampaignApplySent(CampaignApplySentEvent event) { /** * 채팅방이 없으면 생성하고, roomId를 반환합니다. - * 이 리스너는 AFTER_COMMIT 컨텍스트에서 실행되므로 createOrGetRoom이 별도 트랜잭션으로 처리됩니다. + * 이벤트 기반 자동 생성이므로 createOrGetRoomSystem 사용 (권한 검증 없음). */ private Long ensureRoomAndGetId(Long brandUserId, Long creatorUserId) { return chatRoomCommandService - .createOrGetRoom(brandUserId, brandUserId, creatorUserId) + .createOrGetRoomSystem(brandUserId, creatorUserId) .roomId(); } diff --git a/src/main/java/com/example/RealMatch/chat/application/event/proposal/CampaignProposalSentEventListener.java b/src/main/java/com/example/RealMatch/chat/application/event/proposal/CampaignProposalSentEventListener.java index fb8a567c..f8d6620f 100644 --- a/src/main/java/com/example/RealMatch/chat/application/event/proposal/CampaignProposalSentEventListener.java +++ b/src/main/java/com/example/RealMatch/chat/application/event/proposal/CampaignProposalSentEventListener.java @@ -41,7 +41,7 @@ public void handleCampaignProposalSent(CampaignProposalSentEvent event) { event.proposalId(), event.isReProposal()); // 채팅방이 없으면 생성 - Long roomId = ensureRoomAndGetId(event.brandUserId(), event.creatorUserId()); + Long roomId = ensureRoomAndGetId(event); ChatProposalCardPayloadResponse payload = createPayload(event); String eventId = ProposalSentEvent.generateEventId(event.proposalId(), event.isReProposal()); @@ -80,11 +80,11 @@ private ChatProposalDecisionStatus toChatDecisionStatus(ProposalStatus proposalS /** * 채팅방이 없으면 생성하고, roomId를 반환합니다. - * 이 리스너는 AFTER_COMMIT 컨텍스트에서 실행되므로 createOrGetRoom이 별도 트랜잭션으로 처리됩니다. + * 이벤트 기반 자동 생성이므로 createOrGetRoomSystem 사용 (권한 검증 없음). */ - private Long ensureRoomAndGetId(Long brandUserId, Long creatorUserId) { + private Long ensureRoomAndGetId(CampaignProposalSentEvent event) { return chatRoomCommandService - .createOrGetRoom(brandUserId, brandUserId, creatorUserId) + .createOrGetRoomSystem(event.brandUserId(), event.creatorUserId()) .roomId(); } diff --git a/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandService.java b/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandService.java index 23b0e90a..113402d4 100644 --- a/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandService.java +++ b/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandService.java @@ -3,5 +3,14 @@ import com.example.RealMatch.chat.presentation.dto.response.ChatRoomCreateResponse; public interface ChatRoomCommandService { - ChatRoomCreateResponse createOrGetRoom(Long userId, Long brandId, Long creatorId); + + /** + * 사용자 요청 시 사용. 요청자(userId)가 brandId 또는 creatorId일 때만 허용 + */ + ChatRoomCreateResponse createOrGetRoomAsMember(Long userId, Long brandId, Long creatorId); + + /** + * 이벤트/시스템에서 사용. 권한 검증 없음. + */ + ChatRoomCreateResponse createOrGetRoomSystem(Long brandId, Long creatorId); } diff --git a/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandServiceImpl.java b/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandServiceImpl.java index 3c6054d6..974d4759 100644 --- a/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandServiceImpl.java +++ b/src/main/java/com/example/RealMatch/chat/application/service/room/ChatRoomCommandServiceImpl.java @@ -4,6 +4,7 @@ import org.slf4j.LoggerFactory; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import com.example.RealMatch.chat.application.cache.ChatCacheInvalidationService; @@ -35,9 +36,34 @@ public class ChatRoomCommandServiceImpl implements ChatRoomCommandService { @Override @Transactional - public ChatRoomCreateResponse createOrGetRoom(Long userId, Long brandId, Long creatorId) { - validateRequest(userId, brandId, creatorId); + public ChatRoomCreateResponse createOrGetRoomAsMember(Long userId, Long brandId, Long creatorId) { + validateMemberRequest(userId, brandId, creatorId); + return findOrCreateRoom(brandId, creatorId); + } + + @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) + public ChatRoomCreateResponse createOrGetRoomSystem(Long brandId, Long creatorId) { + validateSystemRequest(brandId, creatorId); + return findOrCreateRoom(brandId, creatorId); + } + private void validateMemberRequest(Long userId, Long brandId, Long creatorId) { + if (brandId == null || creatorId == null || brandId.equals(creatorId)) { + throw new CustomException(ChatErrorCode.INVALID_ROOM_REQUEST); + } + if (!userId.equals(brandId) && !userId.equals(creatorId)) { + throw new CustomException(ChatErrorCode.NOT_ROOM_MEMBER); + } + } + + private void validateSystemRequest(Long brandId, Long creatorId) { + if (brandId == null || creatorId == null || brandId.equals(creatorId)) { + throw new CustomException(ChatErrorCode.INVALID_ROOM_REQUEST); + } + } + + private ChatRoomCreateResponse findOrCreateRoom(Long brandId, Long creatorId) { String roomKey = ChatRoomKeyGenerator.createDirectRoomKey(brandId, creatorId); ChatRoom room = chatRoomRepository.findByRoomKey(roomKey).orElse(null); @@ -52,15 +78,6 @@ public ChatRoomCreateResponse createOrGetRoom(Long userId, Long brandId, Long cr ); } - private void validateRequest(Long userId, Long brandId, Long creatorId) { - if (brandId == null || creatorId == null || brandId.equals(creatorId)) { - throw new CustomException(ChatErrorCode.INVALID_ROOM_REQUEST); - } - if (!userId.equals(brandId) && !userId.equals(creatorId)) { - throw new CustomException(ChatErrorCode.NOT_ROOM_MEMBER); - } - } - private ChatRoom createRoomWithMembers( String roomKey, Long brandId, diff --git a/src/main/java/com/example/RealMatch/chat/infrastructure/tx/SpringAfterCommitExecutor.java b/src/main/java/com/example/RealMatch/chat/infrastructure/tx/SpringAfterCommitExecutor.java index 3eafad13..402d9e72 100644 --- a/src/main/java/com/example/RealMatch/chat/infrastructure/tx/SpringAfterCommitExecutor.java +++ b/src/main/java/com/example/RealMatch/chat/infrastructure/tx/SpringAfterCommitExecutor.java @@ -39,9 +39,12 @@ public void afterCommit() { return; } - String message = "AfterCommitExecutor must be used within an active transaction. " + - "hasTransaction=%s, hasSynchronization=%s".formatted(hasTransaction, hasSynchronization); - LOG.error(message); - throw new IllegalStateException(message); + LOG.warn("[AfterCommitExecutor] No active transaction. Executing immediately. hasTransaction={}, hasSynchronization={}", + hasTransaction, hasSynchronization); + try { + task.run(); + } catch (Exception ex) { + LOG.error("Exception occurred in fallback task execution (no transaction).", ex); + } } } diff --git a/src/main/java/com/example/RealMatch/chat/presentation/rest/controller/ChatController.java b/src/main/java/com/example/RealMatch/chat/presentation/rest/controller/ChatController.java index 2b37b445..93b3048d 100644 --- a/src/main/java/com/example/RealMatch/chat/presentation/rest/controller/ChatController.java +++ b/src/main/java/com/example/RealMatch/chat/presentation/rest/controller/ChatController.java @@ -44,7 +44,7 @@ public CustomResponse createOrGetRoom( Long userId = user.getUserId(); Long brandId = request.brandId(); Long creatorId = request.creatorId(); - return CustomResponse.ok(chatRoomCommandService.createOrGetRoom(userId, brandId, creatorId)); + return CustomResponse.ok(chatRoomCommandService.createOrGetRoomAsMember(userId, brandId, creatorId)); } @GetMapping("/rooms")