Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IDLE-462] 요양 보호사와 센터장간 채팅 구현 #262

Merged
merged 11 commits into from
Feb 17, 2025

Conversation

mjj111
Copy link
Member

@mjj111 mjj111 commented Feb 17, 2025

1. 📄 Summary

이번 프로젝트에서는 Redis를 활용하여 요양보호사와 센터 간의 실시간 채팅 기능을 구현했습니다. Redis의 Pub/Sub 시스템을 통해 서버 간 메시지 전달을 처리하며, 이를 통해 효율적인 실시간 커뮤니케이션 환경을 구축하였습니다.

2. 🤔 고민했던 점

  • Redis를 통한 확장성 있는 채팅 구현
    서버의 확장 시, 각 채팅 서버가 개별적으로 클라이언트와 연결되기 때문에, 다른 서버에서 발생한 메시지가 해당 클라이언트에게 전달되려면 외부 메시지 브로커를 통해 처리해야 합니다. Redis의 Pub/Sub를 활용하여 이 문제를 해결하고, 서버 간의 메시지 전달을 효율적으로 관리할 수 있었습니다.

  • 채팅방 요약 정보 조회 시 쿼리 최적화
    ChatRoom과 ChatMessage 테이블의 크기 차이로 인해 성능 문제가 발생할 수 있었습니다. 이를 해결하기 위해, 쿼리 최적화를 신중히 고려하여 성능을 개선했습니다. 특히, 데이터를 필터링하고 정렬하는 순서를 최적화하여 성능을 극대화할 수 있었습니다.

Summary by CodeRabbit

  • 신규 기능

    • 채팅방 생성, 최근 메시지 조회 및 채팅 요약 정보를 제공하는 새로운 API가 도입되었습니다.
    • 실시간 메시지 전송과 읽음 상태 업데이트 기능이 개선되어 원활한 채팅 경험을 제공합니다.
    • FCM 기반 알림 기능이 추가되어 사용자에게 즉각적인 채팅 알림이 전송됩니다.
    • 새로운 Redis 메시징 구성으로 채팅 메시지의 효율적인 처리 및 전송이 가능해졌습니다.
  • 개선 사항

    • 메시지 포맷 단순화 및 데이터 구조 최적화를 통해 사용자 경험이 전반적으로 향상되었습니다.
    • 채팅 메시지 및 채팅방 관련 데이터 모델이 개선되어 데이터 일관성이 높아졌습니다.

@mjj111 mjj111 added the ✨기능 기능 개발 및 구현 label Feb 17, 2025
@mjj111 mjj111 self-assigned this Feb 17, 2025
Copy link

coderabbitai bot commented Feb 17, 2025

"""

Walkthrough

이번 PR은 채팅 기능 관련 여러 모듈의 구조와 흐름을 전면 개편하는 것입니다. 도메인 계층에서는 ChatMessage 및 ChatRoom 엔티티가 수정되고, 데이터베이스 스키마와 SQL 마이그레이션이 업데이트되었으며, Redis 기반 메시징 구성도 재정비되었습니다. 또한, 애플리케이션 계층에 새 서비스와 API, 컨트롤러가 추가되어 채팅룸 생성, 메시지 전송 및 읽음 처리 등의 주요 기능이 개선되었습니다.

Changes

파일/경로 변경 요약
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt ChatMessageService 클래스: 생성자에 Repository 주입, save, read, getRecentMessages 메소드 추가, createByUser 메소드 삭제.
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt ChatRoomService 클래스 추가 (채팅룸 생성 및 요약 조회 기능 제공).
idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt ChatFacadeService 클래스 추가 (메시지 전송, 읽음 처리, 채팅룸 생성, 최근 메시지 조회, 채팅룸 요약 기능 구현).
idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatMessageFacadeService.kt
idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatRoomFacadeService.kt
두 파일 삭제됨.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatMessageRedisConfig.kt 기존 Redis 구성 파일 삭제됨.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt ChatRedisConfig 클래스 추가: Redis 채널, 리스너, 템플릿 및 관련 Bean 구성.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt ChatMessage 엔티티 수정: roomIdchatRoomId, senderType 삭제, receiverId 추가, contentscontent 변경, isRead 추가.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatRoom.kt ChatRoom 엔티티 수정: 기존 senderId, receiverId 삭제 후 carerId, centerId 추가.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisConsumer.kt
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisPublisher.kt
두 파일 삭제됨.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt
새 Redis 메시지 발행/소비 클래스 추가 (ChatRedisPublisher, ChatRedisSubscriber).
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt
새 인터페이스 추가: 각각 메시지와 채팅룸 관련 커스텀 쿼리 메소드 포함.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt
새 데이터 클래스 및 인터페이스 추가: ChatRoomSummaryInfoChatRoomSummaryInfoProjection 도입.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/Content.kt
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/SendUser.kt
ContentSendUser 데이터 클래스 삭제됨.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt ReadMessage 데이터 클래스 추가.
idle-domain/src/main/resources/db/migration/V1__init.sql SQL 스크립트 업데이트: chat_messagechat_room 테이블 구조 변경, job_posting 컬럼 형식 정규화.
idle-domain/src/main/resources/db/migration/V2__add_index.sql 인덱스 추가 SQL 스크립트 삭제됨.
idle-domain/src/main/resources/db/migration/V2__add_phone_number_index.sql 새 SQL 마이그레이션 파일 추가: 유니크 인덱스를 center_manager, carer 테이블에 생성.
idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt ChatNotificationService 클래스 추가: FCM 메시지 생성 및 전송 기능 포함.
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt
새 API 인터페이스 추가: 채팅룸 생성, 최근 메시지 조회, 채팅룸 요약 제공 엔드포인트 정의.
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt
WebSocket 관련 설정 변경: 새 ChatHandshakeInterceptor 추가, WebSocketConfig 업데이트 (엔드포인트 /ws, 브로커 경로 /sub/pub, 메시지 컨버터 구성).
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt
새 컨트롤러 추가: 각각 ChatCarerController, ChatCenterController (API 인터페이스 구현).
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatMessageHandler.kt
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatWebSocketController.kt
두 파일 삭제됨 (채팅 메시지 처리 로직 통합 및 개편).
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt ChatSocketController 추가: WebSocket 메시지 매핑을 통한 전송 및 읽기 기능 구현.
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatHandler.kt ChatHandler 클래스 추가: 이벤트 리스너를 통해 채팅 메시지 및 읽음 이벤트 브로드캐스트 처리.
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/CreateChatRoomRequest.kt
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt
새 데이터 클래스 추가: 각각 CreateChatRoomRequestReadChatMessagesReqeust 도입.
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/SendChatMessageRequest.kt SendChatMessageRequest 데이터 클래스 재구성: 기존 contents 제거 및 chatroomId, content, senderName, receiverId 프로퍼티 추가.
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/center/CenterJobPostingResponse.kt CenterJobPostingResponse 업데이트: centerId 속성 추가 및 of 메소드 수정.

Sequence Diagram(s)

sequenceDiagram
    participant Client as 클라이언트
    participant SC as ChatSocketController
    participant CF as ChatFacadeService
    participant CMS as ChatMessageService
    participant RP as RedisPublisher
    participant NS as NotificationService

    Client->>SC: sendMessage(SendChatMessageRequest, Header)
    SC->>CF: sendMessage(request, userId)
    CF->>CMS: save(chatMessage)
    CMS-->>CF: 저장 완료
    CF->>RP: publish(chatMessage)
    RP-->>CF: 메시지 발행 완료
    CF->>NS: sendNotification (토큰 존재 시)
Loading
sequenceDiagram
    participant Client as 클라이언트
    participant SC as ChatSocketController
    participant CF as ChatFacadeService
    participant CMS as ChatMessageService
    participant RP as RedisPublisher

    Client->>SC: read(ReadChatMessagesReqeust, Header)
    SC->>CF: readMessage(request, userId)
    CF->>CMS: read(chatMessage)
    CMS->>RP: publish(ReadMessage 이벤트)
    RP-->>CF: 이벤트 발행 완료
Loading

Possibly related PRs

Suggested labels

🎯리팩토링
"""


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🔭 Outside diff range comments (1)
idle-domain/src/main/resources/db/migration/V1__init.sql (1)

67-78: ⚠️ Potential issue

채팅 메시지 테이블 제약 조건 추가 필요

외래 키 제약 조건이 누락되어 있어 데이터 무결성이 보장되지 않습니다.

다음 제약 조건들을 추가하는 것을 추천드립니다:

ALTER TABLE chat_message
ADD CONSTRAINT fk_chat_message_room
FOREIGN KEY (chat_room_id) REFERENCES chat_room(id),
ADD CONSTRAINT fk_chat_message_sender
FOREIGN KEY (sender_id) REFERENCES carer(id),
ADD CONSTRAINT fk_chat_message_receiver
FOREIGN KEY (receiver_id) REFERENCES center(id);
🧹 Nitpick comments (30)
idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt (2)

17-21: 메시지 전송 상태 처리를 개선해 주세요.

다음 사항들을 고려해 주시기 바랍니다:

  1. chatMessage와 senderName에 대한 유효성 검사 추가
  2. 전송 성공/실패 여부를 반환하여 호출자가 적절히 대응할 수 있도록 개선
-    fun send(chatMessage: ChatMessage, senderName: String, tokenEntity: DeviceToken?) {
+    fun send(chatMessage: ChatMessage, senderName: String, tokenEntity: DeviceToken?): Boolean {
+        require(chatMessage.content.isNotBlank()) { "메시지 내용은 비어있을 수 없습니다" }
+        require(senderName.isNotBlank()) { "발신자 이름은 비어있을 수 없습니다" }
+
         if (tokenEntity == null) return
         val fcmMessage = createFcmMessage(chatMessage,senderName, tokenEntity.deviceToken)
-        clientSend(fcmMessage)
+        return clientSend(fcmMessage)
     }

23-39: 메서드 시그니처의 포맷팅을 개선해 주세요.

매개변수 간격이 일관되지 않습니다.

-    private fun createFcmMessage(chatMessage: ChatMessage,
-                                 senderName: String,
-                                 token : String,): Message {
+    private fun createFcmMessage(
+        chatMessage: ChatMessage,
+        senderName: String,
+        token: String
+    ): Message {
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt (4)

18-21: KDoc 문서화 추가 제안

채널 토픽의 용도와 CHATROOM 상수의 의미를 설명하는 문서화가 있으면 좋겠습니다.

다음과 같이 KDoc을 추가하는 것을 제안합니다:

+    /**
+     * 채팅방 메시지를 구독하기 위한 Redis 채널 토픽을 생성합니다.
+     * @return 채팅방 채널 토픽
+     */
     @Bean
     fun chatChannelTopic(): ChannelTopic {
         return ChannelTopic(CHATROOM)
     }

23-32: 예외 처리 추가 필요

Redis 연결 실패나 메시지 처리 중 발생할 수 있는 예외 상황에 대한 처리가 필요합니다.

다음과 같은 예외 처리 로직 추가를 제안합니다:

     @Bean
     fun redisListenerContainer(
         connectionFactory: RedisConnectionFactory,
         messageListenerAdapter: MessageListenerAdapter,
     ): RedisMessageListenerContainer {
         return RedisMessageListenerContainer().also {
+            try {
                 it.addMessageListener(messageListenerAdapter, chatChannelTopic())
                 it.setConnectionFactory(connectionFactory)
+            } catch (e: Exception) {
+                // Redis 연결 실패 로깅 및 적절한 예외 처리
+                throw RedisConnectionException("Redis 연결 중 오류가 발생했습니다", e)
+            }
         }
     }

39-52: Redis 템플릿 최적화 제안

Redis 템플릿 구성이 잘 되어있지만, 성능 최적화를 위한 추가 설정이 도움될 수 있습니다.

다음과 같은 최적화 설정을 제안합니다:

     @Bean
     fun chatRoomRedisTemplate(redisConnectionFactory: RedisConnectionFactory):RedisTemplate<String, Any> {
         val template = RedisTemplate<String, Any>()
         template.connectionFactory = redisConnectionFactory
 
         template.keySerializer = StringRedisSerializer()
         template.hashKeySerializer = StringRedisSerializer()
 
         val serializer = Jackson2JsonRedisSerializer(ChatMessage::class.java)
         template.valueSerializer = serializer
         template.hashValueSerializer = serializer
 
+        // 성능 최적화를 위한 추가 설정
+        template.enableTransactionSupport = false
+        template.enableDefaultSerializer = false
 
         return template
     }

54-56: 상수 이름 개선 제안

현재 CHATROOM이라는 상수명이 너무 일반적입니다. 해당 상수의 용도를 더 명확하게 표현하는 이름이 좋겠습니다.

다음과 같은 이름 변경을 제안합니다:

     companion object {
-        const val CHATROOM = "chatroom"
+        const val CHAT_ROOM_CHANNEL = "chatroom"
     }
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt (3)

4-5: 클래스명 오타 확인 권장
ReadChatMessagesReqeust 클래스명이 오탈자로 보입니다. 실제 파일 명이나 클래스 정의가 Request인지 Reqeust인지 다시 확인해 주세요.


14-17: 메시지 저장 로직
save(chatMessage: ChatMessage) 메서드는 단순 저장 기능을 잘 반영하고 있습니다. 필요하다면, 저장 전후 검증이나 로깅을 고려할 수 있습니다.


24-27: 최근 메시지 조회 로직
getRecentMessages는 단순 조회 방식으로 깔끔하게 보입니다. 다만, 조회 범위와 예외처리(존재하지 않는 메시지 ID 등)에 대한 보강도 고려 바랍니다.

idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt (4)

3-21: 임포트 목록 점검
관련 서비스와 보조 클래스를 고루 임포트하고 있습니다. 불필요한 임포트가 없는지 확인해 주세요.


36-53: sendMessage 메서드 동시성 고려
runBlocking 내에서 launch를 병렬로 사용하여 메시지 저장과 퍼블리시, 알림 전송을 동시에 처리하고 있습니다. 혹시 DB 트랜잭션 실패 시 다른 처리(푸시 알림 등)가 계속 진행될 수 있으니, 필요한 경우에는 예외 처리를 추가하고 순서를 명확히 하는 방안을 고려하세요.


67-82: createChatroom 메서드 구조
isCarer 분기를 통해 다른 로직을 호출하는 구조입니다. 객체지향적으로 좀 더 추상화(예: 전략 패턴)할 수도 있으나, 현재 구현도 명확해 유지보수에 큰 문제는 없어 보입니다.


84-90: getRecentMessages 메서드의 기본 messageId
messageIdnull이면 새 UUID를 발급하여 조회하는 방식입니다. 기존 메시지가 하나도 없는 첫 조회 시도를 의미하는 것으로 추정되지만, 오해 요소가 될 수 있으니 추가 주석이나 메서드명 변경(예: getRecentOrAllMessages)을 고려해 보세요.

idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/CreateChatRoomRequest.kt (1)

1-5: CreateChatRoomRequest 데이터 클래스
간단하고 명료한 구조입니다. opponentId라는 변수명이 직관적이지만, 상황에 따라 counterpartId 또는 targetUserId 등 다른 표현을 쓰는 것도 고려할 수 있습니다.

idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt (1)

5-7: 코드 스타일을 개선해주세요.

다음과 같은 개선사항이 있습니다:

  1. 불필요한 빈 블록이 있습니다
  2. 들여쓰기가 일관적이지 않습니다 (5칸 들여쓰기 사용)

다음과 같이 수정해주세요:

-data class ReadMessage(val chatroomId: UUID,
-                       val readUserId: UUID) {
-}
+data class ReadMessage(
+    val chatroomId: UUID,
+    val readUserId: UUID
+)
🧰 Tools
🪛 detekt (1.23.7)

[warning] 6-7: The class or object ReadMessage is empty.

(detekt.empty-blocks.EmptyClassBlock)

idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/SendChatMessageRequest.kt (1)

6-6: 불필요한 빈 줄을 제거해주세요.

6번 줄의 불필요한 빈 줄을 제거하여 코드의 가독성을 개선해주세요.

idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt (1)

37-39: 메시지 유효성 검사를 강화하면 좋겠습니다.

현재는 공백 여부만 확인하고 있습니다. 메시지 길이 제한도 추가하는 것이 좋겠습니다.

    init {
        require(content.isNotBlank()) { "채팅 메시지는 최소 1자 이상 입력해야 합니다." }
+       require(content.length <= 1000) { "채팅 메시지는 1000자를 초과할 수 없습니다." }
    }
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt (1)

13-14: 변수명을 서비스 타입과 일치하도록 수정해주세요.

chatMessageServiceChatFacadeService 타입을 참조하고 있으므로, 변수명을 chatFacadeService로 수정하는 것이 더 명확할 것 같습니다.

-    private val chatMessageService: ChatFacadeService,
+    private val chatFacadeService: ChatFacadeService,
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt (1)

12-14: 불리언 파라미터 대신 enum 사용 권장

false 값의 의미가 불분명하며, 향후 유지보수성을 위해 enum으로 대체하는 것이 좋습니다.

+enum class ChatRoomType {
+    CENTER,
+    CAREGIVER
+}

 class ChatCenterController(
-    private val chatMessageService: ChatFacadeService,
+    private val chatMessageService: ChatFacadeService
 ) : ChatCenterApi {
     override fun createChatroom(request: CreateChatRoomRequest) {
-        chatMessageService.createChatroom(request, false)
+        chatMessageService.createChatroom(request, ChatRoomType.CENTER)
     }
 }
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt (1)

23-32: 쿼리 성능 최적화 필요

  1. 하드코딩된 LIMIT 값을 설정 파일로 분리
  2. 인덱스 힌트 추가
  3. 페이지네이션 파라미터 추가
 @Query(value = """
+    /* INDEX(chat_message idx_chat_room_id) */
     SELECT * 
     FROM chat_message 
     WHERE chat_room_id = :chatroomId 
     AND id < :messageId
     ORDER BY id DESC
-    LIMIT 50
+    LIMIT :limit
 """, nativeQuery = true)
 fun getRecentMessages(@Param("chatroomId") chatroomId: UUID,
-                      @Param("messageId") messageId: UUID): List<ChatMessage>
+                      @Param("messageId") messageId: UUID,
+                      @Param("limit") limit: Int = 50): List<ChatMessage>
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt (1)

31-34: 상수명 개선 필요

현재 상수명이 너무 짧고 의미가 불분명합니다. 더 명확한 이름을 사용하여 코드의 가독성을 높여야 합니다.

-    const val TYPE = "ty"
-    const val DATA = "dt"
-    const val SEND_MESSAGE = "sm"
-    const val READ_MESSAGE = "rm"
+    const val MESSAGE_TYPE = "type"
+    const val MESSAGE_DATA = "data"
+    const val TYPE_SEND_MESSAGE = "send_message"
+    const val TYPE_READ_MESSAGE = "read_message"
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt (1)

18-19: 메서드 파라미터 들여쓰기 개선 필요

메서드 파라미터의 들여쓰기가 일관되지 않아 가독성이 떨어집니다.

다음과 같이 수정을 제안합니다:

-    fun sendMessage(@Payload request: SendChatMessageRequest,
-                             headerAccessor: SimpMessageHeaderAccessor) {
+    fun sendMessage(
+        @Payload request: SendChatMessageRequest,
+        headerAccessor: SimpMessageHeaderAccessor
+    ) {

-    fun read(@Payload request: ReadChatMessagesReqeust,
-                      headerAccessor: SimpMessageHeaderAccessor) {
+    fun read(
+        @Payload request: ReadChatMessagesReqeust,
+        headerAccessor: SimpMessageHeaderAccessor
+    ) {

Also applies to: 25-26

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt (1)

21-21: 채팅방 생성 API 응답 타입 개선 필요

채팅방 생성 API가 생성된 채팅방의 정보를 반환하지 않아 클라이언트에서 추가 요청이 필요할 수 있습니다.

다음과 같이 수정을 제안합니다:

-    fun createChatroom(request: CreateChatRoomRequest)
+    fun createChatroom(request: CreateChatRoomRequest): ChatRoomResponse
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt (2)

30-34: 들여쓰기 불일치 수정 필요

centerChatroomSummary 메서드의 들여쓰기가 다른 메서드들과 일치하지 않습니다.

다음과 같이 수정을 제안합니다:

-        @Secured
+    @Secured

1-35: 코드 중복 제거를 위한 리팩토링 제안

ChatCarerApiChatCenterApi가 유사한 구조를 가지고 있어 코드 중복이 발생합니다. 공통 인터페이스를 도입하여 중복을 제거할 수 있습니다.

다음과 같은 구조로 리팩토링을 제안합니다:

interface ChatApi {
    fun createChatroom(request: CreateChatRoomRequest)
    fun recentMessages(chatroomId: UUID, messageId: UUID?): List<ChatMessage>
    fun getChatroomSummary(): List<ChatRoomSummaryInfo>
}

interface ChatCarerApi : ChatApi
interface ChatCenterApi : ChatApi
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt (1)

21-38: 채팅방 요약 정보 조회 최적화 필요

프로젝션을 통한 매핑 과정이 복잡하며, 데이터베이스에서 직접 DTO로 변환하는 것이 더 효율적일 수 있습니다.

JPA의 생성자 표현식을 사용하여 다음과 같이 최적화할 것을 제안합니다:

@Query("""
    SELECT new com.swm.idle.domain.chat.vo.ChatRoomSummaryInfo(
        cr.id,
        cm.content,
        cm.createdAt,
        COUNT(CASE WHEN cm.isRead = false THEN 1 END),
        CASE WHEN :isCarer = true THEN cr.centerId ELSE cr.carerId END
    )
    FROM ChatRoom cr
    LEFT JOIN cr.messages cm
    WHERE :isCarer = true AND cr.carerId = :userId
        OR :isCarer = false AND cr.centerId = :userId
    GROUP BY cr.id
""")
fun findChatroomSummaries(userId: UUID, isCarer: Boolean): List<ChatRoomSummaryInfo>
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt (1)

19-23: JSON 역직렬화 로직 개선 필요

현재 구현은 불필요한 역직렬화 단계를 포함하고 있습니다. actualJson.asText()를 통해 다시 문자열로 변환한 후 다시 파싱하는 것은 비효율적입니다.

다음과 같이 개선하는 것을 추천드립니다:

-        val rawJson = redisTemplate.stringSerializer.deserialize(message.body)
-        val actualJson = objectMapper.readTree(rawJson).asText()
-        val jsonNode = objectMapper.readTree(actualJson)
+        val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body))
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt (1)

47-78: 중복 쿼리 로직 개선 필요

두 쿼리가 WHERE 절만 다르고 나머지 로직이 동일합니다. 코드 재사용성과 유지보수성을 높이기 위해 개선이 필요합니다.

다음과 같이 공통 쿼리를 추출하여 사용하는 것을 추천드립니다:

@Query("""
    WITH LastMessages AS (
        SELECT 
            chat_room_id,
            content,
            created_at,
            ROW_NUMBER() OVER (PARTITION BY chat_room_id ORDER BY id DESC) as rn
        FROM chat_message
    )
    SELECT
        cr.id AS chatRoomId,
        cr.center_id AS receiverId,
        COUNT(CASE WHEN cm2.is_read = false AND cm2.receiver_id = :userId THEN 1 END) AS unreadCount,
        lm.content AS lastMessage,
        lm.created_at AS lastMessageTime
    FROM chat_room cr
    LEFT JOIN chat_message cm2 ON cm2.chat_room_id = cr.id
    LEFT JOIN LastMessages lm ON lm.chat_room_id = cr.id AND lm.rn = 1
    WHERE cr.:userColumn = :userId
    GROUP BY cr.id, cr.center_id, lm.content, lm.created_at
""", nativeQuery = true)
fun findChatroomSummaries(@Param("userId") userId: UUID, @Param("userColumn") userColumn: String): List<ChatRoomSummaryInfoProjection>
idle-domain/src/main/resources/db/migration/V1__init.sql (1)

80-88: 채팅방 테이블 인덱스 추가 필요

조회 성능 향상을 위한 인덱스가 누락되어 있습니다.

다음 인덱스들을 추가하는 것을 추천드립니다:

CREATE INDEX idx_chat_room_carer ON chat_room(carer_id);
CREATE INDEX idx_chat_room_center ON chat_room(center_id);
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt (1)

34-48: 커스텀 메시지 컨버터 추가
DefaultContentTypeResolverMappingJackson2MessageConverter를 활용해 JSON 직렬화를 설정한 점이 인상적입니다. 다만, 애플리케이션 전역에서 이미 사용하는 ObjectMapper 빈이 있다면 재활용해 일관성을 높일 수 있습니다. 또한 return false로 설정하면 기본 컨버터가 유지되므로, 원하는 바에 맞게 반환값을 설정하는지 확인 부탁드립니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ec970c1 and 1cb7194.

📒 Files selected for processing (38)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt (1 hunks)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt (1 hunks)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt (1 hunks)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatMessageFacadeService.kt (0 hunks)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatRoomFacadeService.kt (0 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatMessageRedisConfig.kt (0 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatRoom.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisConsumer.kt (0 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisPublisher.kt (0 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/Content.kt (0 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt (1 hunks)
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/SendUser.kt (0 hunks)
  • idle-domain/src/main/resources/db/migration/V1__init.sql (2 hunks)
  • idle-domain/src/main/resources/db/migration/V2__add_index.sql (0 hunks)
  • idle-domain/src/main/resources/db/migration/V2__add_phone_number_index.sql (1 hunks)
  • idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatMessageHandler.kt (0 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt (1 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatWebSocketController.kt (0 hunks)
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt (1 hunks)
  • idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/CreateChatRoomRequest.kt (1 hunks)
  • idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt (1 hunks)
  • idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/SendChatMessageRequest.kt (1 hunks)
  • idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/center/CenterJobPostingResponse.kt (2 hunks)
💤 Files with no reviewable changes (10)
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatRoomFacadeService.kt
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/SendUser.kt
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/Content.kt
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisConsumer.kt
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisPublisher.kt
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatWebSocketController.kt
  • idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatMessageHandler.kt
  • idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatMessageFacadeService.kt
  • idle-domain/src/main/resources/db/migration/V2__add_index.sql
  • idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatMessageRedisConfig.kt
🧰 Additional context used
🪛 detekt (1.23.7)
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt

[warning] 6-7: The class or object ReadMessage is empty.

(detekt.empty-blocks.EmptyClassBlock)

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt

[warning] 28-28: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🔇 Additional comments (21)
idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt (1)

1-16: 클래스 구조가 잘 설계되어 있습니다!

의존성 주입과 로깅 설정이 Spring 프레임워크의 모범 사례를 잘 따르고 있습니다.

idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt (1)

1-16: 코드 구조가 깔끔합니다!

Spring 설정 클래스로서의 기본 구조와 필요한 import들이 잘 구성되어 있습니다.

idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt (4)

6-6: Jakarta 트랜잭션 도입 확인
jakarta.transaction.Transactional를 사용하고 계신데, 스프링 부트 3+ 환경에서라면 정상적이지만, 기존 환경과 호환성 이슈가 없는지 확인하는 것이 좋겠습니다.


8-8: UUID 임포트 확인
java.util.UUID 임포트는 표준 사용으로 문제 없어 보입니다.


11-13: 생성자 주입 방식 적용
생성자 주입 방식을 통해 chatMessageRepository를 주입하는 것은 SRP(단일 책임 원칙) 관점에서 간결하며, 테스트 시 유연성도 확보됩니다.


19-21: 메시지 읽음 처리 주의
read 메서드는 DB에서 읽음 처리를 수행하지만, 모든 예외 상황(해당 채팅방이 없는 경우 등 포함)이 처리되는지 확인이 필요합니다.

idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt (4)

1-2: 패키지 선언 확인
새롭게 추가된 패키지 구조로 보이며, 충돌이나 중복 패키지가 없는지 점검해 주세요.


23-34: 클래스 정의
@Service@Transactional(readOnly = true)를 혼합 사용해, 읽기 전용 트랜잭션 기반의 파사드 서비스로 보입니다. 주요 도메인 서비스를 인젝션받아 관리하는 구조가 적절합니다.


55-65: readMessage 메서드 병렬 처리
이 메서드도 runBlocking 내에서 읽음 처리와 퍼블리시를 동시에 실행합니다. 메시지 읽기 로직이 실패할 경우 이벤트가 퍼블리시되지 않도록 예외 관계를 명확히 할지 검토해 주세요.


92-109: getChatroomSummary 메서드의 수신자 정보 보강
receiverNamereceiverProfileImageUrl을 채워주는 로직이 명확하며, 캐싱 등을 고려하면 성능 최적화 여지도 있습니다. 현재는 충분히 간단하므로 괜찮아 보입니다.

idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatRoom.kt (1)

12-13: 구조가 명확하고 적절합니다!

요양보호사와 센터 간의 관계를 명확하게 표현하도록 리팩토링되었습니다. 속성들이 non-null이고 변경 불가능하게 설정된 것이 좋습니다.

Also applies to: 17-18, 21-22

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt (1)

14-16: 메시지 전송 로직이 잘 구현되었습니다.

WebSocket을 통한 실시간 메시지 전송이 적절하게 구현되었습니다.

idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt (1)

34-35: 읽음 상태 추적이 잘 구현되었습니다.

isRead 플래그를 통해 메시지 읽음 상태를 추적하는 것이 좋습니다.

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt (1)

16-26: API 구현이 깔끔합니다.

ChatCarerApi 인터페이스의 구현이 명확하고, 각 메서드의 책임이 잘 분리되어 있습니다.

idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/center/CenterJobPostingResponse.kt (1)

26-28: 구현이 적절합니다!

센터 ID 필드가 적절하게 추가되었으며, Schema 어노테이션을 통해 문서화도 잘 되어있습니다.

Also applies to: 120-121

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt (4)

3-3: 라이브러리 Imports 정리
JSON 직렬화 및 WebSocket 메시지 컨버터 설정을 위한 필수 라이브러리들이 적절하게 import되었습니다. 현재 상태로도 문제는 없어 보입니다.

Also applies to: 5-7, 9-9


17-19: WebSocketConfig 생성자 주입
ChatHandshakeInterceptor를 생성자로 주입받는 구조는 향후 확장성이나 유지보수를 고려했을 때 올바른 설계로 보입니다.


21-24: 메시지 브로커 설정
/sub를 간단 브로커로, /pub를 애플리케이션 경로로 지정한 것은 Pub/Sub 기반의 실시간 통신 구조에 적합해 보입니다.


28-28: STOMP 엔드포인트와 SockJS 설정
/ws 엔드포인트와 SockJS 백업 옵션이 함께 설정되어 있어, 다양한 브라우저 환경에서 WebSocket 통신을 지원할 수 있습니다. 또한 chatHandshakeInterceptor가 정상적으로 연결되도록 설정된 점이 좋습니다.

Also applies to: 30-31

idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt (2)

12-16: ChatHandshakeInterceptor 클래스 등록
@Component로 빈 등록되어 있으며, WebSocket 인증에 특화된 인터셉터로 설계된 점이 전체 구조와 잘 어울립니다.


38-46: afterHandshake 메서드 구현
현재 후속 처리가 필요 없다면 비워 두어도 무방합니다. 필요 시점에 다양한 로직을 추가할 수 있어 확장성 확보에 유리합니다.

Comment on lines +3 to +4
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number);
CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

중요: 인덱스 명칭 중복 이슈
두 테이블(center_manager와 carer)에 대해 동일한 인덱스 이름("idx_phone_number")을 사용하고 있습니다.
일부 데이터베이스(예: PostgreSQL)에서는 동일한 스키마 내에서 인덱스 이름이 고유해야 하므로 충돌이 발생할 수 있습니다.
각 테이블마다 고유한 인덱스 이름(예: idx_center_manager_phone_number, idx_carer_phone_number) 사용을 권장합니다.

-CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number);
+CREATE UNIQUE INDEX idx_carer_phone_number ON carer(phone_number);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number);
CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number);
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number);
CREATE UNIQUE INDEX idx_carer_phone_number ON carer(phone_number);

Comment on lines +41 to +47
private fun clientSend(fcmMessage: Message) {
try {
fcmClient.send(fcmMessage)
} catch (e: Exception) {
logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" }
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오류 처리 로직을 수정해 주세요.

다음과 같은 문제점들이 있습니다:

  1. 로그 메시지에 잘못된 이벤트 이름이 포함되어 있습니다 ("CenterManagerVerificationApproveEvent").
  2. 예외 발생 시 호출자에게 실패 여부를 알리지 않습니다.
-    private fun clientSend(fcmMessage: Message) {
+    private fun clientSend(fcmMessage: Message): Boolean {
         try {
             fcmClient.send(fcmMessage)
+            return true
         } catch (e: Exception) {
-            logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" }
+            logger.warn { "채팅 알림 전송에 실패했습니다: ${e.message}, Message: ${fcmMessage}" }
+            return false
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun clientSend(fcmMessage: Message) {
try {
fcmClient.send(fcmMessage)
} catch (e: Exception) {
logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" }
}
}
private fun clientSend(fcmMessage: Message): Boolean {
try {
fcmClient.send(fcmMessage)
return true
} catch (e: Exception) {
logger.warn { "채팅 알림 전송에 실패했습니다: ${e.message}, Message: ${fcmMessage}" }
return false
}
}

Comment on lines +34 to +37
@Bean
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter {
return MessageListenerAdapter(chatSubscriber,"onMessage")
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

메서드 이름 상수화 필요

문자열로 하드코딩된 메서드 이름은 오타나 리팩토링 시 오류가 발생할 수 있습니다.

다음과 같이 메서드 이름을 상수화하는 것을 제안합니다:

+    companion object {
+        const val CHATROOM = "chatroom"
+        const val ON_MESSAGE_METHOD = "onMessage"
+    }

     @Bean
     fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter {
-        return MessageListenerAdapter(chatSubscriber,"onMessage")
+        return MessageListenerAdapter(chatSubscriber, ON_MESSAGE_METHOD)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter {
return MessageListenerAdapter(chatSubscriber,"onMessage")
}
companion object {
const val CHATROOM = "chatroom"
const val ON_MESSAGE_METHOD = "onMessage"
}
@Bean
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter {
return MessageListenerAdapter(chatSubscriber, ON_MESSAGE_METHOD)
}


import java.util.UUID

data class ReadChatMessagesReqeust(val chatRoomId: UUID)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

클래스 이름의 오타를 수정해주세요.

클래스 이름에 오타가 있습니다: ReadChatMessagesReqeust -> ReadChatMessagesRequest

다음과 같이 수정해주세요:

-data class ReadChatMessagesReqeust(val chatRoomId: UUID)
+data class ReadChatMessagesRequest(val chatRoomId: UUID)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
data class ReadChatMessagesReqeust(val chatRoomId: UUID)
data class ReadChatMessagesRequest(val chatRoomId: UUID)

Comment on lines +6 to +7
fun getChatRoomId(): ByteArray
fun getReceiverId(): ByteArray
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

ID 타입으로 ByteArray 대신 UUID 사용을 고려해주세요.

getChatRoomId()getReceiverId()ByteArray 타입을 반환하고 있습니다. 이는 다음과 같은 문제를 야기할 수 있습니다:

  1. 가독성이 떨어집니다
  2. 다른 부분의 코드에서는 UUID를 사용하고 있어 일관성이 없습니다
  3. 직렬화/역직렬화 시 추가 작업이 필요할 수 있습니다

다음과 같이 수정하는 것을 추천드립니다:

interface ChatRoomSummaryInfoProjection {
-    fun getChatRoomId(): ByteArray
-    fun getReceiverId(): ByteArray
+    fun getChatRoomId(): UUID
+    fun getReceiverId(): UUID
     fun getUnreadCount(): Int
     fun getLastMessage(): String
     fun getLastMessageTime(): LocalDateTime
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun getChatRoomId(): ByteArray
fun getReceiverId(): ByteArray
interface ChatRoomSummaryInfoProjection {
fun getChatRoomId(): UUID
fun getReceiverId(): UUID
fun getUnreadCount(): Int
fun getLastMessage(): String
fun getLastMessageTime(): LocalDateTime
}

@Operation(summary = "보호사의 채팅방 개설 API")
@PostMapping("/chatrooms")
@ResponseStatus(HttpStatus.OK)
fun createChatroom(request: CreateChatRoomRequest)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

요청 검증 어노테이션 추가 필요

요청 객체와 경로 변수에 대한 유효성 검증이 누락되어 있습니다.

다음과 같이 수정을 제안합니다:

-    fun createChatroom(request: CreateChatRoomRequest)
+    fun createChatroom(@Valid request: CreateChatRoomRequest)

     fun recentMessages(
-        @PathVariable(value = "chatroom-id") chatroomId: UUID,
+        @PathVariable(value = "chatroom-id") @NotNull chatroomId: UUID,
         @RequestParam(value = "message-id", required = false) messageId: UUID?
     ): List<ChatMessage>

Also applies to: 27-28

Comment on lines +13 to +19
fun create(carerId: UUID, centerId:UUID) {
val chatRoom = ChatRoom(
carerId = carerId,
centerId = centerId,
)
chatroomRepository.save(chatRoom)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

채팅방 생성 시 유효성 검사 추가 필요

채팅방 생성 시 입력값에 대한 유효성 검사가 누락되어 있습니다. 또한, 중복 채팅방 생성 가능성이 있습니다.

다음과 같이 수정을 제안합니다:

     fun create(carerId: UUID, centerId: UUID) {
+        require(carerId != centerId) { "보호사와 센터 ID가 동일할 수 없습니다" }
+        
+        val existingChatRoom = chatroomRepository.findByCenterIdAndCarerId(centerId, carerId)
+        if (existingChatRoom != null) {
+            throw IllegalStateException("이미 존재하는 채팅방입니다")
+        }
+        
         val chatRoom = ChatRoom(
             carerId = carerId,
             centerId = centerId,
         )
         chatroomRepository.save(chatRoom)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun create(carerId: UUID, centerId:UUID) {
val chatRoom = ChatRoom(
carerId = carerId,
centerId = centerId,
)
chatroomRepository.save(chatRoom)
}
fun create(carerId: UUID, centerId: UUID) {
require(carerId != centerId) { "보호사와 센터 ID가 동일할 수 없습니다" }
val existingChatRoom = chatroomRepository.findByCenterIdAndCarerId(centerId, carerId)
if (existingChatRoom != null) {
throw IllegalStateException("이미 존재하는 채팅방입니다")
}
val chatRoom = ChatRoom(
carerId = carerId,
centerId = centerId,
)
chatroomRepository.save(chatRoom)
}

Comment on lines +24 to +39
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) {
ChatRedisPublisher.SEND_MESSAGE -> {
val chatMessage: ChatMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java
)

eventPublisher.publishEvent(chatMessage)
}
ChatRedisPublisher.READ_MESSAGE -> {
val readMessage: ReadMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java
)

eventPublisher.publishEvent(readMessage)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

예외 처리 로직 추가 필요

메시지 처리 중 발생할 수 있는 예외 상황에 대한 처리가 누락되어 있습니다:

  • JSON 파싱 실패
  • 알 수 없는 메시지 타입
  • 이벤트 발행 실패

다음과 같은 예외 처리를 추가하는 것을 추천드립니다:

     override fun onMessage(message: Message, pattern: ByteArray?) {
+        try {
             val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body))
 
             when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) {
                 ChatRedisPublisher.SEND_MESSAGE -> {
                     val chatMessage: ChatMessage = objectMapper.treeToValue(
                         jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java
                     )
                     eventPublisher.publishEvent(chatMessage)
                 }
                 ChatRedisPublisher.READ_MESSAGE -> {
                     val readMessage: ReadMessage = objectMapper.treeToValue(
                         jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java
                     )
                     eventPublisher.publishEvent(readMessage)
                 }
+                else -> {
+                    throw IllegalArgumentException("Unknown message type")
+                }
             }
+        } catch (e: Exception) {
+            // TODO: 에러 로깅 추가
+            throw RuntimeException("Failed to process message", e)
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) {
ChatRedisPublisher.SEND_MESSAGE -> {
val chatMessage: ChatMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java
)
eventPublisher.publishEvent(chatMessage)
}
ChatRedisPublisher.READ_MESSAGE -> {
val readMessage: ReadMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java
)
eventPublisher.publishEvent(readMessage)
}
}
override fun onMessage(message: Message, pattern: ByteArray?) {
try {
val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body))
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) {
ChatRedisPublisher.SEND_MESSAGE -> {
val chatMessage: ChatMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java
)
eventPublisher.publishEvent(chatMessage)
}
ChatRedisPublisher.READ_MESSAGE -> {
val readMessage: ReadMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java
)
eventPublisher.publishEvent(readMessage)
}
else -> {
throw IllegalArgumentException("Unknown message type")
}
}
} catch (e: Exception) {
// TODO: 에러 로깅 추가
throw RuntimeException("Failed to process message", e)
}
}

Comment on lines +14 to +45
@Query("""
SELECT
cr.id AS chatRoomId,
cr.center_id AS receiverId,
(
SELECT COUNT(*)
FROM chat_message cm2
WHERE cm2.chat_room_id = cr.id
AND cm2.receiver_id = :userId
AND cm2.is_read = false
LIMIT 100
) AS unreadCount,
(
SELECT cm3.content
FROM chat_message cm3
WHERE cm3.chat_room_id = cr.id
ORDER BY cm3.id DESC
LIMIT 1
) AS lastMessage,
(
SELECT cm3.created_at
FROM chat_message cm3
WHERE cm3.chat_room_id = cr.id
ORDER BY cm3.id DESC
LIMIT 1
) AS lastMessageTime
FROM
chat_room cr
WHERE
cr.carer_id = :userId
""", nativeQuery = true)
fun findCaresChatroomSummaries(@Param("userId") userId: UUID): List<ChatRoomSummaryInfoProjection>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

채팅방 요약 정보 조회 쿼리 최적화 필요

  1. 서브쿼리의 LIMIT 100은 미읽은 메시지가 100개 이상일 경우 정확한 카운트를 제공하지 않습니다.
  2. 마지막 메시지 조회를 위한 서브쿼리가 중복되어 있어 성능에 영향을 줄 수 있습니다.

다음과 같이 개선하는 것을 추천드립니다:

WITH LastMessages AS (
    SELECT 
        chat_room_id,
        content,
        created_at,
        ROW_NUMBER() OVER (PARTITION BY chat_room_id ORDER BY id DESC) as rn
    FROM chat_message
)
SELECT
    cr.id AS chatRoomId,
    cr.center_id AS receiverId,
    COUNT(CASE WHEN cm2.is_read = false AND cm2.receiver_id = :userId THEN 1 END) AS unreadCount,
    lm.content AS lastMessage,
    lm.created_at AS lastMessageTime
FROM chat_room cr
LEFT JOIN chat_message cm2 ON cm2.chat_room_id = cr.id
LEFT JOIN LastMessages lm ON lm.chat_room_id = cr.id AND lm.rn = 1
WHERE cr.carer_id = :userId
GROUP BY cr.id, cr.center_id, lm.content, lm.created_at;

Comment on lines +17 to +36
override fun beforeHandshake(
request: ServerHttpRequest,
response: ServerHttpResponse,
wsHandler: WebSocketHandler,
attributes: MutableMap<String, Any>
): Boolean {
if (request is ServletServerHttpRequest) {
val accessToken = request.servletRequest.getHeader("Authorization")

val claims = try {
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow()
} catch (e: Exception) {
return false
}

attributes["userId"] = claims.customClaims["userId"] as String
return true
}
return false
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

예외 처리 시 예외 삼킴 방지
try-catch 블록에서 발생하는 예외를 바로 반환하며 무시하고 있어, 추후 디버깅이 어려울 수 있습니다. 예외 내용을 로깅하거나 처리 로직을 추가하여 문제 상황을 파악할 수 있도록 하는 방안을 권장합니다.

아래 예시처럼 예외를 로깅한 뒤 false를 반환하면 원인 파악에 도움이 됩니다:

 try {
     JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow()
 } catch (e: Exception) {
-    return false
+    // 필요하다면 로깅 처리
+    logger.error("토큰 검증 중 예외 발생", e)
+    return false
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun beforeHandshake(
request: ServerHttpRequest,
response: ServerHttpResponse,
wsHandler: WebSocketHandler,
attributes: MutableMap<String, Any>
): Boolean {
if (request is ServletServerHttpRequest) {
val accessToken = request.servletRequest.getHeader("Authorization")
val claims = try {
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow()
} catch (e: Exception) {
return false
}
attributes["userId"] = claims.customClaims["userId"] as String
return true
}
return false
}
override fun beforeHandshake(
request: ServerHttpRequest,
response: ServerHttpResponse,
wsHandler: WebSocketHandler,
attributes: MutableMap<String, Any>
): Boolean {
if (request is ServletServerHttpRequest) {
val accessToken = request.servletRequest.getHeader("Authorization")
val claims = try {
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow()
} catch (e: Exception) {
// 필요하다면 로깅 처리
logger.error("토큰 검증 중 예외 발생", e)
return false
}
attributes["userId"] = claims.customClaims["userId"] as String
return true
}
return false
}
🧰 Tools
🪛 detekt (1.23.7)

[warning] 28-28: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

@mjj111 mjj111 changed the title [IDLE - 462] 요양 보호사와 센터장간 채팅 구현 [IDLE-462] 요양 보호사와 센터장간 채팅 구현 Feb 17, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageCrawler.kt (1)

24-26: 숫자 형식 처리 개선이 잘 되었습니다!

쉼표가 포함된 게시물 수를 올바르게 처리할 수 있도록 개선되었습니다. 다만, 더 견고한 구현을 위해 다음과 같은 개선사항을 고려해보시면 좋을 것 같습니다:

  1. 공백 문자 제거 추가
  2. 숫자가 아닌 문자가 포함된 경우에 대한 예외 처리

다음과 같이 개선해보시는 것은 어떨까요?

             .text
+            .trim()
             .replace(",", "")
+            .takeIf { it.matches(Regex("\\d+")) }
+            ?.toInt()
+            ?: run {
+                driver.quit()
+                throw Exception("게시물 수가 올바른 숫자 형식이 아닙니다.")
+            }
-            .toInt()
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cb7194 and d2e4430.

📒 Files selected for processing (1)
  • idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageCrawler.kt (1 hunks)

@mjj111 mjj111 merged commit 1b37eb1 into develop Feb 17, 2025
5 checks passed
@mjj111 mjj111 deleted the feat/IDLE-462 branch February 17, 2025 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
✨기능 기능 개발 및 구현
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant