-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 채팅방 검색 기능 추가 #470
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
feat: 채팅방 검색 기능 추가 #470
Changes from all commits
ac5634c
dceff67
92e6acf
5c43771
10d7d6e
2a9d9b7
4f3720a
544193f
5f24070
f69dd83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package gg.agit.konect.domain.chat.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED; | ||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonFormat; | ||
|
|
||
| import gg.agit.konect.domain.chat.enums.ChatType; | ||
| import gg.agit.konect.domain.chat.model.ChatMessage; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record ChatMessageMatchResult( | ||
| @Schema(description = "채팅방 ID", example = "1", requiredMode = REQUIRED) | ||
| Integer roomId, | ||
|
|
||
| @Schema(description = "채팅 타입", example = "DIRECT", requiredMode = REQUIRED) | ||
| ChatType chatType, | ||
|
|
||
| @Schema(description = "채팅방 이름", example = "개발팀", requiredMode = REQUIRED) | ||
| String roomName, | ||
|
|
||
| @Schema(description = "채팅방 이미지 URL", example = "https://example.com/image.png", requiredMode = NOT_REQUIRED) | ||
| String roomImageUrl, | ||
|
|
||
| @Schema(description = "검색에 매칭된 메시지 내용", example = "안녕하세요", requiredMode = REQUIRED) | ||
| String matchedMessage, | ||
|
|
||
| @Schema(description = "매칭된 메시지 전송 시간", example = "2025.12.19 23:21", requiredMode = REQUIRED) | ||
| @JsonFormat(pattern = "yyyy.MM.dd HH:mm") | ||
| LocalDateTime matchedMessageSentAt | ||
| ) { | ||
|
|
||
| public static ChatMessageMatchResult from(ChatRoomSummaryResponse room, ChatMessage message) { | ||
| return new ChatMessageMatchResult( | ||
| room.roomId(), | ||
| room.chatType(), | ||
| room.roomName(), | ||
| room.roomImageUrl(), | ||
| message.getContent(), | ||
| message.getCreatedAt() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package gg.agit.konect.domain.chat.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record ChatMessageMatchesResponse( | ||
| @Schema(description = "조건에 해당하는 메시지 매칭 총 개수", example = "10", requiredMode = REQUIRED) | ||
| Long totalCount, | ||
|
|
||
| @Schema(description = "현재 페이지에서 조회된 메시지 매칭 개수", example = "5", requiredMode = REQUIRED) | ||
| Integer currentCount, | ||
|
|
||
| @Schema(description = "최대 페이지", example = "2", requiredMode = REQUIRED) | ||
| Integer totalPage, | ||
|
|
||
| @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) | ||
| Integer currentPage, | ||
|
|
||
| @Schema(description = "메시지 내용으로 매칭된 채팅방 목록", requiredMode = REQUIRED) | ||
| List<ChatMessageMatchResult> messages | ||
| ) { | ||
|
|
||
| public static ChatMessageMatchesResponse from(Page<ChatMessageMatchResult> page) { | ||
| return new ChatMessageMatchesResponse( | ||
| page.getTotalElements(), | ||
| page.getNumberOfElements(), | ||
| page.getTotalPages(), | ||
| page.getNumber() + 1, | ||
| page.getContent() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package gg.agit.konect.domain.chat.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| import org.springframework.data.domain.Page; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record ChatRoomMatchesResponse( | ||
| @Schema(description = "조건에 해당하는 채팅방 총 개수", example = "10", requiredMode = REQUIRED) | ||
| Long totalCount, | ||
|
|
||
| @Schema(description = "현재 페이지에서 조회된 채팅방 개수", example = "5", requiredMode = REQUIRED) | ||
| Integer currentCount, | ||
|
|
||
| @Schema(description = "최대 페이지", example = "2", requiredMode = REQUIRED) | ||
| Integer totalPage, | ||
|
|
||
| @Schema(description = "현재 페이지", example = "1", requiredMode = REQUIRED) | ||
| Integer currentPage, | ||
|
|
||
| @Schema(description = "채팅방 이름으로 매칭된 채팅방 목록", requiredMode = REQUIRED) | ||
| List<ChatRoomSummaryResponse> rooms | ||
| ) { | ||
|
|
||
| public static ChatRoomMatchesResponse from(Page<ChatRoomSummaryResponse> page) { | ||
| return new ChatRoomMatchesResponse( | ||
| page.getTotalElements(), | ||
| page.getNumberOfElements(), | ||
| page.getTotalPages(), | ||
| page.getNumber() + 1, | ||
| page.getContent() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| package gg.agit.konect.domain.chat.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
|
|
||
| public record ChatSearchResponse( | ||
| @Schema(description = "채팅방 이름으로 매칭된 검색 결과", requiredMode = REQUIRED) | ||
| ChatRoomMatchesResponse roomMatches, | ||
|
|
||
| @Schema(description = "메시지 내용으로 매칭된 검색 결과", requiredMode = REQUIRED) | ||
| ChatMessageMatchesResponse messageMatches | ||
| ) { | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,4 +119,31 @@ SELECT MAX(m2.id) | |
| """) | ||
| List<ChatMessage> findLatestMessagesByRoomIds(@Param("roomIds") List<Integer> roomIds); | ||
|
|
||
| @Query( | ||
| value = """ | ||
| SELECT cm | ||
| FROM ChatMessage cm | ||
| JOIN FETCH cm.chatRoom cr | ||
| WHERE cr.id IN :roomIds | ||
| AND LOCATE(LOWER(:keyword), LOWER(cm.content)) > 0 | ||
| AND cm.id = ( | ||
| SELECT MAX(innerCm.id) | ||
| FROM ChatMessage innerCm | ||
| WHERE innerCm.chatRoom.id = cr.id | ||
| AND LOCATE(LOWER(:keyword), LOWER(innerCm.content)) > 0 | ||
| ) | ||
| ORDER BY cm.createdAt DESC, cm.id DESC | ||
| """ | ||
| ) | ||
| List<ChatMessage> searchLatestMatchingMessagesByChatRoomIds( | ||
| @Param("roomIds") List<Integer> roomIds, | ||
| @Param("keyword") String keyword | ||
| ); | ||
|
Comment on lines
+122
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# 메시지 테이블에 content 검색을 위한 인덱스가 있는지 확인
rg -n "chat_message" --type sql -A 5 -B 2
fd -e sql | xargs grep -l "chat_message" | head -5 | xargs cat 2>/dev/null | head -100Repository: BCSDLab/KONECT_BACK_END Length of output: 17391 🏁 Script executed: fd -e sql src/main/resources/db/migration | sort -V | tail -10Repository: BCSDLab/KONECT_BACK_END Length of output: 496 🏁 Script executed: fd . src/main/resources/db/migration | sort -V | tail -15Repository: BCSDLab/KONECT_BACK_END Length of output: 1178 🏁 Script executed: cat src/main/resources/db/migration/V62__add_performance_indexes.sqlRepository: BCSDLab/KONECT_BACK_END Length of output: 442 🏁 Script executed: cd src/main/resources/db/migration && cat V1__init.sql | grep -A 15 "CREATE TABLE IF NOT EXISTS chat_message"Repository: BCSDLab/KONECT_BACK_END Length of output: 936 🏁 Script executed: rg "searchLatestMatchingMessagesByChatRoomIds" -A 30 src/main/javaRepository: BCSDLab/KONECT_BACK_END Length of output: 4739 🏁 Script executed: cat src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java | head -150 | tail -50Repository: BCSDLab/KONECT_BACK_END Length of output: 1644 LIKE 검색 성능 최적화 검토 권고 메시지 콘텐츠의 전문 검색이 필요한 경우, 현재 LIKE '%keyword%' 패턴 대신 MySQL FULLTEXT 인덱스 도입을 검토하세요. 대규모 메시지 데이터셋에서 키워드 검색 응답 속도를 개선할 수 있습니다. [LEVEL: low] 🤖 Prompt for AI Agents |
||
|
|
||
| @Query(""" | ||
| SELECT COUNT(m) | ||
| FROM ChatMessage m | ||
| WHERE m.chatRoom.id = :chatRoomId | ||
| """) | ||
| long countByChatRoomId(@Param("chatRoomId") Integer chatRoomId); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[LEVEL: high]
limit상한이 없어 과도한 검색 요청을 허용합니다.문제:
GET /chats/rooms/search?limit=100000처럼 큰 값을 전달해도 현재 시그니처에서 제한되지 않습니다.영향: 이 API는 방 이름 검색과 메시지 검색을 함께 수행하므로 대용량 페이지 요청이 DB/애플리케이션 부하를 급격히 높여 운영 장애로 이어질 수 있습니다.
제안:
limit에 서버 공통 최대값(예: 100)을 강제하고, 초과 값은 400으로 거절하도록 계약과 구현을 같이 보강해 주세요.🤖 Prompt for AI Agents