diff --git a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java index 9879efc..49ab5c8 100644 --- a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java +++ b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomItemResponse.java @@ -8,6 +8,7 @@ public record ChatRoomItemResponse( Long chatRoomId, ChatType chatType, String chatRoomName, + String thumbnail, Long groupId, int participantCount, LastMessageResponse lastMessage, @@ -17,6 +18,7 @@ public record ChatRoomItemResponse( public static ChatRoomItemResponse of( ChatRoom chatRoom, String chatRoomName, + String thumbnail, int participantCount, LastMessageResponse lastMessage, int unreadCount @@ -25,6 +27,7 @@ public static ChatRoomItemResponse of( chatRoom.getId(), chatRoom.getChatType(), chatRoomName, + thumbnail, chatRoom.getGroup() != null ? chatRoom.getGroup().getId() : null, participantCount, lastMessage, diff --git a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java index bc2bca2..f821476 100644 --- a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java +++ b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ChatRoomResponse.java @@ -9,6 +9,7 @@ public record ChatRoomResponse( Long chatRoomId, ChatType chatType, String chatRoomName, + String thumbnail, Long groupId, int participantCount, List participants, @@ -18,12 +19,14 @@ public record ChatRoomResponse( public static ChatRoomResponse of( ChatRoom chatRoom, String chatRoomName, + String thumbnail, List participants ) { return new ChatRoomResponse( chatRoom.getId(), chatRoom.getChatType(), chatRoomName, + thumbnail, chatRoom.getGroup() != null ? chatRoom.getGroup().getId() : null, participants.size(), participants, diff --git a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java index f94dc1b..176ed23 100644 --- a/src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java +++ b/src/main/java/team/wego/wegobackend/chat/application/dto/response/ParticipantResponse.java @@ -10,15 +10,17 @@ public record ParticipantResponse( String nickName, String profileImage, ParticipantStatus status, + boolean isOwner, LocalDateTime joinedAt ) { - public static ParticipantResponse from(ChatParticipant participant) { + public static ParticipantResponse from(ChatParticipant participant, boolean isOwner) { return new ParticipantResponse( participant.getId(), participant.getUser().getId(), participant.getUser().getNickName(), participant.getUser().getProfileImage(), participant.getStatus(), + isOwner, participant.getJoinedAt() ); } diff --git a/src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java b/src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java index c198649..4131843 100644 --- a/src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java +++ b/src/main/java/team/wego/wegobackend/chat/application/listener/ChatEventListener.java @@ -9,7 +9,9 @@ import team.wego.wegobackend.chat.config.ChatProperties; import team.wego.wegobackend.chat.domain.entity.JoinType; import team.wego.wegobackend.group.v2.application.event.GroupCreatedEvent; +import team.wego.wegobackend.group.v2.application.event.GroupJoinApprovedEvent; import team.wego.wegobackend.group.v2.application.event.GroupJoinedEvent; +import team.wego.wegobackend.group.v2.application.event.GroupJoinKickedEvent; import team.wego.wegobackend.group.v2.application.event.GroupLeftEvent; @Component @@ -68,6 +70,34 @@ public void handleGroupJoined(GroupJoinedEvent event) { } } + /** + * 모임 참여 승인 시 채팅방 자동 참여 (승인제 모임) + */ + @EventListener + @Async + public void handleGroupJoinApproved(GroupJoinApprovedEvent event) { + log.info("모임 참여 승인 이벤트 수신 - groupId: {}, targetUserId: {}", + event.groupId(), event.targetUserId()); + + if (!chatProperties.getAutoJoin().isEnabled()) { + log.debug("자동 참여 비활성화 - groupId: {}", event.groupId()); + return; + } + + try { + chatRoomService.joinChatRoomByGroup( + event.groupId(), + event.targetUserId(), + JoinType.AUTO + ); + log.info("채팅방 자동 참여 완료 (승인) - groupId: {}, userId: {}", + event.groupId(), event.targetUserId()); + } catch (Exception e) { + log.error("채팅방 자동 참여 실패 (승인) - groupId: {}, userId: {}", + event.groupId(), event.targetUserId(), e); + } + } + /** * 모임 퇴장 시 채팅방 퇴장 처리 (선택 사항) */ @@ -89,4 +119,26 @@ public void handleGroupLeft(GroupLeftEvent event) { event.groupId(), event.leaverUserId(), e); } } + + /** + * 모임 추방 시 채팅방 퇴장 처리 + */ + @EventListener + @Async + public void handleGroupKicked(GroupJoinKickedEvent event) { + log.info("모임 추방 이벤트 수신 - groupId: {}, targetUserId: {}", + event.groupId(), event.targetUserId()); + + try { + chatRoomService.leaveChatRoomByGroup( + event.groupId(), + event.targetUserId() + ); + log.info("채팅방 추방 처리 완료 - groupId: {}, userId: {}", + event.groupId(), event.targetUserId()); + } catch (Exception e) { + log.error("채팅방 추방 처리 실패 - groupId: {}, userId: {}", + event.groupId(), event.targetUserId(), e); + } + } } diff --git a/src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java b/src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java index b7d85dc..6df1d72 100644 --- a/src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java +++ b/src/main/java/team/wego/wegobackend/chat/application/service/ChatRoomService.java @@ -1,5 +1,6 @@ package team.wego.wegobackend.chat.application.service; +import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; @@ -24,7 +25,10 @@ import team.wego.wegobackend.chat.domain.repository.ChatMessageRepository; import team.wego.wegobackend.chat.domain.repository.ChatParticipantRepository; import team.wego.wegobackend.chat.domain.repository.ChatRoomRepository; +import team.wego.wegobackend.group.v2.domain.entity.GroupImageV2; +import team.wego.wegobackend.group.v2.domain.entity.GroupImageV2VariantType; import team.wego.wegobackend.group.v2.domain.entity.GroupV2; +import team.wego.wegobackend.group.v2.domain.repository.GroupImageV2Repository; import team.wego.wegobackend.group.v2.domain.repository.GroupV2Repository; import team.wego.wegobackend.user.domain.User; import team.wego.wegobackend.user.repository.UserRepository; @@ -40,6 +44,7 @@ public class ChatRoomService { private final ChatMessageRepository chatMessageRepository; private final UserRepository userRepository; private final GroupV2Repository groupV2Repository; + private final GroupImageV2Repository groupImageV2Repository; /** * 내 채팅방 목록 조회 @@ -49,6 +54,10 @@ public ChatRoomListResponse getMyChatRooms(Long userId) { List items = chatRooms.stream() .map(chatRoom -> buildChatRoomItem(chatRoom, userId)) + .sorted(Comparator.comparing( + item -> item.lastMessage() != null ? item.lastMessage().timestamp() : null, //Group 채팅의 경우 lastMessage가 비어있는 경우 존재 -> NPE 처리 + Comparator.nullsLast(Comparator.reverseOrder()) // null 처리 + 최신순 + )) .collect(Collectors.toList()); return ChatRoomListResponse.from(items); @@ -61,15 +70,17 @@ public ChatRoomResponse getChatRoom(Long userId, Long roomId) { ChatRoom chatRoom = findChatRoomById(roomId); validateParticipant(chatRoom.getId(), userId); + Long hostId = chatRoom.getHostId(); List participants = chatParticipantRepository .findActiveParticipants(roomId) .stream() - .map(ParticipantResponse::from) + .map(p -> ParticipantResponse.from(p, isOwner(p, hostId))) .collect(Collectors.toList()); String chatRoomName = resolveChatRoomName(chatRoom, userId); + String thumbnail = resolveThumbnail(chatRoom, userId); - return ChatRoomResponse.of(chatRoom, chatRoomName, participants); + return ChatRoomResponse.of(chatRoom, chatRoomName, thumbnail, participants); } /** @@ -79,10 +90,11 @@ public ParticipantListResponse getParticipants(Long userId, Long roomId) { ChatRoom chatRoom = findChatRoomById(roomId); validateParticipant(chatRoom.getId(), userId); + Long hostId = chatRoom.getHostId(); List participants = chatParticipantRepository .findActiveParticipants(roomId) .stream() - .map(ParticipantResponse::from) + .map(p -> ParticipantResponse.from(p, isOwner(p, hostId))) .collect(Collectors.toList()); return ParticipantListResponse.of(roomId, participants); @@ -268,6 +280,7 @@ private void validateHost(ChatRoom chatRoom, Long userId) { private ChatRoomItemResponse buildChatRoomItem(ChatRoom chatRoom, Long userId) { String chatRoomName = resolveChatRoomName(chatRoom, userId); + String thumbnail = resolveThumbnail(chatRoom, userId); int participantCount = chatParticipantRepository.countActiveParticipants(chatRoom.getId()); LastMessageResponse lastMessage = chatMessageRepository.findLatestByChatRoomId(chatRoom.getId()) @@ -279,19 +292,21 @@ private ChatRoomItemResponse buildChatRoomItem(ChatRoom chatRoom, Long userId) { int unreadCount = calculateUnreadCount(chatRoom.getId(), userId); - return ChatRoomItemResponse.of(chatRoom, chatRoomName, participantCount, lastMessage, unreadCount); + return ChatRoomItemResponse.of(chatRoom, chatRoomName, thumbnail, participantCount, lastMessage, unreadCount); } private ChatRoomResponse buildChatRoomResponse(ChatRoom chatRoom, Long userId) { String chatRoomName = resolveChatRoomName(chatRoom, userId); + String thumbnail = resolveThumbnail(chatRoom, userId); + Long hostId = chatRoom.getHostId(); List participants = chatParticipantRepository .findActiveParticipants(chatRoom.getId()) .stream() - .map(ParticipantResponse::from) + .map(p -> ParticipantResponse.from(p, isOwner(p, hostId))) .collect(Collectors.toList()); - return ChatRoomResponse.of(chatRoom, chatRoomName, participants); + return ChatRoomResponse.of(chatRoom, chatRoomName, thumbnail, participants); } private String resolveChatRoomName(ChatRoom chatRoom, Long userId) { @@ -308,6 +323,39 @@ private String resolveChatRoomName(ChatRoom chatRoom, Long userId) { .orElse("알 수 없음"); } + private String resolveThumbnail(ChatRoom chatRoom, Long userId) { + if (chatRoom.getChatType() == ChatType.GROUP && chatRoom.getGroup() != null) { + // 그룹 채팅: 그룹의 첫 번째 이미지의 THUMBNAIL_100_100 variant + List images = groupImageV2Repository + .findAllByGroupIdWithVariants(chatRoom.getGroup().getId()); + + if (images.isEmpty()) { + return null; + } + + return images.get(0).getVariants().stream() + .filter(v -> v.getType() == GroupImageV2VariantType.THUMBNAIL_100_100) + .findFirst() + .map(v -> v.getImageUrl()) + .orElse(null); + } + + // DM인 경우 상대방 프로필 이미지 반환 + return chatParticipantRepository.findActiveParticipants(chatRoom.getId()) + .stream() + .filter(p -> !p.getUser().getId().equals(userId)) + .findFirst() + .map(p -> p.getUser().getProfileImage()) + .orElse(null); + } + + private boolean isOwner(ChatParticipant participant, Long hostId) { + if (hostId == null) { + return false; + } + return participant.getUser().getId().equals(hostId); + } + private int calculateUnreadCount(Long roomId, Long userId) { return chatParticipantRepository.findByChatRoomIdAndUserId(roomId, userId) .filter(ChatParticipant::isActive) diff --git a/src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java b/src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java index 56e6281..a8e0e12 100644 --- a/src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java +++ b/src/main/java/team/wego/wegobackend/chat/config/StompChannelInterceptor.java @@ -53,6 +53,8 @@ public Message preSend(Message message, MessageChannel channel) { } } + //TODO: StompCommand.SUBSCRIBE 분기 처리 로직 추가 (그룹 채팅 참여 검증 로직) + return message; } diff --git a/src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java b/src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java index 7faa20c..3b0aa76 100644 --- a/src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java +++ b/src/main/java/team/wego/wegobackend/chat/domain/entity/ChatRoom.java @@ -22,6 +22,7 @@ import lombok.NoArgsConstructor; import team.wego.wegobackend.common.entity.BaseTimeEntity; import team.wego.wegobackend.group.v2.domain.entity.GroupV2; +import team.wego.wegobackend.user.domain.User; @Entity @Table(name = "chat_room") @@ -87,6 +88,20 @@ public boolean isDmChat() { return chatType == ChatType.DM; } + public boolean isHost(User user) { + if (!isGroupChat() || group == null) { + return false; + } + return group.getHost().getId().equals(user.getId()); + } + + public Long getHostId() { + if (!isGroupChat() || group == null) { + return null; + } + return group.getHost().getId(); + } + public void addParticipant(ChatParticipant participant) { this.participants.add(participant); participant.assignToChatRoom(this); @@ -96,4 +111,5 @@ public void addMessage(ChatMessage message) { this.messages.add(message); message.assignToChatRoom(this); } + } diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java index 53ea99f..a6f426e 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java +++ b/src/main/java/team/wego/wegobackend/group/v2/application/dto/response/GetGroupV2Response.java @@ -35,14 +35,16 @@ public record GetGroupV2Response( LocalDateTime createdAt, LocalDateTime updatedAt, MyMembership myMembership, // 로그인 아니면 null - List joinedMembers // Host면 전체, 아니면 ATTEND만 + List joinedMembers, // Host면 전체, 아니면 ATTEND만 + Long chatRoomId // 채팅방 참여를 위한 ID ) { public static GetGroupV2Response of( GroupV2 group, List images, List users, - Long userIdOrNull + Long userIdOrNull, + Long chatRoomId ) { // 태그 List tagNames = group.getGroupTags().stream() @@ -106,7 +108,8 @@ public static GetGroupV2Response of( group.getCreatedAt(), group.getUpdatedAt(), myMembership, - joinedMembers + joinedMembers, + chatRoomId ); } diff --git a/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java b/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java index f3542f5..d075ae9 100644 --- a/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java +++ b/src/main/java/team/wego/wegobackend/group/v2/application/service/GroupV2Service.java @@ -287,6 +287,8 @@ public GetGroupV2Response getGroup(Long userId, Long groupId) { List images = groupImageV2Repository.findAllByGroupIdWithVariants(groupId); List users = groupUserV2Repository.findAllByGroupIdWithUser(groupId); - return GetGroupV2Response.of(group, images, users, userId); + Long chatRoomId = group.getChatRoom() != null ? group.getChatRoom().getId() : null; + + return GetGroupV2Response.of(group, images, users, userId, chatRoomId); } } diff --git a/src/main/java/team/wego/wegobackend/user/domain/User.java b/src/main/java/team/wego/wegobackend/user/domain/User.java index d557087..e559ba7 100644 --- a/src/main/java/team/wego/wegobackend/user/domain/User.java +++ b/src/main/java/team/wego/wegobackend/user/domain/User.java @@ -85,9 +85,11 @@ public class User extends BaseTimeEntity { @Column(name = "provider") private ProviderType provider; + @Builder.Default @OneToMany(mappedBy = "follower") private List followings = new ArrayList<>(); + @Builder.Default @OneToMany(mappedBy = "followee") private List followers = new ArrayList<>(); @@ -168,7 +170,7 @@ public void updateNotificationEnabled(Boolean flag) { public void updatedeleted(Boolean flag) { this.deleted = flag; - } + } //HARD DELETE로 변경 public void updateMbti(String mbti) { this.mbti = mbti; diff --git a/src/test/java/team/wego/wegobackend/group/GroupCursorDummyTest.java b/src/test/java/team/wego/wegobackend/group/GroupCursorDummyTest.java index d0c015b..b9d7f3a 100644 --- a/src/test/java/team/wego/wegobackend/group/GroupCursorDummyTest.java +++ b/src/test/java/team/wego/wegobackend/group/GroupCursorDummyTest.java @@ -49,7 +49,7 @@ void createDummyUsersAndGroupsForCursorTest() { // 1. 공통 호스트 한 명 생성 User host = userRepository.findByEmail("cursor-host@example.com") .orElseGet(() -> userRepository.save( - new User( + User.createLocalUser( "cursor-host@example.com", "Test1234!@#", "CursorHost", @@ -67,7 +67,7 @@ void createDummyUsersAndGroupsForCursorTest() { // 이미 있으면 재사용, 없으면 새로 생성 User member = userRepository.findByEmail(email) .orElseGet(() -> userRepository.save( - new User( + User.createLocalUser( email, "Test1234!@#", "CursorMember" + uIndex, diff --git a/src/test/java/team/wego/wegobackend/group/MyGroupsDummyTest.java b/src/test/java/team/wego/wegobackend/group/MyGroupsDummyTest.java index 99568b4..3d75763 100644 --- a/src/test/java/team/wego/wegobackend/group/MyGroupsDummyTest.java +++ b/src/test/java/team/wego/wegobackend/group/MyGroupsDummyTest.java @@ -36,7 +36,7 @@ private User getOrCreateUser(String email, String nickname) { .orElseGet(() -> { String encodedPw = passwordEncoder.encode(RAW_PASSWORD); return userRepository.save( - new User( + User.createLocalUser( email, encodedPw, nickname,