Skip to content
23 changes: 23 additions & 0 deletions src/main/java/gg/agit/konect/domain/chat/controller/ChatApi.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gg.agit.konect.domain.chat.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
Expand Down Expand Up @@ -145,4 +146,26 @@ ResponseEntity<Void> updateChatRoomName(
@Valid @RequestBody ChatRoomNameUpdateRequest request,
@UserId Integer userId
);

@Operation(summary = "채팅방에서 나간다.", description = """
## 설명
- 동아리 채팅방은 나갈 수 없습니다.
- 1:1 채팅방은 소프트 딜리트 방식으로 나갑니다.
- 향후 일반 그룹 채팅방은 멤버십 제거 방식으로 나갈 수 있도록 설계합니다.

## 로직
- 1:1 채팅방에서 나간 사용자는 기존 메시지를 숨기고 채팅방 목록에서도 제거됩니다.
- 상대방이 이후 새 메시지를 보내면 나간 사용자는 새 대화처럼 그 메시지부터 다시 보게 됩니다.
- 사용자가 다시 1:1 채팅을 열면 이전 대화가 아니라 새로 시작한 것처럼 보입니다.

## 에러
- CANNOT_LEAVE_GROUP_CHAT_ROOM (400): 동아리 채팅방은 나갈 수 없습니다.
- FORBIDDEN_CHAT_ROOM_ACCESS (403): 채팅방에 접근할 권한이 없습니다.
- NOT_FOUND_CHAT_ROOM (404): 채팅방을 찾을 수 없습니다.
""")
@DeleteMapping("/rooms/{chatRoomId}")
ResponseEntity<Void> leaveChatRoom(
@PathVariable(value = "chatRoomId") Integer chatRoomId,
@UserId Integer userId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,13 @@ public ResponseEntity<Void> updateChatRoomName(
chatService.updateChatRoomName(userId, chatRoomId, request);
return ResponseEntity.ok().build();
}

@Override
public ResponseEntity<Void> leaveChatRoom(
@PathVariable(value = "chatRoomId") Integer chatRoomId,
@UserId Integer userId
) {
chatService.leaveChatRoom(userId, chatRoomId);
return ResponseEntity.noContent().build();
}
}
29 changes: 25 additions & 4 deletions src/main/java/gg/agit/konect/domain/chat/model/ChatRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@

import java.time.LocalDateTime;

import gg.agit.konect.domain.chat.enums.ChatType;
import gg.agit.konect.domain.club.model.Club;
import gg.agit.konect.domain.user.model.User;
import gg.agit.konect.global.exception.CustomException;
import gg.agit.konect.global.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
Expand All @@ -39,38 +42,56 @@ public class ChatRoom extends BaseEntity {
@Column(name = "last_message_sent_at")
private LocalDateTime lastMessageSentAt;

@Enumerated(EnumType.STRING)
@Column(name = "room_type", nullable = false, length = 20)
private ChatType roomType;

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "club_id")
private Club club;

@Builder
private ChatRoom(Integer id, Club club) {
private ChatRoom(Integer id, ChatType roomType, Club club) {
this.id = id;
this.roomType = roomType;
this.club = club;
}

public static ChatRoom directOf() {
return ChatRoom.builder().build();
return ChatRoom.builder()
.roomType(ChatType.DIRECT)
.build();
}

public static ChatRoom groupOf(Club club) {
return ChatRoom.builder()
.roomType(ChatType.GROUP)
.club(club)
.build();
}

public static ChatRoom groupOf() {
return ChatRoom.builder()
.roomType(ChatType.GROUP)
.build();
}

public static void validateIsNotSameParticipant(User sender, User receiver) {
if (sender.getId().equals(receiver.getId())) {
throw CustomException.of(CANNOT_CREATE_CHAT_ROOM_WITH_SELF);
}
}

public boolean isDirectRoom() {
return club == null;
return roomType == ChatType.DIRECT;
}

public boolean isGroupRoom() {
return club != null;
return roomType == ChatType.GROUP;
}

public boolean isClubGroupRoom() {
return roomType == ChatType.GROUP && club != null;
}

public void updateLastMessage(String lastMessageContent, LocalDateTime lastMessageSentAt) {
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/gg/agit/konect/domain/chat/model/ChatRoomMember.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ public class ChatRoomMember extends BaseEntity {
@Column(name = "last_read_at", nullable = false)
private LocalDateTime lastReadAt;

@Column(name = "visible_message_from")
private LocalDateTime visibleMessageFrom;

@Column(name = "left_at")
private LocalDateTime leftAt;

@Column(name = "custom_room_name", length = 30)
private String customRoomName;

Expand All @@ -49,12 +55,16 @@ private ChatRoomMember(
ChatRoom chatRoom,
User user,
LocalDateTime lastReadAt,
LocalDateTime visibleMessageFrom,
LocalDateTime leftAt,
String customRoomName
) {
this.id = id;
this.chatRoom = chatRoom;
this.user = user;
this.lastReadAt = lastReadAt;
this.visibleMessageFrom = visibleMessageFrom;
this.leftAt = leftAt;
this.customRoomName = customRoomName;
}

Expand Down Expand Up @@ -88,4 +98,48 @@ public void updateLastReadAt(LocalDateTime lastReadAt) {
public void updateCustomRoomName(String customRoomName) {
this.customRoomName = customRoomName;
}

public boolean hasLeft() {
return leftAt != null;
}

public void leaveDirectRoom(LocalDateTime leftAt) {
this.leftAt = leftAt;
this.visibleMessageFrom = leftAt;
updateLastReadAt(leftAt);
}

/**
* 나간 이후 새 메시지가 생겨 다시 볼 수 있을 때 사용한다.
* <p>
* 나간 상태만 해제하고, 기존 {@code visibleMessageFrom}은 유지한다.
* 그래서 나간 이후 도착한 메시지부터 계속 보인다.
*/
public void restoreDirectRoom() {
this.leftAt = null;
}

/**
* 사용자가 채팅방을 다시 열어 새 대화를 시작할 때 사용한다.
* <p>
* 나간 상태를 해제하고 {@code visibleMessageFrom}도 새로 갱신한다.
* 그래서 전달한 시점 이후 메시지부터 새 대화처럼 보인다.
*/
public void reopenDirectRoom(LocalDateTime visibleMessageFrom) {
this.leftAt = null;
this.visibleMessageFrom = visibleMessageFrom;
updateLastReadAt(visibleMessageFrom);
}
Comment thread
dh2906 marked this conversation as resolved.

public boolean hasVisibleMessages(ChatRoom room) {
if (room.getLastMessageSentAt() == null) {
return false;
}

if (visibleMessageFrom == null) {
return true;
}

return room.getLastMessageSentAt().isAfter(visibleMessageFrom);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package gg.agit.konect.domain.chat.repository;

import java.time.LocalDateTime;
import java.util.List;

import org.springframework.data.domain.Page;
Expand Down Expand Up @@ -39,9 +40,25 @@ List<UnreadMessageCount> countUnreadMessagesByChatRoomIdsAndUserId(
FROM ChatMessage cm
JOIN FETCH cm.sender
WHERE cm.chatRoom.id = :chatRoomId
AND (:visibleMessageFrom IS NULL OR cm.createdAt > :visibleMessageFrom)
ORDER BY cm.createdAt DESC
""")
Page<ChatMessage> findByChatRoomId(@Param("chatRoomId") Integer chatRoomId, Pageable pageable);
Page<ChatMessage> findByChatRoomId(
@Param("chatRoomId") Integer chatRoomId,
@Param("visibleMessageFrom") LocalDateTime visibleMessageFrom,
Pageable pageable
);
Comment thread
dh2906 marked this conversation as resolved.

@Query("""
SELECT COUNT(m)
FROM ChatMessage m
WHERE m.chatRoom.id = :chatRoomId
AND (:visibleMessageFrom IS NULL OR m.createdAt > :visibleMessageFrom)
""")
long countByChatRoomId(
@Param("chatRoomId") Integer chatRoomId,
@Param("visibleMessageFrom") LocalDateTime visibleMessageFrom
);

@Query("""
SELECT new gg.agit.konect.domain.chat.dto.UnreadMessageCount(
Expand Down Expand Up @@ -102,10 +119,4 @@ SELECT MAX(m2.id)
""")
List<ChatMessage> findLatestMessagesByRoomIds(@Param("roomIds") List<Integer> roomIds);

@Query("""
SELECT COUNT(m)
FROM ChatMessage m
WHERE m.chatRoom.id = :chatRoomId
""")
long countByChatRoomId(@Param("chatRoomId") Integer chatRoomId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import gg.agit.konect.domain.chat.dto.AdminChatRoomProjection;
import gg.agit.konect.domain.chat.model.ChatRoom;
import gg.agit.konect.domain.chat.enums.ChatType;
import gg.agit.konect.domain.user.enums.UserRole;

public interface ChatRoomRepository extends Repository<ChatRoom, Integer> {
Expand All @@ -21,10 +22,10 @@ public interface ChatRoomRepository extends Repository<ChatRoom, Integer> {
JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id
LEFT JOIN FETCH cr.club
WHERE crm.id.userId = :userId
AND cr.club IS NULL
AND cr.roomType = :roomType
ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id
""")
List<ChatRoom> findByUserId(@Param("userId") Integer userId);
List<ChatRoom> findByUserId(@Param("userId") Integer userId, @Param("roomType") ChatType roomType);

@Query("""
SELECT cr
Expand All @@ -39,13 +40,17 @@ public interface ChatRoomRepository extends Repository<ChatRoom, Integer> {
FROM ChatRoom cr
JOIN ChatRoomMember crm
ON crm.id.chatRoomId = cr.id
WHERE cr.club IS NULL
WHERE cr.roomType = :roomType
GROUP BY cr
HAVING COUNT(crm) = 2
AND SUM(CASE WHEN crm.id.userId = :userId1 THEN 1 ELSE 0 END) = 1
AND SUM(CASE WHEN crm.id.userId = :userId2 THEN 1 ELSE 0 END) = 1
""")
Optional<ChatRoom> findByTwoUsers(@Param("userId1") Integer userId1, @Param("userId2") Integer userId2);
Optional<ChatRoom> findByTwoUsers(
@Param("userId1") Integer userId1,
@Param("userId2") Integer userId2,
@Param("roomType") ChatType roomType
);

@Query("""
SELECT cr
Expand All @@ -69,15 +74,16 @@ AND SUM(CASE WHEN crm.id.userId = :userId2 THEN 1 ELSE 0 END) = 1
JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id
LEFT JOIN FETCH cr.club
WHERE crm.id.userId = :userId
AND cr.roomType = :roomType
AND cr.club IS NOT NULL
ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id
""")
List<ChatRoom> findGroupRoomsByUserId(@Param("userId") Integer userId);
List<ChatRoom> findGroupRoomsByUserId(@Param("userId") Integer userId, @Param("roomType") ChatType roomType);

@Query("""
SELECT DISTINCT cr
FROM ChatRoom cr
WHERE cr.club IS NULL
WHERE cr.roomType = :roomType
AND EXISTS (
SELECT 1 FROM ChatRoomMember adminMember
JOIN adminMember.user adminUser
Expand All @@ -92,12 +98,15 @@ AND EXISTS (
)
ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id
""")
List<ChatRoom> findAllAdminUserDirectRooms(@Param("adminRole") UserRole adminRole);
List<ChatRoom> findAllAdminUserDirectRooms(
@Param("adminRole") UserRole adminRole,
@Param("roomType") ChatType roomType
);

@Query("""
SELECT DISTINCT cr
FROM ChatRoom cr
WHERE cr.club IS NULL
WHERE cr.roomType = :roomType
AND EXISTS (
SELECT 1 FROM ChatRoomMember systemAdminMember
WHERE systemAdminMember.id.chatRoomId = cr.id
Expand All @@ -113,7 +122,8 @@ AND EXISTS (
""")
List<ChatRoom> findAllSystemAdminDirectRooms(
@Param("systemAdminId") Integer systemAdminId,
@Param("adminRole") UserRole adminRole
@Param("adminRole") UserRole adminRole,
@Param("roomType") ChatType roomType
);

/**
Expand Down Expand Up @@ -145,7 +155,7 @@ List<ChatRoom> findAllSystemAdminDirectRooms(
LEFT JOIN ChatMessage cm ON cm.chatRoom.id = cr.id
AND cm.sender.id <> :systemAdminId
AND cm.createdAt > adminCrm.lastReadAt
WHERE cr.club IS NULL
WHERE cr.roomType = :roomType
AND u.role != :adminRole
AND EXISTS (
SELECT 1 FROM ChatMessage userReply
Expand All @@ -158,6 +168,7 @@ AND EXISTS (
""")
List<AdminChatRoomProjection> findAdminChatRoomsOptimized(
@Param("systemAdminId") Integer systemAdminId,
@Param("adminRole") UserRole adminRole
@Param("adminRole") UserRole adminRole,
@Param("roomType") ChatType roomType
);
}
Loading
Loading