diff --git a/docs/API-CHANGES-MM-168.md b/docs/API-CHANGES-MM-168.md new file mode 100644 index 00000000..746a6186 --- /dev/null +++ b/docs/API-CHANGES-MM-168.md @@ -0,0 +1,77 @@ +# API 변경 사항 (MM-168) + +## 삭제된 API + +### 1. GET /chatrooms/current +변경 전: 현재 채팅방 상태 조회 +변경 후: 삭제됨 +- 더 이상 "현재 채팅방" 개념이 없음. 채팅방 목록에서 상태 확인 가능. + +### 2. GET /chatrooms/current/messages +변경 전: 현재 채팅방의 메시지 조회 +변경 후: 삭제됨 +- `GET /chatrooms/{chatRoomId}/messages` 사용 + +### 3. POST /chatrooms/current/send +변경 전: 현재 채팅방에 메시지 전송 +변경 후: 삭제됨 +- `POST /chatrooms/{chatRoomId}/messages` 사용 + +### 4. POST /chatrooms/current/complete +변경 전: 현재 채팅방 종료 +변경 후: 삭제됨 +- 채팅방 종료는 서버에서 자동 처리됨 + +--- + +## 신규 API + +### 5. POST /chatrooms +변경 전: 없음 +변경 후: 새로운 채팅방 생성 +- 채팅 시작 전 반드시 채팅방을 먼저 생성해야 함 +- 응답으로 `chatRoomId` 반환 + +### 6. POST /chatrooms/{chatRoomId}/messages +변경 전: 없음 +변경 후: 특정 채팅방에 메시지 전송 +- Request Body: `{ "message": "string" }` +- 기존 `/chatrooms/current/send` 대체 + +--- + +## 변경된 API + +### 7. GET /chatrooms +변경 전: +```json +{ + "chatRoomId": 1, + "totalSummary": "전체 요약", + "situationKeyword": "상황 키워드", + "solutionKeyword": "해결 키워드", + "createdAt": "2026-01-12T10:30:00" +} +``` +변경 후: +```json +{ + "chatRoomId": 1, + "title": "채팅방 제목", + "chatRoomState": "ALIVE", + "level": 1, + "lastMessageSentTime": "2026-01-12T10:35:00", + "createdAt": "2026-01-12T10:30:00" +} +``` +- `totalSummary`, `situationKeyword`, `solutionKeyword` 제거 +- `title`, `chatRoomState`, `level`, `lastMessageSentTime` 추가 + +--- + +## Enum 변경 + +### ChatRoomState +변경 전: `BEFORE_INIT`, `ALIVE`, `PAUSED`, `NEED_NEXT_QUESTION`, `COMPLETED`, `DELETED` +변경 후: `ALIVE`, `COMPLETED`, `DELETED` +- 상태가 3개로 단순화됨 diff --git a/sqls/MM-168.sql b/sqls/MM-168.sql new file mode 100644 index 00000000..15c19c04 --- /dev/null +++ b/sqls/MM-168.sql @@ -0,0 +1,151 @@ +-- 작업 01: ChatRoomState enum 정리 마이그레이션 +-- BEFORE_INIT, PAUSED, NEED_NEXT_QUESTION 상태를 ALIVE로 변경 +-- COMPLETED와 DELETED 상태는 그대로 유지 + +-- BEFORE_INIT 상태를 ALIVE로 변경 +UPDATE chat_room +SET chat_room_state = 'ALIVE' +WHERE chat_room_state = 'BEFORE_INIT'; + +-- PAUSED 상태를 ALIVE로 변경 +UPDATE chat_room +SET chat_room_state = 'ALIVE' +WHERE chat_room_state = 'PAUSED'; + +-- NEED_NEXT_QUESTION 상태를 ALIVE로 변경 +UPDATE chat_room +SET chat_room_state = 'ALIVE' +WHERE chat_room_state = 'NEED_NEXT_QUESTION'; + +-- 마이그레이션 확인 쿼리 (실행 후 확인용) +-- SELECT chat_room_state, COUNT(*) as count +-- FROM chat_room +-- GROUP BY chat_room_state; + +-- 작업 02: ChatRoom 도메인 모델 리팩토링 마이그레이션 +-- title 컬럼 추가 (채팅방 제목) + +-- title 컬럼 추가 +ALTER TABLE chat_room ADD COLUMN title VARCHAR(255); + +-- 기존 데이터는 title이 null이므로 별도 업데이트 불필요 +-- 새 채팅방은 1단계 종료 후 title이 생성됨 + +-- 작업 06: 제목 생성 기능을 위한 마이그레이션 +-- prompt 테이블에 is_for_title_generation 컬럼 추가 + +-- is_for_title_generation 컬럼 추가 +ALTER TABLE prompt ADD COLUMN is_for_title_generation BOOLEAN DEFAULT FALSE; + +-- 제목 생성 프롬프트 데이터 추가 +INSERT INTO prompt (level, content, is_for_system, is_for_summary, is_for_completed_response, + is_for_total_summary, is_for_guideline, is_for_answer_metadata, is_for_title_generation) +VALUES (0, '다음 대화 내용을 바탕으로 20자 이내의 간결한 제목을 생성해주세요. +제목은 사용자의 고민을 잘 반영해야 합니다. +제목만 출력하고 따옴표나 부가 설명은 포함하지 마세요.', + false, false, false, false, false, false, true); + +-- 기존 채팅방의 totalSummary를 title로 마이그레이션 +-- COMPLETED 상태이고 totalSummary가 있는 채팅방의 경우 +-- totalSummary의 앞부분을 title로 복사 (최대 255자) +UPDATE chat_room +SET title = CASE + WHEN LENGTH(total_summary) > 50 THEN CONCAT(SUBSTRING(total_summary, 1, 47), '...') + ELSE total_summary +END +WHERE chat_room_state = 'COMPLETED' + AND total_summary IS NOT NULL + AND title IS NULL; + +-- 마이그레이션 확인 쿼리 (실행 후 확인용) +-- SELECT chat_room_id, chat_room_state, +-- LENGTH(total_summary) as summary_length, +-- LENGTH(title) as title_length, +-- title +-- FROM chat_room +-- WHERE chat_room_state = 'COMPLETED' +-- ORDER BY chat_room_id DESC +-- LIMIT 10; + +-- 작업 07: 4단계 프롬프트 추가 +-- 4단계는 전체 상담 완료 후 사용자가 이전 맥락을 유지하며 자유롭게 대화하는 단계 + +-- 4단계 가이드라인 프롬프트 추가 +INSERT INTO prompt (level, content, is_for_system, is_for_summary, is_for_completed_response, + is_for_total_summary, is_for_guideline, is_for_answer_metadata, is_for_title_generation) +VALUES (4, '현재는 상담의 [4단계: 자유 대화] 단계야. + +4단계는 1~3단계의 정형화된 상담이 종료된 후, 사용자가 이전 상담 맥락을 바탕으로 추가 질문을 하거나 자유롭게 대화하는 단계야. + +[응답 생성 규칙] +1. 이전 1~3단계에서 나눴던 갈등 상황, 분석 내용, 해결책 등의 맥락을 충분히 활용할 것 +2. 사용자의 질문이나 고민이 이전 상담 내용과 연결되는 경우, 그 맥락을 자연스럽게 언급하며 응답할 것 +3. 새로운 고민이나 질문이 나온 경우에도, 이전 상담에서 파악한 사용자의 애착유형, 연애 가치관, 감정 패턴 등을 고려하여 맞춤형 조언을 제공할 것 +4. 친구처럼 편안하게 대화하되, 연애 상담 전문가로서의 전문성을 잃지 말 것 +5. 사용자가 이전 상담 내용에 대해 추가 질문을 하면, 구체적이고 깊이 있게 답변할 것 +6. 단계가 구분되어 있다는 것을 사용자가 인지하지 못하도록, 자연스럽게 대화를 이어갈 것 + +[주의사항] +- 형식적인 종료나 새로운 시작을 알리는 멘트는 하지 말 것 +- 이전 대화 내용을 요약하거나 반복하는 것보다는, 그 내용을 바탕으로 새로운 인사이트를 제공할 것 +- 사용자가 완전히 새로운 주제를 꺼내더라도, 가능하다면 이전 맥락과 연결지어 일관성 있는 상담을 유지할 것', + false, false, false, false, true, false, false); + +-- 4단계 요약 프롬프트 추가 +INSERT INTO prompt (level, content, is_for_system, is_for_summary, is_for_completed_response, + is_for_total_summary, is_for_guideline, is_for_answer_metadata, is_for_title_generation) +VALUES (4, '대화를 바탕으로 핵심적인 내용을 요약해줘. +지금 전달된 대화는 앞으로 참조하지 않을 것이며, 오로지 너가 요약한 내용만을 참조할거야. +4단계는 자유 대화 단계이므로, 사용자가 새롭게 제기한 질문이나 고민, 그리고 그에 대한 답변의 핵심을 요약할 것. +만약 이전 상담 내용과 연결되는 대화라면, 그 연결점도 명시할 것. +*요약 글자수 제한 150자* + +[필수 규칙] +- "사용자"라는 표현은 절대 쓰지 말고 해당 표현이 필요한 상황에서는 그냥 주어를 생략할 것. +- "Assistant", "OK" 등 일상 생활에서 사용하지 않는 표현은 절대 응답에 넣지 말 것. +- 명사형 전성어미 (-ㅁ/음)을 이용해 마무리할 것.', + false, true, false, false, false, false, false); + +-- 4단계 세부 프롬프트 추가 (DetailedPrompt) +-- 4단계는 단일 세부 단계로 구성 (detailedLevel = 1) + +-- 4단계 가이드라인 상세 프롬프트 +-- 4단계는 충분성 검사를 하지 않으므로 검증 프롬프트는 불필요 +INSERT INTO detailed_prompt (level, detailed_level, content, is_for_validation, is_for_summary, + metadata_title, is_last_detailed_prompt, is_for_guideline) +VALUES (4, 1, '[응답 생성 규칙] +이전 상담(1~3단계)의 맥락을 바탕으로 사용자의 질문이나 고민에 응답할 것: + +1. 사용자 메시지 파악 + - 이전 상담 내용과 관련된 추가 질문인지 확인 + - 완전히 새로운 주제나 고민인지 확인 + +2. 맥락 연결 + - 관련 질문인 경우: 이전 단계의 갈등 분석, 애착유형 분석, 제안한 해결책 등을 자연스럽게 참조 + - 새로운 주제인 경우: 이전에 파악한 사용자의 애착유형, 연애 가치관, 감정 패턴을 고려하여 조언 + +3. 응답 생성 + - 구체적이고 실용적인 조언 제공 + - 필요시 이전 대화 내용과 연결하여 일관성 유지 + - 친구처럼 자연스럽게, 하지만 전문가로서 깊이 있게 답변 + +4. 추가 대화 유도 + - 사용자가 더 궁금한 점이 있는지 자연스럽게 확인 + - 열린 질문으로 대화를 이어갈 수 있도록 유도 + +[주의사항] +- 충분성 조건 검증이 없으므로, 자유롭게 대화하되 깊이 있는 상담 품질 유지 +- 사용자가 만족스러운 답변을 받을 때까지 성의 있게 응답 +- 이전 상담 내용을 단순 반복하지 말고, 새로운 관점이나 구체적인 예시 추가', + false, false, '자유 대화 응답', true, true); + +-- 마이그레이션 확인 쿼리 (실행 후 확인용) +-- SELECT level, content, is_for_guideline, is_for_summary +-- FROM prompt +-- WHERE level = 4 +-- ORDER BY prompt_id; +-- +-- SELECT level, detailed_level, is_for_guideline, is_for_validation, metadata_title +-- FROM detailed_prompt +-- WHERE level = 4 +-- ORDER BY detailed_prompt_id; diff --git a/sqls/MM-169.sql b/sqls/MM-169.sql new file mode 100644 index 00000000..2c09fc39 --- /dev/null +++ b/sqls/MM-169.sql @@ -0,0 +1,16 @@ +CREATE TABLE bookmark_entity +( + bookmark_id BIGINT NOT NULL AUTO_INCREMENT, + created_at datetime NULL, + modified_at datetime NULL, + deleted_at datetime NULL, + bookmark_state VARCHAR(255) NULL, + chat_room_id BIGINT NULL, + chat_message_id BIGINT NULL, + member_id BIGINT NOT NULL, + CONSTRAINT pk_bookmark_entity PRIMARY KEY (bookmark_id) +); + +CREATE INDEX idx_bookmark_member_chatroom ON bookmark_entity (member_id, chat_room_id); + +CREATE INDEX idx_bookmark_member_message ON bookmark_entity (member_id, chat_message_id); \ No newline at end of file diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java b/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java index 93be6b04..2bd418b0 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java @@ -64,12 +64,15 @@ public void onMessage(MapRecord record) { case REQUEST_CHAT_MESSAGE: future = processChatMessage(payloadNode); break; - case REQUEST_TOTAL_SUMMARY: - future = processTotalSummary(payloadNode); - break; case REQUEST_EXTRACT_METADATA: future = processMetadata(payloadNode); break; + case REQUEST_TITLE_GENERATION: + future = processTitleGeneration(payloadNode); + break; + case REQUEST_CONVERSATION_SUMMARY: + future = processConversationSummary(payloadNode); + break; default: log.warn("Unknown message type: {}", type); // 알 수 없는 타입은 바로 ACK 처리 @@ -110,14 +113,6 @@ private CompletableFuture processChatMessage(JsonNode payloadNode) { ); } - private CompletableFuture processTotalSummary(JsonNode payloadNode) { - return processMessageUseCase.processTotalSummary( - ProcessMessageUseCase.ProcessTotalSummaryCommand.builder() - .chatRoomId(payloadNode.get("chatRoomId").asLong()) - .build() - ); - } - private CompletableFuture processMetadata(JsonNode payloadNode) { return processMessageUseCase.processAnswerMetadata( ProcessMessageUseCase.ProcessAnswerCommand.builder() @@ -128,6 +123,23 @@ private CompletableFuture processMetadata(JsonNode payloadNode) { ); } + private CompletableFuture processTitleGeneration(JsonNode payloadNode) { + return processMessageUseCase.processTitleGeneration( + ProcessMessageUseCase.ProcessTitleGenerationCommand.builder() + .chatRoomId(payloadNode.get("chatRoomId").asLong()) + .build() + ); + } + + private CompletableFuture processConversationSummary(JsonNode payloadNode) { + return processMessageUseCase.processConversationSummary( + ProcessMessageUseCase.ProcessConversationSummaryCommand.builder() + .chatRoomId(payloadNode.get("chatRoomId").asLong()) + .level(payloadNode.get("level").asInt()) + .build() + ); + } + private void handleFailedMessage(MapRecord record) { try { // 현재 retry 횟수 확인 diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/ChatRoomController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/ChatRoomController.java index bee8ccee..2774277f 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/ChatRoomController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/ChatRoomController.java @@ -6,18 +6,23 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; import lombok.AllArgsConstructor; import lombok.Data; +import lombok.Getter; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import makeus.cmc.malmo.adaptor.in.web.docs.ApiCommonResponses; import makeus.cmc.malmo.adaptor.in.web.docs.SwaggerResponses; import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse; import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse; +import makeus.cmc.malmo.application.port.in.chat.CreateChatRoomUseCase; import makeus.cmc.malmo.application.port.in.chat.DeleteChatRoomUseCase; import makeus.cmc.malmo.application.port.in.chat.GetChatRoomListUseCase; import makeus.cmc.malmo.application.port.in.chat.GetChatRoomMessagesUseCase; import makeus.cmc.malmo.application.port.in.chat.GetChatRoomSummaryUseCase; +import makeus.cmc.malmo.application.port.in.chat.SendChatMessageUseCase; import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.security.core.annotation.AuthenticationPrincipal; @@ -32,10 +37,36 @@ @RequiredArgsConstructor public class ChatRoomController { + private final CreateChatRoomUseCase createChatRoomUseCase; private final GetChatRoomSummaryUseCase getChatRoomSummaryUseCase; private final GetChatRoomListUseCase getChatRoomListUseCase; private final GetChatRoomMessagesUseCase getChatRoomMessagesUseCase; private final DeleteChatRoomUseCase deleteChatRoomUseCase; + private final SendChatMessageUseCase sendChatMessageUseCase; + + @Operation( + summary = "채팅방 생성", + description = "새로운 채팅방을 생성합니다. JWT 토큰이 필요합니다.", + security = @SecurityRequirement(name = "Bearer Authentication") + ) + @ApiResponse( + responseCode = "200", + description = "채팅방 생성 성공", + content = @Content(schema = @Schema(implementation = SwaggerResponses.CreateChatRoomResponse.class)) + ) + @ApiCommonResponses.RequireAuth + @PostMapping + public BaseResponse createChatRoom( + @AuthenticationPrincipal User user) { + + CreateChatRoomUseCase.CreateChatRoomResponse response = createChatRoomUseCase.createChatRoom( + CreateChatRoomUseCase.CreateChatRoomCommand.builder() + .userId(Long.valueOf(user.getUsername())) + .build() + ); + + return BaseResponse.success(response); + } @Operation( summary = "채팅방 요약 조회", @@ -113,6 +144,35 @@ public BaseResponse sendMessage( + @AuthenticationPrincipal User user, + @PathVariable Long chatRoomId, + @Valid @RequestBody SendMessageRequest request) { + + SendChatMessageUseCase.SendChatMessageResponse response = sendChatMessageUseCase.processUserMessage( + SendChatMessageUseCase.SendChatMessageCommand.builder() + .userId(Long.valueOf(user.getUsername())) + .chatRoomId(chatRoomId) + .message(request.getMessage()) + .build() + ); + + return BaseResponse.success(response); + } + @Operation( summary = "채팅방 삭제", description = "채팅방을 id 리스트를 통해 다건 동시 삭제합니다. JWT 토큰이 필요합니다.", @@ -126,7 +186,7 @@ public BaseResponse deleteChatRooms( @AuthenticationPrincipal User user, @RequestBody DeleteChatRoomRequestDto requestDto) { DeleteChatRoomUseCase.DeleteChatRoomsCommand command = DeleteChatRoomUseCase.DeleteChatRoomsCommand.builder() .userId(Long.valueOf(user.getUsername())) @@ -137,6 +197,14 @@ public BaseResponse deleteChatRooms( return BaseResponse.success(null); } + @Getter + @AllArgsConstructor + @NoArgsConstructor + public static class SendMessageRequest { + @NotBlank(message = "메시지는 비어있을 수 없습니다.") + private String message; + } + @Data @AllArgsConstructor @NoArgsConstructor diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CurrentChatController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CurrentChatController.java deleted file mode 100644 index 7cc37d3c..00000000 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/CurrentChatController.java +++ /dev/null @@ -1,136 +0,0 @@ -package makeus.cmc.malmo.adaptor.in.web.controller; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.media.Content; -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.responses.ApiResponse; -import io.swagger.v3.oas.annotations.security.SecurityRequirement; -import io.swagger.v3.oas.annotations.tags.Tag; -import jakarta.validation.Valid; -import jakarta.validation.constraints.NotBlank; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.adaptor.in.web.docs.ApiCommonResponses; -import makeus.cmc.malmo.adaptor.in.web.docs.SwaggerResponses; -import makeus.cmc.malmo.adaptor.in.web.dto.BaseListResponse; -import makeus.cmc.malmo.adaptor.in.web.dto.BaseResponse; -import makeus.cmc.malmo.application.port.in.chat.CompleteChatRoomUseCase; -import makeus.cmc.malmo.application.port.in.chat.GetCurrentChatRoomMessagesUseCase; -import makeus.cmc.malmo.application.port.in.chat.GetCurrentChatRoomUseCase; -import makeus.cmc.malmo.application.port.in.chat.SendChatMessageUseCase; -import org.springframework.data.domain.Pageable; -import org.springframework.data.web.PageableDefault; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.userdetails.User; -import org.springframework.web.bind.annotation.*; - -@Tag(name = "채팅 API", description = "사용자 채팅 전송 및 현재 채팅방을 위한 API") -@RestController -@RequestMapping("/chatrooms/current") -@RequiredArgsConstructor -public class CurrentChatController { - - private final SendChatMessageUseCase sendChatMessageUseCase; - private final GetCurrentChatRoomUseCase getCurrentChatRoomUseCase; - private final GetCurrentChatRoomMessagesUseCase getCurrentChatRoomMessagesUseCase; - private final CompleteChatRoomUseCase completeChatRoomUseCase; - - @Operation( - summary = "채팅방 상태 조회", - description = "현재 채팅방의 상태를 조회합니다. JWT 토큰이 필요합니다.", - security = @SecurityRequirement(name = "Bearer Authentication") - ) - @ApiResponse( - responseCode = "200", - description = "채팅방 상태 조회 성공", - content = @Content(schema = @Schema(implementation = SwaggerResponses.ChatRoomStateResponse.class)) - ) - @ApiCommonResponses.RequireAuth - @GetMapping - public BaseResponse getCurrentChatRoom( - @AuthenticationPrincipal User user) { - GetCurrentChatRoomUseCase.GetCurrentChatRoomCommand command = GetCurrentChatRoomUseCase.GetCurrentChatRoomCommand.builder() - .userId(Long.valueOf(user.getUsername())) - .build(); - return BaseResponse.success(getCurrentChatRoomUseCase.getCurrentChatRoom(command)); - } - - @Operation( - summary = "현재 채팅방 메시지 조회", - description = "현재 채팅방의 메시지를 페이지네이션으로 조회합니다. JWT 토큰이 필요합니다.", - security = @SecurityRequirement(name = "Bearer Authentication") - ) - @ApiResponse( - responseCode = "200", - description = "채팅방 상태 조회 성공", - content = @Content(schema = @Schema(implementation = SwaggerResponses.ChatMessageListSuccessResponse.class)) - ) - @ApiCommonResponses.RequireAuth - @GetMapping("/messages") - public BaseResponse> getCurrentChatRoomMessages( - @PageableDefault(page = 0, size = 10) Pageable pageable, - @AuthenticationPrincipal User user) { - GetCurrentChatRoomMessagesUseCase.GetCurrentChatRoomMessagesCommand command = GetCurrentChatRoomMessagesUseCase.GetCurrentChatRoomMessagesCommand.builder() - .userId(Long.valueOf(user.getUsername())) - .pageable(pageable) - .build(); - GetCurrentChatRoomMessagesUseCase.GetCurrentChatRoomMessagesResponse currentChatRoomMessages = getCurrentChatRoomMessagesUseCase.getCurrentChatRoomMessages(command); - - return BaseListResponse.success(currentChatRoomMessages.getMessages(), currentChatRoomMessages.getTotalCount()); - } - - @Operation( - summary = "채팅 메시지 전송", - description = "서버로 AI 상담을 위한 사용자의 메시지를 전달합니다. AI 응답은 SSE로 전달됩니다. JWT 토큰이 필요합니다.", - security = @SecurityRequirement(name = "Bearer Authentication") - ) - @ApiResponse( - responseCode = "200", - description = "메시지 전송 성공", - content = @Content(schema = @Schema(implementation = SwaggerResponses.SendChatSuccessResponse.class)) - ) - @ApiCommonResponses.RequireAuth - @PostMapping("/send") - public BaseResponse sendChatMessage( - @AuthenticationPrincipal User user, - @Valid @RequestBody ChatRequest request) { - SendChatMessageUseCase.SendChatMessageResponse sendChatMessageResponse = sendChatMessageUseCase.processUserMessage( - SendChatMessageUseCase.SendChatMessageCommand.builder() - .userId(Long.valueOf(user.getUsername())) - .message(request.getMessage()) - .build()); - - return BaseResponse.success(sendChatMessageResponse); - } - - @Operation( - summary = "채팅방 종료", - description = "현재 채팅방을 종료합니다. JWT 토큰이 필요합니다.", - security = @SecurityRequirement(name = "Bearer Authentication") - ) - @ApiResponse( - responseCode = "200", - description = "채팅방 종료 성공; 데이터 응답 값은 없음", - content = @Content(schema = @Schema(implementation = SwaggerResponses.CompleteChatRoomResponse.class)) - ) - @ApiCommonResponses.RequireAuth - @PostMapping("/complete") - public BaseResponse completeChatRoom( - @AuthenticationPrincipal User user) { - CompleteChatRoomUseCase.CompleteChatRoomCommand command = CompleteChatRoomUseCase.CompleteChatRoomCommand.builder() - .userId(Long.valueOf(user.getUsername())) - .build(); - - return BaseResponse.success(completeChatRoomUseCase.completeChatRoom(command)); - } - - @Getter - @AllArgsConstructor - @NoArgsConstructor - public static class ChatRequest { - @NotBlank(message = "메시지는 비어있을 수 없습니다.") - private String message; - } -} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/docs/SwaggerResponses.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/docs/SwaggerResponses.java index 6711522d..5a638a91 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/docs/SwaggerResponses.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/docs/SwaggerResponses.java @@ -537,13 +537,29 @@ public static class SendChatData { private Long messageId; } + @Getter + @Schema(description = "채팅방 생성 성공 응답") + public static class CreateChatRoomResponse extends BaseSwaggerResponse { + } + + @Getter + @Schema(description = "채팅방 생성 응답 데이터") + public static class CreateChatRoomData { + @Schema(description = "채팅방 ID", example = "123") + private Long chatRoomId; + @Schema(description = "채팅방 상태", example = "ALIVE") + private ChatRoomState chatRoomState; + @Schema(description = "생성 시간", example = "2026-01-12T15:30:00") + private LocalDateTime createdAt; + } + @Getter @Schema(description = "채팅방 상태 응답 데이터") public static class ChatRoomStateData { @Schema(description = "현재 채팅방의 ID", example = "1") private Long chatRoomId; - @Schema(description = "현재 채팅방의 상태", example = "BEFORE_INIT") + @Schema(description = "현재 채팅방의 상태", example = "ALIVE") private ChatRoomState chatRoomState; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/message/RequestConversationSummaryMessage.java b/src/main/java/makeus/cmc/malmo/adaptor/message/RequestConversationSummaryMessage.java new file mode 100644 index 00000000..960d10bf --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/message/RequestConversationSummaryMessage.java @@ -0,0 +1,15 @@ +package makeus.cmc.malmo.adaptor.message; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) +public class RequestConversationSummaryMessage implements StreamMessage { + private Long chatRoomId; + private Integer level; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/message/RequestTotalSummaryMessage.java b/src/main/java/makeus/cmc/malmo/adaptor/message/RequestTitleGenerationMessage.java similarity index 69% rename from src/main/java/makeus/cmc/malmo/adaptor/message/RequestTotalSummaryMessage.java rename to src/main/java/makeus/cmc/malmo/adaptor/message/RequestTitleGenerationMessage.java index a9cfa89b..ebe07978 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/message/RequestTotalSummaryMessage.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/message/RequestTitleGenerationMessage.java @@ -3,10 +3,12 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter +@NoArgsConstructor @AllArgsConstructor @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) -public class RequestTotalSummaryMessage implements StreamMessage { +public class RequestTitleGenerationMessage implements StreamMessage { private Long chatRoomId; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/message/StreamMessageType.java b/src/main/java/makeus/cmc/malmo/adaptor/message/StreamMessageType.java index 046fe7b6..6f9bafec 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/message/StreamMessageType.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/message/StreamMessageType.java @@ -2,7 +2,7 @@ public enum StreamMessageType { REQUEST_CHAT_MESSAGE, - REQUEST_TOTAL_SUMMARY, - REQUEST_SUMMARY, - REQUEST_EXTRACT_METADATA; + REQUEST_EXTRACT_METADATA, + REQUEST_TITLE_GENERATION, // 제목 생성 요청 + REQUEST_CONVERSATION_SUMMARY; // 4단계 대화 요약 요청 } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatMessageSummaryPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatMessageSummaryPersistenceAdapter.java index 20e0e39c..b5431cf9 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatMessageSummaryPersistenceAdapter.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatMessageSummaryPersistenceAdapter.java @@ -1,21 +1,20 @@ package makeus.cmc.malmo.adaptor.out.persistence.adapter; import lombok.RequiredArgsConstructor; -import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageSummaryEntity; import makeus.cmc.malmo.adaptor.out.persistence.mapper.ChatMessageSummaryMapper; import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.ChatMessageSummaryRepository; import makeus.cmc.malmo.application.port.out.chat.LoadSummarizedMessages; -import makeus.cmc.malmo.application.port.out.chat.SaveChatMessageSummaryPort; import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import org.springframework.stereotype.Component; import java.util.List; +import java.util.Optional; @RequiredArgsConstructor @Component public class ChatMessageSummaryPersistenceAdapter - implements LoadSummarizedMessages, SaveChatMessageSummaryPort { + implements LoadSummarizedMessages { private final ChatMessageSummaryRepository chatMessageSummaryRepository; private final ChatMessageSummaryMapper chatMessageSummaryMapper; @@ -29,8 +28,8 @@ public List loadSummarizedMessages(ChatRoomId chatRoomId) { } @Override - public void saveChatMessageSummary(ChatMessageSummary chatMessageSummary) { - ChatMessageSummaryEntity entity = chatMessageSummaryMapper.toEntity(chatMessageSummary); - chatMessageSummaryRepository.save(entity); + public Optional loadLatestSummaryByLevel(ChatRoomId chatRoomId, int level) { + return chatMessageSummaryRepository.findLatestByChatRoomIdAndLevel(chatRoomId.getValue(), level) + .map(chatMessageSummaryMapper::toDomain); } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatRoomPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatRoomPersistenceAdapter.java index 2a44a6f5..061833f0 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatRoomPersistenceAdapter.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/ChatRoomPersistenceAdapter.java @@ -2,13 +2,17 @@ import lombok.RequiredArgsConstructor; import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageSummaryEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatRoomEntity; import makeus.cmc.malmo.adaptor.out.persistence.mapper.ChatMessageMapper; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.ChatMessageSummaryMapper; import makeus.cmc.malmo.adaptor.out.persistence.mapper.ChatRoomMapper; import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.ChatMessageRepository; +import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.ChatMessageSummaryRepository; import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.ChatRoomRepository; import makeus.cmc.malmo.application.port.out.chat.*; import makeus.cmc.malmo.domain.model.chat.ChatMessage; +import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.model.chat.ChatRoom; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import makeus.cmc.malmo.domain.value.id.MemberId; @@ -23,12 +27,14 @@ @Component @RequiredArgsConstructor public class ChatRoomPersistenceAdapter - implements LoadMessagesPort, SaveChatRoomPort, LoadChatRoomPort, SaveChatMessagePort, DeleteChatRoomPort { + implements LoadMessagesPort, SaveChatRoomPort, LoadChatRoomPort, SaveChatMessagePort, DeleteChatRoomPort, SaveChatMessageSummaryPort { private final ChatRoomRepository chatRoomRepository; private final ChatMessageRepository chatMessageRepository; + private final ChatMessageSummaryRepository chatMessageSummaryRepository; private final ChatRoomMapper chatRoomMapper; private final ChatMessageMapper chatMessageMapper; + private final ChatMessageSummaryMapper chatMessageSummaryMapper; @Override public Optional loadMessageById(Long messageId) { @@ -63,9 +69,26 @@ public List loadChatRoomLevelAndDetailedLevelMessages(ChatRoomId ch } @Override - public Optional loadCurrentChatRoomByMemberId(MemberId memberId) { - return chatRoomRepository.findCurrentChatRoomByMemberEntityId(memberId.getValue()) - .map(chatRoomMapper::toDomain); + public List loadRecentMessagesByLevel(ChatRoomId chatRoomId, int level, int limit) { + List entities = chatMessageRepository.findByChatRoomIdAndLevelOrderByCreatedAtDesc( + chatRoomId.getValue(), level); + return entities.stream() + .limit(limit) + .map(chatMessageMapper::toDomain) + .toList(); + } + + @Override + public long countMessagesByLevel(ChatRoomId chatRoomId, int level) { + return chatMessageRepository.countByChatRoomIdAndLevel(chatRoomId.getValue(), level); + } + + @Override + public List loadActiveChatRoomsByMemberId(MemberId memberId) { + return chatRoomRepository.findActiveChatRoomsByMemberEntityId(memberId.getValue()) + .stream() + .map(chatRoomMapper::toDomain) + .toList(); } @Override @@ -103,13 +126,7 @@ public Optional loadChatRoomById(ChatRoomId chatRoomId) { } @Override - public Optional loadPausedChatRoomByMemberId(MemberId memberId) { - return chatRoomRepository.findPausedChatRoomByMemberEntityId(memberId.getValue()) - .map(chatRoomMapper::toDomain); - } - - @Override - public Page loadAliveChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable) { + public Page loadChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable) { Page chatRoomEntities = chatRoomRepository.loadChatRoomListByMemberId(memberId.getValue(), keyword, pageable); return new PageImpl<>(chatRoomEntities.stream().map(chatRoomMapper::toDomain).toList(), pageable, @@ -129,4 +146,11 @@ public void deleteChatRooms(List chatRoomIds) { chatRoomIds.stream().map(ChatRoomId::getValue).toList() ); } + + @Override + public ChatMessageSummary saveChatMessageSummary(ChatMessageSummary chatMessageSummary) { + ChatMessageSummaryEntity entity = chatMessageSummaryMapper.toEntity(chatMessageSummary); + ChatMessageSummaryEntity savedEntity = chatMessageSummaryRepository.save(entity); + return chatMessageSummaryMapper.toDomain(savedEntity); + } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/PromptPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/PromptPersistenceAdapter.java index bc3de086..24a95ec7 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/PromptPersistenceAdapter.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/PromptPersistenceAdapter.java @@ -64,4 +64,10 @@ public Optional loadSummaryPromptByLevel(int level) { return promptRepository.findByLevelAndIsForSummaryTrue(level) .map(promptMapper::toDomain); } + + @Override + public Optional loadTitleGenerationPrompt() { + return promptRepository.findByIsForTitleGenerationTrue() + .map(promptMapper::toDomain); + } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatRoomEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatRoomEntity.java index d2a4261e..cfee9822 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatRoomEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatRoomEntity.java @@ -35,6 +35,9 @@ public class ChatRoomEntity extends BaseTimeEntity { private LocalDateTime lastMessageSentTime; + @Column(length = 255) + private String title; + @Column(columnDefinition = "TEXT") private String totalSummary; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/PromptEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/PromptEntity.java index df0fec95..56fe93ff 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/PromptEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/PromptEntity.java @@ -35,4 +35,6 @@ public class PromptEntity extends BaseTimeEntity { private boolean isForAnswerMetadata; + private boolean isForTitleGeneration; + } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatRoomMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatRoomMapper.java index 4b9ae8ce..a47d40aa 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatRoomMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatRoomMapper.java @@ -19,6 +19,7 @@ public ChatRoom toDomain(ChatRoomEntity entity) { entity.getLevel(), entity.getDetailedLevel(), entity.getLastMessageSentTime(), + entity.getTitle(), entity.getTotalSummary(), entity.getSituationKeyword(), entity.getSolutionKeyword(), @@ -42,6 +43,7 @@ public ChatRoomEntity toEntity(ChatRoom domain) { .lastMessageSentTime(domain.getLastMessageSentTime()) .level(domain.getLevel()) .detailedLevel(domain.getDetailedLevel()) + .title(domain.getTitle()) .totalSummary(domain.getTotalSummary()) .situationKeyword(domain.getSituationKeyword()) .solutionKeyword(domain.getSolutionKeyword()) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/PromptMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/PromptMapper.java index 1a69c5ed..3a342424 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/PromptMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/PromptMapper.java @@ -22,6 +22,7 @@ public Prompt toDomain(PromptEntity entity) { entity.isForTotalSummary(), entity.isForGuideline(), entity.isForAnswerMetadata(), + entity.isForTitleGeneration(), entity.getCreatedAt(), entity.getModifiedAt(), entity.getDeletedAt() @@ -43,6 +44,7 @@ public PromptEntity toEntity(Prompt domain) { .isForTotalSummary(domain.isForTotalSummary()) .isForGuideline(domain.isForGuideline()) .isForAnswerMetadata(domain.isForAnswerMetadata()) + .isForTitleGeneration(domain.isForTitleGeneration()) .createdAt(domain.getCreatedAt()) .modifiedAt(domain.getModifiedAt()) .deletedAt(domain.getDeletedAt()) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageRepository.java index dce3deea..29cae069 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageRepository.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageRepository.java @@ -3,9 +3,9 @@ import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; -import java.util.Optional; public interface ChatMessageRepository extends JpaRepository, ChatMessageRepositoryCustom { @@ -14,4 +14,10 @@ public interface ChatMessageRepository extends JpaRepository findByChatRoomIdAndLevelAndDetailedLevel(Long chatRoomId, int level, int detailedLevel); + + @Query("SELECT c FROM ChatMessageEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level ORDER BY c.createdAt DESC") + List findByChatRoomIdAndLevelOrderByCreatedAtDesc(@Param("chatRoomId") Long chatRoomId, @Param("level") int level); + + @Query("SELECT COUNT(c) FROM ChatMessageEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level") + long countByChatRoomIdAndLevel(@Param("chatRoomId") Long chatRoomId, @Param("level") int level); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageSummaryRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageSummaryRepository.java index 698e761d..5efca85d 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageSummaryRepository.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatMessageSummaryRepository.java @@ -3,11 +3,19 @@ import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageSummaryEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; +import java.util.Optional; public interface ChatMessageSummaryRepository extends JpaRepository { @Query("SELECT c FROM ChatMessageSummaryEntity c WHERE c.chatRoomEntityId.value = :chatRoomId") List findSummarizedMessagesByChatRoomEntityId(Long chatRoomId); + + @Query("SELECT c FROM ChatMessageSummaryEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level ORDER BY c.createdAt DESC") + List findByChatRoomIdAndLevelOrderByCreatedAtDesc(@Param("chatRoomId") Long chatRoomId, @Param("level") int level); + + @Query("SELECT c FROM ChatMessageSummaryEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level ORDER BY c.createdAt DESC") + Optional findLatestByChatRoomIdAndLevel(@Param("chatRoomId") Long chatRoomId, @Param("level") int level); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepository.java index 314055fc..9a063af1 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepository.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepository.java @@ -4,13 +4,11 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -import java.util.Optional; +import java.util.List; public interface ChatRoomRepository extends JpaRepository, ChatRoomRepositoryCustom { - @Query("select c from ChatRoomEntity c where c.memberEntityId.value = ?1 AND c.chatRoomState != 'DELETED' AND c.chatRoomState != 'COMPLETED'") - Optional findCurrentChatRoomByMemberEntityId(Long memberId); - - @Query("select c from ChatRoomEntity c where c.memberEntityId.value = ?1 AND c.chatRoomState = 'PAUSED'") - Optional findPausedChatRoomByMemberEntityId(Long memberId); + // 진행 중인 채팅방 목록 조회 (ALIVE 상태만) + @Query("SELECT c FROM ChatRoomEntity c WHERE c.memberEntityId.value = ?1 AND c.chatRoomState = 'ALIVE' ORDER BY c.lastMessageSentTime DESC") + List findActiveChatRoomsByMemberEntityId(Long memberId); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepositoryCustomImpl.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepositoryCustomImpl.java index 30e5fa42..2febcb9e 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepositoryCustomImpl.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/ChatRoomRepositoryCustomImpl.java @@ -19,24 +19,25 @@ public class ChatRoomRepositoryCustomImpl implements ChatRoomRepositoryCustom{ private final JPAQueryFactory queryFactory; public Page loadChatRoomListByMemberId(Long memberId, String keyword, Pageable pageable) { - BooleanExpression keywordCondition = keyword == null || keyword.isEmpty() + // DELETED만 제외하고 조회 + BooleanExpression baseCondition = chatRoomEntity.memberEntityId.value.eq(memberId) + .and(chatRoomEntity.chatRoomState.ne(ChatRoomState.DELETED)); + + // 키워드 검색 (제목 기준) + BooleanExpression keywordCondition = keyword == null || keyword.isBlank() ? null - : chatRoomEntity.totalSummary.containsIgnoreCase(keyword); + : chatRoomEntity.title.containsIgnoreCase(keyword); List content = queryFactory.selectFrom(chatRoomEntity) - .where(chatRoomEntity.memberEntityId.value.eq(memberId) - .and(chatRoomEntity.chatRoomState.eq(ChatRoomState.COMPLETED)) - .and(keywordCondition)) - .orderBy(chatRoomEntity.createdAt.desc()) + .where(baseCondition.and(keywordCondition)) + .orderBy(chatRoomEntity.lastMessageSentTime.desc()) // 최근 활동 순 .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); long total = queryFactory.select(chatRoomEntity.count()) .from(chatRoomEntity) - .where(chatRoomEntity.memberEntityId.value.eq(memberId) - .and(chatRoomEntity.chatRoomState.eq(ChatRoomState.COMPLETED)) - .and(keywordCondition)) + .where(baseCondition.and(keywordCondition)) .fetchOne(); return new PageImpl<>(content, pageable, total); diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/PromptRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/PromptRepository.java index 02fa0fb9..dbb24d08 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/PromptRepository.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/PromptRepository.java @@ -32,4 +32,7 @@ public interface PromptRepository extends JpaRepository { @Query("select p from PromptEntity p where p.level = ?1 and p.isForSummary = true") Optional findByLevelAndIsForSummaryTrue(int level); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForTitleGeneration = true") + Optional findByIsForTitleGenerationTrue(); } diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomCommandHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomCommandHelper.java index accec09c..3997dc0b 100644 --- a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomCommandHelper.java +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomCommandHelper.java @@ -3,8 +3,10 @@ import lombok.RequiredArgsConstructor; import makeus.cmc.malmo.application.port.out.chat.DeleteChatRoomPort; import makeus.cmc.malmo.application.port.out.chat.SaveChatMessagePort; +import makeus.cmc.malmo.application.port.out.chat.SaveChatMessageSummaryPort; import makeus.cmc.malmo.application.port.out.chat.SaveChatRoomPort; import makeus.cmc.malmo.domain.model.chat.ChatMessage; +import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.model.chat.ChatRoom; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import org.springframework.stereotype.Component; @@ -19,6 +21,7 @@ public class ChatRoomCommandHelper { private final DeleteChatRoomPort deleteChatRoomPort; private final SaveChatMessagePort saveChatMessagePort; + private final SaveChatMessageSummaryPort saveChatMessageSummaryPort; public ChatRoom saveChatRoom(ChatRoom chatRoom) { return saveChatRoomPort.saveChatRoom(chatRoom); @@ -35,4 +38,8 @@ public ChatMessage saveChatMessage(ChatMessage chatMessage) { public List saveChatMessages(List chatMessages) { return saveChatMessagePort.saveChatMessages(chatMessages); } + + public ChatMessageSummary saveChatMessageSummary(ChatMessageSummary chatMessageSummary) { + return saveChatMessageSummaryPort.saveChatMessageSummary(chatMessageSummary); + } } diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomQueryHelper.java index 6106a126..8823cf5a 100644 --- a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomQueryHelper.java +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/ChatRoomQueryHelper.java @@ -12,7 +12,6 @@ import makeus.cmc.malmo.domain.model.chat.ChatMessage; import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.model.chat.ChatRoom; -import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; import makeus.cmc.malmo.domain.model.member.MemberMemory; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import makeus.cmc.malmo.domain.value.id.MemberId; @@ -35,13 +34,9 @@ public class ChatRoomQueryHelper { private final LoadMessagesPort loadMessagesPort; private final LoadSummarizedMessages loadSummarizedMessages; - public Optional getCurrentChatRoomByMemberId(MemberId memberId) { - return loadChatRoomPort.loadCurrentChatRoomByMemberId(memberId); - } - - public ChatRoom getCurrentChatRoomByMemberIdOrThrow(MemberId memberId) { - return loadChatRoomPort.loadCurrentChatRoomByMemberId(memberId) - .orElseThrow(ChatRoomNotFoundException::new); + // 진행 중인 채팅방 목록 조회 + public List getActiveChatRoomsByMemberId(MemberId memberId) { + return loadChatRoomPort.loadActiveChatRoomsByMemberId(memberId); } public LoadChatRoomMetadataPort.ChatRoomMetadataDto getChatRoomMetadata(MemberId memberId) { @@ -54,8 +49,8 @@ public ChatRoom getChatRoomByIdOrThrow(ChatRoomId chatRoomId) { .orElseThrow(ChatRoomNotFoundException::new); } - public Page getCompletedChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable) { - return loadChatRoomPort.loadAliveChatRoomsByMemberId(memberId, keyword, pageable); + public Page getChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable) { + return loadChatRoomPort.loadChatRoomsByMemberId(memberId, keyword, pageable); } public void validateChatRoomOwnership(MemberId memberId, ChatRoomId chatRoomId) { @@ -77,17 +72,14 @@ public void validateChatRoomsOwnership(MemberId memberId, List chatR } } - public void validateChatRoomAlive(MemberId memberId) { - loadChatRoomPort.loadCurrentChatRoomByMemberId(memberId) - .ifPresentOrElse(chatRoom -> { - if (!chatRoom.isChatRoomValid()) { - throw new NotValidChatRoomException(); - } - } - , () -> { - throw new ChatRoomNotFoundException(); - } - ); + // 채팅방 유효성 검증 (특정 채팅방 ID 기반) + public void validateChatRoomActive(ChatRoomId chatRoomId) { + ChatRoom chatRoom = loadChatRoomPort.loadChatRoomById(chatRoomId) + .orElseThrow(ChatRoomNotFoundException::new); + + if (!chatRoom.isChatRoomValid()) { + throw new NotValidChatRoomException(); + } } /* @@ -117,4 +109,16 @@ public List getChatRoomLevelAndDetailedLevelMessages(ChatRoomId cha public List getMemberMemoriesByMemberId(MemberId memberId) { return loadMemberMemoryPort.loadMemberMemoryByMemberId(memberId); } + + public List getRecentMessages(ChatRoomId chatRoomId, int level, int limit) { + return loadMessagesPort.loadRecentMessagesByLevel(chatRoomId, level, limit); + } + + public long countMessagesByLevel(ChatRoomId chatRoomId, int level) { + return loadMessagesPort.countMessagesByLevel(chatRoomId, level); + } + + public Optional getLatestSummaryByLevel(ChatRoomId chatRoomId, int level) { + return loadSummarizedMessages.loadLatestSummaryByLevel(chatRoomId, level); + } } diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/DetailedPromptQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/DetailedPromptQueryHelper.java index fd48e331..923bba3d 100644 --- a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/DetailedPromptQueryHelper.java +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/DetailedPromptQueryHelper.java @@ -6,7 +6,6 @@ import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; import org.springframework.stereotype.Component; -import java.util.List; import java.util.Optional; @Component @@ -25,4 +24,22 @@ public Optional getGuidelinePrompt(int level, int detailedLevel) return detailedPromptRepository.findByLevelAndDetailedLevelAndIsForGuidelineTrue(level, detailedLevel) .map(detailedPromptMapper::toDomain); } + + /** + * DetailedPrompt 조회 (fallback 지원) + * 요청한 레벨의 프롬프트가 없으면 3단계 1번 프롬프트 반환 + */ + public DetailedPrompt getGuidelinePromptWithFallback(int level, int detailedLevel) { + Optional prompt = detailedPromptRepository.findByLevelAndDetailedLevelAndIsForGuidelineTrue(level, detailedLevel) + .map(detailedPromptMapper::toDomain); + + if (prompt.isEmpty()) { + // 5단계 이상: 4단계 1번 프롬프트 재사용 + return detailedPromptRepository.findByLevelAndDetailedLevelAndIsForGuidelineTrue(4, 1) + .map(detailedPromptMapper::toDomain) + .orElseThrow(() -> new RuntimeException("Fallback prompt not found")); + } + + return prompt.get(); + } } diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/PromptQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/PromptQueryHelper.java index d7fc28da..379823e7 100644 --- a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/PromptQueryHelper.java +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/PromptQueryHelper.java @@ -6,6 +6,8 @@ import makeus.cmc.malmo.domain.model.chat.Prompt; import org.springframework.stereotype.Component; +import java.util.Optional; + @Component @RequiredArgsConstructor @@ -17,29 +19,51 @@ public Prompt getSystemPrompt() { .orElseThrow(PromptNotFoundException::new); } - @Deprecated - public Prompt getSummaryPrompt() { - return loadPromptPort.loadSummaryPrompt() + + public Prompt getGuidelinePrompt(int level) { + return loadPromptPort.loadGuidelinePrompt(level) .orElseThrow(PromptNotFoundException::new); } - public Prompt getSummaryPrompt(int level) { - return loadPromptPort.loadSummaryPromptByLevel(level) - .orElseThrow(PromptNotFoundException::new); + /** + * 프롬프트 조회 (fallback 지원) + * 요청한 레벨의 프롬프트가 없으면 3단계 프롬프트 반환 + */ + public Prompt getGuidelinePromptWithFallback(int level) { + Prompt prompt = loadPromptPort.loadGuidelinePrompt(level).orElse(null); + + if (prompt == null) { + // 5단계 이상: 4단계 프롬프트 재사용 + return loadPromptPort.loadGuidelinePrompt(4) + .orElseThrow(PromptNotFoundException::new); + } + + // isForCompletedResponse가 true인 경우도 무시하고 4단계 반환 + if (prompt.isForCompletedResponse()) { + return loadPromptPort.loadGuidelinePrompt(4) + .orElseThrow(PromptNotFoundException::new); + } + + return prompt; } - public Prompt getTotalSummaryPrompt() { - return loadPromptPort.loadTotalSummaryPrompt() + public Prompt getAnswerMetadataPrompt() { + return loadPromptPort.loadAnswerMetadataPrompt() .orElseThrow(PromptNotFoundException::new); } - public Prompt getGuidelinePrompt(int level) { - return loadPromptPort.loadGuidelinePrompt(level) - .orElseThrow(PromptNotFoundException::new); + /** + * 제목 생성 프롬프트 조회 + */ + public Prompt getTitleGenerationPrompt() { + return loadPromptPort.loadTitleGenerationPrompt() + .orElseThrow(() -> new RuntimeException("Title generation prompt not found")); } - public Prompt getAnswerMetadataPrompt() { - return loadPromptPort.loadAnswerMetadataPrompt() - .orElseThrow(PromptNotFoundException::new); + /** + * 레벨별 요약 프롬프트 조회 + */ + public Optional getSummaryPromptByLevel(int level) { + return loadPromptPort.loadSummaryPromptByLevel(level); } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/CompleteChatRoomUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/CompleteChatRoomUseCase.java deleted file mode 100644 index a5832a3e..00000000 --- a/src/main/java/makeus/cmc/malmo/application/port/in/chat/CompleteChatRoomUseCase.java +++ /dev/null @@ -1,20 +0,0 @@ -package makeus.cmc.malmo.application.port.in.chat; - -import lombok.Builder; -import lombok.Data; - -public interface CompleteChatRoomUseCase { - CompleteChatRoomResponse completeChatRoom(CompleteChatRoomCommand command); - - @Data - @Builder - class CompleteChatRoomCommand { - private Long userId; - } - - @Data - @Builder - class CompleteChatRoomResponse { - private Long chatRoomId; - } -} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/CreateChatRoomUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/CreateChatRoomUseCase.java new file mode 100644 index 00000000..cf29cf47 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/in/chat/CreateChatRoomUseCase.java @@ -0,0 +1,26 @@ +package makeus.cmc.malmo.application.port.in.chat; + +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.domain.value.state.ChatRoomState; + +import java.time.LocalDateTime; + +public interface CreateChatRoomUseCase { + + CreateChatRoomResponse createChatRoom(CreateChatRoomCommand command); + + @Builder + @Getter + class CreateChatRoomCommand { + private final Long userId; + } + + @Builder + @Getter + class CreateChatRoomResponse { + private final Long chatRoomId; + private final ChatRoomState chatRoomState; + private final LocalDateTime createdAt; + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetChatRoomListUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetChatRoomListUseCase.java index 0016e3cf..690d7977 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetChatRoomListUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetChatRoomListUseCase.java @@ -2,6 +2,7 @@ import lombok.Builder; import lombok.Data; +import makeus.cmc.malmo.domain.value.state.ChatRoomState; import org.springframework.data.domain.Pageable; import java.time.LocalDateTime; @@ -30,9 +31,10 @@ class GetChatRoomListResponse { @Builder class GetChatRoomResponse { private Long chatRoomId; - private String totalSummary; - private String situationKeyword; - private String solutionKeyword; + private String title; // 제목 (nullable) + private ChatRoomState chatRoomState; // 상태 + private int level; // 현재 단계 + private LocalDateTime lastMessageSentTime; private LocalDateTime createdAt; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetCurrentChatRoomUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetCurrentChatRoomUseCase.java deleted file mode 100644 index 6b064bfc..00000000 --- a/src/main/java/makeus/cmc/malmo/application/port/in/chat/GetCurrentChatRoomUseCase.java +++ /dev/null @@ -1,23 +0,0 @@ -package makeus.cmc.malmo.application.port.in.chat; - -import lombok.Builder; -import lombok.Data; -import makeus.cmc.malmo.domain.value.state.ChatRoomState; - -public interface GetCurrentChatRoomUseCase { - - GetCurrentChatRoomResponse getCurrentChatRoom(GetCurrentChatRoomCommand command); - - @Data - @Builder - class GetCurrentChatRoomCommand { - private Long userId; - } - - @Data - @Builder - class GetCurrentChatRoomResponse { - private Long chatRoomId; - private ChatRoomState chatRoomState; - } -} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/ProcessMessageUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/ProcessMessageUseCase.java index 90a72df9..e0fcf6c0 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/chat/ProcessMessageUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/chat/ProcessMessageUseCase.java @@ -8,8 +8,13 @@ public interface ProcessMessageUseCase { CompletableFuture processStreamChatMessage(ProcessMessageCommand command); - CompletableFuture processTotalSummary(ProcessTotalSummaryCommand command); CompletableFuture processAnswerMetadata(ProcessAnswerCommand command); + + // 제목 생성 처리 + CompletableFuture processTitleGeneration(ProcessTitleGenerationCommand command); + + // 4단계 대화 요약 처리 + CompletableFuture processConversationSummary(ProcessConversationSummaryCommand command); @Data @Builder @@ -23,15 +28,22 @@ class ProcessMessageCommand { @Data @Builder - class ProcessTotalSummaryCommand { + class ProcessAnswerCommand { + private Long coupleId; + private Long memberId; + private Long coupleQuestionId; + } + + @Data + @Builder + class ProcessTitleGenerationCommand { private Long chatRoomId; } @Data @Builder - class ProcessAnswerCommand { - private Long coupleId; - private Long memberId; - private Long coupleQuestionId; + class ProcessConversationSummaryCommand { + private Long chatRoomId; + private int level; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/SendChatMessageUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/SendChatMessageUseCase.java index 7f3d1fc6..61ae3f37 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/chat/SendChatMessageUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/chat/SendChatMessageUseCase.java @@ -12,6 +12,7 @@ public interface SendChatMessageUseCase { @Builder class SendChatMessageCommand { private Long userId; + private Long chatRoomId; // 명시적으로 채팅방 ID 지정 private String message; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomPort.java index 6afad951..31350f6a 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadChatRoomPort.java @@ -10,11 +10,15 @@ import java.util.Optional; public interface LoadChatRoomPort { - Optional loadCurrentChatRoomByMemberId(MemberId memberId); + // 진행 중인 채팅방 목록 조회 (복수) + List loadActiveChatRoomsByMemberId(MemberId memberId); + + // ID로 채팅방 조회 (유지) Optional loadChatRoomById(ChatRoomId chatRoomId); - Optional loadPausedChatRoomByMemberId(MemberId memberId); - - Page loadAliveChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable); - + + // 삭제되지 않은 모든 채팅방 조회 (페이지네이션) + Page loadChatRoomsByMemberId(MemberId memberId, String keyword, Pageable pageable); + + // 소유권 검증 (유지) boolean isMemberOwnerOfChatRooms(MemberId memberId, List chatRoomIds); } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadMessagesPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadMessagesPort.java index 799506f1..9f358cba 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadMessagesPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadMessagesPort.java @@ -21,6 +21,10 @@ public interface LoadMessagesPort { List loadChatRoomLevelAndDetailedLevelMessages(ChatRoomId chatRoomId, int level, int detailedLevel); + List loadRecentMessagesByLevel(ChatRoomId chatRoomId, int level, int limit); + + long countMessagesByLevel(ChatRoomId chatRoomId, int level); + @Data @AllArgsConstructor class ChatRoomMessageRepositoryDto { diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadPromptPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadPromptPort.java index d392c885..293975b4 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadPromptPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadPromptPort.java @@ -21,4 +21,6 @@ public interface LoadPromptPort { Optional loadAnswerMetadataPrompt(); Optional loadSummaryPromptByLevel(int level); + + Optional loadTitleGenerationPrompt(); } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadSummarizedMessages.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadSummarizedMessages.java index 3ea3bd0f..9a30d233 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadSummarizedMessages.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/LoadSummarizedMessages.java @@ -4,8 +4,11 @@ import makeus.cmc.malmo.domain.value.id.ChatRoomId; import java.util.List; +import java.util.Optional; public interface LoadSummarizedMessages { List loadSummarizedMessages(ChatRoomId chatRoomId); + + Optional loadLatestSummaryByLevel(ChatRoomId chatRoomId, int level); } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/chat/SaveChatMessageSummaryPort.java b/src/main/java/makeus/cmc/malmo/application/port/out/chat/SaveChatMessageSummaryPort.java index 8a8c7094..c96669b6 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/out/chat/SaveChatMessageSummaryPort.java +++ b/src/main/java/makeus/cmc/malmo/application/port/out/chat/SaveChatMessageSummaryPort.java @@ -3,7 +3,5 @@ import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; public interface SaveChatMessageSummaryPort { - - void saveChatMessageSummary(ChatMessageSummary chatMessageSummary); - + ChatMessageSummary saveChatMessageSummary(ChatMessageSummary chatMessageSummary); } diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatMessageService.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatMessageService.java index 03463fc6..f54b9fb5 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatMessageService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatMessageService.java @@ -2,25 +2,24 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.adaptor.message.RequestConversationSummaryMessage; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomCommandHelper; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.PromptQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.DetailedPromptQueryHelper; -import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataCommandHelper; import makeus.cmc.malmo.application.helper.member.MemberMemoryCommandHelper; import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; import makeus.cmc.malmo.application.helper.question.CoupleQuestionQueryHelper; import makeus.cmc.malmo.application.port.in.chat.ProcessMessageUseCase; import makeus.cmc.malmo.application.port.in.chat.SufficiencyCheckResult; -import makeus.cmc.malmo.application.port.out.sse.SendSseEventPort; -import makeus.cmc.malmo.application.port.out.chat.SaveChatMessageSummaryPort; import makeus.cmc.malmo.domain.model.chat.ChatMessage; import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.model.chat.ChatRoom; import makeus.cmc.malmo.domain.model.chat.Prompt; import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +import makeus.cmc.malmo.util.ChatTokenConstants; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.model.member.MemberMemory; import makeus.cmc.malmo.domain.model.question.CoupleQuestion; @@ -33,11 +32,14 @@ import makeus.cmc.malmo.util.ChatMessageSplitter; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import static makeus.cmc.malmo.adaptor.message.StreamMessageType.REQUEST_CONVERSATION_SUMMARY; + @Slf4j @Service @RequiredArgsConstructor @@ -54,12 +56,13 @@ public class ChatMessageService implements ProcessMessageUseCase { private final ChatRoomCommandHelper chatRoomCommandHelper; private final ChatRoomDomainService chatRoomDomainService; - private final SaveChatMessageSummaryPort saveChatMessageSummaryPort; private final CoupleQuestionQueryHelper coupleQuestionQueryHelper; private final MemberMemoryCommandHelper memberMemoryCommandHelper; + private final makeus.cmc.malmo.application.helper.outbox.OutboxHelper outboxHelper; + @Override public CompletableFuture processStreamChatMessage(ProcessMessageCommand command) { MemberId memberId = MemberId.of(command.getMemberId()); @@ -69,7 +72,12 @@ public CompletableFuture processStreamChatMessage(ProcessMessageCommand co // 1. 유저 메시지 저장 // saveUserMessage(chatRoom, command); - // 2. 충분성 조건 검사 + // 4단계 이상: 충분성 검사 없이 자유 대화 응답 + if (command.getPromptLevel() >= 4) { + return processFreeConversation(member, chatRoom, command); + } + + // 2. 충분성 조건 검사 (1~3단계) CompletableFuture sufficiencyCheck = requestSufficiencyCheck(member, chatRoom, command); @@ -95,8 +103,10 @@ public CompletableFuture processStreamChatMessage(ProcessMessageCommand co } // 마지막 충분성 조건인 경우 - // 단계 요약 요청 (비동기) - requestStageSummaryAsync(chatRoom, command); + // 1단계 종료 시 제목 생성 요청 + if (command.getPromptLevel() == 1) { + requestTitleGenerationAsync(chatRoom); + } // 다음 단계 오프닝 생성 요청 chatRoom.upgradeToNextStage(); @@ -105,27 +115,6 @@ public CompletableFuture processStreamChatMessage(ProcessMessageCommand co }); } - @Override - public CompletableFuture processTotalSummary(ProcessTotalSummaryCommand command) { - ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(ChatRoomId.of(command.getChatRoomId())); - - Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt totalSummaryPrompt = promptQueryHelper.getTotalSummaryPrompt(); - - List> messages = chatPromptBuilder.createForTotalSummary(chatRoom); - - return chatProcessor.requestTotalSummary(messages, systemPrompt, totalSummaryPrompt) - .thenAcceptAsync(summary -> { // CounselingSummary 객체를 받음 - chatRoom.updateChatRoomSummary( - summary.getTotalSummary(), - summary.getSituationKeyword(), - summary.getSolutionKeyword(), - summary.getCounselingType() - ); - chatRoomCommandHelper.saveChatRoom(chatRoom); - log.info("Successfully processed and saved total summary for chatRoomId: {}", command.getChatRoomId()); - }); - } @Override public CompletableFuture processAnswerMetadata(ProcessAnswerCommand command) { @@ -157,14 +146,41 @@ public CompletableFuture processAnswerMetadata(ProcessAnswerCommand comman }); } - private void saveUserMessage(ChatRoom chatRoom, ProcessMessageCommand command) { - ChatMessage userMessage = chatRoomDomainService.createUserMessage( - ChatRoomId.of(chatRoom.getId()), - command.getPromptLevel(), - command.getDetailedLevel(), - command.getNowMessage() - ); - chatRoomCommandHelper.saveChatMessage(userMessage); + + /** + * 4단계 이상: 자유 대화 처리 + * - 충분성 검사 없이 바로 응답 생성 + * - 단계 전환 없이 현재 level 유지 + * - 메타데이터 저장 스킵 + * - 토큰 관리를 위한 요약 트리거 포함 + */ + private CompletableFuture processFreeConversation(Member member, ChatRoom chatRoom, ProcessMessageCommand command) { + ChatRoomId chatRoomId = ChatRoomId.of(chatRoom.getId()); + int level = chatRoom.getLevel(); + + // 메시지 개수 체크 및 요약 트리거 + long messageCount = chatRoomQueryHelper.countMessagesByLevel(chatRoomId, level); + if (messageCount > ChatTokenConstants.FREE_CONVERSATION_SUMMARY_THRESHOLD + && messageCount % ChatTokenConstants.FREE_CONVERSATION_SUMMARY_INTERVAL == 0) { + // 비동기로 요약 생성 요청 (20개 단위로) + requestConversationSummaryAsync(chatRoom); + } + + // createForFreeConversation 사용 (최근 20개 + 요약 포함) + List> messages = chatPromptBuilder.createForFreeConversation( + member, chatRoom, command.getNowMessage()); + + Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); + Prompt prompt = promptQueryHelper.getGuidelinePromptWithFallback(command.getPromptLevel()); + DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( + command.getPromptLevel(), command.getDetailedLevel()); + + return chatProcessor.streamChat(messages, systemPrompt, prompt, detailedPrompt, + chunk -> chatSseSender.sendResponseChunk(MemberId.of(member.getId()), chunk), + fullAnswer -> saveAiMessage(MemberId.of(member.getId()), ChatRoomId.of(chatRoom.getId()), + command.getPromptLevel(), command.getDetailedLevel(), fullAnswer), + errorMessage -> chatSseSender.sendError(MemberId.of(member.getId()), errorMessage) + ).toFuture(); } private CompletableFuture requestSufficiencyCheck(Member member, ChatRoom chatRoom, ProcessMessageCommand command) { @@ -190,10 +206,9 @@ private CompletableFuture requestResponseToMeetCondition(Member member, Ch } Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt prompt = promptQueryHelper.getGuidelinePrompt(command.getPromptLevel()); - DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePrompt( - command.getPromptLevel(), command.getDetailedLevel()) - .orElseThrow(() -> new RuntimeException("Guideline prompt not found")); + Prompt prompt = promptQueryHelper.getGuidelinePromptWithFallback(command.getPromptLevel()); + DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( + command.getPromptLevel(), command.getDetailedLevel()); return chatProcessor.streamChat(messages, systemPrompt, prompt, detailedPrompt, chunk -> chatSseSender.sendResponseChunk(MemberId.of(member.getId()), chunk), @@ -229,10 +244,9 @@ private CompletableFuture requestNextDetailedPromptOpening(ChatRoom chatRo // 시스템 프롬프트 + 현재 단계 프롬프트 + 다음 충분성 조건 프롬프트 Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt prompt = promptQueryHelper.getGuidelinePrompt(chatRoom.getLevel()); - DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePrompt( - command.getPromptLevel(), command.getDetailedLevel() + 1) - .orElseThrow(() -> new RuntimeException("Next guideline prompt not found")); + Prompt prompt = promptQueryHelper.getGuidelinePromptWithFallback(chatRoom.getLevel()); + DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback( + command.getPromptLevel(), command.getDetailedLevel() + 1); return chatProcessor.streamChat(messages, systemPrompt, prompt, nextDetailedPrompt, chunk -> chatSseSender.sendResponseChunk(memberId, chunk), @@ -242,52 +256,117 @@ private CompletableFuture requestNextDetailedPromptOpening(ChatRoom chatRo ).toFuture(); } - private void requestStageSummaryAsync(ChatRoom chatRoom, ProcessMessageCommand command) { - List> messages = chatPromptBuilder.createForStageSummary( - chatRoom, command.getPromptLevel()); + private CompletableFuture requestNextStageOpening(Member member, ChatRoom chatRoom, ProcessMessageCommand command) { + int nextLevel = command.getPromptLevel() + 1; + + // 단계별 요약 없이 컨텍스트 구성 + List> messages = chatPromptBuilder.createForNextStage(member, chatRoom, nextLevel); Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt prompt = promptQueryHelper.getGuidelinePrompt(command.getPromptLevel()); - Prompt summaryPrompt = promptQueryHelper.getSummaryPrompt(command.getPromptLevel()); - chatProcessor.requestStageSummary(messages, systemPrompt, prompt, summaryPrompt) - .thenAcceptAsync(summary -> { - ChatMessageSummary chatMessageSummary = ChatMessageSummary.createChatMessageSummary( - ChatRoomId.of(chatRoom.getId()), summary, command.getPromptLevel()); - saveChatMessageSummaryPort.saveChatMessageSummary(chatMessageSummary); - log.info("Stage summary completed for chatRoomId: {}, level: {}", - chatRoom.getId(), command.getPromptLevel()); - }); - } - - private CompletableFuture requestNextStageOpening(Member member, ChatRoom chatRoom, ProcessMessageCommand command) { - List> messages = chatPromptBuilder.createForNextStage( - member, chatRoom, command.getPromptLevel() + 1); + // 4단계 이상에서는 3단계 프롬프트 재사용 + Prompt nextPrompt = promptQueryHelper.getGuidelinePromptWithFallback(nextLevel); - Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt nextPrompt = promptQueryHelper.getGuidelinePrompt(command.getPromptLevel() + 1); - - if (nextPrompt.isForCompletedResponse()) { - String finalMessage = nextPrompt.getContent(); - saveAiMessage(MemberId.of(member.getId()), ChatRoomId.of(chatRoom.getId()), - command.getPromptLevel(), command.getDetailedLevel(), finalMessage); - chatSseSender.sendLastResponse(chatRoom.getMemberId(), finalMessage); - - return CompletableFuture.completedFuture(null); - } - - DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePrompt( - command.getPromptLevel() + 1, 1) - .orElseThrow(() -> new RuntimeException("Next stage guideline prompt not found")); + // DetailedPrompt도 fallback 로직 적용 + DetailedPrompt nextDetailedPrompt = detailedPromptQueryHelper.getGuidelinePromptWithFallback(nextLevel, 1); return chatProcessor.streamChat(messages, systemPrompt, nextPrompt, nextDetailedPrompt, chunk -> chatSseSender.sendResponseChunk(MemberId.of(member.getId()), chunk), fullAnswer -> saveAiMessage(MemberId.of(member.getId()), ChatRoomId.of(chatRoom.getId()), - command.getPromptLevel() + 1, 1, fullAnswer), + nextLevel, 1, fullAnswer), errorMessage -> chatSseSender.sendError(MemberId.of(member.getId()), errorMessage) ).toFuture(); } + /** + * 비동기 제목 생성 요청 + * Redis Stream을 통해 제목 생성 워커에 전달 + */ + private void requestTitleGenerationAsync(ChatRoom chatRoom) { + outboxHelper.publish( + makeus.cmc.malmo.adaptor.message.StreamMessageType.REQUEST_TITLE_GENERATION, + new makeus.cmc.malmo.adaptor.message.RequestTitleGenerationMessage(chatRoom.getId()) + ); + log.info("Title generation requested for chatRoomId: {}", chatRoom.getId()); + } + + @Override + public CompletableFuture processTitleGeneration(ProcessTitleGenerationCommand command) { + ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(ChatRoomId.of(command.getChatRoomId())); + + // 1단계 대화 내용 조회 + List> messages = chatPromptBuilder.createForTitleGeneration(chatRoom); + + // 제목 생성 프롬프트 조회 + Prompt titlePrompt = promptQueryHelper.getTitleGenerationPrompt(); + + return chatProcessor.requestTitleGeneration(messages, titlePrompt) + .thenAcceptAsync(title -> { + chatRoom.updateTitle(title); + chatRoomCommandHelper.saveChatRoom(chatRoom); + log.info("Title generated for chatRoomId: {}, title: {}", command.getChatRoomId(), title); + }); + } + + @Override + public CompletableFuture processConversationSummary(ProcessConversationSummaryCommand command) { + ChatRoomId chatRoomId = ChatRoomId.of(command.getChatRoomId()); + int level = command.getLevel(); + + // 최신 요약 이후의 메시지들을 가져와서 요약 + // 최근 요약이 있다면 그 이후의 메시지, 없다면 전체 메시지 중 최근 요약 주기만큼 + List messagesToSummarize = chatRoomQueryHelper.getRecentMessages( + chatRoomId, level, ChatTokenConstants.FREE_CONVERSATION_SUMMARY_INTERVAL); + + if (messagesToSummarize.isEmpty()) { + log.debug("No messages to summarize for chatRoomId: {}, level: {}", chatRoomId.getValue(), level); + return CompletableFuture.completedFuture(null); + } + + // 요약할 메시지들을 프롬프트 형식으로 변환 + List> summaryMessages = new ArrayList<>(); + for (ChatMessage chatMessage : messagesToSummarize) { + summaryMessages.add(Map.of( + "role", chatMessage.getSenderType().getApiName(), + "content", chatMessage.getContent() + )); + } + + // 요약 프롬프트 조회 (4단계 요약 프롬프트 사용) + Prompt summaryPrompt = promptQueryHelper.getSummaryPromptByLevel(level) + .orElseGet(() -> { + log.warn("Summary prompt not found for level: {}, using default", level); + return promptQueryHelper.getSummaryPromptByLevel(3) + .orElseThrow(() -> new RuntimeException("Summary prompt not found")); + }); + + // 비동기 요약 생성 및 저장 + return chatProcessor.requestConversationSummary(summaryMessages, summaryPrompt) + .thenAcceptAsync(summaryContent -> { + ChatMessageSummary chatMessageSummary = ChatMessageSummary.createChatMessageSummary( + chatRoomId, summaryContent, level); + chatRoomCommandHelper.saveChatMessageSummary(chatMessageSummary); + log.info("Conversation summary saved for chatRoomId: {}, level: {}", chatRoomId.getValue(), level); + }) + .exceptionally(throwable -> { + log.error("Failed to generate conversation summary for chatRoomId: {}, level: {}", + chatRoomId.getValue(), level, throwable); + return null; + }); + } + + /** + * 비동기 4단계 대화 요약 생성 요청 + * Redis Stream을 통해 요약 생성 워커에 전달 + */ + private void requestConversationSummaryAsync(ChatRoom chatRoom) { + outboxHelper.publish( + REQUEST_CONVERSATION_SUMMARY, + new RequestConversationSummaryMessage(chatRoom.getId(), chatRoom.getLevel()) + ); + log.info("Conversation summary requested for chatRoomId: {}, level: {}", chatRoom.getId(), chatRoom.getLevel()); + } + private void saveAiMessage(MemberId memberId, ChatRoomId chatRoomId, int level, int detailedLevel, String fullAnswer) { // fullAnswer를 문장 단위로 분할하고 세 문장씩 그룹화 List groupedTexts = ChatMessageSplitter.splitIntoGroups(fullAnswer); diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java index 735e3700..5305c95a 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatProcessor.java @@ -2,9 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; @@ -15,6 +12,7 @@ import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -65,26 +63,6 @@ public CompletableFuture requestSummaryAsync(List> m return requestChatApiPort.requestResponse(messages); } - public CompletableFuture requestTotalSummary(List> messages, - Prompt systemPrompt, - Prompt totalSummaryPrompt) { - messages.add(createMessageMap(SenderType.SYSTEM, systemPrompt.getContent())); - messages.add(createMessageMap(SenderType.SYSTEM, "[현재 단계 지시] " + totalSummaryPrompt.getContent())); - - // 비동기 API 호출 후 CompletableFuture 형태로 응답 - return requestChatApiPort.requestJsonResponse(messages) - // 응답(JSON 문자열)이 오면, thenApply를 통해 다음 작업을 연결 - .thenApply(summaryJson -> { - try { - // JSON 문자열을 CounselingSummary 객체로 파싱 - return objectMapper.readValue(summaryJson, CounselingSummary.class); - } catch (JsonProcessingException e) { - log.error("Failed to parse summary JSON: {}", summaryJson, e); - // 예외 발생 시, 런타임 예외로 감싸서 CompletableFuture가 예외를 인지 - throw new RuntimeException("Failed to parse summary JSON", e); - } - }); - } public CompletableFuture requestMetaData(String question, String memberAnswer, @@ -122,16 +100,37 @@ public CompletableFuture requestDetailedSummary(List return requestChatApiPort.requestResponse(messages); } - public CompletableFuture requestStageSummary(List> messages, - Prompt systemPrompt, - Prompt prompt, - Prompt summaryPrompt) { - messages.add(createMessageMap(SenderType.SYSTEM, systemPrompt.getContent())); - messages.add(createMessageMap(SenderType.SYSTEM, prompt.getContent())); + /** + * 4단계 자유 대화 요약 생성 + * @param messages 요약할 메시지 목록 + * @param summaryPrompt 요약 프롬프트 + * @return 생성된 요약 문자열 + */ + public CompletableFuture requestConversationSummary(List> messages, + Prompt summaryPrompt) { messages.add(createMessageMap(SenderType.SYSTEM, summaryPrompt.getContent())); return requestChatApiPort.requestResponse(messages); } + /** + * 제목 생성 요청 + * @return 생성된 제목 문자열 + */ + public CompletableFuture requestTitleGeneration(List> messages, Prompt titlePrompt) { + // OpenAI API 호출하여 제목 생성 + // 짧은 제목 (20자 이내) 생성하도록 프롬프트 구성 + + List> promptMessages = new ArrayList<>(messages); + promptMessages.add(createMessageMap(SenderType.SYSTEM, titlePrompt.getContent())); + + return requestChatApiPort.requestResponse(promptMessages) + .thenApply(title -> { + // 제목 길이 제한 (최대 50자) + String trimmedTitle = title.trim(); + return trimmedTitle.length() > 50 ? trimmedTitle.substring(0, 50) : trimmedTitle; + }); + } + private Map createMessageMap(SenderType senderType, String content) { return Map.of( "role", senderType.getApiName(), @@ -139,13 +138,4 @@ private Map createMessageMap(SenderType senderType, String conte ); } - @Data - @NoArgsConstructor - @AllArgsConstructor - public static class CounselingSummary { - private String totalSummary; - private String situationKeyword; - private String solutionKeyword; - private String counselingType; - } } diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilder.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilder.java index f1471cdb..0d7da67e 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilder.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatPromptBuilder.java @@ -5,12 +5,11 @@ import makeus.cmc.malmo.application.helper.chat_room.MemberChatRoomMetadataQueryHelper; import makeus.cmc.malmo.application.port.out.chat.LoadChatRoomMetadataPort; import makeus.cmc.malmo.domain.model.chat.ChatMessage; -import makeus.cmc.malmo.domain.model.chat.ChatMessageSummary; import makeus.cmc.malmo.domain.model.chat.ChatRoom; import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +import makeus.cmc.malmo.util.ChatTokenConstants; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.model.member.MemberMemory; -import makeus.cmc.malmo.domain.service.MemberDomainService; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import makeus.cmc.malmo.domain.value.id.MemberId; import makeus.cmc.malmo.domain.value.type.SenderType; @@ -24,7 +23,6 @@ @RequiredArgsConstructor public class ChatPromptBuilder { - private final MemberDomainService memberDomainService; private final ChatRoomQueryHelper chatRoomQueryHelper; private final MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; @@ -36,11 +34,11 @@ public List> createForProcessUserMessage(Member member, Chat String metaDataContent = getMetaDataContent(member); messages.add(createMessageMap(SenderType.USER, metaDataContent)); - // 2. 이전 단계 요약본 - List previousLevelsSummarizedMessages = chatRoomQueryHelper.getSummarizedMessages(ChatRoomId.of(chatRoom.getId())); - if (!previousLevelsSummarizedMessages.isEmpty()) { - String summarizedMessageContent = getSummarizedMessageContent(previousLevelsSummarizedMessages); - messages.add(createMessageMap(SenderType.SYSTEM, summarizedMessageContent)); + // 2. MemberChatRoomMetadata 정보 (단계별 요약 대신) + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); } // 3. 현재 단계 메시지들 @@ -63,53 +61,7 @@ public List> createForProcessUserMessage(Member member, Chat return messages; } - public List> createForSummaryAsync(ChatRoom chatRoom) { - List> messages = new ArrayList<>(); - int chatRoomLevel = chatRoom.getLevel(); - // 현재 단계 메시지들 - List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelMessages(ChatRoomId.of(chatRoom.getId()), chatRoomLevel); - for (ChatMessage chatMessage : currentChatRoomMessages) { - messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); - } - return messages; - } - - public List> createForTotalSummary(ChatRoom chatRoom) { - List> messages = new ArrayList<>(); - List summarizedMessages = chatRoomQueryHelper.getSummarizedMessages(ChatRoomId.of(chatRoom.getId())); - - if (summarizedMessages.isEmpty()) { - List lastLevelMessages = chatRoomQueryHelper.getChatRoomLevelMessages(ChatRoomId.of(chatRoom.getId()), chatRoom.getLevel()); - for (ChatMessage lastLevelMessage : lastLevelMessages) { - messages.add( - createMessageMap(lastLevelMessage.getSenderType(), lastLevelMessage.getContent()) - ); - } - } else { - StringBuilder sb = new StringBuilder(); - for (ChatMessageSummary summary : summarizedMessages) { - sb.append("[").append(summary.getLevel()).append(" 단계 요약] \n"); - sb.append(summary.getContent()).append("\n"); - } - messages.add( - createMessageMap(SenderType.SYSTEM, sb.toString()) - ); - } - return messages; - } - - private String getSummarizedMessageContent(List summarizedMessages) { - if (summarizedMessages == null || summarizedMessages.isEmpty()) { - return ""; - } - StringBuilder sb = new StringBuilder(); - sb.append("[이전 단계 요약] \n"); - for (ChatMessageSummary summary : summarizedMessages) { - sb.append("- ").append(summary.getContent()).append("\n"); - } - return sb.toString(); - } private Map createMessageMap(SenderType senderType, String content) { return Map.of( @@ -157,21 +109,14 @@ public List> createForSufficiencyCheck(Member member, ChatRo String metaDataContent = getMetaDataContent(member); messages.add(createMessageMap(SenderType.USER, metaDataContent)); - // 2. 이전 단계 요약본 - List previousLevelsSummarizedMessages = chatRoomQueryHelper.getSummarizedMessages(ChatRoomId.of(chatRoom.getId())); - if (!previousLevelsSummarizedMessages.isEmpty()) { - String summarizedMessageContent = getSummarizedMessageContent(previousLevelsSummarizedMessages); - messages.add(createMessageMap(SenderType.SYSTEM, summarizedMessageContent)); - } - - // 3. MemberChatRoomMetadata 정보 + // 2. MemberChatRoomMetadata 정보 (단계별 요약 대신) List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); if (!metadataList.isEmpty()) { String metadataContent = getMemberChatRoomMetadataContent(metadataList); messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); } - // 4. 현재 단계 메시지들 + // 3. 현재 단계 메시지들 List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelAndDetailedLevelMessages(ChatRoomId.of(chatRoom.getId()), level, detailedLevel); for (ChatMessage chatMessage : currentChatRoomMessages) { messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); @@ -180,17 +125,6 @@ public List> createForSufficiencyCheck(Member member, ChatRo return messages; } - public List> createForStageSummary(ChatRoom chatRoom, int level) { - List> messages = new ArrayList<>(); - - // 현재 단계 메시지들 - List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelMessages(ChatRoomId.of(chatRoom.getId()), level); - for (ChatMessage chatMessage : currentChatRoomMessages) { - messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); - } - - return messages; - } public List> createForNextDetailedPrompt(Member member, ChatRoom chatRoom, int level, int nextDetailedLevel) { List> messages = new ArrayList<>(); @@ -199,21 +133,14 @@ public List> createForNextDetailedPrompt(Member member, Chat String metaDataContent = getMetaDataContent(member); messages.add(createMessageMap(SenderType.USER, metaDataContent)); - // 2. 이전 단계 요약본 - List previousLevelsSummarizedMessages = chatRoomQueryHelper.getSummarizedMessages(ChatRoomId.of(chatRoom.getId())); - if (!previousLevelsSummarizedMessages.isEmpty()) { - String summarizedMessageContent = getSummarizedMessageContent(previousLevelsSummarizedMessages); - messages.add(createMessageMap(SenderType.SYSTEM, summarizedMessageContent)); - } - - // 3. MemberChatRoomMetadata 정보 + // 2. MemberChatRoomMetadata 정보 List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); if (!metadataList.isEmpty()) { String metadataContent = getMemberChatRoomMetadataContent(metadataList); messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); } - // 4. 현재 단계 메시지들 (이전 detailedLevel까지) + // 3. 현재 단계 메시지들 (이전 detailedLevel까지) // List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelAndDetailedLevelMessages(ChatRoomId.of(chatRoom.getId()), level, nextDetailedLevel - 1); // fixed: 현재 단계 메시지들 context 전체 전달(level 기준) List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelMessages(ChatRoomId.of(chatRoom.getId()), level); @@ -231,14 +158,7 @@ public List> createForNextStage(Member member, ChatRoom chat String metaDataContent = getMetaDataContent(member); messages.add(createMessageMap(SenderType.USER, metaDataContent)); - // 2. 이전 단계 요약본 - List previousLevelsSummarizedMessages = chatRoomQueryHelper.getSummarizedMessages(ChatRoomId.of(chatRoom.getId())); - if (!previousLevelsSummarizedMessages.isEmpty()) { - String summarizedMessageContent = getSummarizedMessageContent(previousLevelsSummarizedMessages); - messages.add(createMessageMap(SenderType.SYSTEM, summarizedMessageContent)); - } - - // 3. MemberChatRoomMetadata 정보 + // 2. MemberChatRoomMetadata 정보 List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); if (!metadataList.isEmpty()) { String metadataContent = getMemberChatRoomMetadataContent(metadataList); @@ -259,4 +179,63 @@ private String getMemberChatRoomMetadataContent(List met } return sb.toString(); } + + /** + * 제목 생성을 위한 메시지 구성 + * 1단계 대화 내용만 포함 + */ + public List> createForTitleGeneration(ChatRoom chatRoom) { + List> messages = new ArrayList<>(); + + // 1단계 메시지들만 조회 + List stage1Messages = chatRoomQueryHelper.getChatRoomLevelMessages( + ChatRoomId.of(chatRoom.getId()), 1); + + for (ChatMessage chatMessage : stage1Messages) { + messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); + } + + return messages; + } + + /** + * 4단계 자유 대화를 위한 메시지 구성 + * - 최근 20개 메시지만 로드하여 토큰 관리 + * - 이전 대화 요약 포함 + */ + public List> createForFreeConversation(Member member, ChatRoom chatRoom, String userMessage) { + List> messages = new ArrayList<>(); + int chatRoomLevel = chatRoom.getLevel(); + ChatRoomId chatRoomId = ChatRoomId.of(chatRoom.getId()); + + // 1. 사용자 메타데이터 + String metaDataContent = getMetaDataContent(member); + messages.add(createMessageMap(SenderType.USER, metaDataContent)); + + // 2. 이전 단계 요약 (MemberChatRoomMetadata) + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(chatRoomId); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); + } + + // 3. 4단계 대화 요약 (ChatMessageSummary) - 있는 경우 + chatRoomQueryHelper.getLatestSummaryByLevel(chatRoomId, chatRoomLevel) + .ifPresent(summary -> { + String summaryContent = "[이전 대화 요약]\n" + summary.getContent(); + messages.add(createMessageMap(SenderType.SYSTEM, summaryContent)); + }); + + // 4. 최근 N개 메시지만 로드 (토큰 관리) + List recentMessages = chatRoomQueryHelper.getRecentMessages( + chatRoomId, chatRoomLevel, ChatTokenConstants.FREE_CONVERSATION_RECENT_MESSAGE_LIMIT); + for (ChatMessage chatMessage : recentMessages) { + messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); + } + + // 5. 현재 사용자 메시지 추가 + messages.add(createMessageMap(SenderType.USER, userMessage)); + + return messages; + } } diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomManagementService.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomManagementService.java new file mode 100644 index 00000000..c7015930 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomManagementService.java @@ -0,0 +1,58 @@ +package makeus.cmc.malmo.application.service.chat; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.adaptor.in.aop.CheckValidMember; +import makeus.cmc.malmo.application.helper.chat_room.ChatRoomCommandHelper; +import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; +import makeus.cmc.malmo.application.port.in.chat.CreateChatRoomUseCase; +import makeus.cmc.malmo.domain.model.chat.ChatMessage; +import makeus.cmc.malmo.domain.model.chat.ChatRoom; +import makeus.cmc.malmo.domain.model.member.Member; +import makeus.cmc.malmo.domain.service.ChatRoomDomainService; +import makeus.cmc.malmo.domain.value.id.ChatRoomId; +import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.util.JosaUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHATROOM_LEVEL; +import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHAT_MESSAGE; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatRoomManagementService implements CreateChatRoomUseCase { + + private final ChatRoomDomainService chatRoomDomainService; + private final MemberQueryHelper memberQueryHelper; + private final ChatRoomCommandHelper chatRoomCommandHelper; + + @Override + @Transactional + @CheckValidMember + public CreateChatRoomResponse createChatRoom(CreateChatRoomCommand command) { + MemberId memberId = MemberId.of(command.getUserId()); + Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); + + // 채팅방 생성 (즉시 ALIVE 상태) + ChatRoom chatRoom = chatRoomDomainService.createChatRoom(memberId); + ChatRoom savedChatRoom = chatRoomCommandHelper.saveChatRoom(chatRoom); + + // 초기 AI 메시지 생성 및 저장 + ChatMessage initMessage = chatRoomDomainService.createAiMessage( + ChatRoomId.of(savedChatRoom.getId()), + INIT_CHATROOM_LEVEL, + 1, + JosaUtils.아야(member.getNickname()) + INIT_CHAT_MESSAGE); + chatRoomCommandHelper.saveChatMessage(initMessage); + + log.info("새 채팅방 생성: chatRoomId={}, memberId={}", savedChatRoom.getId(), memberId.getValue()); + + return CreateChatRoomResponse.builder() + .chatRoomId(savedChatRoom.getId()) + .chatRoomState(savedChatRoom.getChatRoomState()) + .createdAt(savedChatRoom.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomService.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomService.java index cb2afddb..c62fdcf9 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatRoomService.java @@ -54,17 +54,18 @@ public GetChatRoomSummaryResponse getChatRoomSummary(GetChatRoomSummaryCommand c @Override @CheckValidMember public GetChatRoomListResponse getChatRoomList(GetChatRoomListCommand command) { - Page chatRoomList = chatRoomQueryHelper.getCompletedChatRoomsByMemberId( + Page chatRoomList = chatRoomQueryHelper.getChatRoomsByMemberId( MemberId.of(command.getUserId()), command.getKeyword(), command.getPageable() ); List response = chatRoomList.getContent().stream() .map(chatRoom -> GetChatRoomResponse.builder() .chatRoomId(chatRoom.getId()) - .totalSummary(chatRoom.getTotalSummary()) - .situationKeyword(chatRoom.getSituationKeyword()) - .solutionKeyword(chatRoom.getSolutionKeyword()) - .createdAt(chatRoom.getLastMessageSentTime()) + .title(chatRoom.getTitle()) // 제목 (null일 수 있음) + .chatRoomState(chatRoom.getChatRoomState()) // 상태 포함 + .level(chatRoom.getLevel()) // 현재 단계 + .lastMessageSentTime(chatRoom.getLastMessageSentTime()) + .createdAt(chatRoom.getCreatedAt()) .build()) .toList(); diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatService.java b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatService.java index b7d73040..02bb8b7a 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/ChatService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/ChatService.java @@ -7,24 +7,20 @@ import makeus.cmc.malmo.adaptor.message.StreamMessageType; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomCommandHelper; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; -import makeus.cmc.malmo.application.helper.chat_room.PromptQueryHelper; import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; import makeus.cmc.malmo.application.helper.outbox.OutboxHelper; import makeus.cmc.malmo.application.port.in.chat.SendChatMessageUseCase; import makeus.cmc.malmo.domain.model.chat.ChatMessage; import makeus.cmc.malmo.domain.model.chat.ChatRoom; -import makeus.cmc.malmo.domain.model.chat.Prompt; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.service.ChatRoomDomainService; import makeus.cmc.malmo.domain.value.id.ChatRoomId; import makeus.cmc.malmo.domain.value.id.MemberId; -import makeus.cmc.malmo.domain.value.state.ChatRoomState; import makeus.cmc.malmo.util.ChatMessageSplitter; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; //import static makeus.cmc.malmo.util.GlobalConstants.FINAL_MESSAGE; @@ -40,7 +36,6 @@ public class ChatService implements SendChatMessageUseCase { private final MemberQueryHelper memberQueryHelper; private final ChatRoomQueryHelper chatRoomQueryHelper; private final ChatRoomCommandHelper chatRoomCommandHelper; - private final PromptQueryHelper promptQueryHelper; private final OutboxHelper outboxHelper; @@ -48,24 +43,15 @@ public class ChatService implements SendChatMessageUseCase { @Transactional @CheckValidMember public SendChatMessageResponse processUserMessage(SendChatMessageCommand command) { - // 활성화된 채팅방이 있는지 확인 MemberId memberId = MemberId.of(command.getUserId()); - chatRoomQueryHelper.validateChatRoomAlive(memberId); - + ChatRoomId chatRoomId = ChatRoomId.of(command.getChatRoomId()); + + // 명시적 채팅방 ID로 조회 및 소유권 검증 + chatRoomQueryHelper.validateChatRoomOwnership(memberId, chatRoomId); + chatRoomQueryHelper.validateChatRoomActive(chatRoomId); + Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); - ChatRoom chatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(memberId); - - // 채팅방의 상담 단계가 마지막인 경우 동일한 메시지를 반복하여 전송 - Prompt prompt = promptQueryHelper.getGuidelinePrompt(chatRoom.getLevel()); - if (prompt.isForCompletedResponse()) { - String finalMessage = prompt.getContent(); - return handleLastPrompt(chatRoom, command.getMessage(), finalMessage); - } - - // 채팅방이 초기화되지 않은 상태인 경우 초기화 - if (chatRoom.getChatRoomState() == ChatRoomState.BEFORE_INIT) { - chatRoom.updateChatRoomStateAlive(); - } + ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(chatRoomId); // 현재 유저 메시지를 저장 ChatMessage savedUserMessage = saveUserMessage(chatRoom, command.getMessage()); @@ -91,20 +77,6 @@ public SendChatMessageResponse processUserMessage(SendChatMessageCommand command .build(); } - // upgradeChatRoom 메서드 제거 - 내부 로직으로 통합됨 - - private SendChatMessageResponse handleLastPrompt(ChatRoom chatRoom, String userMessage, String finalMessage) { - // 마지막 단계에서 고정된 메시지를 반복하여 전송 - ChatMessage savedUserMessage = saveUserMessage(chatRoom, userMessage); - - chatSseSender.sendLastResponse(chatRoom.getMemberId(), finalMessage); - saveAiMessage(chatRoom.getMemberId(), ChatRoomId.of(chatRoom.getId()), - chatRoom.getLevel(), chatRoom.getDetailedLevel(), finalMessage); - return SendChatMessageResponse.builder() - .messageId(savedUserMessage.getId()) - .build(); - } - private ChatMessage saveUserMessage(ChatRoom chatRoom, String message) { ChatMessage userMessage = chatRoomDomainService.createUserMessage( ChatRoomId.of(chatRoom.getId()), diff --git a/src/main/java/makeus/cmc/malmo/application/service/chat/CurrentChatRoomService.java b/src/main/java/makeus/cmc/malmo/application/service/chat/CurrentChatRoomService.java deleted file mode 100644 index d4303c2e..00000000 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/CurrentChatRoomService.java +++ /dev/null @@ -1,149 +0,0 @@ -package makeus.cmc.malmo.application.service.chat; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import makeus.cmc.malmo.adaptor.in.aop.CheckValidMember; -import makeus.cmc.malmo.adaptor.message.RequestTotalSummaryMessage; -import makeus.cmc.malmo.adaptor.message.StreamMessageType; -import makeus.cmc.malmo.application.helper.chat_room.ChatRoomCommandHelper; -import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; -import makeus.cmc.malmo.application.helper.member.MemberQueryHelper; -import makeus.cmc.malmo.application.helper.outbox.OutboxHelper; -import makeus.cmc.malmo.application.port.in.chat.CompleteChatRoomUseCase; -import makeus.cmc.malmo.application.port.in.chat.GetCurrentChatRoomMessagesUseCase; -import makeus.cmc.malmo.application.port.in.chat.GetCurrentChatRoomUseCase; -import makeus.cmc.malmo.application.port.out.chat.LoadMessagesPort; -import makeus.cmc.malmo.domain.model.chat.ChatMessage; -import makeus.cmc.malmo.domain.model.chat.ChatRoom; -import makeus.cmc.malmo.domain.model.member.Member; -import makeus.cmc.malmo.domain.service.ChatRoomDomainService; -import makeus.cmc.malmo.domain.value.id.ChatRoomId; -import makeus.cmc.malmo.domain.value.id.MemberId; -import makeus.cmc.malmo.util.ChatMessageSplitter; -import makeus.cmc.malmo.util.JosaUtils; -import org.springframework.data.domain.Page; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.List; -import java.util.stream.Collectors; - -import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHATROOM_LEVEL; -import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHAT_MESSAGE; - -@Slf4j -@Service -@RequiredArgsConstructor -public class CurrentChatRoomService - implements GetCurrentChatRoomUseCase, GetCurrentChatRoomMessagesUseCase, CompleteChatRoomUseCase { - - private final ChatRoomDomainService chatRoomDomainService; - private final ChatRoomQueryHelper chatRoomQueryHelper; - private final MemberQueryHelper memberQueryHelper; - private final ChatRoomCommandHelper chatRoomCommandHelper; - - private final OutboxHelper outboxHelper; - - @Override - @Transactional - @CheckValidMember - public GetCurrentChatRoomResponse getCurrentChatRoom(GetCurrentChatRoomCommand command) { - // 현재 채팅방 가져오기 - ChatRoom currentChatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberId(MemberId.of(command.getUserId())) - .map(chatRoom -> { - if (chatRoom.isStarted() && chatRoomDomainService.isChatRoomExpired(chatRoom.getLastMessageSentTime())) { - // 마지막 채팅 이후 하루가 지난 경우 채팅방 종료 처리 - chatRoom.expire(); - ChatRoom savedChatRoom = chatRoomCommandHelper.saveChatRoom(chatRoom); - outboxHelper.publish( - StreamMessageType.REQUEST_TOTAL_SUMMARY, - new RequestTotalSummaryMessage(savedChatRoom.getId()) - ); - - return createAndSaveNewChatRoom(MemberId.of(command.getUserId())); - } - - return chatRoom; - }) - .orElseGet(() -> { - // 현재 채팅방이 없으면 새로 생성 - return createAndSaveNewChatRoom(MemberId.of(command.getUserId())); - }); - - return GetCurrentChatRoomResponse.builder() - .chatRoomId(currentChatRoom.getId()) - .chatRoomState(currentChatRoom.getChatRoomState()) - .build(); - } - - private ChatRoom createAndSaveNewChatRoom(MemberId memberId) { - // 새로운 채팅방 생성 - Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); - ChatRoom chatRoom = chatRoomDomainService.createChatRoom(memberId); - ChatRoom savedChatRoom = chatRoomCommandHelper.saveChatRoom(chatRoom); - - // 초기 메시지 생성 및 저장 - String initMessageContent = JosaUtils.아야(member.getNickname()) + INIT_CHAT_MESSAGE; - List groupedTexts = ChatMessageSplitter.splitIntoGroups(initMessageContent); - List chatMessages = groupedTexts.stream() - .map(groupText -> chatRoomDomainService.createAiMessage( - ChatRoomId.of(savedChatRoom.getId()), - INIT_CHATROOM_LEVEL, - 1, - groupText)) - .collect(Collectors.toList()); - - chatRoomCommandHelper.saveChatMessages(chatMessages); - - return savedChatRoom; - } - - @Override - @CheckValidMember - public GetCurrentChatRoomMessagesResponse getCurrentChatRoomMessages(GetCurrentChatRoomMessagesCommand command) { - // 현재 채팅방 가져오기 - MemberId memberId = MemberId.of(command.getUserId()); - ChatRoom currentChatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(memberId); - - Page result = - chatRoomQueryHelper.getChatMessagesDtoDesc(ChatRoomId.of(currentChatRoom.getId()), memberId, command.getPageable()); - - List list = result.stream().map(cm -> - ChatRoomMessageDto.builder() - .messageId(cm.getMessageId()) - .senderType(cm.getSenderType()) - .content(cm.getContent()) - .createdAt(cm.getCreatedAt()) - .bookmarkId(cm.getBookmarkId()) - .build()) - .toList(); - - return GetCurrentChatRoomMessagesResponse.builder() - .messages(list) - .totalCount(result.getTotalElements()) - .build(); - } - - @Override - @Transactional - @CheckValidMember - public CompleteChatRoomResponse completeChatRoom(CompleteChatRoomCommand command) { - ChatRoom chatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(MemberId.of(command.getUserId())); - chatRoom.completeByUser(); - chatRoomCommandHelper.saveChatRoom(chatRoom); - - // 완료된 채팅방의 요약을 요청 - log.info("채팅방 요약 요청 스트림 추가: chatRoomId={}", chatRoom.getId()); - outboxHelper.publish( - StreamMessageType.REQUEST_TOTAL_SUMMARY, - new RequestTotalSummaryMessage(chatRoom.getId()) - ); - - // 사용자에게는 즉시 성공 응답 반환 - return CompleteChatRoomResponse.builder() - .chatRoomId(chatRoom.getId()) - .build(); - } - -} - diff --git a/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatRoom.java b/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatRoom.java index e5c29bdf..3873e97b 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatRoom.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatRoom.java @@ -11,8 +11,6 @@ import java.util.Objects; import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHATROOM_LEVEL; -import static makeus.cmc.malmo.util.GlobalConstants.COMPLETED_ROOM_CREATING_SUMMARY_LINE; -import static makeus.cmc.malmo.util.GlobalConstants.EXPIRED_ROOM_CREATING_SUMMARY_LINE; @Getter @Builder(access = AccessLevel.PRIVATE) @@ -23,6 +21,9 @@ public class ChatRoom { private int level; private int detailedLevel; private LocalDateTime lastMessageSentTime; + private String title; // 채팅방 제목 (1단계 종료 후 생성) + + // 유지: 기존 COMPLETED 채팅방 보고서 조회용 필드들 private String totalSummary; private String situationKeyword; private String solutionKeyword; @@ -39,13 +40,21 @@ public static ChatRoom createChatRoom(MemberId memberId) { .memberId(memberId) .level(INIT_CHATROOM_LEVEL) .detailedLevel(1) - .chatRoomState(ChatRoomState.BEFORE_INIT) + .chatRoomState(ChatRoomState.ALIVE) .lastMessageSentTime(LocalDateTime.now()) + .title(null) // 제목은 1단계 종료 후 생성 + // 새 채팅방은 보고서 관련 필드 null + .totalSummary(null) + .situationKeyword(null) + .solutionKeyword(null) + .chatRoomCompletedReason(null) + .counselingType(null) .build(); } public static ChatRoom from(Long id, MemberId memberId, ChatRoomState chatRoomState, int level, int detailedLevel, LocalDateTime lastMessageSentTime, + String title, String totalSummary, String situationKeyword, String solutionKeyword, ChatRoomCompletedReason chatRoomCompletedReason, String counselingType, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { @@ -56,6 +65,8 @@ public static ChatRoom from(Long id, MemberId memberId, ChatRoomState chatRoomSt .level(level) .detailedLevel(detailedLevel) .lastMessageSentTime(lastMessageSentTime) + .title(title) + // 기존 데이터 매핑용 .totalSummary(totalSummary) .situationKeyword(situationKeyword) .solutionKeyword(solutionKeyword) @@ -76,42 +87,29 @@ public void upgradeToNextStage() { this.detailedLevel = 1; } - public void updateChatRoomStateAlive() { - this.chatRoomState = ChatRoomState.ALIVE; - } - - public void updateChatRoomSummary(String totalSummary, String situationKeyword, String solutionKeyword, String counselingType) { - this.totalSummary = totalSummary; - this.situationKeyword = situationKeyword; - this.solutionKeyword = solutionKeyword; - this.counselingType = counselingType; - } - public void updateLastMessageSentTime() { this.lastMessageSentTime = LocalDateTime.now(); } - public void completeByUser() { - this.chatRoomState = ChatRoomState.COMPLETED; - this.totalSummary = COMPLETED_ROOM_CREATING_SUMMARY_LINE; - this.chatRoomCompletedReason = ChatRoomCompletedReason.COMPLETED_BY_USER; + public void updateTitle(String title) { + this.title = title; } public boolean isChatRoomValid() { - return this.chatRoomState == ChatRoomState.ALIVE || this.chatRoomState == ChatRoomState.BEFORE_INIT; + return this.chatRoomState == ChatRoomState.ALIVE; } - public void expire() { - this.chatRoomState = ChatRoomState.COMPLETED; - this.totalSummary = EXPIRED_ROOM_CREATING_SUMMARY_LINE; - this.chatRoomCompletedReason = ChatRoomCompletedReason.EXPIRED; + public boolean isOwner(MemberId memberId) { + return Objects.equals(this.memberId.getValue(), memberId.getValue()); } - public boolean isStarted() { - return this.chatRoomState != ChatRoomState.BEFORE_INIT; + public void softDelete() { + this.chatRoomState = ChatRoomState.DELETED; } - public boolean isOwner(MemberId memberId) { - return Objects.equals(this.memberId.getValue(), memberId.getValue()); + // 기존 보고서가 있는지 확인 + public boolean hasReport() { + return this.chatRoomState == ChatRoomState.COMPLETED + && this.totalSummary != null; } } diff --git a/src/main/java/makeus/cmc/malmo/domain/model/chat/Prompt.java b/src/main/java/makeus/cmc/malmo/domain/model/chat/Prompt.java index f1618fc5..225d032d 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/chat/Prompt.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/chat/Prompt.java @@ -21,6 +21,7 @@ public class Prompt extends BaseTimeEntity { private boolean isForTotalSummary; private boolean isForGuideline; private boolean isForAnswerMetadata; + private boolean isForTitleGeneration; // BaseTimeEntity fields private LocalDateTime createdAt; @@ -30,6 +31,7 @@ public class Prompt extends BaseTimeEntity { public static Prompt from(Long id, int level, String content, boolean isForSystem, boolean isForSummary, boolean isForCompletedResponse, boolean isForTotalSummary, boolean isForGuideline, boolean isForAnswerMetadata, + boolean isForTitleGeneration, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { return Prompt.builder() .id(id) @@ -41,6 +43,7 @@ public static Prompt from(Long id, int level, String content, .isForTotalSummary(isForTotalSummary) .isForGuideline(isForGuideline) .isForAnswerMetadata(isForAnswerMetadata) + .isForTitleGeneration(isForTitleGeneration) .createdAt(createdAt) .modifiedAt(modifiedAt) .deletedAt(deletedAt) diff --git a/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java b/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java index 7faf8b44..aadc4036 100644 --- a/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java +++ b/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java @@ -6,8 +6,6 @@ import makeus.cmc.malmo.domain.value.id.MemberId; import org.springframework.stereotype.Component; -import java.time.LocalDateTime; - @Component public class ChatRoomDomainService { @@ -22,12 +20,4 @@ public ChatMessage createUserMessage(ChatRoomId chatRoomId, int level, int detai public ChatMessage createAiMessage(ChatRoomId chatRoomId, int level, int detailedLevel, String content) { return ChatMessage.createAssistantTextMessage(chatRoomId, level, detailedLevel, content); } - - public boolean isChatRoomExpired(LocalDateTime lastMessageSentTime) { - if (lastMessageSentTime == null) { - return false; - } - - return lastMessageSentTime.isBefore(LocalDateTime.now().minusDays(1)); - } } diff --git a/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomCompletedReason.java b/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomCompletedReason.java index bb71a2f2..b29b26d2 100644 --- a/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomCompletedReason.java +++ b/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomCompletedReason.java @@ -1,5 +1,5 @@ package makeus.cmc.malmo.domain.value.state; public enum ChatRoomCompletedReason { - EXPIRED, COMPLETED_BY_USER, CHAT_PROCESS_DONE + COMPLETED_BY_USER, EXPIRED } diff --git a/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomState.java b/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomState.java index 0786c11c..783729a5 100644 --- a/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomState.java +++ b/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomState.java @@ -1,5 +1,7 @@ package makeus.cmc.malmo.domain.value.state; public enum ChatRoomState { - BEFORE_INIT, ALIVE, PAUSED, NEED_NEXT_QUESTION, COMPLETED, DELETED + ALIVE, // 진행 중 (생성 즉시 ALIVE) + COMPLETED, // 기존 완료된 채팅방 (보고서 조회용) + DELETED // 삭제됨 (soft delete) } \ No newline at end of file diff --git a/src/main/java/makeus/cmc/malmo/util/ChatTokenConstants.java b/src/main/java/makeus/cmc/malmo/util/ChatTokenConstants.java new file mode 100644 index 00000000..eaf5dd09 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/util/ChatTokenConstants.java @@ -0,0 +1,18 @@ +package makeus.cmc.malmo.util; + +public class ChatTokenConstants { + /** + * 4단계 자유 대화에서 최근 메시지 개수 제한 + */ + public static final int FREE_CONVERSATION_RECENT_MESSAGE_LIMIT = 20; + + /** + * 4단계 자유 대화에서 요약 생성 임계값 (이 개수 초과 시 요약 생성) + */ + public static final int FREE_CONVERSATION_SUMMARY_THRESHOLD = 30; + + /** + * 4단계 자유 대화에서 요약 생성 주기 (이 개수 단위로 요약 생성) + */ + public static final int FREE_CONVERSATION_SUMMARY_INTERVAL = 20; +} diff --git a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java index 32c664eb..46ee9898 100644 --- a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java +++ b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java @@ -10,10 +10,6 @@ public class GlobalConstants { "나와의 대화를 마무리하고 싶다면 종료하기 버튼을 눌러줘! 대화 종료 후에는 대화 요약 리포트를 보여줄게.\n" + "오늘은 어떤 고민 때문에 나를 찾아왔어?"; - public static final String EXPIRED_ROOM_CREATING_SUMMARY_LINE = "하루가 지나 채팅방이 만료되었습니다. 요약 생성 중..."; - - public static final String COMPLETED_ROOM_CREATING_SUMMARY_LINE = "채팅방이 종료되었습니다. 요약 생성 중..."; - public static final String OPENAI_CHAT_URL = "https://api.openai.com/v1"; public static final String OPENAI_STATUS_URL = "https://status.openai.com/api/v2/status.json"; diff --git a/src/main/resources/data-test.sql b/src/main/resources/data-test.sql index 20029730..e41d5144 100644 --- a/src/main/resources/data-test.sql +++ b/src/main/resources/data-test.sql @@ -11,12 +11,12 @@ VALUES ('지금 연애를 시작하게 된 계기는 무엇인가요?', '지금 ('연애 중 가장 고마웠던 순간은 어떤 상황이었나요?', '연애 중 가장 고마웠던 순간은 어떤 상황이었나요?', 4), ('연인이 서운한 마음을 표현할 때, 나는 어떤 마음이 드나요?', '연인이 서운한 마음을 표현할 때, 나는 어떤 마음이 드나요?', 5); -INSERT INTO prompt_entity (level, content, is_for_answer_metadata, is_for_completed_response, is_for_guideline, is_for_summary, is_for_system, is_for_total_summary) +INSERT INTO prompt_entity (level, content, is_for_answer_metadata, is_for_completed_response, is_for_guideline, is_for_summary, is_for_system, is_for_total_summary, is_for_title_generation) VALUES - (-3, '요약용 프롬프트', true, false, false, true, false, true), - (-2, '시스템 프롬프트' , false, false, false, false, true, false), - (-1, '중간 요약용 프롬프트', true, false, false, true, false, false), - (1, '1단계 프롬프트', false, true, true, false, false, false), - (2, '2단계 프롬프트', false, true, true, false, false, false), - (3, '3단계 프롬프트', false, true, true, false, false, false), - (4, '마지막 프롬프트', false, true, true, false, false, false); + (-3, '요약용 프롬프트', true, false, false, true, false, true, false), + (-2, '시스템 프롬프트' , false, false, false, false, true, false, false), + (-1, '중간 요약용 프롬프트', true, false, false, true, false, false, false), + (0, '다음 대화 내용을 바탕으로 20자 이내의 간결한 제목을 생성해주세요.', false, false, false, false, false, false, true), + (1, '1단계 프롬프트', false, false, true, false, false, false, false), + (2, '2단계 프롬프트', false, false, true, false, false, false, false), + (3, '3단계 프롬프트', false, false, true, false, false, false, false); diff --git a/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java b/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java index e40987ce..fbe9763c 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; -import makeus.cmc.malmo.adaptor.message.StreamMessage; import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatMessageSummaryEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.ChatRoomEntity; @@ -19,13 +18,11 @@ import makeus.cmc.malmo.domain.value.type.Provider; import makeus.cmc.malmo.domain.value.type.SenderType; import makeus.cmc.malmo.integration_test.dto_factory.ChatRoomRequestDtoFactory; -import makeus.cmc.malmo.util.GlobalConstants; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -37,13 +34,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; import static makeus.cmc.malmo.adaptor.in.exception.ErrorCode.*; import static makeus.cmc.malmo.util.GlobalConstants.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -92,10 +85,10 @@ void setup() { private MemberEntity createAndSaveMember(String nickname, String email, String inviteCode) { MemberEntity memberEntity = MemberEntity.builder() .provider(Provider.KAKAO) - .providerId(email) // providerId를 email로 사용 + .providerId(email) .memberRole(MemberRole.MEMBER) .memberState(MemberState.ALIVE) - .startLoveDate(LocalDate.of(2023, 1, 1)) // 임의의 연애 시작일 + .startLoveDate(LocalDate.of(2023, 1, 1)) .nickname(nickname) .email(email) .inviteCodeEntityValue(InviteCodeEntityValue.of(inviteCode)) @@ -111,7 +104,7 @@ private MemberEntity createAndSaveDeletedMember(String nickname, String email, S .memberRole(MemberRole.MEMBER) .memberState(MemberState.DELETED) .nickname(nickname) - .startLoveDate(LocalDate.of(2023, 1, 1)) // 임의의 연애 시작일 + .startLoveDate(LocalDate.of(2023, 1, 1)) .email(email) .inviteCodeEntityValue(InviteCodeEntityValue.of(inviteCode)) .build(); @@ -120,102 +113,60 @@ private MemberEntity createAndSaveDeletedMember(String nickname, String email, S } @Nested - @DisplayName("현재 채팅방 상태 조회") - class GetCurrentChatRoom { + @DisplayName("채팅방 생성") + class CreateChatRoom { @Test - @DisplayName("채팅방이 없는 경우 채팅방 상태 조회에 성공하며, 새로운 채팅방이 생성된다") - void 채팅방_없는_경우_상태_조회_성공() throws Exception { + @DisplayName("채팅방 생성에 성공한다") + void 채팅방_생성_성공() throws Exception { // when & then - mockMvc.perform(get("/chatrooms/current") + mockMvc.perform(post("/chatrooms") .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.chatRoomState").value(ChatRoomState.BEFORE_INIT.name())); + .andExpect(jsonPath("$.data.chatRoomId").exists()) + .andExpect(jsonPath("$.data.chatRoomState").value(ChatRoomState.ALIVE.name())); - ChatRoomEntity chatRoom = em.createQuery("SELECT c FROM ChatRoomEntity c WHERE c.memberEntityId.value = :memberId", ChatRoomEntity.class) + List chatRooms = em.createQuery("SELECT c FROM ChatRoomEntity c WHERE c.memberEntityId.value = :memberId", ChatRoomEntity.class) .setParameter("memberId", member.getId()) - .getSingleResult(); - Assertions.assertThat(chatRoom).isNotNull(); - Assertions.assertThat(chatRoom.getChatRoomState()).isEqualTo(ChatRoomState.BEFORE_INIT); + .getResultList(); + Assertions.assertThat(chatRooms).hasSize(1); + Assertions.assertThat(chatRooms.get(0).getChatRoomState()).isEqualTo(ChatRoomState.ALIVE); List messages = em.createQuery("SELECT m FROM ChatMessageEntity m WHERE m.chatRoomEntityId.value = :chatRoomId", ChatMessageEntity.class) - .setParameter("chatRoomId", chatRoom.getId()) + .setParameter("chatRoomId", chatRooms.get(0).getId()) .getResultList(); - Assertions.assertThat(messages).hasSize(2); - String contentCombined = messages.get(0).getContent() + messages.get(1).getContent(); - Assertions.assertThat(contentCombined).isEqualTo(member.getNickname() + "아" + INIT_CHAT_MESSAGE); + Assertions.assertThat(messages).hasSize(1); + Assertions.assertThat(messages.get(0).getContent()).contains(INIT_CHAT_MESSAGE); } @Test - @DisplayName("채팅방이 있는 경우 채팅방 상태 조회에 성공한다") - void 채팅방_있는_경우_상태_조회_성공() throws Exception { - // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.ALIVE) - .build(); - em.persist(chatRoom); - em.flush(); - + @DisplayName("탈퇴한 사용자의 경우 채팅방 생성에 실패한다") + void 탈퇴한_사용자_생성_실패() throws Exception { // when & then - mockMvc.perform(get("/chatrooms/current") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.chatRoomState").value(ChatRoomState.ALIVE.name())); + mockMvc.perform(post("/chatrooms") + .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken())) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); } @Test - @DisplayName("마지막 채팅 시간으로부터 24시간이 지난 경우 채팅방 상태 조회에 성공하며, 새로운 채팅방이 생성된다") - void 마지막_채팅_시간_24시간_지난_경우_상태_조회_성공() throws Exception { - // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.ALIVE) - .lastMessageSentTime(LocalDateTime.now().minusDays(1).minusHours(1)) // 25시간 전 - .build(); - em.persist(chatRoom); - em.flush(); - - ChatProcessor.CounselingSummary mockSummary = new ChatProcessor.CounselingSummary( - "만료된 채팅방 요약", - "상황 키워드", - "솔루션 키워드", - "재회 고민" - ); - when(chatProcessor.requestTotalSummary(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(mockSummary)); - - // when & then - mockMvc.perform(get("/chatrooms/current") + @DisplayName("여러 개의 채팅방을 생성할 수 있다") + void 다중_채팅방_생성_성공() throws Exception { + // when - 첫 번째 채팅방 생성 + mockMvc.perform(post("/chatrooms") .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.chatRoomState").value(ChatRoomState.BEFORE_INIT.name())); + .andExpect(status().isOk()); - Assertions.assertThat(chatRoom.getChatRoomState()).isEqualTo(ChatRoomState.COMPLETED); - Assertions.assertThat(chatRoom.getTotalSummary()).isEqualTo(GlobalConstants.EXPIRED_ROOM_CREATING_SUMMARY_LINE); + // when - 두 번째 채팅방 생성 + mockMvc.perform(post("/chatrooms") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()); - ChatRoomEntity newChatRoom = em.createQuery("SELECT c FROM ChatRoomEntity c WHERE c.memberEntityId.value = :memberId " + - "AND c.chatRoomState = :chatRoomState", ChatRoomEntity.class) + // then + List chatRooms = em.createQuery("SELECT c FROM ChatRoomEntity c WHERE c.memberEntityId.value = :memberId AND c.chatRoomState = :state", ChatRoomEntity.class) .setParameter("memberId", member.getId()) - .setParameter("chatRoomState", ChatRoomState.BEFORE_INIT) - .getSingleResult(); - Assertions.assertThat(newChatRoom).isNotNull(); - - List messages = em.createQuery("SELECT m FROM ChatMessageEntity m WHERE m.chatRoomEntityId.value = :chatRoomId", ChatMessageEntity.class) - .setParameter("chatRoomId", newChatRoom.getId()) + .setParameter("state", ChatRoomState.ALIVE) .getResultList(); - - Assertions.assertThat(messages).hasSize(2); - String contentCombined = messages.get(0).getContent() + messages.get(1).getContent(); - Assertions.assertThat(contentCombined).isEqualTo(member.getNickname() + "아" + INIT_CHAT_MESSAGE); - } - - @Test - @DisplayName("탈퇴한 사용자의 경우 채팅방 상태 조회에 실패한다") - void 탈퇴한_사용자_상태_조회_실패() throws Exception { - // when & then - mockMvc.perform(get("/chatrooms/current") - .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken())) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); + Assertions.assertThat(chatRooms).hasSize(2); } } @@ -224,242 +175,160 @@ class GetCurrentChatRoom { class SendChatMessage { @Test - @DisplayName("채팅방이 있는 경우 채팅 전송에 성공한다") - void 채팅방_있는_경우_채팅_전송_성공() throws Exception { + @DisplayName("채팅방에 메시지 전송에 성공한다") + void 메시지_전송_성공() throws Exception { // given ChatRoomEntity chatRoom = ChatRoomEntity.builder() .memberEntityId(MemberEntityId.of(member.getId())) .chatRoomState(ChatRoomState.ALIVE) .level(1) + .detailedLevel(1) .build(); em.persist(chatRoom); em.flush(); String message = "안녕하세요"; - // Mock GptService - doAnswer(invocation -> { - Consumer onComplete = invocation.getArgument(4); - onComplete.accept("AI 응답입니다."); - return null; - }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any(), any()); - // when & then - mockMvc.perform(post("/chatrooms/current/send") + mockMvc.perform(post("/chatrooms/{chatRoomId}/messages", chatRoom.getId()) .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto(message)))) - .andExpect(status().isOk()); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.messageId").exists()); List messages = em.createQuery("SELECT m FROM ChatMessageEntity m WHERE m.chatRoomEntityId.value = :chatRoomId ORDER BY m.createdAt ASC", ChatMessageEntity.class) .setParameter("chatRoomId", chatRoom.getId()) .getResultList(); - Assertions.assertThat(messages).hasSize(2); + Assertions.assertThat(messages).hasSize(1); Assertions.assertThat(messages.get(0).getContent()).isEqualTo(message); Assertions.assertThat(messages.get(0).getSenderType()).isEqualTo(SenderType.USER); } @Test - @DisplayName("채팅방이 없는 경우 채팅 전송에 실패한다") - void 채팅방_없는_경우_채팅_전송_실패() throws Exception { + @DisplayName("다른 사용자의 채팅방에 메시지 전송에 실패한다") + void 권한_없는_채팅방_메시지_전송_실패() throws Exception { + // given + ChatRoomEntity otherChatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(otherMember.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .level(1) + .detailedLevel(1) + .build(); + em.persist(otherChatRoom); + em.flush(); + // when & then - mockMvc.perform(post("/chatrooms/current/send") + mockMvc.perform(post("/chatrooms/{chatRoomId}/messages", otherChatRoom.getId()) .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto("hi")))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_CHAT_ROOM.getCode())); + .andExpect(status().isForbidden()) + .andExpect(jsonPath("$.code").value(MEMBER_ACCESS_DENIED.getCode())); } @Test - @DisplayName("탈퇴한 사용자의 경우 채팅 전송에 실패한다") - void 탈퇴한_사용자_채팅_전송_실패() throws Exception { + @DisplayName("존재하지 않는 채팅방에 메시지 전송에 실패한다") + void 존재하지_않는_채팅방_메시지_전송_실패() throws Exception { // when & then - mockMvc.perform(post("/chatrooms/current/send") - .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken()) + mockMvc.perform(post("/chatrooms/{chatRoomId}/messages", 999L) + .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto("hi")))) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); + .andExpect(jsonPath("$.code").value(NO_SUCH_CHAT_ROOM.getCode())); } @Test - @DisplayName("채팅방이 시작 단계인 경우 채팅 전송에 성공한다") - void 채팅방_시작_단계_채팅_전송_성공() throws Exception { - // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.ALIVE) - .level(INIT_CHATROOM_LEVEL) - .build(); - em.persist(chatRoom); - em.flush(); - - String message = "시작 메시지"; - - doAnswer(invocation -> { - Consumer onComplete = invocation.getArgument(4); - onComplete.accept("AI 응답입니다."); - return null; - }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any(), any()); - + @DisplayName("탈퇴한 사용자의 경우 메시지 전송에 실패한다") + void 탈퇴한_사용자_메시지_전송_실패() throws Exception { // when & then - mockMvc.perform(post("/chatrooms/current/send") - .header("Authorization", "Bearer " + accessToken) + mockMvc.perform(post("/chatrooms/{chatRoomId}/messages", 1L) + .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken()) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto(message)))) - .andExpect(status().isOk()); - - ChatRoomEntity updatedChatRoom = em.find(ChatRoomEntity.class, chatRoom.getId()); - Assertions.assertThat(updatedChatRoom.getChatRoomState()).isEqualTo(ChatRoomState.ALIVE); + .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto("hi")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); } @Test - @DisplayName("채팅방이 마지막 단계인 경우 채팅 전송에 성공한다") - void 채팅방_마지막_단계_채팅_전송_성공() throws Exception { + @DisplayName("DELETED 상태의 채팅방에 메시지 전송에 실패한다") + void 삭제된_채팅방_메시지_전송_실패() throws Exception { // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder() + ChatRoomEntity deletedChatRoom = ChatRoomEntity.builder() .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.ALIVE) - .level(4) // 마지막 단계 레벨 (하드코딩 대신 실제 값 사용) + .chatRoomState(ChatRoomState.DELETED) + .level(1) + .detailedLevel(1) .build(); - em.persist(chatRoom); + em.persist(deletedChatRoom); em.flush(); - String message = "마지막 메시지"; - // when & then - mockMvc.perform(post("/chatrooms/current/send") + mockMvc.perform(post("/chatrooms/{chatRoomId}/messages", deletedChatRoom.getId()) .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto(message)))) - .andExpect(status().isOk()); - - List messages = em.createQuery("SELECT m FROM ChatMessageEntity m WHERE m.chatRoomEntityId.value = :chatRoomId ORDER BY m.createdAt ASC", ChatMessageEntity.class) - .setParameter("chatRoomId", chatRoom.getId()) - .getResultList(); - - Assertions.assertThat(messages).hasSize(2); - Assertions.assertThat(messages.get(0).getContent()).isEqualTo(message); - Assertions.assertThat(messages.get(0).getSenderType()).isEqualTo(SenderType.USER); - Assertions.assertThat(messages.get(1).getContent()).isEqualTo("마지막 프롬프트"); - Assertions.assertThat(messages.get(1).getSenderType()).isEqualTo(SenderType.ASSISTANT); + .content(objectMapper.writeValueAsString(ChatRoomRequestDtoFactory.createSendChatMessageRequestDto("hi")))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(NOT_VALID_CHAT_ROOM.getCode())); } } @Nested - @DisplayName("현재 채팅방 메시지 조회") - class GetCurrentChatRoomMessages { + @DisplayName("채팅방 리스트 조회") + class GetChatRoomList { @Test - @DisplayName("현재 채팅방 메시지 조회에 성공하고 bookmarkId가 포함되지 않는다") - void 현재_채팅방_메시지_조회_성공_bookmarkId_없음() throws Exception { + @DisplayName("채팅방 리스트 조회에 성공한다") + void 채팅방_리스트_조회_성공() throws Exception { // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.ALIVE).build(); - em.persist(chatRoom); - em.persist(ChatMessageEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).level(1).senderType(SenderType.USER).content("메시지1").createdAt(LocalDateTime.now().minusMinutes(2)).build()); - em.persist(ChatMessageEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).level(1).senderType(SenderType.ASSISTANT).content("메시지2").createdAt(LocalDateTime.now().minusMinutes(1)).build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("첫 번째 채팅방") + .lastMessageSentTime(LocalDateTime.now().minusHours(2)) + .build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("두 번째 채팅방") + .lastMessageSentTime(LocalDateTime.now().minusHours(1)) + .build()); em.flush(); // when & then - mockMvc.perform(get("/chatrooms/current/messages") + mockMvc.perform(get("/chatrooms") .header("Authorization", "Bearer " + accessToken) .param("page", "0").param("size", "10")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.totalCount").value(2)) - .andExpect(jsonPath("$.data.list[0].content").value("메시지2")) // 최신순 - .andExpect(jsonPath("$.data.list[1].content").value("메시지1")) - .andExpect(jsonPath("$.data.list[0].bookmarkId").doesNotExist()) - .andExpect(jsonPath("$.data.list[1].bookmarkId").doesNotExist()); - } - - @Test - @DisplayName("탈퇴한 사용자의 경우 현재 채팅방 메시지 조회에 실패한다") - void 탈퇴한_사용자_메시지_조회_실패() throws Exception { - // when & then - mockMvc.perform(get("/chatrooms/current/messages") - .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken())) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); - } - - @Test - @DisplayName("채팅방이 없는 경우 현재 채팅방 메시지 조회에 실패한다") - void 채팅방_없는_경우_메시지_조회_실패() throws Exception { - // when & then - mockMvc.perform(get("/chatrooms/current/messages") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_CHAT_ROOM.getCode())); + .andExpect(jsonPath("$.data.list[0].title").value("두 번째 채팅방")) + .andExpect(jsonPath("$.data.list[1].title").value("첫 번째 채팅방")); } - } - @Nested - @DisplayName("채팅방 종료") - class CompleteChatRoom { @Test - @DisplayName("채팅방 종료에 성공한다") - void 채팅방_종료_성공() throws Exception { - // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.ALIVE).level(5).build(); - em.persist(chatRoom); - em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약1").level(1).build()); - em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약2").level(2).build()); - em.flush(); - - ChatProcessor.CounselingSummary summary = new ChatProcessor.CounselingSummary("최종 요약", "상황 키워드", "솔루션 키워드", "재회 고민"); - when(chatProcessor.requestTotalSummary(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(summary)); - - // when & then - mockMvc.perform(post("/chatrooms/current/complete") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()); - - ChatRoomEntity completedChatRoom = em.find(ChatRoomEntity.class, chatRoom.getId()); - Assertions.assertThat(completedChatRoom.getChatRoomState()).isEqualTo(ChatRoomState.COMPLETED); - verify(outboxHelper, times(1)).publish(any(), any()); - } - - @Test - @DisplayName("채팅방이 없는 경우 채팅방 종료에 실패한다") - void 채팅방_없는_경우_종료_실패() throws Exception { - // when & then - mockMvc.perform(post("/chatrooms/current/complete") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_CHAT_ROOM.getCode())); - } - - @Test - @DisplayName("탈퇴한 사용자의 경우 채팅방 종료에 실패한다") - void 탈퇴한_사용자_종료_실패() throws Exception { - // when & then - mockMvc.perform(post("/chatrooms/current/complete") - .header("Authorization", "Bearer " + generateTokenPort.generateToken(deletedMember.getId(), deletedMember.getMemberRole()).getAccessToken())) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_MEMBER.getCode())); - } - } - - @Nested - @DisplayName("채팅방 리스트 조회") - class GetChatRoomList { - @Test - @DisplayName("채팅방 리스트 조회에 성공한다") - void 채팅방_리스트_조회_성공() throws Exception { + @DisplayName("ALIVE와 COMPLETED 상태 모두 조회된다") + void ALIVE_COMPLETED_모두_조회() throws Exception { // given - em.persist(ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).totalSummary("요약1").build()); - em.persist(ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).totalSummary("요약2").build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("진행 중인 채팅방") + .lastMessageSentTime(LocalDateTime.now()) + .build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .totalSummary("요약") + .lastMessageSentTime(LocalDateTime.now().minusHours(1)) + .build()); em.flush(); // when & then mockMvc.perform(get("/chatrooms") - .header("Authorization", "Bearer " + accessToken) - .param("page", "0").param("size", "10")) + .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.totalCount").value(2)) - .andExpect(jsonPath("$.data.list[0].totalSummary").value("요약2")) // 최신순 - .andExpect(jsonPath("$.data.list[1].totalSummary").value("요약1")); + .andExpect(jsonPath("$.data.totalCount").value(2)); } @Test @@ -484,19 +353,55 @@ class GetChatRoomList { } @Test - @DisplayName("삭제한 채팅방이 있는 경우 채팅방 리스트 조회에 성공한다") - void 삭제한_채팅방_있는_경우_리스트_조회_성공() throws Exception { + @DisplayName("삭제한 채팅방은 리스트에 조회되지 않는다") + void 삭제한_채팅방_제외_리스트_조회() throws Exception { // given - em.persist(ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).build()); - ChatRoomEntity deletedChatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.DELETED).build(); - em.persist(deletedChatRoom); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("활성 채팅방") + .lastMessageSentTime(LocalDateTime.now()) + .build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.DELETED) + .title("삭제된 채팅방") + .lastMessageSentTime(LocalDateTime.now()) + .build()); em.flush(); // when & then mockMvc.perform(get("/chatrooms") .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isOk()) - .andExpect(jsonPath("$.data.totalCount").value(1)); // DELETED는 조회되지 않음 + .andExpect(jsonPath("$.data.totalCount").value(1)); + } + + @Test + @DisplayName("키워드로 채팅방을 검색할 수 있다") + void 키워드_검색_성공() throws Exception { + // given + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("연애 고민") + .lastMessageSentTime(LocalDateTime.now()) + .build()); + em.persist(ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .title("직장 고민") + .lastMessageSentTime(LocalDateTime.now()) + .build()); + em.flush(); + + // when & then + mockMvc.perform(get("/chatrooms") + .header("Authorization", "Bearer " + accessToken) + .param("keyword", "연애")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.data.totalCount").value(1)) + .andExpect(jsonPath("$.data.list[0].title").value("연애 고민")); } } @@ -507,7 +412,10 @@ class DeleteChatRoom { @DisplayName("채팅방 한 건 삭제에 성공한다") void 채팅방_한건_삭제_성공() throws Exception { // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity chatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .build(); em.persist(chatRoom); em.flush(); em.clear(); @@ -527,8 +435,14 @@ class DeleteChatRoom { @DisplayName("채팅방 여러 건 삭제에 성공한다") void 채팅방_여러건_삭제_성공() throws Exception { // given - ChatRoomEntity chatRoom1 = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); - ChatRoomEntity chatRoom2 = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity chatRoom1 = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .build(); + ChatRoomEntity chatRoom2 = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .build(); em.persist(chatRoom1); em.persist(chatRoom2); em.flush(); @@ -549,7 +463,10 @@ class DeleteChatRoom { @DisplayName("접근 권한이 없으면 채팅방 삭제에 실패한다") void 접근_권한_없으면_삭제_실패() throws Exception { // given - ChatRoomEntity otherChatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(otherMember.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity otherChatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(otherMember.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .build(); em.persist(otherChatRoom); em.flush(); @@ -582,10 +499,25 @@ class GetChatRoomMessages { @DisplayName("채팅방의 메시지 리스트 조회에 성공한다") void 메시지_리스트_조회_성공() throws Exception { // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(member.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity chatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(member.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .build(); em.persist(chatRoom); - em.persist(ChatMessageEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).level(1).senderType(SenderType.USER).content("메시지1").createdAt(LocalDateTime.now().minusMinutes(2)).build()); - em.persist(ChatMessageEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).level(1).senderType(SenderType.ASSISTANT).content("메시지2").createdAt(LocalDateTime.now().minusMinutes(1)).build()); + em.persist(ChatMessageEntity.builder() + .chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())) + .level(1) + .senderType(SenderType.USER) + .content("메시지1") + .createdAt(LocalDateTime.now().minusMinutes(2)) + .build()); + em.persist(ChatMessageEntity.builder() + .chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())) + .level(1) + .senderType(SenderType.ASSISTANT) + .content("메시지2") + .createdAt(LocalDateTime.now().minusMinutes(1)) + .build()); em.flush(); // when & then @@ -594,7 +526,7 @@ class GetChatRoomMessages { .param("page", "0").param("size", "10")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data.totalCount").value(2)) - .andExpect(jsonPath("$.data.list[0].content").value("메시지1")) // 오래된 순 + .andExpect(jsonPath("$.data.list[0].content").value("메시지1")) .andExpect(jsonPath("$.data.list[1].content").value("메시지2")); } @@ -612,7 +544,10 @@ class GetChatRoomMessages { @DisplayName("채팅방 접근 권한이 없는 경우 채팅방의 메시지 리스트 조회에 실패한다") void 접근_권한_없는_경우_메시지_리스트_조회_실패() throws Exception { // given - ChatRoomEntity otherChatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(otherMember.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity otherChatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(otherMember.getId())) + .chatRoomState(ChatRoomState.ALIVE) + .build(); em.persist(otherChatRoom); em.flush(); @@ -647,9 +582,21 @@ class GetChatRoomSummary { .totalSummary("전체 요약") .build(); em.persist(chatRoom); - em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약1").level(1).build()); - em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약2").level(2).build()); - em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약3").level(3).build()); + em.persist(ChatMessageSummaryEntity.builder() + .chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())) + .content("요약1") + .level(1) + .build()); + em.persist(ChatMessageSummaryEntity.builder() + .chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())) + .content("요약2") + .level(2) + .build()); + em.persist(ChatMessageSummaryEntity.builder() + .chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())) + .content("요약3") + .level(3) + .build()); em.flush(); // when & then @@ -676,7 +623,10 @@ class GetChatRoomSummary { @DisplayName("채팅방 접근 권한이 없는 경우 채팅방 요약 조회에 실패한다") void 접근_권한_없는_경우_요약_조회_실패() throws Exception { // given - ChatRoomEntity otherChatRoom = ChatRoomEntity.builder().memberEntityId(MemberEntityId.of(otherMember.getId())).chatRoomState(ChatRoomState.COMPLETED).build(); + ChatRoomEntity otherChatRoom = ChatRoomEntity.builder() + .memberEntityId(MemberEntityId.of(otherMember.getId())) + .chatRoomState(ChatRoomState.COMPLETED) + .build(); em.persist(otherChatRoom); em.flush(); @@ -698,4 +648,3 @@ class GetChatRoomSummary { } } } - diff --git a/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/ChatRoomRequestDtoFactory.java b/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/ChatRoomRequestDtoFactory.java index 349ae267..81506687 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/ChatRoomRequestDtoFactory.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/ChatRoomRequestDtoFactory.java @@ -1,14 +1,13 @@ package makeus.cmc.malmo.integration_test.dto_factory; import makeus.cmc.malmo.adaptor.in.web.controller.ChatRoomController; -import makeus.cmc.malmo.adaptor.in.web.controller.CurrentChatController; import java.util.List; public class ChatRoomRequestDtoFactory { - public static CurrentChatController.ChatRequest createSendChatMessageRequestDto(String message) { - return new CurrentChatController.ChatRequest(message); + public static ChatRoomController.SendMessageRequest createSendChatMessageRequestDto(String message) { + return new ChatRoomController.SendMessageRequest(message); } public static ChatRoomController.DeleteChatRoomRequestDto createDeleteChatRoomsRequestDto(List chatRoomIds) { diff --git a/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java index 1352d0cd..18ada605 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java @@ -35,6 +35,7 @@ void toDomain() { .level(1) .detailedLevel(2) .lastMessageSentTime(now) + .title("테스트 제목") .totalSummary("total summary") .situationKeyword("situation") .solutionKeyword("solution") @@ -55,6 +56,7 @@ void toDomain() { assertThat(domain.getLevel()).isEqualTo(entity.getLevel()); assertThat(domain.getDetailedLevel()).isEqualTo(entity.getDetailedLevel()); assertThat(domain.getLastMessageSentTime()).isEqualTo(entity.getLastMessageSentTime()); + assertThat(domain.getTitle()).isEqualTo(entity.getTitle()); assertThat(domain.getTotalSummary()).isEqualTo(entity.getTotalSummary()); assertThat(domain.getSituationKeyword()).isEqualTo(entity.getSituationKeyword()); assertThat(domain.getSolutionKeyword()).isEqualTo(entity.getSolutionKeyword()); @@ -77,6 +79,7 @@ void toEntity() { 1, 2, now, + "테스트 제목", "total summary", "situation", "solution", @@ -97,6 +100,7 @@ void toEntity() { assertThat(entity.getLevel()).isEqualTo(domain.getLevel()); assertThat(entity.getDetailedLevel()).isEqualTo(domain.getDetailedLevel()); assertThat(entity.getLastMessageSentTime()).isEqualTo(domain.getLastMessageSentTime()); + assertThat(entity.getTitle()).isEqualTo(domain.getTitle()); assertThat(entity.getTotalSummary()).isEqualTo(domain.getTotalSummary()); assertThat(entity.getSituationKeyword()).isEqualTo(domain.getSituationKeyword()); assertThat(entity.getSolutionKeyword()).isEqualTo(domain.getSolutionKeyword()); diff --git a/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java index 30bd0755..f335d9a8 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java @@ -35,6 +35,7 @@ void toDomain() { .isForTotalSummary(false) .isForGuideline(false) .isForAnswerMetadata(false) + .isForTitleGeneration(false) .createdAt(now) .modifiedAt(now) .deletedAt(null) @@ -53,6 +54,7 @@ void toDomain() { assertThat(domain.isForTotalSummary()).isEqualTo(entity.isForTotalSummary()); assertThat(domain.isForGuideline()).isEqualTo(entity.isForGuideline()); assertThat(domain.isForAnswerMetadata()).isEqualTo(entity.isForAnswerMetadata()); + assertThat(domain.isForTitleGeneration()).isEqualTo(entity.isForTitleGeneration()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); assertThat(domain.getModifiedAt()).isEqualTo(entity.getModifiedAt()); assertThat(domain.getDeletedAt()).isEqualTo(entity.getDeletedAt()); @@ -73,6 +75,7 @@ void toEntity() { false, false, false, + false, now, now, null @@ -91,6 +94,7 @@ void toEntity() { assertThat(entity.isForTotalSummary()).isEqualTo(domain.isForTotalSummary()); assertThat(entity.isForGuideline()).isEqualTo(domain.isForGuideline()); assertThat(entity.isForAnswerMetadata()).isEqualTo(domain.isForAnswerMetadata()); + assertThat(entity.isForTitleGeneration()).isEqualTo(domain.isForTitleGeneration()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); assertThat(entity.getModifiedAt()).isEqualTo(domain.getModifiedAt()); assertThat(entity.getDeletedAt()).isEqualTo(domain.getDeletedAt());