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")