diff --git a/src/backend/chat_server/build.gradle b/src/backend/chat_server/build.gradle index fe743b14..8c886947 100644 --- a/src/backend/chat_server/build.gradle +++ b/src/backend/chat_server/build.gradle @@ -25,12 +25,14 @@ dependencies { // Spring implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' - implementation 'org.springframework.boot:spring-boot-starter-actuator' //db implementation group: 'org.postgresql', name: 'postgresql', version: '42.7.3' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + //mongo + implementation('org.springframework.boot:spring-boot-starter-data-mongodb') + // Lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' @@ -54,6 +56,9 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-redis' annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" + + //JDBC + implementation 'org.springframework.boot:spring-boot-starter-jdbc' } tasks.named('test') { diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompConnectInterceptor.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompConnectInterceptor.java index 5e71a1d2..58658472 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompConnectInterceptor.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompConnectInterceptor.java @@ -24,6 +24,8 @@ public class StompConnectInterceptor implements ChannelInterceptor { private final RedisTemplate stringOperRedisTemplate; private final RedisTemplate objectOperRedisTemplate; + private final StompSubscriptionInterceptor stompSubscriptionInterceptor; + @Override public void afterSendCompletion(Message message, MessageChannel channel, boolean sent, Exception ex) { if (hasException(ex)) { @@ -73,6 +75,7 @@ private void handleUserDisconnection(StompHeaderAccessor accessor) { String userId = getUserIdFromSession(sessionId); if (userId != null && !userId.trim().isEmpty()) { + stompSubscriptionInterceptor.handleChatUnsubscription(accessor); cleanupUserSession(userId, sessionId); } } diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompSubscriptionInterceptor.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompSubscriptionInterceptor.java index 3126bb72..37d211ae 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompSubscriptionInterceptor.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/config/interceptor/StompSubscriptionInterceptor.java @@ -1,6 +1,9 @@ package com.jootalkpia.chat_server.config.interceptor; +import com.jootalkpia.chat_server.domain.ChatMessage; import com.jootalkpia.chat_server.dto.RedisKeys; +import com.jootalkpia.chat_server.repository.UserChannelRepository; +import com.jootalkpia.chat_server.repository.mongo.ChatMessageRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; @@ -10,6 +13,7 @@ import org.springframework.messaging.simp.stomp.StompHeaderAccessor; import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; import java.util.HashSet; import java.util.List; @@ -23,10 +27,14 @@ public class StompSubscriptionInterceptor implements ChannelInterceptor { private static final String CHANNEL_ID_DELIMITER = "\\."; private static final long SESSION_EXPIRY_HOURS = 4; private static final String DEFAULT_CHANNEL = "none"; + private static final Long DEFAULT_ID = 1L; private final RedisTemplate stringOperRedisTemplate; private final RedisTemplate objectOperRedisTemplate; + private final ChatMessageRepository chatMessageRepository; + private final UserChannelRepository userChannelRepository; + @Override public void afterSendCompletion(Message message, MessageChannel channel, boolean sent, Exception ex) { if (!isValidMessage(sent, ex)) { @@ -37,6 +45,24 @@ public void afterSendCompletion(Message message, MessageChannel channel, bool handleStompCommand(StompHeaderAccessor.wrap(message)); } + @Transactional + public void handleChatUnsubscription(StompHeaderAccessor accessor) { + String sessionId = accessor.getSessionId(); + String userId = getUserIdFromSessionId(sessionId); + String channelId = getChannelIdFromSessionId(sessionId); + + if (userId != null && !userId.trim().isEmpty()) { + updateChannelFromSession(sessionId); + removeTabFromChannel(channelId, userId, sessionId); + userChannelRepository.updateLastReadId( + userId, + channelId, + chatMessageRepository.findFirstByChannelIdOrderByThreadIdDesc(Long.valueOf(channelId)) + .map(ChatMessage::getThreadId) + .orElse(DEFAULT_ID)); + } + } + private boolean isValidMessage(boolean sent, Exception ex) { return !hasException(ex) && sent; } @@ -127,17 +153,6 @@ private void saveUserTabs(String channelId, String userId, Set userTabs) ); } - private void handleChatUnsubscription(StompHeaderAccessor accessor) { - String sessionId = accessor.getSessionId(); - String userId = getUserIdFromSessionId(sessionId); - String channelId = getChannelIdFromSessionId(sessionId); - - if (userId != null && !userId.trim().isEmpty()) { - updateChannelFromSession(sessionId); - removeTabFromChannel(channelId, userId, sessionId); - } - } - private String getUserIdFromSessionId(String sessionId) { return stringOperRedisTemplate.opsForValue().get(RedisKeys.sessionUser(sessionId)); } diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/ChatMessage.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/ChatMessage.java new file mode 100644 index 00000000..b6d3e208 --- /dev/null +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/ChatMessage.java @@ -0,0 +1,41 @@ +package com.jootalkpia.chat_server.domain; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; + +import java.util.List; + +@Getter +@Builder +@Document(collection = "chat_messages") +@CompoundIndex(name = "channel_thread_idx", def = "{'channel_id': 1, 'thread_id': -1}") +public class ChatMessage extends BaseTimeEntity { + + @Id + private String id; // MongoDB의 ObjectId 자동 생성 + + @Field("channel_id") + private Long channelId; + + @Field("thread_id") + private Long threadId; + + @Field("thread_date_time") + private String threadDateTime; + + @Field("user_id") + private Long userId; + + @Field("user_nickname") + private String userNickname; + + @Field("user_profile_image") + private String userProfileImage; + + @Field("messages") + private List messages; +} diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/Message.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/Message.java new file mode 100644 index 00000000..fae56b45 --- /dev/null +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/Message.java @@ -0,0 +1,32 @@ +package com.jootalkpia.chat_server.domain; + +import lombok.*; +import org.springframework.data.mongodb.core.mapping.Field; + +@Getter +@Builder +public class Message { + @Field("type") + private String type; + + @Field("text") + private String text; + + @Field("image_id") + private Long imageId; + + @Field("image_url") + private String imageUrl; + + @Field("video_id") + private Long videoId; + + @Field("video_thumbnail_id") + private Long videoThumbnailId; + + @Field("thumbnail_url") + private String thumbnailUrl; + + @Field("video_url") + private String videoUrl; +} diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/User.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/User.java index d6ee23fb..0a475ab6 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/User.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/domain/User.java @@ -1,16 +1,7 @@ package com.jootalkpia.chat_server.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Table; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; -import jakarta.persistence.Id; +import jakarta.persistence.*; +import lombok.*; @Entity @Getter diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserChannelRepository.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserChannelRepository.java new file mode 100644 index 00000000..1202a457 --- /dev/null +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserChannelRepository.java @@ -0,0 +1,18 @@ +package com.jootalkpia.chat_server.repository; + +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class UserChannelRepository { + private final JdbcTemplate jdbcTemplate; + + public void updateLastReadId(String userId, String channelId, Long lastReadId) { + String sql = "UPDATE user_channel SET last_read_id = ? " + + "WHERE user_id = ? AND channel_id = ?"; + + jdbcTemplate.update(sql, lastReadId, Long.valueOf(userId), Long.valueOf(channelId)); + } +} diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/FileRepository.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/FileRepository.java similarity index 84% rename from src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/FileRepository.java rename to src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/FileRepository.java index 6700df61..56d30c12 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/FileRepository.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/FileRepository.java @@ -1,4 +1,4 @@ -package com.jootalkpia.chat_server.repository; +package com.jootalkpia.chat_server.repository.jpa; import com.jootalkpia.chat_server.domain.Files; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/ThreadRepository.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/ThreadRepository.java similarity index 83% rename from src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/ThreadRepository.java rename to src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/ThreadRepository.java index 036b44a6..b697b685 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/ThreadRepository.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/ThreadRepository.java @@ -1,4 +1,4 @@ -package com.jootalkpia.chat_server.repository; +package com.jootalkpia.chat_server.repository.jpa; import com.jootalkpia.chat_server.domain.Thread; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserRepository.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/UserRepository.java similarity index 84% rename from src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserRepository.java rename to src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/UserRepository.java index b1c38004..d49aeeb8 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/UserRepository.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/jpa/UserRepository.java @@ -1,4 +1,4 @@ -package com.jootalkpia.chat_server.repository; +package com.jootalkpia.chat_server.repository.jpa; import com.jootalkpia.chat_server.domain.User; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/mongo/ChatMessageRepository.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/mongo/ChatMessageRepository.java new file mode 100644 index 00000000..e7fd7011 --- /dev/null +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/repository/mongo/ChatMessageRepository.java @@ -0,0 +1,10 @@ +package com.jootalkpia.chat_server.repository.mongo; + +import com.jootalkpia.chat_server.domain.ChatMessage; +import org.springframework.data.mongodb.repository.MongoRepository; + +import java.util.Optional; + +public interface ChatMessageRepository extends MongoRepository { + Optional findFirstByChannelIdOrderByThreadIdDesc(Long channelId); +} diff --git a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/service/ChatService.java b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/service/ChatService.java index 6c760cd5..1adb94ed 100644 --- a/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/service/ChatService.java +++ b/src/backend/chat_server/src/main/java/com/jootalkpia/chat_server/service/ChatService.java @@ -10,9 +10,9 @@ import com.jootalkpia.chat_server.dto.messgaeDto.MessageResponse; import com.jootalkpia.chat_server.dto.messgaeDto.TextResponse; import com.jootalkpia.chat_server.dto.messgaeDto.VideoResponse; -import com.jootalkpia.chat_server.repository.FileRepository; -import com.jootalkpia.chat_server.repository.ThreadRepository; -import com.jootalkpia.chat_server.repository.UserRepository; +import com.jootalkpia.chat_server.repository.jpa.FileRepository; +import com.jootalkpia.chat_server.repository.jpa.ThreadRepository; +import com.jootalkpia.chat_server.repository.jpa.UserRepository; import com.jootalkpia.chat_server.util.DateTimeUtil; import jakarta.transaction.Transactional; import java.util.ArrayList; diff --git a/src/backend/chat_server/src/main/resources/application.yml b/src/backend/chat_server/src/main/resources/application.yml index 842f02ed..534a969f 100644 --- a/src/backend/chat_server/src/main/resources/application.yml +++ b/src/backend/chat_server/src/main/resources/application.yml @@ -25,6 +25,10 @@ spring: redis: host: ${REDIS_HOST} port: ${REDIS_PORT} + mongodb: + uri: ${MONGO_URI} + main: + allow-bean-definition-overriding: true logging: level: diff --git a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/ChatMessage.java b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/ChatMessage.java index 5f5b74a7..6cdcb9f3 100644 --- a/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/ChatMessage.java +++ b/src/backend/history_server/src/main/java/com/jootalkpia/history_server/domain/ChatMessage.java @@ -1,15 +1,19 @@ package com.jootalkpia.history_server.domain; -import lombok.*; +import lombok.Builder; +import lombok.Getter; import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; + import java.util.List; @Getter @Builder @Document(collection = "chat_messages") -public class ChatMessage extends BaseTimeEntity { +@CompoundIndex(name = "channel_thread_idx", def = "{'channel_id': 1, 'thread_id': -1}") +public class ChatMessage extends BaseTimeEntity { @Id private String id; // MongoDB의 ObjectId 자동 생성 diff --git a/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/stocks/dto/MinutePrice.java b/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/stocks/dto/MinutePrice.java index 84326471..2d2b6b89 100644 --- a/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/stocks/dto/MinutePrice.java +++ b/src/backend/stock_server/src/main/java/com/jootalkpia/stock_server/stocks/dto/MinutePrice.java @@ -12,7 +12,7 @@ public class MinutePrice { @Id private ObjectId minutePriceId; - @Indexed(background = true) + @Indexed private String code; private String stockName;