diff --git a/.github/workflows/deploy-test.yml b/.github/workflows/deploy-test.yml index 385079cc..b0bec0cf 100644 --- a/.github/workflows/deploy-test.yml +++ b/.github/workflows/deploy-test.yml @@ -1,12 +1,10 @@ -name: Test and Deploy to AWS EC2 - -# TODO : 테스트 클라이언트 개발 완료 시 수정 +name: Test and Deploy to Test Environment on: push: - branches: [ release-test ] + branches: [ qa ] pull_request: - branches: [ release-test ] + branches: [ qa ] jobs: # 1. 테스트 작업 (PR, Push 시 항상 실행) @@ -52,10 +50,8 @@ jobs: deploy: # 'test' 작업이 성공해야만 실행됩니다. needs: test - # 'push' 이벤트이고, 커밋 메시지에 특정 키워드가 있을 때만 실행됩니다. - if: | - github.event_name == 'push' && - (contains(github.event.head_commit.message, 'release-green') || contains(github.event.head_commit.message, 'release-blue')) + # 'push' 이벤트일 때만 실행됩니다. + if: github.event_name == 'push' runs-on: ubuntu-latest steps: - name: Checkout source code @@ -84,19 +80,6 @@ jobs: - name: Build with Gradle run: ./gradlew bootJar --no-daemon - - name: Set Port and Container Name based on Commit Message - id: set_config - run: | - if [[ "${{ github.event.head_commit.message }}" == *"release-green"* ]]; then - echo "port=8080" >> $GITHUB_OUTPUT - echo "container_name=malmo-test-green" >> $GITHUB_OUTPUT - echo "Green deployment: port=8080, container=malmo-test-green" - elif [[ "${{ github.event.head_commit.message }}" == *"release-blue"* ]]; then - echo "port=8081" >> $GITHUB_OUTPUT - echo "container_name=malmo-test-blue" >> $GITHUB_OUTPUT - echo "Blue deployment: port=8081, container=malmo-test-blue" - fi - - name: Login to Docker Hub uses: docker/login-action@v3 with: @@ -111,12 +94,12 @@ jobs: docker push $IMAGE_NAME:${{ github.sha }} docker push $IMAGE_NAME:latest - - name: Deploy to EC2 + - name: Deploy to Test EC2 uses: appleboy/ssh-action@v1.0.3 with: - host: ${{ secrets.EC2_HOST }} + host: ${{ secrets.TEST_EC2_HOST }} username: ${{ secrets.EC2_USERNAME }} - key: ${{ secrets.EC2_PRIVATE_KEY }} + key: ${{ secrets.TEST_EC2_PRIVATE_KEY }} script: | # Docker Hub 로그인 echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin @@ -128,28 +111,34 @@ jobs: fi # 기존 컨테이너 중지 및 삭제 - docker stop ${{ steps.set_config.outputs.container_name }} || true - docker rm ${{ steps.set_config.outputs.container_name }} || true + docker stop malmo-test || true + docker rm malmo-test || true # docker-compose.yml 파일 생성 - cat << 'EOF' > docker-compose.test.yml + cat << 'EOF' > docker-compose-test.yml version: '3.8' services: malmo-test: image: ${{ secrets.DOCKERHUB_USERNAME }}/malmo-test:latest - container_name: ${{ steps.set_config.outputs.container_name }} + container_name: malmo-test ports: - - "${{ steps.set_config.outputs.port }}:8080" + - "8080:8080" + logging: + driver: awslogs + options: + awslogs-group: malmo-test + awslogs-multiline-pattern: "^(INFO|ERROR)" + awslogs-region: ap-northeast-2 environment: - TZ=Asia/Seoul - - SPRING_PROFILES_ACTIVE=prod + - SPRING_PROFILES_ACTIVE=qa - SPRING_DATASOURCE_URL=${{ secrets.TEST_DB_URL }} - SPRING_DATASOURCE_USERNAME=${{ secrets.DB_USERNAME }} - SPRING_DATASOURCE_PASSWORD=${{ secrets.DB_PASSWORD }} - SPRING_DATA_REDIS_HOST=malmo-redis - - REDIS_STREAM_KEY=${{ secrets.TEST_REDIS_STREAM_KEY }} - - REDIS_CONSUMER_GROUP=${{ secrets.TEST_REDIS_CONSUMER_GROUP }} + - REDIS_STREAM_KEY=${{ secrets.REDIS_STREAM_KEY_GREEN }} + - REDIS_CONSUMER_GROUP=${{ secrets.REDIS_CONSUMER_GROUP_GREEN }} - JWT_SECRET=${{ secrets.JWT_SECRET }} - KAKAO_REST_API_KEY=${{ secrets.KAKAO_REST_API_KEY }} - APPLE_REST_API_KEY=${{ secrets.APPLE_REST_API_KEY }} @@ -165,6 +154,8 @@ jobs: - SECURITY_SERVER_URL_PRODUCTION=${{ secrets.SECURITY_SERVER_URL_PRODUCTION }} - SECURITY_SERVER_URL_DEVELOPMENT=${{ secrets.SECURITY_SERVER_URL_DEVELOPMENT }} - AES_ENCRYPTION_KEY=${{ secrets.AES_ENCRYPTION_KEY }} + - SENTRY_DSN=${{ secrets.SENTRY_DSN }} + - AMPLITUDE_API_KEY=${{ secrets.AMPLITUDE_API_KEY }} restart: unless-stopped networks: - malmo-net @@ -175,8 +166,8 @@ jobs: EOF # docker-compose로 애플리케이션 실행 - docker-compose -f docker-compose.test.yml pull - docker-compose -f docker-compose.test.yml up -d + docker-compose -f docker-compose-test.yml pull + docker-compose -f docker-compose-test.yml up -d # 불필요한 Docker 이미지 정리 - docker image prune -f \ No newline at end of file + docker image prune -f diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8773991c..de7af6dd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -145,6 +145,12 @@ jobs: container_name: ${{ steps.set_config.outputs.container_name }} ports: - "${{ steps.set_config.outputs.port }}:8080" + logging: + driver: awslogs + options: + awslogs-group: malmo_logs + awslogs-multiline-pattern: "^(INFO|ERROR)" + awslogs-region: ap-northeast-2 environment: - TZ=Asia/Seoul - SPRING_PROFILES_ACTIVE=prod @@ -170,6 +176,7 @@ jobs: - SECURITY_SERVER_URL_DEVELOPMENT=${{ secrets.SECURITY_SERVER_URL_DEVELOPMENT }} - AES_ENCRYPTION_KEY=${{ secrets.AES_ENCRYPTION_KEY }} - SENTRY_DSN=${{ secrets.SENTRY_DSN }} + - AMPLITUDE_API_KEY=${{ secrets.AMPLITUDE_API_KEY }} restart: unless-stopped networks: - malmo-net diff --git a/.gitignore b/.gitignore index 11afbf73..0ca8831f 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ out/ /src/main/resources/data.sql /src/main/resources/data-ver1.sql /src/main/resources/data-ver2.sql +CLAUDE.md diff --git a/Dockerfile b/Dockerfile index a6a32d15..0a0f7cd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -42,4 +42,4 @@ USER malmo EXPOSE 8080 # 애플리케이션 실행 -ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "/app/app.jar"] \ No newline at end of file +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ 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 781d6175..93be6b04 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/RedisStreamConsumer.java @@ -64,9 +64,6 @@ public void onMessage(MapRecord record) { case REQUEST_CHAT_MESSAGE: future = processChatMessage(payloadNode); break; - case REQUEST_SUMMARY: - future = processSummary(payloadNode); - break; case REQUEST_TOTAL_SUMMARY: future = processTotalSummary(payloadNode); break; @@ -108,15 +105,7 @@ private CompletableFuture processChatMessage(JsonNode payloadNode) { .chatRoomId(payloadNode.get("chatRoomId").asLong()) .nowMessage(payloadNode.get("nowMessage").asText()) .promptLevel(payloadNode.get("promptLevel").asInt()) - .build() - ); - } - - private CompletableFuture processSummary(JsonNode payloadNode) { - return processMessageUseCase.processSummary( - ProcessMessageUseCase.ProcessSummaryCommand.builder() - .chatRoomId(payloadNode.get("chatRoomId").asLong()) - .promptLevel(payloadNode.get("promptLevel").asInt()) + .detailedLevel(payloadNode.get("detailedLevel").asInt()) .build() ); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/GlobalExceptionHandler.java b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/GlobalExceptionHandler.java index a15fcb29..46f81fc9 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/exception/GlobalExceptionHandler.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/exception/GlobalExceptionHandler.java @@ -22,19 +22,19 @@ public class GlobalExceptionHandler { @ExceptionHandler({MemberNotFoundException.class}) public ResponseEntity handleMemberNotFoundException(MemberNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleMemberNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleMemberNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_MEMBER); } @ExceptionHandler({InviteCodeNotFoundException.class}) public ResponseEntity handleCoupleCodeNotFoundException(InviteCodeNotFoundException e) { - log.info("[GlobalExceptionHandler: handleCoupleCodeNotFoundException 호출]", e); + log.info("[GlobalExceptionHandler: handleCoupleCodeNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_COUPLE_CODE); } @ExceptionHandler({OidcIdTokenException.class}) public ResponseEntity handleOidcIdTokenException(OidcIdTokenException e) { - log.warn("[GlobalExceptionHandler: handleOidcIdTokenException 호출]", e); + log.warn("[GlobalExceptionHandler: handleOidcIdTokenException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.INVALID_ID_TOKEN); } @@ -47,7 +47,7 @@ public ResponseEntity handleRestApiException(RestApiException e) @ExceptionHandler({InvalidRefreshTokenException.class}) public ResponseEntity handleInvalidRefreshTokenException(InvalidRefreshTokenException e) { - log.info("[GlobalExceptionHandler: handleInvalidRefreshTokenException 호출]", e); + log.info("[GlobalExceptionHandler: handleInvalidRefreshTokenException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.INVALID_REFRESH_TOKEN); } @@ -60,37 +60,37 @@ public ResponseEntity handleInviteCodeGenerateFailedException(Inv @ExceptionHandler({TermsNotFoundException.class}) public ResponseEntity handleTermsNotFoundException(TermsNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleTermsNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleTermsNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_TERMS); } @ExceptionHandler({LoveTypeNotFoundException.class}) public ResponseEntity handleLoveTypeNotFoundException(LoveTypeNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleLoveTypeNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleLoveTypeNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_LOVE_TYPE); } @ExceptionHandler({LoveTypeQuestionNotFoundException.class}) public ResponseEntity handleLoveTypeQuestionNotFoundException(LoveTypeQuestionNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleLoveTypeQuestionNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleLoveTypeQuestionNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_LOVE_TYPE_QUESTION); } @ExceptionHandler({NotCoupleMemberException.class}) public ResponseEntity handleNotCoupleMemberException(NotCoupleMemberException e) { - log.info("[GlobalExceptionHandler: handleNotCoupleMemberException 호출]", e); + log.info("[GlobalExceptionHandler: handleNotCoupleMemberException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NOT_COUPLE_MEMBER); } @ExceptionHandler({UsedInviteCodeException.class}) public ResponseEntity handleUsedCoupleCodeException(UsedInviteCodeException e) { - log.info("[GlobalExceptionHandler: handleUsedCoupleCodeException 호출]", e); + log.info("[GlobalExceptionHandler: handleUsedCoupleCodeException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.USED_COUPLE_CODE); } @ExceptionHandler({AlreadyCoupledMemberException.class}) public ResponseEntity handleAlreadyCoupledMemberException(AlreadyCoupledMemberException e) { - log.info("[GlobalExceptionHandler: handleAlreadyCoupledMemberException 호출]", e); + log.info("[GlobalExceptionHandler: handleAlreadyCoupledMemberException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.ALREADY_COUPLED_MEMBER); } @@ -103,37 +103,37 @@ public ResponseEntity handleSseConnectionException(SseConnectionE @ExceptionHandler({MemberNotTestedException.class}) public ResponseEntity handleMemberNotTestedException(MemberNotTestedException e) { - log.info("[GlobalExceptionHandler: handleMemberNotTestedException 호출]", e); + log.info("[GlobalExceptionHandler: handleMemberNotTestedException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.MEMBER_NOT_TESTED); } @ExceptionHandler({ChatRoomNotFoundException.class}) public ResponseEntity handleChatRoomNotFoundException(ChatRoomNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleChatRoomNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleChatRoomNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_CHAT_ROOM); } @ExceptionHandler({NotValidChatRoomException.class}) public ResponseEntity handleNotValidChatRoomException(NotValidChatRoomException e) { - log.warn("[GlobalExceptionHandler: handleNotValidChatRoomException 호출]", e); + log.warn("[GlobalExceptionHandler: handleNotValidChatRoomException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NOT_VALID_CHAT_ROOM); } @ExceptionHandler({MemberAccessDeniedException.class}) public ResponseEntity handleMemberAccessDeniedException(MemberAccessDeniedException e) { - log.warn("[GlobalExceptionHandler: handleMemberAccessDeniedException 호출]", e); + log.warn("[GlobalExceptionHandler: handleMemberAccessDeniedException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.MEMBER_ACCESS_DENIED); } @ExceptionHandler({NotValidCoupleCodeException.class}) public ResponseEntity handleNotValidCoupleCodeException(NotValidCoupleCodeException e) { - log.info("[GlobalExceptionHandler: handleNotValidCoupleCodeException 호출]", e); + log.info("[GlobalExceptionHandler: handleNotValidCoupleCodeException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NOT_VALID_COUPLE_CODE); } @ExceptionHandler({CoupleQuestionNotFoundException.class}) public ResponseEntity handleCoupleQuestionNotFoundException(CoupleQuestionNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleCoupleQuestionNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleCoupleQuestionNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_COUPLE_QUESTION); } @@ -146,25 +146,24 @@ public ResponseEntity handleOAuthUnlinkFailureException(OAuthUnli @ExceptionHandler({TempLoveTypeNotFoundException.class}) public ResponseEntity handleTempLoveTypeNotFoundException(TempLoveTypeNotFoundException e) { - log.warn("[GlobalExceptionHandler: handleTempLoveTypeNotFoundException 호출]", e); + log.warn("[GlobalExceptionHandler: handleTempLoveTypeNotFoundException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.NO_SUCH_TEMP_LOVE_TYPE); } - /** * ---------- 공통 예외 처리 핸들러 ---------- */ @ExceptionHandler({NoHandlerFoundException.class, TypeMismatchException.class}) public ResponseEntity handleBadRequestException(Exception e) { - log.warn("[CommonExceptionHandler: handleBadRequestException 호출]", e); + log.warn("[CommonExceptionHandler: handleBadRequestException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.BAD_REQUEST); } @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public ResponseEntity handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) { - log.warn("[CommonExceptionHandler: handleHttpRequestMethodNotSupportedException 호출]", e); + log.warn("[CommonExceptionHandler: handleHttpRequestMethodNotSupportedException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.METHOD_NOT_ALLOWED); } @@ -184,7 +183,7 @@ public ResponseEntity handleRuntimeException(Exception e) { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { - log.warn("[CommonExceptionHandler: handleMethodArgumentNotValidException 호출]", e); + log.warn("[CommonExceptionHandler: handleMethodArgumentNotValidException 호출] {}", e.getMessage()); return ErrorResponse.of(ErrorCode.BAD_REQUEST); } -} +} \ No newline at end of file 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 index a272363e..7cc37d3c 100644 --- 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 @@ -105,26 +105,6 @@ public BaseResponse sendChatMess return BaseResponse.success(sendChatMessageResponse); } - @Operation( - summary = "채팅방 단계 변경", - description = "현재 채팅방의 단계를 업그레이드합니다. 다음 단계의 오프닝 멘트를 SSE로 전달됩니다. JWT 토큰이 필요합니다.", - security = @SecurityRequirement(name = "Bearer Authentication") - ) - @ApiResponse( - responseCode = "200", - description = "채팅방 단계 변경 성공; 데이터 응답 값은 없음", - content = @Content(schema = @Schema(implementation = SwaggerResponses.BaseSwaggerResponse.class)) - ) - @ApiCommonResponses.RequireAuth - @PostMapping("/upgrade") - public BaseResponse sendChatMessage(@AuthenticationPrincipal User user) { - sendChatMessageUseCase.upgradeChatRoom(SendChatMessageUseCase.SendChatMessageCommand.builder() - .userId(Long.valueOf(user.getUsername())) - .build()); - - return BaseResponse.success(null); - } - @Operation( summary = "채팅방 종료", description = "현재 채팅방을 종료합니다. JWT 토큰이 필요합니다.", diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoginController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoginController.java index c1a9fc1f..819cd7a5 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoginController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/LoginController.java @@ -47,6 +47,7 @@ public BaseResponse loginWithKakao( SignInUseCase.SignInKakaoCommand command = SignInUseCase.SignInKakaoCommand.builder() .idToken(requestDto.idToken) .accessToken(requestDto.accessToken) + .deviceId(requestDto.deviceId) .build(); return BaseResponse.success(signInUseCase.signInKakao(command)); } @@ -68,6 +69,7 @@ public BaseResponse loginWithApple( SignInUseCase.SignInAppleCommand command = SignInUseCase.SignInAppleCommand.builder() .idToken(requestDto.idToken) .authorizationCode(requestDto.authorizationCode) + .deviceId(requestDto.deviceId) .build(); return BaseResponse.success(signInUseCase.signInApple(command)); } @@ -100,6 +102,7 @@ public static class KakaoLoginRequestDto { private String idToken; @NotEmpty(message = "accessToken은 필수 입력값입니다.") private String accessToken; + private String deviceId; } @Data @@ -107,5 +110,6 @@ public static class AppleLoginRequestDto { @NotEmpty(message = "idToken은 필수 입력값입니다.") private String idToken; private String authorizationCode; + private String deviceId; } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/MemberController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/MemberController.java index 82de9b5d..b0c2eb77 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/MemberController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/MemberController.java @@ -217,7 +217,7 @@ public BaseResponse registerLoveType( @Operation( summary = "연애 시작일 변경", - description = "연애 시작일을 변경합니다. JWT 토큰이 필요합니다.", + description = "커플로 연동된 사용자의 연애 시작일을 변경합니다. 커플이 아닌 사용자는 사용할 수 없습니다. JWT 토큰이 필요합니다.", security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -226,6 +226,7 @@ public BaseResponse registerLoveType( content = @Content(schema = @Schema(implementation = SwaggerResponses.UpdateStartLoveDateSuccessResponse.class)) ) @ApiCommonResponses.RequireAuth + @ApiCommonResponses.OnlyCouple @PatchMapping("/start-love-date") public BaseResponse updateStartLoveDate( @AuthenticationPrincipal User user, diff --git a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/SignUpController.java b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/SignUpController.java index 27e2b38f..b2c3be01 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/SignUpController.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/in/web/controller/SignUpController.java @@ -20,7 +20,6 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import java.time.LocalDate; import java.util.List; @Tag(name = "회원가입 API", description = "사용자 회원가입 관련 API") @@ -32,7 +31,7 @@ public class SignUpController { @Operation( summary = "회원가입", - description = "인증된 사용자의 추가 정보를 입력받아 회원가입을 완료합니다. JWT 토큰이 필요합니다.", + description = "인증된 사용자의 추가 정보를 입력받아 회원가입을 완료합니다. 연애 시작일은 커플 연동 시 자동으로 설정됩니다. JWT 토큰이 필요합니다.", security = @SecurityRequirement(name = "Bearer Authentication") ) @ApiResponse( @@ -58,8 +57,7 @@ public BaseResponse signUp( .memberId(Long.valueOf(user.getUsername())) .terms(termsCommandList) .nickname(requestDto.getNickname()) - .loveStartDate(requestDto.getLoveStartDate()) - .loveTypeId(requestDto.getLoveTypeId()) // Optional, 애착 유형 결과를 매핑하기 위한 ID + .loveTypeId(requestDto.getLoveTypeId()) .build(); signUpUseCase.signUp(command); @@ -77,10 +75,6 @@ public static class SignUpRequestDto { @Pattern(regexp = "^[가-힣a-zA-Z0-9]+$", message = "닉네임은 한글, 영문, 숫자만 사용 가능합니다.") private String nickname; - @NotNull(message = "시작일은 필수 입력값입니다.") - @PastOrPresent(message = "시작일은 오늘 또는 과거 날짜여야 합니다.") - private LocalDate loveStartDate; - private Long loveTypeId; // Optional, 애착 유형 결과를 매핑하기 위한 ID } 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 a4c386b9..1095b311 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 @@ -289,6 +289,9 @@ public static class PartnerMemberData { @Schema(description = "닉네임", example = "김영희") private String nickname; + + @Schema(description = "디데이 변경 이력 여부", example = "false") + private Boolean isStartLoveDateUpdated; } @Getter diff --git a/src/main/java/makeus/cmc/malmo/adaptor/message/StreamChatMessage.java b/src/main/java/makeus/cmc/malmo/adaptor/message/StreamChatMessage.java index 9b42bc9e..9d00d12d 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/message/StreamChatMessage.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/message/StreamChatMessage.java @@ -12,4 +12,5 @@ public class StreamChatMessage implements StreamMessage { private Long chatRoomId; private String nowMessage; private Integer promptLevel; + private Integer detailedLevel; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java b/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java index 3ffbd11d..487d46b3 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/OpenAiApiClient.java @@ -30,7 +30,7 @@ @RequiredArgsConstructor public class OpenAiApiClient implements RequestChatApiPort, CheckOpenAIHealth { - public static final String GPT_VERSION = "gpt-4o"; + public static final String GPT_VERSION = "gpt-4.1"; public static final double GPT_TEMPERATURE = 0.5; @Value("${openai.api.key}") diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/SseEmitterAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/SseEmitterAdapter.java index 9a3fafc6..f97005bd 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/SseEmitterAdapter.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/SseEmitterAdapter.java @@ -20,7 +20,7 @@ @RequiredArgsConstructor @Component public class SseEmitterAdapter implements SendSseEventPort, ConnectSsePort, ValidateSsePort { - private static final long TIMEOUT = 60 * 1000L; // 1분 + private static final long TIMEOUT = 3 * 60 * 1000L; // 3분 private static final int MAX_SIZE = 1000; public static final long RECONNECT_TIME_MILLIS = 3000L; diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/amplitude/AmplitudeAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/amplitude/AmplitudeAdapter.java new file mode 100644 index 00000000..009cc79a --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/amplitude/AmplitudeAdapter.java @@ -0,0 +1,94 @@ +package makeus.cmc.malmo.adaptor.out.amplitude; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import makeus.cmc.malmo.application.port.out.amplitude.AmplitudePort; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AmplitudeAdapter implements AmplitudePort { + + private final RestTemplate restTemplate; + + @Value("${amplitude.api.key}") + private String apiKey; + + @Value("${amplitude.api.url}") + private String apiUrl; + + @Override + public void identifyUser(IdentifyUserCommand command) { + try { + AmplitudeRequest request = AmplitudeRequest.builder() + .apiKey(apiKey) + .identification(AmplitudeIdentification.builder() + .userId(command.getUserId()) + .deviceId(command.getDeviceId()) + .userProperties(UserProperties.builder() + .email(command.getEmail()) + .nickname(command.getNickname()) + .build()) + .build()) + .build(); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("api_key", request.getApiKey()); + body.add("identification", "[{\"user_id\":\"" + request.getIdentification().getUserId() + + "\",\"device_id\":\"" + request.getIdentification().getDeviceId() + + "\",\"user_properties\":{\"email\":\"" + request.getIdentification().getUserProperties().getEmail() + + "\",\"nickname\":\"" + request.getIdentification().getUserProperties().getNickname() + "\"}}]"); + + HttpEntity> entity = new HttpEntity<>(body, headers); + + ResponseEntity response = restTemplate.postForEntity(apiUrl, entity, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + log.info("Amplitude identify API 호출 성공: userId={}, deviceId={}", + command.getUserId(), command.getDeviceId()); + } else { + log.warn("Amplitude identify API 호출 실패: status={}, userId={}", + response.getStatusCode(), command.getUserId()); + } + } catch (Exception e) { + log.error("Amplitude identify API 호출 중 오류 발생: userId={}, error={}", + command.getUserId(), e.getMessage(), e); + } + } + + @Data + @Builder + private static class AmplitudeRequest { + private String apiKey; + private AmplitudeIdentification identification; + } + + @Data + @Builder + private static class AmplitudeIdentification { + private String userId; + private String deviceId; + private UserProperties userProperties; + } + + @Data + @Builder + private static class UserProperties { + private String email; + private String nickname; + } +} 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 bdd4efdb..3ba16e01 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 @@ -48,6 +48,14 @@ public List loadChatRoomMessagesByLevel(ChatRoomId chatRoomId, int .toList(); } + @Override + public List loadChatRoomLevelAndDetailedLevelMessages(ChatRoomId chatRoomId, int level, int detailedLevel) { + return chatMessageRepository.findByChatRoomIdAndLevelAndDetailedLevel(chatRoomId.getValue(), level, detailedLevel) + .stream() + .map(chatMessageMapper::toDomain) + .toList(); + } + @Override public Optional loadCurrentChatRoomByMemberId(MemberId memberId) { return chatRoomRepository.findCurrentChatRoomByMemberEntityId(memberId.getValue()) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/MemberPersistenceAdapter.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/MemberPersistenceAdapter.java index 2d416f40..15d28e63 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/MemberPersistenceAdapter.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/adapter/MemberPersistenceAdapter.java @@ -127,6 +127,7 @@ public static class PartnerMemberRepositoryDto { private float avoidanceRate; private float anxietyRate; private String nickname; + private Boolean isStartLoveDateUpdated; public MemberQueryHelper.PartnerMemberDto toDto() { return MemberQueryHelper.PartnerMemberDto.builder() @@ -135,6 +136,7 @@ public MemberQueryHelper.PartnerMemberDto toDto() { .avoidanceRate(avoidanceRate) .anxietyRate(anxietyRate) .nickname(nickname) + .isStartLoveDateUpdated(isStartLoveDateUpdated) .build(); } } 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 2f38630c..bc3de086 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 @@ -21,4 +21,47 @@ public Optional loadPromptByLevel(int level) { return promptRepository.findByLevel(level) .map(promptMapper::toDomain); } + + @Override + public Optional loadSystemPrompt() { + return promptRepository.findByIsForSystemTrue() + .map(promptMapper::toDomain); + } + + @Override + @Deprecated + public Optional loadSummaryPrompt() { + return promptRepository.findByIsForSummaryTrue() + .map(promptMapper::toDomain); + } + + @Override + public Optional loadCompletedResponsePrompt() { + return promptRepository.findByIsForCompletedResponseTrue() + .map(promptMapper::toDomain); + } + + @Override + public Optional loadTotalSummaryPrompt() { + return promptRepository.findByIsForTotalSummaryTrue() + .map(promptMapper::toDomain); + } + + @Override + public Optional loadGuidelinePrompt(int level) { + return promptRepository.findByLevelAndIsForGuidelineTrue(level) + .map(promptMapper::toDomain); + } + + @Override + public Optional loadAnswerMetadataPrompt() { + return promptRepository.findByIsForAnswerMetadataTrue() + .map(promptMapper::toDomain); + } + + @Override + public Optional loadSummaryPromptByLevel(int level) { + return promptRepository.findByLevelAndIsForSummaryTrue(level) + .map(promptMapper::toDomain); + } } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatMessageEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatMessageEntity.java index e8f86ec7..8ee92217 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatMessageEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/ChatMessageEntity.java @@ -32,4 +32,6 @@ public class ChatMessageEntity extends BaseTimeEntity { private SenderType senderType; private int level; + + private int detailedLevel; } 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 32c0ae74..d2a4261e 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 @@ -7,6 +7,7 @@ import lombok.experimental.SuperBuilder; import makeus.cmc.malmo.adaptor.out.persistence.entity.BaseTimeEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId; +import makeus.cmc.malmo.domain.value.state.ChatRoomCompletedReason; import makeus.cmc.malmo.domain.value.state.ChatRoomState; import java.time.LocalDateTime; @@ -30,6 +31,8 @@ public class ChatRoomEntity extends BaseTimeEntity { private int level; + private int detailedLevel; + private LocalDateTime lastMessageSentTime; @Column(columnDefinition = "TEXT") @@ -40,4 +43,10 @@ public class ChatRoomEntity extends BaseTimeEntity { @Column(columnDefinition = "TEXT") private String solutionKeyword; + + @Enumerated(EnumType.STRING) + private ChatRoomCompletedReason chatRoomCompletedReason; + + @Column(columnDefinition = "TEXT") + private String counselingType; } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/DetailedPromptEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/DetailedPromptEntity.java new file mode 100644 index 00000000..70883fb7 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/DetailedPromptEntity.java @@ -0,0 +1,38 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity.chat; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import makeus.cmc.malmo.adaptor.out.persistence.entity.BaseTimeEntity; + +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "detailed_prompt") +public class DetailedPromptEntity extends BaseTimeEntity { + + @Column(name = "detailedPromptId") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private int level; + + private int detailedLevel; + + @Column(columnDefinition = "TEXT") + private String content; + + private boolean isForValidation; + + private boolean isForSummary; + + private String metadataTitle; + + private boolean isLastDetailedPrompt; + + private boolean isForGuideline; +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/MemberChatRoomMetadataEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/MemberChatRoomMetadataEntity.java new file mode 100644 index 00000000..7c7c9b5b --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/chat/MemberChatRoomMetadataEntity.java @@ -0,0 +1,34 @@ +package makeus.cmc.malmo.adaptor.out.persistence.entity.chat; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.experimental.SuperBuilder; +import makeus.cmc.malmo.adaptor.out.persistence.entity.BaseTimeEntity; + +@Getter +@SuperBuilder +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Entity +@Table(name = "member_chat_room_metadata") +public class MemberChatRoomMetadataEntity extends BaseTimeEntity { + + @Column(name = "memberChatRoomMetadataId") + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long chatRoomId; + + private Long memberId; + + private int level; + + private int detailedLevel; + + private String title; + + @Column(columnDefinition = "TEXT") + private String summary; +} 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 d4f87084..df0fec95 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 @@ -23,4 +23,16 @@ public class PromptEntity extends BaseTimeEntity { @Column(columnDefinition = "TEXT") private String content; + private boolean isForSystem; + + private boolean isForSummary; + + private boolean isForCompletedResponse; + + private boolean isForTotalSummary; + + private boolean isForGuideline; + + private boolean isForAnswerMetadata; + } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/couple/CoupleEntity.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/couple/CoupleEntity.java index 3adf0b31..72082115 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/couple/CoupleEntity.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/entity/couple/CoupleEntity.java @@ -27,6 +27,9 @@ public class CoupleEntity extends BaseTimeEntity { @Enumerated(EnumType.STRING) private CoupleState coupleState; + @Column(name = "is_start_love_date_updated", nullable = false) + private Boolean isStartLoveDateUpdated = false; + @Embedded @AttributeOverrides({ @AttributeOverride(name = "value", column = @Column(name = "first_member_id")) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatMessageMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatMessageMapper.java index 3abd734a..c433d9f7 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatMessageMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/ChatMessageMapper.java @@ -15,6 +15,7 @@ public ChatMessage toDomain(ChatMessageEntity entity) { entity.getChatRoomEntityId() != null ? ChatRoomId.of(entity.getChatRoomEntityId().getValue()) : null, entity.getLevel(), + entity.getDetailedLevel(), entity.getContent(), entity.getSenderType(), entity.getCreatedAt(), @@ -33,6 +34,7 @@ public ChatMessageEntity toEntity(ChatMessage domain) { .chatRoomEntityId(domain.getChatRoomId() != null ? ChatRoomEntityId.of(domain.getChatRoomId().getValue()) : null) .level(domain.getLevel()) + .detailedLevel(domain.getDetailedLevel()) .content(domain.getContent()) .senderType(domain.getSenderType()) .createdAt(domain.getCreatedAt()) 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 4303063a..4b9ae8ce 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 @@ -17,10 +17,13 @@ public ChatRoom toDomain(ChatRoomEntity entity) { entity.getMemberEntityId() != null ? MemberId.of(entity.getMemberEntityId().getValue()) : null, entity.getChatRoomState(), entity.getLevel(), + entity.getDetailedLevel(), entity.getLastMessageSentTime(), entity.getTotalSummary(), entity.getSituationKeyword(), entity.getSolutionKeyword(), + entity.getChatRoomCompletedReason(), + entity.getCounselingType(), entity.getCreatedAt(), entity.getModifiedAt(), entity.getDeletedAt() @@ -38,9 +41,12 @@ public ChatRoomEntity toEntity(ChatRoom domain) { .chatRoomState(domain.getChatRoomState()) .lastMessageSentTime(domain.getLastMessageSentTime()) .level(domain.getLevel()) + .detailedLevel(domain.getDetailedLevel()) .totalSummary(domain.getTotalSummary()) .situationKeyword(domain.getSituationKeyword()) .solutionKeyword(domain.getSolutionKeyword()) + .chatRoomCompletedReason(domain.getChatRoomCompletedReason()) + .counselingType(domain.getCounselingType()) .createdAt(domain.getCreatedAt()) .modifiedAt(domain.getModifiedAt()) .deletedAt(domain.getDeletedAt()) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/CoupleAggregateMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/CoupleAggregateMapper.java index 81b50756..22279f0d 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/CoupleAggregateMapper.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/CoupleAggregateMapper.java @@ -29,7 +29,8 @@ public Couple toDomain(CoupleEntity entity) { secondSnapshot, entity.getCreatedAt(), entity.getModifiedAt(), - entity.getDeletedAt() + entity.getDeletedAt(), + entity.getIsStartLoveDateUpdated() ); } @@ -44,6 +45,7 @@ public CoupleEntity toEntity(Couple domain) { .id(domain.getId()) .startLoveDate(domain.getStartLoveDate()) .coupleState(domain.getCoupleState()) + .isStartLoveDateUpdated(domain.getIsStartLoveDateUpdated()) .firstMemberId(domain.getFirstMemberId() != null ? MemberEntityId.of(domain.getFirstMemberId().getValue()) : null) .secondMemberId(domain.getSecondMemberId() != null ? MemberEntityId.of(domain.getSecondMemberId().getValue()) : null) .firstMemberSnapshot(firstSnapshotEntity) diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/DetailedPromptMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/DetailedPromptMapper.java new file mode 100644 index 00000000..999e0a64 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/DetailedPromptMapper.java @@ -0,0 +1,43 @@ +package makeus.cmc.malmo.adaptor.out.persistence.mapper; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.DetailedPromptEntity; +import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; +import org.springframework.stereotype.Component; + +@Component +public class DetailedPromptMapper { + + public DetailedPrompt toDomain(DetailedPromptEntity entity) { + return DetailedPrompt.from( + entity.getId(), + entity.getLevel(), + entity.getDetailedLevel(), + entity.getContent(), + entity.isForValidation(), + entity.isForSummary(), + entity.getMetadataTitle(), + entity.isLastDetailedPrompt(), + entity.isForGuideline(), + entity.getCreatedAt(), + entity.getModifiedAt(), + entity.getDeletedAt() + ); + } + + public DetailedPromptEntity toEntity(DetailedPrompt domain) { + return DetailedPromptEntity.builder() + .id(domain.getId()) + .level(domain.getLevel()) + .detailedLevel(domain.getDetailedLevel()) + .content(domain.getContent()) + .isForValidation(domain.isForValidation()) + .isForSummary(domain.isForSummary()) + .metadataTitle(domain.getMetadataTitle()) + .isLastDetailedPrompt(domain.isLastDetailedPrompt()) + .isForGuideline(domain.isForGuideline()) + .createdAt(domain.getCreatedAt()) + .modifiedAt(domain.getModifiedAt()) + .deletedAt(domain.getDeletedAt()) + .build(); + } +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberChatRoomMetadataMapper.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberChatRoomMetadataMapper.java new file mode 100644 index 00000000..f1b1d0ba --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/mapper/MemberChatRoomMetadataMapper.java @@ -0,0 +1,37 @@ +package makeus.cmc.malmo.adaptor.out.persistence.mapper; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.MemberChatRoomMetadataEntity; +import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +import makeus.cmc.malmo.domain.value.id.ChatRoomId; +import makeus.cmc.malmo.domain.value.id.MemberId; +import org.springframework.stereotype.Component; + +@Component +public class MemberChatRoomMetadataMapper { + + public MemberChatRoomMetadata toDomain(MemberChatRoomMetadataEntity entity) { + return MemberChatRoomMetadata.from( + entity.getId(), + ChatRoomId.of(entity.getChatRoomId()), + MemberId.of(entity.getMemberId()), + entity.getLevel(), + entity.getDetailedLevel(), + entity.getTitle(), + entity.getSummary(), + entity.getCreatedAt() + ); + } + + public MemberChatRoomMetadataEntity toEntity(MemberChatRoomMetadata domain) { + return MemberChatRoomMetadataEntity.builder() + .id(domain.getId()) + .chatRoomId(domain.getChatRoomId().getValue()) + .memberId(domain.getMemberId().getValue()) + .level(domain.getLevel()) + .detailedLevel(domain.getDetailedLevel()) + .title(domain.getTitle()) + .summary(domain.getSummary()) + .createdAt(domain.getCreatedAt()) + .build(); + } +} 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 37f54ed7..1a69c5ed 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 @@ -16,6 +16,12 @@ public Prompt toDomain(PromptEntity entity) { entity.getId(), entity.getLevel(), entity.getContent(), + entity.isForSystem(), + entity.isForSummary(), + entity.isForCompletedResponse(), + entity.isForTotalSummary(), + entity.isForGuideline(), + entity.isForAnswerMetadata(), entity.getCreatedAt(), entity.getModifiedAt(), entity.getDeletedAt() @@ -31,6 +37,12 @@ public PromptEntity toEntity(Prompt domain) { .id(domain.getId()) .level(domain.getLevel()) .content(domain.getContent()) + .isForSystem(domain.isForSystem()) + .isForSummary(domain.isForSummary()) + .isForCompletedResponse(domain.isForCompletedResponse()) + .isForTotalSummary(domain.isForTotalSummary()) + .isForGuideline(domain.isForGuideline()) + .isForAnswerMetadata(domain.isForAnswerMetadata()) .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 2f2ce3c7..dce3deea 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 @@ -5,9 +5,13 @@ import org.springframework.data.jpa.repository.Query; import java.util.List; +import java.util.Optional; public interface ChatMessageRepository extends JpaRepository, ChatMessageRepositoryCustom { @Query("SELECT c FROM ChatMessageEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level") List findByChatRoomIdAndLevel(Long chatRoomId, int level); + + @Query("SELECT c FROM ChatMessageEntity c WHERE c.chatRoomEntityId.value = :chatRoomId AND c.level = :level AND c.detailedLevel = :detailedLevel") + List findByChatRoomIdAndLevelAndDetailedLevel(Long chatRoomId, int level, int detailedLevel); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/DetailedPromptRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/DetailedPromptRepository.java new file mode 100644 index 00000000..ede776c5 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/DetailedPromptRepository.java @@ -0,0 +1,30 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository.chat; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.DetailedPromptEntity; +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 DetailedPromptRepository extends JpaRepository { + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level AND dp.detailedLevel = :detailedLevel") + Optional findByLevelAndDetailedLevel(@Param("level") int level, @Param("detailedLevel") int detailedLevel); + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level AND dp.detailedLevel = :detailedLevel AND dp.isForValidation = true") + Optional findByLevelAndDetailedLevelAndIsForValidation(@Param("level") int level, @Param("detailedLevel") int detailedLevel); + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level AND dp.detailedLevel = :detailedLevel AND dp.isForSummary = true") + Optional findByLevelAndDetailedLevelAndIsForSummary(@Param("level") int level, @Param("detailedLevel") int detailedLevel); + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level AND dp.detailedLevel = :detailedLevel AND dp.isLastDetailedPrompt = true") + Optional findByLevelAndDetailedLevelAndIsLastDetailedPrompt(@Param("level") int level, @Param("detailedLevel") int detailedLevel); + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level ORDER BY dp.detailedLevel") + List findByLevelOrderByDetailedLevel(@Param("level") int level); + + @Query("SELECT dp FROM DetailedPromptEntity dp WHERE dp.level = :level AND dp.detailedLevel = :detailedLevel AND dp.isForGuideline = true") + Optional findByLevelAndDetailedLevelAndIsForGuidelineTrue(@Param("level") int level, @Param("detailedLevel") int detailedLevel); +} diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/MemberChatRoomMetadataRepository.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/MemberChatRoomMetadataRepository.java new file mode 100644 index 00000000..89512637 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/chat/MemberChatRoomMetadataRepository.java @@ -0,0 +1,17 @@ +package makeus.cmc.malmo.adaptor.out.persistence.repository.chat; + +import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.MemberChatRoomMetadataEntity; +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; + +public interface MemberChatRoomMetadataRepository extends JpaRepository { + + @Query("SELECT m FROM MemberChatRoomMetadataEntity m WHERE m.chatRoomId = :chatRoomId ORDER BY m.level, m.detailedLevel") + List findAllByChatRoomId(@Param("chatRoomId") Long chatRoomId); + + @Query("SELECT m FROM MemberChatRoomMetadataEntity m WHERE m.chatRoomId = :chatRoomId AND m.level = :level AND m.detailedLevel = :detailedLevel") + List findByChatRoomIdAndLevelAndDetailedLevel(@Param("chatRoomId") Long chatRoomId, @Param("level") int level, @Param("detailedLevel") int detailedLevel); +} 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 590a856b..02fa0fb9 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 @@ -3,6 +3,7 @@ import makeus.cmc.malmo.adaptor.out.persistence.entity.chat.PromptEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.Optional; @@ -10,4 +11,25 @@ public interface PromptRepository extends JpaRepository { @Query("SELECT p FROM PromptEntity p WHERE p.level = :level") Optional findByLevel(int level); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForSystem = true") + Optional findByIsForSystemTrue(); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForSummary = true") + Optional findByIsForSummaryTrue(); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForCompletedResponse = true") + Optional findByIsForCompletedResponseTrue(); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForTotalSummary = true") + Optional findByIsForTotalSummaryTrue(); + + @Query("SELECT p FROM PromptEntity p WHERE p.level = :level AND p.isForGuideline = true") + Optional findByLevelAndIsForGuidelineTrue(@Param("level") int level); + + @Query("SELECT p FROM PromptEntity p WHERE p.isForAnswerMetadata = true") + Optional findByIsForAnswerMetadataTrue(); + + @Query("select p from PromptEntity p where p.level = ?1 and p.isForSummary = true") + Optional findByLevelAndIsForSummaryTrue(int level); } diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/couple/CoupleRepositoryCustomImpl.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/couple/CoupleRepositoryCustomImpl.java index 5efdb0b0..be79e62d 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/couple/CoupleRepositoryCustomImpl.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/couple/CoupleRepositoryCustomImpl.java @@ -33,7 +33,11 @@ public Optional findCoupleByMemberIdAndPartnerId(Long memberId, Lo .and(coupleEntity.secondMemberId.value.eq(partnerId))) .or(coupleEntity.firstMemberId.value.eq(partnerId) .and(coupleEntity.secondMemberId.value.eq(memberId))) + .and(coupleEntity.coupleState.eq(makeus.cmc.malmo.domain.value.state.CoupleState.DELETED)) + .and(coupleEntity.deletedAt.goe(java.time.LocalDateTime.now().minusDays(30))) ) + .orderBy(coupleEntity.deletedAt.desc()) + .limit(1) .fetchOne(); return Optional.ofNullable(result); diff --git a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/member/MemberRepositoryCustomImpl.java b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/member/MemberRepositoryCustomImpl.java index 38796eb9..e88ae1cc 100644 --- a/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/member/MemberRepositoryCustomImpl.java +++ b/src/main/java/makeus/cmc/malmo/adaptor/out/persistence/repository/member/MemberRepositoryCustomImpl.java @@ -89,7 +89,8 @@ public Optional findPartner .when(coupleEntity.firstMemberId.value.eq(memberId)) .then(coupleEntity.secondMemberSnapshot.nickname) .otherwise(coupleEntity.firstMemberSnapshot.nickname) - ) + ), + coupleEntity.isStartLoveDateUpdated )) .from(memberEntity) .join(coupleEntity).on(memberEntity.coupleEntityId.value.eq(coupleEntity.id)) 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 e22adfbb..b4967c00 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,6 +12,7 @@ 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; @@ -57,10 +58,6 @@ public Page getCompletedChatRoomsByMemberId(MemberId memberId, String return loadChatRoomPort.loadAliveChatRoomsByMemberId(memberId, keyword, pageable); } - public Optional getPausedChatRoomByMemberId(MemberId memberId) { - return loadChatRoomPort.loadPausedChatRoomByMemberId(memberId); - } - public void validateChatRoomOwnership(MemberId memberId, ChatRoomId chatRoomId) { // 채팅방이 존재하는지 확인하고, 존재하지 않으면 예외 발생 ChatRoom chatRoom = loadChatRoomPort.loadChatRoomById(chatRoomId) @@ -113,6 +110,10 @@ public List getChatRoomLevelMessages(ChatRoomId chatRoomId, int lev return loadMessagesPort.loadChatRoomMessagesByLevel(chatRoomId, level); } + public List getChatRoomLevelAndDetailedLevelMessages(ChatRoomId chatRoomId, int level, int detailedLevel) { + return loadMessagesPort.loadChatRoomLevelAndDetailedLevelMessages(chatRoomId, level, detailedLevel); + } + public List getMemberMemoriesByMemberId(MemberId memberId) { return loadMemberMemoryPort.loadMemberMemoryByMemberId(memberId); } 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 new file mode 100644 index 00000000..fd48e331 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/DetailedPromptQueryHelper.java @@ -0,0 +1,28 @@ +package makeus.cmc.malmo.application.helper.chat_room; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.DetailedPromptRepository; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.DetailedPromptMapper; +import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; + +@Component +@RequiredArgsConstructor +public class DetailedPromptQueryHelper { + + private final DetailedPromptRepository detailedPromptRepository; + private final DetailedPromptMapper detailedPromptMapper; + + public Optional getValidationPrompt(int level, int detailedLevel) { + return detailedPromptRepository.findByLevelAndDetailedLevelAndIsForValidation(level, detailedLevel) + .map(detailedPromptMapper::toDomain); + } + + public Optional getGuidelinePrompt(int level, int detailedLevel) { + return detailedPromptRepository.findByLevelAndDetailedLevelAndIsForGuidelineTrue(level, detailedLevel) + .map(detailedPromptMapper::toDomain); + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataCommandHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataCommandHelper.java new file mode 100644 index 00000000..190ad028 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataCommandHelper.java @@ -0,0 +1,21 @@ +package makeus.cmc.malmo.application.helper.chat_room; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.MemberChatRoomMetadataRepository; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.MemberChatRoomMetadataMapper; +import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class MemberChatRoomMetadataCommandHelper { + + private final MemberChatRoomMetadataRepository memberChatRoomMetadataRepository; + private final MemberChatRoomMetadataMapper memberChatRoomMetadataMapper; + + public MemberChatRoomMetadata saveMemberChatRoomMetadata(MemberChatRoomMetadata memberChatRoomMetadata) { + var entity = memberChatRoomMetadataMapper.toEntity(memberChatRoomMetadata); + var savedEntity = memberChatRoomMetadataRepository.save(entity); + return memberChatRoomMetadataMapper.toDomain(savedEntity); + } +} diff --git a/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataQueryHelper.java new file mode 100644 index 00000000..26d7af6a --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/helper/chat_room/MemberChatRoomMetadataQueryHelper.java @@ -0,0 +1,32 @@ +package makeus.cmc.malmo.application.helper.chat_room; + +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.out.persistence.repository.chat.MemberChatRoomMetadataRepository; +import makeus.cmc.malmo.adaptor.out.persistence.mapper.MemberChatRoomMetadataMapper; +import makeus.cmc.malmo.domain.model.chat.MemberChatRoomMetadata; +import makeus.cmc.malmo.domain.value.id.ChatRoomId; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class MemberChatRoomMetadataQueryHelper { + + private final MemberChatRoomMetadataRepository memberChatRoomMetadataRepository; + private final MemberChatRoomMetadataMapper memberChatRoomMetadataMapper; + + public List getMemberChatRoomMetadata(ChatRoomId chatRoomId) { + return memberChatRoomMetadataRepository.findAllByChatRoomId(chatRoomId.getValue()) + .stream() + .map(memberChatRoomMetadataMapper::toDomain) + .toList(); + } + + public List getMemberChatRoomMetadata(ChatRoomId chatRoomId, int level, int detailedLevel) { + return memberChatRoomMetadataRepository.findByChatRoomIdAndLevelAndDetailedLevel(chatRoomId.getValue(), level, detailedLevel) + .stream() + .map(memberChatRoomMetadataMapper::toDomain) + .toList(); + } +} 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 7554b15d..d7fc28da 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,35 +6,40 @@ import makeus.cmc.malmo.domain.model.chat.Prompt; import org.springframework.stereotype.Component; -import static makeus.cmc.malmo.util.GlobalConstants.*; @Component @RequiredArgsConstructor public class PromptQueryHelper { private final LoadPromptPort loadPromptPort; - public Prompt getPromptByLevel(int level) { - return loadPromptPort.loadPromptByLevel(level) + public Prompt getSystemPrompt() { + return loadPromptPort.loadSystemPrompt() .orElseThrow(PromptNotFoundException::new); } - public Prompt getSystemPrompt() { - return loadPromptPort.loadPromptByLevel(SYSTEM_PROMPT_LEVEL) + @Deprecated + public Prompt getSummaryPrompt() { + return loadPromptPort.loadSummaryPrompt() .orElseThrow(PromptNotFoundException::new); } - public Prompt getSummaryPrompt() { - return loadPromptPort.loadPromptByLevel(SUMMARY_PROMPT_LEVEL) + public Prompt getSummaryPrompt(int level) { + return loadPromptPort.loadSummaryPromptByLevel(level) .orElseThrow(PromptNotFoundException::new); } public Prompt getTotalSummaryPrompt() { - return loadPromptPort.loadPromptByLevel(TOTAL_SUMMARY_PROMPT_LEVEL) + return loadPromptPort.loadTotalSummaryPrompt() + .orElseThrow(PromptNotFoundException::new); + } + + public Prompt getGuidelinePrompt(int level) { + return loadPromptPort.loadGuidelinePrompt(level) .orElseThrow(PromptNotFoundException::new); } - public Prompt getMemberAnswerMetadata() { - return loadPromptPort.loadPromptByLevel(EXTRACT_METADATA_PROMPT) + public Prompt getAnswerMetadataPrompt() { + return loadPromptPort.loadAnswerMetadataPrompt() .orElseThrow(PromptNotFoundException::new); } } diff --git a/src/main/java/makeus/cmc/malmo/application/helper/member/MemberQueryHelper.java b/src/main/java/makeus/cmc/malmo/application/helper/member/MemberQueryHelper.java index 0ac6b078..efad6a7d 100644 --- a/src/main/java/makeus/cmc/malmo/application/helper/member/MemberQueryHelper.java +++ b/src/main/java/makeus/cmc/malmo/application/helper/member/MemberQueryHelper.java @@ -109,6 +109,7 @@ public static class PartnerMemberDto { private float avoidanceRate; private float anxietyRate; private String nickname; + private Boolean isStartLoveDateUpdated; } } 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 7c788762..90a72df9 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,7 +8,6 @@ public interface ProcessMessageUseCase { CompletableFuture processStreamChatMessage(ProcessMessageCommand command); - CompletableFuture processSummary(ProcessSummaryCommand command); CompletableFuture processTotalSummary(ProcessTotalSummaryCommand command); CompletableFuture processAnswerMetadata(ProcessAnswerCommand command); @@ -19,13 +18,7 @@ class ProcessMessageCommand { private Long chatRoomId; private String nowMessage; private int promptLevel; - } - - @Data - @Builder - class ProcessSummaryCommand { - private Long chatRoomId; - private Integer promptLevel; + private int detailedLevel; } @Data 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 8fe29761..7f3d1fc6 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 @@ -6,7 +6,7 @@ public interface SendChatMessageUseCase { SendChatMessageResponse processUserMessage(SendChatMessageCommand command); - void upgradeChatRoom(SendChatMessageCommand command); +// void upgradeChatRoom(SendChatMessageCommand command); @Data @Builder diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/chat/SufficiencyCheckResult.java b/src/main/java/makeus/cmc/malmo/application/port/in/chat/SufficiencyCheckResult.java new file mode 100644 index 00000000..fcad979b --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/in/chat/SufficiencyCheckResult.java @@ -0,0 +1,16 @@ +package makeus.cmc.malmo.application.port.in.chat; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SufficiencyCheckResult { + private boolean completed; + private String summary; + private String advice; +} diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetPartnerUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetPartnerUseCase.java index 2a6c989f..ae2c7776 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/GetPartnerUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/GetPartnerUseCase.java @@ -23,5 +23,6 @@ class PartnerMemberResponseDto { private float avoidanceRate; private float anxietyRate; private String nickname; + private Boolean isStartLoveDateUpdated; } } diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/SignInUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/SignInUseCase.java index 327013eb..a000e171 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/SignInUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/SignInUseCase.java @@ -13,6 +13,7 @@ public interface SignInUseCase { class SignInKakaoCommand { private String idToken; private String accessToken; + private String deviceId; } @Data @@ -20,6 +21,7 @@ class SignInKakaoCommand { class SignInAppleCommand { private String idToken; private String authorizationCode; + private String deviceId; } @Data diff --git a/src/main/java/makeus/cmc/malmo/application/port/in/member/SignUpUseCase.java b/src/main/java/makeus/cmc/malmo/application/port/in/member/SignUpUseCase.java index 44e791b3..67ba62f0 100644 --- a/src/main/java/makeus/cmc/malmo/application/port/in/member/SignUpUseCase.java +++ b/src/main/java/makeus/cmc/malmo/application/port/in/member/SignUpUseCase.java @@ -3,7 +3,6 @@ import lombok.Builder; import lombok.Data; -import java.time.LocalDate; import java.util.List; public interface SignUpUseCase { @@ -16,7 +15,6 @@ class SignUpCommand { private Long memberId; private List terms; private String nickname; - private LocalDate loveStartDate; private Long loveTypeId; } diff --git a/src/main/java/makeus/cmc/malmo/application/port/out/amplitude/AmplitudePort.java b/src/main/java/makeus/cmc/malmo/application/port/out/amplitude/AmplitudePort.java new file mode 100644 index 00000000..64fd7fe4 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/application/port/out/amplitude/AmplitudePort.java @@ -0,0 +1,18 @@ +package makeus.cmc.malmo.application.port.out.amplitude; + +import lombok.Builder; +import lombok.Data; + +public interface AmplitudePort { + + void identifyUser(IdentifyUserCommand command); + + @Data + @Builder + class IdentifyUserCommand { + private String userId; + private String deviceId; + private String email; + private String nickname; + } +} 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 70f37b96..31a170fe 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 @@ -16,6 +16,8 @@ public interface LoadMessagesPort { Page loadMessagesDtoAsc(ChatRoomId chatRoomId, Pageable pageable); List loadChatRoomMessagesByLevel(ChatRoomId chatRoomId, int level); + List loadChatRoomLevelAndDetailedLevelMessages(ChatRoomId chatRoomId, int level, int detailedLevel); + @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 ca720030..d392c885 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 @@ -6,4 +6,19 @@ public interface LoadPromptPort { Optional loadPromptByLevel(int level); + + Optional loadSystemPrompt(); + + @Deprecated + Optional loadSummaryPrompt(); + + Optional loadCompletedResponsePrompt(); + + Optional loadTotalSummaryPrompt(); + + Optional loadGuidelinePrompt(int level); + + Optional loadAnswerMetadataPrompt(); + + Optional loadSummaryPromptByLevel(int level); } 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 bd4e3329..232685fb 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 @@ -5,17 +5,22 @@ 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.application.port.out.member.ValidateMemberPort; 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.domain.model.member.Member; import makeus.cmc.malmo.domain.model.member.MemberMemory; import makeus.cmc.malmo.domain.model.question.CoupleQuestion; @@ -30,7 +35,6 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicBoolean; @Slf4j @Service @@ -40,9 +44,10 @@ public class ChatMessageService implements ProcessMessageUseCase { private final MemberQueryHelper memberQueryHelper; private final ChatRoomQueryHelper chatRoomQueryHelper; private final PromptQueryHelper promptQueryHelper; + private final DetailedPromptQueryHelper detailedPromptQueryHelper; + private final MemberChatRoomMetadataCommandHelper memberChatRoomMetadataCommandHelper; private final ChatPromptBuilder chatPromptBuilder; private final ChatProcessor chatProcessor; - private final ValidateMemberPort validateMemberPort; private final ChatSseSender chatSseSender; private final ChatRoomCommandHelper chatRoomCommandHelper; @@ -59,53 +64,43 @@ public CompletableFuture processStreamChatMessage(ProcessMessageCommand co Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(ChatRoomId.of(command.getChatRoomId())); - List> messages = chatPromptBuilder.createForProcessUserMessage(member, chatRoom, command.getNowMessage()); + // 1. 유저 메시지 저장 +// saveUserMessage(chatRoom, command); - Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt prompt = promptQueryHelper.getPromptByLevel(command.getPromptLevel()); - boolean isMemberCouple = validateMemberPort.isCoupleMember(memberId); - - AtomicBoolean isOkDetected = new AtomicBoolean(false); - - return chatProcessor.streamChat(messages, systemPrompt, prompt, - chunk -> { - // 실시간 SSE 전송 로직 (onChunk) - if (chunk.contains("OK")) { - isOkDetected.set(true); - } else { - chatSseSender.sendResponseChunk(memberId, chunk); - } - }, - fullAnswer -> { - // 스트림 완료 후 DB 저장 및 후처리 로직 (onComplete) - if (!isOkDetected.get()) { - saveAiMessage(memberId, ChatRoomId.of(chatRoom.getId()), prompt.getLevel(), fullAnswer); - } else { - fullAnswer = fullAnswer.replace("OK", "").trim(); - saveAiMessage(memberId, ChatRoomId.of(chatRoom.getId()), prompt.getLevel(), fullAnswer); - handleLevelFinished(memberId, ChatRoomId.of(chatRoom.getId()), prompt, isMemberCouple); - } - }, - errorMessage -> chatSseSender.sendError(memberId, errorMessage) - ).toFuture(); - } + // 2. 충분성 조건 검사 + CompletableFuture sufficiencyCheck = + requestSufficiencyCheck(member, chatRoom, command); - @Override - public CompletableFuture processSummary(ProcessSummaryCommand command) { - ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(ChatRoomId.of(command.getChatRoomId())); - Prompt systemPrompt = promptQueryHelper.getSystemPrompt(); - Prompt prompt = promptQueryHelper.getPromptByLevel(chatRoom.getLevel()); - Prompt summaryPrompt = promptQueryHelper.getSummaryPrompt(); + return sufficiencyCheck.thenCompose(result -> { + if (!result.isCompleted()) { + // 조건 미충족: advice 포함하여 응답 생성 + return requestResponseToMeetCondition(member, chatRoom, command, result.getAdvice()); + } + + // 조건 충족: MemberChatRoomMetadata 저장 + 요약 요청 + saveMemberChatRoomMetadata(chatRoom, command, result); - // 현재 단계 채팅에 대한 전체 요약 요청 - List> summaryMessages = chatPromptBuilder.createForSummaryAsync(chatRoom); - return chatProcessor.requestSummaryAsync(summaryMessages, systemPrompt, prompt, summaryPrompt) - .thenAcceptAsync(summary -> { // 비동기 작업이 끝나면 이 블록이 실행됨 - ChatMessageSummary chatMessageSummary = ChatMessageSummary.createChatMessageSummary( - ChatRoomId.of(chatRoom.getId()), summary, prompt.getLevel()); - saveChatMessageSummaryPort.saveChatMessageSummary(chatMessageSummary); - log.info("Successfully processed and saved summary for chatRoomId: {}", command.getChatRoomId()); - }); // 별도의 스레드 풀에서 실행되도록 thenAcceptAsync 사용 + DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePrompt( + command.getPromptLevel(), command.getDetailedLevel()) + .orElseThrow(() -> new RuntimeException("Guideline prompt not found")); + + // 마지막 충분성 조건이 아닌 경우 + if (!detailedPrompt.isLastDetailedPrompt()) { + // 다음 충분성 조건으로 + chatRoom.upgradeDetailedLevel(); + chatRoomCommandHelper.saveChatRoom(chatRoom); + return requestNextDetailedPromptOpening(chatRoom, command); + } + + // 마지막 충분성 조건인 경우 + // 단계 요약 요청 (비동기) + requestStageSummaryAsync(chatRoom, command); + + // 다음 단계 오프닝 생성 요청 + chatRoom.upgradeToNextStage(); + chatRoomCommandHelper.saveChatRoom(chatRoom); + return requestNextStageOpening(member, chatRoom, command); + }); } @Override @@ -122,7 +117,8 @@ public CompletableFuture processTotalSummary(ProcessTotalSummaryCommand co chatRoom.updateChatRoomSummary( summary.getTotalSummary(), summary.getSituationKeyword(), - summary.getSolutionKeyword() + summary.getSolutionKeyword(), + summary.getCounselingType() ); chatRoomCommandHelper.saveChatRoom(chatRoom); log.info("Successfully processed and saved total summary for chatRoomId: {}", command.getChatRoomId()); @@ -139,7 +135,7 @@ public CompletableFuture processAnswerMetadata(ProcessAnswerCommand comman CoupleQuestion coupleQuestion = coupleQuestionQueryHelper.getCoupleQuestionByIdOrThrow( CoupleQuestionId.of(command.getCoupleQuestionId())); - Prompt metadataPrompt = promptQueryHelper.getMemberAnswerMetadata(); + Prompt metadataPrompt = promptQueryHelper.getAnswerMetadataPrompt(); // 2. 비동기 API 호출로 CompletableFuture 체인을 시작하고, 그 결과를 즉시 반환합니다. return chatProcessor.requestMetaData( @@ -159,35 +155,139 @@ public CompletableFuture processAnswerMetadata(ProcessAnswerCommand comman }); } - /* - - 1단계의 경우 커플 연동이 되지 않은 상태에서 대화가 종료되면 채팅방 상태를 일시정지로 변경하고 커플 연동을 요청 - - 2단계 이상에서는 현재 단계가 완료되었다는 메시지를 전송 - */ - private void handleLevelFinished(MemberId memberId, ChatRoomId chatRoomId, Prompt prompt, boolean isMemberCoupled) { - if (prompt.isLastPromptForNotCoupleMember() && !isMemberCoupled) { - // 커플 연동이 되지 않은 상태에서 1단계가 종료된 경우 - // 채팅방 상태를 일시정지로 변경하고 커플 연동 요청 - ChatRoom chatRoom = chatRoomQueryHelper.getChatRoomByIdOrThrow(chatRoomId); - chatRoom.updateChatRoomStatePaused(); - chatRoomCommandHelper.saveChatRoom(chatRoom); - chatSseSender.sendFlowEvent( - memberId, - SendSseEventPort.SseEventType.CHAT_ROOM_PAUSED, - "커플 연동 전 대화가 종료되었습니다. 커플 연동을 해주세요." - ); - } else { - // 2단계 이상이거나 커플 연동이 된 상태에서 현재 단계가 완료된 경우 - // 현재 단계가 완료되었다는 메시지를 전송 - chatSseSender.sendFlowEvent( - memberId, - SendSseEventPort.SseEventType.CURRENT_LEVEL_FINISHED, - "현재 단계가 완료되었습니다. upgrade를 요청해주세요." - ); + private void saveUserMessage(ChatRoom chatRoom, ProcessMessageCommand command) { + ChatMessage userMessage = chatRoomDomainService.createUserMessage( + ChatRoomId.of(chatRoom.getId()), + command.getPromptLevel(), + command.getDetailedLevel(), + command.getNowMessage() + ); + chatRoomCommandHelper.saveChatMessage(userMessage); + } + + private CompletableFuture requestSufficiencyCheck(Member member, ChatRoom chatRoom, ProcessMessageCommand command) { + List> messages = chatPromptBuilder.createForSufficiencyCheck( + member, chatRoom, command.getPromptLevel(), command.getDetailedLevel()); + + log.info("Requesting sufficiency check for memberId: {}, chatRoomId: {}, level: {}, detailedLevel: {}", + member.getId(), chatRoom.getId(), command.getPromptLevel(), command.getDetailedLevel()); + DetailedPrompt validationPrompt = detailedPromptQueryHelper.getValidationPrompt( + command.getPromptLevel(), command.getDetailedLevel()) + .orElseThrow(() -> new RuntimeException("Validation prompt not found")); + + return chatProcessor.requestSufficiencyCheck(messages, validationPrompt); + } + + private CompletableFuture requestResponseToMeetCondition(Member member, ChatRoom chatRoom, ProcessMessageCommand command, String advice) { + List> messages = chatPromptBuilder.createForProcessUserMessage( + member, chatRoom, command.getNowMessage()); + + // advice를 포함한 프롬프트 추가 + if (advice != null && !advice.isEmpty()) { + messages.add(Map.of("role", "system", "content", "상담 검수자의 Advice: " + advice)); } + + 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")); + + 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 void saveMemberChatRoomMetadata(ChatRoom chatRoom, ProcessMessageCommand command, SufficiencyCheckResult result) { + DetailedPrompt detailedPrompt = detailedPromptQueryHelper.getGuidelinePrompt( + command.getPromptLevel(), command.getDetailedLevel()) + .orElseThrow(() -> new RuntimeException("Guideline prompt not found")); + MemberChatRoomMetadata metadata = MemberChatRoomMetadata.create( + ChatRoomId.of(chatRoom.getId()), + MemberId.of(command.getMemberId()), + command.getPromptLevel(), + command.getDetailedLevel(), + detailedPrompt.getMetadataTitle(), + result.getSummary() + ); + memberChatRoomMetadataCommandHelper.saveMemberChatRoomMetadata(metadata); + } + + private CompletableFuture requestNextDetailedPromptOpening(ChatRoom chatRoom, ProcessMessageCommand command) { + // 다음 충분성 조건 오프닝 생성 요청 + MemberId memberId = MemberId.of(command.getMemberId()); + Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); + + // 사용자 메타데이터 정보 생성 + List> messages = chatPromptBuilder.createForNextDetailedPrompt( + member, chatRoom, command.getPromptLevel(), command.getDetailedLevel() + 1); + + // 시스템 프롬프트 + 현재 단계 프롬프트 + 다음 충분성 조건 프롬프트 + 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")); + + return chatProcessor.streamChat(messages, systemPrompt, prompt, nextDetailedPrompt, + chunk -> chatSseSender.sendResponseChunk(memberId, chunk), + fullAnswer -> saveAiMessage(memberId, ChatRoomId.of(chatRoom.getId()), + command.getPromptLevel(), command.getDetailedLevel() + 1, fullAnswer), + errorMessage -> chatSseSender.sendError(memberId, errorMessage) + ).toFuture(); + } + + private void requestStageSummaryAsync(ChatRoom chatRoom, ProcessMessageCommand command) { + List> messages = chatPromptBuilder.createForStageSummary( + chatRoom, command.getPromptLevel()); + + 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); + + 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")); + + 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), + errorMessage -> chatSseSender.sendError(MemberId.of(member.getId()), errorMessage) + ).toFuture(); } - private void saveAiMessage(MemberId memberId, ChatRoomId chatRoomId, int level, String fullAnswer) { - ChatMessage aiTextMessage = chatRoomDomainService.createAiMessage(chatRoomId, level, fullAnswer); + private void saveAiMessage(MemberId memberId, ChatRoomId chatRoomId, int level, int detailedLevel, String fullAnswer) { + ChatMessage aiTextMessage = chatRoomDomainService.createAiMessage(chatRoomId, level, detailedLevel, fullAnswer); ChatMessage savedMessage = chatRoomCommandHelper.saveChatMessage(aiTextMessage); chatSseSender.sendAiResponseId(memberId, savedMessage.getId()); } 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 16276db8..735e3700 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 @@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.application.port.out.chat.RequestChatApiPort; +import makeus.cmc.malmo.application.port.in.chat.SufficiencyCheckResult; +import makeus.cmc.malmo.domain.model.chat.DetailedPrompt; import makeus.cmc.malmo.domain.model.chat.Prompt; import makeus.cmc.malmo.domain.value.type.SenderType; import org.springframework.stereotype.Service; @@ -29,17 +31,22 @@ public class ChatProcessor { public Mono streamChat(List> messages, Prompt systemPrompt, Prompt prompt, + DetailedPrompt detailedPrompt, Consumer onChunk, Consumer onComplete, Consumer onError) { messages.add(createMessageMap(SenderType.SYSTEM, systemPrompt.getContent())); messages.add(createMessageMap(SenderType.SYSTEM, prompt.getContent())); + messages.add(createMessageMap(SenderType.SYSTEM, detailedPrompt.getContent())); + + log.info("Starting streamChat with messages: {}", messages); return requestChatApiPort.requestStreamResponse(messages, onChunk) // onChunk 콜백만 넘김 .flatMap(fullAnswer -> { // 스트림이 성공적으로 완료되고 전체 응답(fullAnswer)이 오면 onComplete 로직 실행 onComplete.accept(fullAnswer); + log.info("Stream completed with full answer: {}", fullAnswer); return Mono.empty(); // 성공적으로 완료했음을 알리기 위해 비어있는 Mono 반환 }) .doOnError(throwable -> onError.accept(throwable.getMessage())) // 에러 발생 시 onError 콜백 실행 @@ -91,6 +98,40 @@ public CompletableFuture requestMetaData(String question, return requestChatApiPort.requestResponse(messages); } + public CompletableFuture requestSufficiencyCheck(List> messages, + DetailedPrompt validationPrompt) { + messages.add(createMessageMap(SenderType.SYSTEM, validationPrompt.getContent())); + + log.info("Requesting sufficiency check with messages: {}", messages); + + return requestChatApiPort.requestJsonResponse(messages) + .thenApply(jsonResponse -> { + try { + log.info("Received sufficiency check JSON: {}", jsonResponse); + return objectMapper.readValue(jsonResponse, SufficiencyCheckResult.class); + } catch (JsonProcessingException e) { + log.error("Failed to parse sufficiency check JSON: {}", jsonResponse, e); + throw new RuntimeException("Failed to parse sufficiency check JSON", e); + } + }); + } + + public CompletableFuture requestDetailedSummary(List> messages, + DetailedPrompt summaryPrompt) { + messages.add(createMessageMap(SenderType.SYSTEM, summaryPrompt.getContent())); + 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())); + messages.add(createMessageMap(SenderType.SYSTEM, summaryPrompt.getContent())); + return requestChatApiPort.requestResponse(messages); + } + private Map createMessageMap(SenderType senderType, String content) { return Map.of( "role", senderType.getApiName(), @@ -105,5 +146,6 @@ 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 7ed07d55..f1471cdb 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 @@ -2,10 +2,12 @@ import lombok.RequiredArgsConstructor; import makeus.cmc.malmo.application.helper.chat_room.ChatRoomQueryHelper; +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.domain.model.member.Member; import makeus.cmc.malmo.domain.model.member.MemberMemory; import makeus.cmc.malmo.domain.service.MemberDomainService; @@ -24,6 +26,7 @@ public class ChatPromptBuilder { private final MemberDomainService memberDomainService; private final ChatRoomQueryHelper chatRoomQueryHelper; + private final MemberChatRoomMetadataQueryHelper memberChatRoomMetadataQueryHelper; public List> createForProcessUserMessage(Member member, ChatRoom chatRoom, String userMessage) { List> messages = new ArrayList<>(); @@ -122,8 +125,8 @@ private String getMetaDataContent(Member member) { metadataBuilder.append("[사용자 메타데이터] \n"); String nickname = member.getNickname(); metadataBuilder.append("- 사용자 이름: ").append(nickname).append("\n"); - String dDayState = memberDomainService.getMemberDDayState(member.getStartLoveDate()); - metadataBuilder.append("- 연애 기간: ").append(dDayState).append("\n"); +// String dDayState = memberDomainService.getMemberDDayState(member.getStartLoveDate()); +// metadataBuilder.append("- 연애 기간: ").append(dDayState).append("\n"); LoadChatRoomMetadataPort.ChatRoomMetadataDto chatRoomMetadataDto = chatRoomQueryHelper.getChatRoomMetadata(MemberId.of(member.getId())); String memberLoveTypeTitle = chatRoomMetadataDto.memberLoveType() != null ? chatRoomMetadataDto.memberLoveType().getTitle() : "알 수 없음"; @@ -146,4 +149,114 @@ public String getMemberMemoriesByMemberId(MemberId memberId) { return sb.toString(); } + + public List> createForSufficiencyCheck(Member member, ChatRoom chatRoom, int level, int detailedLevel) { + List> messages = new ArrayList<>(); + + // 1. 사용자 메타데이터 + 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 정보 + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); + } + + // 4. 현재 단계 메시지들 + List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelAndDetailedLevelMessages(ChatRoomId.of(chatRoom.getId()), level, detailedLevel); + for (ChatMessage chatMessage : currentChatRoomMessages) { + messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); + } + + 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<>(); + + // 1. 사용자 메타데이터 + 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 정보 + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); + } + + // 4. 현재 단계 메시지들 (이전 detailedLevel까지) +// List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelAndDetailedLevelMessages(ChatRoomId.of(chatRoom.getId()), level, nextDetailedLevel - 1); + // fixed: 현재 단계 메시지들 context 전체 전달(level 기준) + List currentChatRoomMessages = chatRoomQueryHelper.getChatRoomLevelMessages(ChatRoomId.of(chatRoom.getId()), level); + for (ChatMessage chatMessage : currentChatRoomMessages) { + messages.add(createMessageMap(chatMessage.getSenderType(), chatMessage.getContent())); + } + + return messages; + } + + public List> createForNextStage(Member member, ChatRoom chatRoom, int nextLevel) { + List> messages = new ArrayList<>(); + + // 1. 사용자 메타데이터 + 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 정보 + List metadataList = memberChatRoomMetadataQueryHelper.getMemberChatRoomMetadata(ChatRoomId.of(chatRoom.getId())); + if (!metadataList.isEmpty()) { + String metadataContent = getMemberChatRoomMetadataContent(metadataList); + messages.add(createMessageMap(SenderType.SYSTEM, metadataContent)); + } + + return messages; + } + + private String getMemberChatRoomMetadataContent(List metadataList) { + if (metadataList == null || metadataList.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + sb.append("[사용자의 갈등 내용] \n"); + for (MemberChatRoomMetadata metadata : metadataList) { + sb.append("- ").append(metadata.getTitle()).append(": ").append(metadata.getSummary()).append("\n"); + } + return sb.toString(); + } } 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 cc1e8b40..90fa5995 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 @@ -64,7 +64,7 @@ public GetChatRoomListResponse getChatRoomList(GetChatRoomListCommand command) { .totalSummary(chatRoom.getTotalSummary()) .situationKeyword(chatRoom.getSituationKeyword()) .solutionKeyword(chatRoom.getSolutionKeyword()) - .createdAt(chatRoom.getCreatedAt()) + .createdAt(chatRoom.getLastMessageSentTime()) .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 772f6bab..3e9764ff 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 @@ -3,17 +3,17 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import makeus.cmc.malmo.adaptor.in.aop.CheckValidMember; -import makeus.cmc.malmo.adaptor.message.RequestSummaryMessage; import makeus.cmc.malmo.adaptor.message.StreamChatMessage; 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.application.port.out.chat.PublishStreamMessagePort; 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; @@ -22,8 +22,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import static makeus.cmc.malmo.util.GlobalConstants.FINAL_MESSAGE; -import static makeus.cmc.malmo.util.GlobalConstants.LAST_PROMPT_LEVEL; +import java.util.concurrent.CompletableFuture; + +//import static makeus.cmc.malmo.util.GlobalConstants.FINAL_MESSAGE; @Slf4j @Service @@ -36,6 +37,7 @@ 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; @@ -51,8 +53,10 @@ public SendChatMessageResponse processUserMessage(SendChatMessageCommand command ChatRoom chatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(memberId); // 채팅방의 상담 단계가 마지막인 경우 동일한 메시지를 반복하여 전송 - if (chatRoom.getLevel() == LAST_PROMPT_LEVEL) { - return handleLastPrompt(chatRoom, command.getMessage()); + Prompt prompt = promptQueryHelper.getGuidelinePrompt(chatRoom.getLevel()); + if (prompt.isForCompletedResponse()) { + String finalMessage = prompt.getContent(); + return handleLastPrompt(chatRoom, command.getMessage(), finalMessage); } // 채팅방이 초기화되지 않은 상태인 경우 초기화 @@ -74,7 +78,8 @@ public SendChatMessageResponse processUserMessage(SendChatMessageCommand command member.getId(), chatRoom.getId(), command.getMessage(), - chatRoom.getLevel() + chatRoom.getLevel(), + chatRoom.getDetailedLevel() ) ); @@ -83,66 +88,31 @@ public SendChatMessageResponse processUserMessage(SendChatMessageCommand command .build(); } - @Override - @Transactional - @CheckValidMember - public void upgradeChatRoom(SendChatMessageCommand command) { - // 채팅방의 현재 상담 단계가 완료된 상테 -> 다음 단계로 업그레이드 - MemberId memberId = MemberId.of(command.getUserId()); - Member member = memberQueryHelper.getMemberByIdOrThrow(memberId); - ChatRoom chatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(memberId); - int nowChatRoomLevel = chatRoom.getLevel(); - - // 채팅방 업그레이드 처리 - chatRoom.upgradeChatRoom(); - chatRoomCommandHelper.saveChatRoom(chatRoom); + // upgradeChatRoom 메서드 제거 - 내부 로직으로 통합됨 - // 다음 단계 프롬프트를 통한 AI 응답 요청 스트림에 추가 - if (nowChatRoomLevel + 1 == LAST_PROMPT_LEVEL) { - // 마지막 단계로 업그레이드된 경우, 고정된 메시지를 전송 - chatSseSender.sendLastResponse(chatRoom.getMemberId(), FINAL_MESSAGE); - saveAiMessage(chatRoom.getMemberId(), ChatRoomId.of(chatRoom.getId()), LAST_PROMPT_LEVEL, FINAL_MESSAGE); - } else { - // 다음 단계로 업그레이드된 경우, AI 응답 요청 스트림에 추가 - outboxHelper.publish( - StreamMessageType.REQUEST_CHAT_MESSAGE, - new StreamChatMessage( - member.getId(), - chatRoom.getId(), - "", - nowChatRoomLevel + 1 - ) - ); - } - - // 현재 단계 채팅에 대한 전체 요약 요청 스트림에 추가 - outboxHelper.publish( - StreamMessageType.REQUEST_SUMMARY, - new RequestSummaryMessage(chatRoom.getId(), nowChatRoomLevel) - ); - - // 다음 단계 상담 도달, 채팅방 활성화 - chatRoom.updateChatRoomStateAlive(); - chatRoomCommandHelper.saveChatRoom(chatRoom); - } - - private SendChatMessageResponse handleLastPrompt(ChatRoom chatRoom, String message) { + private SendChatMessageResponse handleLastPrompt(ChatRoom chatRoom, String userMessage, String finalMessage) { // 마지막 단계에서 고정된 메시지를 반복하여 전송 - ChatMessage userMessage = saveUserMessage(chatRoom, message); - chatSseSender.sendLastResponse(chatRoom.getMemberId(), FINAL_MESSAGE); - saveAiMessage(chatRoom.getMemberId(), ChatRoomId.of(chatRoom.getId()), LAST_PROMPT_LEVEL, FINAL_MESSAGE); + 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(userMessage.getId()) + .messageId(savedUserMessage.getId()) .build(); } private ChatMessage saveUserMessage(ChatRoom chatRoom, String message) { - ChatMessage userMessage = chatRoomDomainService.createUserMessage(ChatRoomId.of(chatRoom.getId()), chatRoom.getLevel(), message); + ChatMessage userMessage = chatRoomDomainService.createUserMessage( + ChatRoomId.of(chatRoom.getId()), + chatRoom.getLevel(), + chatRoom.getDetailedLevel(), + message); return chatRoomCommandHelper.saveChatMessage(userMessage); } - private void saveAiMessage(MemberId memberId, ChatRoomId chatRoomId, int level, String fullAnswer) { - ChatMessage aiTextMessage = chatRoomDomainService.createAiMessage(chatRoomId, level, fullAnswer); + private void saveAiMessage(MemberId memberId, ChatRoomId chatRoomId, int level, int detailedLevel, String fullAnswer) { + ChatMessage aiTextMessage = chatRoomDomainService.createAiMessage(chatRoomId, level, detailedLevel, fullAnswer); ChatMessage savedMessage = chatRoomCommandHelper.saveChatMessage(aiTextMessage); chatSseSender.sendAiResponseId(memberId, savedMessage.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 index 6ce7d1e1..f17891c2 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/chat/CurrentChatRoomService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/chat/CurrentChatRoomService.java @@ -7,17 +7,14 @@ 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.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.application.port.out.chat.PublishStreamMessagePort; 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; @@ -28,8 +25,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHATROOM_LEVEL; import static makeus.cmc.malmo.util.GlobalConstants.INIT_CHAT_MESSAGE; @@ -88,6 +83,7 @@ private ChatRoom createAndSaveNewChatRoom(MemberId memberId) { ChatMessage initMessage = chatRoomDomainService.createAiMessage( ChatRoomId.of(savedChatRoom.getId()), INIT_CHATROOM_LEVEL, + 1, JosaUtils.아야(member.getNickname()) + INIT_CHAT_MESSAGE); chatRoomCommandHelper.saveChatMessage(initMessage); @@ -124,7 +120,7 @@ public GetCurrentChatRoomMessagesResponse getCurrentChatRoomMessages(GetCurrentC @CheckValidMember public CompleteChatRoomResponse completeChatRoom(CompleteChatRoomCommand command) { ChatRoom chatRoom = chatRoomQueryHelper.getCurrentChatRoomByMemberIdOrThrow(MemberId.of(command.getUserId())); - chatRoom.complete(); + chatRoom.completeByUser(); chatRoomCommandHelper.saveChatRoom(chatRoom); // 완료된 채팅방의 요약을 요청 diff --git a/src/main/java/makeus/cmc/malmo/application/service/couple/CoupleService.java b/src/main/java/makeus/cmc/malmo/application/service/couple/CoupleService.java index 66b8f63a..46d69618 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/couple/CoupleService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/couple/CoupleService.java @@ -86,12 +86,14 @@ public CoupleLinkResponse coupleLink(CoupleLinkCommand command) { // 커플 생성 또는 재연결 // 과거 두 사용자가 커플이었던 경우 재연결, 아니라면 새로 생성 + // V2: 커플 연동 시 startLoveDate를 당일로 초기화 Couple couple = coupleQueryHelper.getCoupleByMemberAndPartnerId(MemberId.of(member.getId()), MemberId.of(partner.getId())) + .filter(Couple::canRecover) .map(this::reconnectCouple) .orElseGet(() -> createNewCouple( MemberId.of(member.getId()), MemberId.of(partner.getId()), - partner.getStartLoveDate() + LocalDate.now() // V2: 당일로 초기화 )); // 사용자 커플 연결 처리 @@ -191,20 +193,20 @@ private boolean migrateAnswerFromTemp(MemberId memberId, CoupleQuestion coupleQu } private void activateCoupleFeatures(MemberId memberId, MemberId partnerId, Couple couple) { - chatRoomQueryHelper.getPausedChatRoomByMemberId(memberId) - .ifPresent( - chatRoom -> { - chatRoom.updateChatRoomStateNeedNextQuestion(); - chatRoomCommandHelper.saveChatRoom(chatRoom); - } - ); - chatRoomQueryHelper.getPausedChatRoomByMemberId(partnerId) - .ifPresent( - chatRoom -> { - chatRoom.updateChatRoomStateNeedNextQuestion(); - chatRoomCommandHelper.saveChatRoom(chatRoom); - } - ); +// chatRoomQueryHelper.getPausedChatRoomByMemberId(memberId) +// .ifPresent( +// chatRoom -> { +// chatRoom.updateChatRoomStateNeedNextQuestion(); +// chatRoomCommandHelper.saveChatRoom(chatRoom); +// } +// ); +// chatRoomQueryHelper.getPausedChatRoomByMemberId(partnerId) +// .ifPresent( +// chatRoom -> { +// chatRoom.updateChatRoomStateNeedNextQuestion(); +// chatRoomCommandHelper.saveChatRoom(chatRoom); +// } +// ); if (validateSsePort.isMemberOnline(partnerId)) { sendSseEventPort.sendToMember( diff --git a/src/main/java/makeus/cmc/malmo/application/service/member/MemberCommandService.java b/src/main/java/makeus/cmc/malmo/application/service/member/MemberCommandService.java index 9b69fc4a..752aaffc 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/member/MemberCommandService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/member/MemberCommandService.java @@ -1,6 +1,7 @@ package makeus.cmc.malmo.application.service.member; import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.in.aop.CheckCoupleMember; import makeus.cmc.malmo.adaptor.in.aop.CheckValidMember; import makeus.cmc.malmo.application.helper.couple.CoupleCommandHelper; import makeus.cmc.malmo.application.helper.couple.CoupleQueryHelper; @@ -57,25 +58,20 @@ public UpdateMemberResponseDto updateMember(UpdateMemberCommand command) { } @Override - @CheckValidMember + @CheckCoupleMember @Transactional - public UpdateStartLoveDateResponse updateStartLoveDate(UpdateStartLoveDateCommand command) { + public UpdateStartLoveDateUseCase.UpdateStartLoveDateResponse updateStartLoveDate(UpdateStartLoveDateUseCase.UpdateStartLoveDateCommand command) { Member member = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(command.getMemberId())); - LocalDate startLoveDate = command.getStartLoveDate(); - member.updateStartLoveDate(startLoveDate); - Member savedMember = memberCommandHelper.saveMember(member); + LocalDate startLoveDate = command.getStartLoveDate(); - if (member.isCoupleLinked()) { - coupleQueryHelper.getCoupleById(member.getCoupleId()) - .ifPresent(couple -> { - couple.updateStartLoveDate(startLoveDate); - coupleCommandHelper.saveCouple(couple); - }); - } + // 커플의 startLoveDate만 업데이트 (개인의 startLoveDate는 업데이트하지 않음) + Couple couple = coupleQueryHelper.getCoupleByIdOrThrow(member.getCoupleId()); + couple.updateStartLoveDate(startLoveDate); + coupleCommandHelper.saveCouple(couple); - return UpdateStartLoveDateResponse.builder() - .startLoveDate(savedMember.getStartLoveDate()) + return UpdateStartLoveDateUseCase.UpdateStartLoveDateResponse.builder() + .startLoveDate(couple.getStartLoveDate()) .build(); } diff --git a/src/main/java/makeus/cmc/malmo/application/service/member/MemberInfoService.java b/src/main/java/makeus/cmc/malmo/application/service/member/MemberInfoService.java index aac687c2..68e8bea0 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/member/MemberInfoService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/member/MemberInfoService.java @@ -46,6 +46,7 @@ public PartnerMemberResponseDto getPartnerInfo(PartnerInfoCommand command) { .avoidanceRate(partner.getAvoidanceRate()) .anxietyRate(partner.getAnxietyRate()) .nickname(partner.getNickname()) + .isStartLoveDateUpdated(partner.getIsStartLoveDateUpdated()) .build(); } } diff --git a/src/main/java/makeus/cmc/malmo/application/service/member/SignInService.java b/src/main/java/makeus/cmc/malmo/application/service/member/SignInService.java index 81670822..9bdba9c6 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/member/SignInService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/member/SignInService.java @@ -9,6 +9,7 @@ import makeus.cmc.malmo.application.helper.member.OauthTokenHelper; import makeus.cmc.malmo.application.port.in.member.LogOutUseCase; import makeus.cmc.malmo.application.port.in.member.SignInUseCase; +import makeus.cmc.malmo.application.port.out.amplitude.AmplitudePort; import makeus.cmc.malmo.domain.model.member.Member; import makeus.cmc.malmo.domain.service.InviteCodeDomainService; import makeus.cmc.malmo.domain.service.MemberDomainService; @@ -30,6 +31,7 @@ public class SignInService implements SignInUseCase, LogOutUseCase { private final OauthTokenHelper oauthTokenHelper; private final AccessTokenHelper accessTokenHelper; + private final AmplitudePort amplitudePort; private static final int MAX_RETRY = 10; @@ -52,6 +54,16 @@ public SignInResponse signInKakao(SignInKakaoCommand command) { member.updateEmail(email); memberCommandHelper.saveMember(member); + // Amplitude 연동 (deviceId가 있는 경우에만) + if (command.getDeviceId() != null && !command.getDeviceId().trim().isEmpty()) { + amplitudePort.identifyUser(AmplitudePort.IdentifyUserCommand.builder() + .userId(member.getId().toString()) + .deviceId(command.getDeviceId()) + .email(email) + .nickname(member.getNickname()) + .build()); + } + // 3. 최종 응답 생성 return buildSignInResponse(member, tokenInfo); } @@ -80,6 +92,16 @@ public SignInResponse signInApple(SignInAppleCommand command) { member.refreshMemberToken(tokenInfo.getRefreshToken()); memberCommandHelper.saveMember(member); + // Amplitude 연동 (deviceId가 있는 경우에만) + if (command.getDeviceId() != null && !command.getDeviceId().trim().isEmpty()) { + amplitudePort.identifyUser(AmplitudePort.IdentifyUserCommand.builder() + .userId(member.getId().toString()) + .deviceId(command.getDeviceId()) + .email(member.getEmail()) + .nickname(member.getNickname()) + .build()); + } + return buildSignInResponse(member, tokenInfo); } diff --git a/src/main/java/makeus/cmc/malmo/application/service/member/SignUpService.java b/src/main/java/makeus/cmc/malmo/application/service/member/SignUpService.java index 121d0d23..704629b6 100644 --- a/src/main/java/makeus/cmc/malmo/application/service/member/SignUpService.java +++ b/src/main/java/makeus/cmc/malmo/application/service/member/SignUpService.java @@ -31,9 +31,9 @@ public class SignUpService implements SignUpUseCase { @Override @Transactional @CheckValidMember - public void signUp(SignUpCommand command) { + public void signUp(SignUpUseCase.SignUpCommand command) { Member member = memberQueryHelper.getMemberByIdOrThrow(MemberId.of(command.getMemberId())); - member.signUp(command.getNickname(), command.getLoveStartDate()); + member.signUp(command.getNickname()); // 회원가입 전 애착 유형 검사를 진행했던 사용자인 경우 해당 정보를 가져와 덮어쓰기 if (command.getLoveTypeId() != null) { diff --git a/src/main/java/makeus/cmc/malmo/config/SecurityConfig.java b/src/main/java/makeus/cmc/malmo/config/SecurityConfig.java index a70b8cc8..caa31216 100644 --- a/src/main/java/makeus/cmc/malmo/config/SecurityConfig.java +++ b/src/main/java/makeus/cmc/malmo/config/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -26,6 +27,7 @@ @Configuration @EnableWebSecurity @RequiredArgsConstructor +@Profile("prod") public class SecurityConfig { private final JwtAdaptor jwtAdaptor; diff --git a/src/main/java/makeus/cmc/malmo/config/TestSecurityConfig.java b/src/main/java/makeus/cmc/malmo/config/TestSecurityConfig.java new file mode 100644 index 00000000..59625dae --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/config/TestSecurityConfig.java @@ -0,0 +1,92 @@ +package makeus.cmc.malmo.config; + +import jakarta.servlet.DispatcherType; +import lombok.RequiredArgsConstructor; +import makeus.cmc.malmo.adaptor.in.web.security.CustomAccessDeniedHandler; +import makeus.cmc.malmo.adaptor.in.web.security.CustomAuthenticationEntryPoint; +import makeus.cmc.malmo.adaptor.in.web.security.JwtAuthenticationFilter; +import makeus.cmc.malmo.adaptor.out.jwt.JwtAdaptor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@RequiredArgsConstructor +@Profile("!prod") +public class TestSecurityConfig { + + private final JwtAdaptor jwtAdaptor; + private final CustomAuthenticationEntryPoint authenticationEntryPoint; + private final CustomAccessDeniedHandler accessDeniedHandler; + + @Value("${security.client.url.production}") + private String PRODUCTION_CLIENT_URL; + + @Value("${security.client.url.development}") + private String DEVELOPMENT_CLIENT_URL; + + @Value("${security.server.url.development}") + private String DEVELOPMENT_SERVER_URL; + + @Value("${security.server.url.production}") + private String PRODUCTION_SERVER_URL; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .cors(Customizer.withDefaults()) + .csrf(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .logout(AbstractHttpConfigurer::disable) + .sessionManagement(sessionManagement -> + sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authorizeHttpRequests(authorize -> authorize + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() + .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() + .requestMatchers("/login/**", "/refresh", "/terms", "/test", "/love-types/**").permitAll() + .requestMatchers("/swagger-ui/**", "/swagger-ui.html", "/swagger-resources/**", + "/v3/api-docs/**", "/v3/api-docs", "/webjars/**", "/actuator/prometheus").permitAll() + .requestMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated()) + .exceptionHandling(exceptionHandling -> exceptionHandling + .authenticationEntryPoint(authenticationEntryPoint) // 401 처리 + .accessDeniedHandler(accessDeniedHandler)) // 403 처리 + .addFilterBefore( + new JwtAuthenticationFilter(jwtAdaptor), + UsernamePasswordAuthenticationFilter.class + ); + + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration config = new CorsConfiguration(); + + // 여기에서 허용할 도메인만 설정 + config.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:3001", "http://localhost:8080", "https://test-front.malmo.io.kr", PRODUCTION_SERVER_URL, DEVELOPMENT_SERVER_URL, PRODUCTION_CLIENT_URL, DEVELOPMENT_CLIENT_URL)); + config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")); + config.setAllowedHeaders(List.of("*")); + config.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } +} diff --git a/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatMessage.java b/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatMessage.java index 6ceabbd7..86a9f7f4 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatMessage.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/chat/ChatMessage.java @@ -14,6 +14,7 @@ public class ChatMessage { private Long id; private ChatRoomId chatRoomId; private int level; + private int detailedLevel; private String content; private SenderType senderType; @@ -22,29 +23,32 @@ public class ChatMessage { private LocalDateTime modifiedAt; private LocalDateTime deletedAt; - public static ChatMessage createUserTextMessage(ChatRoomId chatRoomId, int level, String content) { + public static ChatMessage createUserTextMessage(ChatRoomId chatRoomId, int level, int detailedLevel, String content) { return ChatMessage.builder() .chatRoomId(chatRoomId) .level(level) + .detailedLevel(detailedLevel) .content(content) .senderType(SenderType.USER) .build(); } - public static ChatMessage createAssistantTextMessage(ChatRoomId chatRoomId, int level, String content) { + public static ChatMessage createAssistantTextMessage(ChatRoomId chatRoomId, int level, int detailedLevel, String content) { return ChatMessage.builder() .chatRoomId(chatRoomId) .level(level) + .detailedLevel(detailedLevel) .content(content) .senderType(SenderType.ASSISTANT) .build(); } - public static ChatMessage from(Long id, ChatRoomId chatRoomId, int level, String content, SenderType senderType, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { + public static ChatMessage from(Long id, ChatRoomId chatRoomId, int level, int detailedLevel, String content, SenderType senderType, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { return ChatMessage.builder() .id(id) .chatRoomId(chatRoomId) .level(level) + .detailedLevel(detailedLevel) .content(content) .senderType(senderType) .createdAt(createdAt) 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 75a61f34..e5c29bdf 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 @@ -4,12 +4,15 @@ import lombok.Builder; import lombok.Getter; import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.state.ChatRoomCompletedReason; import makeus.cmc.malmo.domain.value.state.ChatRoomState; import java.time.LocalDateTime; import java.util.Objects; -import static makeus.cmc.malmo.util.GlobalConstants.*; +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) @@ -18,10 +21,13 @@ public class ChatRoom { private MemberId memberId; private ChatRoomState chatRoomState; private int level; + private int detailedLevel; private LocalDateTime lastMessageSentTime; private String totalSummary; private String situationKeyword; private String solutionKeyword; + private ChatRoomCompletedReason chatRoomCompletedReason; + private String counselingType; // BaseTimeEntity fields private LocalDateTime createdAt; @@ -32,60 +38,63 @@ public static ChatRoom createChatRoom(MemberId memberId) { return ChatRoom.builder() .memberId(memberId) .level(INIT_CHATROOM_LEVEL) + .detailedLevel(1) .chatRoomState(ChatRoomState.BEFORE_INIT) .lastMessageSentTime(LocalDateTime.now()) .build(); } public static ChatRoom from(Long id, MemberId memberId, ChatRoomState chatRoomState, - int level, LocalDateTime lastMessageSentTime, + int level, int detailedLevel, LocalDateTime lastMessageSentTime, String totalSummary, String situationKeyword, String solutionKeyword, + ChatRoomCompletedReason chatRoomCompletedReason, String counselingType, LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { return ChatRoom.builder() .id(id) .memberId(memberId) .chatRoomState(chatRoomState) .level(level) + .detailedLevel(detailedLevel) .lastMessageSentTime(lastMessageSentTime) .totalSummary(totalSummary) .situationKeyword(situationKeyword) .solutionKeyword(solutionKeyword) + .chatRoomCompletedReason(chatRoomCompletedReason) + .counselingType(counselingType) .createdAt(createdAt) .modifiedAt(modifiedAt) .deletedAt(deletedAt) .build(); } - public void updateChatRoomStatePaused() { - this.chatRoomState = ChatRoomState.PAUSED; + public void upgradeDetailedLevel() { + this.detailedLevel += 1; } - public void upgradeChatRoom() { + public void upgradeToNextStage() { this.level += 1; - this.chatRoomState = ChatRoomState.NEED_NEXT_QUESTION; - } - - public void updateChatRoomStateNeedNextQuestion() { - this.chatRoomState = ChatRoomState.NEED_NEXT_QUESTION; + this.detailedLevel = 1; } public void updateChatRoomStateAlive() { this.chatRoomState = ChatRoomState.ALIVE; } - public void updateChatRoomSummary(String totalSummary, String situationKeyword, String solutionKeyword) { + 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 complete() { + public void completeByUser() { this.chatRoomState = ChatRoomState.COMPLETED; this.totalSummary = COMPLETED_ROOM_CREATING_SUMMARY_LINE; + this.chatRoomCompletedReason = ChatRoomCompletedReason.COMPLETED_BY_USER; } public boolean isChatRoomValid() { @@ -95,6 +104,7 @@ public boolean isChatRoomValid() { public void expire() { this.chatRoomState = ChatRoomState.COMPLETED; this.totalSummary = EXPIRED_ROOM_CREATING_SUMMARY_LINE; + this.chatRoomCompletedReason = ChatRoomCompletedReason.EXPIRED; } public boolean isStarted() { diff --git a/src/main/java/makeus/cmc/malmo/domain/model/chat/DetailedPrompt.java b/src/main/java/makeus/cmc/malmo/domain/model/chat/DetailedPrompt.java new file mode 100644 index 00000000..caaf3ba1 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/model/chat/DetailedPrompt.java @@ -0,0 +1,63 @@ +package makeus.cmc.malmo.domain.model.chat; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.adaptor.out.persistence.entity.BaseTimeEntity; + +import java.time.LocalDateTime; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class DetailedPrompt extends BaseTimeEntity { + + private Long id; + private int level; + private int detailedLevel; + private String content; + private boolean isForValidation; + private boolean isForSummary; + private String metadataTitle; + private boolean isLastDetailedPrompt; + private boolean isForGuideline; + + // BaseTimeEntity fields + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private LocalDateTime deletedAt; + + public static DetailedPrompt from(Long id, int level, int detailedLevel, String content, + boolean isForValidation, boolean isForSummary, String metadataTitle, + boolean isLastDetailedPrompt, boolean isForGuideline, + LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { + return DetailedPrompt.builder() + .id(id) + .level(level) + .detailedLevel(detailedLevel) + .content(content) + .isForValidation(isForValidation) + .isForSummary(isForSummary) + .metadataTitle(metadataTitle) + .isLastDetailedPrompt(isLastDetailedPrompt) + .isForGuideline(isForGuideline) + .createdAt(createdAt) + .modifiedAt(modifiedAt) + .deletedAt(deletedAt) + .build(); + } + + public static DetailedPrompt create(int level, int detailedLevel, String content, + boolean isForValidation, boolean isForSummary, String metadataTitle, + boolean isLastDetailedPrompt, boolean isForGuideline) { + return DetailedPrompt.builder() + .level(level) + .detailedLevel(detailedLevel) + .content(content) + .isForValidation(isForValidation) + .isForSummary(isForSummary) + .metadataTitle(metadataTitle) + .isLastDetailedPrompt(isLastDetailedPrompt) + .isForGuideline(isForGuideline) + .build(); + } +} diff --git a/src/main/java/makeus/cmc/malmo/domain/model/chat/MemberChatRoomMetadata.java b/src/main/java/makeus/cmc/malmo/domain/model/chat/MemberChatRoomMetadata.java new file mode 100644 index 00000000..a25cd10f --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/model/chat/MemberChatRoomMetadata.java @@ -0,0 +1,51 @@ +package makeus.cmc.malmo.domain.model.chat; + +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import makeus.cmc.malmo.domain.value.id.ChatRoomId; +import makeus.cmc.malmo.domain.value.id.MemberId; + +import java.time.LocalDateTime; + +@Getter +@Builder(access = AccessLevel.PRIVATE) +public class MemberChatRoomMetadata { + + private Long id; + private ChatRoomId chatRoomId; + private MemberId memberId; + private int level; + private int detailedLevel; + private String title; + private String summary; + private LocalDateTime createdAt; + + public static MemberChatRoomMetadata from(Long id, ChatRoomId chatRoomId, MemberId memberId, + int level, int detailedLevel, String title, String summary, + LocalDateTime createdAt) { + return MemberChatRoomMetadata.builder() + .id(id) + .chatRoomId(chatRoomId) + .memberId(memberId) + .level(level) + .detailedLevel(detailedLevel) + .title(title) + .summary(summary) + .createdAt(createdAt) + .build(); + } + + public static MemberChatRoomMetadata create(ChatRoomId chatRoomId, MemberId memberId, + int level, int detailedLevel, String title, String summary) { + return MemberChatRoomMetadata.builder() + .chatRoomId(chatRoomId) + .memberId(memberId) + .level(level) + .detailedLevel(detailedLevel) + .title(title) + .summary(summary) + .createdAt(LocalDateTime.now()) + .build(); + } +} 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 f68728ee..f1618fc5 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 @@ -7,8 +7,6 @@ import java.time.LocalDateTime; -import static makeus.cmc.malmo.util.GlobalConstants.LAST_PROMPT_LEVEL; -import static makeus.cmc.malmo.util.GlobalConstants.NOT_COUPLE_MEMBER_LAST_PROMPT_LEVEL; @Getter @Builder(access = AccessLevel.PRIVATE) @@ -17,6 +15,12 @@ public class Prompt extends BaseTimeEntity { private Long id; private int level; private String content; + private boolean isForSystem; + private boolean isForSummary; + private boolean isForCompletedResponse; + private boolean isForTotalSummary; + private boolean isForGuideline; + private boolean isForAnswerMetadata; // BaseTimeEntity fields private LocalDateTime createdAt; @@ -24,22 +28,22 @@ public class Prompt extends BaseTimeEntity { private LocalDateTime deletedAt; public static Prompt from(Long id, int level, String content, - LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { + boolean isForSystem, boolean isForSummary, boolean isForCompletedResponse, + boolean isForTotalSummary, boolean isForGuideline, boolean isForAnswerMetadata, + LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { return Prompt.builder() .id(id) .level(level) .content(content) + .isForSystem(isForSystem) + .isForSummary(isForSummary) + .isForCompletedResponse(isForCompletedResponse) + .isForTotalSummary(isForTotalSummary) + .isForGuideline(isForGuideline) + .isForAnswerMetadata(isForAnswerMetadata) .createdAt(createdAt) .modifiedAt(modifiedAt) .deletedAt(deletedAt) .build(); } - - public boolean isLastPromptForNotCoupleMember() { - return level == NOT_COUPLE_MEMBER_LAST_PROMPT_LEVEL; - } - - public boolean isLastPrompt() { - return level == LAST_PROMPT_LEVEL; - } } diff --git a/src/main/java/makeus/cmc/malmo/domain/model/couple/Couple.java b/src/main/java/makeus/cmc/malmo/domain/model/couple/Couple.java index 1479dba2..b5842a60 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/couple/Couple.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/couple/Couple.java @@ -21,6 +21,7 @@ public class Couple { private CoupleMemberSnapshot firstMemberSnapshot; private CoupleMemberSnapshot secondMemberSnapshot; private CoupleState coupleState; + private Boolean isStartLoveDateUpdated; // BaseTimeEntity fields private LocalDateTime createdAt; @@ -33,6 +34,7 @@ public static Couple createCouple(Long memberId, Long partnerId, LocalDate start .secondMemberId(MemberId.of(partnerId)) .startLoveDate(startLoveDate) .coupleState(coupleState) + .isStartLoveDateUpdated(false) .build(); } @@ -40,7 +42,7 @@ public static Couple from(Long id, LocalDate startLoveDate, MemberId firstMemberId, MemberId secondMemberId, CoupleState coupleState, CoupleMemberSnapshot firstMemberSnapshot, CoupleMemberSnapshot secondMemberSnapshot, - LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt) { + LocalDateTime createdAt, LocalDateTime modifiedAt, LocalDateTime deletedAt, Boolean isStartLoveDateUpdated) { return Couple.builder() .id(id) .startLoveDate(startLoveDate) @@ -49,6 +51,7 @@ public static Couple from(Long id, LocalDate startLoveDate, .firstMemberSnapshot(firstMemberSnapshot) .secondMemberSnapshot(secondMemberSnapshot) .coupleState(coupleState) + .isStartLoveDateUpdated(isStartLoveDateUpdated) .createdAt(createdAt) .modifiedAt(modifiedAt) .deletedAt(deletedAt) @@ -59,6 +62,14 @@ public void recover() { this.firstMemberSnapshot = null; this.secondMemberSnapshot = null; this.coupleState = CoupleState.ALIVE; + this.deletedAt = null; + } + + public boolean canRecover() { + if (this.deletedAt == null) { + return false; + } + return this.deletedAt.isAfter(LocalDateTime.now().minusDays(30)); } public boolean isBroken() { @@ -75,6 +86,7 @@ public MemberId getOtherMemberId(MemberId memberId) { public void unlink(MemberId memberId, String nickname, LoveTypeCategory loveTypeCategory, float anxietyRate, float avoidanceRate) { this.coupleState = CoupleState.DELETED; + this.deletedAt = LocalDateTime.now(); CoupleMemberSnapshot coupleMemberSnapshot = new CoupleMemberSnapshot(nickname, loveTypeCategory, anxietyRate, avoidanceRate); if (Objects.equals(memberId, firstMemberId)) { @@ -86,5 +98,6 @@ public void unlink(MemberId memberId, String nickname, LoveTypeCategory loveType public void updateStartLoveDate(LocalDate startLoveDate) { this.startLoveDate = startLoveDate; + this.isStartLoveDateUpdated = true; } } \ No newline at end of file diff --git a/src/main/java/makeus/cmc/malmo/domain/model/member/Member.java b/src/main/java/makeus/cmc/malmo/domain/model/member/Member.java index 0c0562d9..3abbf08c 100644 --- a/src/main/java/makeus/cmc/malmo/domain/model/member/Member.java +++ b/src/main/java/makeus/cmc/malmo/domain/model/member/Member.java @@ -30,6 +30,13 @@ public class Member { private String nickname; private String email; private InviteCodeValue inviteCode; + + /** + * @deprecated 개인의 startLoveDate는 더 이상 사용하지 않습니다. + * 커플의 startLoveDate를 사용하도록 변경되었습니다. + * Entity 호환성을 위해 필드는 유지됩니다. + */ + @Deprecated private LocalDate startLoveDate; private String oauthToken; private CoupleId coupleId; @@ -99,12 +106,26 @@ public static Member from( } + /** + * V1 회원가입 - startLoveDate를 개인 정보로 저장 + * @deprecated v2에서는 signUpV2(String nickname) 사용 + */ + @Deprecated public void signUp(String nickname, LocalDate startLoveDate) { this.nickname = nickname; this.startLoveDate = startLoveDate; this.memberState = MemberState.ALIVE; } + /** + * V2 회원가입 - startLoveDate 없이 회원가입 + * 커플 연동 후 별도로 연애 시작일을 설정합니다. + */ + public void signUp(String nickname) { + this.nickname = nickname; + this.memberState = MemberState.ALIVE; + } + public void updateMemberProfile(String nickname) { this.nickname = nickname; } @@ -119,6 +140,12 @@ public void refreshMemberToken(String refreshToken) { this.refreshToken = refreshToken; } + /** + * 개인의 startLoveDate 업데이트 + * @deprecated v2에서는 더 이상 개인의 startLoveDate를 업데이트하지 않습니다. + * 커플의 startLoveDate만 업데이트합니다. + */ + @Deprecated public void updateStartLoveDate(LocalDate startLoveDate) { this.startLoveDate = startLoveDate; } 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 c1eb3be6..7faf8b44 100644 --- a/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java +++ b/src/main/java/makeus/cmc/malmo/domain/service/ChatRoomDomainService.java @@ -15,12 +15,12 @@ public ChatRoom createChatRoom(MemberId memberId) { return ChatRoom.createChatRoom(memberId); } - public ChatMessage createUserMessage(ChatRoomId chatRoomId, int level, String content) { - return ChatMessage.createUserTextMessage(chatRoomId, level, content); + public ChatMessage createUserMessage(ChatRoomId chatRoomId, int level, int detailedLevel, String content) { + return ChatMessage.createUserTextMessage(chatRoomId, level, detailedLevel, content); } - public ChatMessage createAiMessage(ChatRoomId chatRoomId, int level, String content) { - return ChatMessage.createAssistantTextMessage(chatRoomId, level, content); + public ChatMessage createAiMessage(ChatRoomId chatRoomId, int level, int detailedLevel, String content) { + return ChatMessage.createAssistantTextMessage(chatRoomId, level, detailedLevel, content); } public boolean isChatRoomExpired(LocalDateTime lastMessageSentTime) { diff --git a/src/main/java/makeus/cmc/malmo/domain/service/MemberDomainService.java b/src/main/java/makeus/cmc/malmo/domain/service/MemberDomainService.java index 545f3d40..e89b3a9c 100644 --- a/src/main/java/makeus/cmc/malmo/domain/service/MemberDomainService.java +++ b/src/main/java/makeus/cmc/malmo/domain/service/MemberDomainService.java @@ -15,6 +15,9 @@ public class MemberDomainService { public Member createMember(Provider provider, String providerId, String email, InviteCodeValue inviteCode, String oauthToken) { + if (email == null || email.isEmpty()) { + email = (Provider.APPLE.equals(provider)) ? "apple@example.com" : "kakao@example.com"; + } return Member.createMember( provider, providerId, 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 new file mode 100644 index 00000000..bb71a2f2 --- /dev/null +++ b/src/main/java/makeus/cmc/malmo/domain/value/state/ChatRoomCompletedReason.java @@ -0,0 +1,5 @@ +package makeus.cmc.malmo.domain.value.state; + +public enum ChatRoomCompletedReason { + EXPIRED, COMPLETED_BY_USER, CHAT_PROCESS_DONE +} diff --git a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java index cd4d20ff..32c664eb 100644 --- a/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java +++ b/src/main/java/makeus/cmc/malmo/util/GlobalConstants.java @@ -6,20 +6,9 @@ public class GlobalConstants { // 채팅방 관련 상수 public static final int INIT_CHATROOM_LEVEL = 1; - public static final int SYSTEM_PROMPT_LEVEL = -2; - public static final int SUMMARY_PROMPT_LEVEL = -1; - public static final int TOTAL_SUMMARY_PROMPT_LEVEL = -3; - public static final int EXTRACT_METADATA_PROMPT = -4; - public static final int NOT_COUPLE_MEMBER_LAST_PROMPT_LEVEL = 1; - public static final int LAST_PROMPT_LEVEL = 4; - public static final String INIT_CHAT_MESSAGE = " 안녕! 나는 연애 고민 상담사 모모야.\n" + "나와의 대화를 마무리하고 싶다면 종료하기 버튼을 눌러줘! 대화 종료 후에는 대화 요약 리포트를 보여줄게.\n" + - "오늘은 어떤 고민 때문에 나를 찾아왔어? 먼저 연인과 있었던 갈등 상황을 이야기해 주면 내가 같이 고민해볼게!"; - - public static final String FINAL_MESSAGE = "이제 대화가 종료되었어! 대화 요약 리포트를 보여줄게.\n" + - "대화 요약 리포트는 연애 고민 상담사 모모가 함께 대화했던 내용을 바탕으로 고민과 해결책을 분석해본 내용이야.\n" + - "리포트를 확인하고 싶다면, 종료 버튼을 눌러줘!"; + "오늘은 어떤 고민 때문에 나를 찾아왔어?"; public static final String EXPIRED_ROOM_CREATING_SUMMARY_LINE = "하루가 지나 채팅방이 만료되었습니다. 요약 생성 중..."; @@ -28,5 +17,7 @@ public class GlobalConstants { 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"; + // 커플 복구 관련 상수 + public static final int COUPLE_RECOVERY_LIMIT_DAYS = 30; } diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 9b25b39c..7e392f56 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -9,7 +9,7 @@ spring: connection-timeout: 5000 jpa: hibernate: - ddl-auto: update + ddl-auto: none show-sql: false database-platform: org.hibernate.dialect.MySQL8Dialect open-in-view: false @@ -79,6 +79,11 @@ encryption: aes: key: ${AES_ENCRYPTION_KEY} +amplitude: + api: + key: ${AMPLITUDE_API_KEY} + url: https://api2.amplitude.com/identify + sentry: dsn: ${SENTRY_DSN} traces-sample-rate: 1.0 diff --git a/src/main/resources/application-qa.yml b/src/main/resources/application-qa.yml new file mode 100644 index 00000000..81fa90a9 --- /dev/null +++ b/src/main/resources/application-qa.yml @@ -0,0 +1,96 @@ +spring: + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 10 + connection-timeout: 5000 + jpa: + hibernate: + ddl-auto: update + show-sql: false + database-platform: org.hibernate.dialect.MySQL8Dialect + open-in-view: false + data: + redis: + port: 6379 + host: redis + stream-key: ${REDIS_STREAM_KEY} + consumer-group: ${REDIS_CONSUMER_GROUP} + repositories: + enabled: false + +jwt: + secret: ${JWT_SECRET} + access-token-expiration-seconds: 3600 # 1시간 + refresh-token-expiration-seconds: 2592000 # 30일 + +logging: + level: + com.zaxxer.hikari: WARN + org.hibernate: WARN + makeus.cmc.malmo: INFO + +kakao: + oidc: + iss: https://kauth.kakao.com + aud: ${KAKAO_REST_API_KEY} + jwks-uri: https://kauth.kakao.com/.well-known/jwks.json + user-info-uri: https://kapi.kakao.com/v1/oidc/userinfo + oauth: + unlink-uri: https://kapi.kakao.com/v1/user/unlink + admin-key: ${KAKAO_ADMIN_KEY} + +apple: + oidc: + iss: https://appleid.apple.com + aud: ${APPLE_REST_API_KEY} + jwks-uri: https://appleid.apple.com/auth/keys + oauth: + team-id: ${APPLE_TEAM_ID} + client-id: ${APPLE_CLIENT_ID} + key-id: ${APPLE_KEY_ID} + private-key: ${APPLE_PRIVATE_KEY} + revoke-uri: https://appleid.apple.com/auth/revoke + token-uri: https://appleid.apple.com/auth/token + +swagger: + server: + production: + url: ${SWAGGER_SERVER_PRODUCTION_URL} + +openai: + api: + key: ${OPENAI_API_KEY} + +security: + server: + url: + production: ${SECURITY_SERVER_URL_PRODUCTION} + development: ${SECURITY_SERVER_URL_DEVELOPMENT} + client: + url: + production: ${SECURITY_CLIENT_URL_PRODUCTION} + development: ${SECURITY_CLIENT_URL_DEVELOPMENT} + +encryption: + aes: + key: ${AES_ENCRYPTION_KEY} + +amplitude: + api: + key: ${AMPLITUDE_API_KEY} + url: https://api2.amplitude.com/identify + +sentry: + dsn: ${SENTRY_DSN} + traces-sample-rate: 1.0 + profiles-sample-rate: 1.0 + environment: server-prod + exception-resolver-order: 0 + logging: + minimum-event-level: ERROR + send-default-pii: true + max-request-body-size: medium \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index ab0a752d..575bf9bb 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -87,3 +87,8 @@ security: encryption: aes: key: testaeskeyrZktAgxX77DA== + +amplitude: + api: + key: amplitude_key_sample + url: https://api2.amplitude.com/identify diff --git a/src/main/resources/data-test.sql b/src/main/resources/data-test.sql index 3231322f..20029730 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) +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) VALUES - (-3, '요약용 프롬프트'), - (-2, '시스템 프롬프트'), - (-1, '중간 요약용 프롬프트'), - (1, '1단계 프롬프트'), - (2, '2단계 프롬프트'), - (3, '3단계 프롬프트'), - (4, '마지막 프롬프트'); + (-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); diff --git a/src/test/java/makeus/cmc/malmo/domain/CoupleTest.java b/src/test/java/makeus/cmc/malmo/domain/CoupleTest.java new file mode 100644 index 00000000..188727b0 --- /dev/null +++ b/src/test/java/makeus/cmc/malmo/domain/CoupleTest.java @@ -0,0 +1,99 @@ +package makeus.cmc.malmo.domain; + +import makeus.cmc.malmo.domain.model.couple.Couple; +import makeus.cmc.malmo.domain.value.id.MemberId; +import makeus.cmc.malmo.domain.value.state.CoupleState; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Couple 도메인 테스트") +class CoupleTest { + + @Nested + @DisplayName("updateStartLoveDate 메서드") + class UpdateStartLoveDateTest { + + @Test + @DisplayName("디데이를 변경하면 isStartLoveDateUpdated가 true로 변경된다") + void updateStartLoveDate_shouldSetIsStartLoveDateUpdatedToTrue() { + // given + Couple couple = createCoupleWithIsStartLoveDateUpdated(false); + LocalDate newStartLoveDate = LocalDate.of(2024, 1, 1); + + // when + couple.updateStartLoveDate(newStartLoveDate); + + // then + assertThat(couple.getStartLoveDate()).isEqualTo(newStartLoveDate); + assertThat(couple.getIsStartLoveDateUpdated()).isTrue(); + } + + @Test + @DisplayName("이미 isStartLoveDateUpdated가 true인 경우에도 디데이 변경이 가능하다") + void updateStartLoveDate_whenAlreadyTrue_shouldStillWork() { + // given + Couple couple = createCoupleWithIsStartLoveDateUpdated(true); + LocalDate newStartLoveDate = LocalDate.of(2024, 1, 1); + + // when + couple.updateStartLoveDate(newStartLoveDate); + + // then + assertThat(couple.getStartLoveDate()).isEqualTo(newStartLoveDate); + assertThat(couple.getIsStartLoveDateUpdated()).isTrue(); + } + } + + @Nested + @DisplayName("from 정적 팩토리 메서드") + class FromMethodTest { + + @Test + @DisplayName("isStartLoveDateUpdated 필드가 포함된 Couple을 생성한다") + void from_shouldCreateCoupleWithIsStartLoveDateUpdated() { + // given + Long id = 1L; + LocalDate startLoveDate = LocalDate.of(2023, 1, 1); + MemberId firstMemberId = MemberId.of(100L); + MemberId secondMemberId = MemberId.of(200L); + CoupleState coupleState = CoupleState.ALIVE; + LocalDateTime createdAt = LocalDateTime.now(); + LocalDateTime modifiedAt = LocalDateTime.now(); + LocalDateTime deletedAt = null; + Boolean isStartLoveDateUpdated = false; + + // when + Couple couple = Couple.from(id, startLoveDate, firstMemberId, secondMemberId, coupleState, null, null, createdAt, modifiedAt, deletedAt, isStartLoveDateUpdated); + + // then + assertThat(couple.getId()).isEqualTo(id); + assertThat(couple.getStartLoveDate()).isEqualTo(startLoveDate); + assertThat(couple.getFirstMemberId()).isEqualTo(firstMemberId); + assertThat(couple.getSecondMemberId()).isEqualTo(secondMemberId); + assertThat(couple.getCoupleState()).isEqualTo(coupleState); + assertThat(couple.getIsStartLoveDateUpdated()).isEqualTo(isStartLoveDateUpdated); + } + } + + private Couple createCoupleWithIsStartLoveDateUpdated(Boolean isStartLoveDateUpdated) { + return Couple.from( + 1L, + LocalDate.of(2023, 1, 1), + MemberId.of(100L), + MemberId.of(200L), + CoupleState.ALIVE, + null, + null, + LocalDateTime.now(), + LocalDateTime.now(), + null, + isStartLoveDateUpdated + ); + } +} 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 c1676b89..245de2a9 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/ChatRoomIntegrationTest.java @@ -177,7 +177,8 @@ class GetCurrentChatRoom { ChatProcessor.CounselingSummary mockSummary = new ChatProcessor.CounselingSummary( "만료된 채팅방 요약", "상황 키워드", - "솔루션 키워드" + "솔루션 키워드", + "재회 고민" ); when(chatProcessor.requestTotalSummary(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(mockSummary)); @@ -238,7 +239,7 @@ class SendChatMessage { Consumer onComplete = invocation.getArgument(4); onComplete.accept("AI 응답입니다."); return null; - }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any()); + }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any(), any()); // when & then mockMvc.perform(post("/chatrooms/current/send") @@ -251,7 +252,7 @@ class SendChatMessage { .setParameter("chatRoomId", chatRoom.getId()) .getResultList(); - Assertions.assertThat(messages).hasSize(1); + Assertions.assertThat(messages).hasSize(2); Assertions.assertThat(messages.get(0).getContent()).isEqualTo(message); Assertions.assertThat(messages.get(0).getSenderType()).isEqualTo(SenderType.USER); } @@ -286,7 +287,7 @@ class SendChatMessage { // given ChatRoomEntity chatRoom = ChatRoomEntity.builder() .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.BEFORE_INIT) + .chatRoomState(ChatRoomState.ALIVE) .level(INIT_CHATROOM_LEVEL) .build(); em.persist(chatRoom); @@ -298,7 +299,7 @@ class SendChatMessage { Consumer onComplete = invocation.getArgument(4); onComplete.accept("AI 응답입니다."); return null; - }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any()); + }).when(chatProcessor).streamChat(any(), any(), any(), any(), any(), any(), any()); // when & then mockMvc.perform(post("/chatrooms/current/send") @@ -318,7 +319,7 @@ class SendChatMessage { ChatRoomEntity chatRoom = ChatRoomEntity.builder() .memberEntityId(MemberEntityId.of(member.getId())) .chatRoomState(ChatRoomState.ALIVE) - .level(LAST_PROMPT_LEVEL) + .level(4) // 마지막 단계 레벨 (하드코딩 대신 실제 값 사용) .build(); em.persist(chatRoom); em.flush(); @@ -339,68 +340,11 @@ class SendChatMessage { 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(FINAL_MESSAGE); + Assertions.assertThat(messages.get(1).getContent()).isEqualTo("마지막 프롬프트"); Assertions.assertThat(messages.get(1).getSenderType()).isEqualTo(SenderType.ASSISTANT); } } - @Nested - @DisplayName("채팅방 업그레이드") - class UpgradeChatRoom { - @Test - @DisplayName("채팅방 업그레이드에 성공한다") - void 채팅방_업그레이드_성공() throws Exception { - // given - ChatRoomEntity chatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.ALIVE) - .level(1) - .build(); - em.persist(chatRoom); - em.flush(); - - // Mock publishStreamMessagePort - ArgumentCaptor로 호출 검증 - ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Object.class); - doNothing().when(outboxHelper).publish(any(), (StreamMessage) messageCaptor.capture()); - - // when & then - mockMvc.perform(post("/chatrooms/current/upgrade") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isOk()); - - ChatRoomEntity updatedChatRoom = em.find(ChatRoomEntity.class, chatRoom.getId()); - Assertions.assertThat(updatedChatRoom.getLevel()).isEqualTo(2); - Assertions.assertThat(updatedChatRoom.getChatRoomState()).isEqualTo(ChatRoomState.ALIVE); - - // publishStreamMessagePort가 2번 호출되었는지 검증 - verify(outboxHelper, times(2)).publish(any(), any()); - - // 첫 번째 호출은 REQUEST_SUMMARY, 두 번째 호출은 REQUEST_CHAT_MESSAGE인지 검증 - List capturedMessages = messageCaptor.getAllValues(); - Assertions.assertThat(capturedMessages).hasSize(2); - } - - @Test - @DisplayName("탈퇴한 사용자의 경우 채팅방 업그레이드에 실패한다") - void 탈퇴한_사용자_업그레이드_실패() throws Exception { - // when & then - mockMvc.perform(post("/chatrooms/current/upgrade") - .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(post("/chatrooms/current/upgrade") - .header("Authorization", "Bearer " + accessToken)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.code").value(NO_SUCH_CHAT_ROOM.getCode())); - } - } - @Nested @DisplayName("현재 채팅방 메시지 조회") class GetCurrentChatRoomMessages { @@ -458,7 +402,7 @@ class CompleteChatRoom { em.persist(ChatMessageSummaryEntity.builder().chatRoomEntityId(ChatRoomEntityId.of(chatRoom.getId())).content("요약2").level(2).build()); em.flush(); - ChatProcessor.CounselingSummary summary = new ChatProcessor.CounselingSummary("최종 요약", "상황 키워드", "솔루션 키워드"); + ChatProcessor.CounselingSummary summary = new ChatProcessor.CounselingSummary("최종 요약", "상황 키워드", "솔루션 키워드", "재회 고민"); when(chatProcessor.requestTotalSummary(any(), any(), any())).thenReturn(CompletableFuture.completedFuture(summary)); // when & then diff --git a/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java b/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java index 550c65b7..ab478832 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/CoupleIntegrationTest.java @@ -28,6 +28,8 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import static makeus.cmc.malmo.adaptor.in.exception.ErrorCode.*; @@ -132,7 +134,7 @@ class CoupleLinkFeature { .getSingleResult(); Assertions.assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); - Assertions.assertThat(couple.getStartLoveDate()).isEqualTo(partner.getStartLoveDate()); + Assertions.assertThat(couple.getStartLoveDate()).isEqualTo(LocalDate.now()); List memberIds = List.of(couple.getFirstMemberId().getValue(), couple.getSecondMemberId().getValue()); Assertions.assertThat(memberIds).containsExactlyInAnyOrder(member.getId(), partner.getId()); @@ -147,25 +149,77 @@ class CoupleLinkFeature { Assertions.assertThat(coupleQuestion.getCoupleQuestionState()).isEqualTo(CoupleQuestionState.ALIVE); } +// @Test +// @DisplayName("정지된 채팅방이 있는 경우 커플 연결이 성공 후 채팅방이 활성화된다.") +// void 커플_연결_성공_채팅방_활성화() throws Exception { +// // given +// ChatRoomEntity memberChatRoom = ChatRoomEntity.builder() +// .memberEntityId(MemberEntityId.of(member.getId())) +// .chatRoomState(ChatRoomState.PAUSED) +// .level(INIT_CHATROOM_LEVEL) +// .build(); +// +// ChatRoomEntity partnerChatRoom = ChatRoomEntity.builder() +// .memberEntityId(MemberEntityId.of(partner.getId())) +// .chatRoomState(ChatRoomState.PAUSED) +// .level(INIT_CHATROOM_LEVEL) +// .build(); +// +// em.persist(memberChatRoom); +// em.persist(partnerChatRoom); +// em.flush(); +// +// // when +// MvcResult mvcResult = mockMvc.perform(post("/couples") +// .header("Authorization", "Bearer " + accessToken) +// .contentType(MediaType.APPLICATION_JSON) +// .content(objectMapper.writeValueAsString( +// CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) +// ))) +// .andExpect(status().isOk()) +// .andReturn(); +// String responseContent = mvcResult.getResponse().getContentAsString(); +// Integer coupleId = JsonPath.read(responseContent, "$.data.coupleId"); +// +// // then +// // 커플 생성 여부 확인 +// Assertions.assertThat(coupleId).isNotNull(); +// CoupleEntity couple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) +// .setParameter("coupleId", Long.valueOf(coupleId)) +// .getSingleResult(); +// Assertions.assertThat(couple).isNotNull(); +// Assertions.assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); +// Assertions.assertThat(couple.getStartLoveDate()).isEqualTo(partner.getStartLoveDate()); +// +// List memberIds = List.of(couple.getFirstMemberId().getValue(), couple.getSecondMemberId().getValue()); +// Assertions.assertThat(memberIds).containsExactlyInAnyOrder(member.getId(), partner.getId()); +// +// // 커플 멤버의 채팅방 상태가 활성화 되었는지 확인 +// ChatRoomEntity memberChatRoomAfter = em.createQuery("SELECT cr FROM ChatRoomEntity cr WHERE cr.memberEntityId.value = :memberId", ChatRoomEntity.class) +// .setParameter("memberId", member.getId()) +// .getSingleResult(); +// ChatRoomEntity partnerChatRoomAfter = em.createQuery("SELECT cr FROM ChatRoomEntity cr WHERE cr.memberEntityId.value = :partnerId", ChatRoomEntity.class) +// .setParameter("partnerId", partner.getId()) +// .getSingleResult(); +// Assertions.assertThat(memberChatRoomAfter.getChatRoomState()).isEqualTo(ChatRoomState.NEED_NEXT_QUESTION); +// Assertions.assertThat(partnerChatRoomAfter.getChatRoomState()).isEqualTo(ChatRoomState.NEED_NEXT_QUESTION); +// } + @Test - @DisplayName("정지된 채팅방이 있는 경우 커플 연결이 성공 후 채팅방이 활성화된다.") - void 커플_연결_성공_채팅방_활성화() throws Exception { + @DisplayName("재결합 커플인 경우 커플 연결이 성공 후 데이터가 복구된다.") + void 재결합_커플_연결_성공_데이터_복구() throws Exception { // given - ChatRoomEntity memberChatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(member.getId())) - .chatRoomState(ChatRoomState.PAUSED) - .level(INIT_CHATROOM_LEVEL) - .build(); - - ChatRoomEntity partnerChatRoom = ChatRoomEntity.builder() - .memberEntityId(MemberEntityId.of(partner.getId())) - .chatRoomState(ChatRoomState.PAUSED) - .level(INIT_CHATROOM_LEVEL) - .build(); + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()); - em.persist(memberChatRoom); - em.persist(partnerChatRoom); - em.flush(); + mockMvc.perform(delete("/couples") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()); // when MvcResult mvcResult = mockMvc.perform(post("/couples") @@ -180,32 +234,55 @@ class CoupleLinkFeature { Integer coupleId = JsonPath.read(responseContent, "$.data.coupleId"); // then - // 커플 생성 여부 확인 - Assertions.assertThat(coupleId).isNotNull(); CoupleEntity couple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) .setParameter("coupleId", Long.valueOf(coupleId)) .getSingleResult(); + Assertions.assertThat(couple).isNotNull(); Assertions.assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); - Assertions.assertThat(couple.getStartLoveDate()).isEqualTo(partner.getStartLoveDate()); + } - List memberIds = List.of(couple.getFirstMemberId().getValue(), couple.getSecondMemberId().getValue()); - Assertions.assertThat(memberIds).containsExactlyInAnyOrder(member.getId(), partner.getId()); + @Test + @DisplayName("30일 이내 재연결 시 기존 커플이 복구된다.") + void 삼십일_이내_재연결_기존_커플_복구() throws Exception { + // given + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()); - // 커플 멤버의 채팅방 상태가 활성화 되었는지 확인 - ChatRoomEntity memberChatRoomAfter = em.createQuery("SELECT cr FROM ChatRoomEntity cr WHERE cr.memberEntityId.value = :memberId", ChatRoomEntity.class) - .setParameter("memberId", member.getId()) - .getSingleResult(); - ChatRoomEntity partnerChatRoomAfter = em.createQuery("SELECT cr FROM ChatRoomEntity cr WHERE cr.memberEntityId.value = :partnerId", ChatRoomEntity.class) - .setParameter("partnerId", partner.getId()) + mockMvc.perform(delete("/couples") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()); + + // when - 즉시 재연결 (30일 이내) + MvcResult mvcResult = mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()) + .andReturn(); + String responseContent = mvcResult.getResponse().getContentAsString(); + Integer coupleId = JsonPath.read(responseContent, "$.data.coupleId"); + + // then + CoupleEntity couple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) + .setParameter("coupleId", Long.valueOf(coupleId)) .getSingleResult(); - Assertions.assertThat(memberChatRoomAfter.getChatRoomState()).isEqualTo(ChatRoomState.NEED_NEXT_QUESTION); - Assertions.assertThat(partnerChatRoomAfter.getChatRoomState()).isEqualTo(ChatRoomState.NEED_NEXT_QUESTION); + + Assertions.assertThat(couple).isNotNull(); + Assertions.assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); + Assertions.assertThat(couple.getDeletedAt()).isNull(); // 복구 시 deletedAt이 null로 초기화 } @Test - @DisplayName("재결합 커플인 경우 커플 연결이 성공 후 데이터가 복구된다.") - void 재결합_커플_연결_성공_데이터_복구() throws Exception { + @DisplayName("30일 초과 재연결 시 새로운 커플이 생성된다.") + void 삼십일_초과_재연결_새로운_커플_생성() throws Exception { // given mockMvc.perform(post("/couples") .header("Authorization", "Bearer " + accessToken) @@ -219,6 +296,12 @@ class CoupleLinkFeature { .header("Authorization", "Bearer " + accessToken)) .andExpect(status().isOk()); + // deletedAt을 31일 전으로 수정 + em.createQuery("UPDATE CoupleEntity c SET c.deletedAt = :deletedAt WHERE c.coupleState = 'DELETED'") + .setParameter("deletedAt", java.time.LocalDateTime.now().minusDays(31)) + .executeUpdate(); + em.flush(); + // when MvcResult mvcResult = mockMvc.perform(post("/couples") .header("Authorization", "Bearer " + accessToken) @@ -232,12 +315,47 @@ class CoupleLinkFeature { Integer coupleId = JsonPath.read(responseContent, "$.data.coupleId"); // then - CoupleEntity couple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) + CoupleEntity newCouple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) .setParameter("coupleId", Long.valueOf(coupleId)) .getSingleResult(); - Assertions.assertThat(couple).isNotNull(); - Assertions.assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); + Assertions.assertThat(newCouple).isNotNull(); + Assertions.assertThat(newCouple.getCoupleState()).isEqualTo(CoupleState.ALIVE); + Assertions.assertThat(newCouple.getDeletedAt()).isNull(); + + // 기존 DELETED 커플이 여전히 존재하는지 확인 + List deletedCouples = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.coupleState = 'DELETED'", CoupleEntity.class) + .getResultList(); + Assertions.assertThat(deletedCouples).hasSize(1); + Assertions.assertThat(deletedCouples.get(0).getDeletedAt()).isNotNull(); + } + + @Test + @DisplayName("커플 해지 시 deletedAt이 현재 시간으로 설정된다.") + void 커플_해지_시_deletedAt_설정() throws Exception { + // given + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()); + + // when + LocalDateTime beforeUnlink = LocalDateTime.now(); + mockMvc.perform(delete("/couples") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()); + LocalDateTime afterUnlink = LocalDateTime.now(); + + // then + CoupleEntity deletedCouple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.coupleState = 'DELETED'", CoupleEntity.class) + .getSingleResult(); + + Assertions.assertThat(deletedCouple.getDeletedAt()).isNotNull(); + Assertions.assertThat(deletedCouple.getDeletedAt()).isAfterOrEqualTo(beforeUnlink); + Assertions.assertThat(deletedCouple.getDeletedAt()).isBeforeOrEqualTo(afterUnlink); } @Test diff --git a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java index 85535803..cf1c53ba 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/MemberIntegrationTest.java @@ -14,14 +14,17 @@ import makeus.cmc.malmo.adaptor.out.persistence.entity.question.CoupleQuestionEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.question.QuestionEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.terms.MemberTermsAgreementEntity; +import makeus.cmc.malmo.adaptor.out.persistence.entity.terms.TermsEntity; import makeus.cmc.malmo.adaptor.out.persistence.entity.value.CoupleEntityId; import makeus.cmc.malmo.adaptor.out.persistence.entity.value.InviteCodeEntityValue; import makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId; +import makeus.cmc.malmo.adaptor.in.exception.ErrorCode; import makeus.cmc.malmo.application.port.out.member.GenerateTokenPort; import makeus.cmc.malmo.domain.value.state.*; import makeus.cmc.malmo.domain.value.type.LoveTypeCategory; import makeus.cmc.malmo.domain.value.type.MemberRole; import makeus.cmc.malmo.domain.value.type.Provider; +import makeus.cmc.malmo.domain.value.type.TermsType; import makeus.cmc.malmo.integration_test.dto_factory.CoupleRequestDtoFactory; import makeus.cmc.malmo.integration_test.dto_factory.LoveTypeQuestionRequestDtoFactory; import makeus.cmc.malmo.integration_test.dto_factory.MemberRequestDtoFactory; @@ -42,8 +45,10 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import static makeus.cmc.malmo.adaptor.in.exception.ErrorCode.*; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -72,6 +77,8 @@ public class MemberIntegrationTest { private String accessToken; private MemberEntity member; + + private LocalDate newDday = LocalDate.now().minusDays(100); @BeforeEach void setup() { @@ -82,7 +89,6 @@ void setup() { .memberState(MemberState.ALIVE) .email("testEmail@test.com") .nickname("nickname") - .startLoveDate(LocalDate.of(2023, 10, 1)) .inviteCodeEntityValue(InviteCodeEntityValue.of("testInviteCode")) .build(); @@ -95,7 +101,415 @@ void setup() { } @Nested - @DisplayName("회원가입 기능 검증") + @DisplayName("온보딩 API 테스트") + class SignUpV2Test { + + private String accessToken; + private MemberEntity member; + private TermsEntity terms; + + @BeforeEach + void setup() { + // 약관 생성 + terms = TermsEntity.builder() + .termsType(TermsType.SERVICE_USAGE) + .content("서비스 이용약관") + .isRequired(true) + .build(); + em.persist(terms); + + // 미가입 상태의 멤버 생성 (OAuth 인증만 완료) + member = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("testProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.BEFORE_ONBOARDING) + .email("test@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("TEST1234")) + .build(); + + em.persist(member); + em.flush(); + + TokenInfo tokenInfo = generateTokenPort.generateToken(member.getId(), member.getMemberRole()); + accessToken = tokenInfo.getAccessToken(); + } + + @Test + @DisplayName("온보딩 - startLoveDate 없이 회원가입 성공") + void signUpV2WithoutStartLoveDate() throws Exception { + // given + Map requestDto = Map.of( + "nickname", "테스트유저", + "terms", List.of( + Map.of("termsId", terms.getId(), "isAgreed", true) + ) + ); + + // when & then + mockMvc.perform(post("/members/onboarding") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + + // 회원가입 후 확인 + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + assertThat(savedMember.getNickname()).isEqualTo("테스트유저"); + assertThat(savedMember.getMemberState()).isEqualTo(MemberState.ALIVE); + // V2에서는 개인의 startLoveDate가 null이어야 함 + assertThat(savedMember.getStartLoveDate()).isNull(); + } + } + + @Nested + @DisplayName("커플 연동 시 startLoveDate 당일 초기화 테스트") + class CoupleLinkTest { + + private String accessToken; + private MemberEntity member; + private MemberEntity partner; + + @BeforeEach + void setup() { + // 회원1 생성 (회원가입 완료) + member = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("testProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("테스트유저1") + .email("test1@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("TEST1234")) + .build(); + + // 회원2 (파트너) 생성 + partner = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("partnerProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("파트너") + .email("partner@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("PARTNER1")) + .build(); + + em.persist(member); + em.persist(partner); + em.flush(); + + TokenInfo tokenInfo = generateTokenPort.generateToken(member.getId(), member.getMemberRole()); + accessToken = tokenInfo.getAccessToken(); + } + + @Test + @DisplayName("커플 연동 시 startLoveDate는 당일로 초기화") + void coupleLinkInitializesStartLoveDateToToday() throws Exception { + // given + LocalDate today = LocalDate.now(); + Map requestDto = Map.of( + "coupleCode", "PARTNER1" + ); + + // when - 멤버가 파트너의 초대코드로 커플 연동 + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)); + + // then + em.flush(); + em.clear(); + + MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); + CoupleEntity couple = em.find(CoupleEntity.class, savedMember.getCoupleEntityId().getValue()); + + // 커플의 startLoveDate가 당일인지 확인 + assertThat(couple.getStartLoveDate()).isEqualTo(today); + assertThat(couple.getCoupleState()).isEqualTo(CoupleState.ALIVE); + } + } + + @Nested + @DisplayName("디데이 변경 API 테스트") + class UpdateStartLoveDateV2Test { + + private String accessToken; + private String nonCoupleAccessToken; + private MemberEntity member; + private MemberEntity partner; + private MemberEntity nonCoupleMember; + private CoupleEntity couple; + + @BeforeEach + void setup() { + // 커플 회원1 + member = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("testProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("테스트유저1") + .email("test1@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("TEST1234")) + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .build(); + + // 커플 회원2 (파트너) + partner = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("partnerProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("파트너") + .email("partner@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("PARTNER1")) + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .build(); + + // 커플이 아닌 회원 + nonCoupleMember = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("nonCoupleProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("솔로유저") + .email("solo@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("SOLO123")) + .build(); + + em.persist(member); + em.persist(partner); + em.persist(nonCoupleMember); + em.flush(); + + // 커플 생성 + couple = CoupleEntity.builder() + .startLoveDate(LocalDate.now()) + .coupleState(CoupleState.ALIVE) + .isStartLoveDateUpdated(false) + .firstMemberId(makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId.of(member.getId())) + .secondMemberId(makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId.of(partner.getId())) + .build(); + + em.persist(couple); + em.flush(); + + // 커플 연결 (새로운 Entity로 생성) + MemberEntity updatedMember = em.find(MemberEntity.class, member.getId()); + MemberEntity memberWithCouple = MemberEntity.builder() + .id(updatedMember.getId()) + .provider(updatedMember.getProvider()) + .providerId(updatedMember.getProviderId()) + .memberRole(updatedMember.getMemberRole()) + .memberState(updatedMember.getMemberState()) + .nickname(updatedMember.getNickname()) + .email(updatedMember.getEmail()) + .inviteCodeEntityValue(updatedMember.getInviteCodeEntityValue()) + .loveTypeCategory(updatedMember.getLoveTypeCategory()) + .coupleEntityId(CoupleEntityId.of(couple.getId())) + .build(); + em.merge(memberWithCouple); + + MemberEntity updatedPartner = em.find(MemberEntity.class, partner.getId()); + MemberEntity partnerWithCouple = MemberEntity.builder() + .id(updatedPartner.getId()) + .provider(updatedPartner.getProvider()) + .providerId(updatedPartner.getProviderId()) + .memberRole(updatedPartner.getMemberRole()) + .memberState(updatedPartner.getMemberState()) + .nickname(updatedPartner.getNickname()) + .email(updatedPartner.getEmail()) + .inviteCodeEntityValue(updatedPartner.getInviteCodeEntityValue()) + .loveTypeCategory(updatedPartner.getLoveTypeCategory()) + .coupleEntityId(CoupleEntityId.of(couple.getId())) + .build(); + em.merge(partnerWithCouple); + + em.flush(); + em.clear(); + + TokenInfo tokenInfo = generateTokenPort.generateToken(member.getId(), member.getMemberRole()); + accessToken = tokenInfo.getAccessToken(); + + TokenInfo nonCoupleToken = generateTokenPort.generateToken(nonCoupleMember.getId(), nonCoupleMember.getMemberRole()); + nonCoupleAccessToken = nonCoupleToken.getAccessToken(); + } + + @Test + @DisplayName("디데이 변경 - 커플은 연애 시작일 변경 가능") + void updateStartLoveDateV2ForCouple() throws Exception { + // given + LocalDate newStartDate = LocalDate.of(2024, 1, 1); + Map requestDto = Map.of( + "startLoveDate", newStartDate.toString() + ); + + // when + mockMvc.perform(patch("/members/start-love-date") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.startLoveDate").value(newStartDate.toString())); + + // then - 커플의 startLoveDate만 변경되었는지 확인 + em.flush(); + em.clear(); + + CoupleEntity updatedCouple = em.find(CoupleEntity.class, couple.getId()); + assertThat(updatedCouple.getStartLoveDate()).isEqualTo(newStartDate); + + // 개인의 startLoveDate는 null 유지 + MemberEntity updatedMember = em.find(MemberEntity.class, member.getId()); + assertThat(updatedMember.getStartLoveDate()).isNull(); + } + + @Test + @DisplayName("디데이 변경 - 커플이 아닌 사용자는 실패") + void updateStartLoveDateV2ForNonCoupleFails() throws Exception { + // given + LocalDate newStartDate = LocalDate.of(2024, 1, 1); + Map requestDto = Map.of( + "startLoveDate", newStartDate.toString() + ); + + // when & then - 커플이 아닌 사용자는 403 Forbidden + mockMvc.perform(patch("/members/start-love-date") + .header("Authorization", "Bearer " + nonCoupleAccessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto))) + .andExpect(status().isForbidden()); // @CheckCoupleMember가 403 반환 + } + } + + @Nested + @DisplayName("회원 정보 조회 시 커플의 startLoveDate 반환 테스트") + class GetMemberInfoTest { + + private String accessToken; + private String nonCoupleAccessToken; + private MemberEntity member; + private MemberEntity partner; + private MemberEntity nonCoupleMember; + private CoupleEntity couple; + + @BeforeEach + void setup() { + // 커플 회원1 + member = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("testProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("테스트유저1") + .email("test1@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("TEST1234")) + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .build(); + + // 커플 회원2 (파트너) + partner = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("partnerProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("파트너") + .email("partner@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("PARTNER1")) + .loveTypeCategory(LoveTypeCategory.STABLE_TYPE) + .build(); + + // 커플이 아닌 회원 + nonCoupleMember = MemberEntity.builder() + .provider(Provider.KAKAO) + .providerId("nonCoupleProviderId") + .memberRole(MemberRole.MEMBER) + .memberState(MemberState.ALIVE) + .nickname("솔로유저") + .email("solo@test.com") + .inviteCodeEntityValue(InviteCodeEntityValue.of("SOLO123")) + .build(); + + em.persist(member); + em.persist(partner); + em.persist(nonCoupleMember); + em.flush(); + + // 커플 생성 (특정 날짜로 설정) + LocalDate coupleStartDate = LocalDate.of(2024, 5, 1); + couple = CoupleEntity.builder() + .startLoveDate(coupleStartDate) + .coupleState(CoupleState.ALIVE) + .isStartLoveDateUpdated(false) + .firstMemberId(makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId.of(member.getId())) + .secondMemberId(makeus.cmc.malmo.adaptor.out.persistence.entity.value.MemberEntityId.of(partner.getId())) + .build(); + + em.persist(couple); + em.flush(); + + // 커플 연결 + MemberEntity updatedMember = em.find(MemberEntity.class, member.getId()); + MemberEntity memberWithCouple = MemberEntity.builder() + .id(updatedMember.getId()) + .provider(updatedMember.getProvider()) + .providerId(updatedMember.getProviderId()) + .memberRole(updatedMember.getMemberRole()) + .memberState(updatedMember.getMemberState()) + .nickname(updatedMember.getNickname()) + .email(updatedMember.getEmail()) + .inviteCodeEntityValue(updatedMember.getInviteCodeEntityValue()) + .loveTypeCategory(updatedMember.getLoveTypeCategory()) + .coupleEntityId(CoupleEntityId.of(couple.getId())) + .build(); + em.merge(memberWithCouple); + + em.flush(); + em.clear(); + + TokenInfo tokenInfo = generateTokenPort.generateToken(member.getId(), member.getMemberRole()); + accessToken = tokenInfo.getAccessToken(); + + TokenInfo nonCoupleToken = generateTokenPort.generateToken(nonCoupleMember.getId(), nonCoupleMember.getMemberRole()); + nonCoupleAccessToken = nonCoupleToken.getAccessToken(); + } + + @Test + @DisplayName("회원 정보 조회 - 커플의 startLoveDate 반환") + void getMemberInfoReturnsCoupleStartLoveDate() throws Exception { + // given + LocalDate coupleStartDate = LocalDate.of(2024, 5, 1); + + // when & then + mockMvc.perform(get("/members") + .header("Authorization", "Bearer " + accessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.startLoveDate").value(coupleStartDate.toString())); + } + + @Test + @DisplayName("회원 정보 조회 - 커플이 아닌 경우 startLoveDate 없음") + void getMemberInfoReturnsNullForNonCouple() throws Exception { + // when & then + mockMvc.perform(get("/members") + .header("Authorization", "Bearer " + nonCoupleAccessToken)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.data.startLoveDate").doesNotExist()); + } + } + + @Nested + @DisplayName("회원가입 기능 검증 (기존 정책)") class SignUpFeature { @Test @DisplayName("정상적인 요청의 경우 회원가입이 성공한다") @@ -112,15 +526,12 @@ class SignUpFeature { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임", - LocalDate.of(2023, 10, 1)) - ))) + "테스트닉네임")))) .andExpect(status().isOk()); MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); Assertions.assertThat(savedMember.getNickname()).isEqualTo("테스트닉네임"); - Assertions.assertThat(savedMember.getStartLoveDate()).isEqualTo(LocalDate.of(2023, 10, 1)); List agreements = em.createQuery( "SELECT t FROM MemberTermsAgreementEntity t WHERE t.memberEntityId.value = :memberId", @@ -151,9 +562,8 @@ class SignUpFeature { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임1234", // 10자 - LocalDate.of(2023, 10, 1)) - ))) + "테스트닉네임1234")) // 10자 + )) .andExpect(status().isOk()); } @@ -173,9 +583,8 @@ class SignUpFeature { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임12345", // 11자 - LocalDate.of(2023, 10, 1)) - ))) + "테스트닉네임12345")) // 11자 + )) .andExpect(status().isBadRequest()); } @@ -195,9 +604,8 @@ class SignUpFeature { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임!@#", // 특수문자 포함 - LocalDate.of(2023, 10, 1)) - ))) + "테스트닉네임!@#"))) // 특수문자 포함 + ) .andExpect(status().isBadRequest()); } @@ -212,15 +620,12 @@ class SignUpFeature { MemberRequestDtoFactory.createTermsDto(4L, true) ); - LocalDate today = LocalDate.now(); - mockMvc.perform(post("/members/onboarding") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임", - today) + "테스트닉네임") ))) .andExpect(status().isOk()); } @@ -229,6 +634,8 @@ class SignUpFeature { @Test @DisplayName("시작일이 미래 날짜인 경우 회원가입이 실패한다") void 회원가입_시작일_미래_실패() throws Exception { + // V2 정책에서는 startLoveDate를 회원가입 시 설정하지 않으므로 이 테스트는 더 이상 유효하지 않음 + // 대신 V2 정책에 맞는 테스트로 변경 List terms = List.of( MemberRequestDtoFactory.createTermsDto(1L, true), MemberRequestDtoFactory.createTermsDto(2L, true), @@ -236,17 +643,14 @@ class SignUpFeature { MemberRequestDtoFactory.createTermsDto(4L, true) ); - LocalDate futureDate = LocalDate.now().plusDays(1); - mockMvc.perform(post("/members/onboarding") .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpRequestDto(terms, - "테스트닉네임", - futureDate) + "테스트닉네임") ))) - .andExpect(status().isBadRequest()); + .andExpect(status().isOk()); // V2에서는 startLoveDate 없이 회원가입 성공 } @Test @@ -282,7 +686,6 @@ class SignUpFeature { .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpWithLoveTypeIdRequestDto(terms, "테스트닉네임", - today, Long.valueOf(loveTypeId)) ))) .andExpect(status().isOk()); @@ -290,7 +693,8 @@ class SignUpFeature { MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); Assertions.assertThat(savedMember.getNickname()).isEqualTo("테스트닉네임"); - Assertions.assertThat(savedMember.getStartLoveDate()).isEqualTo(today); + // V2 정책에서는 개인의 startLoveDate가 null이어야 함 + Assertions.assertThat(savedMember.getStartLoveDate()).isNull(); Assertions.assertThat(savedMember.getLoveTypeCategory()).isEqualTo(LoveTypeCategory.STABLE_TYPE); Assertions.assertThat(savedMember.getAnxietyRate()).isEqualTo(1.00f); Assertions.assertThat(savedMember.getAvoidanceRate()).isEqualTo(1.00f); @@ -317,7 +721,6 @@ class SignUpFeature { .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createSignUpWithLoveTypeIdRequestDto(terms, "테스트닉네임", - today, loveTypeId) ))) .andExpect(status().isOk()); @@ -325,7 +728,8 @@ class SignUpFeature { MemberEntity savedMember = em.find(MemberEntity.class, member.getId()); Assertions.assertThat(savedMember.getNickname()).isEqualTo("테스트닉네임"); - Assertions.assertThat(savedMember.getStartLoveDate()).isEqualTo(today); + // V2 정책에서는 개인의 startLoveDate가 null이어야 함 + Assertions.assertThat(savedMember.getStartLoveDate()).isNull(); Assertions.assertThat(savedMember.getLoveTypeCategory()).isNull(); Assertions.assertThat(savedMember.getAnxietyRate()).isEqualTo(0.0f); Assertions.assertThat(savedMember.getAvoidanceRate()).isEqualTo(0.0f); @@ -392,7 +796,7 @@ class MemberDeleteFeature { } @Nested - @DisplayName("멤버 정보 조회 검증") + @DisplayName("멤버 정보 조회 검증 (기존 정책)") class MemberInfoFeature { @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY) @@ -427,12 +831,21 @@ public static class PartnerResponseDto { private float avoidanceRate; private float anxietyRate; private String nickname; + private Boolean isStartLoveDateUpdated; } void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, LocalDate startLoveDate, int coupleQuestionCount, int totalChatRoomCount) { Assertions.assertThat(memberResponse.memberState).isEqualTo(member.getMemberState()); Assertions.assertThat(memberResponse.provider).isEqualTo(member.getProvider()); - Assertions.assertThat(memberResponse.startLoveDate).isEqualTo(startLoveDate); + // V2 정책: 실제 구현에서는 coupleEntity.startLoveDate.coalesce(memberEntity.startLoveDate)를 사용 + // 커플인 경우: 커플의 startLoveDate 사용, 솔로인 경우: null + if (member.getCoupleEntityId() != null) { + // 커플인 경우: 커플의 startLoveDate 사용 + Assertions.assertThat(memberResponse.startLoveDate).isEqualTo(startLoveDate); + } else { + // 솔로인 경우: startLoveDate 없음 (V2 정책) + Assertions.assertThat(memberResponse.startLoveDate).isNull(); + } Assertions.assertThat(memberResponse.loveTypeCategory).isEqualTo(member.getLoveTypeCategory()); Assertions.assertThat(memberResponse.totalCoupleQuestionCount).isEqualTo(coupleQuestionCount); Assertions.assertThat(memberResponse.totalChatRoomCount).isEqualTo(totalChatRoomCount); @@ -459,8 +872,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc new TypeReference<>() {} ); - // 멤버 정보가 정상적으로 조회되었는지 검증 - assertMemberInfo(responseDto.data, member, member.getStartLoveDate(), 0, 0); + // 멤버 정보가 정상적으로 조회되었는지 검증 (V2 정책: 솔로는 startLoveDate 없음) + assertMemberInfo(responseDto.data, member, null, 0, 0); } @Test @@ -491,8 +904,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc new TypeReference<>() {} ); - // 커플인 경우 커플의 연애 시작 날짜(초대코드 주인의 날짜)로 조회 - assertMemberInfo(responseDto.data, member, partner.getStartLoveDate(), 0, 0); + // V2 정책: 커플 연동 시 startLoveDate를 당일로 초기화 + assertMemberInfo(responseDto.data, member, LocalDate.now(), 0, 0); } @Test @@ -542,7 +955,8 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc new TypeReference<>() {} ); - assertMemberInfo(responseDto.data, member, partner.getStartLoveDate(), 3, 3); + // V2 정책: 커플 연동 시 startLoveDate를 당일로 초기화 + assertMemberInfo(responseDto.data, member, LocalDate.now(), 3, 3); } @@ -599,6 +1013,52 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc Assertions.assertThat(partnerDto.avoidanceRate).isEqualTo(partner.getAvoidanceRate()); Assertions.assertThat(partnerDto.anxietyRate).isEqualTo(partner.getAnxietyRate()); Assertions.assertThat(partnerDto.nickname).isEqualTo(partner.getNickname()); + // 새로 생성된 커플이므로 isStartLoveDateUpdated는 false여야 함 + Assertions.assertThat(partnerDto.isStartLoveDateUpdated).isFalse(); + } + + @Test + @DisplayName("디데이 변경 후 파트너 정보 조회 시 isStartLoveDateUpdated가 true인지 확인") + void 디데이_변경_후_파트너_정보_조회_시_isStartLoveDateUpdated_확인() throws Exception { + // given + MemberEntity partner = createAndSavePartner(); + + // 커플 연결 + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()); + + // 디데이 변경 + LocalDate newDday = LocalDate.of(2025, 1, 1); + mockMvc.perform(patch("/members/start-love-date") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + MemberRequestDtoFactory.createUpdateStartLoveDateRequestDto(newDday) + ))) + .andExpect(status().isOk()); + + // when + MvcResult mvcResult = mockMvc.perform(get("/members/partner") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andReturn(); + + // then + String responseContent = mvcResult.getResponse().getContentAsString(); + ResponseDto responseDto = objectMapper.readValue( + responseContent, + new TypeReference<>() {} + ); + + // 디데이 변경 후 isStartLoveDateUpdated가 true인지 확인 + PartnerResponseDto partnerDto = responseDto.data; + Assertions.assertThat(partnerDto.isStartLoveDateUpdated).isTrue(); } @Test @@ -679,7 +1139,7 @@ void assertMemberInfo(MemberResponseDto memberResponse, MemberEntity member, Loc } @Nested - @DisplayName("멤버 정보 수정 검증") + @DisplayName("멤버 정보 수정 검증 (기존 정책)") class MemberInfoUpdateFeature { @Test @DisplayName("멤버 정보 수정 성공") @@ -768,8 +1228,18 @@ class MemberInfoUpdateFeature { @Test @DisplayName("디데이 수정 성공") void 디데이_수정_성공() throws Exception { - // given - LocalDate newDday = LocalDate.of(2024, 1, 1); + // V2 정책에서는 솔로 사용자는 디데이 수정이 불가능하므로 이 테스트는 커플 사용자로 변경 + // given - 커플 사용자 생성 + MemberEntity partner = createAndSavePartner(); + + // 커플 연동 + mockMvc.perform(post("/couples") + .header("Authorization", "Bearer " + accessToken) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString( + CoupleRequestDtoFactory.createCoupleLinkRequestDto(partner.getInviteCodeEntityValue().getValue()) + ))) + .andExpect(status().isOk()); // when mockMvc.perform(patch("/members/start-love-date") @@ -780,14 +1250,15 @@ class MemberInfoUpdateFeature { ))) .andExpect(status().isOk()); - // then + // then - V2 정책에서는 개인의 startLoveDate는 수정되지 않고, 커플의 startLoveDate만 수정됨 em.flush(); em.clear(); MemberEntity savedMember = em.createQuery("SELECT m FROM MemberEntity m WHERE m.email = :email", MemberEntity.class) .setParameter("email", member.getEmail()) .getSingleResult(); - Assertions.assertThat(savedMember.getStartLoveDate()).isEqualTo(newDday); + // V2 정책에서는 개인의 startLoveDate는 null이어야 함 + Assertions.assertThat(savedMember.getStartLoveDate()).isNull(); } @Test @@ -807,7 +1278,6 @@ class MemberInfoUpdateFeature { String responseContent = mvcResult.getResponse().getContentAsString(); Integer coupleId = JsonPath.read(responseContent, "$.data.coupleId"); - LocalDate newDday = LocalDate.of(2025, 1, 1); // when mockMvc.perform(patch("/members/start-love-date") .header("Authorization", "Bearer " + accessToken) @@ -817,13 +1287,14 @@ class MemberInfoUpdateFeature { ))) .andExpect(status().isOk()); - // then + // then - V2 정책에서는 개인의 startLoveDate는 수정되지 않고, 커플의 startLoveDate만 수정됨 MemberEntity savedMember = em.createQuery("SELECT m FROM MemberEntity m WHERE m.email = :email", MemberEntity.class) .setParameter("email", member.getEmail()) .getSingleResult(); - Assertions.assertThat(savedMember.getStartLoveDate()).isEqualTo(newDday); + // V2 정책에서는 개인의 startLoveDate는 null이어야 함 + Assertions.assertThat(savedMember.getStartLoveDate()).isNull(); - // 커플의 디데이도 함께 수정되어야 함 + // 커플의 디데이만 수정되어야 함 CoupleEntity couple = em.createQuery("SELECT c FROM CoupleEntity c WHERE c.id = :coupleId", CoupleEntity.class) .setParameter("coupleId", Long.valueOf(coupleId)) .getSingleResult(); @@ -838,7 +1309,6 @@ class MemberInfoUpdateFeature { .header("Authorization", "Bearer " + accessToken) .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); - LocalDate newDday = LocalDate.of(2024, 1, 1); // when & then mockMvc.perform(patch("/members/start-love-date") @@ -847,9 +1317,9 @@ class MemberInfoUpdateFeature { .content(objectMapper.writeValueAsString( MemberRequestDtoFactory.createUpdateStartLoveDateRequestDto(newDday) ))) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("message").value(NO_SUCH_MEMBER.getMessage())) - .andExpect(jsonPath("code").value(NO_SUCH_MEMBER.getCode())); + .andExpect(status().isForbidden()) + .andExpect(jsonPath("message").value(ErrorCode.NOT_COUPLE_MEMBER.getMessage())) + .andExpect(jsonPath("code").value(ErrorCode.NOT_COUPLE_MEMBER.getCode())); } @Test @@ -1051,7 +1521,6 @@ private MemberEntity createAndSavePartner() { .loveTypeCategory(LoveTypeCategory.ANXIETY_TYPE) .anxietyRate(2.77f) .avoidanceRate(1.66f) - .startLoveDate(LocalDate.of(2024, 1, 1)) .email("testEmail2@test.com") .inviteCodeEntityValue(InviteCodeEntityValue.of("invite2")) .build(); diff --git a/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/MemberRequestDtoFactory.java b/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/MemberRequestDtoFactory.java index 9c212730..065e1436 100644 --- a/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/MemberRequestDtoFactory.java +++ b/src/test/java/makeus/cmc/malmo/integration_test/dto_factory/MemberRequestDtoFactory.java @@ -9,12 +9,10 @@ public class MemberRequestDtoFactory { public static SignUpController.SignUpRequestDto createSignUpRequestDto(List terms, - String nickname, - LocalDate startLoveDate) { + String nickname) { SignUpController.SignUpRequestDto dto = new SignUpController.SignUpRequestDto(); dto.setTerms(terms); dto.setNickname(nickname); - dto.setLoveStartDate(startLoveDate); return dto; } @@ -22,12 +20,10 @@ public static SignUpController.SignUpRequestDto createSignUpRequestDto(List terms, String nickname, - LocalDate startLoveDate, Long loveTypeId) { SignUpController.SignUpRequestDto dto = new SignUpController.SignUpRequestDto(); dto.setTerms(terms); dto.setNickname(nickname); - dto.setLoveStartDate(startLoveDate); dto.setLoveTypeId(loveTypeId); return dto; diff --git a/src/test/java/makeus/cmc/malmo/mapper/ChatMessageMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/ChatMessageMapperTest.java index 05480527..a56d6212 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/ChatMessageMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/ChatMessageMapperTest.java @@ -32,6 +32,7 @@ void toDomain() { .id(1L) .chatRoomEntityId(ChatRoomEntityId.of(100L)) .level(1) + .detailedLevel(2) .content("test content") .senderType(SenderType.USER) .createdAt(now) @@ -46,6 +47,7 @@ void toDomain() { assertThat(domain.getId()).isEqualTo(entity.getId()); assertThat(domain.getChatRoomId().getValue()).isEqualTo(entity.getChatRoomEntityId().getValue()); assertThat(domain.getLevel()).isEqualTo(entity.getLevel()); + assertThat(domain.getDetailedLevel()).isEqualTo(entity.getDetailedLevel()); assertThat(domain.getContent()).isEqualTo(entity.getContent()); assertThat(domain.getSenderType()).isEqualTo(entity.getSenderType()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); @@ -62,6 +64,7 @@ void toEntity() { 1L, ChatRoomId.of(100L), 1, + 2, "test content", SenderType.USER, now, @@ -76,6 +79,7 @@ void toEntity() { assertThat(entity.getId()).isEqualTo(domain.getId()); assertThat(entity.getChatRoomEntityId().getValue()).isEqualTo(domain.getChatRoomId().getValue()); assertThat(entity.getLevel()).isEqualTo(domain.getLevel()); + assertThat(entity.getDetailedLevel()).isEqualTo(domain.getDetailedLevel()); assertThat(entity.getContent()).isEqualTo(domain.getContent()); assertThat(entity.getSenderType()).isEqualTo(domain.getSenderType()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); diff --git a/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java index cdf91742..1352d0cd 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/ChatRoomMapperTest.java @@ -33,10 +33,13 @@ void toDomain() { .memberEntityId(MemberEntityId.of(100L)) .chatRoomState(ChatRoomState.ALIVE) .level(1) + .detailedLevel(2) .lastMessageSentTime(now) .totalSummary("total summary") .situationKeyword("situation") .solutionKeyword("solution") + .chatRoomCompletedReason(null) + .counselingType("상담유형") .createdAt(now) .modifiedAt(now) .deletedAt(null) @@ -50,10 +53,13 @@ void toDomain() { assertThat(domain.getMemberId().getValue()).isEqualTo(entity.getMemberEntityId().getValue()); assertThat(domain.getChatRoomState()).isEqualTo(entity.getChatRoomState()); assertThat(domain.getLevel()).isEqualTo(entity.getLevel()); + assertThat(domain.getDetailedLevel()).isEqualTo(entity.getDetailedLevel()); assertThat(domain.getLastMessageSentTime()).isEqualTo(entity.getLastMessageSentTime()); assertThat(domain.getTotalSummary()).isEqualTo(entity.getTotalSummary()); assertThat(domain.getSituationKeyword()).isEqualTo(entity.getSituationKeyword()); assertThat(domain.getSolutionKeyword()).isEqualTo(entity.getSolutionKeyword()); + assertThat(domain.getChatRoomCompletedReason()).isEqualTo(entity.getChatRoomCompletedReason()); + assertThat(domain.getCounselingType()).isEqualTo(entity.getCounselingType()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); assertThat(domain.getModifiedAt()).isEqualTo(entity.getModifiedAt()); assertThat(domain.getDeletedAt()).isEqualTo(entity.getDeletedAt()); @@ -69,10 +75,13 @@ void toEntity() { MemberId.of(100L), ChatRoomState.ALIVE, 1, + 2, now, "total summary", "situation", "solution", + null, + "상담유형", now, now, null @@ -86,10 +95,13 @@ void toEntity() { assertThat(entity.getMemberEntityId().getValue()).isEqualTo(domain.getMemberId().getValue()); assertThat(entity.getChatRoomState()).isEqualTo(domain.getChatRoomState()); assertThat(entity.getLevel()).isEqualTo(domain.getLevel()); + assertThat(entity.getDetailedLevel()).isEqualTo(domain.getDetailedLevel()); assertThat(entity.getLastMessageSentTime()).isEqualTo(domain.getLastMessageSentTime()); assertThat(entity.getTotalSummary()).isEqualTo(domain.getTotalSummary()); assertThat(entity.getSituationKeyword()).isEqualTo(domain.getSituationKeyword()); assertThat(entity.getSolutionKeyword()).isEqualTo(domain.getSolutionKeyword()); + assertThat(entity.getChatRoomCompletedReason()).isEqualTo(domain.getChatRoomCompletedReason()); + assertThat(entity.getCounselingType()).isEqualTo(domain.getCounselingType()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); assertThat(entity.getModifiedAt()).isEqualTo(domain.getModifiedAt()); assertThat(entity.getDeletedAt()).isEqualTo(domain.getDeletedAt()); diff --git a/src/test/java/makeus/cmc/malmo/mapper/CoupleAggregateMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/CoupleAggregateMapperTest.java index b10d4d16..9d85b1ad 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/CoupleAggregateMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/CoupleAggregateMapperTest.java @@ -46,6 +46,7 @@ void givenCompleteEntity_whenToDomain_thenReturnsCompleteCouple() { assertThat(result.getId()).isEqualTo(entity.getId()); assertThat(result.getStartLoveDate()).isEqualTo(entity.getStartLoveDate()); assertThat(result.getCoupleState()).isEqualTo(entity.getCoupleState()); + assertThat(result.getIsStartLoveDateUpdated()).isEqualTo(entity.getIsStartLoveDateUpdated()); assertThat(result.getFirstMemberId().getValue()).isEqualTo(entity.getFirstMemberId().getValue()); assertThat(result.getSecondMemberId().getValue()).isEqualTo(entity.getSecondMemberId().getValue()); assertThat(result.getCreatedAt()).isEqualTo(entity.getCreatedAt()); @@ -98,6 +99,7 @@ void givenCompleteCouple_whenToEntity_thenReturnsCompleteEntity() { assertThat(result.getId()).isEqualTo(domain.getId()); assertThat(result.getStartLoveDate()).isEqualTo(domain.getStartLoveDate()); assertThat(result.getCoupleState()).isEqualTo(domain.getCoupleState()); + assertThat(result.getIsStartLoveDateUpdated()).isEqualTo(domain.getIsStartLoveDateUpdated()); assertThat(result.getFirstMemberId().getValue()).isEqualTo(domain.getFirstMemberId().getValue()); assertThat(result.getSecondMemberId().getValue()).isEqualTo(domain.getSecondMemberId().getValue()); assertThat(result.getCreatedAt()).isEqualTo(domain.getCreatedAt()); @@ -138,6 +140,7 @@ private CoupleEntity createCompleteEntity() { .id(1L) .startLoveDate(LocalDate.of(2024, 1, 1)) .coupleState(CoupleState.ALIVE) + .isStartLoveDateUpdated(false) .firstMemberId(MemberEntityId.of(100L)) .secondMemberId(MemberEntityId.of(200L)) .firstMemberSnapshot(createSnapshotEntity("first", LoveTypeCategory.STABLE_TYPE, 10, 20)) @@ -169,7 +172,8 @@ private Couple createCompleteCouple() { createSnapshot("second", LoveTypeCategory.ANXIETY_TYPE, 40f, 30f), LocalDateTime.now(), LocalDateTime.now(), - null + null, + false ); } diff --git a/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java b/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java index 2fa5bdc7..30bd0755 100644 --- a/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java +++ b/src/test/java/makeus/cmc/malmo/mapper/PromptMapperTest.java @@ -29,6 +29,12 @@ void toDomain() { .id(1L) .level(1) .content("prompt content") + .isForSystem(true) + .isForSummary(false) + .isForCompletedResponse(false) + .isForTotalSummary(false) + .isForGuideline(false) + .isForAnswerMetadata(false) .createdAt(now) .modifiedAt(now) .deletedAt(null) @@ -41,6 +47,12 @@ void toDomain() { assertThat(domain.getId()).isEqualTo(entity.getId()); assertThat(domain.getLevel()).isEqualTo(entity.getLevel()); assertThat(domain.getContent()).isEqualTo(entity.getContent()); + assertThat(domain.isForSystem()).isEqualTo(entity.isForSystem()); + assertThat(domain.isForSummary()).isEqualTo(entity.isForSummary()); + assertThat(domain.isForCompletedResponse()).isEqualTo(entity.isForCompletedResponse()); + assertThat(domain.isForTotalSummary()).isEqualTo(entity.isForTotalSummary()); + assertThat(domain.isForGuideline()).isEqualTo(entity.isForGuideline()); + assertThat(domain.isForAnswerMetadata()).isEqualTo(entity.isForAnswerMetadata()); assertThat(domain.getCreatedAt()).isEqualTo(entity.getCreatedAt()); assertThat(domain.getModifiedAt()).isEqualTo(entity.getModifiedAt()); assertThat(domain.getDeletedAt()).isEqualTo(entity.getDeletedAt()); @@ -55,6 +67,12 @@ void toEntity() { 1L, 1, "prompt content", + true, + false, + false, + false, + false, + false, now, now, null @@ -67,6 +85,12 @@ void toEntity() { assertThat(entity.getId()).isEqualTo(domain.getId()); assertThat(entity.getLevel()).isEqualTo(domain.getLevel()); assertThat(entity.getContent()).isEqualTo(domain.getContent()); + assertThat(entity.isForSystem()).isEqualTo(domain.isForSystem()); + assertThat(entity.isForSummary()).isEqualTo(domain.isForSummary()); + assertThat(entity.isForCompletedResponse()).isEqualTo(domain.isForCompletedResponse()); + assertThat(entity.isForTotalSummary()).isEqualTo(domain.isForTotalSummary()); + assertThat(entity.isForGuideline()).isEqualTo(domain.isForGuideline()); + assertThat(entity.isForAnswerMetadata()).isEqualTo(domain.isForAnswerMetadata()); assertThat(entity.getCreatedAt()).isEqualTo(domain.getCreatedAt()); assertThat(entity.getModifiedAt()).isEqualTo(domain.getModifiedAt()); assertThat(entity.getDeletedAt()).isEqualTo(domain.getDeletedAt());